Skip to content

compute: a shared-trace primitive for cross-thread arrangement reads - #38386

Draft
antiguru wants to merge 1 commit into
mh/interactive-00-rendezvousfrom
mh/interactive-01-shared-trace
Draft

antiguru wants to merge 1 commit into
mh/interactive-00-rendezvousfrom
mh/interactive-01-shared-trace

Conversation

@antiguru

@antiguru antiguru commented Aug 21, 2026 •

Copy link
Copy Markdown
Member

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.md in #40559.

An arrangement is normally readable only from the timely worker that maintains it. This PR lets the owned TraceAgent from #40512 publish its trace to a Shared point, a rendezvous (#40513) whose items are spans and whose hold kinds are logical and physical compaction.

  • Once published, the agent's TraceBox mirrors 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.
  • An exertion reads the readers' holds only when one of them moved a meet (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.
  • The box applies the meet of the agents' and the readers' holds to the spine, joined with the spine's own since, so a hold below it constrains nothing rather than rewinding the spine. The point publishes the since it is about to apply before applying it, the safe direction.
  • SharedReader implements TraceReader, overrides read_upper to read the published upper without cloning the chain, and imports the point with the analogue of TraceAgent::import_frontier_core. A peer handle (Shared::reader_at_least) holds logically only. SharedReader::fresh_reader mints an import's reader that also holds the chain physically at the coverage it was seeded with, with a # Panics contract on where its hold may sit.
  • The batch-size logger observes the trace through a weak observer, replacing trace_box_unstable. Published and adopt_trace in mz_compute::shared_trace are the Materialize-side glue.

Not inert in code paths: every production TraceWriter insert, exert and drop, and the batch-size logger, now run through the new code, which does nothing for a trace nobody published. Nothing in mz-compute publishes outside tests. The glue module carries #![cfg_attr(not(test), expect(unused))] with a TODO(CPU-215), which #38390 removes.

Tests in shared_trace/tests.rs cover reads and seals across threads, holds and their release, reader floors on merging, holds below the since, 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 that exert's skip misses no reader move.

Posted by Claude Code.

🤖 Generated with Claude Code

@antiguru
antiguru force-pushed the mh/interactive-01-shared-trace branch from fda6a81 to ebd007b Compare August 21, 2026 13:24
@antiguru
antiguru force-pushed the mh/interactive-01-shared-trace branch from ebd007b to 870f336 Compare August 21, 2026 13:42
@antiguru
antiguru requested a review from DAlperin August 21, 2026 13:46
Base automatically changed from mh/interactive-00-arc-spines to main August 21, 2026 14:31
@antiguru
antiguru force-pushed the mh/interactive-01-shared-trace branch from 870f336 to b508d89 Compare August 21, 2026 14:31
pull Bot pushed a commit to Arstman/materialize that referenced this pull request Aug 21, 2026
…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]>
@antiguru
antiguru marked this pull request as ready for review August 21, 2026 16:17
@antiguru
antiguru requested a review from a team as a code owner August 21, 2026 16:17
@def-

def- commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

QA LLM Review

1. MEDIUM -- a live import permanently pins the published arrangement's physical compaction

src/compute/src/shared_trace.rs:1075

The read hold import_snapshot_at retains for the life of the import advances only on the logical axis, and Clone seeds its physical hold from the mint-time since rather than the chain coverage. The accumulated physical frontier therefore sticks at that stale since forever, the publisher never forwards anything higher, and the published spine stops merging: batches pile up in Spine::pending, one per seal, for the life of the import.

Details

Measured on a RowRowSpine published through adopt, with a live import_snapshot_at (until empty) consumed by as_collection, over 40 seal ticks against an identical unimported control:

after register: physical_holds={0: [4]}          coverage_hold=[4]  accumulated=[4]  since=[0]
after import:   physical_holds={2: [0]}          coverage_hold=[4]  accumulated=[0]  since=[0]
end:            physical_holds={2: [0]}          coverage_hold=[40] accumulated=[0]  since=[0]
chain_len: imported=39  control=5

Registration id 2 is the hold clone at shared_trace.rs:1075. register/register_at do install the hold at the chain coverage (id 0 above sits at [4], as shared_trace.rs:151 documents), but Clone at shared_trace.rs:625 writes self.physical into the new registration, and self.physical is initialised to since at shared_trace.rs:482 and shared_trace.rs:537. Nothing then moves it: shared_trace.rs:1167 follows acknowledged on the logical axis only, and the TraceFrontier clone that a join would advance is dropped at build time for every consumer that keeps only the stream. So physical_compaction's meet is pinned at the since observed when the handle was minted, agent.set_physical_compaction joins and cannot be pulled back down, and consider_merges never drains pending again because pending[0].upper() <= physical_frontier stays false.

Cost is unbounded rather than constant: retractions in stranded pending batches never consolidate, and every cursor_through builds a CursorList over a batch count that grows one per seal. On an index with a live shared import and a one-second seal cadence that is thousands of batches per hour.

Suggested fix, verified against the same probe (chain folds to 5, exactly matching the control, and the hold tracks to [40]):

                                 if let Some(hold) = hold.as_mut() {
                                     hold.set_logical_compaction(acknowledged.borrow());
+                                    hold.set_physical_compaction(acknowledged.borrow());
                                 }

acknowledged is the right value on both axes: it is exactly the frontier below which this import will never cut again, which is what the physical hold is supposed to express. Worth separately reconciling Clone at shared_trace.rs:625 with the coverage-seeded hold register/register_at install, since as written a clone silently lowers a registration's physical hold to since and the two paths disagree about the documented invariant at shared_trace.rs:151.

@antiguru

Copy link
Copy Markdown
Member Author

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 get_physical_compaction is seeded at the published since, because a reported frontier may never lead the chain coverage (mz_join_core asserts exactly that). 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 set_physical_compaction both wrote the reported frontier into the hold, so the hold silently dropped to the weaker value, and since the accumulation is a meet, one such registration is a floor under every other hold.

The 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 raised it afterwards.

The fix keeps the two frontiers in separate fields, has Clone inherit the hold, has the setter join into both, and takes the suggested set_physical_compaction(acknowledged) on the import's hold. acknowledged is right on the physical axis for the reason given: it is exactly the frontier below which that import will never cut again.

Two regression tests, each verified red without its half of the fix:

  • live_import_does_not_pin_merging — the two-arm chain-length comparison, 39 against 5 before, folding to the control after. One note on reproducing it: the minting handle has to be dropped after building the import, as render::import_shared_index does. A live mint holds its own coverage-seeded registration and pins the floor by itself, which masks the bug under test. My first attempt at this test failed for that reason rather than the one it was written for.
  • clone_inherits_the_hold_not_the_reported_frontier — asserts the registered hold directly, since the reported frontier cannot be read back through TraceReader. This is what pins the Clone half; the merge test alone does not, because the added physical advance repairs that scenario regardless of what Clone seeded.

The coverage_hold this PR already gives the publisher caps the forwarded physical frontier at the chain coverage, so advancing a reader hold to acknowledged cannot push the forwarded value past what the chain carries even though acknowledged tracks the stream frontier, which leads the coverage by up to a scheduling round.

The stack above this PR is rebased and pushed. Full run at the tip: 124 tests, workspace cargo check --all-targets, clippy, rustdoc with -D warnings.

(Posted by Claude Code.)

antiguru added a commit that referenced this pull request Aug 26, 2026
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]>
antiguru added a commit that referenced this pull request Aug 28, 2026
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]>
@antiguru
antiguru force-pushed the mh/interactive-01-shared-trace branch from 285af14 to e953639 Compare August 28, 2026 14:05
antiguru added a commit that referenced this pull request Sep 3, 2026
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]>
@antiguru
antiguru force-pushed the mh/interactive-01-shared-trace branch from e953639 to e295c50 Compare September 3, 2026 08:48
Comment thread src/compute/src/shared_trace.rs Resolved
Comment thread src/compute/src/shared_trace.rs Resolved
Comment thread src/compute/src/shared_trace.rs Resolved
Comment thread src/compute/src/shared_trace.rs Resolved
Comment thread src/compute/src/shared_trace.rs Resolved
Comment thread src/compute/src/shared_trace.rs Resolved
Comment thread src/compute/src/shared_trace.rs Resolved
Comment thread src/compute/src/shared_trace.rs Resolved
Comment thread src/compute/src/shared_trace.rs Resolved
Comment thread src/compute/src/shared_trace.rs Resolved
antiguru added a commit that referenced this pull request Sep 3, 2026
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]>
@def-

def- commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

QA LLM Review

1. MEDIUM -- empty_logical_request_releases_the_hold can no longer fail

src/compute/src/shared_trace/tests.rs:687

Rewriting the assertion against the accumulated MutableAntichain made it vacuous: the handle registers at since and the standing hold sits at the same minimum time, so logical_holds() == standing_hold() holds identically whether or not the empty request released anything. The test named for the release property can no longer observe it, and no other test in the file does either.

Details

Verified by mutation. With set_logical_compaction changed to ignore an empty request outright, so the handle's hold is never released:

let next = self.logical.join(&frontier.to_owned());
if next.is_empty() { return; }   // hold is never released

all 17 shared_trace tests still pass. The concealed regression is real: an unreleased logical hold sits in logical_compaction for the life of the handle, the publisher forwards that frontier to its agent every activation, and the published arrangement stops compacting past it. That is exactly what the reduce operator triggers on every dataflow whose input finishes, per the test's own doc.

The accumulation is a meet, so a single hold at or above the standing hold is unobservable through logical_holds(). Releasing the standing hold first makes the accumulation carry the handle's contribution alone:

published.note_standing_hold(&Antichain::new());
assert!(published.logical_holds().is_empty());

let mut hold = published.handle();
assert!(!published.logical_holds().is_empty());
hold.set_logical_compaction(Antichain::new().borrow());
assert_eq!(published.logical_holds(), Antichain::new(), "...");

That version passes on the current code and fails on the mutation above.

@linear-code

linear-code Bot commented Sep 3, 2026

Copy link
Copy Markdown

CPU-215

@antiguru
antiguru force-pushed the mh/interactive-01-shared-trace branch 2 times, most recently from a38f3a0 to 18d0897 Compare September 3, 2026 15:41
antiguru added a commit that referenced this pull request Sep 4, 2026
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]>
antiguru added a commit that referenced this pull request Sep 4, 2026
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]>
@antiguru
antiguru force-pushed the mh/interactive-01-shared-trace branch from 18d0897 to 7591e7b Compare September 4, 2026 15:41
antiguru added a commit that referenced this pull request Sep 25, 2026
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]>
@antiguru
antiguru force-pushed the mh/interactive-01-shared-trace branch from 01accf4 to 62fe435 Compare September 25, 2026 10:53
Comment thread src/timely-util/src/shared_trace.rs Resolved
antiguru added a commit that referenced this pull request Oct 2, 2026
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]>
antiguru added a commit that referenced this pull request Oct 2, 2026
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]>
@antiguru
antiguru force-pushed the mh/interactive-01-shared-trace branch from 779eff6 to 2e5c9a1 Compare October 2, 2026 08:28
@antiguru
antiguru removed this pull request from stack #38397 October 2, 2026 15:11
@antiguru
antiguru added this pull request to stack #39485 October 2, 2026 15:15
@antiguru
antiguru force-pushed the mh/interactive-01-shared-trace branch 4 times, most recently from 95a3015 to 4c08314 Compare October 6, 2026 14:30
@antiguru
antiguru force-pushed the mh/interactive-01-shared-trace branch from 4c08314 to d404ac3 Compare October 13, 2026 15:39
@antiguru
antiguru removed the request for review from petrosagg October 13, 2026 15:56
@DAlperin

Copy link
Copy Markdown
Member

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

@antiguru

Copy link
Copy Markdown
Member Author

@DAlperin The registry does not hold import queues. State keeps them as Vec<Weak<ImportQueue>>, and the import operator's Arc is the only strong reference (src/timely-util/src/shared_trace.rs, ImportQueue and import_frontier_core). The diagram makes this look otherwise. The State -> ImportQueue edge should read "Weak, pruned on publish", and I've updated it. The import operator holds the queue directly for three reasons.

  • A queue belongs to one importer, not to an index. Each queue is seeded with the chain as it stood when that importer registered, and it holds only the instructions that importer has not drained yet. Two imports of the same index are at different positions. One queue per point would need a cursor per reader, plus trimming.
  • Ownership is the lifetime. Queued batches are Arcs that keep memory alive. When the operator drops, its queue drops with it. At the next publish the writer's Weak fails to upgrade, and the writer prunes the entry (State::live_queues). If the registry held the queue strongly, every dropped import would need an explicit unregister, and a missed one would pin batches for the life of the slot.
  • Draining takes no shared lock. Each activation locks only the operator's own queue. Reaching the queue through the registry would mean taking the registry mutex and then the State mutex on every drain, which contends with the writer publishing. It would also mean an id lookup, and the registry maps an id to its newest incarnation. An importer has to stay on the point that seeded it. When a new writer attaches, the old queues are cleared on purpose rather than moved over to the new chain.

The alternative is one append-only log in State with a cursor per importer, instead of a queue per importer. That saves a clone per importer when an index has many importers, but it moves the trimming and lifetime bookkeeping into State. It hasn't been measured, and nothing in the stack depends on it.

Posted by Claude Code.

@antiguru
antiguru marked this pull request as draft November 5, 2026 11:22
@antiguru
antiguru removed this pull request from stack #39485 November 5, 2026 16:26
@antiguru
antiguru changed the base branch from main to mh/interactive-00-rendezvous November 5, 2026 16:26
@antiguru
antiguru added this pull request to stack #40514 November 5, 2026 16:26
@antiguru
antiguru force-pushed the mh/interactive-01-shared-trace branch from d404ac3 to 12854df Compare November 5, 2026 16:26
@antiguru
antiguru removed this pull request from stack #40514 November 6, 2026 03:11
@antiguru
antiguru force-pushed the mh/interactive-01-shared-trace branch from 12854df to de39f1b Compare November 6, 2026 03:11
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]>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants