Skip to main content

opendal_layer_foyer/
lib.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18#![doc = include_str!("../README.md")]
19#![cfg_attr(docsrs, feature(doc_cfg))]
20#![cfg_attr(docsrs, doc(auto_cfg))]
21#![deny(missing_docs)]
22mod deleter;
23mod error;
24mod full;
25mod writer;
26
27use std::{
28    collections::HashSet,
29    ops::{Bound, Deref, Range, RangeBounds},
30    sync::{Arc, Mutex},
31};
32
33use foyer::{Code, HybridCache, Result as FoyerResult};
34
35use opendal_core::raw::*;
36use opendal_core::*;
37
38pub use deleter::Deleter;
39pub use writer::Writer;
40
41/// [`FoyerKey`] is a key for the foyer cache. It implements foyer's [`Code`] trait
42/// directly, so the layer does not depend on foyer's `serde` feature (and its bincode
43/// dependency).
44///
45/// It's possible to specify a version in the [`OpRead`] args:
46///
47/// - If a version is given, the object is cached under that versioned key.
48/// - If version is not supplied, the object is cached exactly as returned by the backend,
49///   We do NOT interpret `None` as "latest" and we do not promote it to any other version.
50#[derive(Debug, Clone, PartialEq, Eq, Hash)]
51pub struct FoyerKey {
52    /// Object path used as the cache key.
53    pub path: String,
54    /// Object version, when the read targets a specific version.
55    pub version: Option<String>,
56}
57
58impl Code for FoyerKey {
59    fn encode(&self, writer: &mut impl std::io::Write) -> FoyerResult<()> {
60        write_bytes(writer, self.path.as_bytes())?;
61        match &self.version {
62            None => writer.write_all(&[0u8])?,
63            Some(version) => {
64                writer.write_all(&[1u8])?;
65                write_bytes(writer, version.as_bytes())?;
66            }
67        }
68        Ok(())
69    }
70
71    fn decode(reader: &mut impl std::io::Read) -> FoyerResult<Self> {
72        let path = read_string(reader)?;
73        let mut tag = [0u8; 1];
74        reader.read_exact(&mut tag)?;
75        let version = match tag[0] {
76            0 => None,
77            1 => Some(read_string(reader)?),
78            other => {
79                return Err(std::io::Error::new(
80                    std::io::ErrorKind::InvalidData,
81                    format!("invalid FoyerKey version tag: {other}"),
82                )
83                .into());
84            }
85        };
86        Ok(FoyerKey { path, version })
87    }
88
89    fn estimated_size(&self) -> usize {
90        8 + self.path.len() + 1 + self.version.as_ref().map_or(0, |v| 8 + v.len())
91    }
92}
93
94fn write_bytes(writer: &mut impl std::io::Write, bytes: &[u8]) -> FoyerResult<()> {
95    writer.write_all(&(bytes.len() as u64).to_le_bytes())?;
96    writer.write_all(bytes)?;
97    Ok(())
98}
99
100fn read_string(reader: &mut impl std::io::Read) -> FoyerResult<String> {
101    let mut len = [0u8; 8];
102    reader.read_exact(&mut len)?;
103    let len = u64::from_le_bytes(len) as usize;
104    let mut buf = vec![0u8; len];
105    reader.read_exact(&mut buf)?;
106    String::from_utf8(buf)
107        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e).into())
108}
109
110/// [`FoyerValue`] stores buffered object data in Foyer.
111#[derive(Debug)]
112pub struct FoyerValue(
113    /// Buffered object data.
114    pub Buffer,
115);
116
117impl Deref for FoyerValue {
118    type Target = Buffer;
119
120    fn deref(&self) -> &Self::Target {
121        &self.0
122    }
123}
124
125impl Code for FoyerValue {
126    fn encode(&self, writer: &mut impl std::io::Write) -> FoyerResult<()> {
127        let len = self.0.len() as u64;
128        writer.write_all(&len.to_le_bytes())?;
129        std::io::copy(&mut self.0.clone(), writer)?;
130        Ok(())
131    }
132
133    fn decode(reader: &mut impl std::io::Read) -> FoyerResult<Self>
134    where
135        Self: Sized,
136    {
137        let mut len_bytes = [0u8; 8];
138        reader.read_exact(&mut len_bytes)?;
139        let len = u64::from_le_bytes(len_bytes) as usize;
140        let mut buffer = vec![0u8; len];
141        reader.read_exact(&mut buffer[..len])?;
142        Ok(FoyerValue(buffer.into()))
143    }
144
145    fn estimated_size(&self) -> usize {
146        8 + self.0.len()
147    }
148}
149
150/// `FoyerLayer` caches OpenDAL data with [foyer](https://github.com/foyer-rs/foyer).
151///
152/// # Operation Behavior
153/// - `write`: [`FoyerLayer`] caches data after the service completes the write.
154/// - `read`: [`FoyerLayer`] checks the cache first. On a cache miss, it reads
155///   from the service and caches the result.
156/// - `delete`: [`FoyerLayer`] removes cached data after a successful delete when the deleter
157///   closes. A failed delete is not invalidated. Cache invalidation happens before the underlying
158///   deleter is closed, so the data remains invalidated if closing the deleter fails.
159/// - Other operations: [`FoyerLayer`] passes operations such as `list`, `copy`,
160///   and `rename` to the service without caching their results.
161///
162/// # Examples
163///
164/// ```no_run
165/// use opendal_core::{Operator, services::Memory};
166/// use opendal_layer_foyer::FoyerLayer;
167/// use foyer::HybridCacheBuilder;
168///
169/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
170/// let cache = HybridCacheBuilder::new()
171///     .memory(64 * 1024 * 1024) // 64MB memory cache
172///     .with_shards(4)
173///     .storage()
174///     .build()
175///     .await?;
176///
177/// let op = Operator::new(Memory::default())?
178///     .layer(FoyerLayer::new(cache));
179/// # Ok(())
180/// # }
181/// ```
182///
183/// # Note
184///
185/// When object versioning is enabled, `FoyerLayer` treats objects with the same
186/// path but different versions as different objects.
187#[derive(Debug)]
188pub struct FoyerLayer {
189    cache: HybridCache<FoyerKey, FoyerValue>,
190    size_limit: Range<usize>,
191    deleted_keys: Arc<Mutex<HashSet<FoyerKey>>>,
192}
193
194impl FoyerLayer {
195    /// Creates a new `FoyerLayer` with the given foyer hybrid cache.
196    pub fn new(cache: HybridCache<FoyerKey, FoyerValue>) -> Self {
197        FoyerLayer {
198            cache,
199            size_limit: 0..usize::MAX,
200            deleted_keys: Arc::default(),
201        }
202    }
203
204    /// Set the object-size range eligible for caching.
205    ///
206    /// The layer reads through or writes through objects outside this range
207    /// without retaining them in Foyer. The default range accepts every
208    /// representable object size below `usize::MAX`.
209    pub fn with_size_limit<R: RangeBounds<usize>>(mut self, size_limit: R) -> Self {
210        let start = match size_limit.start_bound() {
211            Bound::Included(v) => *v,
212            Bound::Excluded(v) => *v + 1,
213            Bound::Unbounded => 0,
214        };
215        let end = match size_limit.end_bound() {
216            Bound::Included(v) => *v + 1,
217            Bound::Excluded(v) => *v,
218            Bound::Unbounded => usize::MAX,
219        };
220        self.size_limit = start..end;
221        self
222    }
223}
224
225impl Layer for FoyerLayer {
226    fn apply_service(&self, inner: Servicer) -> Servicer {
227        Arc::new(self.layer(inner))
228    }
229}
230
231impl FoyerLayer {
232    fn layer(&self, inner: Servicer) -> FoyerService {
233        FoyerService {
234            inner,
235            cache: self.cache.clone(),
236            size_limit: self.size_limit.clone(),
237            deleted_keys: self.deleted_keys.clone(),
238        }
239    }
240}
241
242#[derive(Debug)]
243pub(crate) struct Inner {
244    pub(crate) srv: Servicer,
245    pub(crate) ctx: OperationContext,
246    pub(crate) cache: HybridCache<FoyerKey, FoyerValue>,
247    pub(crate) deleted_keys: Arc<Mutex<HashSet<FoyerKey>>>,
248}
249
250#[derive(Debug)]
251#[doc(hidden)]
252pub struct FoyerService {
253    inner: Servicer,
254    cache: HybridCache<FoyerKey, FoyerValue>,
255    size_limit: Range<usize>,
256    deleted_keys: Arc<Mutex<HashSet<FoyerKey>>>,
257}
258
259impl FoyerService {
260    fn operation_inner(&self, ctx: &OperationContext) -> Arc<Inner> {
261        Arc::new(Inner {
262            srv: self.inner.clone(),
263            ctx: ctx.clone(),
264            cache: self.cache.clone(),
265            deleted_keys: self.deleted_keys.clone(),
266        })
267    }
268}
269
270impl Service for FoyerService {
271    type Reader = full::FullReader;
272    type Writer = Writer<oio::Writer>;
273    type Lister = oio::Lister;
274    type Deleter = Deleter<oio::Deleter>;
275    type Copier = oio::Copier;
276    type Composer = ();
277
278    fn info(&self) -> ServiceInfo {
279        self.inner.info()
280    }
281
282    fn capability(&self) -> Capability {
283        let mut capability = self.inner.capability();
284        capability.write_can_copy_from = false;
285        capability.compose = false;
286        capability.compose_with_content_type = false;
287        capability.compose_with_content_disposition = false;
288        capability.compose_with_content_encoding = false;
289        capability.compose_with_cache_control = false;
290        capability.compose_with_user_metadata = false;
291        capability.compose_with_if_match = false;
292        capability.compose_with_if_none_match = false;
293        capability.compose_with_if_version_match = false;
294        capability.compose_with_if_version_not_match = false;
295        capability.compose_with_if_not_exists = false;
296        capability.compose_with_source_version = false;
297        capability.compose_with_source_if_match = false;
298        capability.restore = false;
299        capability.restore_with_version = false;
300        capability.restore_with_if_not_exists = false;
301        capability
302    }
303
304    fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
305        Ok(full::FullReader::new(
306            self.operation_inner(ctx),
307            self.size_limit.clone(),
308            path.to_string(),
309            args,
310        ))
311    }
312
313    fn write(&self, ctx: &OperationContext, path: &str, args: OpWrite) -> Result<Self::Writer> {
314        let inner = self.operation_inner(ctx);
315        let size_limit = self.size_limit.clone();
316        let path = path.to_string();
317        let w = inner.srv.write(&inner.ctx, &path, args)?;
318        Ok(Writer::new(w, path, inner, size_limit))
319    }
320
321    fn delete(&self, ctx: &OperationContext) -> Result<Self::Deleter> {
322        let inner = self.operation_inner(ctx);
323        let d = inner.srv.delete(&inner.ctx)?;
324        Ok(Deleter::new(d, inner))
325    }
326
327    fn copy(
328        &self,
329        ctx: &OperationContext,
330        from: &str,
331        to: &str,
332        args: OpCopy,
333    ) -> Result<Self::Copier> {
334        self.inner.copy(ctx, from, to, args)
335    }
336
337    fn list(&self, ctx: &OperationContext, path: &str, args: OpList) -> Result<Self::Lister> {
338        self.inner.list(ctx, path, args)
339    }
340
341    async fn create_dir(
342        &self,
343        ctx: &OperationContext,
344        path: &str,
345        args: OpCreateDir,
346    ) -> Result<RpCreateDir> {
347        self.inner.create_dir(ctx, path, args).await
348    }
349
350    async fn stat(&self, ctx: &OperationContext, path: &str, args: OpStat) -> Result<RpStat> {
351        self.inner.stat(ctx, path, args).await
352    }
353
354    async fn rename(
355        &self,
356        ctx: &OperationContext,
357        from: &str,
358        to: &str,
359        args: OpRename,
360    ) -> Result<RpRename> {
361        self.inner.rename(ctx, from, to, args).await
362    }
363
364    async fn restore(
365        &self,
366        _ctx: &OperationContext,
367        _path: &str,
368        _args: OpRestore,
369    ) -> Result<RpRestore> {
370        Err(
371            Error::new(ErrorKind::Unsupported, "operation is not supported")
372                .with_operation(Operation::Restore),
373        )
374    }
375
376    async fn presign(
377        &self,
378        ctx: &OperationContext,
379        path: &str,
380        args: OpPresign,
381    ) -> Result<RpPresign> {
382        self.inner.presign(ctx, path, args).await
383    }
384
385    // TODO(MrCroxx): Implement copy, rename with foyer cache.
386}
387
388#[cfg(test)]
389mod tests {
390    use foyer::{
391        BlockEngineConfig, DeviceBuilder, Error as FoyerError, ErrorKind as FoyerErrorKind,
392        FsDeviceBuilder, HybridCache, HybridCacheBuilder, RecoverMode,
393    };
394    use opendal_core::raw::oio::Read as _;
395    use opendal_core::raw::oio::ReadStream as _;
396    use opendal_core::{Buffer, Operator, services::Memory};
397    use size::consts::MiB;
398    use std::io::Cursor;
399    use std::sync::Arc;
400    use std::sync::atomic::{AtomicUsize, Ordering};
401
402    use super::*;
403    use crate::error::extract_err;
404
405    fn key(i: u8) -> String {
406        format!("obj-{i}")
407    }
408
409    fn value(i: u8) -> Vec<u8> {
410        // ~ 64KiB with metadata
411        vec![i; 63 * 1024]
412    }
413
414    async fn memory_cache() -> HybridCache<FoyerKey, FoyerValue> {
415        HybridCacheBuilder::new()
416            .memory(1024 * 1024)
417            .with_shards(1)
418            .storage()
419            .with_recover_mode(RecoverMode::None)
420            .build()
421            .await
422            .unwrap()
423    }
424
425    #[derive(Debug)]
426    struct MockReadState {
427        data: Buffer,
428        stat_calls: AtomicUsize,
429        open_calls: AtomicUsize,
430        read_calls: AtomicUsize,
431        last_stat_args: Mutex<Option<OpStat>>,
432        last_read_args: Mutex<Option<OpRead>>,
433    }
434
435    impl MockReadState {
436        fn new(data: Buffer) -> Self {
437            Self {
438                data,
439                stat_calls: AtomicUsize::new(0),
440                open_calls: AtomicUsize::new(0),
441                read_calls: AtomicUsize::new(0),
442                last_stat_args: Mutex::new(None),
443                last_read_args: Mutex::new(None),
444            }
445        }
446
447        fn metadata(&self) -> Metadata {
448            {
449                let metadata = MetadataBuilder::file(self.data.len() as _);
450                metadata.build()
451            }
452        }
453
454        fn rp_read(&self) -> RpRead {
455            RpRead::new(self.metadata())
456        }
457
458        fn read_range(&self, range: BytesRange) -> Buffer {
459            self.data.slice(range.to_range_as_usize())
460        }
461    }
462
463    #[derive(Debug, Clone)]
464    struct MockReadService {
465        state: Arc<MockReadState>,
466    }
467
468    impl MockReadService {
469        fn new(data: impl Into<Buffer>) -> Self {
470            Self {
471                state: Arc::new(MockReadState::new(data.into())),
472            }
473        }
474    }
475
476    #[derive(Debug)]
477    struct MockReadReader {
478        state: Arc<MockReadState>,
479    }
480
481    impl oio::Read for MockReadReader {
482        async fn open(&self, range: BytesRange) -> Result<(RpRead, Box<dyn oio::ReadStreamDyn>)> {
483            self.state.open_calls.fetch_add(1, Ordering::Relaxed);
484            let buffer = self.state.read_range(range);
485            Ok((
486                self.state.rp_read(),
487                Box::new(buffer) as Box<dyn oio::ReadStreamDyn>,
488            ))
489        }
490
491        async fn read(&self, range: BytesRange) -> Result<(RpRead, Buffer)> {
492            self.state.read_calls.fetch_add(1, Ordering::Relaxed);
493            if range.size().is_none() {
494                return Err(Error::new(
495                    ErrorKind::Unsupported,
496                    "mock reader requires a bounded read range",
497                ));
498            }
499
500            Ok((self.state.rp_read(), self.state.read_range(range)))
501        }
502    }
503
504    impl Service for MockReadService {
505        type Reader = MockReadReader;
506        type Writer = ();
507        type Lister = ();
508        type Deleter = ();
509        type Copier = ();
510        type Composer = ();
511
512        fn info(&self) -> ServiceInfo {
513            ServiceInfo::with_scheme("mock")
514        }
515
516        fn capability(&self) -> Capability {
517            Capability {
518                read: true,
519                stat: true,
520                restore: true,
521                restore_with_version: true,
522                restore_with_if_not_exists: true,
523                ..Default::default()
524            }
525        }
526
527        async fn create_dir(
528            &self,
529            _: &OperationContext,
530            _: &str,
531            _: OpCreateDir,
532        ) -> Result<RpCreateDir> {
533            Err(Error::new(
534                ErrorKind::Unsupported,
535                "operation is not supported",
536            ))
537        }
538
539        async fn stat(&self, _: &OperationContext, _: &str, args: OpStat) -> Result<RpStat> {
540            self.state.stat_calls.fetch_add(1, Ordering::Relaxed);
541            *self.state.last_stat_args.lock().unwrap() = Some(args);
542            Ok(RpStat::new(self.state.metadata()))
543        }
544
545        fn read(&self, _ctx: &OperationContext, _: &str, args: OpRead) -> Result<Self::Reader> {
546            *self.state.last_read_args.lock().unwrap() = Some(args);
547            Ok(MockReadReader {
548                state: self.state.clone(),
549            })
550        }
551
552        fn write(&self, _ctx: &OperationContext, _: &str, _: OpWrite) -> Result<Self::Writer> {
553            Err(Error::new(
554                ErrorKind::Unsupported,
555                "operation is not supported",
556            ))
557        }
558
559        fn delete(&self, _ctx: &OperationContext) -> Result<Self::Deleter> {
560            Err(Error::new(
561                ErrorKind::Unsupported,
562                "operation is not supported",
563            ))
564        }
565
566        fn list(&self, _ctx: &OperationContext, _: &str, _: OpList) -> Result<Self::Lister> {
567            Err(Error::new(
568                ErrorKind::Unsupported,
569                "operation is not supported",
570            ))
571        }
572
573        fn copy(&self, _: &OperationContext, _: &str, _: &str, _: OpCopy) -> Result<Self::Copier> {
574            Err(Error::new(
575                ErrorKind::Unsupported,
576                "operation is not supported",
577            ))
578        }
579
580        async fn rename(
581            &self,
582            _: &OperationContext,
583            _: &str,
584            _: &str,
585            _: OpRename,
586        ) -> Result<RpRename> {
587            Err(Error::new(
588                ErrorKind::Unsupported,
589                "operation is not supported",
590            ))
591        }
592
593        async fn presign(&self, _: &OperationContext, _: &str, _: OpPresign) -> Result<RpPresign> {
594            Err(Error::new(
595                ErrorKind::Unsupported,
596                "operation is not supported",
597            ))
598        }
599    }
600
601    fn service_context(_: &Servicer) -> OperationContext {
602        OperationContext::new()
603    }
604
605    #[tokio::test]
606    async fn test_restore_is_not_supported() {
607        let cache = memory_cache().await;
608        let source = Arc::new(MockReadService::new("0123456789"));
609        assert!(source.capability().restore);
610
611        let service = FoyerLayer::new(cache).apply_service(source);
612        let capability = service.capability();
613        assert!(!capability.restore);
614        assert!(!capability.restore_with_version);
615        assert!(!capability.restore_with_if_not_exists);
616
617        let err = service
618            .restore(&OperationContext::new(), "test", OpRestore::new())
619            .await
620            .expect_err("FoyerLayer must reject restore operations");
621        assert_eq!(err.kind(), ErrorKind::Unsupported);
622    }
623
624    #[tokio::test]
625    async fn test_full_reader_open_fallback_preserves_stream() {
626        let cache = memory_cache().await;
627        let source = Arc::new(MockReadService::new("0123456789"));
628        let state = source.state.clone();
629        let service = FoyerLayer::new(cache)
630            .with_size_limit(0..1)
631            .apply_service(source);
632        let ctx = service_context(&service);
633
634        let reader = service.read(&ctx, "test", OpRead::default()).unwrap();
635        let (_, mut stream) = reader.open(BytesRange::new(0, None)).await.unwrap();
636        let buffer = stream.read_all().await.unwrap();
637
638        assert_eq!(buffer.to_vec(), b"0123456789");
639        assert_eq!(state.open_calls.load(Ordering::Relaxed), 1);
640        assert_eq!(state.read_calls.load(Ordering::Relaxed), 0);
641        assert_eq!(state.stat_calls.load(Ordering::Relaxed), 1);
642    }
643
644    #[tokio::test]
645    async fn test_full_reader_open_fills_cache() {
646        let cache = memory_cache().await;
647        let source = Arc::new(MockReadService::new("0123456789"));
648        let state = source.state.clone();
649        let service = FoyerLayer::new(cache)
650            .with_size_limit(0..100)
651            .apply_service(source);
652        let ctx = service_context(&service);
653
654        let reader = service.read(&ctx, "test", OpRead::default()).unwrap();
655        let (_, mut stream) = reader.open(BytesRange::from(0_u64..2)).await.unwrap();
656        let buffer = stream.read_all().await.unwrap();
657
658        assert_eq!(buffer.to_vec(), b"01");
659        assert_eq!(state.stat_calls.load(Ordering::Relaxed), 1);
660        assert_eq!(state.open_calls.load(Ordering::Relaxed), 1);
661        assert_eq!(state.read_calls.load(Ordering::Relaxed), 0);
662
663        let reader = service.read(&ctx, "test", OpRead::default()).unwrap();
664        let (_, mut stream) = reader.open(BytesRange::from(4_u64..7)).await.unwrap();
665        let buffer = stream.read_all().await.unwrap();
666
667        assert_eq!(buffer.to_vec(), b"456");
668        assert_eq!(state.stat_calls.load(Ordering::Relaxed), 1);
669        assert_eq!(state.open_calls.load(Ordering::Relaxed), 1);
670        assert_eq!(state.read_calls.load(Ordering::Relaxed), 0);
671    }
672
673    #[tokio::test]
674    async fn test_cache_fill_preserves_read_args() {
675        let cache = memory_cache().await;
676        let source = Arc::new(MockReadService::new("0123456789"));
677        let state = source.state.clone();
678        let service = FoyerLayer::new(cache)
679            .with_size_limit(0..100)
680            .apply_service(source);
681        let ctx = service_context(&service);
682
683        let (_, args, _) = options::ReadOptions {
684            version: Some("v1".to_owned()),
685            if_match: Some("etag-1".to_owned()),
686            ..Default::default()
687        }
688        .into();
689        let reader = service.read(&ctx, "test", args).unwrap();
690        let (_, mut stream) = reader.open(BytesRange::new(0, None)).await.unwrap();
691        stream.read_all().await.unwrap();
692
693        let stat_args = state.last_stat_args.lock().unwrap().clone().unwrap();
694        assert_eq!(stat_args.version(), Some("v1"));
695        assert_eq!(stat_args.if_match(), Some("etag-1"));
696
697        let read_args = state.last_read_args.lock().unwrap().clone().unwrap();
698        assert_eq!(read_args.version(), Some("v1"));
699        assert_eq!(read_args.if_match(), Some("etag-1"));
700    }
701
702    #[tokio::test]
703    async fn test() {
704        let dir = tempfile::tempdir().unwrap();
705
706        let cache = HybridCacheBuilder::new()
707            .memory(10)
708            .with_shards(1)
709            .storage()
710            .with_engine_config(
711                BlockEngineConfig::new(
712                    FsDeviceBuilder::new(dir.path())
713                        .with_capacity(16 * MiB as usize)
714                        .build()
715                        .unwrap(),
716                )
717                .with_block_size(MiB as usize),
718            )
719            .with_recover_mode(RecoverMode::None)
720            .build()
721            .await
722            .unwrap();
723
724        let op = Operator::new(Memory::default())
725            .unwrap()
726            .layer(FoyerLayer::new(cache.clone()));
727
728        assert!(op.list("/").await.unwrap().is_empty());
729
730        for i in 0..64 {
731            op.write(&key(i), value(i)).await.unwrap();
732        }
733
734        assert_eq!(op.list("/").await.unwrap().len(), 64);
735
736        for i in 0..64 {
737            let buf = op.read(&key(i)).await.unwrap();
738            assert_eq!(buf.to_vec(), value(i));
739        }
740
741        cache.clear().await.unwrap();
742
743        for i in 0..64 {
744            let buf = op.read(&key(i)).await.unwrap();
745            assert_eq!(buf.to_vec(), value(i));
746        }
747
748        for i in 0..64 {
749            op.delete(&key(i)).await.unwrap();
750        }
751
752        assert!(op.list("/").await.unwrap().is_empty());
753
754        for i in 0..64 {
755            let res = op.read(&key(i)).await;
756            assert!(res.is_err(), "should fail to read deleted file");
757        }
758    }
759
760    #[tokio::test]
761    async fn test_size_limit() {
762        let dir = tempfile::tempdir().unwrap();
763
764        let cache = HybridCacheBuilder::new()
765            .memory(1024 * 1024)
766            .with_shards(1)
767            .storage()
768            .with_engine_config(
769                BlockEngineConfig::new(
770                    FsDeviceBuilder::new(dir.path())
771                        .with_capacity(16 * MiB as usize)
772                        .build()
773                        .unwrap(),
774                )
775                .with_block_size(MiB as usize),
776            )
777            .with_recover_mode(RecoverMode::None)
778            .build()
779            .await
780            .unwrap();
781
782        // Set size limit: only cache files between 1KB and 10KB
783        let op = Operator::new(Memory::default())
784            .unwrap()
785            .layer(FoyerLayer::new(cache.clone()).with_size_limit(1024..10 * 1024));
786
787        let small_data = vec![1u8; 5 * 1024]; // 5KB - should be cached
788        let large_data = vec![2u8; 20 * 1024]; // 20KB - should NOT be cached
789        let tiny_data = vec![3u8; 512]; // 512B - below size limit, should NOT be cached
790
791        // Write all files
792        op.write("small.txt", small_data.clone()).await.unwrap();
793        op.write("large.txt", large_data.clone()).await.unwrap();
794        op.write("tiny.txt", tiny_data.clone()).await.unwrap();
795
796        // All should be readable
797        let read_small = op.read("small.txt").await.unwrap();
798        assert_eq!(read_small.to_vec(), small_data);
799
800        let read_large = op.read("large.txt").await.unwrap();
801        assert_eq!(read_large.to_vec(), large_data);
802
803        let read_tiny = op.read("tiny.txt").await.unwrap();
804        assert_eq!(read_tiny.to_vec(), tiny_data);
805
806        // Clear the cache to test read-through behavior
807        cache.clear().await.unwrap();
808
809        // All files should still be readable from underlying storage
810        let read_small = op.read("small.txt").await.unwrap();
811        assert_eq!(read_small.to_vec(), small_data);
812
813        let read_large = op.read("large.txt").await.unwrap();
814        assert_eq!(read_large.to_vec(), large_data);
815
816        let read_tiny = op.read("tiny.txt").await.unwrap();
817        assert_eq!(read_tiny.to_vec(), tiny_data);
818
819        // After reading, small file should be cached, but large and tiny should not
820        // We can verify this by reading with range - cached files should support range reads
821        let read_small_range = op.read_with("small.txt").range(0..1024).await.unwrap();
822        assert_eq!(read_small_range.len(), 1024);
823        assert_eq!(read_small_range.to_vec(), small_data[0..1024]);
824    }
825
826    #[test]
827    fn test_error() {
828        let e = Error::new(ErrorKind::NotFound, "not found");
829        let fe = FoyerError::new(FoyerErrorKind::External, "external error").with_source(e);
830        let oe = extract_err(fe);
831        assert_eq!(oe.kind(), ErrorKind::NotFound);
832    }
833
834    #[test]
835    fn test_foyer_key_version_none_vs_empty() {
836        let key_none = FoyerKey {
837            path: "test/path".to_string(),
838            version: None,
839        };
840
841        let key_empty = FoyerKey {
842            path: "test/path".to_string(),
843            version: Some("".to_string()),
844        };
845
846        let mut buf_none = Vec::new();
847        key_none.encode(&mut buf_none).unwrap();
848
849        let mut buf_empty = Vec::new();
850        key_empty.encode(&mut buf_empty).unwrap();
851
852        assert_ne!(
853            buf_none, buf_empty,
854            "Serialization of version=None and version=\"\" should be different"
855        );
856
857        let decoded_none = FoyerKey::decode(&mut Cursor::new(&buf_none)).unwrap();
858        assert_eq!(decoded_none, key_none);
859        let decoded_empty = FoyerKey::decode(&mut Cursor::new(&buf_empty)).unwrap();
860        assert_eq!(decoded_empty, key_empty);
861    }
862
863    #[test]
864    fn test_foyer_key_serde() {
865        use std::io::Cursor;
866
867        let test_cases = vec![
868            FoyerKey {
869                path: "simple".to_string(),
870                version: None,
871            },
872            FoyerKey {
873                path: "with/slash/path".to_string(),
874                version: None,
875            },
876            FoyerKey {
877                path: "versioned".to_string(),
878                version: Some("v1.0.0".to_string()),
879            },
880            FoyerKey {
881                path: "empty-version".to_string(),
882                version: Some("".to_string()),
883            },
884            FoyerKey {
885                path: "".to_string(),
886                version: None,
887            },
888            FoyerKey {
889                path: "unicode/θ·―εΎ„/πŸš€".to_string(),
890                version: Some("η‰ˆζœ¬-1".to_string()),
891            },
892            FoyerKey {
893                path: "long/".to_string().repeat(100),
894                version: Some("long-version-".to_string().repeat(50)),
895            },
896        ];
897
898        for original in test_cases {
899            let mut buffer = Vec::new();
900            original
901                .encode(&mut buffer)
902                .expect("encoding should succeed");
903
904            let decoded =
905                FoyerKey::decode(&mut Cursor::new(&buffer)).expect("decoding should succeed");
906
907            assert_eq!(
908                decoded, original,
909                "decode(encode(key)) should equal original key"
910            );
911        }
912    }
913}