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}