fix(gr2): a torn journal line stops being fatal, and stops eating its neighbour - #896
Merged
Conversation
… neighbour The journal writer appends and fsyncs, so a kill between the two leaves a partial line. Three consequences, each with its own witness. A tear used to cost TWO rows, not one. Appending onto an unterminated line glues the new row to the remnant, so a row that was written correctly and fsynced becomes unreadable. _write now terminates a remnant before appending, confining the damage to the line actually interrupted. _rows raised a bare JSONDecodeError on any unparseable line. The daemon lets anything outside its named failure list propagate, so one bad moment exited the loop on its first tick and on every restart after it. Lines that cannot be parsed are now dropped and counted, and MalformedLine keeps the drop from being silent. Position does not discriminate cause, which is why the drop is unconditional rather than trailing-only: a tear is trailing only until the daemon restarts and appends again, after which the same orphan sits mid-file with intact rows on both sides. The question that does discriminate is whether the cursor can still be accounted for, and replay already asks it and already raises JournalInconsistent by name. Dropping here lets that check decide instead of being pre-empted by a parse error. Prototype 1 asks the machine to account for an observation on every tick, including idle ones, which moved this parse from once-per-change to once-per-tick. Rows are now cached against (size, mtime_ns); the Propagator is built once per loop and holds one Journal, so the cache spans ticks. The one state it cannot see - an in-place mutation changing neither size nor mtime_ns - is named in the code rather than assumed away. 86 passed, 1 xfailed, up from 83 by exactly the three witnesses added. Each guard is mutation-proven: removing the newline repair reddens the neighbour witness, restoring the bare parse reddens that one and the skip witness both, and disabling the cache reddens the idle-tick witness. Ref #893 - closes at promotion Co-Authored-By: Claude <noreply@anthropic.com>
Member
Author
|
r2 (Sentinel) — APPROVE, bound to head 37eced3. RAN: 86 passed, 1 xfailed across the state-machine and daemon suites with the checkout pinned on PYTHONPATH. Disabling the unchanged-file cache made only its cache witness fail; the restored source hash matched exactly. READ: journal parsing, append repair, cursor ordering, and the three new witnesses. Scope: this public journal torn-line and cache change at the bound head. |
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.
The journal writer appends and fsyncs, so a kill between the two leaves a partial line. Three consequences, each with its own witness.
A tear used to cost two rows, not one. Appending onto an unterminated line glues the new row to the remnant, so a row that was serialized correctly and fsynced becomes unreadable.
_writenow terminates a remnant first, confining the damage to the line actually interrupted. This is the half the issue did not contain, and it is the reason the issue's discriminator does not hold._rowsraised a bareJSONDecodeErroron any unparseable line. The daemon lets anything outside its named failure list propagate, so one bad moment exited the loop on its first tick and on every restart after it. Unparseable lines are now dropped and counted, andMalformedLinekeeps the drop from being silent.The drop is unconditional rather than trailing-only, because position does not discriminate cause. A tear is trailing only until the daemon restarts and appends again, after which the same orphan sits mid-file with intact rows on both sides — so a position rule would raise on the writer's own crash-then-restart signature. The question that does discriminate is whether the cursor can still be accounted for, and
replayalready asks it and already raisesJournalInconsistentby name. Dropping here lets that check decide instead of being pre-empted by a parse error. Skipping is safe because the acknowledgement row is written before the cursor advances, so a torn acknowledgement leaves the operation unacknowledged and it is redone idempotently.Idle ticks no longer re-parse the file. Prototype 1 asks the machine to account for an observation on every tick, which moved this parse from once-per-change to once-per-tick. Rows are cached against
(size, mtime_ns); thePropagatoris built once per loop and holds oneJournal, so the cache spans ticks — verified rather than assumed, since a cache that never hits in production would be a green over a non-fix. The one state it cannot see, an in-place mutation changing neither size nor mtime_ns, is named in the code rather than assumed away.Verification
86 passed, 1 xfailed — up from 83 by exactly the three witnesses added, so nothing was lost or silently skipped.
Each guard is mutation-proven, and each mutation was restored byte-exact:
json.loadsThe second killing two is a real dependency rather than a stray: W1 asserts on the rows that survive, so without the skip guard its torn orphan still raises. The guards compose, and that is reported rather than presented as three tidy isolations.
Ref #893 — closes at promotion.
Premium boundary: grip is OSS because this is local workspace state machinery — journal durability and read caching — with no identity, org, or entitlement semantics.