RFC 0027: The commit-log seam
- Date: 2026-11-03
Summary
Section titled “Summary”RFC 0024 fixed the boundary with the commit-log library at the slot: the library hands the catalog SlateDB’s write-ahead-log plugs and a committer that races one sequence, and the catalog drives the loop above them — when to offer, what to fold after a loss, when to flush and truncate, which process takes the store and when it gives it back. That loop has since been written again in the library, as a write any SlateDB embedder shares, and the library’s copy is the one simulated under faults. Two loops is one too many, and multi-writer is not this crate’s concern: moraine is single-writer, and the library is what a deployment adds to let a second process commit. This RFC moves the whole loop out of moraine and leaves a seam: the trait the library implements, the hooks the catalog supplies, and the two places the catalog calls through. Nothing on disk changes, and the open build stays complete alone.
- The loop is the library’s, entirely. Offers, folds after a loss, the fold cadence, flushing and truncation, the idle poll, taking the store and giving it back, retries, and the maintenance verbs over them. Moraine calls the seam; it decides nothing about slots.
- The seam is as small as the catalog’s own needs. Committing a batch, adopting at open, closing, and opening the store for the library when it asks. Nothing else crosses.
- Parity. Every behaviour RFC 0024 specifies for the folder, the committer, the sprint, and the fence holds unchanged for a catalog on the seam, and the test obligations of RFC 0024 pass unchanged in meaning.
- The catalog still opens its own stores. Settings, caches, the segment extractor, and the retrying transport are the catalog’s; the library never opens a SlateDB store. It is handed the one the catalog opened, with the session it was opened over.
- Link, do not fork; complete alone. As RFC 0024.
Non-goals:
- The protocol and the store’s internals, as before.
- Changing what a slot carries. The two classification fields and the id ride the batch exactly as they ride the envelope today.
Design
Section titled “Design”Why a seam at all
Section titled “Why a seam at all”The open catalog is on the multi-writer path whenever a provider is
registered: its submit_batch is what stages a batch and asks whether it
committed, and its open paths are what build the SlateDB store and reader
with the catalog’s settings. A build that links the library needs those
two places to call through to it, and a fork is ruled out. The seam is
the smallest declaration of those two places.
The commit store
Section titled “The commit store”write_at is the whole commit path. It offers the commit as the slot at
sequence, conditional on the log holding nothing there. On a win the
library’s store has applied the slot, or has been served it, and a published
durable checkpoint covers it; on a loss it
has folded, or been served, everything that beat the offer, and through
is how far it reaches. The catalog never commits a logged batch into
SlateDB itself and never sees the slots it lost to: it re-prepares against
the store, which is what it does today. The sequence may be at most one
past what the library’s store knows, which is what the head stamp’s
counter implies for a batch prepared against a current head; a stale head
names a taken slot and loses.
adopted and adopt serve the open path alone: a read-write open on a
catalog not yet on the log flushes, then adopts with the format stamp, and
folds whichever stamp won — the library’s, now. fold_tail folds what has
landed past the store’s cursor into a held store, or refreshes a followed
reader, and returns how many slots that was; demoting uses it to fold the
tail and then to prove no committer is still writing. close writes out
and lets go.
SlotCommit keeps its shape — the transaction id, the validated head,
changes_made, the writes — and the library carries all four in the slot
as it does now. The trait is Any so the crate that implements it can
reach its own store behind a catalog, for everything the seam does not
carry.
The provider
Section titled “The provider”A session is the cursor one open shares with its plug: the provider
mints it, the catalog builds the SlateDB store or reader over the plug it
yields, and hands both back to the provider, which recognizes the session
as its own and builds the commit store over it. This replaces
CommitLogProvider::session and the per-path plug methods of RFC 0024.
A writer session answers adopted before the store is built: the plug
replays the log in place of the store’s own, so a store on a log nobody
has adopted drains its own log into the tree first, as RFC 0024’s open
does.
garbage_collector stays, for maintenance’s collector, and so does
external_compaction. Registration and the per-open option are as in RFC
0024.
What the catalog supplies
Section titled “What the catalog supplies”TakeStoreis how the library opens the store as its writer, for the cadence fold and for holding it. The catalog implements it with its writer open: a fresh writer session from the provider, the store built over its plug, and the open’s replay as the fold. It declines — returnsNone— when the store is at its limit of unmerged files and nothing is compacting, asfold_sprintdoes today; the library then folds nothing and the log grows until a compactor serves the lake, as RFC 0024 specifies.inspectis the one-head-stamp-per-slot check of RFC 0024’s “The sequence a committer races”, run by the library on every slot before it folds it; a refused slot is not folded and the fold stops with the refusal, as corruption, as today.pathandobject_storeare where the store lives, as it was opened: what the library’s collector run needs to read the manifests and the checkpoints that pin the log.CommitLogSettingscarries the existingcommit_log_*options — the fold cadence, the flush count, the retention, the fold poll, and the committer’s poll — as values the library reads. The catalog interprets none of them.
CatalogHooks::publish(&db) flushes WAL durability and publishes the current
read checkpoint using the catalog’s path and object store. The library calls it
before reporting a won slot; a publication failure after the slot lands is an
unknown commit outcome, never permission to offer that mutation again. A holder
publishes its applied state; a follower takes a temporary writer through
TakeStore::take_for_publication, verifies replay covers its won slot, publishes,
and closes without flushing an L0 file. The default publication take delegates
to take and reports a refusal; the catalog’s implementation bypasses the L0
ceiling because publication does not write an L0 file. A subsequent flush must
republish the flushed state before releasing the writer so new fixed readers do
not replay an obsolete WAL. The library continues to own all offer, fold, and
publication sequencing.
The catalog also exposes two things for the enterprise maintenance crate, which today reaches the loop through catalog verbs:
Catalog::commit_log_hooks(object_store, options) -> CatalogHooks: the hooks as a live catalog would hand them, so a maintenance process can fold a lake once, measure its unfolded tail, or hold its store with the library and no live catalog; the inspection is the catalog’s to supply, since it reads the catalog’s own keys.Catalog::store_taker(object_store, options) -> Arc<dyn TakeStore>is itstakealone.Catalog::commit_store(&self) -> Option<Arc<dyn CommitStore>>: the store behind a live catalog, for the same crate to reach throughAny.
What moves, what goes
Section titled “What moves, what goes”Moves into the library, and is deleted from commit_log.rs: the
holder and its idle poll; the fold after a loss, for either side; the
flush-by-count and the collector run; adoption’s folding of whichever
stamp won; promotion, demotion, and the fence that demotes; the catch-up a
committer over a reader waits on; the fold cadence and the one-shot fold.
Leaves the catalog’s API: Catalog::take_store, Catalog::fold_sprint,
Catalog::sprint_if_stalled, Catalog::unfolded_slots, and
Catalog::unfolded_slots_of. Their only callers are in the enterprise
maintenance crate, which performs them with the library over the taker
and the store above. The read-only tailing attach and the committer’s poll
interval stay as options; they configure the seam, not the loop.
Stays in this repository: the traits above and their in-tree test
fixture; the sequence a batch races, derived from its head stamp’s
counter and the adoption’s recorded origin; the format stamp and
migration; submit_batch’s staging against a commit session and its
lost-race outcome; the conflict the DuckDB path surfaces; the L0-ceiling
decline in TakeStore.
Errors. The library reports a won slot its store could not take — a
holder fenced or refusing, a follower whose reader has closed — as a
distinct outcome carrying the sequence. The in-tree mapping surfaces it as
Error::CommitOutcomeUnknown naming the sequence, which is what the
catalog reports for that case today: the slot is committed, and a reopen
folds it. A fence on a held store is Error::Fenced; on a taken store it
is absorbed by the library, the store is given back, and the write that
met it returns as landed.
Compatibility
Section titled “Compatibility”The provider trait and the five catalog verbs are public, so this is a breaking change to the crate’s API, released as a minor bump as the crate’s 0.x versioning allows and named in the changelog. The only implementation of the provider known is the enterprise glue crate, and the only callers of the verbs are the enterprise maintenance crate; both move with this RFC. The in-tree provider is a test fixture. The old trait is not kept beside the new one: an adapter from the old committer to the new store would need the loop this RFC deletes, and a deprecated trait nobody implements is weight without a reader.
Migration
Section titled “Migration”The on-disk format, the slot contents, the WAL-id base, and the format stamp are unchanged. A binary on either boundary opens a store the other wrote. The change is in-process: the enterprise glue crate implements the new provider over the library’s store, the enterprise maintenance crate performs its verbs through the taker and the store, and the open build’s in-tree provider implements the seam over the in-tree log, which gains the same loop in miniature for the tests.
Test obligations
Section titled “Test obligations”Every obligation of RFC 0024 stands, re-pointed at the seam. In addition:
- An in-tree commit store on each side: a holder applies its win and folds a loss; a follower is served its win and waits for a loss. The in-tree store holds only for adoption, demotion, and the cadence’s take; promotion, the fence that demotes it, and a won slot the store could not take are the library’s, tested where the library is.
- The cadence: with no holder alive, the follower that lands slot
n × fold_everytakes the store through the catalog’s taker, folds, writes out, and lets go, and the manifest’s replay point moves to it. - The fence that demotes, in the library: a promoted follower another process fences drops back to following and its next commit lands through the log.
- A commit the store could not take surfaces as an unknown outcome naming its sequence, and a reopen folds it; the library’s error, mapped by the provider that carries it.
- The sequence invariant: a batch prepared against a stale head names a taken slot and loses, never being refused as past the store; a batch prepared against the current head of a committer whose reader was served further than its view wins at the sequence its head implies. A demoted lake is refused re-adoption, as RFC 0001 of the library specifies.
- The two-process harness of RFC 0024 runs unchanged over the seam.
Alternatives considered
Section titled “Alternatives considered”- Keep both loops and converge them by review. Two implementations of one protocol drift, and only one of them is simulated under faults. Rejected: this RFC exists because that happened.
- Move moraine’s loop into the library instead. The library’s loop is the one written for any SlateDB embedder, hardened against a faulty store, and covered by the admin and clone paths; moraine’s is the one with catalog knowledge woven in. The generic one is the keeper.
- A store-shaped trait carrying the maintenance verbs too. An earlier
draft of this RFC. It kept
fold_now,take_store,release_store,flushand the handle accessors on the open trait so the catalog’s verbs could pass through. Their callers are all enterprise code, which can reach the library’s store directly; a seam that carries only what the catalog itself needs is smaller and says plainly that multi-writer is not the open crate’s concern. - No seam: an enterprise build that patches the two call sites. A fork by another name, and RFC 0024’s first goal rules it out.
- Have the library open SlateDB stores. It would need the catalog’s settings, caches, segment extractor, and transport, or a builder hook that amounts to the same thing. The catalog opens; the library is handed the result with its session.