Scope the PRISM append-invalidation epoch per anchor (#126) - #151
Open
kiwidream wants to merge 7 commits into
Open
Scope the PRISM append-invalidation epoch per anchor (#126)#151kiwidream wants to merge 7 commits into
kiwidream wants to merge 7 commits into
Conversation
Issue #128 ranks block candidate landing first by incident history, and the harness landed by #139 covered only the writer-lease lifecycle. This teaches it the landing state machine, which is the one owner where two threads of the same process reach the same durable rows. The PostgreSQL model gains the rows landing turns on: qbit_pool_blocks, qbit_block_candidate_outbox, the qbit_confirm_pool_block publication ordinal allocator and the durable floor read. The allocator is modelled on the shipped PL/pgSQL rather than on an approximation of it: the ordinal comes from a sequence, so an exact replay of an already-confirmed row burns nothing, a terminally disposed row reports the superseded disposition, and an aborted confirmation leaves a gap the MAX-based floor tolerates. That distinction is the whole of the two-tail defect -- a later ordinal can publish first and leave an earlier one behind the floor. Landing writes are staged per transaction for the same reason lease writes already were: a deadline-scoped statement sends COMMIT as a separate message, and the interesting window is inside it. Statement classification is anchored on fragments lifted verbatim from the production SQL, as the lease signatures already are, so a change to that SQL breaks classification loudly instead of quietly changing what the fake models. Where a modelled statement is a large CTE, the model reproduces the effects a landing reads back and says so. Three new pieces support the topology. Scheduler-aware locks, conditions, semaphores and events make a contended process-local lock an interleaving point rather than a wedged baton -- the lease scenarios never needed them, because they interleave two processes around one row, while the two landing tails share the payout-balance serializer, the publication order guard and the ledger's admission gates. Phase breakpoints let a scenario stop a tail at a boundary the landing already names for its own watchdog, because landing's interesting windows are the gaps between statements and no statement runs at the instant that matters. And HarnessBase now holds what every state machine's harness owns, so run_scenario and assert_deterministic are generic over it rather than typed to one harness; the two owners still owed to #128 plug in without touching the determinism check again. The acceptance criterion is #133's two-distinct-hash interleaving, driven end to end: block A confirms on the synchronous client-thread tail, block B lands on the accounting actor inside A's confirm-to-publish window and publishes the next ordinal, and A then publishes behind the raised floor. It is already fixed here, so the scenario passes; the evidence is that the interleaving can be expressed at all, which nothing could do before. The suite also pins the load-bearing claim underneath it -- the per-hash disposition lease does not serialise distinct hashes -- and the fresh lease proof that gates the superseded-envelope restore. Every scenario is proved order-stable over 25 runs of the full checkpoint trace. Refs #128, #133.
Issue #125: a landing-class ledger step is two waits, not one. The caller queues for the ledger writer lock -- local, unbounded by any statement deadline -- and only then runs the statement it queued for, under its own full budget. Both are spent on the block-work thread the watchdog monitors, and before 49f04c3 both were heartbeat-silent, so an escalated landing at the reviewed 120s cap could hold the heartbeat still for ~240s against a 120s tolerance. The watchdog hard-exited mid-landing and the in-memory escalation counter died with the process, so the restart came back at the 30s base to repeat the cycle. Add landing-harness scenarios for both halves of the fix: - a real accounting tail queued behind a held writer lock stamps wait-ledger-admission:landing every slice instead of going quiet, and an A/B against the same gate at the same budget with no progress hook shows slicing changes nothing about when the wait ends; - the escalation ladder is climbed by genuinely timed-out landings and every rung's admission-plus-statement pair fits the watchdog tolerance, across a clamped cap, a clamped base and a configuration needing no clamp at all, with the clamp reported exactly once; - both composed: a landing that queues for all but one slice of its escalated budget still lands, inside the tolerance, with no silence longer than one slice. Every scenario is proved order-stable with assert_deterministic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The lease half of the deterministic harness is defended by tests/test_prism_concurrency_harness.py; the landing half added in cefdbfa was not. A fake that answers an unrecognised statement plausibly drifts away from production silently, and every scenario built on it inherits the drift, so a landing scenario that failed could not distinguish "the code under test is wrong" from "the model is wrong". tests/test_prism_landing_harness_model.py pins the model's own contract in four parts: - The publication-ordinal allocator against qbit_confirm_pool_block: a fresh confirmation takes the next sequence value, an exact replay returns 1 without burning one (proved by the next block's ordinal), every terminal disposition reports -1 rather than 0, a height mismatch and a missing row both report 0, and the durable floor — a MAX over the table — stays put across the gap an aborted confirmation leaves behind. - Transaction scope: a deadline-scoped landing write is invisible to another session until COMMIT and visible to its own before it, and a rolled-back landing leaves no row while keeping its ordinal burned. - Classification: every LandingOp is driven through the shipped PsqlShareLedger method that emits it, a landing table mention that matches no signature raises naming _LANDING_SIGNATURES, the deferred markers still name their follow-up state machine, and a deposed writer fails the lease CTE in the two shapes the SQL dictates — the outbox statements report it, the PL/pgSQL functions raise. - The synchronisation primitives: mutual exclusion and FIFO grants, non-blocking and virtual-clock-timed acquires, the non-reentrant re-acquire that a real mutex would deadlock on, RLock depth, the condition's release/re-acquire and restored depth, semaphore exhaustion and over-release, and the event's parked and timed waits. One landing scenario is proved order-stable with assert_deterministic against LandingHarness. Nothing sleeps in wall-clock time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The append-invalidation epoch is one global counter, so a landing that compares its own baseline against the live value learns only that some payout window was invalidated. A share stamped later than this candidate's declared anchor -- but early enough to predate a newer exposed window -- bumped that counter without touching this candidate's window, and the pre-offer fence read the inequality and terminally abandoned the candidate as append_epoch_stale. On the fallback submit lane that happens before qbitd ever sees the block, so a valid block's reward is forfeited. Record, with each bump, the stamp of the row that caused it (max(job_issued_at_ms, accepted_at_ms) -- exactly what _pending_share_predates_anchor compares), keep the most recent 256 in a bounded map alongside the oldest epoch still retained, and let a landing ask whether any bump in (baseline, live] was caused by a row that predates its own declared anchor. Both landing fences ask that instead of comparing counters. The predicate fails closed on everything it cannot prove: an unknown declared anchor, a baseline older than the retained history, or a gap in the scanned range. The post-offer branch is untouched -- it already logs rather than abandons -- and collection_only candidates stay exempt. The relaxation rests on the existing ordering: a landing exposes its declared anchor and drains unfenced in-flight appends before its fences arm, so every append that could predate the anchor has already bumped or bumps behind the fence. tests/test_prism_landing_append_epoch.py drives all of it through the landing harness: the over-abandon (now lands, confirms, publishes), the fail-closed pairs at and inside the anchor, both fences (the bump placed between the advisory read and the fence-held one via phase:tip-height-rpc), a pruned history, and a candidate with no declared anchor. Every scenario is proved order-stable with assert_deterministic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
The retention test captured time.monotonic() and asserted that a silent reconnect cycle at exactly +TTL still resumes, relying on the store's strictly-greater expiry comparison. That boundary arithmetic is not exact for an arbitrary float base: (start + 900.0) - start rounds above 900.0 for roughly one in five thousand captured clock values, which flips the boundary cycle to expired and fails the test. CI hit this on the push run for the map commit while the pull_request run of the same commit passed. Every record and lookup in the test runs under frozen_clock, so the base never needs to be a real monotonic reading. Pin it to an exactly-representable constant, making the boundary arithmetic exact. Refs #145.
The anchor-scoped predicate reads recorded bumps, so it is only as sound as the guarantee that every append predating a landable window's anchor actually bumps. That guarantee had a hole: a seeded window's anchor is exposed to the append-side predates() checks only by the armed payout-ledger artifact, and the bump that consults that anchor also disarms the artifact. The live anchor set could then go empty while jobs from earlier windows were still landable, and a replay row genuinely predating such a candidate's declared anchor committed silently -- _record_late_visible_payout_append returned None, no bump, no stamp -- so the landing's fences read only harmless history and submitted an invalidated window. Demonstrated with a deterministic interleaving: the pre-#126-fix tree failed closed on it; the anchor-scoped tree submitted. Route every write to the armed-artifact slot through _replace_payout_ledger_artifact_locked, which folds the outgoing artifact's snapshot anchor into the monotone, never-retired published-job-window watermark before the slot moves. The fold runs under _job_cache_lock -- the same lock every predates() read takes -- so no append can observe the anchor gone with the watermark not yet raised. The invariant that now actually holds, and that the fence comments now state: the anchor-set maximum never decreases while any job is landable, so every append that predates a live or seeded anchor bumps and leaves a stamp for the fences to read. The watermark's permanence is what the per-anchor predicate makes affordable: the extra bumps a retired anchor forces come only from replay-shaped rows (the pending-commit floor keeps ordinary commits above every job anchor) and are forgiven per anchor rather than over-abandoning. Reword the authoritative-fence comment in block_finalization.py: it claimed the recorded stamps were "the whole history the block's window can still see", which held for bumps but not for predating appends until the fold above made every predating append bump. Also pin the recorded stamp's max(job_issued_at_ms, accepted_at_ms) choice, which every equal-stamp scenario left indistinguishable from min() or either stamp alone: a straddling row -- issued at or before the candidate's anchor, accepted after -- does not predate the window, and recording anything but the max would abandon a valid block, resurrecting the #126 over-abandon. tests/test_prism_landing_append_epoch.py gains the disarm-gap scenario (premise: the bump disarmed the artifact; verdict: nothing submitted) and the straddling pair (lands, and abandons once both stamps predate), all proved order-stable with assert_deterministic; the landing harness's append_late_visible_share learns an additive accepted_at_ms. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The harness PR (#148) was squash-merged, so 2.x.x carries tests/prism_landing_harness.py under a commit unrelated to this branch's history. The merge base therefore predates the file on both sides and git reports an add/add conflict for it. Resolved in favour of this branch: 2.x.x's copy is byte-identical to this branch's pre-change version of the file, and this branch's copy is that same file plus the additive accepted_at_ms kwarg the straddling-row tests need. Nothing from 2.x.x is dropped. Everything else auto-merged; the regions are disjoint. Verified both sides survived: this branch's anchor-set comment and the per-anchor fences in block_finalization.py, and 2.x.x's idempotent-confirm-replay handling from #146.
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.
Summary
Fixes the #126 over-abandon: a valid found block terminally abandoned as
append_epoch_stalewithout ever reaching qbitd, forfeiting the reward.The defect
The append-invalidation epoch is a single global counter. An append whose
share stamp is later than this candidate's declared anchor — but early enough
to predate some newer exposed anchor — bumps the counter, and the landing's
pre-offer fence reads
effective_append_epoch != live_append_epochandabandons. The counter cannot distinguish an append that invalidates this
candidate's window from one that does not. On the fallback submit path the
block is abandoned before any node offer.
The fix
Scope the epoch comparison per anchor:
max(job_issued_at_ms, accepted_at_ms), exactly the quantity the existingpredates-anchor comparison turns on — in a bounded map (256 entries; ample
against concurrent landings while failing closed once pruned) with a
retained-floor watermark.
fence-held read under
_payout_append_landing_fence_lock) now ask ananchor-scoped predicate: invalidated iff some bump in
(baseline, live]was caused by a row predating this candidate's declared anchor.
invalidated; unknown anchor, baseline older than the retained history,
backwards epoch, or any gap in the retained window → invalidated.
a candidate that reached qbitd paid its as-issued window when it landed),
as is the
collection_onlyexemption.found_block["anchor_job_issued_at_ms"],the same reader
_expose_inflight_scan_anchoruses, so the fences and theexposure cannot drift apart. Declared on
BlockFinalizationPort, forwardedfrom
PrismCoordinatornext to the neighbouring epoch helpers.The ordering argument that makes this sound was verified on this tree, not
assumed:
_land_candidateexposes the declared anchor and drains unfencedin-flight appends before its fences arm, so any append that could predate
the anchor has already bumped — or takes the landing fence and bumps behind
it. The stamp is written in the same locked region as the increment it
belongs to, so no bump can skip the history a landing later reads.
Closing the disarm gap
A deep review of the above found the ordering argument had a hole, and the
second commit closes it. The anchor-scoped predicate reads recorded bumps,
so it is only as sound as the guarantee that every append predating a
landable window's anchor actually bumps. That guarantee did not hold: a
seeded window's anchor is exposed to the append-side
predates()checks onlyby the armed payout-ledger artifact, and the bump that consults that anchor
also disarms the artifact. The live anchor set could then go empty while jobs
from earlier windows were still landable, and a replay row genuinely
predating such a candidate's declared anchor committed silently — no
bump, no stamp — so the fences read only harmless history and submitted an
invalidated window. On that interleaving the anchor-scoped tree submitted
where the pre-fix tree had failed closed.
The fix makes the anchor watermark universal. Every write to the armed
artifact slot now routes through
_replace_payout_ledger_artifact_locked,which folds the outgoing artifact's
snapshot_anchor_msinto themonotone, never-retired
_payout_published_job_window_anchor_msbefore theslot moves. Folding the outgoing rather than the incoming anchor is what
keeps the anchor-set maximum monotone even when a lower-anchored artifact
replaces a higher one. The fold runs under
_job_cache_lock— the same lockevery
predates()read takes — so no append can observe the anchor gone withthe watermark not yet raised.
The invariant that now actually holds, and that the fence comments now state:
the anchor-set maximum never decreases while any job is landable, so
every append predating a live or seeded anchor bumps and leaves a stamp for
the fences to read. The watermark's permanence is what the per-anchor
predicate makes affordable — the extra bumps a retired anchor forces come
only from replay-shaped rows (the pending-commit floor keeps ordinary share
commits above every job anchor) and are forgiven per anchor rather than
over-abandoning.
The same commit rewords the authoritative-fence comment in
block_finalization.py, which claimed the recorded stamps were "the wholehistory the block's window can still see" — true for bumps, but not for
predating appends until the fold above made every predating append bump.
Regression coverage
tests/test_prism_landing_append_epoch.py, 10 scenarios on theLandingHarness(from the stacked harness PR), all proved order-stable withassert_deterministic(..., harness_factory=LandingHarness):qbitd, confirms, and is not abandoned. Fails before the fix with
submitted == [](verified by revertingblock_finalization.pyalone;the stamp recording is inert without the fence change).
before, the candidate's declared anchor still abandons with nothing
submitted.
read and the fence-held read is terminal when it predates the candidate's
anchor, harmless when it does not.
next append. Fails before the fold by submitting: the second append
returns
None(no live anchor covers it), the fences see only the firstbump's harmless stamp, and the block is submitted and confirmed with an
envelope written — a coinbase whose window omitted a durable share. After
the fold the second append bumps and the landing abandons.
accepted after. It does not predate the candidate's window, so the block
must still land.
both stamps at or before the anchor.
Scenarios 7 and 8 pin the recorded stamp's
max(job_issued_at_ms, accepted_at_ms)choice, which every equal-stamp scenario above leftindistinguishable from
min()or either stamp alone. Under amin()mutantscenario 7 fails by abandoning a valid block — #126's over-abandon back
again. The harness's
append_late_visible_sharegained an additiveaccepted_at_mskwarg to express the straddle.Testing
GIT_CONFIG_GLOBAL=/dev/null python3 -m unittest tests.test_prism_landing_append_epoch tests.test_prism_block_candidates tests.test_prism_block_finalization tests.test_prism_payout_state tests.test_prism_landing_interleavings tests.test_prism_landing_budget_watchdog tests.test_prism_landing_harness_model— 318 tests, OK.tests/test_prism_*sweep on Linux — 1812 tests, OK. (On macOS the samesweep shows the five known
os.splicefailures intest_prism_job_builder,which are platform-local and pass on Linux.)
python -m compileallon the CI Python — clean;bash -noverall tracked shell scripts — clean.
The harness PR (#148) has merged, so this now targets
2.x.xdirectly.Fixes #126.
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.