Skip to content

Arc-backed spines for cross-thread arrangement sharing#807

Draft
antiguru wants to merge 7 commits into
TimelyDataflow:master-nextfrom
antiguru:claude/spines-differential-arc-j93mho
Draft

Arc-backed spines for cross-thread arrangement sharing#807
antiguru wants to merge 7 commits into
TimelyDataflow:master-nextfrom
antiguru:claude/spines-differential-arc-j93mho

Conversation

@antiguru

@antiguru antiguru commented Jul 20, 2026

Copy link
Copy Markdown
Member

Summary

Adds the ability to read an arrangement from a thread other than the one that maintains it, in additive commits.

trace: add Arc-backed spine variants for cross-thread sharing

Adds arc_blanket_impls (a mirror of rc_blanket_impls using Arc) and the ArcOrdValSpine/ArcOrdKeySpine spines with their ArcOrdValBuilder/ArcOrdKeyBuilder, re-exported as ArcValSpine/ArcKeySpine/ArcValBuilder/ArcKeyBuilder.

This is purely additive. The default OrdValSpine/OrdKeySpine, the RcOrd*Builders, and the Val*/Key* aliases stay Rc-backed, so existing users are unaffected. An Arc'd batch whose contents are Send + Sync can be handed to and read from another thread, which the Rc variants cannot. Atomic reference counting is marginally more expensive (per batch clone/drop, never per record), so Rc remains the default and callers opt into Arc only when they need the sharing.

arrange: share arrangements across timely runtimes

Adds operators::arrange::sharing, a primitive for reading an arrangement from outside the owning worker, built on the Arc-backed spines:

  • Arranged::publish attaches a publisher operator to an arrangement and returns a Published, whose handle() mints Clone + Send SharedTraceHandles.
  • SharedTraceHandle implements TraceReader, so downstream operators drive its compaction and acquire cursors as with any trace handle.
  • SharedTraceHandle::snapshot_at serves a consistent point/full-scan read from any thread, blocking until the publication frontier passes the query time and gating on since <= t so a read past compaction fails rather than returning stale rows.
  • SharedTraceHandle::import replays the shared arrangement into another scope, so a second timely runtime can consume it as an ordinary Arranged.

The publisher forwards the meet of reader holds to its own TraceAgent (the sole writer of the trace's compaction), never the empty meet of zero holds. Handle clones carry independent holds. Compaction is applied to the agent outside the shared lock, since physical compaction can run an unbounded merge. The published since is the frontier the trace will actually hold after forwarding, so a concurrently registering handle cannot latch a floor below the trace's real compaction. A late importer registering after the publisher closed seeds its own terminal frontier rather than hanging.

arrange/sharing: import asserts equal total peers

Records the publisher's total peer count at publish time and asserts an importing scope's peers() matches it. Pairwise import (importer worker i reads publisher worker i) is sound only when both sides shard by key.hashed() % peers, which requires equal total peers (workers-per-process times processes), not just equal workers-per-process. A mismatch now panics at import time instead of silently reading the wrong shard.

arrange/sharing: publishing no longer pins compaction

Previously the publisher held its own TraceAgent at the publish-time since, which never advanced, so merely publishing an arrangement froze the trace's compaction for the life of the arrangement even with zero readers. The publisher now sources the writer-driven compaction frontier from the trace box (the meet of all other handles' holds, i.e. the box's counted meet minus the publisher's own contribution) and forwards that when no reader holds exist, and the meet of reader holds when they do. Publishing carries no independent compaction floor: only a live importer's own as_of hold holds the trace back, released on drop. The published since equals the trace's real compaction, so a live reader can still read at times it holds.

Motivation

This unblocks sharing an arrangement between two adjacent timely runtimes in one process (for example, a maintenance runtime and a latency-sensitive query runtime), and reading arrangement snapshots from non-timely threads.

Testing

  • tests/trace.rs: reads an Arc'd batch from another thread.
  • tests/sharing.rs: a cross-thread snapshot, a cross-dataflow import through a shared handle, publish_without_readers_does_not_pin_compaction, import_hold_pins_then_releases, and import_asserts_equal_peers.
  • Full existing test suite passes; the Rc default path is unchanged.

🤖 Generated with Claude Code

claude added 2 commits July 20, 2026 08:10
Add `arc_blanket_impls`, a mirror of `rc_blanket_impls` using `Arc`, and
`ArcOrdValSpine`/`ArcOrdKeySpine` with their `ArcOrdValBuilder`/`ArcOrdKeyBuilder`
alongside the existing `Rc` spines and builders. Also re-export them as
`ArcValSpine`/`ArcKeySpine`/`ArcValBuilder`/`ArcKeyBuilder`.

The addition is purely additive. The default `OrdValSpine`/`OrdKeySpine` and
`Val`/`Key` aliases remain `Rc`-backed, so existing users are unaffected. An
`Arc`'d batch whose contents are `Send + Sync` can be handed to and read from a
thread other than the one maintaining the trace, which the `Rc` variants cannot
do. Atomic reference counting is marginally more expensive, so `Rc` stays the
default and callers opt into `Arc` only when they need the sharing.

A test reads an `Arc`'d batch from another thread.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FwFQJa3HSy8oGJZ3X9uGZx
Add operators::arrange::sharing, a primitive for reading an arrangement from
outside the worker that maintains it, built on the Arc-backed spines.

Arranged::publish attaches a publisher operator to an arrangement on the owning
worker and returns a Published, whose handle() mints Clone + Send
SharedTraceHandles. A handle implements TraceReader, so downstream operators
drive its compaction and acquire cursors as with any trace handle.
SharedTraceHandle::snapshot_at serves a consistent point or full-scan read from
any thread, blocking until the publication frontier passes the query time.
SharedTraceHandle::import replays the shared arrangement into another scope, so
a second timely runtime can consume it as an ordinary Arranged.

The publisher forwards the meet of reader holds to its own TraceAgent, the sole
writer of the trace's compaction, never the empty meet of zero holds (which
would irreversibly release the trace). Handle clones carry independent holds,
so two consumers of one import cannot release each other's holds. Compaction is
applied to the agent outside the shared lock, since physical compaction can run
an unbounded merge. The published since is the frontier the trace will actually
hold after forwarding, so a handle registering concurrently cannot latch a
floor below the trace's real compaction. A late importer registering after the
publisher closed seeds its own terminal frontier, so it drains and releases
rather than hanging.

Tests run a cross-thread snapshot and a cross-dataflow import through a shared
handle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FwFQJa3HSy8oGJZ3X9uGZx
…peers work

Corrections from adversarial review and the two-runtime compute integration
design:

- snapshot_at returns None when since has advanced beyond the requested time, so
  a point read past compaction fails safe instead of returning coalesced
  results. Mirrors the single-runtime peek path's since gate.
- batches_through asserts a non-empty batch does not straddle the requested cut,
  matching Spine::batches_through, so an import cannot silently return updates at
  times not before the cut and corrupt a downstream cursor_through.
- TODO documenting that publishing currently pins compaction: the publisher
  forwards its own frozen hold, which never advances and pins the trace's
  effective since at publish time. The fix is design-laden (publishing must
  carry no independent floor) and is left to the integration with tests.
- TODO documenting the equal-peers assertion import should carry.

Read holds registered by import are correct and intended. Existing sharing and
trace tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FwFQJa3HSy8oGJZ3X9uGZx
@antiguru
antiguru force-pushed the claude/spines-differential-arc-j93mho branch from 08ecc0d to f497181 Compare July 20, 2026 19:25
antiguru and others added 2 commits July 20, 2026 23:03
Pairwise import (importer worker i reads publisher worker i) is sound
only when both runtimes shard keys by the same total peer count. Record
the publisher's peers at publish time and assert scope.peers() matches
it in import, so a mismatch fails loudly instead of silently reading
the wrong shard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Publishing an arrangement through the shared-trace primitive froze the
trace's logical compaction at the publish-time since for the life of the
arrangement, even with zero readers, because the publisher's own agent
hold pinned the trace-box meet and never advanced.

Source the writer-driven frontier from the trace box's accumulated holds
with the publisher's own contribution removed, and advance the
publisher's hold to it so it follows the writer instead of pinning. The
per-batch since cannot serve as this source: it only advances when the
Spine compacts, which the publisher's own hold prevents. Publish exactly
the trace's real compaction so a live importer hold keeps its own
frontier readable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Imports a published arrangement as a static snapshot at `as_of`, emitting the
sealed chain once (when `upper` reaches `as_of`) through a TraceFrontier/
BatchFrontier wrapper advanced to `as_of` and bounded by `until`, then releasing
the capability. Unlike the live `import`, there is no forward change stream, so
the forward-batch capability handling and the lack of `as_of` coalescing (both
unsound for a read at `as_of`) do not apply. Times are advanced on read, so the
shared Arc batches are reused, never re-arranged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@antiguru
antiguru force-pushed the claude/spines-differential-arc-j93mho branch from b6afe61 to b57e366 Compare July 21, 2026 20:14
Add public TraceBox::logical_compaction()/physical_compaction() accessors
returning the accumulated MutableAntichain holds of all referees. This lets an
external referee (a cross-runtime arrangement publisher living in a downstream
crate) compute the frontier the trace would compact to without its own hold,
which previously required reading the pub(crate) fields from inside this crate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@antiguru
antiguru marked this pull request as draft July 22, 2026 11:44
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