Skip to main content

imago/qcow2/
cache.rs

1//! Provides functionality for the L2 and refblock caches.
2
3use super::*;
4use crate::async_lru_cache::AsyncLruCacheBackend;
5use tracing::trace;
6
7/// I/O back-end for the L2 table cache.
8struct L2CacheBackend<S: Storage> {
9    /// Qcow2 metadata file.
10    file: Arc<S>,
11
12    /// Qcow2 header.
13    header: Arc<Header>,
14}
15
16/// I/O back-end for the refblock cache.
17struct RefBlockCacheBackend<S: Storage> {
18    /// Qcow2 metadata file.
19    file: Arc<S>,
20
21    /// Qcow2 header.
22    header: Arc<Header>,
23}
24
25impl<S: Storage> L2CacheBackend<S> {
26    /// Create a new `L2CacheBackend`.
27    ///
28    /// `file` is the qcow2 metadata (image) file.
29    pub fn new(file: Arc<S>, header: Arc<Header>) -> Self {
30        L2CacheBackend { file, header }
31    }
32}
33
34#[maybe_async(AFIT)]
35impl<S: Storage> AsyncLruCacheBackend for L2CacheBackend<S> {
36    type Key = HostCluster;
37    type Value = L2Table;
38
39    async fn load(&self, l2_cluster: HostCluster) -> io::Result<L2Table> {
40        trace!("Loading L2 table");
41
42        L2Table::load(
43            self.file.as_ref(),
44            &self.header,
45            l2_cluster,
46            self.header.l2_entries(),
47        )
48        .await
49    }
50
51    async fn flush(&self, l2_cluster: HostCluster, l2_table: &L2Table) -> io::Result<()> {
52        trace!("Flushing L2 table");
53        l2_table
54            .write_if_modified(self.file.as_ref(), l2_cluster)
55            .await
56    }
57
58    unsafe fn evict(&self, _l2_cluster: HostCluster, l2_table: L2Table) {
59        trace!(
60            "Evicting L2 table {}",
61            l2_table.get_offset().unwrap_or(HostOffset(0))
62        );
63        l2_table.clear_modified();
64    }
65}
66
67impl<S: Storage> RefBlockCacheBackend<S> {
68    /// Create a new `RefBlockCacheBackend`.
69    ///
70    /// `file` is the qcow2 metadata (image) file.
71    pub fn new(file: Arc<S>, header: Arc<Header>) -> Self {
72        RefBlockCacheBackend { file, header }
73    }
74}
75
76#[maybe_async(AFIT)]
77impl<S: Storage> AsyncLruCacheBackend for RefBlockCacheBackend<S> {
78    type Key = HostCluster;
79    type Value = RefBlock;
80
81    async fn load(&self, rb_cluster: HostCluster) -> io::Result<RefBlock> {
82        RefBlock::load(self.file.as_ref(), &self.header, rb_cluster).await
83    }
84
85    async fn flush(&self, rb_cluster: HostCluster, refblock: &RefBlock) -> io::Result<()> {
86        refblock
87            .write_if_modified(self.file.as_ref(), rb_cluster)
88            .await
89    }
90
91    unsafe fn evict(&self, _rb_cluster: HostCluster, refblock: RefBlock) {
92        refblock.clear_modified();
93    }
94}
95
96/// Current flush dependency direction between L2 and refblock caches
97#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
98enum CacheDependency {
99    /// No dependency set yet
100    #[default]
101    None,
102    /// Write refblock cache before writing anything from the L2 cache (during cluster allocation)
103    L2DependsOnRb,
104    /// Write L2 cache before writing anything from the refblock cache (during cluster freeing)
105    RbDependsOnL2,
106}
107
108/// Qcow2 metadata caches with flush-ordering coordination.
109///
110/// When allocating clusters, we need to increment the refcounts (from 0 to 1) before writing the
111/// L2 pointers.  Therefore, before anything is written from the L2 cache to disk, we need to flush
112/// the refblock cache.
113///
114/// When freeing clusters, we need to clear the L2 pointers before decrementing the refcounts (to
115/// 0).  Therefore before anything is written from the refblock cache to disk, we need to flush the
116/// L2 cache.
117///
118/// This structure takes care to heed those dependencies (though the general qcow2 code still needs
119/// to announce them by calling `.l2_depends_on_rb()` for allocations and `.rb_depends_on_l2()` for
120/// freeing).
121///
122/// Announcing a dependency takes the `direction` lock and flushes the other cache while holding
123/// it, so `direction` is the innermost lock: Never acquire a metadata table’s `writer_lock` (the
124/// lock guarding modifications to its entries, e.g. on [`L2Table`]) while holding that direction
125/// lock.
126///
127/// Conversely, note that for an allocating write, an L2 table is held write-locked across its data
128/// I/O while its prepared data mapping is uncommitted, and allocating the clusters announces its
129/// dependency (taking the `direction` lock) while that write lock is held.  Therefore, writing a
130/// table back to disk must never take the table’s `writer_lock`, because doing so would deadlock
131/// against the still-held lock.  It does take the table’s separate `writeback_lock` (see
132/// [`L2Table::write_if_modified()`]); that is a leaf, held only across the write itself and never
133/// across anything that acquires `direction`.
134pub(super) struct MetadataCaches<S: Storage> {
135    /// L2 table cache
136    l2: AsyncLruCache<HostCluster, L2Table, L2CacheBackend<S>>,
137
138    /// Refblock cache
139    rb: AsyncLruCache<HostCluster, RefBlock, RefBlockCacheBackend<S>>,
140
141    /// Current dependency direction
142    ///
143    /// Wrapped methods hold a read guard for their entire execution,
144    /// preventing the direction from changing mid-operation.  Direction
145    /// switch methods take a write guard.
146    direction: RwLock<CacheDependency>,
147}
148
149#[maybe_async]
150impl<S: Storage> MetadataCaches<S> {
151    /// Create metadata caches for the given file, with the given header.
152    ///
153    /// The L2 cache is going to hold `l2_entries` tables, the refblock cache will have
154    /// `rb_entries` refcount blocks.
155    pub fn new(file: &Arc<S>, header: &Arc<Header>, l2_entries: usize, rb_entries: usize) -> Self {
156        let l2_backend = L2CacheBackend::new(Arc::clone(file), Arc::clone(header));
157        let rb_backend = RefBlockCacheBackend::new(Arc::clone(file), Arc::clone(header));
158
159        MetadataCaches {
160            l2: AsyncLruCache::new(l2_backend, l2_entries),
161            rb: AsyncLruCache::new(rb_backend, rb_entries),
162            direction: Default::default(),
163        }
164    }
165
166    /// Make sure the refblock cache is flushed before the L2 cache.
167    ///
168    /// Use before allocating new clusters.
169    pub async fn l2_depends_on_rb(&self) -> io::Result<()> {
170        let mut dir = self.direction.write().await;
171        if *dir == CacheDependency::L2DependsOnRb {
172            return Ok(());
173        }
174        if *dir == CacheDependency::RbDependsOnL2 {
175            self.l2.flush().await?;
176        }
177        *dir = CacheDependency::L2DependsOnRb;
178        Ok(())
179    }
180
181    /// Make sure the L2 cache is flushed before the refblock cache.
182    ///
183    /// Use before freeing clusters.
184    pub async fn rb_depends_on_l2(&self) -> io::Result<()> {
185        let mut dir = self.direction.write().await;
186        if *dir == CacheDependency::RbDependsOnL2 {
187            return Ok(());
188        }
189        if *dir == CacheDependency::L2DependsOnRb {
190            self.rb.flush().await?;
191        }
192        *dir = CacheDependency::RbDependsOnL2;
193        Ok(())
194    }
195
196    /// Flush both L2 and refblock cache to disk.
197    pub async fn flush_all(&self) -> io::Result<()> {
198        let dir = self.direction.read().await;
199        if *dir == CacheDependency::L2DependsOnRb {
200            self.rb.flush().await?;
201            self.l2.flush().await?;
202        } else {
203            self.l2.flush().await?;
204            self.rb.flush().await?;
205        }
206
207        Ok(())
208    }
209
210    /// Retrieve an L2 table from the cache.
211    ///
212    /// See [`AsyncLruCache::get_or_insert()`] for details.
213    pub async fn l2_get_or_insert(&self, cluster_index: HostCluster) -> io::Result<Arc<L2Table>> {
214        let dir = self.direction.read().await;
215        if let Some(l2) = self.l2.get_or_insert(cluster_index, false).await? {
216            return Ok(l2);
217        }
218
219        if *dir == CacheDependency::L2DependsOnRb {
220            self.rb.flush().await?;
221        }
222
223        // `unwrap()` is safe: Will never return `Ok(None)` with `may_flush` set to true.
224        let l2 = self.l2.get_or_insert(cluster_index, true).await?.unwrap();
225        Ok(l2)
226    }
227
228    /// Force-insert an L2 table.
229    ///
230    /// The L2 table must have a cluster associated with it ([`L2Table::set_cluster()`]).
231    ///
232    /// See [`AsyncLruCache::insert()`] for details.
233    pub async fn l2_insert(&self, table: Arc<L2Table>) -> io::Result<()> {
234        let cluster = table
235            .get_cluster()
236            .ok_or_else(|| io::Error::other("L2 table does not have a cluster"))?;
237        let dir = self.direction.read().await;
238        if !self.l2.insert(cluster, Arc::clone(&table), false).await? {
239            if *dir == CacheDependency::L2DependsOnRb {
240                self.rb.flush().await?;
241            }
242
243            let inserted = self.l2.insert(cluster, table, true).await?;
244            // Will always insert with `may_flush` set to true.
245            assert!(inserted);
246        }
247
248        Ok(())
249    }
250
251    /// Invalidate the L2 cache.
252    ///
253    /// # Safety
254    /// May cause image corruption, you must guarantee the on-disk state is consistent.
255    pub async unsafe fn invalidate_l2(&self) -> io::Result<()> {
256        unsafe { self.l2.invalidate() }.await
257    }
258
259    /// Retrieve a refblock from the cache.
260    ///
261    /// See [`AsyncLruCache::get_or_insert()`] for details.
262    pub async fn rb_get_or_insert(&self, cluster_index: HostCluster) -> io::Result<Arc<RefBlock>> {
263        let dir = self.direction.read().await;
264        if let Some(rb) = self.rb.get_or_insert(cluster_index, false).await? {
265            return Ok(rb);
266        }
267
268        if *dir == CacheDependency::RbDependsOnL2 {
269            self.l2.flush().await?;
270        }
271
272        // `unwrap()` is safe: Will never return `Ok(None)` with `may_flush` set to true.
273        let rb = self.rb.get_or_insert(cluster_index, true).await?.unwrap();
274        Ok(rb)
275    }
276
277    /// Force-insert a refblock.
278    ///
279    /// The refblock must have a cluster associated with it ([`RefBlock::set_cluster()`]).
280    ///
281    /// See [`AsyncLruCache::insert()`] for details.
282    pub async fn rb_insert(&self, rb: Arc<RefBlock>) -> io::Result<()> {
283        let cluster = rb
284            .get_cluster()
285            .ok_or_else(|| io::Error::other("Refblock does not have a cluster"))?;
286        let dir = self.direction.read().await;
287        if !self.rb.insert(cluster, Arc::clone(&rb), false).await? {
288            if *dir == CacheDependency::RbDependsOnL2 {
289                self.l2.flush().await?;
290            }
291
292            let inserted = self.rb.insert(cluster, rb, true).await?;
293            // Will always insert with `may_flush` set to true.
294            assert!(inserted);
295        }
296
297        Ok(())
298    }
299
300    /// Flush the refblock cache to disk.
301    pub async fn flush_rb(&self) -> io::Result<()> {
302        let dir = self.direction.read().await;
303        if *dir == CacheDependency::RbDependsOnL2 {
304            self.l2.flush().await?;
305        }
306        self.rb.flush().await
307    }
308
309    /// Invalidate the refblock cache.
310    ///
311    /// # Safety
312    /// May cause image corruption, you must guarantee the on-disk state is consistent.
313    pub async unsafe fn invalidate_rb(&self) -> io::Result<()> {
314        unsafe { self.rb.invalidate() }.await
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321    use crate::null::Null;
322
323    fn make_test_caches() -> MetadataCaches<Null> {
324        let null = Arc::new(Null::new(1 << 16));
325        let header = Arc::new(Header::new(16, 1, None, None, None));
326        MetadataCaches::new(&null, &header, 16, 16)
327    }
328
329    #[cfg(feature = "sync")]
330    fn block_on<T>(v: T) -> T {
331        v
332    }
333
334    #[cfg(feature = "async")]
335    fn block_on<F: std::future::Future>(f: F) -> F::Output {
336        tokio::runtime::Builder::new_current_thread()
337            .build()
338            .unwrap()
339            .block_on(f)
340    }
341
342    /// Direction switching is idempotent and doesn't error.
343    #[maybe_async::test(feature = "sync", async(feature = "async", tokio::test))]
344    async fn test_direction_switch() {
345        let caches = make_test_caches();
346
347        caches.l2_depends_on_rb().await.unwrap();
348        caches.l2_depends_on_rb().await.unwrap();
349        caches.rb_depends_on_l2().await.unwrap();
350        caches.rb_depends_on_l2().await.unwrap();
351        caches.l2_depends_on_rb().await.unwrap();
352    }
353
354    /// Cross-cache eviction must not deadlock.
355    ///
356    /// Fill both caches to capacity with l2→rb active.  Two threads
357    /// concurrently trigger eviction on each cache.  The coordinator
358    /// flushes deps before map.write(), so no cross-cache lock
359    /// nesting occurs.
360    #[test]
361    fn test_cross_cache_eviction_no_deadlock() {
362        use std::thread;
363
364        let null = Arc::new(Null::new(1 << 20));
365        let header = Arc::new(Header::new(16, 1, None, None, None));
366        let caches = Arc::new(MetadataCaches::new(&null, &header, 1, 1));
367
368        block_on(caches.l2_depends_on_rb()).unwrap();
369
370        // Create entries with cluster set and modified cleared so
371        // flush is a no-op (avoids needing real I/O on Null storage).
372        let mut l2_entry = L2Table::new_cleared(&header);
373        l2_entry.set_cluster(HostCluster(0));
374        l2_entry.clear_modified();
375        let mut rb_entry = RefBlock::new_cleared(null.as_ref(), &header).unwrap();
376        rb_entry.set_cluster(HostCluster(0));
377        rb_entry.clear_modified();
378
379        block_on(caches.l2_insert(Arc::new(l2_entry))).unwrap();
380        block_on(caches.rb_insert(Arc::new(rb_entry))).unwrap();
381
382        let c1 = Arc::clone(&caches);
383        let c2 = Arc::clone(&caches);
384
385        let barrier = Arc::new(std::sync::Barrier::new(2));
386        let bar1 = Arc::clone(&barrier);
387        let bar2 = Arc::clone(&barrier);
388
389        // Thread 1: insert new l2 entry (evicts old one, dep-flushes rb)
390        let header1 = Arc::clone(&header);
391        let t1 = thread::spawn(move || {
392            let mut entry = L2Table::new_cleared(&header1);
393            entry.set_cluster(HostCluster(1));
394            entry.clear_modified();
395            bar1.wait();
396            block_on(c1.l2_insert(Arc::new(entry))).unwrap();
397        });
398
399        // Thread 2: insert new rb entry (evicts old one)
400        let header2 = Arc::clone(&header);
401        let null2 = Arc::new(Null::new(1 << 20));
402        let t2 = thread::spawn(move || {
403            let mut entry = RefBlock::new_cleared(null2.as_ref(), &header2).unwrap();
404            entry.set_cluster(HostCluster(1));
405            entry.clear_modified();
406            bar2.wait();
407            block_on(c2.rb_insert(Arc::new(entry))).unwrap();
408        });
409
410        t1.join().unwrap();
411        t2.join().unwrap();
412    }
413
414    /// Concurrent direction switching must not deadlock or corrupt
415    /// the direction state.
416    #[test]
417    fn test_concurrent_direction_switch() {
418        use std::thread;
419
420        let caches = Arc::new(make_test_caches());
421
422        let c1 = Arc::clone(&caches);
423        let c2 = Arc::clone(&caches);
424
425        let barrier = Arc::new(std::sync::Barrier::new(2));
426        let bar1 = Arc::clone(&barrier);
427        let bar2 = Arc::clone(&barrier);
428
429        let t1 = thread::spawn(move || {
430            bar1.wait();
431            block_on(c1.l2_depends_on_rb()).unwrap();
432        });
433
434        let t2 = thread::spawn(move || {
435            bar2.wait();
436            block_on(c2.rb_depends_on_l2()).unwrap();
437        });
438
439        t1.join().unwrap();
440        t2.join().unwrap();
441    }
442
443    /// Direction switch must flush the opposing cache's dirty entries.
444    #[maybe_async::test(feature = "sync", async(feature = "async", tokio::test))]
445    async fn test_direction_switch_flushes_opposing() {
446        let null = Arc::new(Null::new(1 << 20));
447        let header = Arc::new(Header::new(16, 1, None, None, None));
448        let caches = MetadataCaches::new(&null, &header, 16, 16);
449
450        // Insert a dirty rb entry
451        let mut rb_entry = RefBlock::new_cleared(null.as_ref(), &header).unwrap();
452        rb_entry.set_cluster(HostCluster(0));
453        // Leave modified = true (new_cleared sets it)
454        let rb_arc = Arc::new(rb_entry);
455        caches.rb_insert(Arc::clone(&rb_arc)).await.unwrap();
456
457        assert!(rb_arc.is_modified());
458
459        // Set l2→rb, then switch to rb→l2 — must flush rb
460        caches.l2_depends_on_rb().await.unwrap();
461        caches.rb_depends_on_l2().await.unwrap();
462
463        // rb was flushed during the direction switch
464        assert!(!rb_arc.is_modified());
465    }
466
467    /// Dep cache must be flushed before the main cache evicts.
468    ///
469    /// With l2→rb active, inserting into a full l2 cache must flush
470    /// rb (the dependency) before evicting the l2 entry.
471    #[maybe_async::test(feature = "sync", async(feature = "async", tokio::test))]
472    async fn test_dep_flushed_before_eviction() {
473        let null = Arc::new(Null::new(1 << 20));
474        let header = Arc::new(Header::new(16, 1, None, None, None));
475        let caches = MetadataCaches::new(&null, &header, 1, 16);
476
477        caches.l2_depends_on_rb().await.unwrap();
478
479        // Insert a dirty rb entry
480        let mut rb_entry = RefBlock::new_cleared(null.as_ref(), &header).unwrap();
481        rb_entry.set_cluster(HostCluster(0));
482        let rb_arc = Arc::new(rb_entry);
483        caches.rb_insert(Arc::clone(&rb_arc)).await.unwrap();
484
485        // Fill l2 cache (size 1)
486        let mut l2_entry = L2Table::new_cleared(&header);
487        l2_entry.set_cluster(HostCluster(0));
488        l2_entry.clear_modified();
489        caches.l2_insert(Arc::new(l2_entry)).await.unwrap();
490
491        // Insert another l2 entry — triggers eviction.
492        // The coordinator must flush rb (dep) before the l2 eviction.
493        let mut l2_entry2 = L2Table::new_cleared(&header);
494        l2_entry2.set_cluster(HostCluster(1));
495        l2_entry2.clear_modified();
496        caches.l2_insert(Arc::new(l2_entry2)).await.unwrap();
497
498        // rb must have been flushed by the dep flush
499        assert!(
500            !rb_arc.is_modified(),
501            "rb entry must be flushed before l2 eviction"
502        );
503    }
504}