Skip to content

docs: report what Gumbo needs for durable execution frameworks - #19

Merged
contrasam merged 3 commits into
mainfrom
claude/distribution-design
Jul 26, 2026
Merged

docs: report what Gumbo needs for durable execution frameworks#19
contrasam merged 3 commits into
mainfrom
claude/distribution-design

Conversation

@contrasam

Copy link
Copy Markdown
Contributor

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:

Probe Result
Sequential restart (append, exit, continue) 3 4 5 — correct
Two JVMs, one directory, one execution Both assigned 0 1 2; log reported 3 of 6
strings log.dat after the above All six events physically present
Two executions in one log, tail-read the second Returned the whole stream
Same, through Catalyst's snapshot warm path Sole: 43 steps. Shared: 51 steps

The 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.readFrom passes a per-execution seq into readAfter, 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; trim silently 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

localId was 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 seqnum is assigned vs permanent. So realigning with Boki is orthogonal, not an alternative — it would make localId correct and useless, since the version-keyed read and the conditional-append fence would each still need a number of their own.

Decision recorded: drop localId from the public API, expose a per-tag, storage-owned streamVersion, dense rather than the tag's latest global seqnum. Dense because (a) it avoids a log migration — a seeded counter continues existing sequences, whereas redefining the exposed version as seqnum would silently invalidate persisted cursors such as Catalyst's snapshot throughSeq, 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 main and the two unmerged commits replayed onto it. The tree is identical to what was reviewed before the restart.


Generated by Claude Code

claude added 2 commits July 26, 2026 05:40
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-apps

greptile-apps Bot commented Jul 26, 2026

Copy link
Copy Markdown

Greptile Summary

Adds a standalone report describing Gumbo capabilities required for durable execution frameworks.

  • Documents measured concurrency, indexing, versioned-read, and multi-writer limitations.
  • Defines proposed storage primitives, sequencing semantics, implementation order, and regression tests.
  • Links the distribution design to the new requirements report.

Confidence Score: 5/5

The 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

Filename Overview
docs/gumbo-requirements.md Adds the Gumbo requirements report and now accurately records Catalyst's sequence-density dependency in snapshot scheduling and tests, resolving the prior review finding.
docs/distribution.md Links the existing distribution design to the detailed Gumbo requirements report.

Reviews (2): Last reviewed commit: "docs: correct the sequence-density claim..." | Re-trigger Greptile

Comment thread docs/gumbo-requirements.md Outdated
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
@contrasam
contrasam merged commit 5724adc into main Jul 26, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants