micromegas_object_cache/range_cache/mod.rs
1use std::collections::{BTreeSet, HashMap};
2use std::ops::Range;
3use std::sync::Arc;
4
5use anyhow::Result;
6use async_stream::try_stream;
7use bytes::{BufMut, Bytes, BytesMut};
8use futures::stream::{self, Stream, StreamExt};
9use micromegas_tracing::prelude::*;
10use object_store::{ObjectStore, ObjectStoreExt, path::Path};
11
12use super::backend::{BackendDiskStats, FillHint, RangeCacheBackend};
13use super::blocks::{assemble_range, block_byte_range, blocks_for_range, max_run_bytes};
14use super::metric_tags;
15
16mod error;
17mod fetch;
18mod scheduler;
19
20pub use error::{RangeError, StreamRangesCaller};
21pub use scheduler::{BudgetStats, FetchBudgetStats};
22use scheduler::{
23 FetchScheduler, FulfillGuard, Ownership, Priority, decode_size, reconstruct_shared_error,
24};
25
26pub const DEFAULT_BLOCK_SIZE: u64 = 1024 * 1024;
27
28/// Number of blocks per streamed fetch window used by `stream_ranges`. At the
29/// default 1 MiB block size, 8 blocks (8 MiB) matches one coalesced origin
30/// GET run (`DEFAULT_MAX_COALESCED_GET_BYTES`), with `buffered(2)` giving
31/// modest pipeline overlap. This bounds peak in-flight memory per stream to
32/// roughly `2 * DEMAND_WINDOW_BLOCKS * block_size`, independent of how large
33/// the requested range is.
34pub const DEMAND_WINDOW_BLOCKS: u64 = 8;
35
36/// Default total number of origin GETs allowed to run concurrently. See
37/// `RangeCache::new`.
38pub const DEFAULT_TOTAL_FETCH_PERMITS: usize = 32;
39/// Default number of `DEFAULT_TOTAL_FETCH_PERMITS` slots reserved for demand
40/// reads (never consumed by prefetch).
41pub const DEFAULT_DEMAND_RESERVED_FETCH_PERMITS: usize = 8;
42/// Default max byte span of one coalesced run GET.
43pub const DEFAULT_MAX_COALESCED_GET_BYTES: u64 = 8 * 1024 * 1024;
44/// Default fetch-budget cap on transient origin-GET buffer memory (256 MiB):
45/// the smallest budget at which `DEFAULT_TOTAL_FETCH_PERMITS` runs of
46/// `DEFAULT_MAX_COALESCED_GET_BYTES` each can all be in flight, so at the
47/// defaults a cold contiguous scan is throttled by parallelism, not memory.
48pub const DEFAULT_FETCH_MEMORY_BUDGET_BYTES: u64 =
49 DEFAULT_TOTAL_FETCH_PERMITS as u64 * DEFAULT_MAX_COALESCED_GET_BYTES;
50/// Default promotion granularity: promote only the run(s) covering a demanded
51/// block, not the whole prefetch batch.
52pub const DEFAULT_PROMOTE_WHOLE_BATCH: bool = false;
53
54/// Upper bound on a plausible cached object size. No micromegas lake object
55/// (parquet partition or blob) approaches this; a decoded size above it means a
56/// corrupt/misdecoded cache entry, which is treated as a miss and re-resolved
57/// from origin rather than driving a catastrophic allocation.
58pub const MAX_PLAUSIBLE_OBJECT_SIZE: u64 = 1 << 48; // 256 TiB
59
60/// Range-aware read cache over an origin object store.
61///
62/// # Cache invalidation
63///
64/// This cache assumes object keys are **write-once and content-addressed**: a
65/// given key always maps to the same bytes for the lifetime of the object. The
66/// size and block caches therefore carry no TTL, etag, or generation in their
67/// keys and are never invalidated. Overwriting an existing key with different
68/// content would cause stale size/block data to be served indefinitely. This is
69/// safe for micromegas lake objects (blocks, parquet) which are never
70/// overwritten; do not point this cache at a mutable namespace.
71///
72/// # In-flight map and priority
73///
74/// Concurrent fetches of the same block or size are collapsed via an
75/// in-flight map (`FetchScheduler`): the first caller to ask for a key
76/// becomes its owner and issues the origin request (spawned as a detached
77/// task, so a cancelled caller never strands the others waiting on it);
78/// every other concurrent caller joins and observes the same result.
79/// Contiguous missing blocks the owner controls are coalesced into one
80/// `origin.get_range` per run. Every origin GET is either `Demand` or
81/// `Prefetch` priority; a demand caller joining a prefetch-priority fetch
82/// promotes it (see `own_or_join`), so a late demand read is never stuck
83/// behind unrelated prefetch traffic.
84#[derive(Clone)]
85pub struct RangeCache {
86 origin: Arc<dyn ObjectStore>,
87 backend: Arc<dyn RangeCacheBackend>,
88 block_size: u64,
89 ns: String,
90 scheduler: Arc<FetchScheduler>,
91 max_coalesced_get_bytes: u64,
92 /// Configured `prefix` labels (the server's `allowed_prefixes`, leaked to
93 /// `'static` once at startup). Empty unless `with_prefix_labels` was used, in
94 /// which case every key classifies as `metric_tags::PREFIX_OTHER`.
95 prefix_labels: Arc<[&'static str]>,
96}
97
98impl RangeCache {
99 /// `fetch_memory_budget_bytes` is the total transient origin-GET buffer
100 /// memory allowed across all in-flight coalesced runs; the slice reserved
101 /// for demand is derived, not a separate parameter -- each of
102 /// `demand_reserved_fetch_permits`'s reserved slots reserves one full-size
103 /// run's worth of bytes (`max_run_bytes(block_size, max_coalesced_get_bytes)`),
104 /// so it keeps the same "N full-size demand runs always fit" meaning the
105 /// count reservation already has.
106 #[allow(clippy::too_many_arguments)]
107 pub fn new(
108 origin: Arc<dyn ObjectStore>,
109 backend: Arc<dyn RangeCacheBackend>,
110 block_size: u64,
111 ns: String,
112 total_fetch_permits: usize,
113 demand_reserved_fetch_permits: usize,
114 fetch_memory_budget_bytes: u64,
115 max_coalesced_get_bytes: u64,
116 promote_whole_batch: bool,
117 ) -> Self {
118 let max_run = max_run_bytes(block_size, max_coalesced_get_bytes);
119 let reserved_bytes = demand_reserved_fetch_permits as u64 * max_run;
120 Self {
121 origin,
122 backend,
123 block_size,
124 ns,
125 scheduler: Arc::new(FetchScheduler::new(
126 total_fetch_permits,
127 demand_reserved_fetch_permits,
128 fetch_memory_budget_bytes,
129 reserved_bytes,
130 max_run,
131 promote_whole_batch,
132 )),
133 max_coalesced_get_bytes,
134 prefix_labels: Arc::from(Vec::new()),
135 }
136 }
137
138 /// Attach a `prefix` classifier for the dimensioned hit-rate/request
139 /// metrics: `labels` are the server's configured `allowed_prefixes`,
140 /// leaked to `'static` once at startup by the caller (bounded,
141 /// low-cardinality, set once). Every request key is then
142 /// longest-prefix-matched against `labels` (see `classify`); a key
143 /// matching none classifies as `metric_tags::PREFIX_OTHER`.
144 ///
145 /// `RangeCache::new` itself leaves this empty (every key classifies as
146 /// `"other"`), so its existing callers/tests compile unmodified; only
147 /// `object_cache_srv.rs` opts in.
148 pub fn with_prefix_labels(mut self, labels: Arc<[&'static str]>) -> Self {
149 self.prefix_labels = labels;
150 self
151 }
152
153 /// The `prefix` label `key` falls under (e.g. `"blobs"`, `"views"`, per
154 /// the server's configured `allowed_prefixes`), resolved by longest-prefix
155 /// match against `prefix_labels`, or `metric_tags::PREFIX_OTHER` if it
156 /// matches none. See `with_prefix_labels`.
157 pub fn classify(&self, key: &str) -> &'static str {
158 match metric_tags::longest_prefix_match(&self.prefix_labels, key) {
159 Some(i) => self.prefix_labels[i],
160 None => metric_tags::PREFIX_OTHER,
161 }
162 }
163
164 /// Both fetch-permit budgets' (count and bytes) current occupancy, for
165 /// the saturation sampler.
166 pub fn fetch_budget_stats(&self) -> FetchBudgetStats {
167 self.scheduler.fetch_budget_stats()
168 }
169
170 /// Number of keys (blocks or `size()` heads) currently in flight to
171 /// origin, for the saturation sampler.
172 pub fn inflight_len(&self) -> usize {
173 self.scheduler.inflight_len()
174 }
175
176 /// Number of detached fetch tasks (coalesced block runs + `size()` HEADs)
177 /// currently in flight to origin, for the shutdown path's
178 /// abandoned-fetch-count warning.
179 pub fn outstanding_fetch_tasks(&self) -> usize {
180 self.scheduler.outstanding_tasks()
181 }
182
183 /// Resolves once every detached fetch task has finished. Used by graceful
184 /// shutdown to wait for in-flight origin GETs instead of letting the
185 /// tokio runtime drop them.
186 pub async fn wait_for_fetch_tasks_drain(&self) {
187 self.scheduler.wait_drained().await;
188 }
189
190 /// Backend disk write-path counters (`None` for a backend with no disk
191 /// tier), for the saturation sampler's per-second foyer disk gauges.
192 pub fn backend_disk_stats(&self) -> Option<BackendDiskStats> {
193 self.backend.disk_stats()
194 }
195
196 /// Accounted RAM-tier usage (`None` for a backend with no RAM tier), for
197 /// the saturation sampler's residency gauge.
198 pub fn backend_ram_usage(&self) -> Option<usize> {
199 self.backend.ram_usage_bytes()
200 }
201
202 /// Accounted RAM-tier entry count (`None` for a backend with no RAM
203 /// tier), for the saturation sampler's entry-count gauge.
204 pub fn backend_ram_entries(&self) -> Option<usize> {
205 self.backend.ram_entries()
206 }
207
208 /// Size in bytes of one cache block. Every distinct block a request
209 /// touches is fetched and held whole, so callers gating memory (e.g. the
210 /// server's cross-request budget) need this to account for amplification
211 /// from small scattered ranges.
212 pub fn block_size(&self) -> u64 {
213 self.block_size
214 }
215
216 #[span_fn]
217 pub async fn size(&self, key: &str) -> Result<u64> {
218 // The cache key carries no etag/version: see the module docs — keys
219 // are assumed write-once and content-addressed, so a cached size is
220 // never invalidated.
221 let meta_key = format!("meta:{}:{key}", self.ns);
222 let prefix = self.classify(key);
223
224 if let Some(data) = self.backend.get(&meta_key, 8).await
225 && data.len() == 8
226 {
227 let size = decode_size(&data)?;
228 if size <= MAX_PLAUSIBLE_OBJECT_SIZE {
229 imetric!("range_cache_size_backend_hit", "count", prefix = static prefix, 1_u64);
230 return Ok(size);
231 }
232 imetric!("range_cache_size_implausible", "count", prefix = static prefix, 1_u64);
233 warn!("range_cache implausible cached size {size} for key={key}; treating as miss");
234 // fall through to origin HEAD, which repopulates meta:{ns}:{key}
235 }
236
237 match self
238 .scheduler
239 .own_or_join(meta_key.clone(), Priority::Demand, None)
240 {
241 Ownership::Owner(entry) => {
242 let origin = self.origin.clone();
243 let backend = self.backend.clone();
244 let scheduler = self.scheduler.clone();
245 let key_owned = key.to_string();
246 let meta_key_owned = meta_key.clone();
247 let task_entry = entry.clone();
248 // See `spawn_run_fetch`'s identical comment: constructed
249 // before `tokio::spawn`, and declared before `FulfillGuard`
250 // inside the async block so it's the last thing dropped.
251 let task_guard = FetchScheduler::track_task(&scheduler);
252 tokio::spawn(async move {
253 let _task_guard = task_guard;
254 let guard = FulfillGuard::new(
255 scheduler.clone(),
256 vec![(meta_key_owned.clone(), task_entry.clone())],
257 );
258 imetric!("range_cache_origin_head", "count", prefix = static prefix, 1_u64);
259 let head_path = Path::from(key_owned.as_str());
260 let head_result = instrument_named!(
261 origin.head(&head_path),
262 "range_cache_origin_head_latency"
263 )
264 .await;
265 match head_result {
266 Ok(object_meta) => {
267 let size = object_meta.size;
268 debug!("range_cache origin head key={key_owned} size={size}");
269 if size > MAX_PLAUSIBLE_OBJECT_SIZE {
270 // The origin is the source of truth: an implausible
271 // size here signals corruption, not a bad cache
272 // entry. Surface it instead of caching a value the
273 // cached-read path would reject on every future
274 // read, and keep both paths agreeing that a size
275 // above the ceiling is never cached.
276 imetric!(
277 "range_cache_size_implausible",
278 "count",
279 prefix = static prefix,
280 1_u64
281 );
282 task_entry.fulfill(Err(Arc::new(anyhow::anyhow!(
283 "range_cache implausible origin size {size} for key={key_owned}"
284 ))));
285 } else {
286 let size_bytes = Bytes::from(size.to_le_bytes().to_vec());
287 backend
288 .put(
289 meta_key_owned.clone(),
290 size_bytes.clone(),
291 FillHint::Demand,
292 )
293 .await;
294 task_entry.fulfill(Ok(size_bytes));
295 }
296 }
297 Err(e) => {
298 task_entry.fulfill(Err(Arc::new(anyhow::Error::from(e))));
299 }
300 }
301 scheduler.remove_entry(&meta_key_owned);
302 guard.disarm();
303 });
304 let data = entry
305 .join()
306 .await
307 .map_err(|e| reconstruct_shared_error(&e))?;
308 decode_size(&data)
309 }
310 Ownership::Joiner(entry) => {
311 let data = entry
312 .join()
313 .await
314 .map_err(|e| reconstruct_shared_error(&e))?;
315 decode_size(&data)
316 }
317 }
318 }
319
320 /// Stream the bytes for `ranges` (each half-open `[start, end)`) without
321 /// materializing more than a couple of `DEMAND_WINDOW_BLOCKS` windows of
322 /// blocks at a time, regardless of how large the ranges are in total.
323 ///
324 /// Upfront validation — the `size()` lookup and every range's
325 /// out-of-bounds check — runs before the stream is constructed and
326 /// returned, so 404/416-shaped errors surface synchronously to the
327 /// caller with proper status codes; only a failure *after* that point
328 /// (a mid-stream `fetch_blocks` error, e.g. the origin going down) is
329 /// yielded as the stream's terminal `Err` item. A degenerate
330 /// `start >= end` range is validated like any other (its `end` is still
331 /// checked against `file_size`) but yields no bytes.
332 ///
333 /// Yields a flat, ordered sequence of chunks: ranges are processed in
334 /// the order given and each range's bytes are emitted contiguously
335 /// (possibly split into several `Bytes` chunks at window boundaries)
336 /// before the next range's bytes begin. There is no cross-range block
337 /// dedup — a block shared by two ranges is fetched once per range it
338 /// appears in, though a repeat is always a backend hit or an in-flight
339 /// join, never a second origin GET (see `own_or_join`).
340 ///
341 /// `caller` selects which of the two distinct error counters
342 /// (`range_cache_get_range_error` / `range_cache_get_ranges_error`) this
343 /// call emits on validation failure or a mid-stream fetch error, so
344 /// `get_range`/`get_ranges` (and the two HTTP handlers, which call this
345 /// directly) keep emitting the metric they always have.
346 #[span_fn]
347 pub async fn stream_ranges(
348 &self,
349 key: &str,
350 ranges: Vec<Range<u64>>,
351 caller: StreamRangesCaller,
352 ) -> Result<impl Stream<Item = Result<Bytes>> + Send + 'static> {
353 let file_size = match self.size(key).await {
354 Ok(s) => s,
355 Err(e) => {
356 caller.emit_error_metric();
357 return Err(e);
358 }
359 };
360 self.stream_ranges_inner(key, ranges, file_size, caller)
361 .await
362 }
363
364 /// Like `stream_ranges`, but for a caller that already resolved
365 /// `file_size` itself (e.g. `get_range_handler`, which needs it up front
366 /// for range validation): skips the redundant `self.size()` call, so a
367 /// cache hit doesn't fire `range_cache_size_backend_hit` a second time
368 /// per call.
369 #[span_fn]
370 pub async fn stream_ranges_with_size(
371 &self,
372 key: &str,
373 ranges: Vec<Range<u64>>,
374 file_size: u64,
375 caller: StreamRangesCaller,
376 ) -> Result<impl Stream<Item = Result<Bytes>> + Send + 'static> {
377 self.stream_ranges_inner(key, ranges, file_size, caller)
378 .await
379 }
380
381 async fn stream_ranges_inner(
382 &self,
383 key: &str,
384 ranges: Vec<Range<u64>>,
385 file_size: u64,
386 caller: StreamRangesCaller,
387 ) -> Result<impl Stream<Item = Result<Bytes>> + Send + 'static> {
388 for r in &ranges {
389 if r.end > file_size {
390 caller.emit_error_metric();
391 return Err(RangeError::OutOfBounds {
392 requested_end: r.end,
393 file_size,
394 }
395 .into());
396 }
397 }
398
399 let cache = self.clone();
400 let key = key.to_string();
401 Ok(try_stream! {
402 for r in ranges {
403 if r.start >= r.end {
404 continue;
405 }
406 let blk_range = blocks_for_range(r.start, r.end, cache.block_size);
407 let mut windows = cache.stream_demand_windows(&key, file_size, blk_range);
408 while let Some((w, result)) = windows.next().await {
409 let block_map = result.inspect_err(|_| caller.emit_error_metric())?;
410 yield cache.assemble_window(&block_map, &w, r.start, r.end, file_size);
411 }
412 }
413 })
414 }
415
416 /// Build the ordered, bounded stream of demand-priority window fetches for
417 /// one requested range's block span `blk_range`: chunk it into
418 /// `DEMAND_WINDOW_BLOCKS`-sized windows and fetch each (at most two in
419 /// flight via `buffered(2)`), yielding `(window_indices, fetch_result)`.
420 /// Extracted from `stream_ranges_inner`'s `try_stream!` body to keep that
421 /// generator small; the returned stream owns its own `RangeCache`/key
422 /// clones so it is `'static`.
423 fn stream_demand_windows(
424 &self,
425 key: &str,
426 file_size: u64,
427 blk_range: Range<u64>,
428 ) -> impl Stream<Item = (Vec<u64>, Result<HashMap<u64, Bytes>>)> + Send + 'static {
429 let window_indices: Vec<Vec<u64>> = (blk_range.start..blk_range.end)
430 .collect::<Vec<u64>>()
431 .chunks(DEMAND_WINDOW_BLOCKS as usize)
432 .map(|w| w.to_vec())
433 .collect();
434 let cache = self.clone();
435 let key = key.to_string();
436 stream::iter(window_indices)
437 .map(move |w| {
438 let cache = cache.clone();
439 let key = key.clone();
440 async move {
441 let result = cache
442 .fetch_blocks(&key, file_size, &w, Priority::Demand)
443 .await;
444 (w, result)
445 }
446 })
447 .buffered(2)
448 }
449
450 /// Assemble the bytes for one window: gather its blocks from `block_map`,
451 /// then clamp to the window's own byte span intersected with the outer
452 /// requested range `[req_start, req_end)` before assembling. Clamping to
453 /// the window (not the whole range) matters because `block_map` holds only
454 /// this window's data and `assemble_range` pre-sizes its output buffer from
455 /// `req_end - req_start`, so passing the full range's bounds on every
456 /// window would over-allocate by up to the entire range's size each
457 /// iteration.
458 fn assemble_window(
459 &self,
460 block_map: &HashMap<u64, Bytes>,
461 window: &[u64],
462 req_start: u64,
463 req_end: u64,
464 file_size: u64,
465 ) -> Bytes {
466 let blocks: Vec<(u64, Bytes)> = window
467 .iter()
468 .map(|idx| {
469 let data = block_map
470 .get(idx)
471 .cloned()
472 .expect("fetch_blocks returns every requested index");
473 (*idx, data)
474 })
475 .collect();
476 let win_start = window[0] * self.block_size;
477 let win_end = block_byte_range(
478 *window.last().expect("window is non-empty"),
479 self.block_size,
480 file_size,
481 )
482 .end;
483 let local_start = req_start.max(win_start);
484 let local_end = req_end.min(win_end);
485 assemble_range(&blocks, self.block_size, local_start, local_end)
486 }
487
488 #[span_fn]
489 pub async fn get_range(&self, key: &str, range: Range<u64>) -> Result<Bytes> {
490 let mut stream = Box::pin(
491 self.stream_ranges(key, vec![range], StreamRangesCaller::Range)
492 .await?,
493 );
494 let mut buf = BytesMut::new();
495 while let Some(chunk) = stream.next().await {
496 buf.put_slice(&chunk?);
497 }
498 Ok(buf.freeze())
499 }
500
501 /// Like `get_range`, but for a caller that already resolved `file_size`
502 /// itself, skipping the redundant `self.size()` call inside
503 /// `stream_ranges` (see `stream_ranges_with_size`).
504 #[span_fn]
505 pub async fn get_range_with_size(
506 &self,
507 key: &str,
508 file_size: u64,
509 range: Range<u64>,
510 ) -> Result<Bytes> {
511 let mut stream = Box::pin(
512 self.stream_ranges_with_size(key, vec![range], file_size, StreamRangesCaller::Range)
513 .await?,
514 );
515 let mut buf = BytesMut::new();
516 while let Some(chunk) = stream.next().await {
517 buf.put_slice(&chunk?);
518 }
519 Ok(buf.freeze())
520 }
521
522 #[span_fn]
523 pub async fn get_ranges(&self, key: &str, ranges: &[Range<u64>]) -> Result<Vec<Bytes>> {
524 if ranges.is_empty() {
525 return Ok(vec![]);
526 }
527 let owned_ranges: Vec<Range<u64>> = ranges.to_vec();
528 let stream = Box::pin(
529 self.stream_ranges(key, owned_ranges, StreamRangesCaller::Ranges)
530 .await?,
531 );
532 collect_ranges_from_stream(ranges, stream).await
533 }
534
535 /// Like `get_ranges`, but for a caller that already resolved `file_size`
536 /// itself (see `stream_ranges_with_size`).
537 #[span_fn]
538 pub async fn get_ranges_with_size(
539 &self,
540 key: &str,
541 file_size: u64,
542 ranges: &[Range<u64>],
543 ) -> Result<Vec<Bytes>> {
544 if ranges.is_empty() {
545 return Ok(vec![]);
546 }
547 let owned_ranges: Vec<Range<u64>> = ranges.to_vec();
548 let stream = Box::pin(
549 self.stream_ranges_with_size(key, owned_ranges, file_size, StreamRangesCaller::Ranges)
550 .await?,
551 );
552 collect_ranges_from_stream(ranges, stream).await
553 }
554
555 /// Warm the cache for `ranges` at `Prefetch` priority without returning
556 /// any bytes -- the priority-carrying core that the HTTP prefetch
557 /// endpoint and client method build on. Public (rather than
558 /// crate-private) so integration tests under `tests/` — which compile as
559 /// a separate crate — can exercise the promotion behavior.
560 pub async fn prefetch_ranges(&self, key: &str, ranges: &[Range<u64>]) -> Result<()> {
561 if ranges.is_empty() {
562 return Ok(());
563 }
564 let file_size = self.size(key).await?;
565 let mut all_block_indices = BTreeSet::new();
566 for r in ranges {
567 let start = r.start;
568 let end = r.end;
569 if end > file_size {
570 return Err(RangeError::OutOfBounds {
571 requested_end: end,
572 file_size,
573 }
574 .into());
575 }
576 if start < end {
577 let blk = blocks_for_range(start, end, self.block_size);
578 all_block_indices.extend(blk.start..blk.end);
579 }
580 }
581 self.prefetch_blocks(
582 key,
583 file_size,
584 &all_block_indices.into_iter().collect::<Vec<_>>(),
585 )
586 .await
587 }
588
589 /// Warm the cache for the given block indices at `Prefetch` priority.
590 pub async fn prefetch_blocks(&self, key: &str, file_size: u64, indices: &[u64]) -> Result<()> {
591 self.fetch_blocks(key, file_size, indices, Priority::Prefetch)
592 .await?;
593 Ok(())
594 }
595}
596
597/// Reassemble the flat, ordered chunk sequence a `stream_ranges*` stream
598/// yields back into one `Bytes` per requested range, using each range's
599/// known length rather than relying on a chunk boundary lining up with a
600/// range boundary. Shared by `get_ranges` and `get_ranges_with_size`.
601async fn collect_ranges_from_stream(
602 ranges: &[Range<u64>],
603 mut stream: impl Stream<Item = Result<Bytes>> + Unpin,
604) -> Result<Vec<Bytes>> {
605 let mut result = Vec::with_capacity(ranges.len());
606 let mut pending: Option<Bytes> = None;
607 for r in ranges {
608 let start = r.start;
609 let end = r.end;
610 if start >= end {
611 // `stream_ranges` yields nothing for a degenerate range, so
612 // reinsert the empty chunk at its position ourselves.
613 result.push(Bytes::new());
614 continue;
615 }
616 let need = (end - start) as usize;
617 let mut collected = BytesMut::with_capacity(need);
618 while collected.len() < need {
619 let chunk = match pending.take() {
620 Some(c) => c,
621 None => stream
622 .next()
623 .await
624 .expect("stream_ranges under-yielded for a non-degenerate range")?,
625 };
626 let remaining = need - collected.len();
627 if chunk.len() > remaining {
628 collected.put_slice(&chunk[..remaining]);
629 pending = Some(chunk.slice(remaining..));
630 } else {
631 collected.put_slice(&chunk);
632 }
633 }
634 result.push(collected.freeze());
635 }
636 Ok(result)
637}