Skip to main content

foyer_storage/
store.rs

1// Copyright 2026 foyer Project Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::{
16    any::{Any, TypeId},
17    borrow::Cow,
18    fmt::Debug,
19    hash::Hash,
20    sync::Arc,
21    time::Instant,
22};
23
24use equivalent::Equivalent;
25use foyer_common::{
26    code::{HashBuilder, StorageKey, StorageValue},
27    error::Result,
28    metrics::Metrics,
29    properties::{Age, Properties},
30    spawn::Spawner,
31};
32use foyer_memory::{Cache, Piece};
33
34#[cfg(any(test, feature = "test_utils"))]
35use crate::test_utils::*;
36use crate::{
37    StorageFilterResult,
38    compress::Compression,
39    engine::{
40        Engine, EngineBuildContext, EngineConfig, Load, Populated, RecoverMode,
41        noop::{NoopEngine, NoopEngineConfig},
42    },
43    io::{
44        device::{Device, statistics::Statistics, throttle::Throttle},
45        engine::{IoEngineBuildContext, IoEngineConfig, monitor::MonitoredIoEngine, psync::PsyncIoEngineConfig},
46    },
47    keeper::Keeper,
48    serde::EntrySerializer,
49};
50
51/// The disk cache engine that serves as the storage backend of `foyer`.
52pub struct Store<K, V, S, P>
53where
54    K: StorageKey,
55    V: StorageValue,
56    S: HashBuilder + Debug,
57    P: Properties,
58{
59    inner: Arc<StoreInner<K, V, S, P>>,
60}
61
62struct StoreInner<K, V, S, P>
63where
64    K: StorageKey,
65    V: StorageValue,
66    S: HashBuilder + Debug,
67    P: Properties,
68{
69    hasher: Arc<S>,
70
71    keeper: Keeper<K, V, P>,
72    engine: Arc<dyn Engine<K, V, P>>,
73
74    compression: Compression,
75
76    spawner: Spawner,
77
78    metrics: Arc<Metrics>,
79
80    #[cfg(any(test, feature = "test_utils"))]
81    load_throttle_switch: LoadThrottleSwitch,
82}
83
84impl<K, V, S, P> Debug for Store<K, V, S, P>
85where
86    K: StorageKey,
87    V: StorageValue,
88    S: HashBuilder + Debug,
89    P: Properties,
90{
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        f.debug_struct("Store")
93            .field("keeper", &self.inner.keeper)
94            .field("engine", &self.inner.engine)
95            .field("compression", &self.inner.compression)
96            .field("runtimes", &self.inner.spawner)
97            .finish()
98    }
99}
100
101impl<K, V, S, P> Clone for Store<K, V, S, P>
102where
103    K: StorageKey,
104    V: StorageValue,
105    S: HashBuilder + Debug,
106    P: Properties,
107{
108    fn clone(&self) -> Self {
109        Self {
110            inner: self.inner.clone(),
111        }
112    }
113}
114
115impl<K, V, S, P> Store<K, V, S, P>
116where
117    K: StorageKey,
118    V: StorageValue,
119    S: HashBuilder + Debug,
120    P: Properties,
121{
122    /// Close the disk cache gracefully.
123    ///
124    /// `close` will wait for all ongoing flush and reclaim tasks to finish.
125    pub async fn close(&self) -> Result<()> {
126        self.inner.engine.close().await
127    }
128
129    /// Return if the given key can be picked by the admission filter.
130    pub fn filter(&self, hash: u64, estimated_size: usize) -> StorageFilterResult {
131        self.inner.engine.filter(hash, estimated_size)
132    }
133
134    /// Push a in-memory cache piece to the disk cache write queue.
135    pub fn enqueue(&self, piece: Piece<K, V, P>, force: bool) {
136        tracing::trace!(hash = piece.hash(), "[store]: enqueue piece");
137        let now = Instant::now();
138
139        if force
140            || self
141                .filter(
142                    piece.hash(),
143                    piece.key().estimated_size() + piece.value().estimated_size(),
144                )
145                .is_admitted()
146        {
147            let estimated_size = EntrySerializer::estimated_size(piece.key(), piece.value());
148            let rpiece = self.inner.keeper.insert(piece);
149            self.inner.engine.enqueue(rpiece, estimated_size);
150        } else {
151            self.delete(piece.key());
152        }
153
154        self.inner.metrics.storage_enqueue.increase(1);
155        self.inner
156            .metrics
157            .storage_enqueue_duration
158            .record(now.elapsed().as_secs_f64());
159    }
160
161    /// Load a cache entry from the disk cache.
162    pub async fn load<Q>(&self, key: &Q) -> Result<Load<K, V, P>>
163    where
164        Q: Hash + Equivalent<K> + ?Sized,
165    {
166        let now = Instant::now();
167
168        let hash = self.inner.hasher.hash_one(key);
169
170        if let Some(piece) = self.inner.keeper.get(hash, key) {
171            tracing::trace!(hash, "[store]: load from keeper");
172            return Ok(Load::Piece {
173                piece,
174                populated: Populated { age: Age::Young },
175            });
176        }
177
178        #[cfg(feature = "test_utils")]
179        if self.inner.load_throttle_switch.is_throttled() {
180            self.inner.metrics.storage_throttled.increase(1);
181            self.inner
182                .metrics
183                .storage_throttled_duration
184                .record(now.elapsed().as_secs_f64());
185            return Ok(Load::Throttled);
186        }
187
188        match self.inner.engine.load(hash).await {
189            Ok(Load::Entry {
190                key: k,
191                value: v,
192                populated: p,
193            }) if key.equivalent(&k) => {
194                self.inner.metrics.storage_hit.increase(1);
195                self.inner
196                    .metrics
197                    .storage_hit_duration
198                    .record(now.elapsed().as_secs_f64());
199                Ok(Load::Entry {
200                    key: k,
201                    value: v,
202                    populated: p,
203                })
204            }
205            Ok(Load::Piece { piece, populated }) if key.equivalent(piece.key()) => {
206                self.inner.metrics.storage_hit.increase(1);
207                self.inner
208                    .metrics
209                    .storage_hit_duration
210                    .record(now.elapsed().as_secs_f64());
211                Ok(Load::Piece { piece, populated })
212            }
213            Ok(Load::Entry { .. }) | Ok(Load::Piece { .. }) => {
214                self.inner.metrics.storage_miss.increase(1);
215                self.inner.metrics.storage_false_positive.increase(1);
216                self.inner
217                    .metrics
218                    .storage_miss_duration
219                    .record(now.elapsed().as_secs_f64());
220                Ok(Load::Miss)
221            }
222            Ok(Load::Miss) => {
223                self.inner.metrics.storage_miss.increase(1);
224                self.inner
225                    .metrics
226                    .storage_miss_duration
227                    .record(now.elapsed().as_secs_f64());
228                Ok(Load::Miss)
229            }
230            Ok(Load::Throttled) => {
231                self.inner.metrics.storage_throttled.increase(1);
232                self.inner
233                    .metrics
234                    .storage_throttled_duration
235                    .record(now.elapsed().as_secs_f64());
236                Ok(Load::Throttled)
237            }
238            Err(e) => {
239                self.inner.metrics.storage_error.increase(1);
240                Err(e)
241            }
242        }
243    }
244
245    /// Delete the cache entry with the given key from the disk cache.
246    pub fn delete<'a, Q>(&'a self, key: &'a Q)
247    where
248        Q: Hash + Equivalent<K> + ?Sized,
249    {
250        let now = Instant::now();
251
252        let hash = self.inner.hasher.hash_one(key);
253        self.inner.engine.delete(hash);
254
255        self.inner.metrics.storage_delete.increase(1);
256        self.inner
257            .metrics
258            .storage_delete_duration
259            .record(now.elapsed().as_secs_f64());
260    }
261
262    /// Check if the disk cache contains a cached entry with the given key.
263    ///
264    /// `contains` may return a false-positive result if there is a hash collision with the given key.
265    pub fn may_contains<Q>(&self, key: &Q) -> bool
266    where
267        Q: Hash + Equivalent<K> + ?Sized,
268    {
269        let hash = self.inner.hasher.hash_one(key);
270        self.inner.engine.may_contains(hash)
271    }
272
273    /// Delete all cached entries of the disk cache.
274    pub async fn destroy(&self) -> Result<()> {
275        self.inner.engine.destroy().await
276    }
277
278    /// Get the device of the disk cache.
279    pub fn device(&self) -> &Arc<dyn Device> {
280        self.inner.engine.device()
281    }
282
283    /// Get the statistics information of the disk cache.
284    pub fn statistics(&self) -> &Arc<Statistics> {
285        self.inner.engine.device().statistics()
286    }
287
288    /// Get the io throttle of the disk cache.
289    pub fn throttle(&self) -> &Throttle {
290        self.inner.engine.device().statistics().throttle()
291    }
292
293    /// Get the spawner.
294    pub fn spawner(&self) -> &Spawner {
295        &self.inner.spawner
296    }
297
298    /// Wait for the ongoing flush and reclaim tasks to finish.
299    pub async fn wait(&self) {
300        self.inner.engine.wait().await
301    }
302
303    /// Return the estimated serialized size of the entry.
304    pub fn entry_estimated_size(&self, key: &K, value: &V) -> usize {
305        EntrySerializer::estimated_size(key, value)
306    }
307
308    /// Get the load throttle switch for the disk cache.
309    #[cfg(feature = "test_utils")]
310    pub fn load_throttle_switch(&self) -> &LoadThrottleSwitch {
311        &self.inner.load_throttle_switch
312    }
313
314    /// If the disk cache is enabled.
315    pub fn is_enabled(&self) -> bool {
316        self.inner.engine.type_id() != TypeId::of::<Arc<NoopEngine<K, V, P>>>()
317    }
318}
319
320/// The builder of the disk cache.
321pub struct StoreBuilder<K, V, S, P>
322where
323    K: StorageKey,
324    V: StorageValue,
325    S: HashBuilder + Debug,
326    P: Properties,
327{
328    name: Cow<'static, str>,
329    memory: Cache<K, V, S, P>,
330    metrics: Arc<Metrics>,
331
332    io_engine_config: Option<Box<dyn IoEngineConfig>>,
333    engine_config: Option<Box<dyn EngineConfig<K, V, P>>>,
334
335    spawner: Option<Spawner>,
336
337    compression: Compression,
338    recover_mode: RecoverMode,
339
340    #[cfg(any(test, feature = "test_utils"))]
341    load_throttle_switch: LoadThrottleSwitch,
342}
343
344impl<K, V, S, P> Debug for StoreBuilder<K, V, S, P>
345where
346    K: StorageKey,
347    V: StorageValue,
348    S: HashBuilder + Debug,
349    P: Properties,
350{
351    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
352        f.debug_struct("StoreBuilder")
353            .field("name", &self.name)
354            .field("memory", &self.memory)
355            .field("metrics", &self.metrics)
356            .field("io_engine_builder", &self.io_engine_config)
357            .field("engine_builder", &self.engine_config)
358            .field("spawner", &self.spawner)
359            .field("compression", &self.compression)
360            .field("recover_mode", &self.recover_mode)
361            .finish()
362    }
363}
364
365impl<K, V, S, P> StoreBuilder<K, V, S, P>
366where
367    K: StorageKey,
368    V: StorageValue,
369    S: HashBuilder + Debug,
370    P: Properties,
371{
372    /// Setup disk cache store for the given in-memory cache.
373    pub fn new(name: impl Into<Cow<'static, str>>, memory: Cache<K, V, S, P>, metrics: Arc<Metrics>) -> Self {
374        Self {
375            name: name.into(),
376            memory,
377            metrics,
378
379            io_engine_config: None,
380            engine_config: None,
381
382            spawner: None,
383
384            compression: Compression::default(),
385            recover_mode: RecoverMode::default(),
386            #[cfg(any(test, feature = "test_utils"))]
387            load_throttle_switch: LoadThrottleSwitch::default(),
388        }
389    }
390
391    /// Set io engine config for the disk cache store.
392    ///
393    /// Default: [`crate::io::engine::psync::PsyncIoEngineConfig`].
394    pub fn with_io_engine_config(mut self, io_engine_builder: impl Into<Box<dyn IoEngineConfig>>) -> Self {
395        self.io_engine_config = Some(io_engine_builder.into());
396        self
397    }
398
399    /// Set engine config for the disk cache store.
400    pub fn with_engine_config(mut self, config: impl Into<Box<dyn EngineConfig<K, V, P>>>) -> Self {
401        self.engine_config = Some(config.into());
402        self
403    }
404
405    /// Set the compression algorithm of the disk cache store.
406    ///
407    /// Default: [`Compression::None`].
408    pub fn with_compression(mut self, compression: Compression) -> Self {
409        self.compression = compression;
410        self
411    }
412
413    /// Set the recover mode for the disk cache store.
414    ///
415    /// See more in [`RecoverMode`].
416    ///
417    /// Default: [`RecoverMode::Quiet`].
418    pub fn with_recover_mode(mut self, recover_mode: RecoverMode) -> Self {
419        self.recover_mode = recover_mode;
420        self
421    }
422
423    /// Configure the task spawner for the disk cache store.
424    ///
425    /// By default, it will use the current spawner that built foyer.
426    ///
427    /// For example, with tokio, it will be `tokio::runtime::Handle::current()`.
428    ///
429    /// FYI: [`Spawner`] and [`Spawner::current()`]
430    pub fn with_spawner(mut self, spawner: Spawner) -> Self {
431        self.spawner = Some(spawner);
432        self
433    }
434
435    /// Set the load throttle switch for the disk cache store.
436    #[cfg(any(test, feature = "test_utils"))]
437    pub fn with_load_throttle_switch(mut self, switch: LoadThrottleSwitch) -> Self {
438        self.load_throttle_switch = switch;
439        self
440    }
441
442    #[doc(hidden)]
443    pub fn is_noop(&self) -> bool {
444        self.engine_config.is_none()
445    }
446
447    /// Build the disk cache store with the given configuration.
448    pub async fn build(self) -> Result<Store<K, V, S, P>> {
449        let memory = self.memory;
450        let metrics = self.metrics;
451
452        let compression = self.compression;
453
454        let spawner = self.spawner.unwrap_or_else(Spawner::current);
455
456        let io_engine_builder = match self.io_engine_config {
457            Some(builder) => builder,
458            None => {
459                tracing::info!(
460                    "[store builder]: No I/O engine builder is provided, use `PsyncIoEngineConfig` with default parameters as default."
461                );
462                PsyncIoEngineConfig::new().boxed()
463            }
464        };
465        let io_engine = io_engine_builder
466            .build(IoEngineBuildContext {
467                spawner: spawner.clone(),
468            })
469            .await?;
470        let io_engine = MonitoredIoEngine::new(io_engine, metrics.clone());
471
472        let engine_builder = match self.engine_config {
473            Some(eb) => eb,
474            None => {
475                tracing::info!(
476                    "[store builder]: No engine builder is provided, run disk cache in mock mode that do nothing."
477                );
478
479                Box::<NoopEngineConfig<K, V, P>>::default()
480            }
481        };
482
483        let engine = engine_builder
484            .build(EngineBuildContext {
485                io_engine,
486                metrics: metrics.clone(),
487                spawner: spawner.clone(),
488                recover_mode: self.recover_mode,
489            })
490            .await?;
491
492        let keeper = Keeper::new(memory.shards());
493        let hasher = memory.hash_builder().clone();
494        #[cfg(any(test, feature = "test_utils"))]
495        let load_throttle_switch = self.load_throttle_switch;
496        let inner = StoreInner {
497            hasher,
498            keeper,
499            engine,
500            compression,
501            spawner,
502            metrics,
503            #[cfg(any(test, feature = "test_utils"))]
504            load_throttle_switch,
505        };
506        let inner = Arc::new(inner);
507        let store = Store { inner };
508
509        Ok(store)
510    }
511}
512
513#[cfg(test)]
514mod tests {
515    use foyer_common::hasher::ModHasher;
516    use foyer_memory::CacheBuilder;
517
518    use super::*;
519    use crate::{
520        DeviceBuilder,
521        engine::block::engine::BlockEngineConfig,
522        io::{device::fs::FsDeviceBuilder, engine::psync::PsyncIoEngineConfig},
523    };
524
525    #[tokio::test]
526    async fn test_build_with_unaligned_buffer_pool_size() {
527        let dir = tempfile::tempdir().unwrap();
528        let metrics = Arc::new(Metrics::noop());
529        let memory: Cache<u64, u64> = CacheBuilder::new(10).build();
530        let _ = StoreBuilder::new("test", memory, metrics)
531            .with_io_engine_config(PsyncIoEngineConfig::new())
532            .with_engine_config(
533                BlockEngineConfig::new(
534                    FsDeviceBuilder::new(dir.path())
535                        .with_capacity(64 * 1024)
536                        .build()
537                        .unwrap(),
538                )
539                .with_flushers(3)
540                .with_block_size(16 * 1024)
541                .with_buffer_pool_size(128 * 1024 * 1024),
542            )
543            .build()
544            .await
545            .unwrap();
546    }
547
548    #[tokio::test]
549    async fn test_entry_hash_collision() {
550        let dir = tempfile::tempdir().unwrap();
551        let metrics = Arc::new(Metrics::noop());
552        let memory: Cache<u128, String, ModHasher> =
553            CacheBuilder::new(10).with_hash_builder(ModHasher::default()).build();
554
555        let e1 = memory.insert(1, "foo".to_string());
556        let e2 = memory.insert(1 + 1 + u64::MAX as u128, "bar".to_string());
557
558        assert_eq!(memory.hash(e1.key()), memory.hash(e2.key()));
559
560        let store = StoreBuilder::new("test", memory, metrics)
561            .with_io_engine_config(PsyncIoEngineConfig::new())
562            .with_engine_config(
563                BlockEngineConfig::new(
564                    FsDeviceBuilder::new(dir.path())
565                        .with_capacity(4 * 1024 * 1024)
566                        .build()
567                        .unwrap(),
568                )
569                .with_block_size(16 * 1024),
570            )
571            .build()
572            .await
573            .unwrap();
574
575        store.enqueue(e1.piece(), true);
576        store.enqueue(e2.piece(), true);
577        store.wait().await;
578
579        let l1 = store.load(e1.key()).await.unwrap();
580        let l2 = store.load(e2.key()).await.unwrap();
581
582        assert!(matches!(l1, Load::Miss));
583        assert!(matches!(l2, Load::Entry { .. }));
584        assert_eq!(l2.entry().unwrap().1, "bar");
585    }
586
587    #[tokio::test]
588    async fn test_store_enqueue_admission_reject_update() {
589        use crate::filter::StorageFilter;
590
591        let dir = tempfile::tempdir().unwrap();
592        let metrics = Arc::new(Metrics::noop());
593        let memory: Cache<u64, Vec<u8>> = CacheBuilder::new(10).build();
594
595        let switch = Switch::default();
596        switch.on();
597        let filter = StorageFilter::new().with_condition(switch.clone());
598
599        let store = StoreBuilder::new("test", memory.clone(), metrics)
600            .with_io_engine_config(PsyncIoEngineConfig::new())
601            .with_engine_config(
602                BlockEngineConfig::new(
603                    FsDeviceBuilder::new(dir.path())
604                        .with_capacity(4 * 1024 * 1024)
605                        .build()
606                        .unwrap(),
607                )
608                .with_block_size(16 * 1024)
609                .with_admission_filter(filter),
610            )
611            .build()
612            .await
613            .unwrap();
614
615        let e1 = memory.insert(1, b"v1".to_vec());
616        store.enqueue(e1.piece(), false);
617        store.wait().await;
618
619        let l1 = store.load(&1).await.unwrap();
620        assert!(matches!(l1, Load::Entry { ref value, .. } if value == b"v1"));
621
622        switch.off();
623
624        let e2 = memory.insert(1, b"v2".to_vec());
625        store.enqueue(e2.piece(), false);
626        store.wait().await;
627
628        let l2 = store.load(&1).await.unwrap();
629        assert!(matches!(l2, Load::Miss));
630    }
631}