[glue] Remove Shared database ownership - #4534
Draft
danlaine wants to merge 14 commits into
Draft
Conversation
Benchmark resultsRegressions: ✅ `qmdb::merkleize/v=any::unordered::fixed::mmr k=10000 ch=false s=true cc=true` (2/2 gates passed)
✅ `qmdb::merkleize/v=current::ordered::fixed::mmb chunk=256 k=10000 ch=false s=true cc=true` (2/2 gates passed)
Baseline commit(s): |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
commonware-mcp | ea467df | Aug 20 2026, 02:04 AM |
danlaine
force-pushed
the
danlaine/glue-owned-db
branch
from
August 19, 2026 17:26
c0c9088 to
401af58
Compare
Deploying monorepo with
|
| Latest commit: |
ea467df
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://5582c92f.monorepo-eu0.pages.dev |
| Branch Preview URL: | https://danlaine-glue-owned-db.monorepo-eu0.pages.dev |
danlaine
force-pushed
the
danlaine/glue-owned-db
branch
from
August 19, 2026 17:43
401af58 to
d882bb8
Compare
This was referenced Aug 19, 2026
danlaine
force-pushed
the
danlaine/glue-owned-db
branch
2 times, most recently
from
August 19, 2026 18:48
550ce13 to
6e3e685
Compare
danlaine
force-pushed
the
danlaine/glue-owned-db
branch
from
August 19, 2026 19:27
447c7ed to
f89d48c
Compare
danlaine
force-pushed
the
danlaine/glue-owned-db
branch
from
August 19, 2026 20:13
f89d48c to
b427504
Compare
danlaine
force-pushed
the
danlaine/glue-owned-db
branch
from
August 19, 2026 20:41
b427504 to
0f9f55a
Compare
Shared database ownership
danlaine
force-pushed
the
danlaine/glue-owned-db
branch
from
August 19, 2026 21:00
0f9f55a to
c40d026
Compare
danlaine
force-pushed
the
danlaine/glue-owned-db
branch
2 times, most recently
from
August 19, 2026 21:31
2de62b5 to
a1c4965
Compare
danlaine
force-pushed
the
danlaine/glue-owned-db
branch
from
August 19, 2026 22:14
a1c4965 to
2f5ba77
Compare
The actor now owns its databases outright and doomed verification jobs
find out from the database itself, through a typed error, instead of
being classified and cancelled by the scheduler.
- Shared<DB> (Arc<RwLock<Option<DB>>>), WriteSlot, BatchContext, and the
take/put mutation dance are gone. split(db) -> (Writer, Reader) gives
the actor a sole non-Clone mutation handle whose mutations read as
by-value code; jobs and peer serving hold cloneable Readers whose
guards cover exactly one storage call.
- The Disposition/FinalizationBoundary/VerificationProgress
classification, the invalidation channels, and the
quiesce-before-apply barrier are deleted. Finalization applies
immediately; a job on a losing fork dies at its next database read,
which returns StaleRead, or when its requester drops the response
channel.
- Application::{propose, verify, apply} return Result<_, ExecutionError>
({Stale, Shutdown, Fatal}); applications propagate storage errors with
? and never interpret them. Every storage failure maps to Fatal and
panics, proposals included; Shutdown is application-signaled only and
never inferred from storage errors. The verifier maps Stale by
re-entering its canonical-state recheck (a block that finalized itself
mid-verify answers true, competing blocks answer false), and an
invalid-looking attempt whose anchor moved mid-walk retries instead of
answering false.
- fork_batches refuses while a finalization is mid-flight
(ExecutionState's finalizing window) and re-checks its anchor after
per-database forks, so a batch set can never straddle a boundary.
PrepareFailure::Invalid re-runs check_processed before answering
false.
- Mailbox::subscribe_databases is gone: the actor no longer hands its
databases out, and peers observe only the published snapshot. The
syncer delivers its artifact exactly once on the completion channel
(SyncResult loses Clone; update_targets returns UpdateOutcome), and
DatabaseSet::committed_targets becomes applied_targets.
- DatabaseSet mutations take self and return the successor; ManagedDb
mirrors. Mocks, the reshare harness, and the example migrate from
.unwrap() to ?.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q92PhqGoHbWCGHCsBJ7EBU
The writer-outlives-readers rule was documentation only. Dropping the writer now latches the cell closed, and later leases park instead of answering from a database that can never advance, mirroring the poisoned case. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q92PhqGoHbWCGHCsBJ7EBU
The Fatal doc claimed only Ok(None) declines a proposal and the propose doc claimed every error declines. Neither was true. Spell out the matrix, declines on Ok(None), Shutdown, and Stale, a panic on Fatal, and pin verify's verdict boundary to the applied anchor move. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q92PhqGoHbWCGHCsBJ7EBU
An application still parked inside apply when cooperative shutdown began used to park the finalize future forever. The park sat inside the processing loop's step, so the loop never released its own stop signal and runtime shutdown deadlocked on it. The stop signal now covers the application awaits: the processing loop selects it around the finalize drive and the proposal drive, and the sync handoff does the same around its applies. On stop the actor drops the in-flight future and exits without acknowledging the block, so marshal redelivers it after a restart. Mailbox::verify parks on a lost response channel instead of panicking with a message blaming the actor, which also covers verifications the exiting actor drops. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q92PhqGoHbWCGHCsBJ7EBU
A verification that completed after a finalization swept its branch answered false, even though the execution matched the block's commitments on the supplied ancestry. Validity is relative to the supplied inputs, so a refused cache discards the verified state without changing the verdict, which now stays true. This also drops the classifier round trip through marshal on the refusal path, which could park forever on incomplete ancestry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q92PhqGoHbWCGHCsBJ7EBU
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q92PhqGoHbWCGHCsBJ7EBU
The cell hands out read guards. One word for the concept, matching the type's name. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q92PhqGoHbWCGHCsBJ7EBU
CI's newer clippy flags the sync helper's return type as too complex. A shared alias satisfies it and names the tuple for both spawn helpers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q92PhqGoHbWCGHCsBJ7EBU
Scope the reentrancy and never-cancels claims to what actually holds, name every reader holder in the set doc, unify a duplicated panic message and test helper, and tidy two log lines and a syncer comment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q92PhqGoHbWCGHCsBJ7EBU
The finalize paths renamed the flush handle binding to sync for no reason. Restore the name the callers and docs use. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q92PhqGoHbWCGHCsBJ7EBU
danlaine
force-pushed
the
danlaine/glue-owned-db
branch
from
August 19, 2026 22:42
2f5ba77 to
559cc14
Compare
Codecov Report❌ Patch coverage is @@ Coverage Diff @@
## danlaine/db-snapshot-serving-2 #4534 +/- ##
==================================================================
- Coverage 95.52% 95.52% -0.01%
==================================================================
Files 611 612 +1
Lines 279203 278848 -355
Branches 6663 6641 -22
==================================================================
- Hits 266713 266360 -353
- Misses 10295 10296 +1
+ Partials 2195 2192 -3
... and 10 files with indirect coverage changes Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
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.
Part 4 of a 4-PR stack: #4531 → #4532 → #4533 → #4534.
Background
Glue's stateful actor drives consensus decisions into the databases. Consensus asks it to verify candidate blocks (is this block valid on its branch?), and each verification runs as an independently polled job executing the application's state transition on speculative batches (#4531). Completed verdicts are cached in a pending map keyed by block digest, so duplicate requests and children forking from a verified block reuse them. Marshal delivers finalized blocks. Applying one moves the anchor, the last finalized block reflected in the databases. Blocks form branches, and when a block finalizes, every competing branch has lost. Before this PR, the actor shared its databases through a lock, and a scheduler classified, cancelled, and waited out every in-flight job before each finalization could apply.
Summary
The stateful actor owns its databases outright, and a verification job whose branch loses (i.e. is invalid because a non-ancestor was committed) finds out from the database itself, through a typed error instead of being classified, cancelled, and awaited by the scheduler. This is the payoff the three preceding PRs set up.
Ownership
Shared<DB>(anArc<RwLock<Option<DB>>>),WriteSlot,BatchContext, and the take-the-value-out-put-it-back mutation dance are deleted. In their place,split(db)returns aWriterand aReader(glue/src/stateful/db/cell.rs). TheWriteris the actor's field and the sole mutation authority. It is notClone, and its mutations consume and produce the database, so they read as ordinary by-value code with no reachable poisoned state except a mutation future dropped mid-flight, which is actor shutdown. Dropping theWritercloses the cell, so leftover readers park instead of silently serving a database that can never advance.ReaderisClone. Verification jobs and test probes hold one, and a read guard covers exactly one storage call. The lock is write-preferring, so a readers cannot starve finalization.Cancellation moves from the scheduler to the data
The
Disposition/FinalizationBoundary/VerificationProgressclassification, the invalidation channels, and the quiesce-before-apply barrier are deleted. Finalization applies immediately, never waiting for doomed jobs to unwind. A job on a losing fork now dies at whichever comes first: its next database read returnsStaleReadthrough the gates, or its requester (consensus) drops the response channel it stopped caring about. What a doomed job can waste is bounded by one inter-read gap of application compute. Applications are no longer obligated to write cancellation-tolerant futures, because glue no longer cancels them -- only requester-drop remains, the ordinary async contract.The error channel
Application::{propose, verify, apply}returnResult<_, ExecutionError>, whereExecutionErroris{Stale, Fatal(String)}with aFromimpl for storage errors -- applications write?where they wrote.unwrap()and never interpret the error. Glue, the only layer that knows what each case means, does the mapping.Stalemeans a finalization landed mid-work. Wherever an honest answer exists, glue re-answers from the new canonical chain rather than guessing:Fatalis any other storage failure and panics everywhere, proposals included: a node that has observed broken storage must not keep serving. A closed storage channel maps here too -- the honest outcome when a backend worker dies, instead of a mislabeled cancellation or a silent wedge.Shutdown is the actor's job, not the application's: the loop's stop signal covers the application awaits (the finalize drive, the proposal drive, and the sync handoff's applies), so on stop the actor drops the in-flight future and exits cleanly, leaving the block unacknowledged so marshal redelivers it after restart -- exiting releases the actor's own stop signal instead of parking on the very future graceful shutdown waits for. The stop arm is ordered first, so a
Fatalcaused by shutdown tearing down the application's dependencies is never observed, while aFatalthat arrives before the stop signal is a real failure and panics as it should. A verification in flight at shutdown quietly never resolves: its caller parks, and consensus reaps it on teardown.The finalizing window
fork_batchesrefuses to fork while a finalization is mid-flight and re-checks its anchor after taking per-database forks, so a batch set can never straddle a finalization boundary (each per-database gate would pass individually while the set as a whole mixes two states). Stale retries park until the anchor moves past what they have already seen, instead of spinning.Consequences elsewhere
Mailbox::subscribe_databasesis gone. The actor no longer hands its databases to anyone; peers observe only the published snapshot.SyncResultlosesClone, andupdate_targetsreturns anUpdateOutcomeenum instead of mutating shared state.DatabaseSet::committed_targetsbecomesapplied_targets, matching what it returns..unwrap()to?.