Repository navigation
A reopened issue re-enters intake as a new generation (US-47.10) - #1010
Merged
Merged
Conversation
chg closes a chg-loop issue only after its fingerprint stays silent, and reopens it when the problem recurs. loopctl ignored that reopen once the issue's story had advanced. An issue now has one intake record per generation: a reopen whose newest record's loop has ended (a closure row, or a story at merged, deployed, verified or done) starts generation N+1, which triage picks up like a new issue. Earlier generations, their story and closure stay untouched as the first fix's provenance. The issue closer removes another generation's loopctl:resolution-* label before adding its own (new PullRequestSource.remove_label/3), and counts a closed issue carrying its label as its own close only when this closure recorded labelled_at. Part of #986 (item 3).
Owner
Author
|
Review round 1 (code) counted at |
…eded closures - newest_locked/2 re-reads for a newer generation after taking the lock and moves to it, so a delivery queued behind a bump lands on the new generation; the reopen-only lost-race branch is gone with it. - The issue closer abandons a closure whose record is no longer the issue's newest generation (superseded_by_generation) before any forge call, so generation 1's late closure never closes the recurrence. - An inherited escalation writes its own intake_escalated audit entry. - The race test covers an edit racing the bump; write/4 serves DELETE.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
US-47.10, part of #986 (item 3). chg closes a
chg-loopissue only after the failure stops recurring, and reopens it if the failure comes back. Until now loopctl ignored that reopen once the issue's story had advanced: the triage drain skips any record with a stage row, and only one story or closure may exist per record.Generations
intake_records.generationis a new column, NOT NULL, default 1, with a CHECK that it is ≥ 1. The unique key becomes(tenant_id, source_id, issue_number, generation). Migration20261019180000;downrefuses while any issue has a generation 2.reopeneddelivery starts generation N+1 when the newest record's loop has ended: a closure row exists, or its story has reachedmerged,deployed,verifiedordone(Intake.generation_end_stages/0). The new record waits atpending_triage, and the drain picks it up like any new issue.escalatedandfailedwithout a closure are not "ended": a human holds the first, and the second shipped nothing.order_ambiguous.ON CONFLICT DO NOTHING, so only one bump happens. That race is tested in a sync module, since its subject is two committed transactions, and it is registered in the hygiene test.Issue closer
PullRequestSource.remove_label/3sendsDELETE …/labels/{name}, with the name encoded as one path segment. A 404 counts as success, and errors are classified the way every other write is.loopctl:resolution-*label, so a generation-2 close ends with exactly one. It removes first and adds second: a crash in between leaves no label, which the retry fixes, never two.labelled_at. Otherwise it isabandoned: closed_by_other, which covers a generation-1 label left on an issue that someone else then closed.labelled_at, so our own crash window still reads as ours.labelled_atin its fixture. That is the only state the real crash window can be in.Deploy note: deliveries can be dropped during the rollout (CHANGELOG)
The migration swaps the unique index. Until each old machine is replaced, its intake insert names a conflict target that no longer exists, so it answers
issuesdeliveries with 500, and GitHub does not retry them. loopctl rolls across 2 machines, so the window is about a minute. After the deploy, I redeliver every non-2xxissuesdelivery on each enrolled repo's hook (POST /repos/{o}/{r}/hooks/{id}/deliveries/{delivery_id}/attempts). The CHANGELOG names this step. An expand/contract split would avoid the gap, but generations would not work until the second release; I chose the single release.Tests
intake_generation_test.exscovers:editeddelivery start nothing;intake_generation_concurrency_test.exscovers two concurrent reopens.issue_closer_test.exs, US-47.10 block:github_pull_request_source_test.exscoversremove_label/3.Defaults Mark may want to overrule
merged,deployed,verified,done, or a closure row. Set inIntake.generation_end_stages/0.Intake's generation bump.closed_by_other, notclosed. Nothing is written to GitHub either way.Review round (one, /lean-review high): all 6 findings fixed
intake.exnewest_locked/2chose the newest generation from its snapshot, before it waited on the lock. A delivery queued behind a bump therefore landed on generation N. It now re-reads after taking the lock and moves to the newer generation. With that in place the reopen-only:lost_racebranch could never run, so it is deleted.superseded_by_generationbefore any forge call (IssueClosures.superseded?/1).intake_escalatedfor the new record, withinherited_from_generation.loop_ended?/1.delete/2duplicatedwrite/4. It is now a one-line call towrite/4, which takes an optional body.Remaining window, by design: a reopen whose webhook commits after the closer's supersede check but before its close call. The reopen's own webhook latency already opens a window that no check here can close, so the closer does not re-check before
close_issue.