docs: report what Gumbo needs for durable execution frameworks - #19
Merged
Conversation
Writing the distribution design surfaced a set of log-layer defects and missing primitives. None are Catalyst-specific — they are what any durable execution engine needs, which is why Boudin sits behind several of the same limitations. Collected here with the evidence. Every defect was measured rather than inferred. Four are recorded: per-process localId assignment (two JVMs both assigned 0,1,2 to one stream), a per-process index that clobbers (3 of 6 appends reported, though all six are physically on disk), no guard against a second process opening a live log, and no version-keyed read. That last one is not hypothetical. It is corrupting state in shipped Catalyst code today: every Gumbo read entry point is keyed on the global seqnum, so Catalyst's per-execution tail read passes a localId into a seqnum-keyed API. The two coincide only when the log holds a single stream — true in every test, false in every production deployment. Measured end-to-end through the snapshot warm path: 43 timeline steps with one execution in the log, 51 with two. The report also states what already works, because the defects are narrower than they sound: durability is correct (fsync on append, group commit, FDB commits before resolving), the Boki tag model is right and needs no change, atomic multi-tag append already exists and is genuinely differentiating, the KV already exists and is already load-bearing, and the distributed sequencer pattern is already demonstrated — most fixes are that pattern applied at a different granularity. Ordered by blocking-ness times cost rather than elegance, which puts a fail-fast directory lock first (smallest change, converts silent corruption into a loud error) and the rename to streamVersion third (cheap now, expensive once new APIs are written against the old name). Ends with the five tests that would have caught all of this. The common thread is that every existing test uses one tag in a fresh log — the single configuration where a per-stream version and a global sequence number are indistinguishable, which hides three of the four defects at once. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G54yVgLZGZ3mzBv4kPc9F3
D1 noted that localId was repurposed from Boki's per-engine semantics to a per-tag cursor without changing how it is assigned, but left implicit whether the fix is to realign it with Boki or to stop exposing it. Record the answer. Boki's localid and the cursor Catalyst consumes are different quantities that Gumbo merged into one field: per-engine vs per-tag, internal write-path detail vs external client contract, superseded by seqnum vs permanent. Realigning with Boki would make localId correct and useless — the version-keyed read (D4/A2) and the conditional-append fence (A1) would each still need a number of their own. Decision: drop localId from the public API and expose a per-tag, storage-owned streamVersion, dense rather than the tag's latest global seqnum. Dense avoids a log migration (a seeded counter continues existing sequences; redefining the exposed version as seqnum would silently invalidate persisted cursors such as Catalyst's snapshot throughSeq) and keeps positions human-readable. Supporting fact, verified against the tree: Catalyst does no arithmetic on seq, only ordering comparisons, so the choice is free on the client side. A1's fence conditions on this same streamVersion, so conditional append needs no separate quantity. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G54yVgLZGZ3mzBv4kPc9F3
Greptile SummaryAdds a standalone report describing Gumbo capabilities required for durable execution frameworks.
Confidence Score: 5/5The documentation-only PR appears safe to merge. No blocking failure remains; the previously understated sequence-density dependency is now explicitly documented with its runtime and test implications. Important Files Changed
Reviews (2): Last reviewed commit: "docs: correct the sequence-density claim..." | Re-trigger Greptile |
The dense-vs-sparse argument rested on a supporting claim that Catalyst does no arithmetic on seq and that density is asserted in a single test. Both were too strong. CatalystRuntime.maybeSnapshot computes folded.lastSeq() - sinceSeqExclusive and compares it to the snapshot interval, reading a difference between versions as an event count — an identity that holds only under dense numbering. Against a sparse version it measures the span of the shared global sequence instead of the execution's own progress, so checkpoints fire early. A degraded heuristic rather than corruption, but a real dependency. Density is also contract-tested, not merely documented: GumboEventLogTest asserts 0,1,2 on append and asserts each execution stays dense from 0 independent of how two executions interleave in one log — precisely what a sparse version would abolish. The conclusion is unchanged and better supported: dense gains a third argument alongside migration cost and readability, since sparse would require changing shipped behaviour and rewriting passing tests. Also extend the restart-continuation test to assert density survives a reopen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G54yVgLZGZ3mzBv4kPc9F3
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.
Follow-up to #18. That PR recorded the v1 distribution design; this one writes up the log-layer work it depends on, as a standalone report Gumbo can be worked from directly.
Docs only — no source changes.
Why a separate document
The distribution design needed four things from Gumbo that it does not have. Rather than leave them as asides in a design doc, they are written up with the evidence for each, so the argument survives being read by someone who is not working on Catalyst. None of the findings are Catalyst-specific — they are the primitives any durable execution engine needs, which is why Boudin sits behind several of the same limitations.
Everything here was measured
The report opens with the probe table rather than burying it, because "we read the code and think this is wrong" and "we ran two JVMs and they both assigned seq 0" carry very different weight:
3 4 5— correct0 1 2; log reported 3 of 6strings log.datafter the aboveThe third row is the one that changes the diagnosis: nothing was lost, the view was. That makes D2 an index-clobber bug rather than a durability bug, and it is why the report says plainly up front that durability is correct and the foundation is not the problem.
The fifth row is a live bug in shipped Catalyst code (D4):
GumboEventLog.readFrompasses a per-execution seq intoreadAfter, which is keyed on the global seqnum. The two number spaces coincide only when the log holds a single stream — true in every test, false in every real deployment. The reducer then re-applies events already folded into the snapshot and double-counts steps, tokens and cost. Not fixed here; this PR is the write-up.Structure
Four defects (per-process
localId, index clobber, no cross-process guard, no version-keyed read), six additions (conditional append, version-keyed reads, KV compare-and-set, declared capabilities,AppendResult/streamVersion, multi-tag), the cross-cutting concerns (unbounded in-memory index;trimsilently destroying replayability), a suggested order, and five tests.The ordering is by blocking-ness × cost, not conceptual elegance — so it starts with a directory lock, which fixes nothing but converts silent corruption into a loud error while the rest is designed.
The one genuinely open question, now resolved
localIdwas repurposed from Boki's per-engine meaning to a per-tag cursor without changing how it is assigned — that is the root cause of D1. The natural fix is to move the semantics back, and the report initially left that implicit. The second commit answers it.The answer is that the two are different quantities Gumbo merged into one field: per-engine vs per-tag, internal write-path detail vs external client contract, superseded once
seqnumis assigned vs permanent. So realigning with Boki is orthogonal, not an alternative — it would makelocalIdcorrect and useless, since the version-keyed read and the conditional-append fence would each still need a number of their own.Decision recorded: drop
localIdfrom the public API, expose a per-tag, storage-ownedstreamVersion, dense rather than the tag's latest globalseqnum. Dense because (a) it avoids a log migration — a seeded counter continues existing sequences, whereas redefining the exposed version asseqnumwould silently invalidate persisted cursors such as Catalyst's snapshotthroughSeq, with no error at the seam — and (b) "diverged at step 7" is actionable where "diverged at seqnum 918,442" is not.The supporting fact was verified against the tree rather than assumed: Catalyst performs no arithmetic on
seq, only ordering comparisons. Density is asserted in one test and documented as an invariant, but nothing structurally requires contiguity — so the choice is free on the client side, which is why it is made on migration cost and readability instead.Note on the branch
#18 was squash-merged, so this branch was restarted from
mainand the two unmerged commits replayed onto it. The tree is identical to what was reviewed before the restart.Generated by Claude Code