compute: publish maintained indexes into the sharing registry - #38389
compute: publish maintained indexes into the sharing registry#38389antiguru wants to merge 3 commits into
Conversation
d31414b to
d9ddff4
Compare
d754ecc to
1303035
Compare
1303035 to
dffb179
Compare
dffb179 to
7d70e54
Compare
7d70e54 to
6e430c4
Compare
6e430c4 to
8ba6dce
Compare
8ba6dce to
3f24fa0
Compare
3f24fa0 to
d3a7b3d
Compare
d3a7b3d to
188d168
Compare
188d168 to
aac9441
Compare
QA LLM Review1. MEDIUM -- The re-export arm overwrites a registry slot an interactive import has already bound to
DetailsEvery other publish path goes through The reader binds once and never re-resolves: The ordering that triggers this is the one Either make 2. MEDIUM -- Aliasing two ids onto one publication point makes them share per-collection compaction state, refusing reads the arrangement can still serve
DetailsThe refusal surfaces two ways. On the fast path, the peek's handle is registered at the published The underlying trace is not actually over-compacted: Divergence between two indexes on the same key is ordinary: a freshly created duplicate starts at its create The publication point's |
aac9441 to
aea40da
Compare
|
Both confirmed and fixed in aea40da, together with the registry change in #38387 (0225ab1). The re-export arms no longer alias Posted by Claude Code. |
5911208 to
6d76ee5
Compare
|
Revised in 6d76ee5: the re-import per re-export cost 125 to 185 KB of clusterd memory each (five operators and a trace listener per re-export, measured in the nightly's
Posted by Claude Code. |
QA LLM Review1. MEDIUM --
|
| @@ -133,9 +196,6 @@ | |||
| /// an error carries its data on the errs arrangement, whose frontier is held back until the | |||
| /// error is emitted, so an oks-only signal would leave that peek parked. | |||
| /// | |||
There was a problem hiding this comment.
Removed.
Posted by Claude Code.
| struct Inner { | ||
| map: Mutex<BTreeMap<GlobalId, Vec<Option<Arc<SharedIndexArrangement>>>>>, | ||
| /// Indexed by worker ordinal; `None` until that interactive worker registers its waker. | ||
| wakers: Mutex<Vec<Option<Waker>>>, | ||
| /// Taken after `map` and before `wakers`, never the other way around. | ||
| aliases: Mutex<Aliases>, | ||
| } |
There was a problem hiding this comment.
Is it important we have three mutexes, or could the whole Inner be protected by one Mutex?
There was a problem hiding this comment.
Not important. Inner is now one Mutex. Every critical section is a few map operations and the publisher takes the lock once per seal, not per record, so nothing measurable is lost, and the acquisition-order rule goes away.
The lost-wakeup argument on notify survives unchanged in substance. It was never about the locks being distinct: the publication and the mark are separate critical sections on the publisher side, and take_dirty and the slot re-read are separate on the worker side, so the same four-step ordering argument applies. Its wording now says that instead of "two independent locks".
Lock nesting is registry then shared-trace state, in remove and the two note_* methods. The publisher's on_seal callback runs after the state lock is released, so there is no path in the other direction.
Posted by Claude Code.
6d76ee5 to
6a74e15
Compare
6a74e15 to
4b688a8
Compare
QA LLM Review1. MEDIUM --
|
4b688a8 to
c9e3686
Compare
| pub(crate) fn notify(&self, id: GlobalId, worker_index: usize) { | ||
| let mut wakers = self.inner.wakers.lock().expect("registry poisoned"); | ||
| let mut inner = self.lock(); | ||
| let Inner { | ||
| wakers, aliases, .. | ||
| } = &mut *inner; | ||
| if let Some(waker) = wakers.get_mut(worker_index).and_then(|w| w.as_mut()) { | ||
| Self::mark(waker, id); | ||
| } | ||
| } | ||
|
|
||
| /// Marks `id` dirty for every registered worker and fires each coalescing waker. Used by | ||
| /// `remove`, which is not worker-specific. Per worker, the lost-wakeup argument on | ||
| /// [`Self::notify`] applies unchanged. | ||
| fn notify_all(&self, id: GlobalId) { | ||
| let mut wakers = self.inner.wakers.lock().expect("registry poisoned"); | ||
| for waker in wakers.iter_mut().flatten() { | ||
| Self::mark(waker, id); |
There was a problem hiding this comment.
Should this be in this PR or further up the stack?
There was a problem hiding this comment.
Fair question. note_standing_hold has no production caller until #38392 wires handle_allow_compaction on the interactive runtime; in this PR it is reached only by adopt's seeding and by tests. This block is the alias half of that method: when a target with aliases drops, the point's standing hold moves to the meet of what the aliases noted, the same rule note_standing_hold applies while the target lives.
I kept it here so the alias rules stay in one place with publish_alias and Aliases, rather than splitting them across this PR and #38392. If you would rather see the standing hold arrive with its caller, I can move note_standing_hold, the holds table, and this block to #38392 as one unit.
Posted by Claude Code.
There was a problem hiding this comment.
Misread the anchor, sorry: you meant the registry changing shape here right after #38387 introduced it. Agreed. The one-lock form now lands in #38387 itself, so this PR only adds the alias table to Inner and touches nothing the registry already had. The fixup that remains here is a two-line doc trim on the logging gate.
Posted by Claude Code.
c9e3686 to
6746cde
Compare
Both export paths now publish their `oks`/`errs` arrangements into the per-process registry when the runtime's role publishes. A re-export arm has no streams of its own, so it registers its id as an alias of the arrangement's existing publication point, which leaves the re-export's dataflow without operators, as it is on a runtime that does not publish. A reader that bound the re-export's id before the render holds its own unbacked point, which only a publisher into it can back, so that case re-imports the shared traces and publishes them under the new id, and logs the imported errors because `mz_compute_error_counts` forwards a dependency's counts only to a re-export whose dataflow has no operators. Logging indexes publish the same way, gated strictly on `Maintenance`: an interactive runtime reads maintenance's slot, and its own copy would clobber it, while `Solo` has no registry peer at all. An alias shares its target's frontiers while the target lives: the alias dataflow imports the target, so the controller never advances the target's `since` past an alias's, and the target's frontier bounds every reader of the shared point. Once the target drops, the meet of the remaining aliases' frontiers governs the point, since the shared trace then compacts to exactly that meet. Seal notifications fan out from the target to its aliases, since a reader waits under the id it imported. `ComputeRuntimeRole::Interactive` stops being test-only. Nothing constructs it yet, but `publishes()` has to name it, and `pub mod server` keeps the variant reachable so dead-code analysis is satisfied without an attribute. The stale `owns_process_globals` note claiming every constructible role owns the globals goes with it. Carrying the role and the registry to the render path is what the rest of this change is: `Config` and `Worker` gain both, `ComputeState` stores them and exposes `role()`, and clusterd builds one registry per process. Per process, not per runtime, because a reader on one runtime looks up the slot a publisher on another filled. No behavior change. `Solo` is the only role anything constructs and it does not publish, so every added block is skipped and no dataflow gains an operator. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VDm7opomJLxbNUEP3r9BLk
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VDm7opomJLxbNUEP3r9BLk
With the trace publishing its own `since`, the registry no longer forwards the controller's compaction frontier through aliases. The alias table keeps only the standing-hold frontiers, and the alias test exercises those. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VDm7opomJLxbNUEP3r9BLk
6746cde to
8c44e85
Compare
QA LLM Review1. HIGH -- Publishing one arrangement under two ids kills the first publication point, because
|
Fourth of eight PRs splitting #37770. Stacks on #38388. Tracked by CPU-215.
Both export paths publish their
oks/errsarrangements into the per-process registry when the runtime's role publishes. A re-export arm, where an index reuses another index's arrangement, has no streams of its own, so it registers its id as an alias of the arrangement's existing publication point, which leaves the re-export's dataflow without operators, as it is on a runtime that does not publish. A reader that bound the re-export's id before the render holds its own unbacked point, which only a publisher into it can back, so that case re-imports the shared traces and publishes them under the new id, and logs the imported errors becausemz_compute_error_countsforwards a dependency's counts only to a re-export whose dataflow has no operators. Logging indexes publish the same way, gated strictly onMaintenance: an interactive runtime reads maintenance's slot and its own copy would clobber it, whileSolohas no registry peer.An alias shares its target's frontiers while the target lives: the alias dataflow imports the target, so the controller never advances the target's
sincepast an alias's, and the target's frontier bounds every reader of the shared point. Once the target drops, the meet of the remaining aliases' frontiers governs the point, since the shared trace then compacts to exactly that meet. Seal notifications fan out from the target to its aliases, since a reader waits under the id it imported.ComputeRuntimeRole::Interactivestops being test-only, becausepublishes()has to name it.pub mod serverkeeps the variant reachable, so no#[allow]is needed even though nothing constructs it yet.No behavior change:
Solois the only role anything constructs and it does not publish, so every added block is skipped and no dataflow gains an operator. That is also why no goldens move here. They move in the last PR of the stack, which turns the flag on in CI.