compute: serve fast-path peeks on the interactive runtime - #38392
compute: serve fast-path peeks on the interactive runtime#38392antiguru wants to merge 3 commits into
Conversation
cf82e1f to
77e275a
Compare
| pub(crate) fn role(&self) -> ComputeRuntimeRole { | ||
| self.role | ||
| } | ||
|
|
There was a problem hiding this comment.
Function moved for no particular reason.
There was a problem hiding this comment.
Agreed, no reason. Moved back to where #38389 introduced it, right after new(). This was churn imported by taking compute_state.rs wholesale from the original branch. (Posted by Claude Code.)
6984b18 to
f359f28
Compare
f359f28 to
f45dc4c
Compare
f45dc4c to
40e6744
Compare
40e6744 to
0ba0d59
Compare
0ba0d59 to
2db1bb4
Compare
2db1bb4 to
76b96ab
Compare
76b96ab to
b288248
Compare
b288248 to
f29b660
Compare
f29b660 to
bb7c5fc
Compare
| || (dataflow.is_transient() | ||
| && !dataflow.until.is_empty() | ||
| && dataflow.subscribe_ids().next().is_none() | ||
| && dataflow.copy_to_ids().next().is_none()), |
There was a problem hiding this comment.
I wonder if this should be a function on the dataflow description. We're repeating the condition from the multiplexer.
There was a problem hiding this comment.
Agreed, and it is now DataflowDescription::is_peek_dataflow() in mz-compute-types, next to is_single_time(). #38388's router and this tripwire ask the same method of the same description, so the assert can finally check what it claims to: previously a drift between the two spellings was exactly what it was meant to catch, and it was written as a second copy of the thing that might drift.
The predicate also got stricter on the way: is_single_time() in place of !until.is_empty(), per the discussion on the router. Details in the reply on #38388.
Posted by Claude Code.
| /// | ||
| /// `fuel` bounds how far the walk may go on this worker, exactly as it bounds a walk over a | ||
| /// maintained trace. | ||
| fn seek_fulfillment( |
There was a problem hiding this comment.
This looks like duplication with the regular index peek logic, is that right? The peek offload work aimed at reducing duplication, and canonicalizing all peek structure (which it not has done completely, but that's a different story.) So my ask here would be to check if we can combine the walk, maybe by making them generic over the type of index they're walking.
There was a problem hiding this comment.
Right that it was duplicated, though not in the walk. The walk was already shared: both build a PeekScan and hand it to the generic walk_scan, which is what the peek-offload canonicalisation bought. What was copied is the ~25 lines of frontier gating in front of it, and the only differences were cosmetic: where (oks, errs) come from, and TraceBundle::compaction_frontier() versus an explicit antichain_join of the same two frontiers, which is literally what that method computes.
Extracted as gate_peek, generic over both traces via the existing PeekOksTrace/PeekErrsTrace markers, returning a PeekGate of Open/NotReady/Compacted. That required pinning Time = Timestamp on the two marker traits, which they should have carried anyway: both only ever describe traces over mz_repr::Timestamp, and only the cursor GATs were constrained before.
IndexPeek::collect_finished_data is deliberately left alone. It is main's method with main's tests in index_peek_tests.rs, which call it to exercise the walk while bypassing the gate, so folding it in would have churned code this stack has no business touching.
Posted by Claude Code.
| // One is per collection and carries no dataflow identity, so it cannot go stale across a | ||
| // reconnection, and it only ever rises. Clearing it would drop the hold back to the | ||
| // minimum time until the replayed compactions raised it again. See | ||
| // `doc/developer/design/20260720_two_runtime_compute/design.md`. |
There was a problem hiding this comment.
Is the design still part of the stack?
There was a problem hiding this comment.
It never was in this stack. design.md lives in #38239, which is open and design-only, and whose description says it lands first so the implementation PRs can cite it rather than restate the argument. So the reference resolves iff #38239 merges ahead of this stack, and today it points at a file on no landed branch.
That ordering was tracked nowhere, which is why it could go unnoticed. Filed as CPU-239 on the Interactive read isolation project, naming this call site as the reason: the standing-hold argument is written down only there.
Left the reference as it is. Inlining the argument here would duplicate the design rather than cite it, which is the thing the design PR exists to avoid.
Posted by Claude Code.
bb7c5fc to
911e0ce
Compare
152df60 to
7baf4dc
Compare
QA LLM Review1. MEDIUM --
|
7baf4dc to
861a6db
Compare
861a6db to
a4480c6
Compare
| // arrangement at or below the `as_of` it is about to read at. | ||
| self.compute_state | ||
| .sharing_registry | ||
| .note_standing_hold(id, worker_index, &frontier); |
There was a problem hiding this comment.
If this was keyed just by id, could we avoid the scaffolding to check interactive and peer_published, and instead just go by existence of state for id?
There was a problem hiding this comment.
Yes, and it goes further than removing the checks here. The scaffolding existed only because the interactive runtime installed empty copies of the logging indexes (initialize_logging forced logging off but still built the dataflow and inserted collections and traces). Those copies served nothing: peeks on the logging indexes read the maintenance runtime's publications from the registry, report_frontiers skipped them, and only the maintenance runtime publishes them.
initialize_logging now returns early on the interactive runtime, so it hosts only the transient dataflows the multiplexer routes to it, and "no collection for this id" is exactly "the peer publishes this id". handle_allow_compaction reduces to: unhosted id, record the standing hold and return, otherwise the normal path. report_frontiers loses its transient-only filter as well, since there is nothing left to filter.
The role appears once, in a soft_assert_or_log that the maintenance runtime never sees compaction for an unknown collection, which drop_collection used to panic on and would otherwise now pass silently.
Without a compute_logger the interactive runtime's own events are not logged, which was already the case with the copies (logging was forced off). That is the CPU-222 gap and there is a TODO pointing at it.
Posted by Claude Code.
| let traces = if self.compute_state.role == ComputeRuntimeRole::Interactive { | ||
| // The interactive runtime maintains no traces of its own. It reads the | ||
| // arrangements the maintenance runtime publishes into the sharing registry. | ||
| IndexTraces::Shared { | ||
| registry: self.compute_state.sharing_registry.clone(), | ||
| worker_index: self.timely_worker.index(), | ||
| } | ||
| } else { | ||
| // Acquire a copy of the trace suitable for fulfilling the peek. | ||
| IndexTraces::Local(self.compute_state.traces.get(id).unwrap().clone()) | ||
| }; |
There was a problem hiding this comment.
I think a match would read nicer here. Also check other places that could be a match instead of if.
There was a problem hiding this comment.
Done, match self.compute_state.role with Maintenance | Solo as the local arm. The other role checks in this file went away with the empty logging copies (see the thread above), and the two that remain in render.rs and logging/initialize.rs are early returns, which read fine as if.
Posted by Claude Code.
| // frontiers. Reporting our empty copies' frontiers races the owner's report for the same | ||
| // collection id in the controller's single per-collection frontier stream, regressing it. | ||
| // Report only the wholly-transient query dataflows this runtime exclusively hosts. | ||
| let report_only_transient = self.compute_state.role() == ComputeRuntimeRole::Interactive; |
There was a problem hiding this comment.
Instead of making the decision here, could we generalize the is_subscribe_or_copy field and only condition on it? This would avoid the additional complexity here.
There was a problem hiding this comment.
Went the other way: the condition is gone rather than generalised. With the interactive runtime installing no logging dataflow (see the handle_allow_compaction thread), every collection it holds is a transient dataflow it hosts, so there is nothing left for report_only_transient to skip and is_subscribe_or_copy keeps its one meaning.
Posted by Claude Code.
a4480c6 to
6f357bd
Compare
6f357bd to
e6a88fa
Compare
e6a88fa to
5501841
Compare
5501841 to
13da69b
Compare
The interactive runtime holds no local traces, so an index peek there resolves against the sharing registry instead. `PeekScan` and the error walk beneath it become generic over the traces they read, and an interactive peek opens its scan over the registry's `SharedOksHandle` and `SharedErrsHandle`. Both flavours of index peek then spend one budget, report one set of metrics, and reach the peek stash through the one offload driver. A shared peek whose arrangement is not yet published, or whose upper has not sealed the peek timestamp, waits in `pending_work` keyed by a `WorkId` and indexed by the id it waits on. A publication or seal marks that id dirty and wakes the worker, which gives a turn to exactly the items indexed under the ids that changed, so wakeups scale with what changed rather than with total pending work. Past that gate a shared peek is an ordinary index peek: it queues for a turn like any other, and a walk that outruns the activation's fuel or fills a batch bound for the stash leaves for a driver. The sweep still runs on every step, because a persist read and an offloaded walk each wake the worker through a channel of their own rather than through the dirty set, but nothing waiting on a publication or a seal is ever swept. Interactive dataflows build immediately in command arrival order rather than deferring until their dependency is published. An import over an unadopted placeholder produces no data and holds its output frontier at the minimum until a publisher adopts the same slot, so late binding replaces the deferral. The interactive runtime reports only its transient collections' frontiers. It shares the identity of every non-transient collection with maintenance, which owns and reports the real frontiers, and the controller keeps one frontier stream per collection, so reporting the shared ones would race the owner and regress it. For the same reason its logging is forced off: it serves introspection peeks from maintenance's published copies, and its own empty copies would clobber them. The consequence is that nothing the interactive runtime does appears in introspection, tracked as CPU-222. Reconciliation drops `pending_work` and `dep_index`, whose peeks belong to the reconciled-away connection. The standing holds in the registry deliberately survive: one is per collection, carries no dataflow identity, and only rises, so clearing it would drop the arrangement's bound to the minimum until replayed compactions raised it again. Reachable only on a runtime holding the `Interactive` role, which requires the dyncfg that is still off everywhere. Tests are out of line in `compute_state/tests.rs`, per the convention in `src/compute/AGENTS.md`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The interactive runtime installed empty copies of the logging indexes, with logging forced off, so that `initialize_logging`'s bookkeeping stayed uniform. Every command handler then had to tell those copies apart from the runtime's own collections: `handle_allow_compaction` carried a role check and a transience check to route broadcast compaction around them, and `report_frontiers` skipped non-transient ids so the copies' frontiers would not regress the maintenance runtime's reports. The copies served nothing. Peeks on the logging indexes read the maintenance runtime's publications from the registry, and only the maintenance runtime publishes them. With the interactive runtime installing no logging dataflow, "is this a collection this runtime hosts" is a question `collections` answers, and `handle_allow_compaction` reduces to: an unhosted id is the peer's, so its frontier is the standing hold. The role check survives only as a soft assertion that the maintenance runtime never sees compaction for an unknown collection, which used to panic in `drop_collection`. `report_frontiers` loses its filter. `handle_peek` selects the trace source by matching on the role. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VDm7opomJLxbNUEP3r9BLk
`handle_allow_compaction` no longer notes the controller's frontier on the registry: the trace manager's compaction of the index is what the published `since` reports. Tests keep a trace agent alive for as long as they read, since the point closes with the trace. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VDm7opomJLxbNUEP3r9BLk
13da69b to
8efd922
Compare
QA LLM Review1. HIGH -- reconciliation panics on the interactive runtime at every controller reconnect
Details
The base branch did not have this: Gate the block on the role that installs the traces: // The interactive runtime installs no logging dataflow, so it has no logging traces to pad.
if compute_state.role() != ComputeRuntimeRole::Interactive {
if let Some(config) = old_instance_config {
for id in config.logging.index_logs.values() {
let trace = compute_state
.traces
.remove(id)
.expect("logging trace exists");
let padded = trace.into_padded();
compute_state.traces.set(*id, padded);
}
}
}Keeping the 2. MEDIUM -- a seal on any published index wakes every interactive worker, whether or not a peek waits on it
Registering the waker ( DetailsThe cost is not just the wakeups. That mutex is the one maintenance's publishers take from
|
Seventh of eight PRs splitting #37770. Stacks on #38391. Tracked by CPU-216.
The interactive runtime holds no local traces, so an index peek there resolves against the sharing registry.
IndexPeekgains anIndexTracessource:Localpins aTraceBundlefor the peek's life,Sharedresolves handles from the registry on every attempt and holds nothing in between. Both hand the scan the same enum trace types, so oneIndexPeek, oneIndexPeekScan, and onePendingPeek::Indexserve both runtimes and the walk, gate, offload, and cancel paths are written once.A shared peek that is not ready lives in
pending_work, keyed by the index it waits on, not inqueued_peeks. A publication or seal marks that id dirty and wakes the worker, which re-examines exactly the peeks parked under the changed ids, so wakeups scale with what changed rather than with total pending work. Fast-path persist peeks still land inpending_peekson every runtime, because their persist-read task wakes the worker with no dirty signal.Interactive dataflows build immediately in command arrival order rather than deferring until their dependency is published. An import over an unbacked publication point produces no data and holds its output frontier at the minimum until a publisher adopts the same slot, so late binding replaces the deferral.
The interactive runtime reports only its transient collections' frontiers, since it shares the identity of every non-transient collection with maintenance and the controller keeps one frontier stream per collection. For the same reason its logging is forced off: it serves introspection peeks from maintenance's published copies. The consequence is that nothing the interactive runtime does appears in introspection, tracked as CPU-222. That wants resolving before the flag is turned on anywhere it would hide real work.
Reconciliation drops
pending_work. The standing holds in the registry deliberately survive: one is per collection, carries no dataflow identity, and only rises, so clearing it would drop the arrangement's bound to the minimum until replayed compactions raised it again.Reachable only on a runtime holding the
Interactiverole, which requires the dyncfg that is still off everywhere.