Skip to main content

micromegas_object_cache/
client.rs

1use anyhow::{Context, Result, anyhow};
2use async_stream::stream as gen_stream;
3use async_trait::async_trait;
4use bytes::{Buf, BufMut, Bytes, BytesMut};
5use futures::stream::TryStreamExt;
6use futures::stream::{self, BoxStream, StreamExt};
7use micromegas_tracing::prelude::*;
8use object_store::{
9    Attributes, CopyOptions, GetOptions, GetResult, GetResultPayload, ListResult, MultipartUpload,
10    ObjectMeta, ObjectStore, PutMultipartOptions, PutOptions, PutPayload, PutResult, path::Path,
11};
12use reqwest::Client;
13use serde_json::json;
14use std::ops::Range;
15use std::sync::Arc;
16use std::time::{Duration, Instant};
17
18use crate::prefetch::{ObjectPrefetch, PrefetchItem, PrefetchResponse};
19
20/// Fail fast if the cache server can't be reached, so reads fall back to the
21/// direct store instead of stalling on a hung connection.
22const CACHE_CONNECT_TIMEOUT: Duration = Duration::from_secs(2);
23/// Overall per-request timeout; a slow cache surfaces as an error and triggers
24/// the existing fallback-to-direct path.
25const CACHE_REQUEST_TIMEOUT: Duration = Duration::from_secs(15);
26
27#[derive(Debug)]
28pub struct CacheClientStore {
29    http: Client,
30    cache_base_url: String,
31    api_key: Option<String>,
32    direct: Arc<dyn ObjectStore>,
33}
34
35impl CacheClientStore {
36    pub fn new(
37        cache_base_url: String,
38        api_key: Option<String>,
39        direct: Arc<dyn ObjectStore>,
40    ) -> Self {
41        let http = Client::builder()
42            .connect_timeout(CACHE_CONNECT_TIMEOUT)
43            .timeout(CACHE_REQUEST_TIMEOUT)
44            .build()
45            .expect("building reqwest client");
46        Self {
47            http,
48            cache_base_url,
49            api_key,
50            direct,
51        }
52    }
53
54    fn obj_url(&self, location: &Path) -> String {
55        format!(
56            "{}/obj/{}",
57            self.cache_base_url.trim_end_matches('/'),
58            location.as_ref()
59        )
60    }
61
62    fn add_auth(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
63        if let Some(key) = &self.api_key {
64            req.bearer_auth(key)
65        } else {
66            req
67        }
68    }
69
70    /// Issue a range GET and build a streaming `GetResult`, mirroring
71    /// `get_full_stream` but for ranged reads: the body is streamed rather
72    /// than buffered with `.bytes()`, which would otherwise materialize the
73    /// whole range (now unbounded, since the server no longer caps total
74    /// requested bytes) as one contiguous allocation before any of it is
75    /// used. The actual served byte range and the full object size both come
76    /// from the 206's `Content-Range: bytes {start}-{end}/{size}` header
77    /// rather than a buffered body length, avoiding a separate HEAD
78    /// round-trip in the common case.
79    ///
80    /// An open-ended `end` (`None`) requests `bytes={start}-`, i.e. from `start`
81    /// to the end of the object, which the cache server resolves against the
82    /// true object size.
83    ///
84    /// `options` (carrying the original requested range) is threaded through
85    /// so a stream error before the first chunk reaches the consumer can fall
86    /// back to `self.direct` for the same range, via `full_stream_with_fallback`.
87    async fn get_range_stream(
88        &self,
89        location: &Path,
90        start: u64,
91        end: Option<u64>,
92        options: GetOptions,
93    ) -> Result<GetResult> {
94        let round_trip_start = Instant::now();
95        let url = self.obj_url(location);
96        let range_header = match end {
97            Some(end) => format!("bytes={}-{}", start, end.saturating_sub(1)),
98            None => format!("bytes={start}-"),
99        };
100        let resp = self
101            .add_auth(self.http.get(&url))
102            .header("Range", range_header)
103            .send()
104            .await
105            .with_context(|| "sending GET to cache")?;
106        if !resp.status().is_success() {
107            return Err(anyhow!("cache GET {url} status {}", resp.status()));
108        }
109
110        if let Some((served_range, object_size)) = parse_content_range(resp.headers()) {
111            let raw = resp
112                .bytes_stream()
113                .map_err(|e| object_store::Error::Generic {
114                    store: "CacheClientStore",
115                    source: Box::new(e),
116                })
117                .boxed();
118            let body =
119                full_stream_with_fallback(self.direct.clone(), location.clone(), options, raw);
120            // Measured at time-to-usable-stream, i.e. before any body bytes
121            // are read (the body streams lazily) — distinct from
122            // `range_cache_client_ranges_ms`, which covers the buffered
123            // `/ranges` path and is measured after the full body is read.
124            fmetric!(
125                "range_cache_client_roundtrip_ms",
126                "ms",
127                round_trip_start.elapsed().as_secs_f64() * 1000.0
128            );
129            return Ok(stream_get_result(location, body, served_range, object_size));
130        }
131
132        // No `Content-Range`: the server serves a zero-length range (an
133        // empty/zero-byte object, or an open-ended range starting exactly at
134        // EOF) as a plain 200 with an empty body rather than a 206 (see
135        // `get_range_handler`), so there's nothing to stream. The full object
136        // size still isn't known from this response; resolve it with a HEAD.
137        let size = self.head_size(location).await?;
138        fmetric!(
139            "range_cache_client_roundtrip_ms",
140            "ms",
141            round_trip_start.elapsed().as_secs_f64() * 1000.0
142        );
143        Ok(build_get_result(location, Bytes::new(), 0..0, size))
144    }
145
146    /// Issue an unranged GET and build a streaming `GetResult`, mapping the
147    /// response body to `GetResultPayload::Stream` so the whole object is never
148    /// buffered in memory (matching how the direct store streams the body). The
149    /// object size comes from the `Content-Length` header, which is required to
150    /// populate `meta.size` and the `0..size` range without reading the body.
151    ///
152    /// The body is wrapped with `full_stream_with_fallback` so a stream error
153    /// before the first chunk reaches the consumer transparently falls back
154    /// to `self.direct`, at the `GetResult` level instead of the
155    /// whole-response level, since this path streams rather than buffers
156    /// (the ranged path, `get_range_stream`, uses the same helper for the
157    /// same reason).
158    async fn get_full_stream(&self, location: &Path, options: GetOptions) -> Result<GetResult> {
159        let round_trip_start = Instant::now();
160        let url = self.obj_url(location);
161        let resp = self
162            .add_auth(self.http.get(&url))
163            .send()
164            .await
165            .with_context(|| "sending GET to cache")?;
166        if !resp.status().is_success() {
167            return Err(anyhow!("cache GET {url} status {}", resp.status()));
168        }
169        let size = resp
170            .content_length()
171            .ok_or_else(|| anyhow!("missing Content-Length in GET response"))?;
172        let raw = resp
173            .bytes_stream()
174            .map_err(|e| object_store::Error::Generic {
175                store: "CacheClientStore",
176                source: Box::new(e),
177            })
178            .boxed();
179        let body = full_stream_with_fallback(self.direct.clone(), location.clone(), options, raw);
180        // Measured at time-to-usable-stream, before any body bytes are read
181        // (see the matching comment in `get_range_stream`).
182        fmetric!(
183            "range_cache_client_roundtrip_ms",
184            "ms",
185            round_trip_start.elapsed().as_secs_f64() * 1000.0
186        );
187        Ok(stream_get_result(location, body, 0..size, size))
188    }
189
190    async fn head_size(&self, location: &Path) -> Result<u64> {
191        let url = self.obj_url(location);
192        let resp = self
193            .add_auth(self.http.head(&url))
194            .send()
195            .await
196            .with_context(|| "sending HEAD to cache")?;
197        if !resp.status().is_success() {
198            return Err(anyhow!("cache HEAD {url} status {}", resp.status()));
199        }
200        resp.headers()
201            .get("content-length")
202            .and_then(|v| v.to_str().ok())
203            .and_then(|s| s.parse::<u64>().ok())
204            .ok_or_else(|| anyhow!("missing Content-Length in HEAD response"))
205    }
206
207    /// POST a batch of keys to warm at the cache server's prefetch priority.
208    /// Best-effort: there is no demand read to fall back to, so callers should
209    /// treat an `Err` as "the warm didn't happen" and move on rather than
210    /// retrying inline.
211    pub async fn prefetch(&self, items: Vec<PrefetchItem>) -> Result<PrefetchResponse> {
212        let url = format!("{}/prefetch", self.cache_base_url.trim_end_matches('/'));
213        let mut body = Vec::new();
214        for item in &items {
215            serde_json::to_writer(&mut body, item).with_context(|| "serializing PrefetchItem")?;
216            body.push(b'\n');
217        }
218
219        let result: Result<PrefetchResponse> = async {
220            let resp = self
221                .add_auth(
222                    self.http
223                        .post(&url)
224                        .header("Content-Type", "application/x-ndjson")
225                        .body(body),
226                )
227                .send()
228                .await
229                .with_context(|| "sending POST to cache prefetch")?;
230            if !resp.status().is_success() {
231                return Err(anyhow!("cache prefetch {url} status {}", resp.status()));
232            }
233            resp.json::<PrefetchResponse>()
234                .await
235                .with_context(|| "reading prefetch response")
236        }
237        .await;
238
239        if let Err(e) = &result {
240            imetric!("range_cache_client_prefetch_error", "count", 1_u64);
241            debug!("prefetch request to {url} failed: {e}");
242        }
243        result
244    }
245}
246
247#[async_trait]
248impl ObjectPrefetch for CacheClientStore {
249    async fn prefetch(&self, items: Vec<PrefetchItem>) -> Result<PrefetchResponse> {
250        CacheClientStore::prefetch(self, items).await
251    }
252}
253
254/// Parse a `Content-Range: bytes {start}-{end}/{size}` response header,
255/// returning the actual byte range served (`start..end+1`) and the full
256/// object size. Returns `None` when the header is absent or not in the
257/// expected form (e.g. the unsatisfiable `bytes */size` form, or an
258/// unparseable value).
259fn parse_content_range(headers: &reqwest::header::HeaderMap) -> Option<(Range<u64>, u64)> {
260    let value = headers.get(reqwest::header::CONTENT_RANGE)?.to_str().ok()?;
261    let value = value.strip_prefix("bytes ")?;
262    let (span, size) = value.split_once('/')?;
263    let size: u64 = size.trim().parse().ok()?;
264    let (start, end) = span.split_once('-')?;
265    let start: u64 = start.trim().parse().ok()?;
266    let end: u64 = end.trim().parse().ok()?;
267    Some((start..end.saturating_add(1), size))
268}
269
270impl std::fmt::Display for CacheClientStore {
271    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272        write!(f, "CacheClientStore({})", self.cache_base_url)
273    }
274}
275
276/// Build a streaming `GetResult` from an already-built byte stream (see
277/// `full_stream_with_fallback`), so the object/range is delivered in chunks
278/// rather than buffered whole. `range` is the slice actually being streamed
279/// (`0..object_size` for an unranged GET) and `object_size` is the full
280/// object size, per the `ObjectMeta` contract.
281fn stream_get_result(
282    location: &Path,
283    body: BoxStream<'static, object_store::Result<Bytes>>,
284    range: Range<u64>,
285    object_size: u64,
286) -> GetResult {
287    let meta = ObjectMeta {
288        location: location.clone(),
289        last_modified: chrono::Utc::now(),
290        size: object_size,
291        e_tag: None,
292        version: None,
293    };
294    GetResult {
295        payload: GetResultPayload::Stream(body),
296        meta,
297        range,
298        attributes: Attributes::default(),
299    }
300}
301
302/// Wrap the raw byte stream for a GET (full or ranged) so a stream error
303/// *before* any bytes reach the consumer transparently falls back to
304/// `direct`: zero bytes have been yielded downstream yet, so retrying
305/// against the origin can't re-deliver a duplicate prefix. Once the first
306/// chunk has been yielded downstream, a later error simply ends the stream —
307/// retrying at that point would re-emit already-delivered bytes from the
308/// start, which is unsound.
309fn full_stream_with_fallback(
310    direct: Arc<dyn ObjectStore>,
311    location: Path,
312    options: GetOptions,
313    mut first: BoxStream<'static, object_store::Result<Bytes>>,
314) -> BoxStream<'static, object_store::Result<Bytes>> {
315    gen_stream! {
316        match first.next().await {
317            None => {}
318            Some(Ok(chunk)) => {
319                yield Ok(chunk);
320                while let Some(item) = first.next().await {
321                    yield item;
322                }
323            }
324            Some(Err(e)) => {
325                imetric!("range_cache_client_fallback", "count", 1_u64);
326                debug!(
327                    "cache GET stream for {location} failed before the first chunk, \
328                     falling back to direct: {e}"
329                );
330                let direct_start = Instant::now();
331                let direct_result = direct.get_opts(&location, options).await;
332                fmetric!(
333                    "range_cache_client_direct_ms",
334                    "ms",
335                    direct_start.elapsed().as_secs_f64() * 1000.0
336                );
337                match direct_result {
338                    Ok(result) => {
339                        let mut body = result.into_stream();
340                        while let Some(item) = body.next().await {
341                            yield item;
342                        }
343                    }
344                    Err(direct_err) => yield Err(direct_err),
345                }
346            }
347        }
348    }
349    .boxed()
350}
351
352/// Build a `GetResult` for an already-buffered, small ranged payload (used
353/// only for the zero-length-range edge cases in `get_range_stream` and the
354/// HEAD-only path in `get_opts`, where there is nothing worth streaming).
355/// `range` is the slice actually returned while `object_size` is the full
356/// object size, per the `ObjectMeta` contract.
357fn build_get_result(
358    location: &Path,
359    data: Bytes,
360    range: Range<u64>,
361    object_size: u64,
362) -> GetResult {
363    let meta = ObjectMeta {
364        location: location.clone(),
365        last_modified: chrono::Utc::now(),
366        size: object_size,
367        e_tag: None,
368        version: None,
369    };
370    let payload = GetResultPayload::Stream(Box::pin(stream::once(async move { Ok(data) })));
371    GetResult {
372        payload,
373        meta,
374        range,
375        attributes: Attributes::default(),
376    }
377}
378
379/// Why reading a streamed `/ranges` response body failed: distinguishing the
380/// two cases lets `get_ranges` keep logging them the way the previous
381/// buffered implementation did (a transport error at `debug`, a
382/// protocol-level truncation at `warn`).
383enum RangesReadError {
384    Transport(reqwest::Error),
385    Truncated,
386}
387
388impl From<reqwest::Error> for RangesReadError {
389    fn from(e: reqwest::Error) -> Self {
390        RangesReadError::Transport(e)
391    }
392}
393
394/// Reassemble `count` length-prefixed frames (an 8-byte little-endian length
395/// followed by that many bytes, repeated once per requested range — see the
396/// server's `frame_ranges_stream`) from a streaming multi-range response
397/// body, mirroring `RangeCache::get_ranges`'s pending-chunk reassembly on the
398/// server side (`range_cache.rs`) instead of buffering the whole response
399/// with `resp.bytes().await` before parsing it.
400async fn read_framed_ranges(
401    mut stream: BoxStream<'static, reqwest::Result<Bytes>>,
402    count: usize,
403) -> Result<Vec<Bytes>, RangesReadError> {
404    let mut pending: Option<Bytes> = None;
405    let mut results = Vec::with_capacity(count);
406    for _ in 0..count {
407        let mut prefix = pull_exact(&mut stream, &mut pending, 8).await?;
408        let len = prefix.get_u64_le() as usize;
409        let data = pull_exact(&mut stream, &mut pending, len).await?;
410        results.push(data);
411    }
412    Ok(results)
413}
414
415/// Pull exactly `need` bytes out of `stream`, using `pending` as a one-chunk
416/// lookahead so a frame that straddles a network chunk boundary is
417/// reassembled correctly (mirrors `RangeCache::get_ranges`'s reassembly loop
418/// in `range_cache.rs`).
419async fn pull_exact(
420    stream: &mut BoxStream<'static, reqwest::Result<Bytes>>,
421    pending: &mut Option<Bytes>,
422    need: usize,
423) -> Result<Bytes, RangesReadError> {
424    let mut collected = BytesMut::with_capacity(need);
425    while collected.len() < need {
426        let chunk = match pending.take() {
427            Some(c) => c,
428            None => match stream.next().await {
429                Some(Ok(c)) => c,
430                Some(Err(e)) => return Err(e.into()),
431                None => return Err(RangesReadError::Truncated),
432            },
433        };
434        let remaining = need - collected.len();
435        if chunk.len() > remaining {
436            collected.put_slice(&chunk[..remaining]);
437            *pending = Some(chunk.slice(remaining..));
438        } else {
439            collected.put_slice(&chunk);
440        }
441    }
442    Ok(collected.freeze())
443}
444
445#[async_trait]
446impl ObjectStore for CacheClientStore {
447    async fn put_opts(
448        &self,
449        location: &Path,
450        payload: PutPayload,
451        opts: PutOptions,
452    ) -> object_store::Result<PutResult> {
453        self.direct.put_opts(location, payload, opts).await
454    }
455
456    async fn put_multipart_opts(
457        &self,
458        location: &Path,
459        opts: PutMultipartOptions,
460    ) -> object_store::Result<Box<dyn MultipartUpload>> {
461        self.direct.put_multipart_opts(location, opts).await
462    }
463
464    async fn get_opts(
465        &self,
466        location: &Path,
467        options: GetOptions,
468    ) -> object_store::Result<GetResult> {
469        use object_store::GetRange;
470
471        // The cache HTTP protocol can't convey conditional/version preconditions,
472        // so any such request must go straight to the direct store to preserve
473        // the expected 412/304 semantics.
474        if options.if_match.is_some()
475            || options.if_none_match.is_some()
476            || options.if_modified_since.is_some()
477            || options.if_unmodified_since.is_some()
478            || options.version.is_some()
479        {
480            return self.direct.get_opts(location, options).await;
481        }
482
483        // A head-only request needs metadata, not the body: return an empty
484        // payload with the true object size instead of streaming the object.
485        if options.head {
486            let result: Result<GetResult> = match self.head_size(location).await {
487                Ok(size) => Ok(build_get_result(location, Bytes::new(), 0..0, size)),
488                Err(e) => Err(e),
489            };
490            return match result {
491                Ok(r) => Ok(r),
492                Err(e) => {
493                    // Falling back to the direct store is a by-design graceful
494                    // degradation path (cache restarting/unreachable), not an
495                    // error: keep it at debug and let the fallback metric (which
496                    // is what dashboards alert on) carry the signal, so a cache
497                    // outage doesn't flood logs with one warning per read.
498                    imetric!("range_cache_client_fallback", "count", 1_u64);
499                    debug!("cache miss for {location} (head), falling back to direct: {e}");
500                    let direct_start = Instant::now();
501                    let direct_result = self.direct.get_opts(location, options).await;
502                    fmetric!(
503                        "range_cache_client_direct_ms",
504                        "ms",
505                        direct_start.elapsed().as_secs_f64() * 1000.0
506                    );
507                    direct_result
508                }
509            };
510        }
511
512        let result: Result<GetResult> = match &options.range {
513            None => self.get_full_stream(location, options.clone()).await,
514            // Issue the range GET and stream the body; the actual served
515            // range and the full object size come from the 206's
516            // `Content-Range` header (see `get_range_stream`), avoiding a
517            // preceding HEAD round-trip in the common case.
518            Some(GetRange::Bounded(r)) => {
519                self.get_range_stream(location, r.start, Some(r.end), options.clone())
520                    .await
521            }
522            // Open-ended range: the server resolves `-` against the true
523            // object size, returned to us via `Content-Range`.
524            Some(GetRange::Offset(offset)) => {
525                self.get_range_stream(location, *offset, None, options.clone())
526                    .await
527            }
528            // Suffix reads need the object size up front to compute the start
529            // offset, since the cache server's Range parser does not accept the
530            // `bytes=-N` suffix form. A HEAD is unavoidable here.
531            Some(GetRange::Suffix(suffix)) => match self.head_size(location).await {
532                Ok(size) => {
533                    let start = size.saturating_sub(*suffix);
534                    self.get_range_stream(location, start, Some(size), options.clone())
535                        .await
536                }
537                Err(e) => Err(e),
538            },
539        };
540
541        match result {
542            Ok(r) => Ok(r),
543            Err(e) => {
544                imetric!("range_cache_client_fallback", "count", 1_u64);
545                debug!("cache miss for {location}, falling back to direct: {e}");
546                let direct_start = Instant::now();
547                let direct_result = self.direct.get_opts(location, options).await;
548                fmetric!(
549                    "range_cache_client_direct_ms",
550                    "ms",
551                    direct_start.elapsed().as_secs_f64() * 1000.0
552                );
553                direct_result
554            }
555        }
556    }
557
558    async fn get_ranges(
559        &self,
560        location: &Path,
561        ranges: &[Range<u64>],
562    ) -> object_store::Result<Vec<Bytes>> {
563        if ranges.is_empty() {
564            return Ok(vec![]);
565        }
566
567        let round_trip_start = Instant::now();
568        let url = format!(
569            "{}/ranges/{}",
570            self.cache_base_url.trim_end_matches('/'),
571            location.as_ref()
572        );
573        let ranges_json: Vec<[u64; 2]> = ranges.iter().map(|r| [r.start, r.end]).collect();
574        let body = json!({ "ranges": ranges_json }).to_string();
575
576        let resp = match self
577            .add_auth(
578                self.http
579                    .post(&url)
580                    .header("Content-Type", "application/json")
581                    .body(body),
582            )
583            .send()
584            .await
585        {
586            Ok(r) if r.status().is_success() => r,
587            Ok(r) => {
588                imetric!("range_cache_client_fallback", "count", 1_u64);
589                debug!("cache ranges {url} status {}, falling back", r.status());
590                let direct_start = Instant::now();
591                let result = self.direct.get_ranges(location, ranges).await;
592                fmetric!(
593                    "range_cache_client_direct_ms",
594                    "ms",
595                    direct_start.elapsed().as_secs_f64() * 1000.0
596                );
597                return result;
598            }
599            Err(e) => {
600                imetric!("range_cache_client_fallback", "count", 1_u64);
601                debug!("cache ranges request failed: {e}, falling back to direct");
602                let direct_start = Instant::now();
603                let result = self.direct.get_ranges(location, ranges).await;
604                fmetric!(
605                    "range_cache_client_direct_ms",
606                    "ms",
607                    direct_start.elapsed().as_secs_f64() * 1000.0
608                );
609                return result;
610            }
611        };
612
613        // Stream the length-prefixed multi-range body (see the server's
614        // `frame_ranges_stream`) and reassemble each range's `Bytes` as its
615        // chunks arrive, instead of buffering the whole response with
616        // `.bytes()` into one contiguous allocation before any of it is
617        // used — the response can now be arbitrarily large since the server
618        // no longer caps total requested bytes. `read_framed_ranges` is a
619        // plain `Future` (not a `Stream`) that only resolves once every
620        // range has been read, so nothing is ever exposed to the caller
621        // before completion, and any read failure can still safely fall back
622        // to `self.direct` here, exactly like the previous buffered
623        // implementation.
624        match read_framed_ranges(resp.bytes_stream().boxed(), ranges.len()).await {
625            Ok(results) => {
626                // Distinct from `range_cache_client_roundtrip_ms`: that metric
627                // covers the streaming GET paths and is measured at
628                // time-to-headers (before any body bytes are read), while this
629                // path buffers the full framed response body via
630                // `read_framed_ranges` before emitting, so it measures
631                // time-to-full-body. Keeping them under separate names avoids
632                // conflating two different quantities in one distribution.
633                fmetric!(
634                    "range_cache_client_ranges_ms",
635                    "ms",
636                    round_trip_start.elapsed().as_secs_f64() * 1000.0
637                );
638                Ok(results)
639            }
640            Err(RangesReadError::Transport(e)) => {
641                imetric!("range_cache_client_fallback", "count", 1_u64);
642                debug!("reading ranges response failed: {e}, falling back to direct");
643                let direct_start = Instant::now();
644                let result = self.direct.get_ranges(location, ranges).await;
645                fmetric!(
646                    "range_cache_client_direct_ms",
647                    "ms",
648                    direct_start.elapsed().as_secs_f64() * 1000.0
649                );
650                result
651            }
652            Err(RangesReadError::Truncated) => {
653                // A truncated/garbled framing from our own cache is a protocol
654                // violation (unexpected), unlike the clean miss/outage paths
655                // above — keep this at warn.
656                imetric!("range_cache_client_fallback", "count", 1_u64);
657                warn!("truncated ranges response, falling back to direct");
658                let direct_start = Instant::now();
659                let result = self.direct.get_ranges(location, ranges).await;
660                fmetric!(
661                    "range_cache_client_direct_ms",
662                    "ms",
663                    direct_start.elapsed().as_secs_f64() * 1000.0
664                );
665                result
666            }
667        }
668    }
669
670    fn delete_stream(
671        &self,
672        locations: BoxStream<'static, object_store::Result<Path>>,
673    ) -> BoxStream<'static, object_store::Result<Path>> {
674        self.direct.delete_stream(locations)
675    }
676
677    fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, object_store::Result<ObjectMeta>> {
678        self.direct.list(prefix)
679    }
680
681    async fn list_with_delimiter(&self, prefix: Option<&Path>) -> object_store::Result<ListResult> {
682        self.direct.list_with_delimiter(prefix).await
683    }
684
685    async fn copy_opts(
686        &self,
687        from: &Path,
688        to: &Path,
689        options: CopyOptions,
690    ) -> object_store::Result<()> {
691        self.direct.copy_opts(from, to, options).await
692    }
693}