Skip to content

compute: publish maintained indexes into the sharing registry - #38389

Draft
antiguru wants to merge 1 commit into
mh/interactive-03-multiplexfrom
mh/interactive-04-publish
Draft

antiguru wants to merge 1 commit into
mh/interactive-03-multiplexfrom
mh/interactive-04-publish

Conversation

@antiguru

@antiguru antiguru commented Aug 21, 2026 •

Copy link
Copy Markdown
Member

6 of 14 in the interactive-runtime stack. Stacks on #38388, followed by #38390. Tracked by CPU-215. The design document is doc/developer/design/20261107_interactive_runtime.md in #40559.

Both export paths publish their oks/errs arrangements through the runtime's publisher, chosen when ComputeState is built: the sharing registry on maintenance, none elsewhere. The PublishToken it returns rides on the exported TraceBundle, so dropping the trace ends the publication. A re-export arm, where an index reuses another index's arrangement, publishes the shared trace under a publication point of its own. A trace can be published to several points, and its trace box applies the meet of every point's holds to the spine, so each id keeps its own holds and seal notifications, and the controller can compact the two collections independently. Publishing adds no operators to any dataflow. A reader that bound the re-export's id before the render holds an unbacked point, which the publish backs in place like any other. clusterd creates one registry per local worker ordinal, and each worker takes the one for its ordinal when it is built. Each maintenance worker attaches as its registry's publisher. Logging indexes publish through the same publisher, at the minimum time.

ComputeRuntimeRole::publishes_indexes chooses the publisher, and is true for Maintenance only. Interactive stays test-only until #38391 constructs it.

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. No goldens move.

The test reexport_publishes_its_own_point_over_the_same_trace covers the re-export arm.

A reviewer should check that every path that sets an index's trace in traces publishes it, and that the token lives exactly as long as the bundle.

Posted by Claude Code.

🤖 Generated with Claude Code

@antiguru
antiguru force-pushed the mh/interactive-04-publish branch from d31414b to d9ddff4 Compare August 21, 2026 11:23
@antiguru
antiguru force-pushed the mh/interactive-04-publish branch 2 times, most recently from d754ecc to 1303035 Compare August 21, 2026 13:42
@antiguru
antiguru requested a review from DAlperin August 21, 2026 13:46
@antiguru
antiguru force-pushed the mh/interactive-04-publish branch from 1303035 to dffb179 Compare August 21, 2026 14:31
@antiguru
antiguru force-pushed the mh/interactive-04-publish branch from dffb179 to 7d70e54 Compare August 21, 2026 17:54
@antiguru
antiguru force-pushed the mh/interactive-04-publish branch from 7d70e54 to 6e430c4 Compare August 28, 2026 14:06
@antiguru
antiguru force-pushed the mh/interactive-04-publish branch from 6e430c4 to 8ba6dce Compare September 3, 2026 08:48
@antiguru
antiguru force-pushed the mh/interactive-04-publish branch from 8ba6dce to 3f24fa0 Compare September 3, 2026 15:58
@antiguru
antiguru force-pushed the mh/interactive-04-publish branch from 3f24fa0 to d3a7b3d Compare September 4, 2026 15:57
@antiguru
antiguru force-pushed the mh/interactive-04-publish branch from d3a7b3d to 188d168 Compare September 4, 2026 17:38
@antiguru
antiguru force-pushed the mh/interactive-04-publish branch from 188d168 to aac9441 Compare September 4, 2026 19:37
@antiguru
antiguru marked this pull request as ready for review September 4, 2026 19:39
@antiguru
antiguru requested a review from a team as a code owner September 4, 2026 19:40
@antiguru
antiguru requested a review from petrosagg September 4, 2026 19:40
@def-

def- commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

QA LLM Review

1. MEDIUM -- The re-export arm overwrites a registry slot an interactive import has already bound to

src/compute/src/render.rs:801

ArrangementSharingRegistry::reexport assigns slots[worker_index] = Some(arr) unconditionally (src/compute/src/sharing.rs:161), so when the interactive runtime has already created a placeholder for idx_id and built its import over it, that placeholder is orphaned. Nothing ever adopts it, its chain stays empty and its upper stays at minimum, so the importing dataflow never produces and the peek behind it hangs until cancelled.

Details

Every other publish path goes through get_or_create + PublishArrangement::adopt precisely so that a point a reader has already imported is backed in place "rather than being overwritten by a second, disconnected arrangement" (sharing.rs:111). The comment on the new call site claims that same property for the alias, but reexport does not have it.

The reader binds once and never re-resolves: import_shared_index takes slot = registry.get_or_create(idx_id, ..), mints slot.oks/slot.errs handles, builds import_snapshot_at over them and keeps the Arc in tokens for the life of the dataflow. A later map replacement is invisible to it.

The ordering that triggers this is the one get_or_create is built for ("whichever side ... touches id first creates the slot"): a peek dataflow routed to interactive renders as soon as it arrives, while maintenance's own CreateDataflow for idx_id may still be queued behind other render work. Reaching the re-export arm at all needs only a second index on an already-indexed key, e.g. CREATE INDEX i2 ON t (k) while i1 ON t (k) exists: i2's dataflow imports i1 and re-exports its arrangement.

Either make reexport fill only an unoccupied slot (and report loudly otherwise, since silently orphaning a live reader is worse than a panic), or resolve the alias on the read side so an importer of idx_id reaches gid's slot rather than a second map entry that can be swapped underneath it.

2. MEDIUM -- Aliasing two ids onto one publication point makes them share per-collection compaction state, refusing reads the arrangement can still serve

src/compute/src/render.rs:801

reexport stores the same Arc<SharedIndexArrangement> under gid and idx_id, so the two collections share one Published/SharedTrace. The controller's compaction frontier is per collection, but the registry funnels both ids into that one point: note_writer_logical assigns (shared_trace/publish.rs:111) so the last id the controller sent wins, and advance_standing_hold joins (shared_trace/state.rs:158) so the standing hold becomes the max of the two. When the two frontiers diverge, the publisher's since can settle at the faster id's frontier, and a read on the slower id at a timestamp the arrangement still holds is refused.

Details

The refusal surfaces two ways. On the fast path, the peek's handle is registered at the published since, gate_peek compares it to peek.timestamp and returns PeekResponse::Error("Arrangement compaction frontier ... is beyond the time of the attempted read") -- a spurious user-visible error on a valid read. On the slow path, import_shared_index's handle_at(as_of) fails and report_compacted_past panics the replica.

The underlying trace is not actually over-compacted: compute_state.traces.set(idx_id, trace) clones the TraceBundle, so gid and idx_id keep distinct TraceAgents and the TraceBox meet still protects the data. That is why this shows up as a false refusal rather than wrong results, and why it cannot be caught by watching for corruption.

Divergence between two indexes on the same key is ordinary: a freshly created duplicate starts at its create as_of while the original sits a compaction lag behind, and a read hold (a SUBSCRIBE, an explicit AS OF) on one and not the other keeps them apart for as long as it is held. writer_logical then flips between the two ids' frontiers as the controller's AllowCompaction messages interleave, so the failure is intermittent rather than sticky.

The publication point's writer_logical and standing_hold are properties of a collection, not of an arrangement. If two collections are going to share one point, those two fields need to be tracked per id and combined by meet, not overwritten and joined.

@antiguru
antiguru force-pushed the mh/interactive-04-publish branch from d60d9b2 to feba37d Compare September 25, 2026 10:54
@def-

def- commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

QA LLM Review

1. HIGH -- publish_reexport adds operators only on the workers that fall back, so maintenance workers build different dataflow graphs

src/compute/src/render.rs:980

publish_alias decides separately for each worker, but only its false branch builds operators (two import_named sources and a log_dataflow_errors unary). Timely requires every worker to build the same graph. When interactive readers bind the re-export's id on some workers but not others, the re-export's dataflow gets extra operators on only some maintenance workers, and the replica panics or cross-wires its channels.

Details

Each interactive worker binds the id through its own get_or_create(idx_id, outer.index(), ..) at render time, without waiting for the publisher. Maintenance workers render independently too, so a mixed outcome is ordinary. alias_bookkeeping_is_per_worker models exactly this split. A peek imports the re-export once the original is gone from the catalog but still alive on the replica: CREATE INDEX i2 ON t (k); DROP INDEX i1; SELECT ... FROM t, or a replica replaying its history in that state.

On the workers that fall back, each new operator takes a child index in the build region and a value from the worker's new_identifier() counter (timely 0.31 Scope::reserve_operator, new_input_connection). Their capability changes are broadcast to peers that have no such child. timely's Tracker::propagate_all indexes per_operator[source.node] without a bounds check, so those peers panic. Even without that panic, the fallback worker's identifier counter now runs ahead, so the exchange channels of every dataflow rendered after it get ids that do not match their counterparts on the other workers.

Fix: back the reader's placeholder without building anything. adopt needs only the TraceAgent, a worker handle for the activator, and the point, so gid's spines can be attached to idx_id's point directly from the TraceBundle, with no import_named. Drop the log_dataflow_errors as well. The re-export's dataflow then has no operators on any worker, so mz_compute_error_counts forwards the dependency's counts to it on every worker. The other option, building the import on every publishing worker whatever the alias outcome, brings back the per-re-export memory cost that aliasing exists to avoid.

@antiguru
antiguru force-pushed the mh/interactive-04-publish branch from feba37d to 8a403c9 Compare September 25, 2026 12:56
@antiguru

Copy link
Copy Markdown
Member Author

Agreed. Fixed in 8a403c9: the fallback now attaches the source's traces to the reader's point directly, through a new ArrangementSharingRegistry::publish_traces that builds no operators, and it no longer logs errors. The re-export's dataflow has no operators on any worker in either case, so mz_compute_error_counts forwards the source's counts on every worker. A test checks that the fallback allocates no more timely identifiers than an empty dataflow, and it fails against the old import path. TraceBundle::import_named, which only the fallback used, is gone. The commit message and PR description are updated.

Posted by Claude Code.

@antiguru
antiguru force-pushed the mh/interactive-04-publish branch from 8a403c9 to 746a81c Compare September 25, 2026 17:06
Comment thread src/compute/src/render.rs Resolved
Comment thread src/compute/src/sharing.rs Resolved
Comment thread src/compute/src/sharing.rs Resolved
Comment thread src/compute/src/sharing.rs Resolved
Comment thread src/compute/src/shared_trace/publish.rs Resolved
Comment thread src/compute/src/sharing.rs Resolved
@antiguru
antiguru force-pushed the mh/interactive-04-publish branch from 746a81c to e8cfc40 Compare September 29, 2026 09:05
Comment thread src/compute/src/sharing.rs Resolved
Comment thread src/compute/src/sharing.rs Resolved
Comment thread src/compute/src/sharing.rs Resolved
Comment thread src/compute/src/sharing.rs Resolved
@antiguru
antiguru force-pushed the mh/interactive-04-publish branch from 22b8f02 to 6cbaddd Compare October 1, 2026 12:55
@antiguru
antiguru force-pushed the mh/interactive-04-publish branch from 6cbaddd to 25a37b0 Compare October 1, 2026 13:24
@def-

def- commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

QA LLM Review

1. MEDIUM -- A dropped re-export's publication point stays attached to the original index's trace, and keeps receiving updates and seal notifications

src/compute/src/render.rs:838

The re-export arm attaches a second point to gid's SharedSpine, and nothing ever detaches it. attach only appends (src/timely-util/src/shared_trace.rs:328), and the spine's Drop is the only place an attachment is released (:541). Each create/drop of an index that re-exports a long-lived one therefore leaves dead points behind, one per worker for each of oks and errs. gid's arrange operator keeps feeding them until gid itself drops.

Details

Dropping is the normal order, not an edge case. The controller holds gid through the re-export's dependency read hold and releases it only when the re-export goes away (src/compute-client/src/controller/instance.rs:2002), so the re-export always drops first. registry.remove (wired into drop_collection later in the stack) only clears the map entry. Published's doc assumes the writer "lives with its dataflow" (src/compute/src/shared_trace/publish.rs:39), which holds for the Local arm but not here, where the writer lives with gid's dataflow.

Each dead point costs a chain clone and a lock on every mutation of gid's trace, and another lock in every apply_holds. It also fires an on_seal that takes the process-wide registry mutex and marks the dropped id dirty for the interactive worker. The interactive runtime contends on that same mutex in take_dirty and in imports. In the scratch test, after remove(&reexport) the point is still alive, its upper advances 1→2→3, and take_dirty(0) returns the removed id after every seal.

A cheap fix is to prune attachments nobody can read any more. In publish_chain, keep only the attachments where Arc::strong_count(&attachment.shared) > 1. Once the registry slot and every reader handle are gone, the attachment holds the last Arc, so no other thread can mint a reader from it and the check cannot race.

@antiguru
antiguru force-pushed the mh/interactive-04-publish branch from 25a37b0 to 75f75b5 Compare October 1, 2026 14:16
@antiguru

antiguru commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

Re the QA review above (dropped re-export's point stays attached): confirmed. Publishing a re-export under its own point attaches a second point to gid's trace, and attach only ever appended, so the point outlived the re-export until gid dropped.

Fixed in the primitive (#38386, 779eff6e15), with the check you suggested. SharedSpine::publish_chain first detaches every attachment whose Arc<Shared> has no other reference. The registry slot, the Published owner and each reader hold one. Minting a reader needs one of them, so nothing can revive the point concurrently. a_point_nothing_references_is_detached fails without the fix: the dropped point stays alive. It passes with the fix, and the trace's other point keeps receiving updates.

Posted by Claude Code

Comment thread src/compute/src/sharing.rs Resolved

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.

2 participants