Skip to main content

lsm_tree/
descriptor_table.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025-present, fjall-rs
3// Copyright (c) 2026-present, Dmitry Prudnikov
4
5use crate::sharded_cache::{ShardedCache, UnitWeighter};
6use alloc::sync::Arc;
7
8use crate::{GlobalTableId, fs::FsFile};
9
10const TAG_BLOCK: u8 = 0;
11const TAG_BLOB: u8 = 1;
12
13type Item = Arc<dyn FsFile>;
14
15#[derive(Clone, Copy, Eq, core::hash::Hash, PartialEq)]
16struct CacheKey(u8, u64, u64);
17
18/// Number of shards in the FD cache. Smaller than the block cache: the FD cache
19/// holds at most a few thousand descriptors, so a large shard array would leave
20/// most shards empty.
21const FD_CACHE_SHARDS: usize = 16;
22
23/// Caches file descriptors to tables and blob files
24pub struct DescriptorTable {
25    inner: ShardedCache<CacheKey, Item, UnitWeighter, rustc_hash::FxBuildHasher>,
26    /// Advanced after every table descriptor removed: a scan reading through
27    /// a descriptor it resolved under an earlier epoch takes it afresh, so a
28    /// descriptor removed because its path now names another inode (an
29    /// in-place heal's detached copy) is not read through again.
30    table_epoch: portable_atomic::AtomicU64,
31}
32
33impl DescriptorTable {
34    #[must_use]
35    pub fn new(capacity: usize) -> Self {
36        // Unit weight → the byte capacity is a max entry count. `est_items`
37        // seeds the ghost-queue sizing; the descriptor count is the natural
38        // estimate.
39        let inner = ShardedCache::with_weighter(
40            capacity as u64,
41            FD_CACHE_SHARDS,
42            capacity,
43            UnitWeighter,
44            rustc_hash::FxBuildHasher,
45        );
46
47        Self {
48            inner,
49            table_epoch: portable_atomic::AtomicU64::new(0),
50        }
51    }
52
53    /// The table descriptor epoch, see [`Self::remove_for_table`].
54    pub(crate) fn table_epoch(&self) -> u64 {
55        self.table_epoch.load(portable_atomic::Ordering::Acquire)
56    }
57
58    /// Advances the table descriptor epoch after a removal. Release, so a
59    /// read that sees the new epoch also sees the entry gone and takes the
60    /// descriptor the path names now.
61    fn advance_table_epoch(&self) {
62        self.table_epoch
63            .fetch_add(1, portable_atomic::Ordering::Release);
64    }
65
66    pub(crate) fn len(&self) -> usize {
67        self.inner.len()
68    }
69
70    /// The most descriptors the cache keeps open.
71    pub(crate) fn capacity(&self) -> u64 {
72        self.inner.capacity()
73    }
74
75    /// Closes the cached descriptors of every tree whose id is `first` or
76    /// later, tables and blob files alike.
77    pub(crate) fn remove_trees_from(&self, first: crate::tree::inner::TreeId) {
78        self.inner.remove_where(|key| key.1 >= first);
79        self.advance_table_epoch();
80    }
81
82    #[must_use]
83    pub fn access_for_table(&self, id: &GlobalTableId) -> Option<Arc<dyn FsFile>> {
84        let key = CacheKey(TAG_BLOCK, id.tree_id(), id.table_id());
85        self.inner.get(&key)
86    }
87
88    /// Like [`Self::access_for_table`], but without promoting the entry, for
89    /// a read that must leave the table as it found it.
90    #[must_use]
91    pub(crate) fn peek_for_table(&self, id: &GlobalTableId) -> Option<Arc<dyn FsFile>> {
92        let key = CacheKey(TAG_BLOCK, id.tree_id(), id.table_id());
93        self.inner.peek(&key)
94    }
95
96    pub fn insert_for_table(&self, id: GlobalTableId, item: Item) {
97        let key = CacheKey(TAG_BLOCK, id.tree_id(), id.table_id());
98        self.inner.insert(key, item);
99    }
100
101    #[must_use]
102    pub fn access_for_blob_file(&self, id: &GlobalTableId) -> Option<Arc<dyn FsFile>> {
103        let key = CacheKey(TAG_BLOB, id.tree_id(), id.table_id());
104        self.inner.get(&key)
105    }
106
107    pub fn insert_for_blob_file(&self, id: GlobalTableId, item: Item) {
108        let key = CacheKey(TAG_BLOB, id.tree_id(), id.table_id());
109        self.inner.insert(key, item);
110    }
111
112    /// Closes the table's cached descriptor and advances the table descriptor
113    /// epoch, so no scan reads through that descriptor again.
114    pub fn remove_for_table(&self, id: &GlobalTableId) {
115        let key = CacheKey(TAG_BLOCK, id.tree_id(), id.table_id());
116        self.inner.remove(&key);
117        self.advance_table_epoch();
118    }
119
120    pub fn remove_for_blob_file(&self, id: &GlobalTableId) {
121        let key = CacheKey(TAG_BLOB, id.tree_id(), id.table_id());
122        self.inner.remove(&key);
123    }
124}