fix(wake): #917 gate the final observed_seq cursor write + observed.set/cursor consistency (defense-in-depth)
Some checks failed
ci/woodpecker/pr/ci Pipeline failed
Some checks failed
ci/woodpecker/pr/ci Pipeline failed
Defense-in-depth hardening of store.sh cmd_enqueue surfaced by the #915 review (obs#2): the final observed_seq cursor _atomic_write was the ONE durable write not wrapped in a failure check, and was cross-file non-atomic with the observed.set write immediately before it. Practically unreachable today, but a cursor-write failure after a successful observed.set write could (a) return spurious success while the allocation stayed uncommitted, and (b) leave observed.set advanced while the cursor lagged (cross-file inconsistent). Fix (additive; preserves #908 exactly): - GATE the final cursor write like the pending/observed.set writes (#908): a cursor-write failure is now FAIL-LOUD (non-zero + diagnostic), never swallowed. - On cursor-write failure, ROLL observed.set BACK to its pre-write snapshot, so observed.set and the cursor either BOTH advance or NEITHER does — never a stranded observed.set entry above the cursor. #908 invariants intact: single store-side allocator, atomic allocate+enqueue under flock, arrow-1 no-burn (pending write still FIRST, still aborts before any cursor advance), anti-swallow <=consumed fail-loud, W2 contiguous-prefix CONSUMED. The cursor stays the sole COMMIT point, so a pending-ahead state is exactly the one #908 already tolerates on its observed.set-failure path. On-disk format UNCHANGED (read-compatible). Test: new T11 in test-wake-store-ack.sh forces the final cursor _atomic_write to fail (bind-mount EBUSY over observed_seq inside an unshare mount+user namespace, seeded so the cursor READ still succeeds) after pending+observed.set succeed, and asserts fail-loud + observed.set/cursor consistency + intact consumed prefix. RED against pre-#917 code; all store-ack/race/reconcile/detector groups stay GREEN. manifest bumped 0.6.8 -> 0.6.9. Closes #917 Part of #892 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0158NZqN2n2ymKFeJAZ4GUCb
This commit is contained in:
@@ -251,9 +251,15 @@ cmd_enqueue() {
|
||||
fi
|
||||
|
||||
# --- observed.set: record the seq in the live window (gap-detector SoT) ----
|
||||
# Snapshot observed.set's prior content FIRST: if the final cursor write below
|
||||
# fails AFTER this write advances observed.set, we roll observed.set back to this
|
||||
# snapshot so observed.set can never be left cross-file inconsistent with the
|
||||
# cursor (#917 — observed.set ahead of a cursor that never committed).
|
||||
local prev_observed_set
|
||||
prev_observed_set="$(cat "$STATE_DIR/observed.set" 2>/dev/null || true)"
|
||||
if ! {
|
||||
cat "$STATE_DIR/observed.set" 2>/dev/null
|
||||
printf '%s\n' "$seq"
|
||||
printf '%s' "$prev_observed_set"
|
||||
printf '\n%s\n' "$seq"
|
||||
} | grep -v '^[[:space:]]*$' | sort -n | uniq | _atomic_write "$STATE_DIR/observed.set"; then
|
||||
_wake_lock_release
|
||||
echo "store.sh enqueue: observed.set write FAILED for seq $seq — observed_seq cursor NOT advanced." >&2
|
||||
@@ -264,8 +270,25 @@ cmd_enqueue() {
|
||||
# Written LAST: only now, after the durable pending + observed.set writes
|
||||
# succeeded, does the cursor advance. This is the atomic commit point of the
|
||||
# allocation.
|
||||
#
|
||||
# #917 (defense-in-depth): this final write is GATED like the pending/observed.set
|
||||
# writes (#908) — its failure is FAIL-LOUD, never silently swallowed (which would
|
||||
# return success while the allocation stayed uncommitted). And because observed.set
|
||||
# was already advanced just above, a cursor-write failure would leave observed.set
|
||||
# ahead of the cursor (cross-file inconsistent); so on failure we ROLL observed.set
|
||||
# BACK to its pre-write snapshot before failing loud. Net: observed.set and the
|
||||
# cursor either BOTH advance or NEITHER does — never a stranded observed.set entry
|
||||
# above the cursor. (The pending write may remain ahead, exactly as #908 already
|
||||
# tolerates on the observed.set-failure path: the cursor is the sole COMMIT point,
|
||||
# so an uncommitted pending entry is re-derived/reconciled, never consumed — the
|
||||
# allocation is simply not committed.) On-disk format is UNCHANGED (read-compatible).
|
||||
if [ "$seq" -gt "$observed" ]; then
|
||||
printf '%s' "$seq" | _atomic_write "$STATE_DIR/observed_seq"
|
||||
if ! printf '%s' "$seq" | _atomic_write "$STATE_DIR/observed_seq"; then
|
||||
printf '%s' "$prev_observed_set" | grep -v '^[[:space:]]*$' | _atomic_write "$STATE_DIR/observed.set" || true
|
||||
_wake_lock_release
|
||||
echo "store.sh enqueue: observed_seq cursor write FAILED for seq $seq — allocation NOT committed; rolled observed.set back so it stays consistent with the cursor (#917)." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
_wake_lock_release
|
||||
|
||||
Reference in New Issue
Block a user