Skip to main content

fxfs/object_store/
flush.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
5// This module is responsible for flushing (a.k.a. compacting) the object store trees.
6
7use crate::errors::FxfsError;
8use crate::filesystem::{FlushReason, ForceMajor};
9use crate::log::*;
10use crate::lsm_tree::types::{ItemRef, LayerIterator, MaybeContainsKey};
11use crate::lsm_tree::{LSMTree, LockedLayer, layer_from_handle};
12use crate::object_handle::{INVALID_OBJECT_ID, ObjectHandle, ReadObjectHandle};
13use crate::object_store::extent_record::ExtentValue;
14use crate::object_store::object_manager::{ObjectManager, ReservationUpdate};
15use crate::object_store::object_record::{ObjectKey, ObjectValue};
16use crate::object_store::transaction::{
17    AssociatedObject, LockKey, Mutation, ReservationOptions, lock_keys,
18};
19use crate::object_store::{
20    AssocObj, DirectWriter, EncryptedMutations, HandleOptions, LastObjectId, LastObjectIdInfo,
21    LockState, MAX_ENCRYPTED_MUTATIONS_SIZE, ObjectStore, Options, ReservedId, StoreInfo,
22    layer_size_from_encrypted_mutations_size, tree,
23};
24use crate::serialized_types::{LATEST_VERSION, Version, VersionedLatest};
25use anyhow::{Context, Error, anyhow, bail};
26use fxfs_crypto::UnwrappedKey;
27use std::sync::OnceLock;
28use std::sync::atomic::Ordering;
29
30#[fxfs_trace::trace]
31impl ObjectStore {
32    /// Takes a flush lock on self and performs the flush with a default
33    /// FlushReason::Journal(ForceMajor::False).
34    pub async fn flush(&self) -> Result<Version, Error> {
35        self.flush_with_reason(FlushReason::Journal(ForceMajor::False)).await
36    }
37
38    /// Takes a flush lock on self and performs the flush.
39    pub async fn flush_with_reason(&self, reason: FlushReason) -> Result<Version, Error> {
40        let filesystem = self.filesystem();
41
42        let keys = lock_keys![LockKey::flush(self.store_object_id())];
43        let _guard = filesystem.lock_manager().write_lock(keys).await;
44
45        self.flush_guarded_with_reason(reason).await
46    }
47
48    /// Performs a flush while already holding a flush guard on self.
49    #[trace("store_object_id" => self.store_object_id)]
50    pub async fn flush_guarded_with_reason(&self, reason: FlushReason) -> Result<Version, Error> {
51        if self.parent_store.is_none() {
52            // Early exit, but still return the earliest version used by a struct in the tree
53            return Ok(self.tree.get_earliest_version());
54        }
55
56        // After taking the lock, check to see if the store has been deleted. Stores that are
57        // unlocked read-only (e.g. during volume fsck) behave like locked stores when flushed:
58        // mutations are written to the encrypted mutations file rather than layer files.
59        let flush_as_locked = match *self.lock_state.lock() {
60            LockState::Deleted => {
61                // When we compact, it's possible that the store has been deleted since we gathered
62                // the list of stores that need compacting.  This is benign.
63                return Ok(LATEST_VERSION);
64            }
65            LockState::Invalid => {
66                // An invalid store can't be flushed (e.g. its store info might have been discarded
67                // if locking it failed). Fail before committing anything, rather than panicking
68                // later.
69                bail!(
70                    anyhow!(FxfsError::Internal)
71                        .context(format!("Cannot flush invalid store {}", self.store_object_id))
72                );
73            }
74            LockState::Locked | LockState::UnlockedReadOnly(..) => true,
75            _ => false,
76        };
77
78        let filesystem = self.filesystem();
79        let object_manager = filesystem.ensure_local().object_manager();
80        let earliest_version = self.tree.get_earliest_version();
81        let needs_flush = object_manager.needs_flush(self.store_object_id);
82        // If we don't need to do anything for the stated flush purpose, do nothing.
83        if (reason == FlushReason::Journal(ForceMajor::False) && !needs_flush)
84            || (reason == FlushReason::Journal(ForceMajor::True)
85                && !needs_flush
86                && (flush_as_locked || self.tree.immutable_layer_count() <= 1))
87            || (reason == FlushReason::UpgradeVersion && earliest_version == LATEST_VERSION)
88            || (reason == FlushReason::EncryptedMutations
89                && self.store_info().unwrap().encrypted_mutations_object_id == INVALID_OBJECT_ID)
90        {
91            // Early exit, but still return the earliest version used by a struct in the
92            // tree.
93            return Ok(earliest_version);
94        }
95
96        let trace = self.trace.load(Ordering::Relaxed);
97        if trace {
98            info!(store_id = self.store_object_id(); "OS: begin flush");
99        }
100
101        if flush_as_locked {
102            self.flush_locked().await.with_context(|| {
103                format!("Failed to flush object store {}", self.store_object_id)
104            })?;
105        } else {
106            self.flush_unlocked(reason).await.with_context(|| {
107                format!("Failed to flush object store {}", self.store_object_id)
108            })?;
109        }
110
111        if trace {
112            info!(store_id = self.store_object_id(); "OS: end flush");
113        }
114        if let Some(callback) = &*self.flush_callback.lock() {
115            callback(self);
116        }
117
118        // NOTE: `num_flushes` must only be incremented while the flush lock is held, as
119        // `ObjectStore::unlock` relies on this to detect concurrent flushes.
120        let mut counters = self.counters.lock();
121        counters.num_flushes += 1;
122        counters.last_flush_time = Some(std::time::SystemTime::now());
123        // Return the earliest version used by a struct in the tree
124        Ok(self.tree.get_earliest_version())
125    }
126
127    // Flushes an unlocked store. Returns the layer file sizes.
128    async fn flush_unlocked(&self, reason: FlushReason) -> Result<Vec<u64>, Error> {
129        struct StoreInfoSnapshot<'a> {
130            store: &'a ObjectStore,
131            store_info: OnceLock<StoreInfo>,
132        }
133        impl AssociatedObject for StoreInfoSnapshot<'_> {
134            fn will_apply_mutation(
135                &self,
136                _mutation: &Mutation,
137                _object_id: u64,
138                _manager: &ObjectManager,
139            ) {
140                let mut store_info = self.store.store_info().unwrap();
141
142                let lock_state = self.store.lock_state.lock();
143                if let LockState::Unlocked { mutations_cipher, .. } = &*lock_state {
144                    store_info.mutations_cipher_offset = mutations_cipher.sequence_number();
145                }
146
147                self.store_info.set(store_info).unwrap();
148            }
149        }
150
151        let store_info_snapshot = StoreInfoSnapshot { store: self, store_info: OnceLock::new() };
152
153        let filesystem = self.filesystem();
154        let txn_options = Options {
155            skip_journal_checks: true,
156            reservation: ReservationOptions::Borrowed,
157            ..Default::default()
158        };
159
160        // The BeginFlush mutation must be within a transaction that has no impact on StoreInfo
161        // since we want to get an accurate snapshot of StoreInfo.
162        let mut transaction = self.new_transaction(lock_keys![], txn_options).await?;
163        transaction.add_with_object(
164            self.store_object_id(),
165            Mutation::BeginFlush,
166            AssocObj::Borrowed(&store_info_snapshot),
167        );
168        transaction.commit().await.context("Failed to commit BeginFlush transaction")?;
169
170        let mut new_store_info = store_info_snapshot.store_info.into_inner().unwrap();
171
172        // There is a transaction to create objects at the start and then another transaction at the
173        // end. Between those two transactions, there are transactions that write to the files.  In
174        // the first transaction, objects are created in the graveyard. Upon success, the objects
175        // are removed from the graveyard.
176        let mut transaction = self.new_transaction(lock_keys![], txn_options).await?;
177
178        // Create and write a new layer, compacting existing layers.
179        let parent_store = self.parent_store.as_ref().unwrap();
180        let handle_options = HandleOptions { skip_journal_checks: true, ..Default::default() };
181        let id_and_key = {
182            let mut lock_state = self.lock_state.lock();
183            match &mut *lock_state {
184                LockState::Unlocked { cached_keys, .. } => {
185                    if let Some(item) = cached_keys.pop() {
186                        Ok(Some(item))
187                    } else {
188                        log::warn!("No cached keys for flush for store {}", self.store_object_id());
189                        Err(anyhow!(FxfsError::Internal).context("No cached keys for flush"))
190                    }
191                }
192                LockState::UnlockedReadOnly(..) => {
193                    Err(anyhow!(FxfsError::Internal).context("Flush on read-only store"))
194                }
195                LockState::Unencrypted => Ok(None),
196                _ => Err(anyhow!(FxfsError::Internal))
197                    .with_context(|| format!("Invalid lock state ({:?}) for flush", *lock_state)),
198            }
199        }?;
200
201        let (new_object_tree_layer, unwrapped_key) = if let Some((raw_id, key, unwrapped_key)) =
202            id_and_key
203        {
204            let object_id = ReservedId::new(parent_store, raw_id);
205            let handle = ObjectStore::create_object_with_key(
206                parent_store,
207                &mut transaction,
208                object_id,
209                handle_options,
210                key,
211                UnwrappedKey::new(unwrapped_key.clone()),
212            )
213            .await?;
214            (handle, Some(unwrapped_key))
215        } else {
216            let handle =
217                ObjectStore::create_object(parent_store, &mut transaction, handle_options, None)
218                    .await?;
219            (handle, None)
220        };
221        let writer = DirectWriter::new(&new_object_tree_layer, txn_options).await;
222        let new_object_tree_layer_object_id = new_object_tree_layer.object_id();
223        parent_store.add_to_graveyard(&mut transaction, new_object_tree_layer_object_id);
224
225        transaction.commit().await.context("Failed to commit create layer transaction")?;
226
227        // *Do* the actual compaction.
228        let full_compaction = match reason {
229            FlushReason::UpgradeVersion | FlushReason::Journal(ForceMajor::True) => true,
230            _ => false,
231        };
232        let (layers_to_keep, old_layers) = tree::flush(
233            &self.tree,
234            writer,
235            matches!(reason, FlushReason::Journal(_))
236                .then(|| filesystem.ensure_local().journal().get_compaction_yielder()),
237            full_compaction,
238        )
239        .await
240        .context("Failed to flush tree")?;
241
242        // Finalise the compaction.
243        let mut new_layers = vec![layer_from_handle(new_object_tree_layer, unwrapped_key).await?];
244        new_layers.extend(layers_to_keep.iter().map(|l| (*l).clone()));
245
246        new_store_info.layers = Vec::new();
247        for layer in &new_layers {
248            if let Some(handle) = layer.handle() {
249                new_store_info.layers.push(handle.object_id());
250            }
251        }
252
253        let reservation_update: ReservationUpdate; // Must live longer than end_transaction.
254        let mut end_transaction = parent_store
255            .new_transaction(
256                lock_keys![LockKey::object(
257                    self.parent_store.as_ref().unwrap().store_object_id(),
258                    self.store_info_handle_object_id().unwrap(),
259                )],
260                txn_options,
261            )
262            .await?;
263
264        parent_store.remove_from_graveyard(&mut end_transaction, new_object_tree_layer_object_id);
265
266        // Move the existing layers we're compacting to the graveyard at the end.
267        for layer in &old_layers {
268            if let Some(handle) = layer.handle() {
269                parent_store.add_to_graveyard(&mut end_transaction, handle.object_id());
270            }
271        }
272
273        let old_encrypted_mutations_object_id =
274            std::mem::replace(&mut new_store_info.encrypted_mutations_object_id, INVALID_OBJECT_ID);
275        if old_encrypted_mutations_object_id != INVALID_OBJECT_ID {
276            parent_store.add_to_graveyard(&mut end_transaction, old_encrypted_mutations_object_id);
277        }
278
279        // `last_object_id` is updated differently to other members of `StoreInfo`.  We must ensure
280        // that those fields match the current in-memory values.  See the lengthy comment in
281        // `get_next_object_id` for more information.  `end_transaction` has a lock on the same lock
282        // that `get_next_object_id` uses, so there's no danger of the key changing now.
283        //
284        // This might capture object IDs that might be in transactions not yet committed.  In
285        // theory, we could do better than this but it's not worth the effort.
286        match &mut new_store_info.last_object_id {
287            LastObjectIdInfo::Unencrypted { id } => {
288                let LastObjectId::Unencrypted { id: in_memory_value } =
289                    &*self.last_object_id.lock()
290                else {
291                    unreachable!()
292                };
293                *id = *in_memory_value;
294            }
295            LastObjectIdInfo::Encrypted { id, key } => {
296                let LastObjectId::Encrypted { id: in_memory_value, .. } =
297                    &*self.last_object_id.lock()
298                else {
299                    unreachable!()
300                };
301                *id = *in_memory_value;
302                let guard = self.store_info.lock();
303                let current_store_info = guard.as_ref().unwrap();
304                let LastObjectIdInfo::Encrypted { key: in_memory_value, .. } =
305                    &current_store_info.last_object_id
306                else {
307                    unreachable!()
308                };
309                *key = in_memory_value.clone();
310            }
311            LastObjectIdInfo::Low32Bit => {}
312        }
313
314        self.write_store_info(&mut end_transaction, &new_store_info).await?;
315
316        let layer_file_sizes = new_layers
317            .iter()
318            .map(|l| l.handle().map(ReadObjectHandle::get_size).unwrap_or(0))
319            .collect::<Vec<u64>>();
320
321        let total_layer_size = layer_file_sizes.iter().sum();
322
323        // Even when initiated as a minor compaction, `tree::flush` merges all layers into a single
324        // base layer (`layers_to_keep.is_empty()`) if the existing layer is smaller than
325        // `DEFAULT_RECLAIM_SIZE`, which purges all tombstones and resets `store_borrowed` to 0.
326        reservation_update = ReservationUpdate::new(total_layer_size);
327        let new_borrowed = if layers_to_keep.is_empty() {
328            0
329        } else {
330            let old_merged_size = old_layers
331                .iter()
332                .map(|l| l.handle().map(ReadObjectHandle::get_size).unwrap_or(0))
333                .sum();
334            filesystem.ensure_local().object_manager().store_borrowed_for_flush(
335                self.store_object_id(),
336                old_merged_size,
337                layer_file_sizes[0],
338                total_layer_size,
339            )
340        };
341
342        end_transaction.add_with_object(
343            self.store_object_id(),
344            Mutation::EndFlush(new_borrowed),
345            AssocObj::Borrowed(&reservation_update),
346        );
347
348        if self.trace.load(Ordering::Relaxed) {
349            info!(
350                store_id = self.store_object_id(),
351                old_layer_count = old_layers.len(),
352                new_layer_count = new_layers.len(),
353                total_layer_size,
354                new_store_info:?;
355                "OS: compacting"
356            );
357        }
358
359        end_transaction
360            .commit_with_callback(|_| {
361                let mut store_info = self.store_info.lock();
362                let info = store_info.as_mut().unwrap();
363                info.layers = new_store_info.layers;
364                info.encrypted_mutations_object_id = new_store_info.encrypted_mutations_object_id;
365                info.mutations_cipher_offset = new_store_info.mutations_cipher_offset;
366                self.tree.set_layers(new_layers);
367            })
368            .await
369            .context("Failed to commit EndFlush transaction")?;
370
371        // Now close the layers and purge them.
372        for layer in old_layers {
373            let object_id = layer.handle().map(|h| h.object_id());
374            layer.close_layer().await;
375            if let Some(object_id) = object_id {
376                parent_store
377                    .tombstone_object(object_id, txn_options, None)
378                    .await
379                    .context("Failed to tombstone old layer")?;
380            }
381        }
382
383        if old_encrypted_mutations_object_id != INVALID_OBJECT_ID {
384            parent_store
385                .tombstone_object(old_encrypted_mutations_object_id, txn_options, None)
386                .await
387                .context("Failed to tombstone old encrypted mutations")?;
388        }
389
390        Ok(layer_file_sizes)
391    }
392
393    // Flushes a locked store.
394    async fn flush_locked(&self) -> Result<(), Error> {
395        let filesystem = self.filesystem();
396        let txn_options = Options {
397            skip_journal_checks: true,
398            reservation: ReservationOptions::Borrowed,
399            ..Default::default()
400        };
401
402        let mut transaction = self.new_transaction(lock_keys![], txn_options).await?;
403        transaction.add(self.store_object_id(), Mutation::BeginFlush);
404        transaction.commit().await.context("Failed to commit BeginFlush transaction")?;
405
406        let mut new_store_info = self.load_store_info().await?;
407
408        // There is a transaction to create objects at the start and then another transaction at the
409        // end. Between those two transactions, there are transactions that write to the files.  In
410        // the first transaction, objects are created in the graveyard. Upon success, the objects
411        // are removed from the graveyard.
412        let mut transaction = self.new_transaction(lock_keys![], txn_options).await?;
413
414        let reservation_update: ReservationUpdate; // Must live longer than end_transaction.
415        let handle; // Must live longer than end_transaction.
416        let mut end_transaction;
417
418        // We need to either write our encrypted mutations to a new file, or append them to an
419        // existing one.
420        let parent_store = self.parent_store.as_ref().unwrap();
421        handle = if new_store_info.encrypted_mutations_object_id == INVALID_OBJECT_ID {
422            let handle = ObjectStore::create_object(
423                parent_store,
424                &mut transaction,
425                HandleOptions { skip_journal_checks: true, ..Default::default() },
426                None,
427            )
428            .await?;
429            let oid = handle.object_id();
430            end_transaction = parent_store
431                .new_transaction(
432                    lock_keys![
433                        LockKey::object(parent_store.store_object_id(), oid),
434                        LockKey::object(
435                            parent_store.store_object_id(),
436                            self.store_info_handle_object_id().unwrap(),
437                        ),
438                    ],
439                    txn_options,
440                )
441                .await?;
442            new_store_info.encrypted_mutations_object_id = oid;
443            parent_store.add_to_graveyard(&mut transaction, oid);
444            parent_store.remove_from_graveyard(&mut end_transaction, oid);
445            handle
446        } else {
447            end_transaction = parent_store
448                .new_transaction(
449                    lock_keys![
450                        LockKey::object(
451                            parent_store.store_object_id(),
452                            new_store_info.encrypted_mutations_object_id,
453                        ),
454                        LockKey::object(
455                            parent_store.store_object_id(),
456                            self.store_info_handle_object_id().unwrap(),
457                        ),
458                    ],
459                    txn_options,
460                )
461                .await?;
462            ObjectStore::open_object(
463                parent_store,
464                new_store_info.encrypted_mutations_object_id,
465                HandleOptions { skip_journal_checks: true, ..Default::default() },
466                None,
467            )
468            .await?
469        };
470        transaction
471            .commit()
472            .await
473            .context("Failed to commit create encrypted mutations transaction")?;
474
475        // Append the encrypted mutations, which need to be read from the journal.
476        // This assumes that the journal has no buffered mutations for this store (see Self::lock).
477        let journaled = filesystem
478            .ensure_local()
479            .journal()
480            .read_transactions_for_object(self.store_object_id)
481            .await
482            .context("Failed to read encrypted mutations from journal")?;
483        let mut buffer = handle.allocate_buffer(MAX_ENCRYPTED_MUTATIONS_SIZE).await;
484        let mut writer = buffer.writer();
485        EncryptedMutations::from_replayed_mutations(self.store_object_id, journaled)
486            .serialize_with_version(&mut writer)?;
487        let len = writer.position();
488        handle
489            .txn_write(&mut end_transaction, handle.get_size(), buffer.subslice(..len))
490            .await
491            .context("Failed to write encrypted mutations")?;
492
493        self.write_store_info(&mut end_transaction, &new_store_info)
494            .await
495            .context("Failed to write store info")?;
496
497        let mut total_layer_size = 0;
498        for &oid in &new_store_info.layers {
499            total_layer_size += parent_store.get_file_size(oid).await?;
500        }
501        let old_encrypted_size = layer_size_from_encrypted_mutations_size(handle.get_size());
502        let new_encrypted_size =
503            layer_size_from_encrypted_mutations_size(handle.get_size() + len as u64);
504        total_layer_size += new_encrypted_size;
505
506        // Flushing a locked store appends to the encrypted mutations file without compacting
507        // tree layers, increasing the store's layer size from `old_encrypted_size` to
508        // `new_encrypted_size`.
509        reservation_update = ReservationUpdate::new(total_layer_size);
510        let new_borrowed = filesystem.ensure_local().object_manager().store_borrowed_for_flush(
511            self.store_object_id(),
512            old_encrypted_size,
513            new_encrypted_size,
514            total_layer_size,
515        );
516
517        end_transaction.add_with_object(
518            self.store_object_id(),
519            Mutation::EndFlush(new_borrowed),
520            AssocObj::Borrowed(&reservation_update),
521        );
522
523        end_transaction
524            .commit_with_callback(|_| {
525                // If the store is `UnlockedReadOnly`, `self.store_info` is populated in memory, so
526                // keep its `encrypted_mutations_object_id` in sync with the on-disk `StoreInfo`.
527                if let Some(info) = &mut *self.store_info.lock() {
528                    info.encrypted_mutations_object_id =
529                        new_store_info.encrypted_mutations_object_id;
530                }
531            })
532            .await
533            .context("Failed to commit EndFlush transaction")?;
534
535        Ok(())
536    }
537}
538
539#[cfg(test)]
540mod tests {
541    use super::{FlushReason, ForceMajor};
542    use crate::filesystem::{FxFilesystem, FxFilesystemBuilder};
543    use crate::lsm_tree::types::{LayerIterator, MaybeContainsKey};
544    use crate::object_handle::{
545        INVALID_OBJECT_ID, ObjectHandle, ReadObjectHandle, WriteObjectHandle,
546    };
547    use crate::object_store::allocator::max_extent_size_for_block_size;
548    use crate::object_store::directory::{Directory, replace_child};
549    use crate::object_store::extent_record::ExtentValue;
550    use crate::object_store::journal::JournalOptions;
551    use crate::object_store::transaction::{Options, lock_keys};
552    use crate::object_store::volume::root_volume;
553    use crate::object_store::{
554        DirType, HandleOptions, LockKey, NewChildStoreOptions, ObjectKey, ObjectStore, ObjectValue,
555        StoreOptions, layer_size_from_encrypted_mutations_size,
556    };
557    use fxfs_insecure_crypto::new_insecure_crypt;
558    use std::collections::HashSet;
559    use std::ops::Bound;
560    use std::sync::Arc;
561    use storage_device::DeviceHolder;
562    use storage_device::fake_device::FakeDevice;
563
564    #[fuchsia::test]
565    async fn test_flush_when_locked() {
566        let device = DeviceHolder::new(FakeDevice::new(8192, 1024));
567        let fs = FxFilesystem::new_empty(device).await.expect("new_empty failed");
568        let root_volume = root_volume(fs.clone()).await.expect("root_volume failed");
569        let crypt = Arc::new(new_insecure_crypt());
570        let store = root_volume
571            .new_volume(
572                "test",
573                NewChildStoreOptions {
574                    options: StoreOptions { crypt: Some(crypt.clone()), ..StoreOptions::default() },
575                    ..NewChildStoreOptions::default()
576                },
577            )
578            .await
579            .expect("new_volume failed");
580        let root_dir =
581            Directory::open(&store, store.root_directory_object_id()).await.expect("open failed");
582        let mut transaction = fs
583            .root_store()
584            .new_transaction(
585                lock_keys![LockKey::object(store.store_object_id(), root_dir.object_id())],
586                Options::default(),
587            )
588            .await
589            .expect("new_transaction failed");
590        let foo = root_dir
591            .create_child_file(&mut transaction, "foo")
592            .await
593            .expect("create_child_file failed");
594        transaction.commit().await.expect("commit failed");
595
596        // When the volume is first created it will include a new mutations key but we want to test
597        // what happens when the encrypted mutations file doesn't contain a new mutations key, so we
598        // flush here.
599        store.flush().await.expect("flush failed");
600
601        let mut transaction = fs
602            .root_store()
603            .new_transaction(
604                lock_keys![LockKey::object(store.store_object_id(), root_dir.object_id())],
605                Options::default(),
606            )
607            .await
608            .expect("new_transaction failed");
609        let bar = root_dir
610            .create_child_file(&mut transaction, "bar")
611            .await
612            .expect("create_child_file failed");
613        transaction.commit().await.expect("commit failed");
614
615        store.lock().await.expect("lock failed");
616
617        // Flushing the store whilst locked should create an encrypted mutations file.
618        store.flush().await.expect("flush failed");
619
620        // Check the reservation.
621        let info = store.load_store_info().await.unwrap();
622        let parent_store = store.parent_store().unwrap();
623        let mut total_layer_size = 0;
624        for &oid in &info.layers {
625            total_layer_size +=
626                parent_store.get_file_size(oid).await.expect("get_file_size failed");
627        }
628        assert_ne!(info.encrypted_mutations_object_id, INVALID_OBJECT_ID);
629        total_layer_size += layer_size_from_encrypted_mutations_size(
630            parent_store
631                .get_file_size(info.encrypted_mutations_object_id)
632                .await
633                .expect("get_file_size failed"),
634        );
635        assert_eq!(
636            fs.object_manager().reservation(store.store_object_id()),
637            Some(fs.object_manager().tree_reservation(store.store_object_id(), total_layer_size))
638        );
639
640        // Unlocking the store should replay that encrypted mutations file.
641        store.unlock(crypt).await.expect("unlock failed");
642
643        ObjectStore::open_object(&store, foo.object_id(), HandleOptions::default(), None)
644            .await
645            .expect("open_object failed");
646
647        ObjectStore::open_object(&store, bar.object_id(), HandleOptions::default(), None)
648            .await
649            .expect("open_object failed");
650
651        fs.close().await.expect("close failed");
652    }
653
654    #[fuchsia::test]
655    async fn test_major_compaction_frees_reservation_and_merges_layers() {
656        let device = DeviceHolder::new(FakeDevice::new(32768, 512));
657        let fs = FxFilesystem::new_empty(device).await.expect("new_empty failed");
658        let root_volume = root_volume(fs.clone()).await.expect("root_volume failed");
659        let store = root_volume
660            .new_volume("test", NewChildStoreOptions::default())
661            .await
662            .expect("new_volume failed");
663        let root_dir =
664            Directory::open(&store, store.root_directory_object_id()).await.expect("open failed");
665
666        // 1. Populate the store with files to create an initial layer > 512 KiB
667        // (DEFAULT_RECLAIM_SIZE).
668        let num_files = 2000;
669        let mut file_ids = Vec::with_capacity(num_files);
670        for i in 0..num_files {
671            let filename = format!("file_{:04}_{:<200}", i, i);
672            let mut transaction = store
673                .new_transaction(
674                    lock_keys![LockKey::object(store.store_object_id(), root_dir.object_id())],
675                    Options::default(),
676                )
677                .await
678                .expect("new_transaction failed");
679            let file = root_dir
680                .create_child_file(&mut transaction, &filename)
681                .await
682                .expect("create_child_file failed");
683            transaction.commit().await.expect("commit failed");
684            file_ids.push((file.object_id(), filename));
685        }
686
687        // Flush (minor) to create Layer 0.
688        store.flush().await.expect("flush failed");
689
690        let info_initial = store.load_store_info().await.expect("load_store_info failed");
691        assert_eq!(info_initial.layers.len(), 1);
692        let initial_reservation = fs
693            .object_manager()
694            .reservation(store.store_object_id())
695            .expect("reservation not found");
696
697        // 2. Delete half the files (1000 files).
698        for (oid, filename) in &file_ids[0..1000] {
699            let mut transaction = store
700                .new_transaction(
701                    lock_keys![
702                        LockKey::object(store.store_object_id(), root_dir.object_id()),
703                        LockKey::object(store.store_object_id(), *oid),
704                    ],
705                    Options::default(),
706                )
707                .await
708                .expect("new_transaction failed");
709            let replaced = crate::object_store::directory::replace_child(
710                &mut transaction,
711                None,
712                (&root_dir, filename.as_str()),
713            )
714            .await
715            .expect("replace_child failed");
716            assert_matches::assert_matches!(
717                replaced,
718                crate::object_store::directory::ReplacedChild::Object(id) if id == *oid
719            );
720            transaction.commit().await.expect("commit failed");
721        }
722
723        // 3. Minor flush after deleting 1000 files (FlushReason::Journal(ForceMajor::False)).
724        // Minor compaction will NOT merge with the base layer because base layer > 512 KiB.
725        store
726            .flush_with_reason(FlushReason::Journal(ForceMajor::False))
727            .await
728            .expect("minor flush failed");
729
730        let info_after_minor = store.load_store_info().await.expect("load_store_info failed");
731        assert_eq!(
732            info_after_minor.layers.len(),
733            2,
734            "Minor compaction should keep the base layer, creating 2 layers"
735        );
736        let res_after_minor = fs
737            .object_manager()
738            .reservation(store.store_object_id())
739            .expect("reservation not found");
740        assert!(
741            res_after_minor >= initial_reservation,
742            "Minor compaction should not decrease reservation because base layer is kept"
743        );
744
745        // 4. Major flush (FlushReason::Journal(ForceMajor::True)).
746        // Major compaction must merge ALL layers, purge tombstones, and reduce reservation.
747        store
748            .flush_with_reason(FlushReason::Journal(ForceMajor::True))
749            .await
750            .expect("major flush failed");
751
752        let info_after_major = store.load_store_info().await.expect("load_store_info failed");
753        assert_eq!(
754            info_after_major.layers.len(),
755            1,
756            "Major compaction should merge all layers into 1"
757        );
758        let res_after_major = fs
759            .object_manager()
760            .reservation(store.store_object_id())
761            .expect("reservation not found");
762        assert!(
763            res_after_major < initial_reservation,
764            "Major compaction should decrease reservation (was {}, initial was {})",
765            res_after_major,
766            initial_reservation
767        );
768
769        // 5. Verify data integrity.
770        // Deleted files must be gone.
771        for (oid, filename) in &file_ids[0..1000] {
772            assert!(
773                root_dir.lookup(filename).await.expect("lookup failed").is_none(),
774                "Deleted file {} should not exist",
775                oid
776            );
777        }
778        for (oid, filename) in &file_ids[1000..num_files] {
779            let res = root_dir.lookup(filename).await.expect("lookup failed");
780            assert!(res.is_some(), "Surviving file {} should exist", oid);
781            assert_eq!(res.unwrap().0, *oid);
782        }
783
784        fs.close().await.expect("close failed");
785    }
786
787    #[fuchsia::test]
788    async fn test_major_compaction_without_journal_mutations() {
789        let device = DeviceHolder::new(FakeDevice::new(32768, 512));
790        let fs = FxFilesystem::new_empty(device).await.expect("new_empty failed");
791        let root_volume = root_volume(fs.clone()).await.expect("root_volume failed");
792        let crypt = Arc::new(new_insecure_crypt());
793        let store = root_volume
794            .new_volume(
795                "test",
796                NewChildStoreOptions {
797                    options: StoreOptions { crypt: Some(crypt.clone()), ..StoreOptions::default() },
798                    ..Default::default()
799                },
800            )
801            .await
802            .expect("new_volume failed");
803        let root_dir =
804            Directory::open(&store, store.root_directory_object_id()).await.expect("open failed");
805
806        // Create initial base layer > 512 KiB.
807        for i in 0..2000 {
808            let filename = format!("file_{:04}_{:<200}", i, i);
809            let mut transaction = store
810                .new_transaction(
811                    lock_keys![LockKey::object(store.store_object_id(), root_dir.object_id())],
812                    Options::default(),
813                )
814                .await
815                .expect("new_transaction failed");
816            root_dir.create_child_file(&mut transaction, &filename).await.expect("create failed");
817            transaction.commit().await.expect("commit failed");
818        }
819        store.flush().await.expect("flush failed");
820
821        // Write a small file and do a minor flush to create a second layer.
822        let mut transaction = store
823            .new_transaction(
824                lock_keys![LockKey::object(store.store_object_id(), root_dir.object_id())],
825                Options::default(),
826            )
827            .await
828            .expect("new_transaction failed");
829        root_dir.create_child_file(&mut transaction, "extra_file").await.expect("create failed");
830        transaction.commit().await.expect("commit failed");
831        store
832            .flush_with_reason(FlushReason::Journal(ForceMajor::False))
833            .await
834            .expect("flush failed");
835
836        let info = store.load_store_info().await.expect("load_store_info failed");
837        assert_eq!(info.layers.len(), 2, "Should have 2 layers before major flush");
838
839        // While unlocked read-only with 2 layers and no journal mutations, a major flush must
840        // early-exit rather than calling `flush_locked` and creating an empty encrypted_mutations
841        // file.
842        store.lock().await.expect("lock failed");
843        store.unlock_read_only(crypt.clone()).await.expect("unlock_read_only failed");
844        assert!(!fs.object_manager().needs_flush(store.store_object_id()));
845        fs.object_manager()
846            .flush(FlushReason::Journal(ForceMajor::True))
847            .await
848            .expect("read-only major flush failed");
849        assert_eq!(
850            store
851                .load_store_info()
852                .await
853                .expect("load_store_info failed")
854                .encrypted_mutations_object_id,
855            INVALID_OBJECT_ID
856        );
857        store.lock_read_only();
858        store.unlock(crypt).await.expect("unlock failed");
859
860        // Now, do a major compaction when there are NO journal mutations in ObjectManager.
861        assert!(!fs.object_manager().needs_flush(store.store_object_id()));
862        store
863            .flush_with_reason(FlushReason::Journal(ForceMajor::True))
864            .await
865            .expect("major flush failed");
866
867        let info_after_major = store.load_store_info().await.expect("load_store_info failed");
868        assert_eq!(
869            info_after_major.layers.len(),
870            1,
871            "Major flush should merge 2 layers into 1 even without journal mutations"
872        );
873
874        // Doing another major flush when already at 1 layer and no mutations should early-exit
875        // without rewriting the layer or exhausting cached keys.
876        assert!(!fs.object_manager().needs_flush(store.store_object_id()));
877        store
878            .flush_with_reason(FlushReason::Journal(ForceMajor::True))
879            .await
880            .expect("second major flush failed");
881        let info_after_second = store.load_store_info().await.expect("load_store_info failed");
882        assert_eq!(
883            info_after_second.layers, info_after_major.layers,
884            "Major flush when at 1 layer with no mutations should be a no-op"
885        );
886
887        // Locking the store and doing a major flush with no mutations should also early-exit
888        // without creating an empty encrypted_mutations file.
889        store.lock().await.expect("lock failed");
890        assert!(!fs.object_manager().needs_flush(store.store_object_id()));
891        store
892            .flush_with_reason(FlushReason::Journal(ForceMajor::True))
893            .await
894            .expect("locked major flush failed");
895        let info_after_locked = store.load_store_info().await.expect("load_store_info failed");
896        assert_eq!(info_after_locked.encrypted_mutations_object_id, INVALID_OBJECT_ID);
897
898        fs.close().await.expect("close failed");
899    }
900
901    #[fuchsia::test]
902    async fn test_major_compaction_purges_deleted_extents() {
903        let device = DeviceHolder::new(FakeDevice::new(32768, 512));
904        let fs = FxFilesystem::new_empty(device).await.expect("new_empty failed");
905        let root_volume = root_volume(fs.clone()).await.expect("root_volume failed");
906        let store = root_volume
907            .new_volume("test", NewChildStoreOptions::default())
908            .await
909            .expect("new_volume failed");
910        let root_dir =
911            Directory::open(&store, store.root_directory_object_id()).await.expect("open failed");
912
913        // 1. Create a base layer > 512 KiB so minor compaction won't merge with it.
914        for i in 0..2000 {
915            let filename = format!("file_{:04}_{:<200}", i, i);
916            let mut transaction = store
917                .new_transaction(
918                    lock_keys![LockKey::object(store.store_object_id(), root_dir.object_id())],
919                    Options::default(),
920                )
921                .await
922                .expect("new_transaction failed");
923            root_dir.create_child_file(&mut transaction, &filename).await.expect("create failed");
924            transaction.commit().await.expect("commit failed");
925        }
926
927        // Create a data file with extents.
928        let data_oid = {
929            let mut transaction = store
930                .new_transaction(
931                    lock_keys![LockKey::object(store.store_object_id(), root_dir.object_id())],
932                    Options::default(),
933                )
934                .await
935                .expect("new_transaction failed");
936            let file = root_dir
937                .create_child_file(&mut transaction, "data_file")
938                .await
939                .expect("create_child_file failed");
940            let oid = file.object_id();
941            transaction.commit().await.expect("commit failed");
942            oid
943        };
944
945        let data_file = ObjectStore::open_object(&store, data_oid, HandleOptions::default(), None)
946            .await
947            .expect("open_object failed");
948        {
949            let mut transaction = store
950                .new_transaction(
951                    lock_keys![LockKey::object(store.store_object_id(), data_oid)],
952                    Options::default(),
953                )
954                .await
955                .expect("new_transaction failed");
956            let mut buffer = data_file.allocate_buffer(131072).await;
957            buffer.fill(0xAB);
958            data_file
959                .txn_write(&mut transaction, 0, buffer.as_ref())
960                .await
961                .expect("txn_write failed");
962            transaction.commit().await.expect("commit failed");
963        }
964
965        // Flush (minor) to commit extents to Layer 0.
966        store.flush().await.expect("flush failed");
967
968        let info0 = store.load_store_info().await.expect("load_store_info failed");
969        assert_eq!(info0.layers.len(), 1);
970
971        // Truncate data_file to 0 to write extent tombstones (ExtentValue::None).
972        data_file.truncate(0).await.expect("truncate failed");
973
974        // Minor flush: writes Layer 1 with extent tombstones on top of Layer 0.
975        store
976            .flush_with_reason(FlushReason::Journal(ForceMajor::False))
977            .await
978            .expect("minor flush failed");
979        let info1 = store.load_store_info().await.expect("load_store_info failed");
980        assert_eq!(info1.layers.len(), 2, "Minor flush should keep Layer 0 and create Layer 1");
981
982        // Major flush: purges ExtentValue::None tombstones via major_iter and merges into 1 layer.
983        store
984            .flush_with_reason(FlushReason::Journal(ForceMajor::True))
985            .await
986            .expect("major flush failed");
987        let info2 = store.load_store_info().await.expect("load_store_info failed");
988        assert_eq!(info2.layers.len(), 1, "Major flush should merge into 1 layer");
989
990        // Verify data_file size is 0.
991        assert_eq!(data_file.get_size(), 0);
992
993        fs.close().await.expect("close failed");
994    }
995
996    // This test case is a regression test for b/548631578.  It verifies that we will force major
997    // compactions when borrowed_metadata_space gets too large relative to metadata_reservation.
998    // Minor compactions are not sufficient in all cases to return borrowed metadata space, which
999    // can eventually exhuast the metadata reservation and prevent all operations (including
1000    // compaction itself).
1001    #[fuchsia::test]
1002    async fn test_borrow_metadata_space_fails_without_major_compaction() {
1003        let reclaim_size = 65536;
1004        // 102,400 blocks of 512 bytes = 50 MiB device.
1005        let device = DeviceHolder::new(FakeDevice::new(102400, 512));
1006        let fs = FxFilesystemBuilder::new()
1007            .journal_options(JournalOptions { reclaim_size, ..Default::default() })
1008            .format(true)
1009            .open(device)
1010            .await
1011            .expect("open failed");
1012
1013        // The test creates many files in several stores, and then deletes them.  The deletions
1014        // create additional layer files, which should be collapsed into the base layers (and
1015        // cancel out the file creations) following a major compaction, which should happen
1016        // automatically due to the large amount of borrowed space.
1017        let root_volume = root_volume(fs.clone()).await.expect("root_volume failed");
1018        let num_stores = 3;
1019        let files_per_store = 3500;
1020        let mut stores_and_files = Vec::new();
1021
1022        for s in 0..num_stores {
1023            let store = root_volume
1024                .new_volume(&format!("test_{s}"), NewChildStoreOptions::default())
1025                .await
1026                .expect("new_volume failed");
1027            let root_dir = Directory::open(&store, store.root_directory_object_id())
1028                .await
1029                .expect("open failed");
1030            let mut file_ids = Vec::with_capacity(files_per_store);
1031            for i in 0..files_per_store {
1032                let filename = format!("file_{:04}_{:<200}", i, i);
1033                let mut transaction = store
1034                    .new_transaction(
1035                        lock_keys![LockKey::object(store.store_object_id(), root_dir.object_id())],
1036                        Options::default(),
1037                    )
1038                    .await
1039                    .expect("new_transaction failed");
1040                let file = root_dir
1041                    .create_child_file(&mut transaction, &filename)
1042                    .await
1043                    .expect("create_child_file failed");
1044                transaction.commit().await.expect("commit failed");
1045                file_ids.push((file.object_id(), filename));
1046            }
1047            store
1048                .flush_with_reason(FlushReason::Journal(ForceMajor::True))
1049                .await
1050                .expect("flush failed");
1051            stores_and_files.push((store, root_dir, file_ids));
1052        }
1053
1054        for (_, root_dir, file_ids) in stores_and_files {
1055            for (oid, filename) in &file_ids[0..2500] {
1056                let mut context = root_dir
1057                    .acquire_context_for_replace(None, filename.as_str(), true)
1058                    .await
1059                    .expect("acquire_context failed");
1060                let replaced = crate::object_store::directory::replace_child(
1061                    &mut context.transaction,
1062                    None,
1063                    (&root_dir, filename.as_str()),
1064                )
1065                .await
1066                .expect("replace_child failed");
1067                assert_matches::assert_matches!(
1068                    replaced,
1069                    crate::object_store::directory::ReplacedChild::Object(id) if id == *oid
1070                );
1071                context.transaction.commit().await.expect("commit failed");
1072            }
1073        }
1074
1075        fs.close().await.expect("close failed");
1076    }
1077
1078    #[fuchsia::test]
1079    async fn test_file_truncate_and_extend_does_not_force_major_compaction() {
1080        let device = DeviceHolder::new(FakeDevice::new(65536, 512));
1081        let fs = FxFilesystemBuilder::new()
1082            .journal_options(JournalOptions { reclaim_size: 32768, ..Default::default() })
1083            .format(true)
1084            .open(device)
1085            .await
1086            .expect("open failed");
1087        let root_volume = root_volume(fs.clone()).await.expect("root_volume failed");
1088        let store = root_volume
1089            .new_volume("test", NewChildStoreOptions::default())
1090            .await
1091            .expect("new_volume failed");
1092        let store_id = store.store_object_id();
1093        let root_dir =
1094            Directory::open(&store, store.root_directory_object_id()).await.expect("open failed");
1095
1096        // 1. Populate the store with old files so that its base layer exceeds 512 KiB
1097        // (`DEFAULT_RECLAIM_SIZE`).
1098        for i in 0..2000 {
1099            let filename = format!("base_{i:04}_{:<200}", i);
1100            let mut transaction = store
1101                .new_transaction(
1102                    lock_keys![LockKey::object(store_id, root_dir.object_id())],
1103                    Options::default(),
1104                )
1105                .await
1106                .expect("new_transaction failed");
1107            root_dir
1108                .create_child_file(&mut transaction, &filename)
1109                .await
1110                .expect("create_child_file failed");
1111            transaction.commit().await.expect("commit failed");
1112        }
1113
1114        // Create a working file and write 4 KiB to it.
1115        let file = {
1116            let mut transaction = store
1117                .new_transaction(
1118                    lock_keys![LockKey::object(store_id, root_dir.object_id())],
1119                    Options::default(),
1120                )
1121                .await
1122                .expect("new_transaction failed");
1123            let file = root_dir
1124                .create_child_file(&mut transaction, "work_file")
1125                .await
1126                .expect("create_child_file failed");
1127            transaction.commit().await.expect("commit failed");
1128            file
1129        };
1130        let mut buf = file.allocate_buffer(4096).await;
1131        buf.fill(0xaa);
1132        file.write_or_append(Some(0), buf.as_ref()).await.expect("write_or_append failed");
1133        fs.journal().force_compact().await.expect("force_compact failed");
1134
1135        let bytes_before_loop = fs.object_manager().compaction_bytes_written();
1136
1137        // 2. Repeatedly truncate `work_file` to 0 (borrowing metadata space) and write 4 KiB back
1138        // to `work_file` (funded metadata space), letting the journal compact naturally. Writing
1139        // 0..4096 again replaces the `ExtentValue::None` record from `truncate`, so after minor
1140        // compaction the minor layer returns to its original 4 KiB size with all history gone and
1141        // the > 512 KiB base layer must never be rewritten by a major compaction.
1142        for _ in 0..450 {
1143            file.truncate_with_options(
1144                Options {
1145                    reservation: crate::object_store::transaction::ReservationOptions::Borrowed,
1146                    ..Default::default()
1147                },
1148                0,
1149            )
1150            .await
1151            .expect("truncate failed");
1152
1153            file.write_or_append(Some(0), buf.as_ref()).await.expect("write_or_append failed");
1154        }
1155        fs.journal().force_compact().await.expect("force_compact failed");
1156
1157        // Across 450 iterations (~12 journal compactions), minor compactions only write the 4 KiB
1158        // minor layer (plus small root_store / allocator layers, < 600 KiB total), whereas even a
1159        // single major compaction of the > 512 KiB base layer adds > 530 KiB.
1160        let compaction_bytes = fs.object_manager().compaction_bytes_written() - bytes_before_loop;
1161        assert!(
1162            compaction_bytes < 600 * 1024,
1163            "Truncate and extend churn caused unnecessary major compaction: \
1164             {compaction_bytes} bytes written"
1165        );
1166
1167        fs.close().await.expect("close failed");
1168    }
1169
1170    #[fuchsia::test]
1171    async fn test_temporary_file_create_and_delete_does_not_force_major_compaction() {
1172        let device = DeviceHolder::new(FakeDevice::new(65536, 512));
1173        let fs = FxFilesystemBuilder::new()
1174            .journal_options(JournalOptions { reclaim_size: 32768, ..Default::default() })
1175            .format(true)
1176            .open(device)
1177            .await
1178            .expect("open failed");
1179        let root_volume = root_volume(fs.clone()).await.expect("root_volume failed");
1180        let store = root_volume
1181            .new_volume("test", NewChildStoreOptions::default())
1182            .await
1183            .expect("new_volume failed");
1184        let store_id = store.store_object_id();
1185        let root_dir =
1186            Directory::open(&store, store.root_directory_object_id()).await.expect("open failed");
1187
1188        // 1. Populate the store with old files so that its base layer exceeds 512 KiB
1189        // (`DEFAULT_RECLAIM_SIZE`).
1190        for i in 0..2000 {
1191            let filename = format!("base_{i:04}_{:<200}", i);
1192            let mut transaction = store
1193                .new_transaction(
1194                    lock_keys![LockKey::object(store_id, root_dir.object_id())],
1195                    Options::default(),
1196                )
1197                .await
1198                .expect("new_transaction failed");
1199            root_dir
1200                .create_child_file(&mut transaction, &filename)
1201                .await
1202                .expect("create_child_file failed");
1203            transaction.commit().await.expect("commit failed");
1204        }
1205        fs.journal().force_compact().await.expect("force_compact failed");
1206        store
1207            .flush_with_reason(FlushReason::Journal(ForceMajor::True))
1208            .await
1209            .expect("major flush failed");
1210        fs.journal().force_compact().await.expect("force_compact failed");
1211
1212        let (base_layer_object_id, base_layer_len) = {
1213            let layers = store.tree().immutable_layer_set().layers;
1214            assert_eq!(layers.len(), 1);
1215            assert!(layers[0].handle().unwrap().get_size() > 512 * 1024);
1216            (layers[0].handle().unwrap().object_id(), layers[0].len())
1217        };
1218
1219        // 2. Repeatedly create temporary files and delete/tombstone them across `NUM_TEMP_FILES`
1220        // iterations, forcing minor compactions between creation and deletion periodically so that
1221        // the creation records and tombstones span separate minor layers before merging.
1222        const NUM_TEMP_FILES: usize = 200;
1223        const MINOR_FLUSH_INTERVAL: usize = 25;
1224        let graveyard_dir_id = store.graveyard_directory_object_id();
1225        let mut tombstone_keys = Vec::with_capacity(NUM_TEMP_FILES * 3);
1226        for i in 0..NUM_TEMP_FILES {
1227            let filename = format!("tmp_{i:04}");
1228            let oid = {
1229                let mut transaction = store
1230                    .new_transaction(
1231                        lock_keys![LockKey::object(store_id, root_dir.object_id())],
1232                        Options::default(),
1233                    )
1234                    .await
1235                    .expect("new_transaction failed");
1236                let file = root_dir
1237                    .create_child_file(&mut transaction, &filename)
1238                    .await
1239                    .expect("create_child_file failed");
1240                transaction.commit().await.expect("commit failed");
1241                file.object_id()
1242            };
1243
1244            // Periodically flush right after creating the file so its live records are written to
1245            // a persistent minor layer before it is deleted. That forces subsequent minor
1246            // compactions to merge creation and tombstone records across separate persistent minor
1247            // layers, while the remaining iterations exercise creation and deletion within the same
1248            // mutable layer between journal-triggered compactions.
1249            if i % MINOR_FLUSH_INTERVAL == 0 {
1250                store
1251                    .flush_with_reason(FlushReason::Journal(ForceMajor::False))
1252                    .await
1253                    .expect("minor flush failed");
1254            }
1255
1256            {
1257                let mut context = root_dir
1258                    .acquire_context_for_replace(None, filename.as_str(), true)
1259                    .await
1260                    .expect("acquire_context failed");
1261                replace_child(&mut context.transaction, None, (&root_dir, filename.as_str()))
1262                    .await
1263                    .expect("replace_child failed");
1264                context.transaction.commit().await.expect("commit failed");
1265            }
1266            fs.graveyard().queue_tombstone_object(store_id, oid);
1267            fs.graveyard().flush().await;
1268
1269            tombstone_keys.push(ObjectKey::object(oid));
1270            tombstone_keys.push(ObjectKey::child(root_dir.object_id(), &filename, DirType::Normal));
1271            tombstone_keys.push(ObjectKey::graveyard_entry(graveyard_dir_id, oid));
1272        }
1273        fs.journal().force_compact().await.expect("force_compact failed");
1274
1275        // Almost all temporary file records and tombstones should have been eliminated during minor
1276        // compactions via the base layer's bloom filter without ever rewriting the > 512 KiB base
1277        // layer.
1278        {
1279            let layers = store.tree().immutable_layer_set().layers;
1280            assert_eq!(layers.len(), 2);
1281
1282            // Every compaction allocates a fresh layer object ID and moves old layers to the
1283            // graveyard, so matching `base_layer_object_id` proves no major compaction occurred.
1284            assert_eq!(layers[1].handle().unwrap().object_id(), base_layer_object_id);
1285            assert_eq!(layers[1].len(), base_layer_len);
1286
1287            // Without minor-compaction tombstone elision, `layers[0]` would contain
1288            // `1 + 3 * NUM_TEMP_FILES` (601) items (`root_dir` plus `Object`, `Child`, and
1289            // `GraveyardEntry` tombstones for all 200 deleted files). With tombstone elision,
1290            // `layers[0]` retains only `root_dir` plus whichever tombstones hit a false positive in
1291            // `layers[1]`'s bloom filter.
1292            let expected_false_positives = tombstone_keys
1293                .iter()
1294                .filter(|key| layers[1].maybe_contains_key(key) != MaybeContainsKey::False)
1295                .count();
1296            assert_eq!(layers[0].len(), 1 + expected_false_positives);
1297
1298            // `layers[1]`'s bloom filter targets a 0.1% false-positive rate (`TARGET_FP_RATE =
1299            // 0.001`), so across 600 tombstone keys the expected false-positive count is < 0.6
1300            // (and P(> 15) < 1e-19), compared to 601 items without tombstone elision.
1301            assert!(
1302                layers[0].len() <= 16,
1303                "unexpectedly many items in minor layer: {}",
1304                layers[0].len()
1305            );
1306
1307            let mut iter = layers[0].seek(Bound::Unbounded).await.expect("seek failed");
1308            let mut saw_root_dir = false;
1309            let mut count = 0;
1310            while let Some(item) = iter.get() {
1311                count += 1;
1312                if item.key == &ObjectKey::object(root_dir.object_id()) {
1313                    saw_root_dir = true;
1314                } else {
1315                    // Any other surviving item in the minor layer must be a tombstone kept due to a
1316                    // bloom filter false positive against the base layer.
1317                    assert!(
1318                        matches!(
1319                            item.value,
1320                            ObjectValue::None | ObjectValue::Extent(ExtentValue::None)
1321                        ),
1322                        "unexpected non-tombstone item in minor layer: {item:?}"
1323                    );
1324                    assert_ne!(
1325                        layers[1].maybe_contains_key(item.key),
1326                        MaybeContainsKey::False,
1327                        "tombstone survived despite base layer bloom filter returning False: \
1328                         {item:?}"
1329                    );
1330                }
1331                iter.advance().await.expect("advance failed");
1332            }
1333            assert!(saw_root_dir, "expected root_dir item in minor layer");
1334            assert_eq!(count, layers[0].len());
1335        }
1336
1337        fs.close().await.expect("close failed");
1338    }
1339
1340    #[fuchsia::test]
1341    async fn test_minor_compaction_extent_tombstones() {
1342        const MIB: u64 = 1024 * 1024;
1343        let device = DeviceHolder::new(FakeDevice::new(16384, 4096));
1344        let fs = FxFilesystem::new_empty(device).await.expect("new_empty failed");
1345        let root_volume = root_volume(fs.clone()).await.expect("root_volume failed");
1346        let store = root_volume
1347            .new_volume("test", NewChildStoreOptions::default())
1348            .await
1349            .expect("new_volume failed");
1350        let store_id = store.store_object_id();
1351        let root_dir =
1352            Directory::open(&store, store.root_directory_object_id()).await.expect("open failed");
1353
1354        // 1. Populate the store with 2000 files so its base layer exceeds 512 KiB
1355        // (`DEFAULT_RECLAIM_SIZE`), plus four files exercising multi-MiB extent ranges across the
1356        // 1 MiB fuzzy-hash buckets (`EXTENT_HASH_BUCKET_SIZE`) and `MAX_HASH_PARTITIONS` (4):
1357        //   - `large_base_file`: single contiguous maximum-sized (`1.75 MiB` for 4 KiB blocks)
1358        //     extent at `512 KiB..2.25 MiB`, spanning 3 buckets (`[0..1M)`, `[1..2M)`, `[2..3M)`).
1359        //   - `tail_4m_file`: 4 KiB extent only in the 4th bucket (`3.5 MiB..3.5 MiB + 4 KiB`).
1360        //   - `tail_8m_file`: 4 KiB extent only in the 8th bucket (`7.5 MiB..7.5 MiB + 4 KiB`).
1361        //   - `prefix_base_file`: 64 KiB extent only in the 1st bucket (`0..64 KiB`).
1362        for i in 0..2000 {
1363            let filename = format!("base_{i:04}_{:<200}", i);
1364            let mut transaction = store
1365                .new_transaction(
1366                    lock_keys![LockKey::object(store_id, root_dir.object_id())],
1367                    Options::default(),
1368                )
1369                .await
1370                .expect("new_transaction failed");
1371            root_dir
1372                .create_child_file(&mut transaction, &filename)
1373                .await
1374                .expect("create_child_file failed");
1375            transaction.commit().await.expect("commit failed");
1376        }
1377
1378        let create_file = |name: &'static str| {
1379            let store = &store;
1380            let root_dir = &root_dir;
1381            async move {
1382                let mut transaction = store
1383                    .new_transaction(
1384                        lock_keys![LockKey::object(store_id, root_dir.object_id())],
1385                        Options::default(),
1386                    )
1387                    .await
1388                    .expect("new_transaction failed");
1389                let file = root_dir
1390                    .create_child_file(&mut transaction, name)
1391                    .await
1392                    .expect("create_child_file failed");
1393                transaction.commit().await.expect("commit failed");
1394                file
1395            }
1396        };
1397
1398        let large_base_len = max_extent_size_for_block_size(fs.block_size());
1399        assert_eq!(large_base_len, 1792 * 1024);
1400        let large_base_start = 512 * 1024;
1401        let large_base_end = large_base_start + large_base_len;
1402        let large_base_file = create_file("large_base_file").await;
1403        {
1404            let mut buf = large_base_file.allocate_buffer(large_base_len as usize).await;
1405            buf.fill(0xaa);
1406            large_base_file
1407                .write_or_append(Some(large_base_start), buf.as_ref())
1408                .await
1409                .expect("write_or_append failed");
1410        }
1411
1412        let tail_4m_offset = 3 * MIB + 512 * 1024;
1413        let tail_4m_file = create_file("tail_4m_file").await;
1414        {
1415            let mut buf = tail_4m_file.allocate_buffer(4096).await;
1416            buf.fill(0xbb);
1417            tail_4m_file
1418                .write_or_append(Some(tail_4m_offset), buf.as_ref())
1419                .await
1420                .expect("write_or_append failed");
1421        }
1422
1423        let tail_8m_offset = 7 * MIB + 512 * 1024;
1424        let tail_8m_file = create_file("tail_8m_file").await;
1425        {
1426            let mut buf = tail_8m_file.allocate_buffer(4096).await;
1427            buf.fill(0xcc);
1428            tail_8m_file
1429                .write_or_append(Some(tail_8m_offset), buf.as_ref())
1430                .await
1431                .expect("write_or_append failed");
1432        }
1433
1434        let prefix_base_file = create_file("prefix_base_file").await;
1435        {
1436            let mut buf = prefix_base_file.allocate_buffer(64 * 1024).await;
1437            buf.fill(0xdd);
1438            prefix_base_file
1439                .write_or_append(Some(0), buf.as_ref())
1440                .await
1441                .expect("write_or_append failed");
1442        }
1443
1444        store
1445            .flush_with_reason(FlushReason::Journal(ForceMajor::True))
1446            .await
1447            .expect("major flush failed");
1448        let base_layer_object_id = {
1449            let layers = store.tree().immutable_layer_set().layers;
1450            assert_eq!(layers.len(), 1);
1451            assert!(layers[0].handle().unwrap().get_size() > 512 * 1024);
1452
1453            // Confirm `large_base_file`'s 1.75 MiB allocation is stored as a single contiguous
1454            // extent record spanning 3 fuzzy-hash buckets (`[0..1M)`, `[1..2M)`, `[2..3M)`) in the
1455            // base layer.
1456            let expected_key = ObjectKey::extent(
1457                large_base_file.object_id(),
1458                large_base_file.attribute_id(),
1459                large_base_start..large_base_end,
1460            );
1461            let iter =
1462                layers[0].seek(Bound::Included(&expected_key)).await.expect("base seek failed");
1463            assert_eq!(iter.get().map(|item| item.key), Some(&expected_key));
1464
1465            layers[0].handle().unwrap().object_id()
1466        };
1467
1468        // 2. Write new multi-MiB extents in ranges that have no records in the base layer (`1..4
1469        // MiB` on `prefix_base_file` and `0..4 MiB` on a newly created `temp_extent_file`), and
1470        // flush them into a persistent minor layer.
1471        {
1472            let mut buf = prefix_base_file.allocate_buffer((3 * MIB) as usize).await;
1473            buf.fill(0xee);
1474            prefix_base_file
1475                .write_or_append(Some(MIB), buf.as_ref())
1476                .await
1477                .expect("write_or_append failed");
1478        }
1479        let temp_extent_file = create_file("temp_extent_file").await;
1480        {
1481            let mut buf = temp_extent_file.allocate_buffer((4 * MIB) as usize).await;
1482            buf.fill(0xff);
1483            temp_extent_file
1484                .write_or_append(Some(0), buf.as_ref())
1485                .await
1486                .expect("write_or_append failed");
1487        }
1488        store
1489            .flush_with_reason(FlushReason::Journal(ForceMajor::False))
1490            .await
1491            .expect("first minor flush failed");
1492
1493        // 3. Delete/zero extent ranges across all five files and run another minor compaction:
1494        //   - On `large_base_file` (`512 KiB..2.25 MiB` in base): punch holes at the start in
1495        //     bucket 0 (`512..516 KiB`), strictly inside interior bucket 1 (`1.5 MiB..1.5 MiB +
1496        //     4 KiB`), straddling the bucket 1/2 boundary (`2 MiB - 4 KiB..2 MiB + 4 KiB`), and at
1497        //     the very end in bucket 2 (`2.25 MiB - 4 KiB..2.25 MiB`).
1498        //   - On `tail_4m_file`: zero `0..4 MiB` (4 buckets == `MAX_HASH_PARTITIONS`, overlapping
1499        //     the base extent only in the 4th bucket).
1500        //   - On `tail_8m_file`: zero `0..8 MiB` (8 buckets > `MAX_HASH_PARTITIONS`, returning
1501        //     `RangeKeyTooLarge` and overlapping the base extent only in the 8th bucket).
1502        //   - On `prefix_base_file`: zero `1..4 MiB` (buckets 1..4 are absent from the base layer,
1503        //     even though bucket 0 is present for `prefix_base_file`).
1504        //   - On `temp_extent_file`: truncate to 0 (emitting `0..4 MiB` `ExtentValue::None` across
1505        //     4 buckets, all absent from the base layer).
1506        let hole_start = large_base_start..(large_base_start + 4096);
1507        let hole_interior = (MIB + 512 * 1024)..(MIB + 512 * 1024 + 4096);
1508        let hole_boundary = (2 * MIB - 4096)..(2 * MIB + 4096);
1509        let hole_end = (large_base_end - 4096)..large_base_end;
1510        for hole in
1511            [hole_start.clone(), hole_interior.clone(), hole_boundary.clone(), hole_end.clone()]
1512        {
1513            let mut transaction =
1514                large_base_file.new_transaction().await.expect("new_transaction failed");
1515            large_base_file.zero(&mut transaction, hole).await.expect("zero failed");
1516            transaction.commit().await.expect("commit failed");
1517        }
1518        {
1519            let mut transaction =
1520                tail_4m_file.new_transaction().await.expect("new_transaction failed");
1521            tail_4m_file.zero(&mut transaction, 0..4 * MIB).await.expect("zero failed");
1522            transaction.commit().await.expect("commit failed");
1523        }
1524        {
1525            let mut transaction =
1526                tail_8m_file.new_transaction().await.expect("new_transaction failed");
1527            tail_8m_file.zero(&mut transaction, 0..8 * MIB).await.expect("zero failed");
1528            transaction.commit().await.expect("commit failed");
1529        }
1530        {
1531            let mut transaction =
1532                prefix_base_file.new_transaction().await.expect("new_transaction failed");
1533            prefix_base_file.zero(&mut transaction, MIB..4 * MIB).await.expect("zero failed");
1534            transaction.commit().await.expect("commit failed");
1535        }
1536        temp_extent_file.truncate(0).await.expect("truncate failed");
1537
1538        store
1539            .flush_with_reason(FlushReason::Journal(ForceMajor::False))
1540            .await
1541            .expect("second minor flush failed");
1542
1543        // 4. Verify `maybe_contains_key` behavior and surviving `ExtentValue::None` records in the
1544        // compacted minor layer (`layers[0]`), confirming no major compaction occurred.
1545        let must_keep_keys = [
1546            ObjectKey::extent(
1547                large_base_file.object_id(),
1548                large_base_file.attribute_id(),
1549                hole_start.clone(),
1550            ),
1551            ObjectKey::extent(
1552                large_base_file.object_id(),
1553                large_base_file.attribute_id(),
1554                hole_interior.clone(),
1555            ),
1556            ObjectKey::extent(
1557                large_base_file.object_id(),
1558                large_base_file.attribute_id(),
1559                hole_boundary.clone(),
1560            ),
1561            ObjectKey::extent(
1562                large_base_file.object_id(),
1563                large_base_file.attribute_id(),
1564                hole_end.clone(),
1565            ),
1566            ObjectKey::extent(tail_4m_file.object_id(), tail_4m_file.attribute_id(), 0..4 * MIB),
1567            ObjectKey::extent(tail_8m_file.object_id(), tail_8m_file.attribute_id(), 0..8 * MIB),
1568        ];
1569        let elidable_keys = [
1570            ObjectKey::extent(
1571                prefix_base_file.object_id(),
1572                prefix_base_file.attribute_id(),
1573                MIB..4 * MIB,
1574            ),
1575            ObjectKey::extent(
1576                temp_extent_file.object_id(),
1577                temp_extent_file.attribute_id(),
1578                0..4 * MIB,
1579            ),
1580        ];
1581
1582        {
1583            let layers = store.tree().immutable_layer_set().layers;
1584            assert_eq!(layers.len(), 2);
1585            assert_eq!(layers[1].handle().unwrap().object_id(), base_layer_object_id);
1586
1587            // All 5 `<= 4`-bucket overlapping keys must return `Maybe` against the base layer
1588            // (covering start, interior, boundary, and end buckets of `large_base_file` as well as
1589            // `0..4 MiB` hitting only in the 4th bucket of `tail_4m_file`).
1590            for key in &must_keep_keys[..5] {
1591                assert_eq!(layers[1].maybe_contains_key(key), MaybeContainsKey::Maybe, "{key:?}");
1592            }
1593
1594            // The 8-bucket key on `tail_8m_file` exceeds `MAX_HASH_PARTITIONS` (4) and must return
1595            // `RangeKeyTooLarge`.
1596            assert_eq!(
1597                layers[1].maybe_contains_key(&must_keep_keys[5]),
1598                MaybeContainsKey::RangeKeyTooLarge
1599            );
1600
1601            let mut minor_deleted_extents = HashSet::new();
1602            let mut iter = layers[0].seek(Bound::Unbounded).await.expect("seek failed");
1603            while let Some(item) = iter.get() {
1604                if matches!(item.value, ObjectValue::Extent(ExtentValue::None)) {
1605                    minor_deleted_extents.insert(item.key.clone());
1606                }
1607                iter.advance().await.expect("advance failed");
1608            }
1609
1610            for key in &must_keep_keys {
1611                assert!(
1612                    minor_deleted_extents.contains(key),
1613                    "expected overlapping/large extent tombstone to be kept: {key:?}"
1614                );
1615            }
1616            for key in &elidable_keys {
1617                let expected_kept = layers[1].maybe_contains_key(key) != MaybeContainsKey::False;
1618                assert_eq!(
1619                    minor_deleted_extents.contains(key),
1620                    expected_kept,
1621                    "unexpected retention state for non-overlapping extent tombstone: {key:?}"
1622                );
1623            }
1624        }
1625
1626        // 5. Read back file contents to ensure none of the zeroed base-layer ranges were
1627        // resurrected and live base-layer data remains intact.
1628        let mut buf_4k = large_base_file.allocate_buffer(4096).await;
1629        let mut buf_8k = large_base_file.allocate_buffer(8192).await;
1630
1631        assert_eq!(
1632            large_base_file
1633                .read_aligned(hole_start.start, buf_4k.as_mut())
1634                .await
1635                .expect("read_aligned failed"),
1636            4096
1637        );
1638        assert_eq!(buf_4k.as_ptr_slice().to_vec(), vec![0u8; 4096]);
1639        assert_eq!(
1640            large_base_file
1641                .read_aligned(hole_start.end, buf_4k.as_mut())
1642                .await
1643                .expect("read_aligned failed"),
1644            4096
1645        );
1646        assert_eq!(buf_4k.as_ptr_slice().to_vec(), vec![0xaa; 4096]);
1647
1648        assert_eq!(
1649            large_base_file
1650                .read_aligned(hole_interior.start - 4096, buf_4k.as_mut())
1651                .await
1652                .expect("read_aligned failed"),
1653            4096
1654        );
1655        assert_eq!(buf_4k.as_ptr_slice().to_vec(), vec![0xaa; 4096]);
1656        assert_eq!(
1657            large_base_file
1658                .read_aligned(hole_interior.start, buf_4k.as_mut())
1659                .await
1660                .expect("read_aligned failed"),
1661            4096
1662        );
1663        assert_eq!(buf_4k.as_ptr_slice().to_vec(), vec![0u8; 4096]);
1664        assert_eq!(
1665            large_base_file
1666                .read_aligned(hole_interior.end, buf_4k.as_mut())
1667                .await
1668                .expect("read_aligned failed"),
1669            4096
1670        );
1671        assert_eq!(buf_4k.as_ptr_slice().to_vec(), vec![0xaa; 4096]);
1672
1673        assert_eq!(
1674            large_base_file
1675                .read_aligned(hole_boundary.start - 4096, buf_4k.as_mut())
1676                .await
1677                .expect("read_aligned failed"),
1678            4096
1679        );
1680        assert_eq!(buf_4k.as_ptr_slice().to_vec(), vec![0xaa; 4096]);
1681        assert_eq!(
1682            large_base_file
1683                .read_aligned(hole_boundary.start, buf_8k.as_mut())
1684                .await
1685                .expect("read_aligned failed"),
1686            8192
1687        );
1688        assert_eq!(buf_8k.as_ptr_slice().to_vec(), vec![0u8; 8192]);
1689        assert_eq!(
1690            large_base_file
1691                .read_aligned(hole_boundary.end, buf_4k.as_mut())
1692                .await
1693                .expect("read_aligned failed"),
1694            4096
1695        );
1696        assert_eq!(buf_4k.as_ptr_slice().to_vec(), vec![0xaa; 4096]);
1697
1698        assert_eq!(
1699            large_base_file
1700                .read_aligned(hole_end.start - 4096, buf_4k.as_mut())
1701                .await
1702                .expect("read_aligned failed"),
1703            4096
1704        );
1705        assert_eq!(buf_4k.as_ptr_slice().to_vec(), vec![0xaa; 4096]);
1706        assert_eq!(
1707            large_base_file
1708                .read_aligned(hole_end.start, buf_4k.as_mut())
1709                .await
1710                .expect("read_aligned failed"),
1711            4096
1712        );
1713        assert_eq!(buf_4k.as_ptr_slice().to_vec(), vec![0u8; 4096]);
1714
1715        assert_eq!(
1716            tail_4m_file
1717                .read_aligned(tail_4m_offset, buf_4k.as_mut())
1718                .await
1719                .expect("read_aligned failed"),
1720            4096
1721        );
1722        assert_eq!(buf_4k.as_ptr_slice().to_vec(), vec![0u8; 4096]);
1723
1724        assert_eq!(
1725            tail_8m_file
1726                .read_aligned(tail_8m_offset, buf_4k.as_mut())
1727                .await
1728                .expect("read_aligned failed"),
1729            4096
1730        );
1731        assert_eq!(buf_4k.as_ptr_slice().to_vec(), vec![0u8; 4096]);
1732
1733        let mut buf_64k = prefix_base_file.allocate_buffer(64 * 1024).await;
1734        assert_eq!(
1735            prefix_base_file.read_aligned(0, buf_64k.as_mut()).await.expect("read_aligned failed"),
1736            64 * 1024
1737        );
1738        assert_eq!(buf_64k.as_ptr_slice().to_vec(), vec![0xdd; 64 * 1024]);
1739        assert_eq!(
1740            prefix_base_file.read_aligned(MIB, buf_4k.as_mut()).await.expect("read_aligned failed"),
1741            4096
1742        );
1743        assert_eq!(buf_4k.as_ptr_slice().to_vec(), vec![0u8; 4096]);
1744        assert_eq!(temp_extent_file.get_size(), 0);
1745
1746        fs.close().await.expect("close failed");
1747    }
1748
1749    #[fuchsia::test]
1750    async fn test_mixed_deletes_and_creates_reclaim_space_via_major_compaction() {
1751        let device = DeviceHolder::new(FakeDevice::new(16384, 512));
1752        let fs = FxFilesystemBuilder::new()
1753            .journal_options(JournalOptions { reclaim_size: 32768, ..Default::default() })
1754            .format(true)
1755            .open(device)
1756            .await
1757            .expect("open failed");
1758        let root_volume = root_volume(fs.clone()).await.expect("root_volume failed");
1759        let store = root_volume
1760            .new_volume("test", NewChildStoreOptions::default())
1761            .await
1762            .expect("new_volume failed");
1763        let store_id = store.store_object_id();
1764        let root_dir =
1765            Directory::open(&store, store.root_directory_object_id()).await.expect("open failed");
1766
1767        // 1. Populate the store with 2000 files so its base layer exceeds 512 KiB.
1768        let num_files = 2000;
1769        let mut file_ids = Vec::with_capacity(num_files);
1770        for i in 0..num_files {
1771            let filename = format!("file_{i:04}_{:<200}", i);
1772            let mut transaction = store
1773                .new_transaction(
1774                    lock_keys![LockKey::object(store_id, root_dir.object_id())],
1775                    Options::default(),
1776                )
1777                .await
1778                .expect("new_transaction failed");
1779            let file = root_dir
1780                .create_child_file(&mut transaction, &filename)
1781                .await
1782                .expect("create_child_file failed");
1783            transaction.commit().await.expect("commit failed");
1784            file_ids.push((file.object_id(), filename));
1785        }
1786        fs.journal().force_compact().await.expect("force_compact failed");
1787        fs.journal().force_compact().await.expect("force_compact failed");
1788
1789        // Create a filler file, fill the disk, and then truncate 512 KiB off `filler` so the
1790        // filesystem has ~512 KiB of free space headroom.
1791        let filler = {
1792            let mut transaction = store
1793                .new_transaction(
1794                    lock_keys![LockKey::object(store_id, root_dir.object_id())],
1795                    Options::default(),
1796                )
1797                .await
1798                .expect("new_transaction failed");
1799            let file = root_dir
1800                .create_child_file(&mut transaction, "filler")
1801                .await
1802                .expect("create_child_file failed");
1803            transaction.commit().await.expect("commit failed");
1804            file
1805        };
1806        let chunk = filler.allocate_buffer(65536).await;
1807        let mut offset = 0;
1808        while filler.write_or_append(Some(offset), chunk.as_ref()).await.is_ok() {
1809            offset += 65536;
1810        }
1811        filler.truncate(offset - 512 * 1024).await.expect("truncate filler failed");
1812        fs.journal().force_compact().await.expect("force_compact failed");
1813
1814        // 2. Repeatedly delete an old file and create a new file 1500 times (keeping the live file
1815        // count constant at 2000). Because the deleted files reside in the base layer, major
1816        // compaction must run periodically to purge the dead base-layer records and minor-layer
1817        // tombstones; otherwise the accumulating minor layers and their metadata reservations will
1818        // exhaust the 512 KiB of free space and fail with `NoSpace`.
1819        for (i, (oid, filename)) in file_ids[0..1500].iter().enumerate() {
1820            {
1821                let mut context = root_dir
1822                    .acquire_context_for_replace(None, filename.as_str(), true)
1823                    .await
1824                    .expect("acquire_context failed");
1825                replace_child(&mut context.transaction, None, (&root_dir, filename.as_str()))
1826                    .await
1827                    .expect("replace_child failed");
1828                context.transaction.commit().await.expect("commit failed");
1829            }
1830            fs.graveyard().queue_tombstone_object(store_id, *oid);
1831            fs.graveyard().flush().await;
1832
1833            let new_filename = format!("new_{i:04}_{:<200}", i);
1834            let mut transaction = store
1835                .new_transaction(
1836                    lock_keys![LockKey::object(store_id, root_dir.object_id())],
1837                    Options::default(),
1838                )
1839                .await
1840                .expect("new_transaction failed");
1841            root_dir
1842                .create_child_file(&mut transaction, &new_filename)
1843                .await
1844                .expect("create_child_file failed");
1845            transaction.commit().await.expect("commit failed");
1846        }
1847
1848        fs.close().await.expect("close failed");
1849    }
1850}
1851
1852impl tree::MajorCompactable<ObjectKey, ObjectValue> for LSMTree<ObjectKey, ObjectValue> {
1853    async fn major_iter(
1854        iter: impl LayerIterator<ObjectKey, ObjectValue>,
1855    ) -> Result<impl LayerIterator<ObjectKey, ObjectValue>, Error> {
1856        iter.filter(|item: ItemRef<'_, _, _>| match item {
1857            // Object Tombstone.
1858            ItemRef { value: ObjectValue::None, .. } => false,
1859            // Deleted extent.
1860            ItemRef { value: ObjectValue::Extent(ExtentValue::None), .. } => false,
1861            _ => true,
1862        })
1863        .await
1864    }
1865
1866    async fn minor_iter(
1867        iter: impl LayerIterator<ObjectKey, ObjectValue>,
1868        layers_to_keep: impl AsRef<[LockedLayer<ObjectKey, ObjectValue>]> + Send + Sync,
1869    ) -> Result<impl LayerIterator<ObjectKey, ObjectValue>, Error> {
1870        iter.filter(move |item: ItemRef<'_, _, _>| match item {
1871            // Object Tombstone or deleted extent: only keep if a kept layer might contain the key.
1872            ItemRef { key, value: ObjectValue::None | ObjectValue::Extent(ExtentValue::None) } => {
1873                layers_to_keep
1874                    .as_ref()
1875                    .iter()
1876                    .any(|layer| layer.maybe_contains_key(key) != MaybeContainsKey::False)
1877            }
1878            _ => true,
1879        })
1880        .await
1881    }
1882}