|
| 1 | +# Issue #635: journal ceiling decision (measure, then design) |
| 2 | + |
| 3 | +Status: design decision awaiting grading. No production implementation in this change. |
| 4 | + |
| 5 | +## Measured numbers |
| 6 | + |
| 7 | +Artifact: `.brigade/measurements/issue-635-journal-ceiling.json` (local, same shape family as `issue-568-slice6.json`: environment metadata plus structured aggregates; volume mode adds bounds and scenario blocks). |
| 8 | + |
| 9 | +Current hard bounds: `MAX_JOURNAL_EVENTS = 512`, `MAX_JOURNAL_BYTES = 8 MiB`. |
| 10 | + |
| 11 | +| Scenario | Events | Bytes | % event ceiling | % byte ceiling | Hit ceiling? | |
| 12 | +| --- | ---: | ---: | ---: | ---: | --- | |
| 13 | +| Scanned real runs (n=13, single-seat journal-hardening runs) | p50/max 17 | p50 14,279 / max 14,286 | 3.3% | 0.17% | no | |
| 14 | +| Synthetic representative (1 seat, 1 attempt, 0 pauses) | 11 | 7,780 | 2.1% | 0.09% | no | |
| 15 | +| Synthetic roster-sized (11 seats, 2 attempts, 3 pause/resume cycles) | 155 | 124,777 | 30.3% | 1.5% | no | |
| 16 | +| Synthetic worst case (32 seats, 3 attempts, 8 pause cycles) | 512 | 420,683 | 100% | 5.0% | yes, mid-dispatch | |
| 17 | + |
| 18 | +Worst-case stop reason: `LifecycleJournalError: bound exceeded: journal event sequence above MAX_JOURNAL_EVENTS` at `seat-28` attempt 1. Bytes were still ~5% of the 8 MiB cap. |
| 19 | + |
| 20 | +## Decision |
| 21 | + |
| 22 | +Raise the event bound. Do not segment the journal yet. |
| 23 | + |
| 24 | +Reasoning: |
| 25 | + |
| 26 | +1. Event count is the binding constraint. At a full 512-event journal the file is only ~0.4 MiB. |
| 27 | +2. Realistic and roster-sized load stays far from 512 (3% real, 30% for 11 seats with retries and three approval cycles). |
| 28 | +3. The ceiling is reachable, but only under a synthetic fleet larger than today's 11-seat roster with three attempts per seat. |
| 29 | +4. Segmentation (cross-linked `lifecycle.NNNNN.jsonl`, reader/projector/doctor/recovery awareness) is a large surface change. The measured gap does not justify that cost before a cheaper bound raise plus a soft warning. |
| 30 | +5. Keep segmentation as the explicit escape hatch if #593 budget events or larger fleets push roster-sized runs past ~50% of the raised ceiling. |
| 31 | + |
| 32 | +Recommended production change (separate implementation issue, after this design is graded): |
| 33 | + |
| 34 | +- Raise `MAX_JOURNAL_EVENTS` from 512 to 2048 (4x). |
| 35 | +- Leave `MAX_JOURNAL_BYTES` at 8 MiB for now (still ~20x headroom at 2048 median-sized events). |
| 36 | +- Add a doctor soft threshold at 75% of the event bound (1536 of 2048): `WARN`, not `FAIL`. |
| 37 | +- Keep the hard refuse-to-append behavior only at the new ceiling, and make that failure visible (already true today via `LifecycleJournalError`). |
| 38 | + |
| 39 | +## Soft-threshold doctor warning |
| 40 | + |
| 41 | +When `brigade doctor` inspects a run journal and `event_count >= ceil(0.75 * MAX_JOURNAL_EVENTS)` while still under the hard ceiling: |
| 42 | + |
| 43 | +- status: `WARN` |
| 44 | +- check name: `runs: journal event headroom` |
| 45 | +- detail shape: `lifecycle journal at {event_count}/{MAX_JOURNAL_EVENTS} events ({pct}%); raise or segment before the hard ceiling halts appends` |
| 46 | + |
| 47 | +Below 75%: silent on this check. At or above the hard ceiling: keep today's `FAIL` / `bound exceeded` path for recovery verdicts that already treat oversize journals as failed. |
| 48 | + |
| 49 | +## Recovery after upgrade for a run already at the old ceiling |
| 50 | + |
| 51 | +A run that stopped at sequence 512 under the old bound has a complete, chain-valid journal and paired checkpoints. It does not need rewrite or segmentation to become readable again. |
| 52 | + |
| 53 | +After the bound raise ships: |
| 54 | + |
| 55 | +1. Readers (`read_journal` / `read_journal_bounded`) accept the existing 512-event file because it is under the new ceiling. |
| 56 | +2. Doctor recovery checks that previously returned `FAIL` / `bound exceeded` solely for the old event ceiling clear once the constant moves. |
| 57 | +3. If the run is still live or being resumed, the active lock owner may append sequence 513+ again; checkpoint pairing and projection continue from the existing chain tip. |
| 58 | +4. No migration tool is required for journals that stopped exactly at 512. Journals that somehow exceed the new byte bound remain fail-closed (unchanged). |
| 59 | + |
| 60 | +## Explicit non-goals for the follow-up implementation issue |
| 61 | + |
| 62 | +- No journal segmentation. |
| 63 | +- No change to worker/provider event streams (#592 stays separate). |
| 64 | +- No silent truncation or dropping of lifecycle facts when the hard ceiling is hit. |
| 65 | + |
| 66 | +## Reproducing the measurement |
| 67 | + |
| 68 | +```bash |
| 69 | +python scripts/measure_run_journal.py \ |
| 70 | + --mode volume \ |
| 71 | + --scan /path/to/.brigade/runs \ |
| 72 | + --output .brigade/measurements/issue-635-journal-ceiling.json |
| 73 | +``` |
0 commit comments