Skip to content

Commit 652f314

Browse files
committed
persist: fetch source parts concurrently
Hydrating a shard with many small parts is bound by the blob-store round-trip rather than bandwidth. Let `shard_source_fetch` issue several part fetches at once so the round-trips overlap. The fetch operator stays an async timely operator. Two changes make it safe to complete fetches out of order: * Key the operator's capability bookkeeping by the time a part was minted at rather than by arrival (FIFO) order. For each time we retain a data and a completed-fetches capability and count the outstanding fetches at that time; when the count reaches zero both capabilities drop, advancing the data frontier and releasing that time's leases. Results can now complete in any order without advancing the frontier past unproduced data. * Run up to `persist_source_fetch_concurrency` (default 1, i.e. the previous serial behavior) fetches at once via a `FuturesUnordered`, each on a cheap per-call `BatchFetcher` clone that shares the schema cache. In-flight bytes stay bounded by the existing fetch semaphore inside `fetch_leased_part`. On a missing blob the operator reports the error and freezes (retains all capabilities, stops draining results) exactly as before, so the frontier never advances past a part we failed to emit. New tests cover data fetching, mid-stream shutdown, the listing- and fetch-path freezes, and the concurrent fetch and concurrent-freeze paths. Recreates antiguru#181 on top of the async operators, omitting that PR's deasync refactor of the shard_source operators (and its dependent txn-wal and introspection-naming follow-ups). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MR6SwYEgWVmMJUGn4ZmLMX
1 parent 5f2a19d commit 652f314

3 files changed

Lines changed: 783 additions & 60 deletions

File tree

src/persist-client/src/cfg.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ pub fn all_dyncfgs(configs: ConfigSet) -> ConfigSet {
312312
.add(&crate::cfg::USE_CRITICAL_SINCE_CATALOG)
313313
.add(&crate::cfg::USE_CRITICAL_SINCE_SOURCE)
314314
.add(&crate::cfg::USE_CRITICAL_SINCE_SNAPSHOT)
315+
.add(&crate::cfg::SOURCE_FETCH_CONCURRENCY)
315316
.add(&BATCH_BUILDER_MAX_OUTSTANDING_PARTS)
316317
.add(&COMPACTION_HEURISTIC_MIN_INPUTS)
317318
.add(&COMPACTION_HEURISTIC_MIN_PARTS)
@@ -489,6 +490,17 @@ pub const USE_CRITICAL_SINCE_SOURCE: Config<bool> = Config::new(
489490
"Use the critical since (instead of the overall since) in the Persist source.",
490491
);
491492

493+
/// Maximum number of part fetches the persist source issues concurrently, per
494+
/// worker. Concurrent fetches amortize the blob-store round-trip, which
495+
/// dominates when there are many small parts (e.g. a fine-grained hydration
496+
/// replay). `1` keeps the previous serial behavior.
497+
pub const SOURCE_FETCH_CONCURRENCY: Config<usize> = Config::new(
498+
"persist_source_fetch_concurrency",
499+
1,
500+
"Maximum number of part fetches the persist source issues concurrently per worker \
501+
(1 = serial).",
502+
);
503+
492504
/// Migrate snapshots to use the critical since when opening a new read handle.
493505
pub const USE_CRITICAL_SINCE_SNAPSHOT: Config<bool> = Config::new(
494506
"persist_use_critical_since_snapshot",

src/persist-client/src/fetch.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,33 @@ where
155155
pub(crate) _phantom: PhantomData<fn() -> (K, V, T, D)>,
156156
}
157157

158+
// Hand-written (rather than derived) so cloning does not require `K: Clone`
159+
// etc.: every field is an `Arc` or independently `Clone`. The `schema_cache`
160+
// clone shares the schema-lookup maps and applier, so clones reuse cached
161+
// schema fetches and only duplicate a small per-clone migration memo. Used to
162+
// run several `fetch_leased_part` calls concurrently, each on its own clone.
163+
impl<K, V, T, D> Clone for BatchFetcher<K, V, T, D>
164+
where
165+
T: Timestamp + Lattice + Codec64,
166+
K: Debug + Codec,
167+
V: Debug + Codec,
168+
D: Monoid + Codec64 + Send + Sync,
169+
{
170+
fn clone(&self) -> Self {
171+
Self {
172+
cfg: self.cfg.clone(),
173+
blob: Arc::clone(&self.blob),
174+
metrics: Arc::clone(&self.metrics),
175+
shard_metrics: Arc::clone(&self.shard_metrics),
176+
shard_id: self.shard_id.clone(),
177+
read_schemas: self.read_schemas.clone(),
178+
schema_cache: self.schema_cache.clone(),
179+
is_transient: self.is_transient,
180+
_phantom: PhantomData,
181+
}
182+
}
183+
}
184+
158185
impl<K, V, T, D> BatchFetcher<K, V, T, D>
159186
where
160187
K: Debug + Codec,

0 commit comments

Comments
 (0)