compute: two-runtime read isolation (interactive runtime) - #37770
compute: two-runtime read isolation (interactive runtime)#37770antiguru wants to merge 37 commits into
Conversation
f90cdae to
6728fc8
Compare
This is an architectural commitment, not just a featureBefore we land this, it's worth being explicit that two-runtime is not a "sure, let's do it" change. It commits the compute layer to a new shape and is effectively a one-way door. This comment lays out the trade-off honestly so the decision is made with eyes open. What it actually isTwo runtimes do not add CPU and do not magically "isolate" reads — both runtimes share the same cores. What the second runtime buys is one precise thing: a separate, OS-preemptible run loop, so an interactive read is no longer trapped behind a long, run-to-completion maintenance operator step on the same timely worker. The right way to frame it is separation of concerns, not isolation:
A single runtime cannot be both without compromising one of them. Two runtimes let each be pure. That is the real argument. Why the obvious alternatives don't apply"Just use a read replica." Fails on the use case that matters most: introspection. A replica's introspection describes that replica, so it fundamentally cannot be served from another one — and you need it exactly when the maintenance runtime is pinned (hydration, batchy work), which is when it blocks. Today we fly dark precisely then. Nothing but an in-process second runtime fixes that. Separately, replicas mostly redline on memory, not CPU, so a second replica doubles the binding resource; the shared-arrangement approach here duplicates no memory. And because those boxes are memory-bound, the spare CPU the interactive runtime needs is exactly what is already idle — the CPU-saturated case (where two runtimes help least) is not the common one. "Just yield more finely in the one runtime." That is the anti-pattern above. It would degrade the maintenance runtime's core contract to buy interactivity it shouldn't be responsible for. The costs we are accepting
Why it's a one-way door
That irreversibility is the crux. It is acceptable only because the end-state — maintenance as a pure batch runtime, interactive as a pure reader — is the architecture we would choose deliberately, given the points above. The bar for merging is therefore not "we can back out." It is "we would design it this way on purpose." On the introspection-during-hydration and memory-bound-fleet facts, we would. Bottom lineWorth doing, but as a deliberate architectural commitment. The durable, defensible assets are (a) zero-copy cross-thread arrangement sharing as a primitive and (b) introspection that survives hydration — both survive every counter-argument. The framing to avoid leaning on is "reads get faster," which a CPU-bound box or the unsolved coordinator cap can each undercut. Reviewers should weigh this as a commitment to the two-runtime shape, not just as an isolated feature. |
| /// Export `index_id` as [`GlobalId::Transient`] rather than [`GlobalId::User`]. | ||
| /// | ||
| /// The multiplexer fronting a two-runtime `clusterd` (see | ||
| /// `mz_compute_client::multiplex`) routes a `CreateDataflow` to the interactive | ||
| /// runtime only when every export id is transient and the dataflow has no | ||
| /// subscribe sink; a `User` export always stays on maintenance. Setting this | ||
| /// is how a script drives a dataflow onto the interactive runtime, e.g. to | ||
| /// exercise a query dataflow that imports a maintenance index and is itself | ||
| /// served by the interactive slow path. | ||
| #[serde(default)] | ||
| transient: bool, |
There was a problem hiding this comment.
Alternative would be to allow setting transient global ids based on the name, using the canonical GloablId string representation.
| fn threshold_shared_trace<'scope, T: RenderTimestamp>( | ||
| arrangement: Arranged<'scope, SharedOksEnter<T>>, | ||
| name: &str, | ||
| ) -> Arranged<'scope, RowRowAgent<T, Diff>> { |
There was a problem hiding this comment.
Could this function be generic over traces surfacing rows?
There was a problem hiding this comment.
Spiked. This is the same reduce_abelian higher-ranked output-key normalization wall documented on threshold_local: the bound only normalizes when the input and output trace types are concrete through the function signature, so a generic Tr: TraceReader does not compile. The three helpers (_local, _trace, _shared_trace) share identical bodies but must stay concrete for that reason.
Unifying them needs mz_reduce_abelian's key-container bounds reworked (the same class of fix as differential #797/#798's named key-container parameter), which is a broader reduce-plumbing change than this file. A macro could remove the textual duplication but adds indirection for three ~10-line functions without touching the type constraint. Leaving concrete and tracking the genericization with the reduce_abelian work rather than here.
| } | ||
| PeekStatus::NotReady => Some(peek), | ||
| PeekStatus::UsePeekStash => { | ||
| unreachable!("the interactive peek is never peek-stash eligible") |
There was a problem hiding this comment.
This is a limitation that we shouldn't have (and don't document).
Script ids were raw u64s always mapped to GlobalId::User, so a spec could not target the system or transient namespaces. Parse ids the same way GlobalId's own Display does: a bare number defaults to the user namespace, while an explicit s/si/u/t prefix (s42, u1000, t7) selects the namespace directly. Spec and Command fields now carry GlobalId end to end instead of wrapping a u64 at each call site. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…aring, interactive runtime) Squashed work-log for the two-runtime compute branch. Migrates production arrangements from Rc to Arc batches, adds cross-runtime arrangement sharing, and stands up a second in-process compute runtime that serves ephemeral peeks off the maintenance runtime's shared arrangements, fronted by a Multiplexer. Includes fixes landed while stabilizing the branch: - keep shared-trace snapshot import consistent so joins don't double-count - guard the shared-index snapshot bound against a Timestamp::MAX as_of - stop the multiplexer from dropping peek responses on non-zero processes - release transient read holds so two-runtime read frontiers advance: forward the interactive runtime's terminal empty-Frontiers report for a dropped transient even after ownership eviction, and report frontiers only for the transient collections the interactive runtime exclusively hosts Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
Arc batches carry more fixed per-batch overhead than the Rc batches this check was written against, so a single-record index arrangement now exceeds the coarse `size < 16 * 1024` "not egregious" bound (the empty arrangement still fits). Bump the bound to 32 KiB, which one Arc batch on top of the empty spine stays under. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
…w is dropped
A deferred interactive dataflow whose shared-index dependencies are not yet
published is cancelled from `pending_work` without ever building. The controller
had already created the collection, initialized its per-replica frontiers to the
as_of, and acquired read holds on its storage inputs. It releases those input
read holds only once the collection's frontiers all reach the empty antichain,
but the cancelled dataflow never reported any frontier, so the holds leaked and
pinned the inputs' read frontiers. This surfaced as an MV's `since` never
advancing under two-runtime (read_frontier_advancement).
Send empty `Frontiers` from `drop_collection` on the deferred-cancel path so the
controller can clean the collection up and drop the holds.
Also filter interactive-runtime frontier reports on `id.is_transient()` in the
multiplexer instead of `owner_of(id)` plus a terminal-empty special case. The
interactive runtime reports frontiers only for the transient collections it
hosts (see `report_frontiers`), so every such report must forward regardless of
`transient_owner`, whose eviction on `AllowCompaction{empty}` races ahead of a
dropped transient's trailing frontier reports. This replaces the earlier
terminal-transient hack and no longer depends on ownership state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
Measures peek p50/p99/qps under a saturated maintenance runtime. Two closed-loop `HydrationChurn` actions continuously build, hydrate, and drop heavy maintained MVs, keeping the maintenance workers busy. Two open-loop peeks (a fast-path index point lookup and a slower range-scan reduce) run at a fixed rate against a pre-hydrated indexed table. Run twice, once with `enable_two_runtime_compute=true` and once false, and compare the SELECT latencies. With two runtimes on, the interactive runtime serves the peeks off the shared arrangements, isolated from the maintenance workers, so the open-loop peeks accumulate far less queue-wait latency than when they contend with hydration on the same workers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
Two indexes on the same key make the optimizer build the second as a cross-dataflow re-export of the first: the second index's dataflow imports the first's arrangement and re-exports it under its own id. On the maintenance runtime, render's `ArrangementFlavor::Trace` arm publishes that re-export into the sharing registry via `reexport`, but unlike the `Local` arm it builds no streams of its own and so installs no seal-signal frontier tap. An interactive peek on the re-exported id that arrives when the arrangement's `upper` equals the peek `as_of` defers in `pending_work` waiting for the seal. Nothing ever fires `note_frontier` for the re-exported id when it advances, so the peek is never re-examined and hangs. A `DROP INDEX` of one of two same-key indexes followed by a `SELECT` hit this deterministically. Have the registry track re-export aliases. `reexport(from, to)` records `to` as an alias of `from`, and `notify` marks an id together with the transitive closure of ids that re-export it. The source's own dataflow outlives its catalog drop while a re-export still imports its arrangement, so its live tap keeps sealing the re-export. `remove` prunes an id only as a target, never as a source, so that trailing seal signal survives the source's `DROP INDEX`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
Records the sound architecture for cross-runtime index arrangement sharing: a capture-based lifecycle with two synchronization barriers (seal-gated capture, tombstone teardown), grounded in the invariant that the interactive runtime only performs single-time reads. Removes the incremental replay machinery that caused the row-doubling and delayed-capability-panic bugs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
Replaces the v1 single-time/tombstone sketch with the converged design after three adversarial review rounds and two feasibility spikes. Three principles: build on a correct protocol and panic outside it (no local safeguards, shared fate); the multiplexer splits the controller's one correct protocol into two correct sub-protocols, making the interactive one well-formed with an internal Import command; render in command arrival order with late-bound imports for deterministic construction. Keeps the frontier-tracked replay (deleting it reintroduces row-doubling) and fixes the delayed-capability panic at its root by single-sourcing the replay feed. Late-binding uses pre-allocated publication points (placeholder plus adopt-in-place), spike-validated feasible. Teardown safety rests on the controller's read-hold discipline rather than a compute-level lease, which review showed to be inert under a correct controller. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
The interactive query's Get(id) is already a dataflow import that the interactive runtime resolves from the registry by role, since it holds no traces of its own. Formalize that: the interactive sub-protocol's index imports are shared imports, a self-describing import kind that references a registry id with no prior local creation. This makes the sub-protocol well-defined without a new command. Placeholder teardown rides existing events, the maintenance publisher's close-on-drop for an adopted slot and last-reader eviction for a never-adopted one, so no import verb and no withdrawal are needed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
Nine staged TDD tasks from the committed design: single-source replay feed (F1), placeholder plus adopt (F2), registry get-or-create, placeholder close/eviction, bounded-read routing, arrival-order rendering with late-bound imports, since<=as_of assert, delete dead live import(), and regression/concurrency suites. Tasks 1-2 seed from the validated spike branches spike/single-source-publish and spike-f2-placeholder. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
…rontier The publisher derived the published upper from the trace's map_batches, which leads the arrangement stream within a worker step, so a Frontier instruction could be enqueued before a Batch whose hint is below it and the importer's caps.delayed panicked. Use the stream frontier, which never leads the delivered batches, as the authoritative upper for both the published state and the importer Frontier instructions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
…place) A SharedTrace placeholder can be created empty and handed to an importer as a live handle before the arrangement is published. The maintenance publisher later adopts the same Arc in place rather than constructing a fresh one. Publishing becomes the degenerate case of adopting a publication point with no prior reader. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
Whichever runtime touches an id first creates its placeholder slot; the other adopts or reads the same Arc. Replaces create-fresh-and-overwrite so a placeholder a reader already imports is filled in place and never overwritten. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
A placeholder whose index creation is cancelled before it publishes must not wedge its importers. Closing pushes a terminal empty frontier to them and the registry evicts the slot when its last reader leaves. An adopted slot is closed by the maintenance drop instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
Route a CreateDataflow to the interactive runtime only when its until is bounded and it has no subscribe or copy-to sink. Copy-to is finite-until but drives an S3 sink and is refused by reconciliation, so it belongs on maintenance. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
…d imports Remove the per-worker deferral, which built dataflows in each worker's own publication order and so allocated timely channel ids in a worker-divergent order. Every dataflow now builds immediately in command arrival order, and an import of a not-yet-published dependency binds through a registry placeholder that adopts later. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
Matches the assert the maintenance import path already makes. A since above the requested as_of means the controller offered an unreadable as_of, a protocol error, so it panics rather than reading coalesced data silently. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
The interactive runtime issues only bounded reads, so the unbounded live import had no non-test callers. import_snapshot_at is the sole interactive import path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
…s-first invariant Add a debug_assert on the interactive CreateDataflow path as a tripwire for a Multiplexer routing bug, matching the bounded-read predicate the multiplexer enforces. Document that evict_unadopted consults only the oks adoption flag, which is sound because every publish site adopts oks before errs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
… read-hold discipline Record where correctness comes from for cleanup of a maintenance index an interactive runtime imports. The controller holds a read hold on the imported id for every reader and frees it only after the reader retires, so the index is never compacted or dropped while an interactive import still reads it, with no cross-runtime signal (Instance::finish_peek). A controller that violated this would be an incorrect protocol instantiation, which the runtime panics on. evict_unadopted is registry hygiene for a bounded, empty leaked placeholder, not a correctness mechanism, and Published::close guards only a wedge that a correct protocol never produces. Both are retained for a future hygiene path and have no production caller today. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
rustdoc under --document-private-items rejects these: SharedTraceHandle and PublishArrangement are not in scope in the linking modules (use full crate paths), Self::notify on the private Inner struct resolves to nothing (name ArrangementSharingRegistry::notify), and public docs on get_or_create_placeholder and evict_unadopted linked to the private insert_shared and adopted_and (demote to plain code spans). cargo check does not run rustdoc, so these passed the per-task build gates. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
The old `ArrangementSharingRegistry::insert`/`insert_shared` and the `PublishArrangement::publish`/`publish_named` primitives are production-dead: every maintenance publish path now binds through `get_or_create_placeholder` plus `adopt`, which fills a placeholder in place rather than overwriting a slot a reader may already have imported. `insert` reintroduced exactly that placeholder-overwrite hazard, so remove it. Delete the four items and port their remaining (test-only) callers to the production pattern: registry-based sites route through `get_or_create_placeholder` + `adopt` + `notify`, and the standalone shared-trace primitive sites use `Published::placeholder` + `adopt` via a new `adopt_fresh` test helper. Behavior is unchanged: `insert(publish, publish)` into a fresh slot is equivalent to placeholder-then-fill. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
`PendingWork` lost its `Dataflow` variant earlier in this branch, leaving a single `Peek(SharedIndexPeek)` wrapper around every value in `pending_work`. Store `SharedIndexPeek` directly in the map, drop the enum, and straight-line the four single-arm match sites (`resolve_dirty`, `handle_cancel_peek`, and the reconciliation drop loop in `server.rs`). No peek behavior changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
psycopg 3.3.3's Cursor.execute accepts LiteralString, bytes, sql.SQL, sql.Composed, or Template, but not a plain str. An f-string with interpolated attributes types as str, so pyright rejected the three HydrationChurn.execute calls. Encode each f-string to bytes, matching the repo convention (for example util.py PgConnInfo.connect and parallel_workload/action.py). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
…ontier-filtered one A late importer's initial snapshot came from state.chain, which the publisher built by filtering map_batches to batch.upper() <= stream_frontier. The stream frontier lags the trace by a scheduling round: the batch data is delivered, the frontier notification catches up a round later. When the Spine merges an already streamed batch with a leading one into a single batch whose upper leads the frontier, that filter drops the whole batch, stranding its historical part. A late importer registering in that window seeds an incomplete snapshot missing rows, so its reducer later sees a retraction without its insert and reports a non-positive DistinctBy multiplicity, or returns wrong rows. Seed with the full map_batches instead. This costs only momentary memory, since batches are Arc-shared, and cannot double-count: the stream emits each original batch once and never re-emits a merged batch, so incremental batches never carry what the seed already holds. The stream frontier still drives the published upper and the incremental Frontier instructions, which is where it is authoritative. Exposed by removing the interactive-dataflow deferral, which made interactive reads register against an actively-updating arrangement rather than a settled one. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
…rators The dataflow-operator introspection now shows the arrangement-sharing operators (PublishShared and the InspectBatch seal taps) and the ArcBatch batch type name (mz_row_spine::arc_batch::ArcBatch instead of alloc::rc::Rc) that the two-runtime publish path installs. This is expected output, not a behavior change. Rewritten with --rewrite-results under enable_two_runtime_compute; verified stable across repeated runs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
…ream tap A fast-path peek parked on a shared index's seal is re-examined only when the registry wakes its interactive worker. That wake came from an inspect_container tap on the arrangement stream, placed upstream of the publisher sink. The tap fired note_frontier when the frontier reached the tap, but the sink advances the published state.upper a scheduling round later, downstream. So the peek woke, read a stale state.upper, found itself NotReady, and parked. If that frontier advance was the arrangement's last (a static index or view, the common case), no further tap fired and the peek hung. The sink's own post-upper wakeups (the importer activators and the upper_changed condvar) do not reach the peek server loop, so nothing re-woke it. Flaky, because it depended on the tap-versus-sink scheduling gap. Move the seal signal into the publisher sink: adopt takes an on_seal callback that the sink fires once per activation on which state.upper advances, after it releases the state lock. Firing after the lock keeps the wakers lock strictly after the trace state lock, matching the reader order (wakers then state) and avoiding a deadlock. Firing after the advance means a peek the wake re-examines reads the advanced upper and completes. This makes the seal notify program ordered after the state.upper advance, so the lost-wakeup contract on ArrangementSharingRegistry::notify covers the seal, not just publication. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
…nal move Moving the seal signal from the upstream inspect_container taps into the publisher sink removed the InspectBatch tap operators from the dataflow-operator introspection, so relations.slt over-counted them. Rewrite the golden to match. Also demote the intra-doc reference to ArrangementSharingRegistry::notify (a pub(crate) item) from a link to a code span, since adopt is public and lint-doc rejects a public-to-private doc link. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
The cross-runtime arrangement publisher derived its writer-driven compaction floor by reaching into `agent.trace_box_unstable()` (an upstream API documented as unstable, with mutation undefined behavior) plus fork-only TraceBox compaction getters, computing the meet of all other referees' holds minus its own. That was the only remaining reason the differential-dataflow fork was required. Replace it with the authoritative sources Materialize already has. The controller pushes logical compaction through `AllowCompaction`, and physical compaction follows the trace upper (as `TraceManager::maintenance` does). So the publisher now takes its logical floor from the last `AllowCompaction` frontier, forwarded into the shared slot via `ArrangementSharingRegistry::note_allow_compaction` from `handle_allow_compaction`, and its physical floor from the stream `upper` it already holds. `SharedTraceState` gains a `writer_logical` field, seeded `None` so the publisher falls back to its own hold (the `as_of`) before the first command arrives. With the trace-box read gone, nothing links against the fork: `mz_row_spine::ArcBatch` already supplies the cross-thread batch impls, and the sharing primitive lives in `shared_trace.rs`. Remove the `[patch.crates-io]` override for differential-dataflow and differential-dogs3, which resolve to crates.io 0.25.1. This also clears the lint-and-rustfmt dd-git-source deny. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
At 200+50 reads/s the combined rate exceeds both the two-runtime and the single-runtime serving drain, so both runs overload and report queue-wait latency growing to the run length. That masks the isolation the scenario means to show. Halve to 100+25/s. This sits above the baseline drain but below the two-runtime drain, so the comparison separates cleanly: two-runtime holds reads flat (p50 ~11ms, near-zero slope) through hydration churn while the baseline backlogs to tens of seconds. The reported latency now reflects service time rather than an undrained open-loop backlog. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
…isher Two `render::interactive_import_tests` drove compaction by advancing a writer trace handle and relied on the publisher observing it through the trace-box read, which no longer exists. Forward the same frontier through `ArrangementSharingRegistry::note_allow_compaction`, the production `handle_allow_compaction` path, so the publisher advances its `since`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
On the constrained CI runner the churn saturates every core, so even the halved 100+25/s read probe backlogs: the interactive runtime cannot get CPU while maintenance is pinned. Keep the churn heavy (that saturation is the point) and shrink the read probe further to 50+12/s so it fits the interactive runtime's CPU slice and its latency reflects serving under contention rather than a coordinated-omission backlog. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
Replace the four planning and design docs (implementation-plan, stage2-detailed-plan, arrangement-sharing-lifecycle-design and -plan) with one design of record that reflects the current implementation and explains the rationale behind the decisions. The prior docs spanned two eras and carried stale mechanics: a differential fork and Arc-batches base branch that no longer exist, a Stage-1 peek-offload strategy that was abandoned for inline interactive serving, and an ArrangementFlavor path once flagged known-broken that now ships as a real SharedTrace flavor. The consolidated doc adds the AllowCompaction-driven compaction mechanism (which postdated every prior doc), records why the architecture is a deliberate commitment rather than a drop-in feature, and lists the known limitations that remain, including the coordinator control-plane bottleneck and the interactive serving-loop ceiling. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
Add three known limitations to the two-runtime design: the interactive runtime is an introspection blind spot (logging disabled, introspection forwarded from maintenance), storage introspection remains patched into the maintenance runtime's introspection, and the role metric label breaks exact-match dashboards when the feature is enabled because the maintenance runtime's series gain a role="maintenance" label. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
The flag only gated whether a runtime publishes into the sharing registry, and publication is already forced for the Maintenance and Interactive roles. So the flag was a no-op with two-runtime on, and with it off a Solo process has no registry peer to read what it would publish, so publishing there was dead. Its only remaining use was as a test knob. Publication is now decided purely by the role: `ComputeRuntimeRole::publishes` (renamed from `publishes_unconditionally`, whose qualifier is meaningless with no flag to be unconditional about) returns true for Maintenance and Interactive, false for Solo. Drop the standalone dyncfg, inline the former `should_publish_index` helper, and remove the flag from the system-parameter and parallel-workload lists. Addresses the "unused" review comments on both Python files. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
The multiplexer only ever records `Runtime::Interactive` as a transient owner, with maintenance the default in `owner_of`, so the value was dead weight. Store the interactive-owned transient ids as a `BTreeSet<GlobalId>` instead of a `BTreeMap<GlobalId, Runtime>`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
Stacking on MaterializeInc#37880 (typed s/u/t-prefixed global ids in specs) makes the bare `transient` export flag moot: an export id parses to `GlobalId::Transient` directly from a `t` prefix. Drop the flag and its driver-side machinery (taken from MaterializeInc#37880's rewrite of script.rs/text.rs), and mark the interactive query dataflow's output `t4000` in two_runtime_query_dataflow.spec so the multiplexer still routes it to the interactive runtime. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CY7GHdrTBfAG4tgvSgPGJ9
6c243c1 to
5abfb74
Compare
A second in-process "interactive" compute runtime that serves reads against the arrangements the maintenance runtime maintains, isolating latency-sensitive reads from CPU-bound maintenance. Implements the two-runtime design (#37747).
The cross-runtime arrangement-sharing primitive lives entirely in Materialize (
mz-compute::shared_trace,mz-row-spine::ArcBatch), so it builds against released differential-dataflow with no fork or[patch.crates-io]. The diff also carries the production Rc→Arc spine migration that cross-thread sharing needs. The publisher follows the controller's compaction throughAllowCompactionrather than reading trace internals.How it works:
oks/errsarrangements into a per-processArrangementSharingRegistry. AComputeRuntimeRole { Solo, Maintenance, Interactive }distinguishes runtimes;Solo(default, single-runtime) is byte-behaviorally unchanged, metrics included.PeekResponseper uuid, and each collection's frontiers only from the runtime that owns it (both runtimes install the internal logging dataflows, so this keeps the interactive runtime's empty copies from regressing the controller's per-collection frontier).ArrangementFlavor::SharedTracearrangements (not re-derived collections) and render joins/reduces over them; the query's own transient output is itself published so its result peek is served the same way. A spike verified a join over an imported shared arrangement is correct (the join cuts each trace at its own acknowledged batch boundary, so the shared handle'sbatches_throughis always batch-aligned).Maintenancerole, since those indexes bypass the normalexport_indexpublish path). The interactive runtime runs with logging disabled and serves introspection peeks from maintenance's published copies, so introspection queries during hydration return promptly (possibly stale) instead of blocking behind maintenance.SyncActivator+ dirty-id inbox. A read whose dependency is not yet published/sealed is enqueued, not blocked; publication (insert) and seal (note_frontier, fired from the maintenance export's frontier probe) mark the id dirty and wake the interactive worker, which re-examines only the affected pending work. A query dataflow whose imported deps aren't published yet is deferred (not built) and built once all deps publish. The per-stepprocess_peeksscan is removed on the interactive runtime.ENABLE_TWO_RUNTIME_COMPUTE(dyncfg, off by default in production) makes the controller launch replicas with a second interactive runtime (--interactive-compute-timely-config, its own worker ports). Enabled by default in the variable CI system parameters so the suite exercises the two-runtime path broadly.Acceptance: a
clusterd-test-driverworkflow drives an interactive query dataflow that imports an unpublished maintenance index, scheduled before the index publishes (mirroring controller ordering), and its result peek resolves correctly only after publication — proving the defer→build→resolve read path is served off the maintenance worker.A
TwoRuntimeReadIsolationparallel-benchmark scenario measures read latency while the maintenance runtime is saturated by hydration churn. With two runtimes on, point-read p50 stays flat (~11ms) through the churn; the single-runtime baseline backlogs to tens of seconds.Design and per-task plans: #37747,
doc/developer/design/20260720_two_runtime_compute/stage2-detailed-plan.md.Scope / follow-ups:
until/as_ofcoalescing (correct for one-shot peeks; a future subscribe migration must add it).index_peek_total_seconds; pre-existingprintln!debug lines inrender.rsNonearms.🤖 Generated with Claude Code
https://claude.ai/code/session_01DAfFVVJ8BwahdaAHkRtbf6