fix(run-control): serialize journal appends across processes (#651) - #660
Conversation
13 failing tests written by the brigade run red phase: real two-process append race via subprocess (reproduces the chain fork), sibling lifecycle.jsonl.lock expectations, steer-against-completed-run refusal, bounded control.failed payloads, and the three minor items from #651. Implementation follows; tests must pass without being weakened.
Add a sibling lifecycle.jsonl.lock acquired with fcntl.flock inside the existing append critical section, so the run owner, the steer/interrupt control CLI, redaction, and recovery serialize across OS processes rather than only within one. Lock nesting is SIGTERM mask, then the process lock, then the file lock, with unlock before close and the primary exception preserved. Guard control appends on run status and lock ownership so steering a completed run refuses instead of journaling into a terminal chain, and replace free transport text in control.failed payloads with a bounded error class plus digest reference. Salvaged from run 20260801-191101-6612a08a, which produced a complete implementation before its worker timed out at 1500s and its worktree was pruned (see #656).
|
Important Review skippedAuto reviews are limited based on label configuration. 🏷️ Required labels (at least one) (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository: escoffier-labs/coderabbit/.coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Grader: APPROVE, merging. Verified independently rather than trusting the green suite, since I assembled this branch from two salvaged changes.patch artifacts and the tests share provenance with the implementation:
This closes the Critical introduced by #641. Live steer/interrupt on journal-backed runs is safe again. |
…-journal-lock # Conflicts: # src/brigade/run_redaction.py
Fixes #651.
Closes the Critical defect found in the post-merge grading review of #641: the
runs steer/interruptcontrol CLI appends to a run's lifecycle journal from a second OS process, while_append_critical_sectionheld only athreading.Lockplus signal mask. Two processes could each read tail sequence N and append N+1 to the O_APPEND file, forking the hash chain and permanently fail-closing a journal-authoritative run with no recovery path.Fix and regression map
lifecycle.jsonl.lockacquired withfcntl.flock(LOCK_EX)inside the existing critical section, taken by every append path: owner lifecycle writes, the control CLI, redaction, and recovery. Nesting is SIGTERM mask, then process lock, then file lock; unlock precedes close and a primary exception is preserved through cleanup. Hosts withoutfcntlorpthread_sigmaskkeep the prior in-process guards.test_cross_process_append_race_serializes(realsubprocesschildren, not monkeypatch)StaleSequenceErrorinstead of dying when a control append lands between read and write.test_owner_retries_stale_sequence_after_external_appendtest_steer_against_completed_run_refuses_without_journalingcontrol.failedpayloadsrun_events.EVENT_TYPES.test_control_failed_payload_carries_bounded_error_classtests/test_run_events_cursor_control.pyruns eventsno longer stops at the first terminal event, sorun.redaction.recordedandcontrol.*are reachable through the cursor reader.tests/test_run_events_cursor_control.pytests/test_run_events_cursor_control.pyNo changes to event canonicalization, existing event types, or golden fixtures.
Verification
brigade work verify run --target . --command "./scripts/verify" --capture brigade-work20260801-200416-work-verify-135e03, exit 0: 5,675 passed, 3 skipped20260801-200338-work-verify-138809: 308 passed across the four affected suites, including all 13 previously-red testsProvenance note for the reviewer
This branch was assembled in two salvage steps, both from
changes.patchartifacts ofbrigade runinvocations that terminated non-successfully after producing good uncommitted work and then had their worktrees pruned (see #656):0dcbe970red suite, from run20260801-182926-facaecff(endedincomplete; a worker misread its base commit and cascadedskipped: prerequisite failed)711e1fefimplementation, from run20260801-191101-6612a08a(endedtimeoutat 1500s after the implementation was complete)The red suite was verified failing against the pre-fix tree before the implementation was applied:
AssertionError: cross-process append race forked the journal: ['sequence gap/duplicate: expected 202, got 201'].