compute-client: multiplex one command stream across two runtimes - #38388
compute-client: multiplex one command stream across two runtimes#38388antiguru wants to merge 2 commits into
Conversation
92a68e1 to
1db2f7e
Compare
1db2f7e to
b6ef7c0
Compare
b6ef7c0 to
017ca4b
Compare
92b0861 to
c595599
Compare
c595599 to
2e15845
Compare
2e15845 to
9fb1ae9
Compare
9fb1ae9 to
1a75556
Compare
1a75556 to
05faeed
Compare
05faeed to
ea5a4a5
Compare
ea5a4a5 to
2068331
Compare
2068331 to
12529cc
Compare
| // `until`. Copy-to is transient and finite-until too, but it drives an S3 sink and | ||
| // is refused by reconciliation, so it is excluded here for that reason, not a | ||
| // frontier one. | ||
| let to_interactive = desc.is_transient() |
There was a problem hiding this comment.
We need to document that no object can depend on transient descs, otherwise a downstream dependency might render on the other runtime, which would be surprising. I think we're avoiding this by restricting to dataflows with a non-empty until, but still worth pointing out.
There was a problem hiding this comment.
Your worry is right, and it was unenforced. CreateDataflow decided routing from the description's own shape and never consulted transient_owner, which only owner_of reads for Schedule, AllowWrites, and AllowCompaction. So a chain of temporary objects worked only because the predicate is uniform, not because anything held it.
Now asserted rather than assumed: a dataflow routed to interactive must import no transient id. Loud rather than silently rerouted by owner_of, per your point about recognising the situation. Rerouting would make the case work and therefore never surface, and the placement decision you want the control plane to make would lose its forcing function. The TODO(CPU-216) on the assert records that alternative, naming the trade you identified: it is the control plane's concern, at the cost of surfacing placement in the protocol.
Transience alone was never the guarantee, as you say. Introspection subscribes are transient and unbounded, and they are excluded by the subscribe clause, not by transience. The routing comment now says what each clause buys instead of leading with transience.
Posted by Claude Code.
| // is refused by reconciliation, so it is excluded here for that reason, not a | ||
| // frontier one. | ||
| let to_interactive = desc.is_transient() | ||
| && !desc.until.is_empty() |
There was a problem hiding this comment.
Should it be !until.is_empty() or until = [as_of.step_forward()]?
There was a problem hiding this comment.
until = as_of.step_forward(), and that already has a name: DataflowDescription::is_single_time().
The whole predicate moved onto DataflowDescription as is_peek_dataflow(), next to it, and both this site and handle_create_dataflow's tripwire in #38392 now ask the same method. !until.is_empty() was weaker than the interactive path needs: import_shared_index bounds its snapshot one step past as_of regardless of desc.until, so a transient dataflow with until beyond as_of + 1 would render with silently truncated inputs. Not reachable today, because optimize::peek sets until = as_of + 1 exactly, but nothing enforced the coincidence.
Switching also settled the as_of = MAX case, which I had wrong. I first thought it needed excluding, because is_single_time() is true there while until is empty. It does not: render already filters try_step_forward for exactly this, giving the empty snapshot bound and the semantics of reading the final state. So the emptiness of until at MAX means end-of-time, not unbounded, and the read routes to interactive like any other single-time read. routing_excludes_copy_to_and_subscribe_and_unbounded now covers both: MAX with empty until goes to interactive, a finite as_of with empty until stays on maintenance.
Confirmed end to end rather than argued. #38393 gains read_at_max_timestamp.slt, which reads at 18446744073709551615 off constant-backed indexes (constant collections seal, so a read at MAX is answerable at all), both as a fast-path peek and as a join that has to build a peek dataflow. Passes with the flag on and off.
Posted by Claude Code.
12529cc to
f0fc9ed
Compare
QA LLM Review1. MEDIUM --
|
f0fc9ed to
287fc37
Compare
|
Both fixed in 287fc37. The test drains past the first response and asserts nothing further arrives within 50 ms, so a removed filter fails it every time. The module doc no longer points at a design file; the broadcast rationale it cited is stated inline. Posted by Claude Code. |
| let (source, response) = tokio::select! { | ||
| r = self.maintenance.recv() => (Runtime::Maintenance, r?), | ||
| r = self.interactive.recv() => (Runtime::Interactive, r?), | ||
| }; |
There was a problem hiding this comment.
Add a comment: GenericClient::recv cancellation-safe by invariant.
There was a problem hiding this comment.
Added at the select!. The trait doc on GenericClient::recv is what mandates it, so the comment cites that.
Posted by Claude Code.
287fc37 to
127ed0f
Compare
127ed0f to
dc271dc
Compare
dc271dc to
4bc5cf9
Compare
4bc5cf9 to
b62bdd6
Compare
A replica running two compute runtimes still speaks one compute protocol to the controller. This adds the multiplexer that fans a single command stream out to both and merges their responses back into one, so neither the controller nor the protocol learns that the replica is split. Routing follows collection identity rather than command kind. Lifecycle commands go to both runtimes, a dataflow goes to the runtime that will host it, and `AllowCompaction` for a maintained collection is broadcast to both because the interactive runtime may be reading a published copy of it. Frontier reports are forwarded only from the runtime that owns the collection, since the controller keeps one frontier stream per collection and two reporters would race and regress it. Peek responses are forwarded verbatim without dedup, because a peek is answered by exactly one runtime. Nothing constructs a multiplexer yet, so the module is inert. Its tests cover the routing table, the broadcast, ownership eviction, and that `recv` loses no message when both sides are ready. Tests are out of line in `multiplex/tests.rs`, per the convention in `src/compute/AGENTS.md`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Also drop the claim that the interactive runtime installs copies of the introspection indexes from the response filter's doc. It hosts only transient query dataflows, which is the whole reason the filter can go by transience. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VDm7opomJLxbNUEP3r9BLk
b62bdd6 to
56711a5
Compare
Third of eight PRs splitting #37770. Stacks on #38387. Tracked by CPU-216.
A replica running two compute runtimes still speaks one compute protocol to the controller. This adds the multiplexer that fans a single command stream out to both and merges their responses into one, so neither the controller nor the protocol learns that the replica is split.
Routing follows collection identity rather than command kind. Lifecycle commands go to both runtimes, a dataflow goes to the runtime that will host it, and
AllowCompactionfor a maintained collection is broadcast to both because the interactive runtime may be reading a published copy of it. Frontier reports are forwarded only from the owning runtime, since the controller keeps one frontier stream per collection and two reporters would race and regress it. Peek responses are forwarded verbatim, because exactly one runtime answers a peek.A dataflow goes to interactive when it is a peek dataflow:
DataflowDescription::is_peek_dataflow, added here next tois_single_time, meaning it installs transient collections, reads a single time, and drives no sink that outlives the read. Each clause earns its place. Transience becausefilter_responseforwards interactive's frontier reports only for transient ids. Single-time because the shared-index import is a snapshot bounded one step pastas_ofand cannot feed a dataflow that runs further, and because transience alone says nothing about when a dataflow stops: aREFRESH ATmaterialized view is durable and still gets a finiteuntil, an introspection subscribe is transient and never stops. The predicate lives on the description so that #38392's tripwire can ask the same question of the same value rather than restate it.Routing reads only the description in front of it, never
transient_owner. A peek dataflow reads published maintenance indexes, not another temporary collection, so that is sound today, but nothing enforces it. A soft assertion now fails loudly if a peek dataflow ever imports a transient id, whose producer might sit on the other runtime. Rerouting instead would make such a case work and therefore never surface. The durable fix is for the control plane to name the runtime in the description, recorded as aTODO(CPU-216)rather than folded in here, since it surfaces placement in the protocol.Inert: nothing constructs a multiplexer. Independent of #38386 and #38387; it sits above them only to keep the stack linear.