Repository navigation
Conversation
fda6a81 to
ebd007b
Compare
ebd007b to
870f336
Compare
870f336 to
b508d89
Compare
…nt sharing (MaterializeInc#38396) Replaces MaterializeInc#37881, whose head branch lives on a fork and so cannot be the base of a stacked PR in this repository. Same commits, same tree, on an upstream branch instead. This is the root of the stack MaterializeInc#38386 through MaterializeInc#38393, which splits MaterializeInc#37770. ### Motivation Cross-runtime arrangement sharing (the two-runtime read-isolation work, MaterializeInc#37770) needs batches readable from a thread other than the one maintaining the trace. Differential's default spines reference-count batches with `Rc`, which is worker-local. ### Description Introduce `mz_row_spine::ArcBatch`, a local newtype around `Arc<B>` that carries differential's batch traits (the orphan rule forbids the blanket impl on a bare `Arc<B>`), and switch the production spines and their builders — `RowRowSpine`, `RowValSpine`, `RowSpine`, `ValRowSpine`, `ColValSpine`, `ColKeySpine` — from `Rc`/`RcBuilder` to `ArcBatch`/`ArcBuilder`. An `Arc`-backed batch whose contents are `Send + Sync` can be read across threads, which `Rc` cannot do. Only the batch handle becomes atomic; the batch contents are unchanged, so the cost is a marginally more expensive refcount. Also adds generic `ArcOrdVal`/`ArcOrdKeySpine` aliases for callers outside `mz_compute`, adapts batch-size logging (`log_arrangement_size_inner`) to reach through the newtype to the inner `Arc`, and switches the storage sink trace to the `Arc`-backed spine. Builds against released differential-dataflow 0.25 with no fork or `[patch.crates-io]`. ### Verification `cargo check --workspace` passes with no `Cargo.lock` churn. `relations.slt`'s golden is rewritten because the spine type name appears in operator names. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
QA LLM Review1. MEDIUM -- a live import permanently pins the published arrangement's physical compaction
The read hold DetailsMeasured on a Registration id 2 is the Cost is unbounded rather than constant: retractions in stranded Suggested fix, verified against the same probe (chain folds to 5, exactly matching the control, and the hold tracks to if let Some(hold) = hold.as_mut() {
hold.set_logical_compaction(acknowledged.borrow());
+ hold.set_physical_compaction(acknowledged.borrow());
}
|
|
Confirmed and fixed in 285af14. The measurement reproduces exactly: 39 batches against an unimported control's 5, over 40 seals. Both of the report's points were real, and the second one is the root cause rather than a separate cleanup. A handle carries two physical frontiers that are not interchangeable. The one it reports through The import's read hold hit this on both counts: it is a clone, so it registered at The fix keeps the two frontiers in separate fields, has Two regression tests, each verified red without its half of the fix:
The The stack above this PR is rebased and pushed. Full run at the tip: 124 tests, workspace (Posted by Claude Code.) |
Several modules in `mz-compute` carry test modules many times the size of the production code they cover, so the code has to be scrolled past to read. `src/cluster-controller` already uses the out-of-line pattern, where `#[cfg(test)] mod tests;` points at a sibling `tests.rs`. This records that as the crate convention, with a threshold so it is decidable rather than a matter of taste. Out-of-line tests still reach private items through `super::`, so moving a module needs no visibility changes. Worth landing before #38386 and the seven PRs stacked on it, which follow this rule and between them move about 5,900 lines of in-file test modules out of line. Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
A handle carries two physical frontiers, and they are not interchangeable. The one it reports through `get_physical_compaction` is seeded at the published `since`, because a reported frontier may never lead the chain coverage. The one it holds is seeded at that coverage, because a merge spanning the coverage destroys the boundary the reader was seeded with. `Clone` and the setter both wrote the reported frontier into the hold, which silently lowers it. Since the accumulation is a meet, one such registration is a floor under every other hold, so the published spine stops merging: batches pile up in `Spine::pending`, one per seal, for as long as that registration lives. The cost is unbounded rather than constant, since retractions in stranded batches never consolidate and every `cursor_through` builds a `CursorList` over all of them. An import's read hold hit this on both counts. It is a clone, so it registered at `since`, and it advanced only on the logical axis, so nothing ever raised it. Measured against an unimported control over 40 seals: 39 batches against 5. Keep the two frontiers in separate fields, have `Clone` inherit the hold, have the setter join into both, and advance the import's hold on both axes. `acknowledged` is the right value for the physical axis too: it is exactly the frontier below which that import will never cut again. Reported by the QA LLM review on #38386. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
285af14 to
e953639
Compare
A handle carries two physical frontiers, and they are not interchangeable. The one it reports through `get_physical_compaction` is seeded at the published `since`, because a reported frontier may never lead the chain coverage. The one it holds is seeded at that coverage, because a merge spanning the coverage destroys the boundary the reader was seeded with. `Clone` and the setter both wrote the reported frontier into the hold, which silently lowers it. Since the accumulation is a meet, one such registration is a floor under every other hold, so the published spine stops merging: batches pile up in `Spine::pending`, one per seal, for as long as that registration lives. The cost is unbounded rather than constant, since retractions in stranded batches never consolidate and every `cursor_through` builds a `CursorList` over all of them. An import's read hold hit this on both counts. It is a clone, so it registered at `since`, and it advanced only on the logical axis, so nothing ever raised it. Measured against an unimported control over 40 seals: 39 batches against 5. Keep the two frontiers in separate fields, have `Clone` inherit the hold, have the setter join into both, and advance the import's hold on both axes. `acknowledged` is the right value for the physical axis too: it is exactly the frontier below which that import will never cut again. Reported by the QA LLM review on #38386. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
e953639 to
e295c50
Compare
Review feedback on #38386. A handle carried two physical frontiers, one it reported and one it held. Reporting the chain coverage satisfies the only consumer that reads the frontier back, `mz_join_core`, whose assertion compares it against the coverage it derives from `map_batches`. So the two collapse into one field seeded at the coverage, and the class of bug the split was guarding against stops being expressible. The publication point also kept a `BTreeMap` of per-registration holds on each axis, solely to recover the previous value when computing a delta into the accumulation. The handle already owns that value, so the maps go and the setters pass `previous` explicitly. A handle no longer needs a registration id at all; ids now serve only importer queues. `coverage_hold` goes the same way: the publisher falls back to the chain coverage when the accumulation is empty, which is the shape the logical axis already used. Test-only surface moves into `shared_trace/tests.rs`, which reaches private items through `super::`. `snapshot_at` gains a deadline so a wedged publisher fails with the frontier it stalled on rather than hanging. `Tr::Time: TotalOrder` is now a bound on the `TraceReader` impl, so `batches_through` stopping at the first batch beyond the cut rests on a stated property rather than a comment. The module is `pub(crate)` with an explicit `allow(dead_code)`, rather than `pub` to keep dead-code analysis quiet. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
QA LLM Review1. MEDIUM --
|
a38f3a0 to
18d0897
Compare
A handle carries two physical frontiers, and they are not interchangeable. The one it reports through `get_physical_compaction` is seeded at the published `since`, because a reported frontier may never lead the chain coverage. The one it holds is seeded at that coverage, because a merge spanning the coverage destroys the boundary the reader was seeded with. `Clone` and the setter both wrote the reported frontier into the hold, which silently lowers it. Since the accumulation is a meet, one such registration is a floor under every other hold, so the published spine stops merging: batches pile up in `Spine::pending`, one per seal, for as long as that registration lives. The cost is unbounded rather than constant, since retractions in stranded batches never consolidate and every `cursor_through` builds a `CursorList` over all of them. An import's read hold hit this on both counts. It is a clone, so it registered at `since`, and it advanced only on the logical axis, so nothing ever raised it. Measured against an unimported control over 40 seals: 39 batches against 5. Keep the two frontiers in separate fields, have `Clone` inherit the hold, have the setter join into both, and advance the import's hold on both axes. `acknowledged` is the right value for the physical axis too: it is exactly the frontier below which that import will never cut again. Reported by the QA LLM review on #38386. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Review feedback on #38386. A handle carried two physical frontiers, one it reported and one it held. Reporting the chain coverage satisfies the only consumer that reads the frontier back, `mz_join_core`, whose assertion compares it against the coverage it derives from `map_batches`. So the two collapse into one field seeded at the coverage, and the class of bug the split was guarding against stops being expressible. The publication point also kept a `BTreeMap` of per-registration holds on each axis, solely to recover the previous value when computing a delta into the accumulation. The handle already owns that value, so the maps go and the setters pass `previous` explicitly. A handle no longer needs a registration id at all; ids now serve only importer queues. `coverage_hold` goes the same way: the publisher falls back to the chain coverage when the accumulation is empty, which is the shape the logical axis already used. Test-only surface moves into `shared_trace/tests.rs`, which reaches private items through `super::`. `snapshot_at` gains a deadline so a wedged publisher fails with the frontier it stalled on rather than hanging. `Tr::Time: TotalOrder` is now a bound on the `TraceReader` impl, so `batches_through` stopping at the first batch beyond the cut rests on a stated property rather than a comment. The module is `pub(crate)` with an explicit `allow(dead_code)`, rather than `pub` to keep dead-code analysis quiet. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
18d0897 to
7591e7b
Compare
Review feedback on #38386. A handle carried two physical frontiers, one it reported and one it held. Reporting the chain coverage satisfies the only consumer that reads the frontier back, `mz_join_core`, whose assertion compares it against the coverage it derives from `map_batches`. So the two collapse into one field seeded at the coverage, and the class of bug the split was guarding against stops being expressible. The publication point also kept a `BTreeMap` of per-registration holds on each axis, solely to recover the previous value when computing a delta into the accumulation. The handle already owns that value, so the maps go and the setters pass `previous` explicitly. A handle no longer needs a registration id at all; ids now serve only importer queues. `coverage_hold` goes the same way: the publisher falls back to the chain coverage when the accumulation is empty, which is the shape the logical axis already used. Test-only surface moves into `shared_trace/tests.rs`, which reaches private items through `super::`. `snapshot_at` gains a deadline so a wedged publisher fails with the frontier it stalled on rather than hanging. `Tr::Time: TotalOrder` is now a bound on the `TraceReader` impl, so `batches_through` stopping at the first batch beyond the cut rests on a stated property rather than a comment. The module is `pub(crate)` with an explicit `allow(dead_code)`, rather than `pub` to keep dead-code analysis quiet. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
01accf4 to
62fe435
Compare
A handle carries two physical frontiers, and they are not interchangeable. The one it reports through `get_physical_compaction` is seeded at the published `since`, because a reported frontier may never lead the chain coverage. The one it holds is seeded at that coverage, because a merge spanning the coverage destroys the boundary the reader was seeded with. `Clone` and the setter both wrote the reported frontier into the hold, which silently lowers it. Since the accumulation is a meet, one such registration is a floor under every other hold, so the published spine stops merging: batches pile up in `Spine::pending`, one per seal, for as long as that registration lives. The cost is unbounded rather than constant, since retractions in stranded batches never consolidate and every `cursor_through` builds a `CursorList` over all of them. An import's read hold hit this on both counts. It is a clone, so it registered at `since`, and it advanced only on the logical axis, so nothing ever raised it. Measured against an unimported control over 40 seals: 39 batches against 5. Keep the two frontiers in separate fields, have `Clone` inherit the hold, have the setter join into both, and advance the import's hold on both axes. `acknowledged` is the right value for the physical axis too: it is exactly the frontier below which that import will never cut again. Reported by the QA LLM review on #38386. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Review feedback on #38386. A handle carried two physical frontiers, one it reported and one it held. Reporting the chain coverage satisfies the only consumer that reads the frontier back, `mz_join_core`, whose assertion compares it against the coverage it derives from `map_batches`. So the two collapse into one field seeded at the coverage, and the class of bug the split was guarding against stops being expressible. The publication point also kept a `BTreeMap` of per-registration holds on each axis, solely to recover the previous value when computing a delta into the accumulation. The handle already owns that value, so the maps go and the setters pass `previous` explicitly. A handle no longer needs a registration id at all; ids now serve only importer queues. `coverage_hold` goes the same way: the publisher falls back to the chain coverage when the accumulation is empty, which is the shape the logical axis already used. Test-only surface moves into `shared_trace/tests.rs`, which reaches private items through `super::`. `snapshot_at` gains a deadline so a wedged publisher fails with the frontier it stalled on rather than hanging. `Tr::Time: TotalOrder` is now a bound on the `TraceReader` impl, so `batches_through` stopping at the first batch beyond the cut rests on a stated property rather than a comment. The module is `pub(crate)` with an explicit `allow(dead_code)`, rather than `pub` to keep dead-code analysis quiet. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
779eff6 to
2e5c9a1
Compare
95a3015 to
4c08314
Compare
4c08314 to
d404ac3
Compare
|
One question I have looking at the diagrams is why the import operator has to hold a direct handle to the ImportQueue when the registry already contains some of these as well |
|
@DAlperin The registry does not hold import queues.
The alternative is one append-only log in Posted by Claude Code. |
d404ac3 to
12854df
Compare
12854df to
de39f1b
Compare
The Materialize-owned trace agent can publish its trace to a point that readers on other threads hold. Once published, the trace box mirrors the spine's span chain and frontiers into the point on every insert, and on every exertion that changed them, and applies the meet of the agents' and readers' holds to the spine. An exertion reads the holds only when a reader moved a meet, so an activation that changes nothing takes no lock. Imports on other threads replay the point through their own queue. The batch-size logger observes the trace through a weak observer instead of the trace box. A publication point is a rendezvous whose items are spans and whose hold kinds are logical and physical compaction. The rendezvous owns the synchronization: a point accepts one trace in its life, so a later incarnation publishes into a point of its own instead of replacing the chain an earlier one's readers hold, and a closed attachment leaves the point as last published. The trace box closes its attachments when the writer drops, so the empty seal the dropped writer makes stays out of the points, and readers keep the last upper the trace published whichever of trace and writer drops first. An import therefore reads one trace's chain for its life. That chain only grows until it freezes, so it never regresses below the frontier the import acknowledged. Co-Authored-By: Claude Opus 5.5 <[email protected]>
3 of 14 in the interactive-runtime stack. Stacks on #40513, followed by #38387. Tracked by CPU-215. The design document is
doc/developer/design/20261107_interactive_runtime.mdin #40559.An arrangement is normally readable only from the timely worker that maintains it. This PR lets the owned
TraceAgentfrom #40512 publish its trace to aSharedpoint, a rendezvous (#40513) whose items are spans and whose hold kinds are logical and physical compaction.TraceBoxmirrors the spine's span chain,upper, and compaction frontiers into the point on every insert, and on every exertion that changed them. The lock is held for a chain swap, never for merge work.take_holds_moved), and publishes only when the frontiers, the chain's shape, or a point's reachability changed. An activation that changes nothing takes no lock.since, so a hold below it constrains nothing rather than rewinding the spine. The point publishes thesinceit is about to apply before applying it, the safe direction.SharedReaderimplementsTraceReader, overridesread_upperto read the publishedupperwithout cloning the chain, and imports the point with the analogue ofTraceAgent::import_frontier_core. A peer handle (Shared::reader_at_least) holds logically only.SharedReader::fresh_readermints an import's reader that also holds the chain physically at the coverage it was seeded with, with a# Panicscontract on where its hold may sit.observer, replacingtrace_box_unstable.Publishedandadopt_traceinmz_compute::shared_traceare the Materialize-side glue.Not inert in code paths: every production
TraceWriterinsert, exert and drop, and the batch-size logger, now run through the new code, which does nothing for a trace nobody published. Nothing inmz-computepublishes outside tests. The glue module carries#![cfg_attr(not(test), expect(unused))]with aTODO(CPU-215), which #38390 removes.Tests in
shared_trace/tests.rscover reads and seals across threads, holds and their release, reader floors on merging, holds below thesince, imports after the writer or the trace dropped, and a join over an import against the direct result.A reviewer should check the lock order and ordering argument in
sync_holds, and thatexert's skip misses no reader move.Posted by Claude Code.
🤖 Generated with Claude Code