Skip to main content

fxfs/object_store/
caching_object_handle.rs

1// Copyright 2021 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 crate::errors::FxfsError;
6use crate::object_handle::{ObjectHandle, ReadObjectHandle};
7use anyhow::{Error, anyhow, ensure};
8use event_listener::{Event, EventListener};
9use fuchsia_sync::Mutex;
10use std::ops::Deref;
11use std::sync::Arc;
12use storage_device::buffer::BufferFuture;
13use storage_units::BlockSize;
14
15pub const CHUNK_SIZE: BlockSize = BlockSize::SIZE_128KIB;
16
17fn block_aligned_size(source: &impl ReadObjectHandle) -> u64 {
18    source.block_size().align_up(source.get_size()).unwrap()
19}
20
21/// A reference to a chunk of data which is currently in the cache.  The data will be held in the
22/// cache as long as a reference to this chunk exists.
23#[repr(transparent)]
24#[derive(Clone)]
25pub struct CachedChunk(Arc<Box<[u8]>>);
26
27impl std::fmt::Debug for CachedChunk {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
29        f.debug_struct("CachedChunk").field("len", &self.len()).finish()
30    }
31}
32
33impl Deref for CachedChunk {
34    type Target = [u8];
35
36    fn deref(&self) -> &Self::Target {
37        &**self.0
38    }
39}
40
41#[derive(Default, Debug)]
42enum Chunk {
43    #[default]
44    Missing,
45    Pending,
46    Present(CachedChunk),
47    // The chunk has been marked for eviction, and on the next pass it will be deallocated, unless
48    // it is used before then (which returns it to Present).
49    Expired(Box<[u8]>),
50}
51
52impl Chunk {
53    // If the chunk is Present and has no remaining references, move it to Expired.
54    // If the chunk is Expired, move it to Missing, and return its data (so the caller can
55    // deallocate).
56    fn maybe_purge(&mut self) -> Option<Box<[u8]>> {
57        let this = std::mem::take(self);
58        match this {
59            Chunk::Expired(data) => Some(data),
60            Chunk::Present(chunk) => {
61                match Arc::try_unwrap(chunk.0) {
62                    Ok(data) => *self = Chunk::Expired(data),
63                    Err(chunk) => *self = Chunk::Present(CachedChunk(chunk)),
64                }
65                None
66            }
67            _ => {
68                *self = this;
69                None
70            }
71        }
72    }
73}
74
75/// A wrapper handle around a `ReadObjectHandle` which provides a memory cache for its contents.
76/// Contents are fetched as needed and can be evicted (for example due to memory pressure).
77pub struct CachingObjectHandle<S> {
78    source: S,
79
80    // Data is stored in separately accessible chunks.  These can be independently loaded and
81    // evicted.  The size of this array never changes.
82    chunks: Mutex<Vec<Chunk>>,
83
84    // Reads that are waiting on another read to load a chunk will wait on this event. There's only
85    // 1 event for all chunks so a read may receive a notification for a different chunk than the
86    // one it's waiting for and need to re-wait. It's rare for multiple chunks to be loading at once
87    // and even rarer for a read to be waiting on a chunk to be loaded while another chunk is also
88    // loading.
89    event: Event,
90}
91
92// SAFETY: Only `buffer` isn't Sync. Access to `buffer` is synchronized with `chunk_states`.
93unsafe impl<S> Sync for CachingObjectHandle<S> {}
94
95#[fxfs_trace::trace]
96impl<S: ReadObjectHandle> CachingObjectHandle<S> {
97    pub fn new(source: S) -> Self {
98        let block_size = source.block_size();
99        assert!(block_size.is_aligned(CHUNK_SIZE.get()));
100        let aligned_size = block_aligned_size(&source);
101        let chunk_count = CHUNK_SIZE.align_up_to_blocks(aligned_size) as usize;
102
103        let mut chunks = Vec::<Chunk>::new();
104        chunks.resize_with(chunk_count, Default::default);
105        Self { source, chunks: Mutex::new(chunks), event: Event::new() }
106    }
107
108    pub fn source(&self) -> &S {
109        &self.source
110    }
111
112    /// Returns a reference to the chunk (up to `CHUNK_SIZE` bytes) containing `offset`.  If the
113    /// data is already cached, this does not require reading from `source`.
114    /// `offset` must be less than the size of `source`.
115    pub async fn read(&self, offset: usize) -> Result<CachedChunk, Error> {
116        ensure!(offset < self.source.get_size() as usize, FxfsError::OutOfRange);
117        let chunk_num = (offset as u64 / CHUNK_SIZE) as usize;
118
119        enum Action {
120            Wait(EventListener),
121            Load,
122        }
123        loop {
124            let action = {
125                let mut chunks = self.chunks.lock();
126                match std::mem::take(&mut chunks[chunk_num]) {
127                    Chunk::Missing => {
128                        chunks[chunk_num] = Chunk::Pending;
129                        Action::Load
130                    }
131                    Chunk::Pending => {
132                        chunks[chunk_num] = Chunk::Pending;
133                        Action::Wait(self.event.listen())
134                    }
135                    Chunk::Present(cached_chunk) => {
136                        chunks[chunk_num] = Chunk::Present(cached_chunk.clone());
137                        return Ok(cached_chunk);
138                    }
139                    Chunk::Expired(data) => {
140                        let cached_chunk = CachedChunk(Arc::new(data));
141                        chunks[chunk_num] = Chunk::Present(cached_chunk.clone());
142                        return Ok(cached_chunk);
143                    }
144                }
145            };
146            match action {
147                Action::Wait(listener) => {
148                    listener.await;
149                }
150                Action::Load => {
151                    return self.load(chunk_num).await;
152                }
153            }
154        }
155    }
156
157    pub fn try_read(&self, offset: usize) -> Option<CachedChunk> {
158        if offset >= self.source.get_size() as usize {
159            return None;
160        }
161        let chunk_num = (offset as u64 / CHUNK_SIZE) as usize;
162        let mut chunks = self.chunks.lock();
163        match &chunks[chunk_num] {
164            Chunk::Present(cached_chunk) => Some(cached_chunk.clone()),
165            Chunk::Expired(_) => {
166                let Chunk::Expired(data) = std::mem::take(&mut chunks[chunk_num]) else {
167                    unreachable!()
168                };
169                let cached_chunk = CachedChunk(Arc::new(data));
170                chunks[chunk_num] = Chunk::Present(cached_chunk.clone());
171                Some(cached_chunk)
172            }
173            _ => None,
174        }
175    }
176
177    #[trace]
178    async fn load(&self, chunk_num: usize) -> Result<CachedChunk, Error> {
179        // If this future is dropped or reading fails then put the chunk back into the
180        // `Missing` state.
181        let drop_guard = scopeguard::guard((), |_| {
182            {
183                let mut chunks = self.chunks.lock();
184                debug_assert!(matches!(chunks[chunk_num], Chunk::Pending));
185                chunks[chunk_num] = Chunk::Missing;
186            }
187            self.event.notify(usize::MAX);
188        });
189
190        let read_start = chunk_num as u64 * CHUNK_SIZE;
191        let len =
192            (std::cmp::min(read_start + CHUNK_SIZE, self.source.get_size()) - read_start) as usize;
193        let aligned_len = (std::cmp::min(read_start + CHUNK_SIZE, block_aligned_size(&self.source))
194            - read_start) as usize;
195
196        let mut read_buf = self.source.allocate_buffer(aligned_len).await;
197        let amount_read = self.source.read_aligned(read_start, read_buf.as_mut()).await?;
198        ensure!(amount_read >= len, anyhow!(FxfsError::Internal).context("Short read"));
199
200        log::debug!("COH {}: Read {len}@{read_start}", self.source.object_id());
201
202        let data = read_buf.subslice(..len).to_vec().into_boxed_slice();
203        let cached_chunk = CachedChunk(Arc::new(data));
204
205        {
206            let mut chunks = self.chunks.lock();
207            debug_assert!(matches!(chunks[chunk_num], Chunk::Pending));
208            chunks[chunk_num] = Chunk::Present(cached_chunk.clone());
209        }
210        self.event.notify(usize::MAX);
211
212        scopeguard::ScopeGuard::into_inner(drop_guard);
213        return Ok(cached_chunk);
214    }
215
216    /// Purges unused extents, freeing unused memory.  This follows a second-chance algorithm:
217    /// unused extents will be marked for purging the first time this is called, and if they are not
218    /// used again by the next time this is called, they will be deallocated.
219    /// This is intended to be run regularly, e.g. on a timer.
220    pub fn purge(&self) {
221        // Deallocate out of the lock scope, so we don't hold up readers.
222        let mut to_deallocate = vec![];
223        let mut chunks = self.chunks.lock();
224        for chunk in chunks.iter_mut() {
225            if let Some(data) = chunk.maybe_purge() {
226                to_deallocate.push(data);
227            }
228        }
229        log::debug!(
230            "COH {}: Purging {} cached chunks ({} bytes)",
231            self.source.object_id(),
232            to_deallocate.len(),
233            to_deallocate.len() as u64 * CHUNK_SIZE
234        );
235    }
236
237    /// Immediately clears all cached chunks.
238    pub fn clear(&self) {
239        let mut to_deallocate = Vec::new();
240        {
241            let mut chunks = self.chunks.lock();
242            for chunk in chunks.iter_mut() {
243                if matches!(chunk, Chunk::Present(_) | Chunk::Expired(_)) {
244                    to_deallocate.push(std::mem::take(chunk));
245                }
246            }
247        }
248        log::debug!(
249            "COH {}: Clearing {} cached chunks ({} bytes)",
250            self.source.object_id(),
251            to_deallocate.len(),
252            to_deallocate.len() as u64 * CHUNK_SIZE
253        );
254    }
255}
256
257impl<S: ReadObjectHandle> ObjectHandle for CachingObjectHandle<S> {
258    fn set_trace(&self, v: bool) {
259        self.source.set_trace(v);
260    }
261
262    fn object_id(&self) -> u64 {
263        self.source.object_id()
264    }
265
266    fn allocate_buffer(&self, size: usize) -> BufferFuture<'_> {
267        self.source.allocate_buffer(size)
268    }
269
270    fn block_size(&self) -> BlockSize {
271        self.source.block_size()
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::{CHUNK_SIZE, CachingObjectHandle};
278    use crate::errors::FxfsError;
279    use crate::object_handle::{ObjectHandle, ReadObjectHandle};
280    use anyhow::{Error, anyhow, ensure};
281    use async_trait::async_trait;
282    use event_listener::Event;
283    use std::sync::Arc;
284    use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
285    use storage_device::Device;
286    use storage_device::buffer::{BufferFuture, MutableBufferRef};
287    use storage_device::fake_device::FakeDevice;
288    use storage_units::BlockSize;
289
290    // Fills a buffer with a pattern seeded by counter.
291    fn fill_buf(buf: &mut [u8], counter: u8) {
292        for (i, chunk) in buf.chunks_exact_mut(2).enumerate() {
293            chunk[0] = counter;
294            chunk[1] = i as u8;
295        }
296    }
297
298    // Returns a buffer filled with fill_buf.
299    fn make_buf(counter: u8, size: usize) -> Vec<u8> {
300        let mut buf = vec![0; size];
301        fill_buf(&mut buf, counter);
302        buf
303    }
304
305    struct FakeSource {
306        device: Arc<dyn Device>,
307        size: usize,
308        started: AtomicBool,
309        allow_reads: AtomicBool,
310        wake: Event,
311        counter: AtomicU8,
312    }
313
314    impl FakeSource {
315        // `device` is only used to provide allocate_buffer; reads don't go to the device.
316        fn new(device: Arc<dyn Device>, size: usize) -> Self {
317            FakeSource {
318                started: AtomicBool::new(false),
319                allow_reads: AtomicBool::new(true),
320                size,
321                wake: Event::new(),
322                device,
323                counter: AtomicU8::new(1),
324            }
325        }
326
327        fn start(&self) {
328            self.started.store(true, Ordering::SeqCst);
329            self.wake.notify(usize::MAX);
330        }
331
332        // Toggle whether reads from source are allowed.  If an uncached read occurs while this was
333        // set to false, the test panics.
334        fn allow_reads(&self, allow: bool) {
335            self.allow_reads.store(allow, Ordering::SeqCst);
336        }
337
338        async fn wait_for_start(&self) {
339            while !self.started.load(Ordering::SeqCst) {
340                let listener = self.wake.listen();
341                if self.started.load(Ordering::SeqCst) {
342                    break;
343                }
344                listener.await;
345            }
346        }
347    }
348
349    #[async_trait]
350    impl ReadObjectHandle for FakeSource {
351        async fn read_aligned(
352            &self,
353            offset: u64,
354            mut buf: MutableBufferRef<'_>,
355        ) -> Result<usize, Error> {
356            let block_size = self.block_size();
357            ensure!(block_size.is_aligned(offset), FxfsError::InvalidArgs);
358            ensure!(block_size.is_aligned(buf.len() as u64), FxfsError::InvalidArgs);
359            ensure!(self.allow_reads.load(Ordering::SeqCst), anyhow!("Received unexpected read"));
360            let counter = self.counter.fetch_add(1, Ordering::Relaxed);
361            self.wait_for_start().await;
362            let mut data = vec![0u8; buf.len()];
363            fill_buf(&mut data, counter);
364            buf.copy_from_slice(&data);
365            Ok(buf.len())
366        }
367
368        fn get_size(&self) -> u64 {
369            self.size as u64
370        }
371    }
372
373    impl ObjectHandle for FakeSource {
374        fn object_id(&self) -> u64 {
375            0
376        }
377
378        fn block_size(&self) -> BlockSize {
379            BlockSize::new(self.device.block_size()).unwrap()
380        }
381
382        fn allocate_buffer(&self, size: usize) -> BufferFuture<'_> {
383            self.device.allocate_buffer(size)
384        }
385    }
386
387    #[fuchsia::test]
388    async fn test_read_with_missing_chunk() {
389        let device = Arc::new(FakeDevice::new(1024, 512));
390        let source = FakeSource::new(device, 4096);
391        source.start();
392        let caching_object_handle = CachingObjectHandle::new(source);
393
394        let chunk = caching_object_handle.read(0).await.unwrap();
395        assert_eq!(&*chunk, make_buf(1, 4096));
396    }
397
398    #[fuchsia::test]
399    async fn test_read_with_present_chunk() {
400        let device = Arc::new(FakeDevice::new(1024, 512));
401        let source = FakeSource::new(device, 4096);
402        source.start();
403        let caching_object_handle = CachingObjectHandle::new(source);
404
405        let expected = make_buf(1, 4096);
406        let chunk = caching_object_handle.read(0).await.unwrap();
407        assert_eq!(&*chunk, expected);
408
409        // The chunk was already populated so this read receives the same value as the above read.
410        let chunk = caching_object_handle.read(0).await.unwrap();
411        assert_eq!(&*chunk, expected);
412    }
413
414    #[fuchsia::test]
415    async fn test_read_with_pending_chunk() {
416        let device = Arc::new(FakeDevice::new(1024, 512));
417        let source = FakeSource::new(device, 8192);
418        let caching_object_handle = CachingObjectHandle::new(source);
419
420        // The two reads should target the same chunk.
421        let mut read_fut1 = std::pin::pin!(caching_object_handle.read(0));
422        let mut read_fut2 = std::pin::pin!(caching_object_handle.read(4096));
423
424        // The first future will transition the chunk from `Missing` to `Pending` and then wait
425        // on the source.
426        assert!(futures::poll!(&mut read_fut1).is_pending());
427        // The second future will wait on the event.
428        assert!(futures::poll!(&mut read_fut2).is_pending());
429        caching_object_handle.source.start();
430        // Even though the source is ready the second future can't make progress.
431        assert!(futures::poll!(&mut read_fut2).is_pending());
432        // The first future reads from the source, transition the chunk from `Pending` to
433        // `Present`, and then notifies the event.
434        let expected = make_buf(1, 8192);
435        assert_eq!(&*read_fut1.await.unwrap(), expected);
436        // The event has been notified and the second future can now complete.
437        assert_eq!(&*read_fut2.await.unwrap(), expected);
438    }
439
440    #[fuchsia::test]
441    async fn test_read_with_notification_for_other_chunk() {
442        let device = Arc::new(FakeDevice::new(1024, 512));
443        let source = FakeSource::new(device, (CHUNK_SIZE + 4096) as usize);
444        let caching_object_handle = CachingObjectHandle::new(source);
445
446        let mut read_fut1 = std::pin::pin!(caching_object_handle.read(0));
447        let mut read_fut2 = std::pin::pin!(caching_object_handle.read(CHUNK_SIZE.get() as usize));
448        let mut read_fut3 = std::pin::pin!(caching_object_handle.read(0));
449
450        // The first and second futures will transition their chunks from `Missing` to `Pending`
451        // and then wait on the source.
452        assert!(futures::poll!(&mut read_fut1).is_pending());
453        assert!(futures::poll!(&mut read_fut2).is_pending());
454        // The third future will wait on the event.
455        assert!(futures::poll!(&mut read_fut3).is_pending());
456        caching_object_handle.source.start();
457        // Even though the source is ready the third future can't make progress.
458        assert!(futures::poll!(&mut read_fut3).is_pending());
459        // The second future will read from the source and notify the event.
460        assert_eq!(&*read_fut2.await.unwrap(), make_buf(2, 4096));
461        // The event was notified but the first chunk is still `Pending` so the third future
462        // resumes waiting.
463        assert!(futures::poll!(&mut read_fut3).is_pending());
464        // The first future will read from the source, transition the first chunk to `Present`,
465        // and notify the event.
466        let expected = make_buf(1, CHUNK_SIZE.get() as usize);
467        assert_eq!(&*read_fut1.await.unwrap(), expected);
468        // The first chunk is now present so the third future can complete.
469        assert_eq!(&*read_fut3.await.unwrap(), expected);
470    }
471
472    #[fuchsia::test]
473    async fn test_read_with_dropped_future() {
474        let device = Arc::new(FakeDevice::new(1024, 512));
475        let source = FakeSource::new(device, 4096);
476        let caching_object_handle = CachingObjectHandle::new(source);
477
478        let mut read_fut2 = std::pin::pin!(caching_object_handle.read(0));
479        {
480            let mut read_fut1 = std::pin::pin!(caching_object_handle.read(0));
481
482            // The first future will transition the chunk from `Missing` to `Pending` and then
483            // wait on the source.
484            assert!(futures::poll!(&mut read_fut1).is_pending());
485            // The second future will wait on the event.
486            assert!(futures::poll!(&mut read_fut2).is_pending());
487            caching_object_handle.source.start();
488            // Even though the source is ready the second future can't make progress.
489            assert!(futures::poll!(&mut read_fut2).is_pending());
490        }
491        // The first future was dropped which transitioned the chunk from `Pending` to `Missing`
492        // and notified the event. When the second future is polled it transitions the chunk
493        // from `Missing` back to `Pending`, reads from the source, and then transitions the
494        // chunk to `Present`.
495        assert_eq!(&*read_fut2.await.unwrap(), make_buf(2, 4096));
496    }
497
498    #[fuchsia::test]
499    async fn test_read_past_end_of_source() {
500        let device = Arc::new(FakeDevice::new(1024, 512));
501        let source = FakeSource::new(device, 300);
502        source.start();
503        let caching_object_handle = CachingObjectHandle::new(source);
504
505        caching_object_handle.read(500).await.expect_err("Read should fail");
506    }
507
508    #[fuchsia::test]
509    async fn test_read_to_end_of_source() {
510        let device = Arc::new(FakeDevice::new(1024, 512));
511        const SOURCE_SIZE: usize = 300;
512        let source = FakeSource::new(device, SOURCE_SIZE);
513        source.start();
514        let caching_object_handle = CachingObjectHandle::new(source);
515
516        let chunk = caching_object_handle.read(0).await.unwrap();
517        assert_eq!(&*chunk, make_buf(1, SOURCE_SIZE));
518    }
519
520    #[fuchsia::test]
521    async fn test_chunk_purging() {
522        let device = Arc::new(FakeDevice::new(1024, 512));
523        let source = Arc::new(FakeSource::new(device, (CHUNK_SIZE + 4096) as usize));
524        source.start();
525        let caching_object_handle =
526            CachingObjectHandle::new(source.clone() as Arc<dyn ReadObjectHandle>);
527
528        let _chunk1 = caching_object_handle.read(0).await.unwrap();
529        // Immediately drop the second chunk.
530        caching_object_handle.read(CHUNK_SIZE.get() as usize).await.unwrap();
531
532        source.allow_reads(false);
533
534        // The first purge should not evict the second chunk yet, and the read should save the chunk
535        // from being evicted by the next purge too.
536        caching_object_handle.purge();
537        caching_object_handle.read(0).await.unwrap();
538        caching_object_handle.read(CHUNK_SIZE.get() as usize).await.unwrap();
539
540        caching_object_handle.purge();
541        caching_object_handle.read(0).await.unwrap();
542        caching_object_handle.read(CHUNK_SIZE.get() as usize).await.unwrap();
543
544        // Purging twice should result in evicting the second chunk.
545        caching_object_handle.purge();
546        caching_object_handle.purge();
547        caching_object_handle.read(0).await.unwrap();
548        caching_object_handle
549            .read(CHUNK_SIZE.get() as usize)
550            .await
551            .expect_err("Chunk was not purged");
552    }
553
554    #[fuchsia::test]
555    async fn test_chunk_clearing() {
556        let device = Arc::new(FakeDevice::new(1024, 512));
557        let source = Arc::new(FakeSource::new(device, (CHUNK_SIZE + 4096) as usize));
558        source.start();
559        let caching_object_handle =
560            CachingObjectHandle::new(source.clone() as Arc<dyn ReadObjectHandle>);
561
562        let chunk1 = caching_object_handle.read(0).await.unwrap();
563        caching_object_handle.read(CHUNK_SIZE.get() as usize).await.unwrap();
564        assert!(caching_object_handle.try_read(0).is_some());
565        assert!(caching_object_handle.try_read(CHUNK_SIZE.get() as usize).is_some());
566
567        source.allow_reads(false);
568
569        // A single clear() should immediately evict all cached chunks from the handle.
570        caching_object_handle.clear();
571        assert!(caching_object_handle.try_read(0).is_none());
572        assert!(caching_object_handle.try_read(CHUNK_SIZE.get() as usize).is_none());
573
574        // Existing CachedChunk references remain valid until dropped.
575        assert_eq!(&*chunk1, make_buf(1, CHUNK_SIZE.get() as usize));
576
577        // Verify that calling clear() while a read is in-flight (Pending) does not break the
578        // pending read.
579        source.allow_reads(true);
580        source.started.store(false, Ordering::SeqCst);
581        let mut pending_fut = std::pin::pin!(caching_object_handle.read(0));
582        assert!(futures::poll!(&mut pending_fut).is_pending());
583        caching_object_handle.clear();
584        source.start();
585        assert_eq!(&*pending_fut.await.unwrap(), make_buf(3, CHUNK_SIZE.get() as usize));
586    }
587}