Skip to content

design: two-runtime compute read isolation - #38239

Open
antiguru wants to merge 13 commits into
MaterializeInc:mainfrom
antiguru:mh/two-runtime-design
Open

antiguru wants to merge 13 commits into
MaterializeInc:mainfrom
antiguru:mh/two-runtime-design

Conversation

@antiguru

@antiguru antiguru commented Aug 17, 2026 •

Copy link
Copy Markdown
Member

Motivation

A compute replica maintains indexes and serves reads on the same timely workers, and timely does not preempt a running task. So a read waits behind maintenance work, not because the machine is out of CPU but because the one run loop is busy and cannot be interrupted. The sharpest form is introspection: mz_introspection describes a replica's own state and cannot be served from any other replica, so it is unavailable exactly when an operator needs it.

This is design only. No production code. It is the design of record for the implementation stack, #38386 through #38393, with the benchmarks in #38676 and #38678 and the hold-gap metric in #38736, and it describes what those pull requests ship.

Description

One document, design.md. It targets the interactive runtime: a second in-process timely runtime that renders temporary dataflows and serves reads off the maintenance runtime's arrangements, zero-copy through a per-process sharing registry.

It opens with the decomposition that shaped everything else: twelve reported symptoms, the mechanisms behind them, and which of sixteen candidate solutions reaches each. Every cell carries an evidence label distinguishing measured from argued from read-off-the-code, and the table deliberately includes mechanisms nothing here addresses. Two of the widest-blast-radius ones are among those, which is the point: a decomposition listing only the causes we have answers for is not a decomposition.

Several claims the work started with are retired in it:

  • The peek walk substrate, not the second runtime, is what removes head-of-line blocking between peeks.
  • Moving the walk off the worker is reproducibly worse than doing nothing when a peek queues behind a long operator activation.
  • The second runtime's case is temporary dataflows and introspection during hydration. Judging the sharing registry and its protocol against a peek-latency claim weighs them against the wrong benefit.

Peek placement is therefore treated as an orthogonal axis and discussed once. It needs no second runtime and this design needs none of it. It shipped separately, as budgeted peek slices on the worker with offload past the budget, and #38449 is its design. The interactive runtime inherits that path unchanged.

The protocol section describes what shipped: the compute controller tags each dataflow with a DataflowClass, the multiplexer forwards every command to both runtimes, and the runtime that does not render a dataflow records its exports as peers, holding each peer index at its as_of in the TraceManager. It includes the counterexample that rules out broadcasting without the hold and the liveness coupling the hold introduces. Capping the frontier at the multiplexer, a synthesized AcquireHolds command, an in-process sequence barrier, and routing creates while broadcasting only compaction are recorded as rejected alternatives with the reason each failed.

What the current revision changed

The document now describes the reworked stack.

  • Placement is the controller's: DataflowClass on the dataflow description, a passive broadcasting multiplexer, peers, and enable_compute_interactive_dataflows to keep peek dataflows off the interactive runtime.
  • The registry is one per worker ordinal, holds slots weakly, gives every published id its own point instead of aliasing re-exports, and wakes the reading thread with an unpark instead of a dirty set.
  • The shared import is an IndexTrace in the TraceManager, with one import_index for both provenances, so it honours SnapshotMode.
  • A saturated staging run is recorded. Reads off arrangements stayed at the client floor through hydration, while persist-backed reads wait on the process-wide fetch semaphore (CPU-298).
  • The latest stack review's fixes are described: the publisher registers the peer hold so a lagging reader cannot see a compacted index, registry slots are keyed by incarnation, and peer bundles are padded on reconnect.
  • Pinning is described as unmeasured: the doc states what the code does and what an experiment would compare.
  • The nightly numbers are from runs 18441 and 18442. The introspection read under hydration churn is the headline: p50 99 ms against 481 s at the merge base.

What moved out

The experimental evaluation is a project document under Interactive read isolation, which also tracks the work as issues. Evaluation artifacts do not belong in a design.

Both model checks are removed with their CI jobs. The TLA+ spec modelled the shipped mechanism; the Lean model's scope was the controller's read hold and the multiplexer's cap, and both of its counterexamples target behaviours no longer present in any form. The reasoning each contributed survives as prose in the rejected alternatives.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VDm7opomJLxbNUEP3r9BLk

@antiguru
antiguru requested a review from a team as a code owner August 17, 2026 08:09
antiguru added a commit to antiguru/materialize that referenced this pull request Aug 17, 2026
The design of record, the evaluation record and the two machine-checked protocol
models land separately in MaterializeInc#38239, along with their CI checks. They have no
dependency on this code and reviewing 4,700 lines of prose against 7,000 lines of
compute is worse for both.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019G29DBfgE8LXpE5jamm2Zi
The design of record for isolating reads from index maintenance inside a compute
replica, plus the two machine-checked protocol models and the experiment record
behind it.

`design.md` opens with the decomposition that shaped everything else: twelve
reported symptoms, the mechanisms behind them, and which of sixteen candidate
solutions reaches each. Every cell carries an evidence label, and the table
deliberately includes mechanisms nothing here addresses, because a decomposition
listing only the causes we have answers for is not a decomposition. Two of the
widest-blast-radius mechanisms are among the unaddressed.

`evaluation.md` records the experiments with their fixtures and venues. Several
retired claims the design started with. The offload alone captures the peek-tail
win, so the second runtime is not what removes head-of-line blocking between
peeks. The offload is measurably worse than doing nothing when a peek queues
behind a long operator activation. And the second runtime's case is temporary
dataflows and introspection during hydration, not peeks at all.

`peek-placement.md` parks the peek-substrate question as orthogonal, with the
deciding experiment and its rule registered in advance rather than settled by
implementing every candidate and learning afterwards.

The models are checked in CI, because a model nobody runs reads as assurance
without being any. `protocol-holds/` is TLA+ over the read-hold protocol and runs
three configs: the shipped design, plus two that are expected to violate the
invariant and whose passing would mean the model can no longer express the defect
the design fixes. It earned its place by refuting broadcast-alone before that was
implemented. `protocol/` is a Lean development of the command-ordering argument.
`check-copyright.sh` gains an exemption for `lean-toolchain`, which is a version
pin with nowhere to put a header.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019G29DBfgE8LXpE5jamm2Zi
antiguru added a commit to antiguru/materialize that referenced this pull request Aug 21, 2026
The design of record, the evaluation record and the two machine-checked protocol
models land separately in MaterializeInc#38239, along with their CI checks. They have no
dependency on this code and reviewing 4,700 lines of prose against 7,000 lines of
compute is worse for both.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019G29DBfgE8LXpE5jamm2Zi
@antiguru
antiguru force-pushed the mh/two-runtime-design branch from c6e43ec to bd6bfb6 Compare August 21, 2026 07:39
The directory held seven documents, three of which covered the read-hold
mechanism and two of which were spent scaffolding. It now holds one design
document that targets the interactive runtime.

The protocol section described the wrong mechanism. It specified capping the
compaction frontier at the multiplexer, which the implementation replaced with
broadcast compaction and a standing hold per shared collection. That section is
rewritten from `broadcast-compaction.md`, including the counterexample that
rules out broadcasting without the hold and the liveness coupling the hold
introduces. The cap, the synthesized `AcquireHolds` command, and the in-process
sequence barrier are recorded as rejected alternatives, with the reason each
failed: the cap failed at its retirement point rather than at the cap, since
`send` carries no ack.

Both model checks are removed, along with their CI jobs and images. The TLA+
spec modelled the shipped mechanism and the Lean model did not: its scope was
the controller's read hold and the multiplexer's cap, and both of its
counterexamples target behaviours no longer present in any form. The reasoning
each contributed survives as prose in the rejected alternatives.

Peek placement becomes an orthogonal axis discussed once rather than an
argument threaded through the document. The solution matrix keeps its row,
because the comparison is what bounds the interactive runtime's claims, but the
adjudication between placement candidates, the substrate follow-ups and the
routing policy move to the tracking issues. `peek-placement.md` and
`peek-offload-stash-plan.md` move to the branch that implements the offload.

The experimental evaluation moves to a project document. Evaluation artifacts
do not belong in a design, and the document also carried experiment protocol
for arms the code no longer reaches the same way.

Also corrected: the serving path claimed walking off the worker was abandoned,
the arrangement-size doubling is recorded as not reproducing on staging, and
the open findings drop the items describing deleted code.
antiguru and others added 11 commits September 6, 2026 07:06
The document described the branch as it stood before the implementation was
split into reviewable layers, and three parts of it no longer matched the code.

Peek execution shipped separately. The summary and the placement section said
peek offloading was parked pending an experiment; it now records that the walk
runs in budgeted slices with offload past the budget, that both are dyncfgs, and
that the interactive runtime inherits that path. The serving path section
replaces the runtime's own pending-peek variant with the unified `IndexPeek`
over `IndexTraces`, and the registry section describes how a shared peek parks
in `pending_work` until its target's dirty mark.

Two open findings were fixed and are documented as mechanism rather than kept as
defects. The reader's handle mirrors `TraceAgent`: it joins on set, reports the
join, and seeds its physical hold at the chain coverage, so the inert physical
hold finding is removed and the compaction section explains why the coverage is
the only correct seed. The import keeps no separate hold pinned at `as_of`; the
read hold is the returned arrangement's own trace handle, so the frozen-hold
section becomes a description of the feedback path.

Re-exports are aliases. The registry section replaces the sentence about sharing
an `Arc` under a new id with the alias mechanism: `publish_alias`, its refusal
when a reader bound first and the import-based fallback that covers it, the
target-governed frontiers, and the meet over aliases after the target drops. The
discarded-failure finding is removed and the measured cost of the earlier
import-based publication is recorded.

The multiplexer, configuration and testing sections are updated to the shipped
routing predicate, the soft assertion on transient imports, the CI variable
system parameter, the four driver specs, the sqllogictest, the feature and
parallel benchmark scenarios, and the swap scenarios, with the numbers one
nightly run produced. The implementation history lists the stacked pull
requests.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VDm7opomJLxbNUEP3r9BLk
The shared import follows the dataflow's `until` instead of synthesising a bound
one step past `as_of`, so the section that presented the bound as a call-site
narrowing now places the single-time restriction where it lives, in the routing
predicate. An export whose arrangement is an imported shared arrangement aliases
its publication point instead of hitting an `unreachable!`, which closes that
open finding and adds a fifth driver spec. The interactive runtime installs no
logging dataflow, so the runtime tells the peer's publications apart by whether
it hosts the collection rather than by transience, and the multiplexer's
frontier filter is described as a guard on that invariant rather than as a
repair for empty copies. The registry's state sits behind one lock.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VDm7opomJLxbNUEP3r9BLk
The sharing primitive became `SharedSpine`, a `Trace` wrapper that mirrors the
spine's chain and frontiers into the publication point inside each mutation. The
design records why a spine behind a mutex was rejected, with the measured reader
and writer latencies, replaces the sink-based feed and compaction sections, drops
the forwarded controller frontier, and marks the arrangement-size doubling as gone
with the sink.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VDm7opomJLxbNUEP3r9BLk
Four questions the document left open have numbers now, all taken on one process
and one binary with the replica-scoped dyncfg toggled between points, which is what
every earlier comparison lacked.

Publishing an index costs nothing in reported arrangement size. The doubling the
document recorded belonged to the publisher sink operator, which the trace wrapper
replaced; a published index now reports the same size and allocations as an
unpublished one at 1, 1000, 100000, and 1000000 records.

The compaction coupling has a metric. `mz_compute_shared_arrangement_hold_gap_ms`
and `mz_compute_shared_arrangement_held_count` report, per worker, how far the
readers' holds keep maintenance from applying the frontier its controller asked
for, and how many arrangements are held at all.

An interactive reader over a published index leaves the merge schedule alone but
more than doubles the arrangement-maintenance CPU maintenance spends, which is
recorded as a new open finding rather than explained.

And the concrete long-lived import today, the coordinator's four introspection
subscribes, costs 4.9% of one core on an idle replica and nothing measurable during
hydration. That is what moving them off maintenance is worth.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_015tLhSbZdXrTSK2KwSocT59
The finding recorded that interactive reads roughly double maintenance's
arrangement-maintenance CPU, and guessed at a per-read mechanism. Sweeping reader
concurrency 1, 2 and 4, plus an arm at concurrency 4 against a join a hundred times
smaller, holds it flat at 0.164, 0.144, 0.143 and 0.129 CPU-seconds per second
against a quiet baseline of 0.068. It falls slightly as concurrency rises, which is
what opportunistic `exert` does when the workers get busier.

So the charge is not per read, per row, per dataflow install, or per reader. It is a
fixed 6 to 10% of one core for having any reader attached, which is about 1% of what
the reads cost the process, and it bounds no throughput.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_015tLhSbZdXrTSK2KwSocT59
Bring the implementation sections in line with the stack: placement by
DataflowClass set in the controller, a passive multiplexer that broadcasts
every command, peer holds in the TraceManager in place of the standing
hold, per-worker registries without aliases, the shared import as an
IndexTrace, the role components, and the enable_compute_interactive_dataflows
flag. Record the saturated staging run, the persist fetch semaphore finding
(CPU-298), two open findings from the latest stack review, and the latest
nightly numbers.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The publisher registers the peer hold, registry slots are keyed by the
incarnation's as_of, peer bundles are padded on reconnect, and the
reconciliation spec runs against two runtimes. The pinning paragraph now
states what the code does and leaves the layout to measurement.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…ialization

Describe how per-runtime holds derived from the shared command stream,
downgraded to the writer over a channel, remove the lock-based admission
rules, which places still create holds on the reader thread, and the two
physical-hold designs that keep the channel sound. Record the single-time
specialization, where every interactive read takes one snapshot after the
trace passes its time, as the variant to land first, and the result of
the owned-agent spike.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_015tLhSbZdXrTSK2KwSocT59

@bosconi bosconi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The design needs a few corrections before it merges, mainly the inverted M4 comparison and the unsupported Solo claim; details inline.

  • Add a Verification section to the PR description saying the document was checked against the stack, since that is the failure the revision notes describe fixing.
  • The revision notes do not mention the new Simplifying the sharing protocol section, the only forward-looking design in the document.

### What follows

Two conclusions bear on this design. The rest of the matrix belongs to the peek program and
is tracked in the project's peek issues.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section repeats 296-366 nearly verbatim, and the copies already disagree: 309 says concurrent scans and 392 says concurrent peeks. Cut it to the two new conclusions, that S3 stands alone on M3 and S4 is not separable, and link back to the evidence above.

the instance's logging configuration arrives it records every log index as a peer at the
minimum time, and the maintenance runtime publishes them.

#### The price: compaction is coupled to the rendering runtime's drain rate

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the body says reading runtime; the heading still says rendering.

Suggested change
#### The price: compaction is coupled to the rendering runtime's drain rate
#### The price: compaction is coupled to the reading runtime's drain rate

`AllowCompaction`, so an interactive runtime that drains promptly leaves the two frontiers
equal. On an 8-worker replica under continuous churn the gauges read zero with no reader, and
peaked at one compaction round with 31 arrangements held while 509 interactive joins ran over
the published index. The lag did not grow with time under that load.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Give the hold-gap gauges an actionable threshold, even a provisional one, for when the interactive runtime has stopped draining and what to do then. This is the only detection for the one new price the design names. Also say here that the gauges are added in #38736, which is still a draft.

publishing `since` before applying it, the peer-hold reservation and its FIFO pairing, and the
physical hold an import registers at the chain coverage. This section records two designs that
remove those rules. The first is general and keeps imports live. The second exploits the
one-shot-read boundary, removes more, and is the one to land first.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Landing the specialization first removes ImportQueue, incremental replay and the per-read logical hold (1272-1282), which S14 (235-246), Compaction feedback and Non-goals (1490-1492) present as in place. Either say here that the general design is kept alongside the specialization, or say in those three places that they depend on the general design landing.

and the writer merges freely. The snapshot log is unnecessary, because each read needs one
snapshot.
* **Seeding through placeholders.** A read over a slot that is not yet published waits. It
does not bind to the point before the trace exists, so attach has nothing to seed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As written this reads as the deferred build that Principle 3 rules out (766-769), and the import needs a real trace at construction (1033-1036). Say what the import holds between construction and the snapshot, and that the wait is inside the operator, not the build.

Comment on lines +117 to +119
loaded, on either runtime. It is larger than the tail that runtime placement
recovers, so for the workload M3's remedy is justified by, this is the dominant
term.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

M4's floor of about 900 ms is smaller than the roughly 1400 ms tail that placement recovers (321-323, E7's 2835 to 1456 ms), so it is not the dominant term. The same correction is needed at 320-323, 379-380 and 439-441.

Suggested change
loaded, on either runtime. It is larger than the tail that runtime placement
recovers, so for the workload M3's remedy is justified by, this is the dominant
term.
loaded, on either runtime. At about 900 ms it is roughly two thirds of the tail
that runtime placement recovers, so for the workload M3's remedy is justified
by, it is a first-order cost next to that benefit.

Comment on lines +846 to +847
> **I1c.** A shared arrangement compacts only as fast as the slowest runtime's stream
> position.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I1c names a rate, not a relation, so no assertion or test can check it. State it as a predicate, matching the restatement at 1205-1206 and what a_peer_bundle_pins_until_the_importing_runtime_applies asserts.

Suggested change
> **I1c.** A shared arrangement compacts only as fast as the slowest runtime's stream
> position.
> **I1c.** For every published index `I`, the publishing trace's `since` is at or below the
> reading runtime's peer hold on `I`: the `as_of` of `I`'s `CreateDataflow`, advanced by every
> `AllowCompaction` for `I` that the reading runtime has applied.

Comment on lines +1428 to +1430
* `Solo` is the sole runtime of a single-runtime process and is behaviorally identical to a
deployment from before this work. Its metric and log label is `None`, so a single-runtime
registration collides with nothing and looks unchanged.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typedefs.rs:46-55 wraps every spine in SharedSpine with no role or flag gate, so Solo is not behaviorally identical to a pre-work deployment, and the wrapper's cost is unmeasured. The summary at 20-21 makes the same claim.

Suggested change
* `Solo` is the sole runtime of a single-runtime process and is behaviorally identical to a
deployment from before this work. Its metric and log label is `None`, so a single-runtime
registration collides with nothing and looks unchanged.
* `Solo` is the sole runtime of a single-runtime process. It renders every class, publishes
nothing and reads nothing from the registry, and its metric and log label is `None`, so a
single-runtime registration collides with nothing and looks unchanged. It is not unchanged
underneath: every spine in `mz_compute::typedefs` is a `SharedSpine` regardless of role, so
`Solo` runs every trace call through the unattached wrapper.

Comment on lines +1847 to +1848
only approximate. The wrapper reads both off the spine, and the registry stopped
forwarding the controller's frontier.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1077-1080 says the published since is the frontier the wrapper is about to apply, matching apply_holds. "Reads both off the spine" describes the previous order.

Suggested change
only approximate. The wrapper reads both off the spine, and the registry stopped
forwarding the controller's frontier.
only approximate. The wrapper publishes the spine's chain and the exact `since` it is about
to apply, and the registry stopped forwarding the controller's frontier.

|---|---|---|
| Point lookup under hydration churn | 3.3 / 28 ms | 186 / 1683 ms |
| Range count under hydration churn | 27 / 56 ms | 1635 / 4884 ms |
| Introspection read under hydration churn | 99 / 168 ms | 481 / 519 s |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cell is censored by the load phase, not a measured latency; mark it as a lower bound so nobody derives a speedup ratio from it.

Suggested change
| Introspection read under hydration churn | 99 / 168 ms | 481 / 519 s |
| Introspection read under hydration churn | 99 / 168 ms | > 481 / > 519 s |

This branch has not been deployed

No deployments
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