Skip to content

persist: deasync shard_source operators#37751

Draft
antiguru wants to merge 4 commits into
MaterializeInc:mainfrom
antiguru:mh/persist-deasync-v2
Draft

persist: deasync shard_source operators#37751
antiguru wants to merge 4 commits into
MaterializeInc:mainfrom
antiguru:mh/persist-deasync-v2

Conversation

@antiguru

Copy link
Copy Markdown
Member

Motivation

The persist source operators are built on the timely async bridge, which we are migrating away from (cf. #36810). This converts them to synchronous operators with no behavior change.

Rebase/re-derivation of #36910 on current main. Folds back in the two features that landed on the async operators after that PR was first written: concurrent part fetching (#37315) and hydration frontier coalescing (#37313).

Description

Convert shard_source_descs and shard_source_fetch from AsyncOperatorBuilder to synchronous OperatorBuilderRc operators, each paired with a tokio task that owns the persist I/O.

  • shard_source_descs runs a listen task on the chosen worker that owns the reader, snapshot, listen loop, and stats filtering, and sends parts (split into ExchangeableBatchPart + Lease) plus progress over a channel. The former shard_source_descs_return operator is merged in as a disconnected completed_fetches input; the listen handle (the reader's SeqNo hold) is released via a oneshot once that frontier empties. The listen task coalesces hydration-time frontier downgrades under persist_source_hydration_frontier_coalesce_bytes while parts keep flowing per batch.
  • shard_source_fetch forwards descs to a fetch task and retains a per-flight capability pair keyed by time; results are matched by time and emitted at the data capability, with the completed-fetches capability dropped to release the lease only after the download completes. The fetch task runs up to persist_source_fetch_concurrency downloads at once via a FuturesUnordered of cloned BatchFetchers. On a missing blob it reports through the ErrorHandler and freezes, so a later good result cannot advance the frontier past the missing part.
  • Both operators reproduce builder_async's two-phase shutdown (build_reschedule + the coordinated button), so a local-only press cannot advance the downstream frontier past times other workers' instances still feed.
  • filter_fn, listen_sleep, and start_signal gain Send bounds; all callers already satisfy them.
  • The txn-wal stress test parked its read workers indefinitely between steps, relying on the async operators' listen retries as accidental wakeups; with the listen in a task a caught-up source produces none, so the parks are now bounded.

🤖 Generated with Claude Code

https://claude.ai/code/session_01N1erbcNjoki3HsfBJCTFUd

antiguru and others added 4 commits July 20, 2026 16:51
Convert `shard_source_descs` and `shard_source_fetch` from
`AsyncOperatorBuilder` to synchronous `OperatorBuilderRc` operators, each
paired with a tokio task that owns the persist I/O.

* `shard_source_descs` runs a listen task on the chosen worker that owns the
  reader, snapshot, listen loop, and stats filtering, and sends parts (split
  into `ExchangeableBatchPart` + `Lease`) plus progress over a channel. The
  former `shard_source_descs_return` operator is merged in as a disconnected
  `completed_fetches` input; the listen handle (the reader's SeqNo hold) is
  released via a oneshot once that frontier empties, preserving the old lease
  lifetime.
* `shard_source_fetch` forwards descs to a fetch task and retains a
  per-flight capability pair keyed by time; results are matched by time and
  emitted at the data capability, with the completed-fetches capability
  dropped to release the lease only after the download completes. On a
  missing blob it reports through the `ErrorHandler` and freezes, so a later
  good result cannot advance the frontier past the missing part.
* Both operators reproduce `builder_async`'s two-phase shutdown
  (`build_reschedule` + the coordinated button), so a local-only press cannot
  advance the downstream frontier past times other workers' instances still
  feed.
* `filter_fn`, `listen_sleep`, and `start_signal` gain `Send` bounds; all
  callers already satisfy them.

Preserves the two features that landed on the async operators after this work
was first written: the fetch task runs up to `persist_source_fetch_concurrency`
fetches at once via a `FuturesUnordered` of cloned `BatchFetcher`s (the
time-keyed operator bookkeeping tolerates out-of-order completion), and the
listen task coalesces hydration-time frontier downgrades under
`persist_source_hydration_frontier_coalesce_bytes` while parts keep flowing
per batch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1erbcNjoki3HsfBJCTFUd
The stress test parked its read workers indefinitely between steps, relying
on the async persist source operators' listen retries as accidental wakeups.
With the listen loop in a tokio task, a caught-up source with no retry timer
produces no further activations, so the parks are now bounded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1erbcNjoki3HsfBJCTFUd
The lease-return operator is now a disconnected input on the merged
`shard_source_descs` operator, so the introspection relation reports that
name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1erbcNjoki3HsfBJCTFUd
The `shard_source_fetch` task drives its downloads with a `FuturesUnordered`,
which only advances the futures inside it while it is being polled. The task
forwarded each completed download with `blob_tx.send().await`, so once the
bounded result channel filled the task blocked in `send` and stopped polling
`in_flight`. That froze every other in-flight fetch, collapsing effective
fetch concurrency far below `persist_source_fetch_concurrency` (the channel
capacity became the real ceiling) and leaving persist reads slow while CPU and
network sat idle.

Park completed downloads in a `ready` queue and forward them with a reserved
permit instead. When the channel is full the reserve arm stays pending rather
than blocking the loop, so `in_flight` keeps being polled and the downloads
overlap. Resident parts are bounded by `max_concurrency` (we stop starting new
fetches once `in_flight.len() + ready.len()` reaches the cap), which replaces
the channel capacity as the memory bound.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ALAjT4DRJhPf99SwXgpQdz
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.

1 participant