Re-land payout-artifact reuse with event-driven validity and a kill-switch - #107
Merged
Conversation
djh58
force-pushed
the
dan/prism-ledger-window-reuse
branch
from
August 3, 2026 14:22
6cfbf5c to
982b28d
Compare
The 2026-07-29 rollback and the 2026-07-30 brownout were the same arithmetic error at different anchors: a wall-clock validity budget shorter than the production interval between consecutive shared builds, on an object whose rebuild pays a multi-second reward-window walk. Round 1 measured the budget from the pre-walk anchor, so every artifact arrived born expired; round 2 moved it to install time, so the budget expired between consecutive builds and every build fell back to the synchronous walk while the re-arm worker walked the same window in parallel. Wall-clock time is no longer a validity input below the audit ceiling: - The reuse probe serves any armed artifact whose payout generation, difficulty, and balances fences hold and whose declared anchor is inside PRISM_PAYOUT_ARTIFACT_MAX_ANCHOR_AGE_SECONDS. Past the new PRISM_PAYOUT_ARTIFACT_REANCHOR_SECONDS floor it schedules the debounced background re-anchor and KEEPS SERVING; post-anchor shares settle in the next window by construction, so a re-anchor is freshness maintenance, not correctness. - PRISM_PAYOUT_ARTIFACT_REUSE_STALENESS_SECONDS is removed. Cached no-artifact ready bundles gate on the re-anchor floor instead (their declared anchor is already ceiling-gated, and the bundle-cache TTL bounds them tighter in production). - PRISM_PAYOUT_ARTIFACT_REUSE (default on) is a master kill-switch: off restores the pre-reuse deployment exactly -- probe refuses, no background walks -- so a production regression is an env flip instead of an emergency rollback (both prior incidents required redeploys). Test changes mirror the semantics: install-age rejection tests become serve-past-install-age brownout regressions, rearm/debounce and landed-preview suppression drive aging through the anchor, in-flight retention ages through the ceiling, and the kill-switch has dedicated coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three observability gaps the incident review had to work around: - The reuse probe was invisible: nothing distinguished served reuse from ceiling rejections. served_reuse / probe_rejected_ceiling join the payout-artifact event family (recorded outside the cache lock, matching the family's lock ordering). - current_tip_job_coverage demands template-generation equality and template object identity, so under generation churn it reads 0.0 essentially continuously even while every miner holds semantically current work. The new semantic_current_work_ratio gauge compares template fingerprint and payout generation only. The strict gauge is untouched -- it drives mining-health gating and stays fail-closed. - Slow waves logged nothing attributable: latency had to be reconstructed by pairing tip-observation lines with "refreshed N client job(s)" lines, which cannot see superseded passes. Waves over 1s now log outcome, refreshed count, pass count, and elapsed time in one line. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The job-build executor and payout-artifact preparation workers never pass through the per-client delivery entry points that clear and flush the thread-local phase dict, so every wave-level phase they accrued -- template, ledger, assembly, bundle, payout_artifact -- exported as exactly 0.0 forever despite continuous template fetches and multi-second window walks. The bundle-build cost had to be discovered from the separate tip_refresh_bundle_phase histograms and ad-hoc profiling because this family was blind. _flush_job_build_phases folds phase seconds only (build counts and the duration histogram stay per-delivery) and clears the dict so a long-lived worker cannot double-report an earlier request's accruals. Wired at the two worker boundaries: _execute_job_build_request and each _payout_artifact_preparation_loop iteration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
djh58
marked this pull request as ready for review
August 3, 2026 14:24
Review verdicts: the revert purity, round-2 diagnosis, serve-window correctness, test archaeology, and zero-regression claims survived; the kill-switch equivalence claim was refuted as written, and the review surfaced one genuinely dangerous miss plus four smaller ones. This commit addresses all of them: - Startup validation (the dangerous miss): a ceiling at or below what the background re-anchor can beat re-creates the 2026-07-30 walk-per-build shape with a single env var. __init__ now refuses MAX_ANCHOR_AGE <= 2x REANCHOR via validate_payout_artifact_age_bounds, with direct test coverage. - Kill-switch now gates ARMING too: _install_payout_ledger_artifact, the payout-publication pointer swap, and sync-build window seeding all no-op while disabled. Previously artifacts kept arming with the switch off, churning install events and re-keying the idle-bundle fast path to a generation the probe refuses -- a permanent idle cache miss in the disabled state. Comments and the PR description no longer claim the disabled state matches the pre-reuse deployment "exactly": it restores pre-reuse delivery economics while the synchronous path keeps the re-landed anchor-selection semantics. - prior_balances_sha256 is memoized on the artifact at construction and re-derived at the accepted-preview balance patch; the reuse probe and both reused-build fences stop paying O(accounts) JSON canonicalization per serving decision. The restamp-race test now drives the un-memoized fallback path it was written to protect. - The idle issuance path enforces the anchor ceiling like every other NEW serving decision (it was the one route that could issue a window past the ceiling), and the probe docstring states the in-flight-retention exception instead of overselling the ceiling. - probe_past_floor joins the event family so the canary can distinguish serving-past-floor from never-crossing-the-floor; rearm_scheduled alone under-counts through debounce/suppression. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Problem
Tip-change refresh latency is dominated by the shared bundle build, and profiling attributes the build almost entirely to the synchronous reward-window walk — a multi-second ledger read that runs far more often than payout state actually changes. Every unnecessary walk extends the window in which miners grind a stale tip (forfeiting block solves as
tip_movedabandons) and queues initial-job builds behind the single-flight future, which is what starves the stratum health probes.#102 and #103 already built the reuse fix and were both emergency-rolled-back (#104 abandoned the line). Re-reading both incidents against production cadences shows they were the same arithmetic error at different anchors: a wall-clock validity budget shorter than the interval between consecutive shared builds, applied to an object whose rebuild pays a multi-second walk.
Correctness never needed the wall-clock knob: the payout-generation and balances-sha fences invalidate on real payout events, and post-anchor shares settle in the next window by construction (the anchor contract #102 documented).
Change
Four commits on top of
1.x.x:git revertof the abandonment commit (~1,800 lines, zero hand-edited). It restores the reuse line to the exact tree previously reviewed and merged as Survive production build latency in payout-artifact reuse (2026-07-29 re-land) #103 — which already included Make payout-ledger-artifact reuse anchor-scoped so it survives production share rates #102 and the round-3 hardening fromartifact-reuse-latency-hardening(install-site freshness stamps, probe restamp admission, publication born-expired gate, event counters).PRISM_PAYOUT_ARTIFACT_MAX_ANCHOR_AGE_SECONDS. Past the newPRISM_PAYOUT_ARTIFACT_REANCHOR_SECONDSfloor it schedules the debounced background re-anchor and keeps serving.PRISM_PAYOUT_ARTIFACT_REUSE_STALENESS_SECONDSis removed; cached no-artifact bundles gate on the re-anchor floor (their declared anchor is already ceiling-gated, and the bundle-cache TTL bounds them tighter in production).PRISM_PAYOUT_ARTIFACT_REUSE(default on) is a master kill-switch: off disables the reuse line — probe refusals, no background walks, no arming — restoring pre-reuse delivery economics, while the synchronous build path keeps the re-landed anchor-selection semantics. Both prior incidents required emergency redeploys; a third delivery regression is an env flip.served_reuse/probe_rejected_ceilingjoin the payout-artifact event family; asemantic_current_work_ratiogauge (fingerprint + payout generation) lands beside the strict object-identity coverage gauge (which reads 0.0 essentially continuously under generation churn, but stays untouched because it drives mining-health gating); waves >1s log one attributable line.template,ledger,assembly,bundle) exported exactly 0.0 despite continuous fetches and multi-second walks because those threads never passed through the per-client flush points.probe_past_floorcanary event so serving-past-floor is observable independently ofrearm_scheduled.Why re-land via revert instead of fresh commits. Commit 1 being a pure revert means the reuse base is byte-identical to code that was already reviewed twice and hardened through three review rounds — reviewers can verify zero drift by diffing commit 1's tree against the #103 merge commit (
cd32c13); the only differences are the unrelated #97/#106 changes that landed after #104. The genuine review surface of this PR is therefore commits 2–4: the validity-semantics delta plus the observability additions — exactly the difference between "what browned out" and "what this ships." One consequence to be explicit about: commit 1 alone reintroduces the browned-out behavior; the PR is atomic, and commit 2's event-driven semantics plus the kill-switch are what make the third landing safe. (A squash merge collapses this distinction, but the commit boundaries are what make the review tractable.)Tests
tests/test_prism_coordinator_job_cache.py: 174/174. Install-age rejection tests became serve-past-install-age brownout regressions; rearm/debounce and landed-preview suppression age through the anchor; in-flight retention ages through the ceiling; the kill-switch has dedicated coverage (probe refusal + zero background scheduling + clean re-enable).Full suite: 1,390 tests — failure set byte-identical to clean
1.x.xin the same environment (30 pre-existing sandbox failures — 27 Makefile-lifecycle errors from the missing make binary plus 3 deploy-pin failures; zero regressions).cargo test -p qbit-prismnot run locally (no toolchain in the build sandbox; no crate files touched) — CI covers it.Deploy notes
PRISM_PAYOUT_ARTIFACT_REUSE=0+ restart. No redeploy.qbit_prism_stale_job_abandons_total{class="balance_stale"}must stay 0.Expected outcome (verifiable within hours of deploy)
served_reusedominating the event family with full reads tracking the payout-event rate;ledger_snapshotdurations collapsing between payout events; tip→refresh latency dropping to low single-digit seconds except immediately after pool-won blocks (until a follow-up speculative pre-build);tip_movedabandons and health-probe timeouts falling toward their propagation-and-noise floors.🤖 Generated with Claude Code