fxfs/object_store/
tree_cache.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use super::object_record::{ObjectKey, ObjectKeyData, ObjectValue};
6use crate::lsm_tree::cache::{ObjectCache, ObjectCachePlaceholder, ObjectCacheResult};
7use fuchsia_sync::Mutex;
8use linked_hash_map::{Entry, LinkedHashMap};
9use std::hash::BuildHasherDefault;
10use std::sync::atomic::{AtomicU64, Ordering};
11
12fn filter(key: &ObjectKey) -> bool {
13    match key.data {
14        // Attribute and keys could also be added here to some immediate benefit, but would be
15        // somewhat redundant with a node cache planned to be added after.
16        ObjectKeyData::Object => true,
17        ObjectKeyData::ExtendedAttribute { .. } => true,
18        _ => false,
19    }
20}
21
22// Limiting to ~100KiB of space usage. 56 bytes of linear overhead per item plus the overhead of
23// the structure. This is just used directly for now, we can parameterize it in the type if this is
24// ever desired to vary.
25const ITEM_LIMIT: usize = 1535;
26
27struct Placeholder<'a> {
28    cache: &'a TreeCache,
29    key: ObjectKey,
30    placeholder_id: u64,
31}
32
33impl Placeholder<'_> {
34    fn replace_entry(&mut self, value: Option<CacheValue>) {
35        let key = std::mem::replace(&mut self.key, ObjectKey::object(0));
36        let mut inner = self.cache.inner.lock();
37        // The value is present...
38        if let Entry::Occupied(mut entry) = inner.entry(key) {
39            // And the same placeholder as the token has...
40            let is_current = match entry.get() {
41                CacheValue::Placeholder(placeholder_id) => &self.placeholder_id == placeholder_id,
42                _ => false,
43            };
44            if is_current {
45                match value {
46                    Some(v) => *(entry.get_mut()) = v,
47                    None => {
48                        entry.remove();
49                    }
50                }
51            }
52        }
53    }
54}
55
56impl Drop for Placeholder<'_> {
57    fn drop(&mut self) {
58        self.replace_entry(None);
59    }
60}
61
62impl<'a> ObjectCachePlaceholder<ObjectValue> for Placeholder<'a> {
63    fn complete(mut self: Box<Self>, value: Option<&ObjectValue>) {
64        let entry_value = match value {
65            value @ Some(ObjectValue::Object { .. }) => value.cloned(),
66            value @ Some(ObjectValue::ExtendedAttribute(_)) => value.cloned(),
67            _ => None,
68        }
69        .map(|v| CacheValue::Value(v));
70        self.replace_entry(entry_value);
71    }
72}
73
74enum CacheValue {
75    Placeholder(u64),
76    Value(ObjectValue),
77}
78
79/// Supports caching for Objects directly for now. Speeds up stat calls.
80pub struct TreeCache {
81    inner: Mutex<LinkedHashMap<ObjectKey, CacheValue, BuildHasherDefault<rustc_hash::FxHasher>>>,
82    placeholder_counter: AtomicU64,
83}
84
85impl TreeCache {
86    pub fn new() -> Self {
87        Self {
88            inner: Mutex::new(LinkedHashMap::with_capacity_and_hasher(
89                ITEM_LIMIT + 1,
90                BuildHasherDefault::<rustc_hash::FxHasher>::default(),
91            )),
92            placeholder_counter: AtomicU64::new(0),
93        }
94    }
95}
96
97impl ObjectCache<ObjectKey, ObjectValue> for TreeCache {
98    fn lookup_or_reserve(&self, key: &ObjectKey) -> ObjectCacheResult<'_, ObjectValue> {
99        if !filter(key) {
100            return ObjectCacheResult::NoCache;
101        }
102        let mut inner = self.inner.lock();
103        match inner.get_refresh(key) {
104            Some(CacheValue::Value(entry)) => ObjectCacheResult::Value(entry.clone()),
105            Some(CacheValue::Placeholder(_)) => ObjectCacheResult::NoCache,
106            _ => {
107                let placeholder_id = self.placeholder_counter.fetch_add(1, Ordering::Relaxed);
108                inner.insert(key.clone(), CacheValue::Placeholder(placeholder_id));
109                if inner.len() > ITEM_LIMIT {
110                    let _ = inner.pop_front();
111                }
112                ObjectCacheResult::Placeholder(Box::new(Placeholder {
113                    cache: self,
114                    key: key.clone(),
115                    placeholder_id,
116                }))
117            }
118        }
119    }
120
121    fn invalidate(&self, key: ObjectKey, value: Option<ObjectValue>) {
122        if !filter(&key) {
123            return;
124        }
125        let mut inner = self.inner.lock();
126        if let Entry::Occupied(mut entry) = inner.entry(key) {
127            if let Some(replacement) = value {
128                *(entry.get_mut()) = CacheValue::Value(replacement);
129            } else {
130                entry.remove();
131            }
132        }
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::super::object_record::{ObjectKey, ObjectValue, Timestamp};
139    use super::{TreeCache, ITEM_LIMIT};
140    use crate::lsm_tree::cache::{ObjectCache, ObjectCacheResult};
141    use assert_matches::assert_matches;
142
143    #[fuchsia::test]
144    async fn test_basic_operations() {
145        let cache = TreeCache::new();
146        let key = ObjectKey::object(1);
147        let now = Timestamp::now();
148        let value = ObjectValue::file(1, 0, now, now, now, now, 0, None);
149
150        let placeholder = match cache.lookup_or_reserve(&key) {
151            ObjectCacheResult::Placeholder(placeholder) => placeholder,
152            _ => panic!("Expected cache miss with placeholder returned."),
153        };
154        placeholder.complete(Some(&value));
155
156        let result = match cache.lookup_or_reserve(&key) {
157            ObjectCacheResult::Value(value) => value,
158            _ => panic!("Expected to find item."),
159        };
160        assert_eq!(&result, &value);
161
162        cache.invalidate(key.clone(), None);
163
164        match cache.lookup_or_reserve(&key) {
165            ObjectCacheResult::Placeholder(placeholder) => placeholder.complete(None),
166            _ => panic!("Expected cache miss with placeholder returned."),
167        };
168    }
169
170    #[fuchsia::test]
171    async fn test_enforce_limits() {
172        let cache = TreeCache::new();
173        let now = Timestamp::now();
174
175        for i in 1..(ITEM_LIMIT as u64 + 2) {
176            let key = ObjectKey::object(i);
177            let value = ObjectValue::file(1, 0, now, now, now, now, 0, None);
178            let placeholder = match cache.lookup_or_reserve(&key) {
179                ObjectCacheResult::Placeholder(placeholder) => placeholder,
180                _ => panic!("Expected cache miss with placeholder returned."),
181            };
182            placeholder.complete(Some(&value));
183        }
184
185        // Item 1 should be evicted.
186        assert_matches!(
187            cache.lookup_or_reserve(&ObjectKey::object(1)),
188            ObjectCacheResult::Placeholder(_)
189        );
190
191        // And item 2 has been evicted by the lookup of item 1.
192        for i in 3..(ITEM_LIMIT as u64 + 2) {
193            let key = ObjectKey::object(i);
194            assert_matches!(cache.lookup_or_reserve(&key), ObjectCacheResult::Value(_));
195        }
196    }
197
198    #[fuchsia::test]
199    async fn test_invalidate_inserts() {
200        let cache = TreeCache::new();
201        let key = ObjectKey::object(1);
202        let now = Timestamp::now();
203        let value1 = ObjectValue::file(1, 0, now, now, now, now, 0, None);
204        let value2 = ObjectValue::file(2, 0, now, now, now, now, 0, None);
205
206        let placeholder = match cache.lookup_or_reserve(&key) {
207            ObjectCacheResult::Placeholder(placeholder) => placeholder,
208            _ => panic!("Expected cache miss with placeholder returned."),
209        };
210        placeholder.complete(Some(&value1));
211
212        let result = match cache.lookup_or_reserve(&key) {
213            ObjectCacheResult::Value(value) => value,
214            _ => panic!("Expected to find item."),
215        };
216        assert_eq!(&result, &value1);
217
218        cache.invalidate(key.clone(), Some(value2.clone()));
219
220        let result = match cache.lookup_or_reserve(&key) {
221            ObjectCacheResult::Value(value) => value,
222            _ => panic!("Expected to find item."),
223        };
224        assert_eq!(&result, &value2);
225    }
226
227    #[fuchsia::test]
228    async fn test_no_caching_for_filtered_item() {
229        let cache = TreeCache::new();
230        let key = ObjectKey::extent(1, 1, 1..2);
231
232        assert!(matches!(cache.lookup_or_reserve(&key), ObjectCacheResult::NoCache));
233    }
234
235    // Two clients looking for the same key don't interfere with each other. Prevents priority
236    // inversion.
237    #[fuchsia::test]
238    async fn test_two_parallel_clients() {
239        let cache = TreeCache::new();
240        let key = ObjectKey::object(1);
241        let now = Timestamp::now();
242        let value1 = ObjectValue::file(1, 0, now, now, now, now, 0, None);
243        let value2 = ObjectValue::file(2, 0, now, now, now, now, 0, None);
244
245        let placeholder1 = match cache.lookup_or_reserve(&key) {
246            ObjectCacheResult::Placeholder(placeholder) => placeholder,
247            _ => panic!("Expected cache miss with placeholder returned."),
248        };
249
250        // Another search should not get a placeholder, as one is already held.
251        assert!(matches!(cache.lookup_or_reserve(&key), ObjectCacheResult::NoCache));
252
253        // Invalidate the current placeholder.
254        cache.invalidate(key.clone(), None);
255
256        // Get a new placeholder
257        let placeholder2 = match cache.lookup_or_reserve(&key) {
258            ObjectCacheResult::Placeholder(placeholder) => placeholder,
259            _ => panic!("Expected cache miss with placeholder returned."),
260        };
261
262        // Complete them out of order.
263        placeholder2.complete(Some(&value2));
264        placeholder1.complete(Some(&value1));
265
266        // Result should be from the second placeholder, as the first was invalidated.
267        let result = match cache.lookup_or_reserve(&key) {
268            ObjectCacheResult::Value(value) => value,
269            _ => panic!("Expected to find item."),
270        };
271        assert_eq!(&result, &value2);
272    }
273}