Skip to main content

file_backed/
lib.rs

1use std::{
2    ops::{Deref, DerefMut},
3    sync::Arc,
4};
5
6use consume_on_drop::{Consume, ConsumeOnDrop};
7use cutoff_list::CutoffList;
8use parking_lot::RwLock;
9use tokio::{
10    sync::{RwLockMappedWriteGuard, RwLockReadGuard},
11    task::JoinHandle,
12};
13
14use uuid::Uuid;
15
16mod entries;
17
18pub mod backing_store;
19pub mod convenience;
20#[cfg(feature = "fbstore")]
21pub mod fbstore;
22#[cfg(feature = "redbstore")]
23pub mod redbstore;
24
25use self::backing_store::{Strategy, TrackedPath};
26use self::entries::{FullEntry, LimitedEntry};
27
28pub use self::backing_store::{BackingStore, BackingStoreT};
29
30/// A handle to data managed by an `FBPool`.
31///
32/// This acts like a `Box` but for potentially large data (`T`) that might
33/// reside either in memory (in an LRU cache) or on disk (in the backing store) or
34/// both.
35/// Accessing the underlying data requires calling one of the `load` methods.
36///
37/// When dropped:
38/// - If the item was never persisted or written to the backing store's temporary
39///   location, it is simply dropped.
40/// - If the item exists in the backing store's temporary location, a background
41///   task is spawned via the `BackingStore` to delete it using `BackingStoreT::delete`.
42// Field ordering is important to remove entries from the cutoff list when
43// the last reference to the entry is dropped
44pub struct Fb<T, B: BackingStoreT> {
45    entry: FullEntry<T, B>,
46    inner: FbInner<T, B>,
47}
48
49struct FbInner<T, B: BackingStoreT> {
50    index: cutoff_list::Index,
51    pool: Arc<FBPool<T, B>>,
52}
53
54impl<T, B: BackingStoreT> Drop for FbInner<T, B> {
55    fn drop(&mut self) {
56        let mut write_guard = self.pool.entries.write();
57        write_guard.remove(self.index).unwrap();
58    }
59}
60
61/// Manages a pool of `Fb` instances, backed by an in-memory cache
62/// and a `BackingStore` for disk persistence.
63///
64/// ## Caching Strategy (Segmented LRU Variant)
65///
66/// The pool utilizes a variation of an LRU (Least Recently Used) cache designed
67/// to reduce overhead for frequently accessed items. The cache is conceptually
68/// divided into two segments (e.g., a "front half" and a "back half", typically
69/// split evenly based on the total `mem_size`).
70///
71/// - **Promotion:** Items loaded from the backing store (`Strategy::load`) or
72///   accessed while residing in the "back half" of the cache are promoted
73///   to the most recently used position (the front of the "front half").
74/// - **No Movement:** Items accessed while *already* in the "front half" do
75///   **not** change position. This avoids the overhead of cache entry shuffling
76///   for frequently hit "hot" items that are already near the front.
77/// - **Insertion:** New items inserted via [`FBPool::insert`] also enter the
78///   front of the "front half".
79/// - **Eviction:** Items are eventually evicted from the least recently used
80///   end of the "back half" when the cache is full.
81///
82/// This approach aims to provide LRU-like behavior while optimizing for workloads
83/// where a subset of items is accessed very frequently.
84///
85/// Internally, we store each item in an `Option<T>`. When an item is evicted from cache,
86/// we will replace `Some(val)` with None. Note that the size of `None::<T>`` on the
87/// stack is the exact same as that of Some(val). What this means is that if T's resources
88/// are primarily represented by its space on the stack (e.g. when `T` is `[f32; 4096]`),
89/// there will be zero savings when it's removed from cache. In these cases, you should
90/// use `Box<T>` instead.
91pub struct FBPool<T, B: BackingStoreT> {
92    entries: RwLock<CutoffList<LimitedEntry<T, B>>>,
93    store: Arc<BackingStore<B>>,
94}
95
96impl<T, B: BackingStoreT> FBPool<T, B> {
97    /// Creates a new pool managing items of type `T`.
98    ///
99    /// # Arguments
100    /// * `store` - The configured `BackingStore` manager.
101    /// * `mem_size` - The maximum number of items to keep loaded in the in-memory cache.
102    ///   This size is divided internally (50/50) to implement the
103    ///   two-segment caching strategy (see main [`FBPool`] documentation for details).
104    pub fn new(store: Arc<BackingStore<B>>, mem_size: usize) -> Self {
105        let entries = RwLock::new(CutoffList::new(vec![mem_size / 2, mem_size]));
106        Self { entries, store }
107    }
108
109    /// Returns a reference to the underlying `BackingStore`.
110    pub fn store(&self) -> &Arc<BackingStore<B>> {
111        &self.store
112    }
113
114    /// Inserts new `data` into the pool, returning an `Fb` handle.
115    ///
116    /// The data is initially placed only in the in-memory LRU cache. It will only be
117    /// written to the backing store's temporary location if it's evicted from the cache
118    /// or explicitly written via`persist`/`blocking_persist`/`spawn_write_now`/`blocking_write_now`.
119    ///
120    /// Whenever the data is evicted from memory, after being written to the backing store
121    /// with `B::store`, the data will be dropped normally, which means if there's a custom `Drop`
122    /// implementation, it will be called. Each time the data is loaded back into memory, this
123    /// could happen again if the data is evicted again.
124    pub fn insert(self: &Arc<Self>, data: T) -> Fb<T, B>
125    where
126        T: Send + Sync + 'static,
127        B: Strategy<T>,
128    {
129        let entry = FullEntry::new(data);
130        let mut guard = self.entries.write();
131        let index = guard.insert_first(entry.limited());
132        let dump_entry = guard.get(guard.index_following_qth_cutoff(1));
133        if let Some(entry) = dump_entry {
134            entry.try_dump_to_disk(&self.store);
135        }
136        drop(guard);
137        Fb {
138            entry,
139            inner: FbInner {
140                index,
141                pool: Arc::clone(self),
142            },
143        }
144    }
145
146    /// Asynchronously registers an existing item from a persistent path into the pool.
147    ///
148    /// Creates an `Fb` handle for an item identified by `key` located at the
149    /// tracked persistent `path`. This typically involves calling `BackingStoreT::register`
150    /// (e.g., hard-linking the file into the managed temporary area).
151    ///
152    /// The item data is *not* loaded into memory by this call.
153    /// Returns `None` if the registration fails (e.g., the underlying store fails to find the key).
154    pub async fn register(
155        self: &Arc<Self>,
156        path: &Arc<TrackedPath<B::PersistPath>>,
157        key: Uuid,
158    ) -> Option<Fb<T, B>>
159    where
160        T: Send + Sync + 'static,
161    {
162        let entry = FullEntry::register(key, &self.store, path).await?;
163        let index = self.entries.write().insert_last(entry.limited());
164        Some(Fb {
165            entry,
166            inner: FbInner {
167                index,
168                pool: Arc::clone(self),
169            },
170        })
171    }
172
173    /// Blocking version of `register`. Waits for the registration to complete.
174    /// Must not be called from an async context that isn't allowed to block.
175    pub fn blocking_register(
176        self: &Arc<Self>,
177        path: &TrackedPath<B::PersistPath>,
178        key: Uuid,
179    ) -> Option<Fb<T, B>> {
180        let entry = FullEntry::blocking_register(key, &self.store, path)?;
181        let index = self.entries.write().insert_last(entry.limited());
182        Some(Fb {
183            entry,
184            inner: FbInner {
185                index,
186                pool: Arc::clone(self),
187            },
188        })
189    }
190
191    /// Returns the current number of items managed by the pool (both in memory and on disk).
192    pub fn size(&self) -> usize {
193        self.entries.read().len()
194    }
195}
196
197impl<T, B: BackingStoreT> Fb<T, B> {
198    /// Returns the unique identifier (`Uuid`) for the data associated with this handle.
199    /// This key will change if the data is mutated via `try_load_mut` or `make_mut`.
200    pub fn key(&self) -> Uuid {
201        self.entry.key()
202    }
203
204    /// Returns a reference to the `FBPool` this `Fb` belongs to.
205    pub fn pool(&self) -> &Arc<FBPool<T, B>> {
206        &self.inner.pool
207    }
208}
209
210impl<T: Send + Sync + 'static, B: Strategy<T>> Fb<T, B> {
211    /// Asynchronously loads the data and returns a read guard.
212    ///
213    /// Returns a `Future` that resolves to a `ReadGuard` once the data is available
214    /// in memory (either immediately or after loading from the backing store).
215    /// Suitable for use within `async` functions and tasks.
216    pub async fn load(&self) -> ReadGuard<'_, T, B> {
217        // We do this _before_ loading the backing value so that if the caller
218        // cancels the operation, we don't waste the work done to load it by
219        // immediately dumping it back to disk.
220        shift_forward(&self.inner.pool, self.inner.index);
221        // Construct before loading so that if cancelled, the object will be dumped
222        // if necessary (possible if a lot of things are loaded simultaneously).
223        let on_drop = GuardDropper::new(&self.inner.pool, self.inner.index);
224        let data_guard = self.entry.load(&self.inner.pool.store).await;
225        ReadGuard {
226            data_guard,
227            _on_drop: on_drop,
228        }
229    }
230
231    /// Attempts to load the data and return a read guard, returning None if the data is not
232    /// already in memory or is currently being evicted.
233    /// The entry will only be potentially shifted in the LRU cache on success.
234    pub fn try_load(&self) -> Option<ReadGuard<'_, T, B>> {
235        let guard = self.entry.try_load()?;
236        shift_forward(&self.inner.pool, self.inner.index);
237        let on_drop = GuardDropper::new(&self.inner.pool, self.inner.index);
238        Some(ReadGuard {
239            data_guard: guard,
240            _on_drop: on_drop,
241        })
242    }
243
244    /// Loads the data and returns a read guard, performing blocking I/O if necessary.
245    ///
246    /// - If the data is already in the memory cache, returns immediately.
247    /// - If the data is not in memory, it performs a blocking load operation via
248    ///   `Strategy::load`.
249    ///
250    /// This method should only be called from a context where blocking is acceptable
251    /// (e.g., outside a Tokio runtime, or within `spawn_blocking` or `block_in_place`).
252    pub fn blocking_load(&self) -> ReadGuard<'_, T, B> {
253        shift_forward(&self.inner.pool, self.inner.index);
254        let on_drop = GuardDropper::new(&self.inner.pool, self.inner.index);
255        let data_guard = self.entry.blocking_load(&self.inner.pool.store);
256        ReadGuard {
257            data_guard,
258            _on_drop: on_drop,
259        }
260    }
261
262    /// Loads the data and returns a read guard for immutable access.
263    ///
264    /// - If the data is already in the memory cache, returns immediately.
265    /// - If the data is not in memory, it uses `tokio::task::block_in_place` to
266    ///   call `blocking_load` to load it from the backing store.
267    ///
268    /// This is for the somewhat niche situation where you need to load an FBArc in a
269    /// blocking function nested many blocking calls deep within an async task running
270    /// on a tokio multithreaded runtime. Ideally you would propagate async down and use
271    /// `load` instead.
272    ///
273    /// # Panics
274    /// This method will panic if called from within a `tokio::runtime::Runtime`
275    /// created using `Runtime::new_current_thread`, as `block_in_place` is not
276    /// supported there. Use `load` instead in async contexts and
277    /// `blocking_load` in known blocking contexts.
278    pub fn load_in_place(&self) -> ReadGuard<'_, T, B> {
279        shift_forward(&self.inner.pool, self.inner.index);
280        let on_drop = GuardDropper::new(&self.inner.pool, self.inner.index);
281        let data_guard = self.entry.load_in_place(&self.inner.pool.store);
282        ReadGuard {
283            data_guard,
284            _on_drop: on_drop,
285        }
286    }
287
288    /// Asynchronously acquires mutable access to the data.
289    ///
290    /// On return:
291    /// 1. The data is ensured to be in memory.
292    /// 2. The corresponding file in the backing store's temporary location (if any) is deleted.
293    /// 3. The internal `Uuid` key for this data is changed.
294    /// 4. A `WriteGuard` providing mutable access is returned.
295    pub async fn load_mut(&mut self) -> WriteGuard<'_, T, B> {
296        // We do this _before_ loading the backing value so that if the caller
297        // cancels the operation, we don't waste the work done to load it by
298        // immediately dumping it back to disk.
299        shift_forward(&self.inner.pool, self.inner.index);
300        // Construct before loading so that if cancelled, the object will be dumped
301        // if necessary (possible if a lot of things are loaded simultaneously).
302        let on_drop = GuardDropper::new(&self.inner.pool, self.inner.index);
303        let data_guard = self.entry.load_mut(&self.inner.pool.store).await;
304        WriteGuard {
305            data_guard,
306            _on_drop: on_drop,
307        }
308    }
309
310    /// Attempts to load the data and return a write guard, returning None if the data is not
311    /// already in memory or is currently being evicted.
312    /// The entry will only be potentially shifted in the LRU cache on success.
313    pub fn try_load_mut(&mut self) -> Option<WriteGuard<'_, T, B>> {
314        let guard = self.entry.try_load_mut()?;
315        shift_forward(&self.inner.pool, self.inner.index);
316        let on_drop = GuardDropper::new(&self.inner.pool, self.inner.index);
317        Some(WriteGuard {
318            data_guard: guard,
319            _on_drop: on_drop,
320        })
321    }
322
323    /// Blocking version of `load_mut`. Waits for the operation to complete.
324    /// Must not be called from an async context that isn't allowed to block.
325    pub fn blocking_load_mut(&mut self) -> WriteGuard<'_, T, B> {
326        shift_forward(&self.inner.pool, self.inner.index);
327        let on_drop = GuardDropper::new(&self.inner.pool, self.inner.index);
328        let data_guard = self.entry.blocking_load_mut(&self.inner.pool.store);
329        WriteGuard {
330            data_guard,
331            _on_drop: on_drop,
332        }
333    }
334
335    /// Spawns a background task to immediately write the data to the backing store's
336    /// temporary location if it isn't already there.
337    ///
338    /// Acquires the read guard and then returns a `JoinHandle` that completes when the write
339    /// operation finishes.
340    pub async fn spawn_write_now(&self) -> JoinHandle<()> {
341        self.entry.spawn_write_now(&self.inner.pool.store).await
342    }
343
344    /// Performs a blocking write of the data to the backing store's temporary location
345    /// if it is isn't already there. Waits for the write to complete.
346    /// Must not be called from an async context that isn't allowed to block.
347    pub fn blocking_write_now(&self) {
348        self.entry.blocking_write_now(&self.inner.pool.store);
349    }
350
351    /// Spawns a background task to persist the data to the specified `TrackedPath`.
352    ///
353    /// This calls `BackingStoreT::persist` (typically a hard-link). If the data
354    /// is currently only in memory, it ensures it's written to the temporary
355    /// location first before attempting persistence.
356    /// If the data is already in the persistent location, this is a no-op.
357    ///
358    /// Acquires the read guard and then returns a `JoinHandle` that completes when the persistence
359    /// operation finishes.
360    pub async fn spawn_persist(&self, path: &Arc<TrackedPath<B::PersistPath>>) -> JoinHandle<()> {
361        self.entry.spawn_persist(&self.inner.pool.store, path).await
362    }
363
364    /// Performs a blocking persistence of the data to the specified `TrackedPath`.
365    /// Waits for the operation (including any preliminary writes) to complete.
366    /// Must not be called from an async context that isn't allowed to block.
367    pub fn blocking_persist(&self, path: &TrackedPath<B::PersistPath>) {
368        self.entry.blocking_persist(&self.inner.pool.store, path)
369    }
370}
371
372fn shift_forward<T: Send + Sync + 'static, B: Strategy<T>>(
373    pool: &FBPool<T, B>,
374    index: cutoff_list::Index,
375) {
376    let read_guard = pool.entries.read();
377    let preceding_cutoffs = read_guard.preceding_cutoffs(index).unwrap();
378    if preceding_cutoffs == 0 {
379        return;
380    }
381    drop(read_guard);
382    let mut write_guard = pool.entries.write();
383    let preceding_cutoffs = write_guard.preceding_cutoffs(index).unwrap();
384    if preceding_cutoffs == 0 {
385        return;
386    }
387    write_guard.shift_to_front(index);
388    if preceding_cutoffs == 1 {
389        return;
390    }
391    assert!(preceding_cutoffs == 2);
392    let read_guard = parking_lot::RwLockWriteGuard::downgrade(write_guard);
393    let dump_entry = read_guard
394        .get(read_guard.index_following_qth_cutoff(1))
395        .unwrap();
396    dump_entry.try_dump_to_disk(&pool.store);
397}
398
399/// An RAII guard providing immutable access (`Deref`) to the underlying data `T`.
400///
401/// While this guard is alive, the data is guaranteed to remain loaded in memory
402/// and will not be immediately evicted if it leaves the LRU cache.
403// Field ordering is important for try_dump_to_disk to succeed on drop
404pub struct ReadGuard<'a, T: Send + Sync + 'static, B: Strategy<T>> {
405    data_guard: RwLockReadGuard<'a, T>,
406    _on_drop: ConsumeOnDrop<GuardDropper<'a, T, B>>,
407}
408
409impl<T: Send + Sync + 'static, B: Strategy<T>> Deref for ReadGuard<'_, T, B> {
410    type Target = T;
411
412    /// Dereferences to the immutable underlying data `T`.
413    fn deref(&self) -> &Self::Target {
414        &self.data_guard
415    }
416}
417
418/// An RAII guard providing mutable access (`DerefMut`) to the underlying data `T`.
419///
420/// While this guard is alive, the data is guaranteed to remain loaded in memory.
421// Field ordering is important for try_dump_to_disk to succeed on drop
422pub struct WriteGuard<'a, T: Send + Sync + 'static, B: Strategy<T>> {
423    data_guard: RwLockMappedWriteGuard<'a, T>,
424    _on_drop: ConsumeOnDrop<GuardDropper<'a, T, B>>,
425}
426
427impl<T: Send + Sync + 'static, B: Strategy<T>> Deref for WriteGuard<'_, T, B> {
428    type Target = T;
429
430    fn deref(&self) -> &Self::Target {
431        &self.data_guard
432    }
433}
434
435impl<T: Send + Sync + 'static, B: Strategy<T>> DerefMut for WriteGuard<'_, T, B> {
436    fn deref_mut(&mut self) -> &mut Self::Target {
437        &mut self.data_guard
438    }
439}
440
441struct GuardDropper<'a, T: Send + Sync + 'static, B: Strategy<T>> {
442    pool: &'a FBPool<T, B>,
443    index: cutoff_list::Index,
444}
445
446impl<T: Send + Sync + 'static, B: Strategy<T>> Consume for GuardDropper<'_, T, B> {
447    fn consume(self) {
448        let entry_guard = self.pool.entries.read();
449        let preceding_cutoffs = entry_guard.preceding_cutoffs(self.index).unwrap();
450        assert!(preceding_cutoffs <= 2);
451        if preceding_cutoffs == 2 {
452            entry_guard
453                .get(self.index)
454                .unwrap()
455                .try_dump_to_disk(&self.pool.store);
456        }
457    }
458}
459
460impl<'a, T: Send + Sync + 'static, B: Strategy<T>> GuardDropper<'a, T, B> {
461    pub fn new(pool: &'a FBPool<T, B>, index: cutoff_list::Index) -> ConsumeOnDrop<Self> {
462        ConsumeOnDrop::new(GuardDropper { pool, index })
463    }
464}