Skip to main content

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};
14use super::metric_tags::{self, PrefixTags};
15
16mod error;
17mod fetch;
18mod scheduler;
19
20pub use error::{RangeError, StreamRangesCaller};
21use scheduler::{
22    FetchScheduler, FulfillGuard, Ownership, Priority, decode_size, reconstruct_shared_error,
23};
24
25pub const DEFAULT_BLOCK_SIZE: u64 = 1024 * 1024;
26
27/// Number of blocks per streamed fetch window used by `stream_ranges`. At the
28/// default 1 MiB block size, 8 blocks (8 MiB) matches one coalesced origin
29/// GET run (`DEFAULT_MAX_COALESCED_GET_BYTES`), with `buffered(2)` giving
30/// modest pipeline overlap. This bounds peak in-flight memory per stream to
31/// roughly `2 * DEMAND_WINDOW_BLOCKS * block_size`, independent of how large
32/// the requested range is.
33pub const DEMAND_WINDOW_BLOCKS: u64 = 8;
34
35/// Default total number of origin GETs allowed to run concurrently. See
36/// `RangeCache::new`.
37pub const DEFAULT_TOTAL_FETCH_PERMITS: usize = 32;
38/// Default number of `DEFAULT_TOTAL_FETCH_PERMITS` slots reserved for demand
39/// reads (never consumed by prefetch).
40pub const DEFAULT_DEMAND_RESERVED_FETCH_PERMITS: usize = 8;
41/// Default max byte span of one coalesced run GET.
42pub const DEFAULT_MAX_COALESCED_GET_BYTES: u64 = 8 * 1024 * 1024;
43/// Default promotion granularity: promote only the run(s) covering a demanded
44/// block, not the whole prefetch batch.
45pub const DEFAULT_PROMOTE_WHOLE_BATCH: bool = false;
46
47/// Upper bound on a plausible cached object size. No micromegas lake object
48/// (parquet partition or blob) approaches this; a decoded size above it means a
49/// corrupt/misdecoded cache entry, which is treated as a miss and re-resolved
50/// from origin rather than driving a catastrophic allocation (#1287).
51pub const MAX_PLAUSIBLE_OBJECT_SIZE: u64 = 1 << 48; // 256 TiB
52
53/// Range-aware read cache over an origin object store.
54///
55/// # Cache invalidation
56///
57/// This cache assumes object keys are **write-once and content-addressed**: a
58/// given key always maps to the same bytes for the lifetime of the object. The
59/// size and block caches therefore carry no TTL, etag, or generation in their
60/// keys and are never invalidated. Overwriting an existing key with different
61/// content would cause stale size/block data to be served indefinitely. This is
62/// safe for micromegas lake objects (blocks, parquet) which are never
63/// overwritten; do not point this cache at a mutable namespace.
64///
65/// # In-flight map and priority
66///
67/// Concurrent fetches of the same block or size are collapsed via an
68/// in-flight map (`FetchScheduler`): the first caller to ask for a key
69/// becomes its owner and issues the origin request (spawned as a detached
70/// task, so a cancelled caller never strands the others waiting on it);
71/// every other concurrent caller joins and observes the same result.
72/// Contiguous missing blocks the owner controls are coalesced into one
73/// `origin.get_range` per run. Every origin GET is either `Demand` or
74/// `Prefetch` priority; a demand caller joining a prefetch-priority fetch
75/// promotes it (see `own_or_join`), so a late demand read is never stuck
76/// behind unrelated prefetch traffic.
77#[derive(Clone)]
78pub struct RangeCache {
79    origin: Arc<dyn ObjectStore>,
80    backend: Arc<dyn RangeCacheBackend>,
81    block_size: u64,
82    ns: String,
83    scheduler: Arc<FetchScheduler>,
84    max_coalesced_get_bytes: u64,
85    /// Configured `prefix` labels (the server's `allowed_prefixes`, leaked to
86    /// `'static` once at startup), in the same order as `prefix_tags`. Empty
87    /// unless `with_prefix_labels` was used, in which case every key
88    /// classifies as `metric_tags::PREFIX_OTHER`.
89    prefix_labels: Arc<[&'static str]>,
90    /// Precomputed `PrefixTags` parallel to `prefix_labels` (`prefix_tags[i]`
91    /// corresponds to `prefix_labels[i]`), so `classify_tags` is an array
92    /// lookup rather than an allocation + intern-lock per call.
93    prefix_tags: Arc<[PrefixTags]>,
94    /// Precomputed tags for a key matching none of `prefix_labels`.
95    other_tags: PrefixTags,
96}
97
98impl RangeCache {
99    #[allow(clippy::too_many_arguments)]
100    pub fn new(
101        origin: Arc<dyn ObjectStore>,
102        backend: Arc<dyn RangeCacheBackend>,
103        block_size: u64,
104        ns: String,
105        total_fetch_permits: usize,
106        demand_reserved_fetch_permits: usize,
107        max_coalesced_get_bytes: u64,
108        promote_whole_batch: bool,
109    ) -> Self {
110        Self {
111            origin,
112            backend,
113            block_size,
114            ns,
115            scheduler: Arc::new(FetchScheduler::new(
116                total_fetch_permits,
117                demand_reserved_fetch_permits,
118                promote_whole_batch,
119            )),
120            max_coalesced_get_bytes,
121            prefix_labels: Arc::from(Vec::new()),
122            prefix_tags: Arc::from(Vec::new()),
123            other_tags: PrefixTags::new(metric_tags::PREFIX_OTHER),
124        }
125    }
126
127    /// Attach a `prefix` classifier for the dimensioned hit-rate/request
128    /// metrics: `labels` are the server's configured `allowed_prefixes`,
129    /// leaked to `'static` once at startup by the caller (bounded,
130    /// low-cardinality, set once). Every request key is then
131    /// longest-prefix-matched against `labels` (see `classify`); a key
132    /// matching none classifies as `metric_tags::PREFIX_OTHER`.
133    ///
134    /// `RangeCache::new` itself leaves this empty (every key classifies as
135    /// `"other"`), so its existing callers/tests compile unmodified; only
136    /// `object_cache_srv.rs` opts in.
137    pub fn with_prefix_labels(mut self, labels: Arc<[&'static str]>) -> Self {
138        let tags: Vec<PrefixTags> = labels.iter().map(|&label| PrefixTags::new(label)).collect();
139        self.prefix_tags = Arc::from(tags);
140        self.prefix_labels = labels;
141        self
142    }
143
144    /// The precomputed tags for the `prefix` `key` falls under, resolved by
145    /// longest-prefix match against `prefix_labels` (`other_tags` on no
146    /// match). Private: callers needing just the label use `classify`;
147    /// hot-path callers inside this module use the tags directly.
148    fn classify_tags(&self, key: &str) -> &PrefixTags {
149        match metric_tags::longest_prefix_match(&self.prefix_labels, key) {
150            Some(i) => &self.prefix_tags[i],
151            None => &self.other_tags,
152        }
153    }
154
155    /// The `prefix` label `key` falls under (e.g. `"blobs"`, `"views"`, per
156    /// the server's configured `allowed_prefixes`), or
157    /// `metric_tags::PREFIX_OTHER` if it matches none. See
158    /// `with_prefix_labels`.
159    pub fn classify(&self, key: &str) -> &'static str {
160        self.classify_tags(key).label
161    }
162
163    /// `(shared_available, shared_total, prefetch_available, prefetch_total)`
164    /// -- the fetch-permit budget's current occupancy, for the saturation
165    /// sampler.
166    pub fn fetch_budget_stats(&self) -> (usize, usize, usize, usize) {
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_tag = self.classify_tags(key).prefix;
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_tag, 1_u64);
230                return Ok(size);
231            }
232            imetric!("range_cache_size_implausible", "count", prefix_tag, 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_tag, 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 (#1287).
276                                imetric!(
277                                    "range_cache_size_implausible",
278                                    "count",
279                                    prefix_tag,
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 HTTP surface for this (endpoint + client method) is
557    /// #1198; this is the priority-carrying core it builds on. Public (rather
558    /// than crate-private) so integration tests under `tests/` — which
559    /// compile as a separate crate — can exercise the promotion behavior
560    /// described in the fetch-rework plan.
561    pub async fn prefetch_ranges(&self, key: &str, ranges: &[Range<u64>]) -> Result<()> {
562        if ranges.is_empty() {
563            return Ok(());
564        }
565        let file_size = self.size(key).await?;
566        let mut all_block_indices = BTreeSet::new();
567        for r in ranges {
568            let start = r.start;
569            let end = r.end;
570            if end > file_size {
571                return Err(RangeError::OutOfBounds {
572                    requested_end: end,
573                    file_size,
574                }
575                .into());
576            }
577            if start < end {
578                let blk = blocks_for_range(start, end, self.block_size);
579                all_block_indices.extend(blk.start..blk.end);
580            }
581        }
582        self.prefetch_blocks(
583            key,
584            file_size,
585            &all_block_indices.into_iter().collect::<Vec<_>>(),
586        )
587        .await
588    }
589
590    /// Warm the cache for the given block indices at `Prefetch` priority.
591    pub async fn prefetch_blocks(&self, key: &str, file_size: u64, indices: &[u64]) -> Result<()> {
592        self.fetch_blocks(key, file_size, indices, Priority::Prefetch)
593            .await?;
594        Ok(())
595    }
596}
597
598/// Reassemble the flat, ordered chunk sequence a `stream_ranges*` stream
599/// yields back into one `Bytes` per requested range, using each range's
600/// known length rather than relying on a chunk boundary lining up with a
601/// range boundary. Shared by `get_ranges` and `get_ranges_with_size`.
602async fn collect_ranges_from_stream(
603    ranges: &[Range<u64>],
604    mut stream: impl Stream<Item = Result<Bytes>> + Unpin,
605) -> Result<Vec<Bytes>> {
606    let mut result = Vec::with_capacity(ranges.len());
607    let mut pending: Option<Bytes> = None;
608    for r in ranges {
609        let start = r.start;
610        let end = r.end;
611        if start >= end {
612            // `stream_ranges` yields nothing for a degenerate range, so
613            // reinsert the empty chunk at its position ourselves.
614            result.push(Bytes::new());
615            continue;
616        }
617        let need = (end - start) as usize;
618        let mut collected = BytesMut::with_capacity(need);
619        while collected.len() < need {
620            let chunk = match pending.take() {
621                Some(c) => c,
622                None => stream
623                    .next()
624                    .await
625                    .expect("stream_ranges under-yielded for a non-degenerate range")?,
626            };
627            let remaining = need - collected.len();
628            if chunk.len() > remaining {
629                collected.put_slice(&chunk[..remaining]);
630                pending = Some(chunk.slice(remaining..));
631            } else {
632                collected.put_slice(&chunk);
633            }
634        }
635        result.push(collected.freeze());
636    }
637    Ok(result)
638}