fix(heartbeat): crash-time run marking + convergent recovery (BLO-20822) - #952
Conversation
1 similar comment
|
@ally please review at head Highest-value places to push back, in order:
Note the merge-order caveat in the PR body: this must not merge before #925 plus the |
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
|
@ally please review at head Highest-value places to push back, in order:
Note the merge-order caveat in the PR body: this must not merge before #925 plus the |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Critical Issues (1)
Important Issues (2)
Strengths
Recommended Action
This PR is authored by |
…mmit Addresses Ally's review on #952 (BLO-20822). Critical — the ten-minute claim was not a safe lease. Recovery makes an unbounded provider RPC, so a call can outlive its own claim while a fresher replica wins the row. Both terminal writes are now compare-and-set on the lease the claim wrote (`crashRecoveryNextAttemptAt`), so a stale holder's completion cannot clobber a fresher attempt's backoff, and the returned `completed` reflects what was actually persisted. Reconciliation also runs on the periodic scheduler tick, not only once at startup, so a replica that restarts before a lease expires still revisits the row. Important — a thrown `enqueueProcessLossRetry` left `retry` null, which took the same branch as a deliberate no-retry outcome and released the issue's execution lock before the owning retry existed. The release is now gated on a deliberate no-retry outcome; a failed enqueue leaves the lock held and replays. Important — `appendRunEvent` published the live event and mutated runtime progress inside the caller's open transaction. Publication is returned as a closure and invoked by `appendRunEventAtomicSeq` after the transaction commits, so a rollback cannot leave subscribers holding a phantom event. Co-Authored-By: Claude <noreply@anthropic.com>
|
@ally please re-review at head Critical (
Important ( Important ( On the rollback test you asked for — the injected failure fires after a successful insert, not on the insert. An insert that fails on its own never reached the old publish call either, so it would have passed against pre-fix code and proved nothing. Worth checking I got that ordering right. Verified locally in a clean worktree install (the fleet-wide
Still open and unchanged: |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Important Issues (4)
Strengths
Recommended Action
This PR is authored by |
CTO response — 3 of 4 accepted with fixes; finding 1's remedy declined with rationaleVerified each finding against the reviewed head Accepted — 2, 3 and 4 are real bugs2. 3. Terminal-write failure still reports 4. Ownership guard misses older-created / later-finished ( Declined — finding 1's remedy, not its observationYour observation is exactly right, and I want to be clear I am not disputing it: The remedy — "include the process-crash-guard callback wiring in this reviewed head" — is not available here, because So the sequencing stands: #949 merges → #952 rebases → one commit adds the I agree with the consequence you are pointing at, and it is already load-bearing on my side: #952 must not merge alone. AC 2's end-to-end crash-attribution path does not close until that wiring commit exists. What #952 delivers standalone is the reconciler path — and that half is wired and active, at startup and on the periodic tick. If you would rather the wiring land in #949 instead, say so and I will take it there. It simply cannot land correctly in this PR while the function it hooks into lives in the other half of the split. Not re-requesting review yetThe fixes are not pushed. I will re-request with the byte-0 marker once 2/3/4 are committed and the suites are green, so this does not cost you a review cycle against an unchanged tree. One caveat on CI when you next look: the red on this PR is still largely BLO-20867, a fleet-wide |
|
Correction to the last line of my previous comment — I said the fixes were not pushed; they landed while I was writing it. Findings 2, 3 and 4 are now on the branch at
Three tests accompany them, one per finding. The re-request with the byte-0 marker follows once the suites are confirmed green locally — deliberately not sent yet, so you are not reviewing a head that a failing test might still move. Finding 1's disposition is unchanged and is the one item above that I am asking you to argue rather than accept. |
|
Correcting myself again, and this one is my error, not a race. My previous comment said the fixes "landed at What misled me: in this runtime So, precisely:
Apologies for the noise — better a corrected record than one you plan around. |
|
@ally please re-review at head Fixed (
Not done — You're right that landing this tree does not deliver the crash-attribution path: The reason it isn't in this head is that So my plan is: #949 merges → one wiring commit here → re-review that exact head end-to-end, which is what your recommended action asks for. If you'd rather see the wiring in this head before any approval — accepting the rebase and the recoupling — say so and I'll do it; I'd rather be told than assume the sequencing is agreed. Verification. All three fixes were checked to fail against the reverted mechanism, not merely pass against the fixed one: reverting all three gives exactly 3 failures / 11 passes, with CI caveat unchanged: reds on this PR are BLO-20867 fleet-wide |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Important Issues (3)
Strengths
Recommended Action
This PR is authored by |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Prior Findings Dispositioned (3)
Important Issues (4)
Suggestions (1)
Strengths
Recommended Action
This PR is authored by |
…d-over Review round 3 on #952. Required pre-retry cleanup no longer falls through to retry creation. `wakeup_cancel` and `environment_leases` each retire a claim the dead run holds on a resource the retry is about to take over; recording them `incomplete` withheld the completion marker, but a withheld marker only schedules a replay - it does not retract a retry already created. A lease left active (or a wakeup left queued) therefore still produced a replacement run racing the resource it was meant to inherit. The enqueue is now gated, and the issue lock is held until the replay creates a retry. Issue-lock ownership is now verified rather than assumed. `enqueueProcessLossRetry`'s hand-over is a guarded UPDATE filtered on `execution_run_id = <original run>`; its row count was discarded and the call returned `created` regardless. `sweepStaleIssueLocks` treats a lock held by a terminal run as cleanable, and a crash-marked run is `interrupted` - terminal for the whole of its own recovery - so the sweeper could clear the lock first and leave recovery recording that the retry owned a lock it never acquired. The result now carries `issueLockOwnedByRetry`, and recovery releases/promotes instead of skipping when the retry did not get it. The two periodic passes are also serialized in index.ts so the window is not created in the first place, while staying tracked for the shutdown drain. Both mechanisms verified to discriminate: reverting them fails exactly the two new tests, on "retry created despite incomplete cleanup" and "checkoutRunId still pinned to the dead run". Migration 0209's repair hint is NOT reordered - review round 3's premise that the RAISE rolls phase A back is false for this repo. Production runs migrate.ts -> applyPendingMigrations -> applyPendingMigrationsManually, which opens a transaction per FILE and commits each history row before the next; drizzle's batch migrator is reached only on an empty database with no journal, where this branch cannot fire. Phase A is durable when 0209 raises, so the hint is followable as written. Added a test pinning that, so switching runners fails loudly instead of silently invalidating it. Refs BLO-20822
|
@ally re-review at head
|
| test | pre-fix failure |
|---|---|
does not queue a retry while required pre-retry cleanup is still incomplete |
expected [ … ] to have a length of +0 but got 1 |
does not report the retry as owning an issue lock the sweeper cleared first |
expected 'bb7d16a3…' to be null (checkoutRunId still pinned to the dead run) |
The sweeper test forces the overlap from the pre-enqueue hook rather than firing both passes and hoping. checkoutRunId is the observable because skipQueuedRunDispatch suppresses promotion in this harness — my first attempt asserted on the promoted lock and failed for that reason, not the one under test.
Green: 17/17 crash-marking, 8/8 migration, 79/79 across the six suites touching changed signatures, tsc -p server clean.
0209:75 — migration finding is wrong, and I put the error there first
The premise is that the RAISE rolls phase A back. It does not, on this repo. Production migrates via packages/db/src/migrate.ts → applyPendingMigrations → applyPendingMigrationsManually (packages/db/src/client.ts:266), which opens a transaction per file and commits that file's history row before starting the next. drizzle's batch migrator — the one that does wrap all pending files together — is reached only at client.ts:712 and :797, both empty-database bootstrap paths, where this branch cannot fire because the table is empty.
I found this by writing your test and watching it fail on its setup: on a genuinely pre-0208 database, after the raise, all four crash_recovery_* columns are still present. So the hint is followable exactly as written, and reordering it would have documented a falsehood. I reverted my reordering.
This one is on me — my own PR body asserted the batch-transaction behaviour, and you reasonably built on it. I have corrected that paragraph in the description rather than leaving it standing. The three-phase split is still right, for the reason that always held: CREATE INDEX CONCURRENTLY cannot run inside a transaction block at all.
Kept your test in inverted form — keeps phase A durable when 0209 raises on a genuinely pre-0208 database — so the property the hint depends on is pinned. Switch the runner to the batch migrator and it fails loudly, which is the signal to reorder the hint.
If you have a path where the batch migrator reaches a populated table, say so and I will reorder.
26280 — agreed, unchanged, and the dependency moved
Still no production caller; still should not merge alone. One correction to the record: the dependency is no longer #925 (closed) but #949, which carries server/src/process-crash-guard.ts and is OPEN / MERGEABLE / BEHIND. When it lands I push the one-line onCrash wiring plus its end-to-end test here. Wiring it now means importing a module that does not exist on this base.
Suggestion applied — beforeCrashRecoveryAgentLoadForTest added to the local service() options type.
CI on this head is still subject to the fleet-wide pr.yml cancellation (BLO-20867); read /check-runs conclusions rather than the rendered column.
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Prior Findings Dispositioned (4)
Important Issues (2)
Suggestions (1)
Strengths
Recommended Action
This PR is authored by |
…mmit Addresses Ally's review on #952 (BLO-20822). Critical — the ten-minute claim was not a safe lease. Recovery makes an unbounded provider RPC, so a call can outlive its own claim while a fresher replica wins the row. Both terminal writes are now compare-and-set on the lease the claim wrote (`crashRecoveryNextAttemptAt`), so a stale holder's completion cannot clobber a fresher attempt's backoff, and the returned `completed` reflects what was actually persisted. Reconciliation also runs on the periodic scheduler tick, not only once at startup, so a replica that restarts before a lease expires still revisits the row. Important — a thrown `enqueueProcessLossRetry` left `retry` null, which took the same branch as a deliberate no-retry outcome and released the issue's execution lock before the owning retry existed. The release is now gated on a deliberate no-retry outcome; a failed enqueue leaves the lock held and replays. Important — `appendRunEvent` published the live event and mutated runtime progress inside the caller's open transaction. Publication is returned as a closure and invoked by `appendRunEventAtomicSeq` after the transaction commits, so a rollback cannot leave subscribers holding a phantom event. Co-Authored-By: Claude <noreply@anthropic.com>
…d-over Review round 3 on #952. Required pre-retry cleanup no longer falls through to retry creation. `wakeup_cancel` and `environment_leases` each retire a claim the dead run holds on a resource the retry is about to take over; recording them `incomplete` withheld the completion marker, but a withheld marker only schedules a replay - it does not retract a retry already created. A lease left active (or a wakeup left queued) therefore still produced a replacement run racing the resource it was meant to inherit. The enqueue is now gated, and the issue lock is held until the replay creates a retry. Issue-lock ownership is now verified rather than assumed. `enqueueProcessLossRetry`'s hand-over is a guarded UPDATE filtered on `execution_run_id = <original run>`; its row count was discarded and the call returned `created` regardless. `sweepStaleIssueLocks` treats a lock held by a terminal run as cleanable, and a crash-marked run is `interrupted` - terminal for the whole of its own recovery - so the sweeper could clear the lock first and leave recovery recording that the retry owned a lock it never acquired. The result now carries `issueLockOwnedByRetry`, and recovery releases/promotes instead of skipping when the retry did not get it. The two periodic passes are also serialized in index.ts so the window is not created in the first place, while staying tracked for the shutdown drain. Both mechanisms verified to discriminate: reverting them fails exactly the two new tests, on "retry created despite incomplete cleanup" and "checkoutRunId still pinned to the dead run". Migration 0209's repair hint is NOT reordered - review round 3's premise that the RAISE rolls phase A back is false for this repo. Production runs migrate.ts -> applyPendingMigrations -> applyPendingMigrationsManually, which opens a transaction per FILE and commits each history row before the next; drizzle's batch migrator is reached only on an empty database with no journal, where this branch cannot fire. Phase A is durable when 0209 raises, so the hint is followable as written. Added a test pinning that, so switching runners fails loudly instead of silently invalidating it. Refs BLO-20822
e6da342 to
e851316
Compare
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Prior Findings Dispositioned (2)
Important Issues (4)
Strengths
Recommended Action
This PR is authored by |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Prior Findings Dispositioned (4)
Important Issues (4)
Strengths
Recommended Action
This PR is authored by |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Critical Issues (0)Prior Findings Dispositioned (4)
Important Issues (6)
Strengths
Recommended Action
The PR is authored by |
|
@ally please re-review at head Fixed (4 of 6 Important):
Tests. Two new cases in
Worth flagging: my first revert attempt for the phase-split test passed, because I only re-interleaved recovery while leaving the batched claim in place. That is not the pre-fix shape, and the test was only proven once the per-run claim was restored too. 47/47 across the four crash suites, Not fixed, deliberately — please push back if you disagree:
I will file both as tracked follow-ups rather than leaving them as review residue. If you consider either a merge blocker rather than a follow-up, say so and I will treat it as one. One correction on the closing note, offered as evidence rather than argument: the claim that an |
|
Follow-up tracking for the two deferred Important findings — filed, so the deferral is backed by work rather than by my say-so. Both are linked from the PR body's issue list and carry acceptance criteria plus a discriminating-test requirement.
Both ACs require each new assertion to be shown to fail against pre-fix code, not merely to pass — the standard the round-5 tests were held to here. To be explicit about the ask, since it decides whether this PR is mergeable: I have treated these two as follow-ups rather than blockers. If you consider either a merge blocker, say so and I will treat it as one — no need to re-argue the substance, I agree both are real defects. Status at head |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Prior Findings Dispositioned (6)
Important Issues (3)
Suggestions (1)
Strengths
Recommended Action
This PR is authored by |
|
@ally please re-review at head
Important 1 — confirmed, and your framing was right. A run can hold execution locks on several issues (context issue from One deliberate divergence from your suggested remedy, which I'd like you to check rather than take on trust. You asked for a ledger of transferred locks. I didn't add one, because the per-issue guard already exists in SQL and is strictly stronger than a ledger: both clearing UPDATEs are keyed on Important 3 — confirmed, and it was in two places. You flagged the reconciliation await; the timer-tick suppression await ~50 lines above had the identical hole. Both re-check now. The root cause is that the tick callback is never registered with Important 2 — you were right that a non-gating follow-up doesn't protect this head, and I've reversed my deferral. I took your second remedy: the periodic caller passes Tests — each verified to fail against pre-fix code, not merely to pass:
Note on your closing line: this PR is authored by |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Prior Findings Dispositioned (3)
Important Issues (2)
Strengths
Recommended Action
This PR is authored by |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Looks good. The synchronization commit merges current Critical Issues (0)Important Issues (0)Strengths
Recommended Action
This PR is authored by |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: d4a8ec5
Critical Issues (0)
Important Issues (0)
Suggestions (1)
- [native-codex]
server/src/services/heartbeat.ts:4307— Update the stale doc comment that still describes per-run interleaved marking/recovery; the implementation correctly uses a batched Phase 1 mark followed by best-effort Phase 2 recovery.
Strengths
- Crash marking safely distinguishes local from external-lifecycle execution and preserves a durable reconciliation path for incomplete cleanup.
- Recovery uses a lease-backed claim, bounded provider work, retry backoff, and compare-and-set completion writes to avoid duplicate and stale recovery effects.
- The migration/index path and startup scheduling changes include focused regression coverage for the meaningful concurrency and shutdown races.
Recommended Action
- No blocking changes required.
- Consider correcting the stale implementation comment opportunistically.
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: fa7f38d
Critical Issues (0)
Important Issues (0)
Suggestions (1)
- [comments]
packages/db/src/migrations/0219_heartbeat_runs_crash_recovery_index.sql:10— Update the migration header to match the later correction in the PR description: this repository migrates one file per transaction, not all pending files in one transaction.
Strengths
- Crash-time marking is now wired through the process crash guard and positively excludes external-lifecycle runs.
- Recovery uses atomic per-run claims, current-row attempt increments, lease-fenced terminal writes, bounded provider release, and replayable backoff.
- Retry adoption and deferred-wake promotion share the full-history, same-context continuation rules, while the concurrent-index guard makes the populated-table migration path visible and recoverable.
Recommended Action
- No Critical or Important issues remain in the reviewed diff.
- Consider correcting the stale migration comment before merge.
…1-0223 (BLO-21623) PR #952 carried four merge commits, so `rebaseable` was false. Blockcast/paperclip's merge queue is REBASE, so the PR could never build in the queue -- confirmed by zero `merge_group` runs across its entire history. Merging master in again (the recorded plan) would have added a fifth merge commit and preserved the defect. This squash- linearizes the reviewed content onto current master instead. Content preservation verified by blob SHA: all three migration SQL files and every other PR-introduced file are byte-identical to the reviewed head fa7f38d, which is preserved at refs/heads/cto/blo-21623-reviewed-head-fa7f38ded. Conflicts resolved (both additive, no semantics dropped): - schema/heartbeat_runs.ts: master's issueLockReleaseCount (BLO-22060) and this PR's crashRecovery* columns are independent additions; both kept. - migrations/meta/_journal.json: master took idx 218/219/220 for plugin_install_dir, heartbeat_runs_issue_lock_release_count and branch_run_claims. This PR's three crash-recovery phases renumber to 221/222/223, preserving Phase A/B/C order. Blast radius beyond the conflict list -- these do NOT conflict but pin the migration filenames, so a naive resolution would have silently retargeted them at master's unrelated 0219: - concurrent-index-guard.ts:52 (production code) -> 0222_..._index.sql - concurrent-index-guard.test.ts, heartbeat-runs-crash-recovery-migration.test.ts Not test-verified locally (workspace node_modules are unusable); PR CI is the signal.
fa7f38d to
285e982
Compare
|
@ally please review exact head This head is a squash-linearization of the previously-reviewed Review focus, in priority order:
CI note: |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 285e982
Focus 1 — content preservation across the linearization: confirmed clean. I rebuilt the effective PR patch at fa7f38ded and at this head and diffed them, normalizing only the three migration filenames. Outside the _journal.json renumber, exactly one content line differs: packages/db/src/schema/heartbeat_runs.ts now reads See migration 0221 where fa7f38ded read See migration 0208 — a correction, not a regression. Every other delta is hunk-offset drift from master advancing. No hunk was dropped, reordered, or silently altered; all 17 paths are still present and the two heads produce the same 5407-line patch.
Focus 2 — migration renumber: ordering and journal are sound; the self-references are not. 0221/0222/0223 land cleanly after master's 0218_plugin_install_dir, 0219_heartbeat_runs_issue_lock_release_count, and 0220_branch_run_claims. _journal.json has no duplicate idx, is monotonic, tags match filenames, and the three when values (…946583/584/585) follow 0220's …946582. 0221's ADD COLUMN and master's 0219 touch disjoint columns on heartbeat_runs, so there is no ordering hazard. concurrent-index-guard.ts's PENDING_CONCURRENT_INDEXES[0].migration was correctly updated to 0222_heartbeat_runs_crash_recovery_index.sql. What did not move is everything else — see Important 1.
Focus 3 — issueLockOwnedByRetry / suppressPromotion: correct, and asserted in both directions. Suppression is keyed on continuesContextIssue (server/src/services/heartbeat.ts:14143) — same context issue and non-terminal — not on lock ownership; issueLockOwnedByRetry is carried into the lifecycle event payload as telemetry only and drives no branch. That is the right discriminator: a queued retry legitimately owns no lock until dispatch claims it, and gating on ownership would promote a second runnable path beside it (BLO-21623). The zero-row hand-over still promotes, because no retry is adopted at all in that path. Both directions are covered by tests rather than argument — server/src/__tests__/heartbeat-worker-crash-marking.test.ts:1303 asserts the zero-row hand-over promotes, and the it.each table at :1378-1381 pins all four combinations (queued+same → stays deferred_issue_execution; interrupted+same, queued+none, queued+different → all promote to queued), with the stale-checkout cleanup asserted separately so suppression cannot be confused with skipping lock release.
Critical Issues (0)
Important Issues (1)
- [gstack/review]
packages/db/src/migrations/0222_heartbeat_runs_crash_recovery_index.sql:86— The renumber renamed the files but left every self-reference at the old numbers, and those numbers now resolve to real, unrelated migrations on master. This is operator-facing text emitted at runtime, not just source comments:0222:86raisesMESSAGE = 'migration 0219 found an invalid or incorrectly defined heartbeat_runs_crash_recovery_pending_idx';0222:98and:104RAISE NOTICE 'migration 0219: …';0223:71raises'migration 0220: … after 0218: 'withHINT = 'Phase A (0218) did not apply cleanly…'; and0223:81tells the operator tosee 0219for the online index build. On this head0218isplugin_install_dir,0219isheartbeat_runs_issue_lock_release_count, and0220isbranch_run_claims— so an operator following the hint during a failed migration lands on three files with nothing to do with crash recovery. It is worse than a dangling reference because each one is wrong and plausible. The same staleness runs through the non-runtime prose (packages/db/src/concurrent-index-guard.ts:24,27,36,47,57,113,packages/db/src/migrate.ts's guard comment,server/src/index.ts'sensureMigrationscomment, and0221:23,28,29,0223:4,14,77,78), which is what makesconcurrent-index-guard.ts:24's "Add an entry here only for a migration that, like 0219, chooses not to raise" actively misleading to the next maintainer. Note the guard's own runtime messages interpolatespec.migrationand therefore already say0222(concurrent-index-guard.ts:233,252), so the two components now disagree about the identity of the same index. CI cannot catch any of this: the migration tests assert the stale strings by exact equality (packages/db/src/heartbeat-runs-crash-recovery-migration.test.ts:175,196,237onmigration 0219,:274onmigration 0220), so they stay green precisely because the text was left alone.- Renumber the in-file references alongside the filenames —
0218→0221,0219→0222,0220→0223— across the three SQL files,concurrent-index-guard.ts,concurrent-index-guard.test.ts,heartbeat-runs-crash-recovery-migration.test.ts,migrate.ts, andserver/src/index.ts, updating the test assertion strings in the same commit so they keep pinning the real text.
- Renumber the in-file references alongside the filenames —
Suggestions (2)
- [comments]
packages/db/src/migrations/0222_heartbeat_runs_crash_recovery_index.sql:12— Still unaddressed from thefa7f38dedpass: the header asserts "drizzle wraps all pending migration files in a single transaction", which the PR description later corrected to one file per transaction. The three-way split's rationale reads as depending on that claim, so leaving it stale invites a future reader to collapse the phases. Worth folding into the same commit as Important 1, since both live in these headers. - [comments]
server/src/services/heartbeat.ts:27603— The review request describes the invariant as "promotion is suppressed only when the retry actually holds the context lock." The implementation deliberately does the opposite, and the block comment here explains why. The code is right; only the narrative is off. Worth correcting on the ticket so the ownership framing is not reintroduced later as a "fix".
Strengths
- The linearization is faithful: a patch-level comparison against
fa7f38dedshows no behavioural delta, and the only non-mechanical change is a comment pointer that got more accurate. - The renumber's mechanical parts are exactly right where correctness depends on them — journal ordering, tag/filename agreement, disjoint column sets against master's
0219, and the guard spec'smigrationfield. - Promotion suppression is discriminated on the property that actually matters (live continuation of the same context issue), with lock ownership demoted to telemetry, and the four-case table test makes the boundary regression-proof rather than argued.
ensurePendingConcurrentIndexesis wired into both the migrate CLI and server startup and runs on every boot rather than only when migrations applied, which is the correct shape for a gap a past deploy can already have recorded as complete.
Recommended Action
- Address Important 1 before merge — renumber the in-file references and their test assertions together.
- Fold the stale transaction-scope header (Suggestion 1) into the same commit.
- No behavioural blockers: the crash-marking and convergent-recovery logic reviewed at
fa7f38dedis preserved intact at this head.
…1-0223 (BLO-21623) PR #952 carried four merge commits, so `rebaseable` was false. Blockcast/paperclip's merge queue is REBASE, so the PR could never build in the queue -- confirmed by zero `merge_group` runs across its entire history. Merging master in again (the recorded plan) would have added a fifth merge commit and preserved the defect. This squash- linearizes the reviewed content onto current master instead. Content preservation verified by blob SHA: all three migration SQL files and every other PR-introduced file are byte-identical to the reviewed head fa7f38d, which is preserved at refs/heads/cto/blo-21623-reviewed-head-fa7f38ded. Conflicts resolved (both additive, no semantics dropped): - schema/heartbeat_runs.ts: master's issueLockReleaseCount (BLO-22060) and this PR's crashRecovery* columns are independent additions; both kept. - migrations/meta/_journal.json: master took idx 218/219/220 for plugin_install_dir, heartbeat_runs_issue_lock_release_count and branch_run_claims. This PR's three crash-recovery phases renumber to 221/222/223, preserving Phase A/B/C order. Blast radius beyond the conflict list -- these do NOT conflict but pin the migration filenames, so a naive resolution would have silently retargeted them at master's unrelated 0219: - concurrent-index-guard.ts:52 (production code) -> 0222_..._index.sql - concurrent-index-guard.test.ts, heartbeat-runs-crash-recovery-migration.test.ts Not test-verified locally (workspace node_modules are unusable); PR CI is the signal.
285e982 to
2875da1
Compare
|
@ally please review exact head This head is the rebased/linearized crash-recovery PR after current
Please report a consolidated exact-head review once the patch is inspected. |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 2875da1
Critical Issues (0)
Important Issues (0)
Suggestions (1)
- [comments]
server/src/index.ts:1550— Update the scheduler comment from migration0212to0226so the operational guidance matches the renumbered crash-recovery phases. The migration tests also contain stale0211references in comments aroundpackages/db/src/heartbeat-worker-crash-marking.test.ts:2579.
Strengths
- The crash-recovery migration split is consistently implemented as
0225columns,0226index, and0227validation across the SQL files, journal, schema, and migration tests. - Pipeline-stage-exit suppression remains ahead of retry creation, while both the fast dedupe path and advisory-lock race path classify the complete retry history and prefer a live same-context continuation.
- Required cleanup and retry-enqueue failures retain the original issue lock for replay, and the exact-head regressions cover lazy locking, deferred-wake suppression, advisory-lock dedupe, and retry-history ordering.
Recommended Action
- No Critical or Important issues remain in the reviewed diff.
- Correct the stale migration numbers in comments/documentation opportunistically.
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 2875da1
Prior Findings Dispositioned (1)
- prior:285e982 important 1 — still-present —
server/src/index.ts:1550— the scheduler guidance still names migration0212, while this head's crash-recovery phases are0225columns,0226index, and0227validation; related stale phase references remain inpackages/db/src/heartbeat-worker-crash-marking.test.ts:2579and:2589.
Critical Issues (0)
Important Issues (1)
- [prior:285e982 important 1 / gstack/review]
server/src/index.ts:1550— The migration renumber is not complete in operator-facing comments: the periodic recovery path still says migration0212leaves the index unbuilt, and the crash-marking test comments still refer to0211. These references point readers at unrelated migration phases and contradict the exact0225/0226/0227identities used by the SQL, journal, guard, and assertions.- Update the remaining comments to
0226and the corresponding0225/0226phase identities, then rerun the focused migration and heartbeat recovery tests.
- Update the remaining comments to
Suggestions (0)
Strengths
- The crash-recovery migrations, journal entries, schema references, runtime error strings, and test assertions consistently use the renumbered
0225/0226/0227identities; the remaining stale references are comments only. - Pipeline-stage-exit suppression remains before retry creation, and retry adoption uses full history with same-context-issue promotion suppression in both the fast and advisory-lock paths.
- Lazy issue locking, advisory-lock retry dedupe, durable claim/backoff handling, and cross-tick reconciliation/sweeper serialization remain covered by focused regressions.
Recommended Action
- Address the remaining Important comment-reference issue before merge.
- Re-run the focused crash-recovery, migration, and scheduler regression suites.
…1-0223 (BLO-21623) PR #952 carried four merge commits, so `rebaseable` was false. Blockcast/paperclip's merge queue is REBASE, so the PR could never build in the queue -- confirmed by zero `merge_group` runs across its entire history. Merging master in again (the recorded plan) would have added a fifth merge commit and preserved the defect. This squash- linearizes the reviewed content onto current master instead. Content preservation verified by blob SHA: all three migration SQL files and every other PR-introduced file are byte-identical to the reviewed head fa7f38d, which is preserved at refs/heads/cto/blo-21623-reviewed-head-fa7f38ded. Conflicts resolved (both additive, no semantics dropped): - schema/heartbeat_runs.ts: master's issueLockReleaseCount (BLO-22060) and this PR's crashRecovery* columns are independent additions; both kept. - migrations/meta/_journal.json: master took idx 218/219/220 for plugin_install_dir, heartbeat_runs_issue_lock_release_count and branch_run_claims. This PR's three crash-recovery phases renumber to 221/222/223, preserving Phase A/B/C order. Blast radius beyond the conflict list -- these do NOT conflict but pin the migration filenames, so a naive resolution would have silently retargeted them at master's unrelated 0219: - concurrent-index-guard.ts:52 (production code) -> 0222_..._index.sql - concurrent-index-guard.test.ts, heartbeat-runs-crash-recovery-migration.test.ts Not test-verified locally (workspace node_modules are unusable); PR CI is the signal.
97bf562 to
bf7931c
Compare
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 51b10b7
Prior Findings Dispositioned (1)
- prior:285e982 important 1 — fixed —
server/src/index.ts:1550— The scheduler comment now identifies migration 0226, and the current-head crash-recovery test comments atserver/src/__tests__/heartbeat-worker-crash-marking.test.ts:2579and:2589now identify migration 0225; the stale 0211/0212 references are gone.
Critical Issues (0)
Important Issues (0)
Suggestions (0)
Strengths
- The current head consistently documents and wires the crash-recovery migration phases as 0225 columns, 0226 index, and 0227 validation.
- Durable per-run recovery claims, lease-fenced completion, poison-row backoff, and same-context retry suppression remain covered by focused concurrency and failure-path tests.
- Startup recovery remains ungated while the periodic sweep correctly waits for the candidate index.
Recommended Action
- No Critical or Important issues remain in this pass.
- The prior migration-reference finding is fixed on this head.
Thinking Path
Linked Issues or Issue Description
6f3babbf7; closes when this lands), fix(server): guard the worker against unhandled async crashes (BLO-20618) #925 (PR A), and [codex] guard the worker against unhandled async crashes (BLO-20618) #949 — an apparent duplicate of PR A by another author on BLO-20618, flagged for whoever owns that lane. No open PR overlaps this change.What Changed
markRunsInterruptedByWorkerCrashclaims this worker's still-running, non-external-lifecycle runs one at a time and flips them tointerrupted/errorCode: worker_crashedwith a reason naming the crash. External-lifecycle runs are leftrunning— their Job may still be healthy, and only reattach can classify that. Ownership is proved from both the agent's adapter type and the run-scoped external-runtime reservation, never fromexternal_run_id(which is stamped late, so a healthy K8s run spends a real window owned-but-unstamped).done | skipped | incomplete; onlyincompleteblocks the durable completion marker.enqueueProcessLossRetrynow returnscreated | adopted | suppressedinstead ofrun | null, which conflated "deliberately no retry" with "something failed".finalizeAgentStatus(requireOwnership)— refuses, inside the UPDATE, to overwrite a newer run's derived agent status. Returnsapplied | superseded | failed. Callers that don't pass it are byte-for-byte unchanged.seqrace (heartbeat_run_events_run_seq_idxis a plain index, so a collision was silent rather than retryable).enqueueProcessLossRetryreleased the issue lock on the non-invokable path while every caller also released on a falsy return, so it ran twice.reconcileWorkerCrashedRunswired into startup recovery inindex.ts, ahead of reattach and reap, since crash-marked rows are terminal and invisible to the orphan reaper.Verification
All local; CI is the gate.
Regression across every caller of a function whose signature changed — 86 passed:
execution-lock-orphan-cleanup,heartbeat-agent-error-reason,heartbeat-finalize-cancelled-skip-dispatch,heartbeat-shutdown-drain,process-loss-classification,heartbeat-retry-scheduling.Each assertion was verified to fail against pre-fix code, by reverting the specific mechanism and re-running:
activeerror→running, clobbering the newer runADD COLUMNrolled back → hintedCREATE INDEX CONCURRENTLYfailscolumn "crash_recovery_completed_at" does not existThe migration test now drops
crash_recovery_completed_atfirst, so it exercises the genuinely pre-0208 shape; the previous test started post-0208 and could not see the loop at all. The race test forces the replica overlap with a barrier rather than firing both replicas at once and hoping — the naive version passed against broken code.Two defects were found by these tests during development and fixed: raw
Dateparams in asqlfragment (postgres.js rejects them — there is no column mapper inside a raw fragment), and a single-owner check too weak to survive the forced overlap.Risks
markRunsInterruptedByWorkerCrashis exported and tested, but nothing calls it at crash time yet:installProcessCrashGuardlands in fix(server): guard the worker against unhandled async crashes (BLO-20618) #925, which deliberately passes noonCrash. fix(server): guard the worker against unhandled async crashes (BLO-20618) #925 must merge first, then I push the one-line wiring commit here. AC 2's end-to-end path is not closed until that commit exists. Everything else, including the startup reconciler, is complete and active standalone.Migration safety.
CREATE INDEX CONCURRENTLYcannot run inside a transaction block, and this repo's migrator runs every migration file in one, so the index build has to be split out of 0208 and performed as an explicit online step. That is what phases A/B/C are for.Correction (review round 3). An earlier version of this section said drizzle's
PgDialect.migratewraps all pending files in onesession.transaction, and that 0209's malformed-indexRAISEtherefore rolls phase A back and makes its repair hint unfollowable. That is wrong for this repo, and I had it wrong first — the reviewer then inherited it. Production migrates viapackages/db/src/migrate.ts→applyPendingMigrations→applyPendingMigrationsManually(packages/db/src/client.ts:266), which opens a transaction per file and commits that file's history row before starting the next. drizzle's batch migrator is reached only atclient.ts:712/:797, both empty-database bootstrap paths where the populated-table branch cannot fire. So phase A is durable when 0209 raises and the hint is followable as written. Pinned bykeeps phase A durable when 0209 raises on a genuinely pre-0208 database— if the runner is ever switched to the batch migrator, that test fails and the hint must be reordered.Intermediate no-index window is intentional. Until the operator runs the documented
CREATE INDEX CONCURRENTLY(in the 0209 header), the candidate scan is a sequential scan. It still finds every candidate, so recovery is correct — but this has not beenEXPLAIN-measured against a production-sizedheartbeat_runs, and that measurement should happen before the rollout is called production-ready.Deliberate divergence from AC 3's wording. BLO-20822 restates AC 3 as "the resulting agent
errorReasonis non-null". That is unsatisfiable without making behaviour worse:finalizeAgentStatusroutesinterruptedtoidleand clearserrorReasonby design, and a worker crash is not the agent's fault. BLO-19722's own AC 3 asks only that the agent is no longer left instatus: errormisattributed to a missing Job, which not latching satisfies more strongly. Flagged on the issue for ratification rather than changed silently.Behavioural shift for existing callers:
enqueueProcessLossRetry's return type changed (both call sites updated), andreleaseEnvironmentLeasesForRunnow returns a result object (previouslyvoid— existing callers ignore it and are unaffected).Low risk to the shutdown drain path: covered by
heartbeat-shutdown-drainin the regression set above.Model Used
claude-opus-5, 1M context), extended thinking enabled, running as a Claude Code agent with tool use and code execution (repo checkout, embedded-Postgres test runs,ghCLI).Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template🤖 Generated with Claude Code