Skip to main content

fxfs/object_store/
object_manager.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//! # Metadata Reservations in Fxfs
6//!
7//! Fxfs maintains a metadata reservation (`M_total`) in the allocator so that space-freeing
8//! operations and journal compactions can proceed even when the filesystem has zero free space:
9//! - **Funded transactions**: Normal transactions reserve metadata space from the allocator up
10//!   front (`4x` their journal byte size) and transfer that reservation into `M_total` on commit.
11//! - **Borrowing transactions**: Operations that free space (such as unlinking files, purging
12//!   graveyard tombstones, or trimming extents) must succeed even when the disk is full, so they
13//!   borrow from `M_total` (`Borrowed`) instead of reserving
14//!   from the allocator.
15//! - **Minor vs. Major Compaction**: Flushing an object store normally performs a *minor
16//!   compaction*, writing recent journal mutations (including deletion tombstones) into a new small
17//!   layer file stacked on top of existing immutable layers. Minor compactions *consume* blocks
18//!   from `M_total` to write the new layer file and do not free older layers. Borrowed metadata
19//!   space is only reclaimed when a store performs a *major compaction*, which merges all layers
20//!   into a single base layer, drops all tombstones, and deletes the old layer files. To prevent
21//!   repeated minor compactions from draining `M_total`, Fxfs forces a major compaction once
22//!   borrowed space reaches `50%` of the available borrowing headroom.
23//!
24//! ## Why Per-Store Borrowing Tracking Is Required (and the Locked-Store Problem)
25//!
26//! Encrypted volumes can be locked (`ObjectStore::lock`), dropping their decryption keys from
27//! memory. If a locked store has unflushed journal mutations when the journal compacts,
28//! `ObjectStore::flush_locked` appends them to a sidecar `encrypted_mutations` file in
29//! `root_store`; critically, a locked store cannot decrypt and merge its layers to perform a major
30//! compaction until it is unlocked again.
31//!
32//! If borrowed space and major-compaction thresholds were tracked as a single global scalar across
33//! the entire filesystem (`borrowed = R_total - M_total >= R_total / 2`, as Fxfs did prior to V61),
34//! a locked store could tie up the filesystem's metadata space in two ways:
35//! 1. **Consuming other stores' headroom before locking**: Because a global `R_total / 2` threshold
36//!    includes `journal::RESERVED_SPACE` plus `1.5 * L` for `root_store`, the `allocator`, and
37//!    *all* child volumes, a single volume `S_locked` could perform many borrowing deletions and
38//!    minor compactions while unlocked without ever crossing global `R_total / 2`, borrowing far
39//!    more than its own `0.5 * L` headroom and eating into the reservations of other volumes and
40//!    `root_store`. If `S_locked` was then locked, all of that borrowed space remained frozen in
41//!    its unmerged layers while locked.
42//! 2. **Starving other stores and forcing endless major compactions**: With `S_locked` tying up
43//!    most of global `R_total / 2`, any small borrow by an active unlocked store `S_active` or
44//!    `root_store` pushed global `borrowed` past `R_total / 2`. Every subsequent journal flush then
45//!    forced a major compaction on *every* unlocked store (`S_active` and `root_store`) even when
46//!    they had no tombstones to reclaim, while `S_locked` (the store actually holding the debt) was
47//!    locked and skipped (or worse, exhausted `M_total` altogether).
48//!
49//! Assigning each child store `S` its own borrowing budget `req_S` and borrowed counter `B_S`
50//! prevents both failure modes:
51//! - While `S_locked` is unlocked, it is forced to major-compact as soon as its own borrowing
52//!   reaches `B_S >= req_S / 2` (resetting `B_S = 0` and freeing its tombstone layers), regardless
53//!   of how large other stores are. When `S_locked` is locked, the uncompacted debt it can hold is
54//!   strictly bounded by its own `req_S` share (which `S_locked` itself funded when its layers were
55//!   created) and can never dip into `req_other` or `req_sys`.
56//! - While `S_locked` is locked, each unlocked store `S_active` is evaluated solely against its own
57//!   `B_{S_active} >= req_{S_active} / 2`, `root_store` subtracts `sum(B_S)` when computing system
58//!   borrowed space, and `S_locked` is ignored by compactions until unlocked.
59//!
60//! ## Reservation Model
61//!
62//! Each LSM tree (the allocator, the root store, and each child volume) with total layer size `L`
63//! has a target reservation budget of `1.5 * L`, which decomposes into:
64//! - **Compaction scratch space (`1.0 * L`)**: Worst-case temporary space needed to write a new
65//!   compacted layer file before tombstoning the old layer files. Because only one tree is
66//!   compacted at a time, this scratch space is shared across all trees (`max(L)` across all
67//!   trees).
68//! - **Borrowing and system headroom (`0.5 * L`)**: Space that can be borrowed across compactions
69//!   (e.g. unmerged minor layers of deletion tombstones, or `encrypted_mutations` in a locked
70//!   volume) before a major compaction is required to reclaim it.
71//!
72//! To isolate borrowing between volumes while sharing compaction scratch space, `ObjectManager`
73//! computes required reservations as:
74//!
75//! 1. **System Requirement (`req_sys`)**: Covers shared compaction scratch space across all trees
76//!    (`max(L)`), the allocator and root store headroom (`0.5 * (L_alloc + L_root)`), a scaling
77//!    share of child stores (`1/8 * sum(L_child)` for `root_parent_store`, `root_store`, and
78//!    `allocator` records of the child stores' layer files themselves; if extreme layer file
79//!    fragmentation ever exceeded this share, `R_total` is unaffected and `root_store` would simply
80//!    major-compact slightly earlier), unflushed system journal usage (`4 * J_sys`), and
81//!    `journal::RESERVED_SPACE`.
82//! 2. **Per-Child-Store Requirement (`req_S`)**: For each child volume `S`, covers that volume's
83//!    borrowing headroom (`3/8 * L_S`) and its unflushed journal mutations (`4 * J_S`).
84//! 3. **Total Required Reservation (`R_total`)**: `req_sys + sum(req_S)`.
85//!
86//! Rather than partitioning the physical reservation `M_total` (`metadata_reservation.amount()`)
87//! into per-store balances (which would require scaling per-store balances down whenever `M_total`
88//! has a shortfall and could cause repeated major compactions on subsequent transactions),
89//! `ObjectManager` tracks:
90//! - **`M_total`**: The total physical bytes currently held in `metadata_reservation`.
91//! - **`store_borrowed[S]`**: The metadata space borrowed by each child store `S`'s flushed
92//!   non-base layers (or `encrypted_mutations`), while `journal_checkpoints[S]` tracks unflushed
93//!   journal borrowing before (`Old`) and after (`Current`) `BeginFlush`. Store `S`'s total
94//!   borrowed space is `B_S = store_borrowed[S] + 4 * journal_checkpoints[S].borrowed_usage()`.
95//!
96//! ## Accounting Lifecycle
97//!
98//! - **Transaction Commit**: Non-borrowing transactions (`New` / `Hold`) transfer `4 * txn_size`
99//!   into `M_total` and record `usage` in `journal_checkpoints[S]`. Borrowing transactions
100//!   (`Borrowed`) transfer nothing into `M_total` and record both `usage` and
101//!   `borrowed_usage` in `journal_checkpoints[S]` for each mutated child store `S`.
102//! - **Tree Compaction (`BeginFlush` & `EndFlush`)**: When `BeginFlush` is applied to child store
103//!   `S`, `journal_checkpoints[S]` moves `Current` into `Old` so any concurrent transactions
104//!   between `BeginFlush` and `EndFlush` accumulate into a fresh `Current` span. At
105//!   `EndFlush(borrowed)`, `Old` is removed (leaving only any post-`BeginFlush` borrowing in
106//!   `Current`) and `store_borrowed[S]` is set to `borrowed` (`0` for a full compaction, or the
107//!   updated flushed layer borrowing computed by `store_borrowed_for_flush` for a minor or locked
108//!   flush).
109//! - **Major Compaction**: Each child store `S` independently forces a major compaction when its
110//!   borrowed space exceeds half of its layer headroom (`B_S > h_S / 2`, where `h_S = 3/8 * L_S`)
111//!   and the store can be compacted (`needs_flush(S)` or `layers.len() > 1`). `root_store` forces
112//!   a major compaction when system borrowed space (`(R_total - M_total).saturating_sub(sum(B_S))`)
113//!   exceeds half of the system's non-scratch layer and journal headroom
114//!   (`0.5 * (L_alloc + L_root) + 1/8 * sum(L_child) + RESERVED_SPACE`). Because a full compaction
115//!   of `S` unconditionally resets `store_borrowed[S] = 0` and drops `Checkpoints::Old` even when
116//!   `M_total < R_total`, a single subsequent transaction on `S` will not trigger another major
117//!   compaction.
118//!
119//! ## Journal Replay and Software Upgrades
120//!
121//! The target reservation formulas (such as `journal::RESERVED_SPACE` or the layer/journal
122//! multipliers) can change between software versions, and during journal replay the full set of
123//! child volumes and their layer sizes are not yet known. If the super-block only recorded global
124//! borrowed space `B` and computed the mount-time reservation as `R_total - B`, any software update
125//! that increased `R_total` would try to reserve *more* space from the allocator at mount time than
126//! was actually set aside before the unmount or crash, which could fail with `NoSpace` on a nearly
127//! full filesystem.
128//!
129//! Instead, `SuperBlockHeader::reserved_metadata_space` records `M_total` at key `0` and each child
130//! store's flushed layer borrowing `store_borrowed[S]` at key `store_id` as of
131//! `super_block_journal_file_offset`, and the journal distinguishes non-borrowing (`Commit`) from
132//! borrowing (`CommitBorrowed(u64)`) transactions:
133//!
134//! 1. **Seeding from the Super-Block**: `init_replay_metadata_reservation` seeds `M_total` from key
135//!    `0` and `store_borrowed[S]` from each `store_id != 0`.
136//! 2. **Replaying Journal Transactions (`replay_mutations`)**:
137//!    - All mutations in `transactions` (which `Journal::read_transactions` already filtered to
138//!      those unflushed as of `super_block.journal_file_offsets`) run through
139//!      `Inner::apply_mutation` and `record_journal_usage`, taking `journal_checkpoints` and
140//!      `store_borrowed` through the same `BeginFlush` -> `Old` -> `EndFlush(borrowed)` state
141//!      transitions as live mode.
142//!    - For transactions after `super_block_journal_file_offset`:
143//!      - `Commit` (non-borrowing): Adds `4 * txn_size` to `M_total`.
144//!      - `CommitBorrowed(reserved)` (borrowing): Updates `M_total` to `reserved` (the
145//!        post-`apply_transaction` `metadata_reservation.amount()` recorded in live mode).
146//! 3. **Reconciling with Current Software Requirements (`init_metadata_reservation`)**: Once replay
147//!    completes and all child volumes are opened, `min(M_total, R_total)` is reserved from the
148//!    allocator (while `total_store_borrowed` clamps each `store_borrowed[S]` to `h_S` on read):
149//!    - **If required reservations increased** (`M_total < R_total`): Only `M_total` is reserved
150//!      from the allocator at mount time, never asking for extra space.
151//!    - **If required reservations decreased** (`M_total > R_total`): The reservation is capped
152//!      at `R_total`, leaving the excess unreserved in the allocator.
153//!    - **Legacy (< V61) migration**: Pre-V61 super-blocks only recorded global borrowed space `B`
154//!      computed against the legacy required reservation (`legacy_required = max(1.5 * L_i) + 4 *
155//!      J_total + reserved_space`), so `M_total = min(legacy_required - B, R_total)` is reserved
156//!      and any non-journal deficit is distributed proportionally across child stores' layer
157//!      headroom to seed `store_borrowed[S]`.
158
159use crate::errors::FxfsError;
160use crate::filesystem::{ApplyContext, ApplyMode, FlushReason, ForceMajor, JournalingObject};
161use crate::log::*;
162use crate::metrics;
163use crate::object_handle::{INVALID_OBJECT_ID, ReadObjectHandle};
164use crate::object_store::allocator::{Allocator, Reservation};
165use crate::object_store::directory::Directory;
166use crate::object_store::journal::{self, BorrowedMetadata, JournalCheckpoint};
167use crate::object_store::transaction::{
168    AssocObj, AssociatedObject, MetadataReservation, Mutation, ObjectMutationIterator, Transaction,
169    TxnMutation,
170};
171use crate::object_store::tree_cache::TreeCache;
172use crate::object_store::volume::{VOLUMES_DIRECTORY, list_volumes};
173use crate::object_store::{ObjectDescriptor, ObjectStore};
174use crate::round::round_div;
175use crate::serialized_types::{LATEST_VERSION, Version};
176use anyhow::{Context, Error, anyhow, bail, ensure};
177use fuchsia_inspect::{Property as _, UintProperty};
178use fuchsia_sync::RwLock;
179use futures::FutureExt as _;
180use rustc_hash::FxHashMap as HashMap;
181use std::cmp::Reverse;
182use std::collections::hash_map::Entry;
183use std::num::Saturating;
184use std::sync::{Arc, OnceLock};
185
186// Data written to the journal eventually needs to be flushed somewhere (typically into layer
187// files).  Here we conservatively assume that could take up to four times as much space as it does
188// in the journal.  In the layer file, it'll take up at least as much, but we must reserve the same
189// again that so that there's enough space for compactions, and then we need some spare for
190// overheads.
191//
192// TODO(https://fxbug.dev/42178158): We should come up with a better way of determining what the
193// multiplier should be here.  2x was too low, as it didn't cover any space for metadata.  4x might
194// be too much.
195pub const fn reserved_space_from_journal_usage(journal_usage: u64) -> u64 {
196    journal_usage * 4
197}
198
199/// Tracks the metadata reservation state during journal replay before `init_metadata_reservation`
200/// initializes the live `Reservation`.
201#[derive(Clone, Copy, Debug)]
202pub enum ReplayMetadataReservation {
203    /// Legacy super-blocks (< V61) track borrowed space.
204    Borrowed(u64),
205
206    /// V61+ super-blocks (and newly formatted filesystems, initialized to `u64::MAX`) track the
207    /// actual reserved metadata space.
208    Reserved(u64),
209}
210
211/// ObjectManager is a global loading cache for object stores and other special objects.
212pub struct ObjectManager {
213    inner: RwLock<Inner>,
214    metadata_reservation: OnceLock<Reservation>,
215    volume_directory: OnceLock<Directory<ObjectStore>>,
216    on_new_store: Option<Box<dyn Fn(&ObjectStore) + Send + Sync>>,
217}
218
219// Tracks the earliest unflushed journal checkpoint for an object alongside the journal bytes
220// written for that object since the checkpoint.
221#[derive(Clone, Debug)]
222struct CheckpointUsage {
223    checkpoint: JournalCheckpoint,
224
225    // Total journal bytes (excluding `BeginFlush`/`EndFlush`/`DeleteVolume`) written for this
226    // object in this checkpoint span.
227    usage: u64,
228
229    // Subset of `usage` written by transactions that borrowed metadata space (`CommitBorrowed` /
230    // `LegacyUpdateBorrowed`).
231    borrowed_usage: u64,
232}
233
234impl CheckpointUsage {
235    fn new(checkpoint: JournalCheckpoint) -> Self {
236        Self { checkpoint, usage: 0, borrowed_usage: 0 }
237    }
238}
239
240// Whilst we are flushing we need to keep track of the old checkpoint that we are hoping to flush,
241// and a new one that should apply if we successfully finish the flush.
242#[derive(Debug)]
243enum Checkpoints {
244    Current(CheckpointUsage),
245    Old(CheckpointUsage),
246    Both(/* old: */ CheckpointUsage, /* current: */ CheckpointUsage),
247}
248
249impl Checkpoints {
250    // Returns the earliest checkpoint (which will always be the old one if present).
251    fn earliest(&self) -> &JournalCheckpoint {
252        match self {
253            Checkpoints::Old(x) | Checkpoints::Both(x, _) | Checkpoints::Current(x) => {
254                &x.checkpoint
255            }
256        }
257    }
258
259    // Returns the total unflushed journal bytes for this object across both `Old` and `Current`
260    // checkpoint spans.
261    #[cfg(test)]
262    fn usage(&self) -> u64 {
263        match self {
264            Checkpoints::Current(x) | Checkpoints::Old(x) => {
265                debug_assert!(x.borrowed_usage <= x.usage);
266                x.usage
267            }
268            Checkpoints::Both(x, y) => {
269                debug_assert!(x.borrowed_usage <= x.usage);
270                debug_assert!(y.borrowed_usage <= y.usage);
271                x.usage.saturating_add(y.usage)
272            }
273        }
274    }
275
276    // Returns the unflushed journal bytes from borrowing transactions for this object.
277    fn borrowed_usage(&self) -> u64 {
278        match self {
279            Checkpoints::Current(x) | Checkpoints::Old(x) => {
280                debug_assert!(x.borrowed_usage <= x.usage);
281                x.borrowed_usage
282            }
283            Checkpoints::Both(x, y) => {
284                debug_assert!(x.borrowed_usage <= x.usage);
285                debug_assert!(y.borrowed_usage <= y.usage);
286                x.borrowed_usage.saturating_add(y.borrowed_usage)
287            }
288        }
289    }
290
291    // Adds `amount` of journal bytes to the active (`Current`) checkpoint span.
292    fn add_usage(&mut self, amount: u64, is_borrowed: bool) {
293        match self {
294            Checkpoints::Current(x) | Checkpoints::Both(_, x) => {
295                x.usage = x.usage.saturating_add(amount);
296                if is_borrowed {
297                    x.borrowed_usage = x.borrowed_usage.saturating_add(amount);
298                }
299                debug_assert!(x.borrowed_usage <= x.usage);
300            }
301
302            // Any non-flush mutation applied while in `Checkpoints::Old` transitions the state to
303            // `Checkpoints::Both` in `ObjectManager::apply_mutation` before `add_usage` is called,
304            // and flush mutations (`BeginFlush`/`EndFlush`) are excluded by
305            // `Mutation::counts_towards_object_journal_usage`.
306            Checkpoints::Old(_) => unreachable!(),
307        }
308    }
309}
310
311// We currently maintain strong references to all stores that have been opened, but there's no
312// currently no mechanism for releasing stores that aren't being used.
313struct Inner {
314    stores: HashMap<u64, Arc<ObjectStore>>,
315    root_parent_store_object_id: u64,
316    root_store_object_id: u64,
317    allocator_object_id: u64,
318    allocator: Option<Arc<Allocator>>,
319
320    // Records dependencies on the journal for objects i.e. an entry for object ID 1, would mean it
321    // has a dependency on journal records from that offset.
322    journal_checkpoints: HashMap<u64, Checkpoints>,
323
324    // Mappings from object-id to total tree layer size (including any encrypted mutations
325    // estimate).
326    layer_sizes: HashMap<u64, u64>,
327
328    // Mappings from child store object-id to the metadata space borrowed by that store's flushed
329    // non-base layers (or `encrypted_mutations`).
330    store_borrowed: HashMap<u64, u64>,
331
332    // The last journal end offset for a transaction that has been applied.  This is not necessarily
333    // the same as the start offset for the next transaction because of padding.
334    last_end_offset: u64,
335
336    // Tracks metadata reservation state during journal replay (and for read-only mounts).
337    replay_metadata_reservation: ReplayMetadataReservation,
338
339    // The maximum transaction size that has been encountered so far.
340    max_transaction_size: (u64, UintProperty),
341
342    // Extra temporary space that might be tied up in the journal that hasn't yet been deallocated.
343    reserved_space: u64,
344}
345
346impl Inner {
347    // Returns the overhead headroom above `layer_size` for a system tree (`root_store`,
348    // `root_parent_store`, or `allocator`), reserved to cover metadata growth between major
349    // compactions.
350    fn system_tree_headroom(layer_size: u64) -> u64 {
351        layer_size / 2
352    }
353
354    // Returns the temporary scratch space needed to write out a new compacted layer file before
355    // the old layers can be freed.
356    fn tree_scratch(layer_size: u64) -> u64 {
357        layer_size
358    }
359
360    // Returns the headroom attributed to the system bucket for a child store to cover allocator
361    // and `root_store` metadata mutations incurred when compacting the child store.
362    fn child_headroom_system_share(layer_size: u64) -> u64 {
363        layer_size / 8
364    }
365
366    // Returns the headroom attributed to a child store's own borrowing budget to cover growth of
367    // its own layer files between major compactions.
368    fn child_headroom_store_share(layer_size: u64) -> u64 {
369        layer_size * 3 / 8
370    }
371
372    // Returns the total headroom for `object_id` given its `layer_size`.
373    fn tree_headroom(&self, object_id: u64, layer_size: u64) -> u64 {
374        if self.is_child_store(object_id) {
375            Self::child_headroom_system_share(layer_size)
376                + Self::child_headroom_store_share(layer_size)
377        } else {
378            Self::system_tree_headroom(layer_size)
379        }
380    }
381
382    // Returns the total reservation for `object_id` given its `layer_size`.
383    fn tree_reservation(&self, object_id: u64, layer_size: u64) -> u64 {
384        Self::tree_scratch(layer_size) + self.tree_headroom(object_id, layer_size)
385    }
386
387    fn is_system_store(&self, store_id: u64) -> bool {
388        store_id == self.root_store_object_id || store_id == self.root_parent_store_object_id
389    }
390
391    fn is_child_store(&self, object_id: u64) -> bool {
392        !self.is_system_store(object_id) && object_id != self.allocator_object_id
393    }
394
395    fn earliest_journal_offset(&self) -> Option<u64> {
396        self.journal_checkpoints.values().map(|c| c.earliest().file_offset).min()
397    }
398
399    // Because only one tree is compacted at a time, a single scratch allocation equal to the
400    // largest tree suffices for any compaction across the filesystem.
401    fn max_tree_scratch(&self) -> u64 {
402        self.layer_sizes.values().copied().map(Self::tree_scratch).max().unwrap_or(0)
403    }
404
405    // Returns the system bucket's non-scratch borrowing headroom.
406    fn system_headroom(&self) -> u64 {
407        let mut headroom = self.reserved_space;
408        for (&object_id, &layer_size) in &self.layer_sizes {
409            if self.is_child_store(object_id) {
410                headroom += Self::child_headroom_system_share(layer_size);
411            } else {
412                headroom += Self::system_tree_headroom(layer_size);
413            }
414        }
415        headroom
416    }
417
418    // Returns the byte span of the journal that still contains unflushed mutations for any object.
419    fn total_journal_usage(&self) -> u64 {
420        self.earliest_journal_offset().map_or(0, |o| self.last_end_offset.saturating_sub(o))
421    }
422
423    // Returns the required reservation for child store `store_id`.
424    #[cfg(test)]
425    fn required_store_reservation(&self, store_id: u64) -> u64 {
426        let layer_size = self.layer_sizes.get(&store_id).copied().unwrap_or(0);
427        let journal_usage = self.journal_checkpoints.get(&store_id).map_or(0, |c| c.usage());
428        Self::child_headroom_store_share(layer_size)
429            + reserved_space_from_journal_usage(journal_usage)
430    }
431
432    // Returns the total metadata space (`B_S`) currently borrowed by child store `store_id` across
433    // its flushed non-base layers (`store_borrowed`, clamped to the store's layer headroom) and its
434    // unflushed journal checkpoints.
435    fn total_store_borrowed(&self, store_id: u64) -> u64 {
436        let max_layer_borrowed =
437            Self::child_headroom_store_share(self.layer_sizes.get(&store_id).copied().unwrap_or(0));
438        let layer_borrowed = std::cmp::min(
439            self.store_borrowed.get(&store_id).copied().unwrap_or(0),
440            max_layer_borrowed,
441        );
442        let journal_borrowed = reserved_space_from_journal_usage(
443            self.journal_checkpoints.get(&store_id).map_or(0, |c| c.borrowed_usage()),
444        );
445        layer_borrowed.saturating_add(journal_borrowed)
446    }
447
448    // Removes `store_borrowed` entries for stores that are no longer child stores in `self.stores`.
449    fn prune_stores_borrowed(&mut self) {
450        self.store_borrowed.retain(|&id, _| {
451            self.stores.contains_key(&id)
452                && id != self.root_store_object_id
453                && id != self.root_parent_store_object_id
454                && id != self.allocator_object_id
455        });
456    }
457
458    // Returns the total required size of the metadata reservation assuming that no space has been
459    // borrowed.
460    fn required_reservation(&self) -> u64 {
461        let mut child_headroom = 0;
462        for (&object_id, &layer_size) in &self.layer_sizes {
463            if self.is_child_store(object_id) {
464                child_headroom += Self::child_headroom_store_share(layer_size);
465            }
466        }
467        self.max_tree_scratch()
468            + self.system_headroom()
469            + child_headroom
470            + reserved_space_from_journal_usage(self.total_journal_usage())
471    }
472
473    // Returns the total borrowed metadata space, using the live reservation if initialized or
474    // `replay_metadata_reservation` during journal replay / read-only mounts.
475    fn borrowed_metadata_space(&self, metadata_reservation: Option<&Reservation>) -> u64 {
476        let required = self.required_reservation();
477        if let Some(reservation) = metadata_reservation {
478            required.saturating_sub(reservation.amount())
479        } else {
480            match self.replay_metadata_reservation {
481                ReplayMetadataReservation::Borrowed(borrowed) => borrowed,
482                ReplayMetadataReservation::Reserved(reserved) => required.saturating_sub(reserved),
483            }
484        }
485    }
486
487    // Updates per-store journal checkpoint usage.
488    fn record_journal_usage(&mut self, object_usages: &[(u64, u64)], is_borrowed: bool) {
489        for &(object_id, bytes) in object_usages {
490            if self.is_child_store(object_id)
491                && let Some(checkpoint) = self.journal_checkpoints.get_mut(&object_id)
492            {
493                checkpoint.add_usage(bytes, is_borrowed);
494            }
495        }
496    }
497
498    // Returns true if `object_id` has unflushed journal mutations or multiple immutable layers that
499    // can be merged by a major compaction.
500    fn can_compact(&self, object_id: u64) -> bool {
501        self.journal_checkpoints.contains_key(&object_id)
502            || self
503                .stores
504                .get(&object_id)
505                .is_some_and(|s| s.can_flush_unlocked() && s.tree().immutable_layer_count() > 1)
506    }
507
508    // Determines whether `object_id` should be forced to perform a major compaction during a
509    // journal flush.
510    fn should_force_major_compaction(&self, object_id: u64, total_reserved: u64) -> ForceMajor {
511        let over_threshold = if self.is_child_store(object_id) {
512            // Force a major compaction of child store `S` when its borrowed space `B_S` exceeds
513            // half of its layer headroom. Excluding journal usage ensures that unflushed journal
514            // writes do not inflate the threshold and allow `B_S` to exceed the store's layer
515            // headroom.
516            let b_s = self.total_store_borrowed(object_id);
517            let store_headroom = Self::child_headroom_store_share(
518                self.layer_sizes.get(&object_id).copied().unwrap_or(0),
519            );
520            b_s > store_headroom / 2
521        } else if object_id == self.root_store_object_id {
522            // Derive the system bucket's borrowed space and compare it against half of the system
523            // bucket's non-scratch borrowing headroom.
524            let total_borrowed = self.required_reservation().saturating_sub(total_reserved);
525            let store_borrowed_sum = self
526                .store_borrowed
527                .keys()
528                .fold(0u64, |acc, &id| acc.saturating_add(self.total_store_borrowed(id)));
529            let b_sys = total_borrowed.saturating_sub(store_borrowed_sum);
530            b_sys > self.system_headroom() / 2
531        } else {
532            false
533        };
534        if over_threshold && self.can_compact(object_id) {
535            ForceMajor::True
536        } else {
537            ForceMajor::False
538        }
539    }
540
541    fn journaling_object(&self, object_id: u64) -> Option<Arc<dyn JournalingObject>> {
542        if object_id == self.allocator_object_id {
543            Some(self.allocator.clone().unwrap() as Arc<dyn JournalingObject>)
544        } else {
545            self.stores.get(&object_id).map(|x| x.clone() as Arc<dyn JournalingObject>)
546        }
547    }
548
549    // Removes the pre-`BeginFlush` (`Old`) checkpoint span for `object_id`.
550    fn end_flush_checkpoints(&mut self, object_id: u64) {
551        if let Entry::Occupied(mut o) = self.journal_checkpoints.entry(object_id) {
552            match o.get_mut() {
553                Checkpoints::Old(_) => {
554                    o.remove();
555                }
556                Checkpoints::Both(_, current) => {
557                    *o.get_mut() = Checkpoints::Current(current.clone());
558                }
559                Checkpoints::Current(_) => {}
560            }
561        }
562    }
563
564    // Updates `ObjectManager`'s checkpoint and reservation state for `mutation`. Returns `false` if
565    // the mutation is `DeleteVolume` (which is handled entirely by `ObjectManager`).
566    fn apply_mutation(
567        &mut self,
568        object_id: u64,
569        mutation: &Mutation,
570        context: &ApplyContext<'_, '_>,
571    ) -> bool {
572        match mutation {
573            Mutation::BeginFlush => {
574                if let Some(entry) = self.journal_checkpoints.get_mut(&object_id) {
575                    match entry {
576                        Checkpoints::Current(x) => {
577                            *entry = Checkpoints::Old(x.clone());
578                        }
579
580                        // If a previous flush failed or was aborted before `EndFlush`, merge the
581                        // two checkpoint spans into `Old`.
582                        Checkpoints::Both(x, y) => {
583                            *entry = Checkpoints::Old(CheckpointUsage {
584                                checkpoint: x.checkpoint.clone(),
585                                usage: x.usage.saturating_add(y.usage),
586                                borrowed_usage: x.borrowed_usage.saturating_add(y.borrowed_usage),
587                            });
588                        }
589                        Checkpoints::Old(_) => {}
590                    }
591                }
592            }
593            Mutation::EndFlush(borrowed) => {
594                self.end_flush_checkpoints(object_id);
595                // During journal replay, `store_borrowed` is seeded from the super-block at
596                // `super_block_journal_file_offset` (stored in `self.last_end_offset`), so any
597                // `EndFlush` prior to `self.last_end_offset` is already reflected in
598                // `store_borrowed`.
599                if self.is_child_store(object_id)
600                    && context.checkpoint.file_offset >= self.last_end_offset
601                {
602                    self.store_borrowed.insert(object_id, *borrowed);
603                }
604            }
605            Mutation::DeleteVolume => {
606                if let Some(store) = self.stores.remove(&object_id) {
607                    store.mark_deleted();
608                }
609                self.layer_sizes.remove(&object_id);
610                self.store_borrowed.remove(&object_id);
611                self.journal_checkpoints.remove(&object_id);
612                return false;
613            }
614            _ => {
615                if object_id != self.root_parent_store_object_id {
616                    self.journal_checkpoints
617                        .entry(object_id)
618                        .and_modify(|entry| {
619                            if let Checkpoints::Old(x) = entry {
620                                *entry = Checkpoints::Both(
621                                    x.clone(),
622                                    CheckpointUsage::new(context.checkpoint.clone()),
623                                );
624                            }
625                        })
626                        .or_insert_with(|| {
627                            Checkpoints::Current(CheckpointUsage::new(context.checkpoint.clone()))
628                        });
629                }
630            }
631        }
632        true
633    }
634}
635
636impl ObjectManager {
637    pub fn new(on_new_store: Option<Box<dyn Fn(&ObjectStore) + Send + Sync>>) -> ObjectManager {
638        ObjectManager {
639            inner: RwLock::new(Inner {
640                stores: HashMap::default(),
641                root_parent_store_object_id: INVALID_OBJECT_ID,
642                root_store_object_id: INVALID_OBJECT_ID,
643                allocator_object_id: INVALID_OBJECT_ID,
644                allocator: None,
645                journal_checkpoints: HashMap::default(),
646                layer_sizes: HashMap::default(),
647                store_borrowed: HashMap::default(),
648                last_end_offset: 0,
649                replay_metadata_reservation: ReplayMetadataReservation::Reserved(u64::MAX),
650                max_transaction_size: (0, metrics::detail().create_uint("max_transaction_size", 0)),
651                reserved_space: journal::RESERVED_SPACE,
652            }),
653            metadata_reservation: OnceLock::new(),
654            volume_directory: OnceLock::new(),
655            on_new_store,
656        }
657    }
658
659    pub fn required_reservation(&self) -> u64 {
660        self.inner.read().required_reservation()
661    }
662
663    /// Returns the total reservation for `object_id` given its `layer_size`.
664    pub fn tree_reservation(&self, object_id: u64, layer_size: u64) -> u64 {
665        self.inner.read().tree_reservation(object_id, layer_size)
666    }
667
668    pub fn root_parent_store_object_id(&self) -> u64 {
669        self.inner.read().root_parent_store_object_id
670    }
671
672    pub fn root_parent_store(&self) -> Arc<ObjectStore> {
673        let inner = self.inner.read();
674        inner.stores.get(&inner.root_parent_store_object_id).unwrap().clone()
675    }
676
677    pub fn set_root_parent_store(&self, store: Arc<ObjectStore>) {
678        if let Some(on_new_store) = &self.on_new_store {
679            on_new_store(&store);
680        }
681        let mut inner = self.inner.write();
682        let store_id = store.store_object_id();
683        inner.stores.insert(store_id, store);
684        inner.root_parent_store_object_id = store_id;
685    }
686
687    pub fn root_store_object_id(&self) -> u64 {
688        self.inner.read().root_store_object_id
689    }
690
691    pub fn root_store(&self) -> Arc<ObjectStore> {
692        let inner = self.inner.read();
693        inner.stores.get(&inner.root_store_object_id).unwrap().clone()
694    }
695
696    pub fn set_root_store(&self, store: Arc<ObjectStore>) {
697        if let Some(on_new_store) = &self.on_new_store {
698            on_new_store(&store);
699        }
700        let mut inner = self.inner.write();
701        let store_id = store.store_object_id();
702        inner.stores.insert(store_id, store);
703        inner.root_store_object_id = store_id;
704    }
705
706    /// Returns true if `store_id` is a system store (`root_store` or `root_parent_store`).
707    pub fn is_system_store(&self, store_id: u64) -> bool {
708        self.inner.read().is_system_store(store_id)
709    }
710
711    /// Returns the store which might or might not be locked.
712    pub fn store(&self, store_object_id: u64) -> Option<Arc<ObjectStore>> {
713        self.inner.read().stores.get(&store_object_id).cloned()
714    }
715
716    /// Returns the total bytes written to the LSM trees in the allocator and all object stores
717    /// during compaction operations.
718    pub fn compaction_bytes_written(&self) -> u64 {
719        let inner = self.inner.read();
720        let mut total = 0;
721        if let Some(allocator) = &inner.allocator {
722            total += allocator.tree().compaction_bytes_written();
723        }
724        for store in inner.stores.values() {
725            total += store.tree().compaction_bytes_written();
726        }
727        total
728    }
729
730    /// This is not thread-safe: it assumes that a store won't be forgotten whilst the loop is
731    /// running.  This is to be used after replaying the journal.
732    pub async fn on_replay_complete(&self) -> Result<(), Error> {
733        let root_store = self.root_store();
734
735        let root_directory = Directory::open(&root_store, root_store.root_directory_object_id())
736            .await
737            .context("Unable to open root volume directory")?;
738
739        match root_directory.lookup(VOLUMES_DIRECTORY).await? {
740            None => bail!("Root directory not found"),
741            Some((object_id, ObjectDescriptor::Directory, _)) => {
742                let volume_directory = Directory::open(&root_store, object_id)
743                    .await
744                    .context("Unable to open volumes directory")?;
745                self.volume_directory.set(volume_directory).unwrap();
746            }
747            _ => {
748                bail!(
749                    anyhow!(FxfsError::Inconsistent)
750                        .context("Unexpected type for volumes directory")
751                )
752            }
753        }
754
755        let object_ids = list_volumes(self.volume_directory.get().unwrap())
756            .await
757            .context("Failed to list volumes")?;
758
759        for store_id in object_ids {
760            self.open_store(&root_store, store_id).await?;
761        }
762
763        // This can fail if a filesystem is created and truncated to a size
764        // that doesn't leave enough free space for metadata reservations.
765        self.init_metadata_reservation()
766            .await
767            .context("Insufficient free space for metadata reservation.")?;
768
769        Ok(())
770    }
771
772    pub fn volume_directory(&self) -> &Directory<ObjectStore> {
773        self.volume_directory.get().unwrap()
774    }
775
776    pub fn set_volume_directory(&self, volume_directory: Directory<ObjectStore>) {
777        self.volume_directory.set(volume_directory).unwrap();
778    }
779
780    pub fn add_store(&self, store: Arc<ObjectStore>) {
781        if let Some(on_new_store) = &self.on_new_store {
782            on_new_store(&store);
783        }
784        let mut inner = self.inner.write();
785        let store_object_id = store.store_object_id();
786        assert_ne!(store_object_id, inner.root_parent_store_object_id);
787        assert_ne!(store_object_id, inner.root_store_object_id);
788        assert_ne!(store_object_id, inner.allocator_object_id);
789        inner.stores.insert(store_object_id, store);
790        inner.store_borrowed.entry(store_object_id).or_default();
791    }
792
793    pub fn forget_store(&self, store_object_id: u64) {
794        let mut inner = self.inner.write();
795        assert_ne!(store_object_id, inner.allocator_object_id);
796        inner.stores.remove(&store_object_id);
797        inner.layer_sizes.remove(&store_object_id);
798        inner.store_borrowed.remove(&store_object_id);
799        inner.journal_checkpoints.remove(&store_object_id);
800    }
801
802    pub fn set_allocator(&self, allocator: Arc<Allocator>) {
803        let mut inner = self.inner.write();
804        assert!(!inner.stores.contains_key(&allocator.object_id()));
805        inner.allocator_object_id = allocator.object_id();
806        inner.allocator = Some(allocator);
807    }
808
809    pub fn allocator(&self) -> Arc<Allocator> {
810        self.inner.read().allocator.clone().unwrap()
811    }
812
813    /// Applies `mutation` to `object` with `context`.
814    pub fn apply_mutation(
815        &self,
816        object_id: u64,
817        mutation: Mutation,
818        context: &ApplyContext<'_, '_>,
819        associated_object: AssocObj<'_>,
820    ) -> Result<(), Error> {
821        debug!(oid = object_id, mutation:?; "applying mutation");
822        associated_object.map(|o| o.will_apply_mutation(&mutation, object_id, self));
823        let object = {
824            let mut inner = self.inner.write();
825            if !inner.apply_mutation(object_id, &mutation, context) {
826                return Ok(());
827            }
828            inner.journaling_object(object_id).unwrap()
829        };
830        object.apply_mutation(mutation, context, associated_object)
831    }
832
833    /// Replays `mutations` for a single transaction.  `journal_offsets` contains the per-object
834    /// starting offsets; if the current transaction offset precedes an offset, the mutations for
835    /// that object are ignored.  `context` contains the location in the journal file for this
836    /// transaction and `end_offset` is the ending journal offset for this transaction.
837    pub async fn replay_mutations(
838        &self,
839        mutations: Vec<(u64, Mutation)>,
840        object_journal_usage: &[(u64, u64)],
841        journal_offsets: &HashMap<u64, u64>,
842        context: &ApplyContext<'_, '_>,
843        end_offset: u64,
844        borrowed_metadata: BorrowedMetadata,
845    ) -> Result<(), Error> {
846        debug!(checkpoint = context.checkpoint.file_offset; "REPLAY");
847        let allocator_object_id = self.inner.read().allocator_object_id;
848
849        for (object_id, mutation) in mutations {
850            // If this transaction precedes the object's last flush (or the volume was later
851            // deleted), the object's on-disk layers already reflect it, so only update
852            // `ObjectManager`'s checkpoint state without opening or mutating the object itself.
853            if let Some(&offset) = journal_offsets.get(&object_id)
854                && context.checkpoint.file_offset < offset
855            {
856                self.inner.write().apply_mutation(object_id, &mutation, context);
857                continue;
858            }
859
860            // If this is the first time we've encountered this store, we'll need to open it.
861            if object_id != allocator_object_id {
862                self.open_store(&self.root_store(), object_id).await?;
863            }
864
865            self.apply_mutation(object_id, mutation, context, AssocObj::None)?;
866        }
867
868        let mut inner = self.inner.write();
869        let is_borrowed = !matches!(borrowed_metadata, BorrowedMetadata::None);
870        inner.record_journal_usage(object_journal_usage, is_borrowed);
871
872        if end_offset > inner.last_end_offset {
873            let old_last_end_offset = std::mem::replace(&mut inner.last_end_offset, end_offset);
874            let txn_space = inner.earliest_journal_offset().map_or(0, |min| {
875                reserved_space_from_journal_usage(
876                    end_offset.saturating_sub(std::cmp::max(old_last_end_offset, min)),
877                )
878            });
879            match borrowed_metadata {
880                BorrowedMetadata::None => {
881                    if let ReplayMetadataReservation::Reserved(ref mut reserved) =
882                        inner.replay_metadata_reservation
883                    {
884                        *reserved = reserved
885                            .checked_add(txn_space)
886                            .ok_or(FxfsError::Inconsistent)
887                            .context("reserved overflow on funded txn")?;
888                    }
889                }
890                BorrowedMetadata::Borrowed(reserved) => {
891                    inner.replay_metadata_reservation =
892                        ReplayMetadataReservation::Reserved(reserved);
893                }
894                BorrowedMetadata::LegacyBorrowed(borrowed) => {
895                    match inner.replay_metadata_reservation {
896                        ReplayMetadataReservation::Borrowed(ref mut b) => {
897                            *b = borrowed
898                                .checked_add(txn_space)
899                                .ok_or(FxfsError::Inconsistent)
900                                .context("legacy borrowed overflow")?;
901                        }
902                        ReplayMetadataReservation::Reserved(_) => {
903                            bail!(
904                                anyhow!(FxfsError::Inconsistent)
905                                    .context("unexpected LegacyBorrowed in Reserved mode")
906                            );
907                        }
908                    }
909                }
910            }
911        }
912        Ok(())
913    }
914
915    /// Opens the specified store if it isn't already.  This is *not* thread-safe.
916    async fn open_store(&self, parent: &Arc<ObjectStore>, object_id: u64) -> Result<(), Error> {
917        if self.inner.read().stores.contains_key(&object_id) {
918            return Ok(());
919        }
920        let store = ObjectStore::open(parent, object_id, Some(Box::new(TreeCache::new())))
921            .await
922            .with_context(|| format!("Failed to open store {object_id}"))?;
923        if let Some(on_new_store) = &self.on_new_store {
924            on_new_store(&store);
925        }
926        let mut inner = self.inner.write();
927        assert!(inner.stores.insert(object_id, store).is_none());
928        inner.store_borrowed.entry(object_id).or_default();
929        Ok(())
930    }
931
932    /// Called by the journaling system to apply a transaction.  `checkpoint` indicates the location
933    /// in the journal file for this transaction.
934    pub fn apply_transaction(
935        &self,
936        transaction: &mut Transaction<'_>,
937        checkpoint: &JournalCheckpoint,
938    ) -> Result<(), Error> {
939        // Record old values so we can see if anything changes as a result of this transaction.
940        let old_amount = self.metadata_reservation().amount();
941        let old_required = self.inner.read().required_reservation();
942
943        debug!(checkpoint = checkpoint.file_offset; "BEGIN TXN");
944        let mutations = transaction.take_mutations();
945        let context =
946            ApplyContext { mode: ApplyMode::Live(transaction), checkpoint: checkpoint.clone() };
947        for TxnMutation { object_id, mutation, associated_object, .. } in mutations {
948            self.apply_mutation(object_id, mutation, &context, associated_object)?;
949        }
950        debug!("END TXN");
951
952        if !transaction.metadata_reservation.is_borrowed() {
953            // This transaction should have had no impact on the metadata reservation or the
954            // amount we need to reserve.
955            debug_assert_eq!(self.metadata_reservation().amount(), old_amount);
956            debug_assert_eq!(self.inner.read().required_reservation(), old_required);
957        }
958
959        Ok(())
960    }
961
962    /// Called by the journaling system after a transaction has been written providing the end
963    /// offset for the transaction so that we can adjust metadata reservations accordingly.
964    pub fn did_commit_transaction(
965        &self,
966        transaction: &mut Transaction<'_>,
967        end_offset: u64,
968        object_usages: &[(u64, u64)],
969    ) {
970        let reservation = self.metadata_reservation();
971        let mut inner = self.inner.write();
972        let old_last_end_offset = std::mem::replace(&mut inner.last_end_offset, end_offset);
973        let journal_usage = end_offset - old_last_end_offset;
974
975        if journal_usage > inner.max_transaction_size.0 {
976            inner.max_transaction_size.0 = journal_usage;
977            inner.max_transaction_size.1.set(journal_usage);
978        }
979
980        let txn_space = inner.earliest_journal_offset().map_or(0, |min| {
981            reserved_space_from_journal_usage(
982                end_offset.saturating_sub(std::cmp::max(old_last_end_offset, min)),
983            )
984        });
985        let is_borrowed = transaction.metadata_reservation.is_borrowed();
986        match &mut transaction.metadata_reservation {
987            MetadataReservation::Borrowed => {}
988            MetadataReservation::Hold(hold) => {
989                // Transfer reserved space into the metadata reservation.
990                let txn_reservation = *hold.owner();
991                assert_ne!(
992                    txn_reservation as *const _, reservation as *const _,
993                    "MetadataReservation::Borrowed should be used."
994                );
995                txn_reservation.commit(txn_space);
996                if txn_reservation.owner_object_id() != reservation.owner_object_id() {
997                    assert_eq!(
998                        reservation.owner_object_id(),
999                        None,
1000                        "Should not be mixing attributed owners."
1001                    );
1002                    inner
1003                        .allocator
1004                        .as_ref()
1005                        .unwrap()
1006                        .disown_reservation(txn_reservation.owner_object_id(), txn_space);
1007                }
1008                assert!(
1009                    hold.amount() >= txn_space,
1010                    "Transaction was larger than metadata reservation"
1011                );
1012                hold.forget_some(txn_space);
1013                reservation.add(txn_space);
1014            }
1015            MetadataReservation::Reservation(txn_reservation) => {
1016                // Transfer reserved space into the metadata reservation.
1017                txn_reservation.move_to(reservation, txn_space);
1018            }
1019        }
1020        inner.record_journal_usage(object_usages, is_borrowed);
1021    }
1022
1023    /// Drops a transaction.  This is called automatically when a transaction is dropped.  If the
1024    /// transaction has been committed, it should contain no mutations and so nothing will get
1025    /// rolled back.  For each mutation, drop_mutation is called to allow for roll back (e.g. the
1026    /// allocator will unreserve allocations).
1027    pub fn drop_transaction(&self, transaction: &mut Transaction<'_>) {
1028        for TxnMutation { object_id, mutation, .. } in transaction.take_mutations() {
1029            self.journaling_object(object_id).map(|o| o.drop_mutation(mutation, transaction));
1030        }
1031    }
1032
1033    /// Returns the journal file offsets that each object depends on and the checkpoint for the
1034    /// minimum offset.
1035    pub fn journal_file_offsets(&self) -> (HashMap<u64, u64>, Option<JournalCheckpoint>) {
1036        let inner = self.inner.read();
1037        let mut min_checkpoint = None;
1038        let mut offsets = HashMap::default();
1039        for (&object_id, checkpoint) in &inner.journal_checkpoints {
1040            let checkpoint = checkpoint.earliest();
1041            match &mut min_checkpoint {
1042                None => min_checkpoint = Some(checkpoint),
1043                Some(min_checkpoint) => {
1044                    if checkpoint.file_offset < min_checkpoint.file_offset {
1045                        *min_checkpoint = checkpoint;
1046                    }
1047                }
1048            }
1049            offsets.insert(object_id, checkpoint.file_offset);
1050        }
1051        (offsets, min_checkpoint.cloned())
1052    }
1053
1054    /// Returns the checkpoint into the journal that the object depends on, or None if the object
1055    /// has no journaled updates.
1056    pub fn journal_checkpoint(&self, object_id: u64) -> Option<JournalCheckpoint> {
1057        self.inner
1058            .read()
1059            .journal_checkpoints
1060            .get(&object_id)
1061            .map(|checkpoints| checkpoints.earliest().clone())
1062    }
1063
1064    /// Returns true if the object identified by `object_id` is known to have updates recorded in
1065    /// the journal that the object depends upon.
1066    pub fn needs_flush(&self, object_id: u64) -> bool {
1067        self.inner.read().journal_checkpoints.contains_key(&object_id)
1068    }
1069
1070    /// Flushes changes to device with the given reason.
1071    /// Also returns the earliest known version of a struct on the filesystem.
1072    pub async fn flush(&self, reason: FlushReason) -> Result<Version, Error> {
1073        let objects = {
1074            let inner = self.inner.read();
1075            let total_reserved = self.metadata_reservation.get().map_or(0, |r| r.amount());
1076            let mut objects = Vec::new();
1077            let candidate_ids = inner
1078                .stores
1079                .keys()
1080                .copied()
1081                .filter(|&id| id != inner.root_parent_store_object_id)
1082                .chain(std::iter::once(inner.allocator_object_id));
1083            for object_id in candidate_ids {
1084                let has_checkpoint = inner.journal_checkpoints.contains_key(&object_id);
1085                let object_reason = match reason {
1086                    FlushReason::Journal(ForceMajor::False) => {
1087                        let force_major =
1088                            inner.should_force_major_compaction(object_id, total_reserved);
1089                        if !has_checkpoint && force_major == ForceMajor::False {
1090                            continue;
1091                        }
1092                        FlushReason::Journal(force_major)
1093                    }
1094                    FlushReason::Journal(ForceMajor::True) => {
1095                        if !inner.can_compact(object_id) {
1096                            continue;
1097                        }
1098                        FlushReason::Journal(ForceMajor::True)
1099                    }
1100                    other => {
1101                        if !has_checkpoint {
1102                            continue;
1103                        }
1104                        other
1105                    }
1106                };
1107                objects.push((
1108                    object_id,
1109                    inner.journaling_object(object_id).unwrap(),
1110                    object_reason,
1111                ));
1112            }
1113
1114            // Process objects in reverse sorted order because that will mean we compact the root
1115            // object store last which will ensure we include the metadata from the compactions of
1116            // other objects.
1117            objects.sort_unstable_by_key(|(oid, _, _)| Reverse(*oid));
1118            objects
1119        };
1120
1121        // As we iterate, keep track of the earliest version used by structs in these objects
1122        let mut earliest_version: Version = LATEST_VERSION;
1123        for (object_id, object, object_reason) in objects {
1124            let object_earliest_version = object
1125                .flush(object_reason)
1126                .await
1127                .with_context(|| format!("Failed to flush oid {object_id}"))?;
1128            if object_earliest_version < earliest_version {
1129                earliest_version = object_earliest_version;
1130            }
1131        }
1132
1133        Ok(earliest_version)
1134    }
1135
1136    pub fn journaling_object(&self, object_id: u64) -> Option<Arc<dyn JournalingObject>> {
1137        self.inner.read().journaling_object(object_id)
1138    }
1139
1140    // Computes the `< V61` required metadata reservation from first principles so that `< V61`
1141    // migration remains fixed even if `layer_sizes`, `tree_headroom`,
1142    // `layer_size_from_encrypted_mutations_size`, `reserved_space_from_journal_usage`, or
1143    // `journal::RESERVED_SPACE` change in future versions.
1144    async fn legacy_required_reservation(&self) -> Result<u64, Error> {
1145        const LEGACY_RESERVED_SPACE: u64 = 1_048_576;
1146
1147        let (stores, allocator, total_journal_usage) = {
1148            let inner = self.inner.read();
1149            (
1150                inner.stores.values().cloned().collect::<Vec<_>>(),
1151                inner.allocator.clone().unwrap(),
1152                inner.total_journal_usage(),
1153            )
1154        };
1155
1156        let mut max_tree_reservation = allocator
1157            .tree()
1158            .immutable_layer_set()
1159            .layers
1160            .iter()
1161            .map(|l| l.handle().map(ReadObjectHandle::get_size).unwrap_or(0))
1162            .sum::<u64>()
1163            * 3
1164            / 2;
1165
1166        for store in stores {
1167            let total_layer_size = if store.is_locked() {
1168                let info = store.load_store_info().await?;
1169                let parent_store = store.parent_store().unwrap();
1170                let mut size = 0;
1171                for &oid in &info.layers {
1172                    size += parent_store.get_file_size(oid).await?;
1173                }
1174                if info.encrypted_mutations_object_id != INVALID_OBJECT_ID {
1175                    size +=
1176                        parent_store.get_file_size(info.encrypted_mutations_object_id).await? * 3;
1177                }
1178                size
1179            } else {
1180                store
1181                    .tree()
1182                    .immutable_layer_set()
1183                    .layers
1184                    .iter()
1185                    .map(|l| l.handle().map(ReadObjectHandle::get_size).unwrap_or(0))
1186                    .sum()
1187            };
1188            max_tree_reservation = std::cmp::max(max_tree_reservation, total_layer_size * 3 / 2);
1189        }
1190
1191        Ok(max_tree_reservation + total_journal_usage * 4 + LEGACY_RESERVED_SPACE)
1192    }
1193
1194    /// Initializes the live `metadata_reservation` from the allocator after journal replay and
1195    /// opening all volumes.
1196    pub async fn init_metadata_reservation(&self) -> Result<(), Error> {
1197        let legacy_required = if matches!(
1198            self.inner.read().replay_metadata_reservation,
1199            ReplayMetadataReservation::Borrowed(_)
1200        ) {
1201            Some(self.legacy_required_reservation().await?)
1202        } else {
1203            None
1204        };
1205        let (amount, required, replay_state, allocator) = {
1206            let mut inner = self.inner.write();
1207            let required = inner.required_reservation();
1208            let replay_state = inner.replay_metadata_reservation;
1209            let amount = match replay_state {
1210                // When migrating from a `< V61` super-block, only a single global `borrowed`
1211                // scalar is known, which was computed against `legacy_required_reservation()`.
1212                // Recover `M_total = min(legacy_required - legacy_borrowed, required)`, subtract
1213                // any unflushed journal borrowing already tracked in `journal_checkpoints`, and
1214                // distribute the remaining non-journal deficit proportionally across child stores'
1215                // layer headroom using u128 arithmetic to avoid overflow.
1216                ReplayMetadataReservation::Borrowed(legacy_borrowed) => {
1217                    let legacy_required = legacy_required.unwrap();
1218                    ensure!(legacy_required >= legacy_borrowed, FxfsError::Inconsistent);
1219                    let amount = std::cmp::min(legacy_required - legacy_borrowed, required);
1220                    let borrowed = required - amount;
1221                    let max_scratch = inner.max_tree_scratch();
1222                    let total_journal_space =
1223                        reserved_space_from_journal_usage(inner.total_journal_usage());
1224                    let flushed_headroom =
1225                        required.saturating_sub(max_scratch + total_journal_space);
1226                    let total_unflushed_borrowed: u64 = inner
1227                        .journal_checkpoints
1228                        .values()
1229                        .map(|c| reserved_space_from_journal_usage(c.borrowed_usage()))
1230                        .sum();
1231                    let flushed_borrowed = borrowed.saturating_sub(total_unflushed_borrowed);
1232                    inner.store_borrowed.clear();
1233                    let store_ids: Vec<u64> = inner.stores.keys().copied().collect();
1234                    for id in store_ids {
1235                        if inner.is_child_store(id) {
1236                            let layer_size = inner.layer_sizes.get(&id).copied().unwrap_or(0);
1237                            let h_s = Inner::child_headroom_store_share(layer_size);
1238                            let layer_borrowed = if flushed_headroom > 0 {
1239                                std::cmp::min(
1240                                    h_s,
1241                                    ((h_s as u128 * flushed_borrowed as u128)
1242                                        / flushed_headroom as u128)
1243                                        as u64,
1244                                )
1245                            } else {
1246                                0
1247                            };
1248                            inner.store_borrowed.insert(id, layer_borrowed);
1249                        }
1250                    }
1251                    inner.prune_stores_borrowed();
1252                    inner.replay_metadata_reservation = ReplayMetadataReservation::Reserved(amount);
1253                    amount
1254                }
1255
1256                // For V61+ super-blocks, prune any removed stores from `store_borrowed` and
1257                // reserve `min(reserved_total, required)` so that if a software update increased
1258                // `required_reservation()`, we never attempt to reserve more from the allocator at
1259                // mount time than was previously held on disk.
1260                ReplayMetadataReservation::Reserved(reserved_total) => {
1261                    inner.prune_stores_borrowed();
1262                    std::cmp::min(reserved_total, required)
1263                }
1264            };
1265            (amount, required, replay_state, inner.allocator.as_ref().cloned().unwrap())
1266        };
1267        if self.root_parent_store().filesystem().options().read_only {
1268            // We don't need metadata reservations in read-only mode.
1269            return Ok(());
1270        }
1271        self.metadata_reservation
1272            .set(allocator.clone().reserve(None, amount).with_context(|| {
1273                format!(
1274                    "Failed to reserve {} bytes (required: {}, replay: {:?}), free={}, \
1275                             owner_bytes={}",
1276                    amount,
1277                    required,
1278                    replay_state,
1279                    Saturating(allocator.get_disk_bytes()) - allocator.get_used_bytes(),
1280                    allocator.owner_bytes_debug(),
1281                )
1282            })?)
1283            .unwrap();
1284        Ok(())
1285    }
1286
1287    pub fn metadata_reservation(&self) -> &Reservation {
1288        self.metadata_reservation.get().unwrap()
1289    }
1290
1291    pub fn is_metadata_reservation(&self, reservation: &Reservation) -> bool {
1292        self.metadata_reservation.get().is_some_and(|r| std::ptr::eq(r, reservation))
1293    }
1294
1295    /// Reconciles the metadata reservation at `checkpoint_offset` (returning any excess over
1296    /// `required_reservation()` to the allocator) and returns the map to persist in the
1297    /// super-block (key 0 for `M_total`, and child store IDs for each child store's flushed
1298    /// `store_borrowed`).
1299    pub fn reconcile_metadata_reservation(&self, checkpoint_offset: u64) -> HashMap<u64, u64> {
1300        let reservation = self.metadata_reservation();
1301        let mut inner = self.inner.write();
1302        inner.last_end_offset = checkpoint_offset;
1303        let required = inner.required_reservation();
1304        inner.prune_stores_borrowed();
1305        let mut result = inner.store_borrowed.clone();
1306
1307        // Cap `M_total` at `required` (`R_total`) and return any excess to the allocator. Note
1308        // that we do not subtract `sum(B_S)` here because after a minor compaction or locked
1309        // flush, `B_S` can differ from the exact global deficit (`R_total - M_total`).
1310        let to_give_back = reservation.amount().saturating_sub(required);
1311        if to_give_back > 0 {
1312            reservation.give_back(to_give_back);
1313        }
1314        result.insert(0, reservation.amount());
1315        result
1316    }
1317
1318    /// Sets the tree layer size (`layer_size`) for `object_id`.
1319    pub fn update_layer_size(&self, object_id: u64, layer_size: u64) {
1320        self.inner.write().layer_sizes.insert(object_id, layer_size);
1321    }
1322
1323    /// Returns the new flushed layer borrowing (`store_borrowed`) for `object_id` when a minor or
1324    /// locked flush changes its layer size from `old_size` to `new_size` (with total layer size
1325    /// `total_layer_size`).
1326    pub fn store_borrowed_for_flush(
1327        &self,
1328        object_id: u64,
1329        old_size: u64,
1330        new_size: u64,
1331        total_layer_size: u64,
1332    ) -> u64 {
1333        let inner = self.inner.read();
1334        if !inner.is_child_store(object_id) {
1335            return 0;
1336        }
1337        let current = inner.store_borrowed.get(&object_id).copied().unwrap_or(0);
1338        let updated = if new_size >= old_size {
1339            let borrowed_usage = match inner.journal_checkpoints.get(&object_id) {
1340                Some(Checkpoints::Old(old) | Checkpoints::Both(old, _)) => old.borrowed_usage,
1341                _ => 0,
1342            };
1343            let added = std::cmp::min(
1344                reserved_space_from_journal_usage(borrowed_usage),
1345                inner.tree_reservation(object_id, new_size - old_size),
1346            );
1347            current.saturating_add(added)
1348        } else {
1349            let released = inner.tree_reservation(object_id, old_size - new_size);
1350            current.saturating_sub(released)
1351        };
1352        std::cmp::min(updated, Inner::child_headroom_store_share(total_layer_size))
1353    }
1354
1355    pub fn reservation(&self, object_id: u64) -> Option<u64> {
1356        let inner = self.inner.read();
1357        inner
1358            .layer_sizes
1359            .get(&object_id)
1360            .copied()
1361            .map(|layer_size| inner.tree_reservation(object_id, layer_size))
1362    }
1363
1364    /// Returns the maximum layer reservation held by any object.
1365    pub fn max_store_reservation(&self) -> u64 {
1366        let inner = self.inner.read();
1367        inner
1368            .layer_sizes
1369            .iter()
1370            .map(|(&id, &layer_size)| inner.tree_reservation(id, layer_size))
1371            .max()
1372            .unwrap_or(0)
1373    }
1374
1375    pub fn set_reserved_space(&self, amount: u64) {
1376        self.inner.write().reserved_space = amount;
1377    }
1378
1379    pub fn last_end_offset(&self) -> u64 {
1380        self.inner.read().last_end_offset
1381    }
1382
1383    pub fn set_last_end_offset(&self, v: u64) {
1384        self.inner.write().last_end_offset = v;
1385    }
1386
1387    /// Returns the total borrowed metadata space.
1388    pub fn borrowed_metadata_space(&self) -> u64 {
1389        self.inner.read().borrowed_metadata_space(self.metadata_reservation.get())
1390    }
1391
1392    /// Returns the total borrowed metadata space for each child store.
1393    pub fn stores_borrowed(&self) -> HashMap<u64, u64> {
1394        let inner = self.inner.read();
1395        inner.store_borrowed.keys().map(|&id| (id, inner.total_store_borrowed(id))).collect()
1396    }
1397
1398    /// Initializes the replay metadata reservation state from the super-block header before
1399    /// replaying the journal. For `< V61` super-blocks, key `0` holds the legacy `borrowed`
1400    /// scalar; for `>= V61` super-blocks, key `0` holds `M_total` and non-zero keys hold each
1401    /// child store's borrowed space `B_S`.
1402    pub fn init_replay_metadata_reservation(&self, version: Version, reserved: &HashMap<u64, u64>) {
1403        let mut inner = self.inner.write();
1404        inner.store_borrowed.clear();
1405        if version.major < 61 {
1406            inner.replay_metadata_reservation =
1407                ReplayMetadataReservation::Borrowed(reserved.get(&0).copied().unwrap_or(0));
1408        } else {
1409            let total = reserved.get(&0).copied().unwrap_or(0);
1410            for (&object_id, &amount) in reserved {
1411                if object_id != 0 {
1412                    inner.store_borrowed.insert(object_id, amount);
1413                }
1414            }
1415            inner.replay_metadata_reservation = ReplayMetadataReservation::Reserved(total);
1416        }
1417    }
1418
1419    pub fn write_mutations(
1420        &self,
1421        object_id: u64,
1422        mutations: ObjectMutationIterator<'_, '_>,
1423        writer: journal::Writer<'_>,
1424    ) {
1425        self.journaling_object(object_id).unwrap().write_mutations(mutations, writer);
1426    }
1427
1428    pub fn unlocked_stores(&self) -> Vec<Arc<ObjectStore>> {
1429        let inner = self.inner.read();
1430        let mut stores = Vec::new();
1431        for store in inner.stores.values() {
1432            if !store.is_locked() {
1433                stores.push(store.clone());
1434            }
1435        }
1436        stores
1437    }
1438
1439    /// Creates a lazy inspect node named `str` under `parent` which will yield statistics for the
1440    /// object manager when queried.
1441    pub fn track_statistics(self: &Arc<Self>, parent: &fuchsia_inspect::Node, name: &str) {
1442        let this = Arc::downgrade(self);
1443        parent.record_lazy_child(name, move || {
1444            let this_clone = this.clone();
1445            async move {
1446                let inspector = fuchsia_inspect::Inspector::default();
1447                if let Some(this) = this_clone.upgrade() {
1448                    let (required, borrowed, earliest_checkpoint) = {
1449                        // TODO(https://fxbug.dev/42069513): Push-back or rate-limit to prevent DoS.
1450                        let inner = this.inner.read();
1451                        let required = inner.required_reservation();
1452                        let borrowed =
1453                            inner.borrowed_metadata_space(this.metadata_reservation.get());
1454                        (required, borrowed, inner.earliest_journal_offset())
1455                    };
1456                    let root = inspector.root();
1457                    if let Some(reservation) = this.metadata_reservation.get() {
1458                        root.record_uint("metadata_reservation", reservation.amount());
1459                    }
1460                    root.record_uint("required_reservation", required);
1461                    root.record_uint("borrowed_reservation", borrowed);
1462                    if let Some(earliest_checkpoint) = earliest_checkpoint {
1463                        root.record_uint("earliest_checkpoint", earliest_checkpoint);
1464                    }
1465
1466                    // TODO(https://fxbug.dev/42068224): Post-compute rather than manually
1467                    // computing metrics.
1468                    if let Some(x) = round_div(100 * borrowed, required) {
1469                        root.record_uint("borrowed_to_required_reservation_percent", x);
1470                    }
1471                }
1472                Ok(inspector)
1473            }
1474            .boxed()
1475        });
1476    }
1477
1478    /// Normally, we make new transactions pay for overheads incurred by the journal, such as
1479    /// checksums and padding, but if the journal has discarded a significant amount after a replay,
1480    /// we run the risk of there not being enough reserved.  To handle this, if the amount is
1481    /// significant, we force the journal to borrow the space (using a journal created transaction).
1482    ///
1483    /// We allow up to `BLOCK_SIZE + 64` bytes before borrowing to cover up to a full block of
1484    /// padding (from `pad_to_block` or `skip_to_end_of_block`) plus any `RESET` version, `Discard`,
1485    /// or `DidFlushDevice` records written in `pre_commit`.
1486    pub fn needs_borrow_for_journal(&self, checkpoint: u64) -> bool {
1487        let inner = self.inner.read();
1488        !inner.journal_checkpoints.is_empty()
1489            && checkpoint.checked_sub(inner.last_end_offset).unwrap()
1490                > journal::BLOCK_SIZE.get() + 64
1491    }
1492
1493    #[cfg(test)]
1494    pub fn store_borrowed(&self, store_id: u64) -> Option<(u64, u64)> {
1495        let inner = self.inner.read();
1496        inner.store_borrowed.contains_key(&store_id).then(|| {
1497            (inner.total_store_borrowed(store_id), inner.required_store_reservation(store_id))
1498        })
1499    }
1500
1501    #[cfg(test)]
1502    pub fn set_store_borrowed(&self, store_id: u64, amount: u64) {
1503        let mut inner = self.inner.write();
1504        let unflushed_borrowed = reserved_space_from_journal_usage(
1505            inner.journal_checkpoints.get(&store_id).map_or(0, |c| c.borrowed_usage()),
1506        );
1507        inner.store_borrowed.insert(store_id, amount.saturating_sub(unflushed_borrowed));
1508    }
1509
1510    #[cfg(test)]
1511    pub fn reserved_metadata_space(&self) -> HashMap<u64, u64> {
1512        let inner = self.inner.read();
1513        let required = inner.required_reservation();
1514        let total = self.metadata_reservation.get().map_or(0, |r| r.amount());
1515        let mut result = HashMap::default();
1516        for &id in inner.store_borrowed.keys() {
1517            result.insert(id, inner.total_store_borrowed(id));
1518        }
1519        result.insert(0, std::cmp::min(total, required));
1520        result
1521    }
1522}
1523
1524/// `ReservationUpdate` is an associated object attached to `Mutation::EndFlush` that updates the
1525/// tree layer size (`layer_size`) for an object.
1526pub struct ReservationUpdate(u64);
1527
1528impl ReservationUpdate {
1529    pub fn new(layer_size: u64) -> Self {
1530        Self(layer_size)
1531    }
1532}
1533
1534impl AssociatedObject for ReservationUpdate {
1535    fn will_apply_mutation(&self, _mutation: &Mutation, object_id: u64, manager: &ObjectManager) {
1536        manager.update_layer_size(object_id, self.0);
1537    }
1538}
1539
1540#[cfg(test)]
1541mod tests {
1542    use super::{ForceMajor, ObjectManager};
1543    use crate::filesystem::{FxFilesystem, FxFilesystemBuilder, SyncOptions};
1544    use crate::fsck::{FsckOptions, fsck_volume_with_options, fsck_with_options};
1545    use crate::object_handle::{ObjectHandle, WriteObjectHandle};
1546    use crate::object_store::directory::replace_child;
1547    use crate::object_store::journal::JournalOptions;
1548    use crate::object_store::transaction::{
1549        AssocObj, AssociatedObject, LockKey, Mutation, Options, ReservationOptions, lock_keys,
1550    };
1551    use crate::object_store::volume::root_volume;
1552    use crate::object_store::{
1553        Directory, FlushReason, HandleOptions, INVALID_OBJECT_ID, NewChildStoreOptions, ObjectKey,
1554        ObjectStore, ObjectValue, StoreOptions,
1555    };
1556    use fxfs_crypto::Crypt;
1557    use fxfs_insecure_crypto::new_insecure_crypt;
1558    use rand::rngs::SmallRng;
1559    use rand::{RngExt, SeedableRng};
1560    use std::sync::Arc;
1561    use std::time::Instant;
1562    use storage_device::DeviceHolder;
1563    use storage_device::fake_device::FakeDevice;
1564
1565    // Runs a randomized stress test exercising per-store metadata reservations, borrowing,
1566    // minor/major compactions, locked flushes, dynamic volume creation/deletion, and remounts
1567    // under tight disk-space headroom.
1568    //
1569    // By default (in CQ), this runs a single 160-step episode with a random seed. It can also be
1570    // run manually for extended stress testing or failure reproduction using environment variables:
1571    //   - `FXFS_STRESS_SECS=<seconds>`: run episodes continuously for the given duration.
1572    //   - `FXFS_STRESS_SEED=<u64>`: start from a specific RNG seed.
1573    #[fuchsia::test(threads = 4)]
1574    async fn test_metadata_reservation_stress() {
1575        struct VolState {
1576            name: String,
1577            store_id: u64,
1578            crypt: Option<Arc<dyn Crypt>>,
1579            permanent: bool,
1580            churn_cursor: usize,
1581            churn_gen: Vec<u64>,
1582        }
1583
1584        async fn check_all_volumes(fs: &Arc<FxFilesystem>, vols: &[VolState]) {
1585            fs.graveyard().flush().await;
1586            let options = FsckOptions {
1587                fail_on_warning: true,
1588                halt_on_error: true,
1589                do_slow_passes: true,
1590                quiet: true,
1591                ..Default::default()
1592            };
1593            fsck_with_options(fs.clone(), &options).await.expect("fsck failed");
1594            for vol in vols {
1595                if fs.object_manager().store(vol.store_id).is_some() {
1596                    fsck_volume_with_options(
1597                        fs.as_ref(),
1598                        &options,
1599                        vol.store_id,
1600                        vol.crypt.clone(),
1601                    )
1602                    .await
1603                    .expect("fsck_volume failed");
1604                }
1605            }
1606        }
1607
1608        async fn list_dir_files(store: &Arc<ObjectStore>) -> Vec<(String, u64)> {
1609            let root_dir = Directory::open(store, store.root_directory_object_id())
1610                .await
1611                .expect("open root_dir failed");
1612            let layer_set = store.tree().layer_set();
1613            let mut merger = layer_set.merger();
1614            let mut iter = root_dir.iter(&mut merger).await.expect("iter failed");
1615            let mut files = Vec::new();
1616            while let Some((name, object_id, _)) = iter.get() {
1617                if name != "filler" {
1618                    files.push((name.to_string(), object_id));
1619                }
1620                iter.advance().await.expect("advance failed");
1621            }
1622            files
1623        }
1624
1625        // `vol_2` and `vol_3` are permanent and unencrypted, so at least 2 volumes are always
1626        // unlocked.
1627        fn pick_unlocked_volume(
1628            fs: &FxFilesystem,
1629            vols: &[VolState],
1630            rng: &mut SmallRng,
1631        ) -> Arc<ObjectStore> {
1632            let unlocked: Vec<usize> = (0..vols.len())
1633                .filter(|&i| {
1634                    fs.object_manager().store(vols[i].store_id).is_some_and(|s| !s.is_locked())
1635                })
1636                .collect();
1637            let idx = unlocked[rng.random_range(0..unlocked.len())];
1638            fs.object_manager().store(vols[idx].store_id).unwrap()
1639        }
1640
1641        async fn unlink_and_tombstone_file(
1642            fs: &FxFilesystem,
1643            store: &Arc<ObjectStore>,
1644            fname: &str,
1645            oid: u64,
1646            immediate: bool,
1647        ) {
1648            let root_dir = Directory::open(store, store.root_directory_object_id())
1649                .await
1650                .expect("open root_dir failed");
1651            let ctx = root_dir
1652                .acquire_context_for_replace(None, fname, true)
1653                .await
1654                .expect("acquire_context_for_replace failed");
1655            let mut txn = ctx.transaction;
1656            let truncate_guard = ctx.truncate_guard;
1657            replace_child(&mut txn, None, (&root_dir, fname)).await.expect("replace_child failed");
1658            txn.commit().await.expect("commit unlink failed");
1659            if immediate {
1660                fs.tombstone_object(store.store_object_id(), oid, truncate_guard.as_ref())
1661                    .await
1662                    .expect("tombstone_object failed");
1663            } else {
1664                std::mem::drop(truncate_guard);
1665                fs.graveyard().queue_tombstone_object(store.store_object_id(), oid);
1666            }
1667        }
1668
1669        let stress_secs =
1670            std::env::var("FXFS_STRESS_SECS").ok().and_then(|s| s.parse::<u64>().ok()).unwrap_or(0);
1671        let base_seed = std::env::var("FXFS_STRESS_SEED")
1672            .ok()
1673            .map(|s| {
1674                if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
1675                    u64::from_str_radix(hex, 16)
1676                } else {
1677                    s.parse::<u64>()
1678                }
1679                .expect("invalid FXFS_STRESS_SEED")
1680            })
1681            .unwrap_or_else(rand::random);
1682
1683        let start_time = Instant::now();
1684        let mut episode = 0u64;
1685
1686        loop {
1687            let seed = base_seed.wrapping_add(episode);
1688            let mut rng = SmallRng::seed_from_u64(seed);
1689            let steps_per_episode = if stress_secs > 0 { 400 } else { 160 };
1690            if episode == 0 || (stress_secs > 0 && episode % 25 == 0) {
1691                eprintln!(
1692                    "[stress] episode {episode} (seed={seed:#x}, elapsed={:?})",
1693                    start_time.elapsed()
1694                );
1695            }
1696
1697            let reclaim_size = if rng.random_bool(0.5) { 32768 } else { 65536 };
1698            let device = DeviceHolder::new(FakeDevice::new(4096, 4096));
1699            let mut fs = FxFilesystemBuilder::new()
1700                .format(true)
1701                .journal_options(JournalOptions { reclaim_size, ..Default::default() })
1702                .open(device)
1703                .await
1704                .expect("open failed");
1705
1706            let mut vols = Vec::new();
1707            {
1708                let root_vol = root_volume(fs.clone()).await.expect("root_volume failed");
1709                for i in 0..4 {
1710                    let encrypted = i < 2;
1711                    let crypt: Option<Arc<dyn Crypt>> =
1712                        encrypted.then(|| Arc::new(new_insecure_crypt()) as _);
1713                    let name = format!("vol_{i}");
1714                    let store = root_vol
1715                        .new_volume(
1716                            &name,
1717                            NewChildStoreOptions {
1718                                options: StoreOptions {
1719                                    crypt: crypt.clone(),
1720                                    ..StoreOptions::default()
1721                                },
1722                                ..NewChildStoreOptions::default()
1723                            },
1724                        )
1725                        .await
1726                        .expect("new_volume failed");
1727                    let store_id = store.store_object_id();
1728                    let root_dir_id = store.root_directory_object_id();
1729
1730                    // Pre-populate `vol_0` (encrypted) and `vol_2` (unencrypted) with > 512 KiB
1731                    // (`DEFAULT_RECLAIM_SIZE`) of base-layer metadata so minor compactions keep
1732                    // their base layer instead of collapsing into full compactions.
1733                    let base_count = if i == 0 || i == 2 { 1400 } else { 0 };
1734                    for batch in 0..(base_count / 50) {
1735                        let mut txn = store
1736                            .new_transaction(
1737                                lock_keys![LockKey::object(store_id, root_dir_id)],
1738                                Options::default(),
1739                            )
1740                            .await
1741                            .expect("new_transaction failed");
1742                        for j in 0..50 {
1743                            let idx = batch * 50 + j;
1744                            txn.add(
1745                                store_id,
1746                                Mutation::replace_or_insert_object(
1747                                    ObjectKey::extended_attribute(
1748                                        root_dir_id,
1749                                        format!("churn_{idx:04}_gen_0_{:<190}", idx).into_bytes(),
1750                                    ),
1751                                    ObjectValue::inline_extended_attribute(vec![0x55; 200]),
1752                                ),
1753                            );
1754                        }
1755                        txn.commit().await.expect("commit failed");
1756                    }
1757                    if base_count > 0 {
1758                        store
1759                            .flush_with_reason(FlushReason::Journal(ForceMajor::True))
1760                            .await
1761                            .expect("initial major flush failed");
1762                    }
1763
1764                    vols.push(VolState {
1765                        name,
1766                        store_id,
1767                        crypt,
1768                        permanent: true,
1769                        churn_cursor: 0,
1770                        churn_gen: vec![0; base_count],
1771                    });
1772                }
1773                fs.journal().force_compact().await.expect("force_compact failed");
1774                fs.journal().force_compact().await.expect("force_compact failed");
1775
1776                // Write 64 KiB chunks to `filler` in `vol_3` until remaining free space is tight
1777                // (<= 1.25 MiB). Any failure to major-compact dead base-layer records or any
1778                // reservation leak will exhaust this headroom and cause funded operations below to
1779                // panic with `NoSpace`.
1780                let store_3 = fs.object_manager().store(vols[3].store_id).unwrap();
1781                let root_dir_3 = Directory::open(&store_3, store_3.root_directory_object_id())
1782                    .await
1783                    .expect("open root_dir_3 failed");
1784                let filler = {
1785                    let mut txn = store_3
1786                        .new_transaction(
1787                            lock_keys![LockKey::object(vols[3].store_id, root_dir_3.object_id())],
1788                            Options::default(),
1789                        )
1790                        .await
1791                        .expect("new_transaction failed");
1792                    let file = root_dir_3
1793                        .create_child_file(&mut txn, "filler")
1794                        .await
1795                        .expect("create filler failed");
1796                    txn.commit().await.expect("commit filler failed");
1797                    file
1798                };
1799                let chunk = filler.allocate_buffer(65536).await;
1800                let mut offset = 0;
1801                while fs
1802                    .allocator()
1803                    .get_disk_bytes()
1804                    .saturating_sub(fs.allocator().get_used_bytes().0)
1805                    > 1280 * 1024
1806                {
1807                    filler
1808                        .write_or_append(Some(offset), chunk.as_ref())
1809                        .await
1810                        .expect("write filler failed");
1811                    offset += 65536;
1812                }
1813                fs.journal().force_compact().await.expect("force_compact failed");
1814                fs.sync(SyncOptions { flush_device: true, ..Default::default() })
1815                    .await
1816                    .expect("sync failed");
1817            }
1818
1819            let mut next_id = 0u64;
1820            for _step in 0..steps_per_episode {
1821                if stress_secs > 0 && start_time.elapsed().as_secs() >= stress_secs {
1822                    break;
1823                }
1824                next_id += 1;
1825                let op = rng.random_range(0..100);
1826
1827                if op < 18 {
1828                    // 1) Create and write a file in a random unlocked volume (capping live user
1829                    // files per volume at 3 so live data stays bounded).
1830                    let store = pick_unlocked_volume(&fs, &vols, &mut rng);
1831                    let existing = list_dir_files(&store).await;
1832                    if existing.len() >= 3 {
1833                        let (ref old_name, old_oid) = existing[0];
1834                        unlink_and_tombstone_file(&fs, &store, old_name, old_oid, true).await;
1835                    }
1836                    fs.graveyard().flush().await;
1837                    let root_dir = Directory::open(&store, store.root_directory_object_id())
1838                        .await
1839                        .expect("open failed");
1840                    let name = format!("f_{next_id}");
1841                    let mut txn = store
1842                        .new_transaction(
1843                            lock_keys![LockKey::object(
1844                                store.store_object_id(),
1845                                root_dir.object_id()
1846                            )],
1847                            Options::default(),
1848                        )
1849                        .await
1850                        .unwrap_or_else(|e| panic!("new_transaction failed (seed={seed:#x}): {e}"));
1851                    let handle = root_dir
1852                        .create_child_file(&mut txn, &name)
1853                        .await
1854                        .expect("create_child_file failed");
1855                    txn.commit().await.expect("commit create_child_file failed");
1856                    let blocks = rng.random_range(1..=2);
1857                    let mut buf = handle.allocate_buffer(blocks * 4096).await;
1858                    buf.fill((next_id & 0xff) as u8);
1859                    handle
1860                        .write_or_append(Some(0), buf.as_ref())
1861                        .await
1862                        .unwrap_or_else(|e| panic!("write_or_append failed (seed={seed:#x}): {e}"));
1863                } else if op < 32 {
1864                    // 2) Overwrite or truncate an existing file in a random unlocked volume.
1865                    let store = pick_unlocked_volume(&fs, &vols, &mut rng);
1866                    let files = list_dir_files(&store).await;
1867                    if files.is_empty() {
1868                        continue;
1869                    }
1870                    let (_, oid) = files[rng.random_range(0..files.len())];
1871                    let handle =
1872                        ObjectStore::open_object(&store, oid, HandleOptions::default(), None)
1873                            .await
1874                            .expect("open_object failed");
1875                    if rng.random_bool(0.5) {
1876                        if rng.random_bool(0.5) {
1877                            handle
1878                                .truncate_with_options(
1879                                    Options {
1880                                        reservation: ReservationOptions::Borrowed,
1881                                        ..Default::default()
1882                                    },
1883                                    0,
1884                                )
1885                                .await
1886                                .expect("borrowed truncate failed");
1887                        } else {
1888                            let new_size = rng.random_range(0..=4096);
1889                            handle.truncate(new_size).await.expect("truncate failed");
1890                        }
1891                    } else {
1892                        let mut buf = handle.allocate_buffer(4096).await;
1893                        buf.fill(0x5a);
1894                        let offset = rng.random_range(0..=1) * 4096;
1895                        handle
1896                            .write_or_append(Some(offset), buf.as_ref())
1897                            .await
1898                            .expect("write_or_append failed");
1899                    }
1900                } else if op < 40 {
1901                    // 3) Unlink and tombstone a file in a random unlocked volume.
1902                    let store = pick_unlocked_volume(&fs, &vols, &mut rng);
1903                    let files = list_dir_files(&store).await;
1904                    if files.is_empty() {
1905                        continue;
1906                    }
1907                    let (ref fname, oid) = files[rng.random_range(0..files.len())];
1908                    let immediate = rng.random_bool(0.7);
1909                    unlink_and_tombstone_file(&fs, &store, fname, oid, immediate).await;
1910                } else if op < 54 {
1911                    // 4) Insert or delete (via Borrowed) extended attributes on root_dir.
1912                    let store = pick_unlocked_volume(&fs, &vols, &mut rng);
1913                    let store_id = store.store_object_id();
1914                    let root_dir_id = store.root_directory_object_id();
1915                    let attr_idx = rng.random_range(0..32);
1916                    let key_name = format!("xattr_{attr_idx:02}_{:<120}", attr_idx).into_bytes();
1917                    if rng.random_bool(0.45) {
1918                        let mut txn = store
1919                            .new_transaction(
1920                                lock_keys![LockKey::object(store_id, root_dir_id)],
1921                                Options::default(),
1922                            )
1923                            .await
1924                            .expect("funded xattr txn failed");
1925                        let len = rng.random_range(64..=256);
1926                        txn.add(
1927                            store_id,
1928                            Mutation::replace_or_insert_object(
1929                                ObjectKey::extended_attribute(root_dir_id, key_name),
1930                                ObjectValue::inline_extended_attribute(vec![0x77; len]),
1931                            ),
1932                        );
1933                        txn.commit().await.expect("funded xattr commit failed");
1934                    } else {
1935                        let mut txn = store
1936                            .new_transaction(
1937                                lock_keys![LockKey::object(store_id, root_dir_id)],
1938                                Options {
1939                                    skip_journal_checks: true,
1940                                    reservation: ReservationOptions::Borrowed,
1941                                    ..Default::default()
1942                                },
1943                            )
1944                            .await
1945                            .expect("borrowed txn failed");
1946                        txn.add(
1947                            store_id,
1948                            Mutation::replace_or_insert_object(
1949                                ObjectKey::extended_attribute(root_dir_id, key_name),
1950                                ObjectValue::None,
1951                            ),
1952                        );
1953                        txn.commit().await.expect("borrowed commit failed");
1954                    }
1955                } else if op < 74 {
1956                    // 5) Base-layer delete + create churn on a volume with a > 512 KiB base layer
1957                    // (`vol_2` is permanent and unencrypted, so `candidates` is never empty):
1958                    // delete a batch of existing entries (using `Borrowed`) and insert an
1959                    // equal number of new-generation entries (using a funded transaction). Because
1960                    // the live entry count stays constant while dead records accumulate, major
1961                    // compaction must trigger automatically to reclaim space.
1962                    let candidates: Vec<usize> = (0..vols.len())
1963                        .filter(|&i| {
1964                            !vols[i].churn_gen.is_empty()
1965                                && fs
1966                                    .object_manager()
1967                                    .store(vols[i].store_id)
1968                                    .is_some_and(|s| !s.is_locked())
1969                        })
1970                        .collect();
1971                    let idx = candidates[rng.random_range(0..candidates.len())];
1972                    let store = fs.object_manager().store(vols[idx].store_id).unwrap();
1973                    let store_id = store.store_object_id();
1974                    let root_dir_id = store.root_directory_object_id();
1975
1976                    // Perform two 25-entry sub-batches per step so each transaction stays well
1977                    // within `TRANSACTION_MAX_JOURNAL_USAGE` (24 KiB) while generating enough
1978                    // churn (50 entries per step) to require major compactions in a 160-step run.
1979                    let churn_len = vols[idx].churn_gen.len();
1980                    for _ in 0..2 {
1981                        let start = vols[idx].churn_cursor;
1982                        vols[idx].churn_cursor = (start + 25) % churn_len;
1983
1984                        let mut del_txn = store
1985                            .new_transaction(
1986                                lock_keys![LockKey::object(store_id, root_dir_id)],
1987                                Options {
1988                                    skip_journal_checks: true,
1989                                    reservation: ReservationOptions::Borrowed,
1990                                    ..Default::default()
1991                                },
1992                            )
1993                            .await
1994                            .expect("borrowed base delete txn failed");
1995                        for offset in 0..25 {
1996                            let j = (start + offset) % churn_len;
1997                            let old_gen = vols[idx].churn_gen[j];
1998                            del_txn.add(
1999                                store_id,
2000                                Mutation::replace_or_insert_object(
2001                                    ObjectKey::extended_attribute(
2002                                        root_dir_id,
2003                                        format!("churn_{j:04}_gen_{old_gen}_{:<190}", j)
2004                                            .into_bytes(),
2005                                    ),
2006                                    ObjectValue::None,
2007                                ),
2008                            );
2009                        }
2010                        del_txn.commit().await.expect("borrowed base delete commit failed");
2011
2012                        let mut ins_txn = store
2013                            .new_transaction(
2014                                lock_keys![LockKey::object(store_id, root_dir_id)],
2015                                Options::default(),
2016                            )
2017                            .await
2018                            .unwrap_or_else(|e| {
2019                                panic!("funded base replacement txn failed (seed={seed:#x}): {e}")
2020                            });
2021                        for offset in 0..25 {
2022                            let j = (start + offset) % churn_len;
2023                            let new_gen = vols[idx].churn_gen[j] + 1;
2024                            vols[idx].churn_gen[j] = new_gen;
2025                            ins_txn.add(
2026                                store_id,
2027                                Mutation::replace_or_insert_object(
2028                                    ObjectKey::extended_attribute(
2029                                        root_dir_id,
2030                                        format!("churn_{j:04}_gen_{new_gen}_{:<190}", j)
2031                                            .into_bytes(),
2032                                    ),
2033                                    ObjectValue::inline_extended_attribute(vec![0x66; 200]),
2034                                ),
2035                            );
2036                        }
2037                        ins_txn.commit().await.expect("funded base replacement commit failed");
2038                    }
2039                } else if op < 84 {
2040                    // 6) Lock / unlock / flush-while-locked an encrypted volume (`vol_0` and
2041                    // `vol_1` are permanent and encrypted, so `encrypted` is never empty).
2042                    let encrypted: Vec<usize> =
2043                        (0..vols.len()).filter(|&i| vols[i].crypt.is_some()).collect();
2044                    let idx = encrypted[rng.random_range(0..encrypted.len())];
2045                    let crypt = vols[idx].crypt.clone().unwrap();
2046                    let store = fs.object_manager().store(vols[idx].store_id).unwrap();
2047                    if store.is_locked() {
2048                        match rng.random_range(0..3) {
2049                            0 => {
2050                                store.flush().await.expect("locked flush failed");
2051                            }
2052                            1 => {
2053                                store
2054                                    .unlock_read_only(crypt)
2055                                    .await
2056                                    .expect("unlock_read_only failed");
2057                                store.lock_read_only();
2058                            }
2059                            _ => {
2060                                store.unlock(crypt).await.expect("unlock failed");
2061                            }
2062                        }
2063                    } else {
2064                        fs.graveyard().flush().await;
2065                        store.lock().await.expect("lock failed");
2066                        if rng.random_bool(0.5) {
2067                            store.flush().await.expect("locked flush failed");
2068                        }
2069                    }
2070                } else if op < 91 {
2071                    // 7) Explicit minor flush (`ForceMajor::False`) or journal compaction. Never
2072                    // pass `ForceMajor::True` here so major compaction is only triggered when
2073                    // `should_force_major_compaction` decides it is required.
2074                    fs.journal().pause_compactions().await;
2075                    let idx = rng.random_range(0..vols.len());
2076                    let store = fs.object_manager().store(vols[idx].store_id).unwrap();
2077                    match rng.random_range(0..3) {
2078                        0 => {
2079                            store
2080                                .flush_with_reason(FlushReason::Journal(ForceMajor::False))
2081                                .await
2082                                .expect("minor flush failed");
2083                        }
2084                        1 => {
2085                            fs.object_manager()
2086                                .flush(FlushReason::Journal(ForceMajor::False))
2087                                .await
2088                                .expect("object_manager flush failed");
2089                        }
2090                        _ => {
2091                            fs.journal().force_compact().await.expect("force_compact failed");
2092                        }
2093                    }
2094                    fs.journal().resume_compactions();
2095                } else if op < 95 {
2096                    // 8) Dynamically create or delete a child volume (at most 1 dynamic volume at
2097                    // a time so volume reservations stay within the tight disk headroom).
2098                    let root_vol = root_volume(fs.clone()).await.expect("root_volume failed");
2099                    let dynamic_indices: Vec<usize> =
2100                        (0..vols.len()).filter(|&i| !vols[i].permanent).collect();
2101                    if dynamic_indices.is_empty() {
2102                        let encrypted = rng.random_bool(0.5);
2103                        let crypt: Option<Arc<dyn Crypt>> =
2104                            encrypted.then(|| Arc::new(new_insecure_crypt()) as _);
2105                        let name = format!("dyn_{next_id}");
2106                        let store = root_vol
2107                            .new_volume(
2108                                &name,
2109                                NewChildStoreOptions {
2110                                    options: StoreOptions {
2111                                        crypt: crypt.clone(),
2112                                        ..StoreOptions::default()
2113                                    },
2114                                    ..NewChildStoreOptions::default()
2115                                },
2116                            )
2117                            .await
2118                            .expect("dynamic new_volume failed");
2119                        let store_id = store.store_object_id();
2120                        let root_dir_id = store.root_directory_object_id();
2121                        let mut txn = store
2122                            .new_transaction(
2123                                lock_keys![LockKey::object(store_id, root_dir_id)],
2124                                Options::default(),
2125                            )
2126                            .await
2127                            .expect("dynamic volume txn failed");
2128                        txn.add(
2129                            store_id,
2130                            Mutation::replace_or_insert_object(
2131                                ObjectKey::extended_attribute(root_dir_id, b"dyn_attr".to_vec()),
2132                                ObjectValue::inline_extended_attribute(vec![0x33; 256]),
2133                            ),
2134                        );
2135                        txn.commit().await.expect("dynamic volume commit failed");
2136                        vols.push(VolState {
2137                            name,
2138                            store_id,
2139                            crypt,
2140                            permanent: false,
2141                            churn_cursor: 0,
2142                            churn_gen: Vec::new(),
2143                        });
2144                    } else {
2145                        fs.graveyard().flush().await;
2146                        let idx = dynamic_indices[rng.random_range(0..dynamic_indices.len())];
2147                        let vol = vols.remove(idx);
2148                        let (_, delete_txn) = root_vol
2149                            .acquire_transaction_for_remove_volume(
2150                                &vol.name,
2151                                [],
2152                                true,
2153                                Options {
2154                                    reservation: ReservationOptions::Borrowed,
2155                                    ..Default::default()
2156                                },
2157                            )
2158                            .await
2159                            .expect("acquire_transaction_for_remove_volume failed");
2160                        root_vol
2161                            .delete_volume(&vol.name, delete_txn, || {})
2162                            .await
2163                            .expect("delete_volume failed");
2164                        assert!(fs.object_manager().store_borrowed(vol.store_id).is_none());
2165                    }
2166                } else {
2167                    // 9) Remount (clean close, synced snapshot replay, or power-fail discard).
2168                    fs.graveyard().flush().await;
2169                    let remount_kind = rng.random_range(0..3);
2170                    if remount_kind == 0 {
2171                        // Power-fail snapshot with random discarded blocks since last flush.
2172                        fs.journal().pause_compactions().await;
2173                        fs.sync(SyncOptions::default()).await.expect("sync failed");
2174                        let snapshot = fs.device().snapshot().expect("snapshot failed");
2175                        fs.journal().resume_compactions();
2176                        snapshot
2177                            .discard_random_since_last_flush()
2178                            .expect("discard_random_since_last_flush failed");
2179                        let pf_fs = FxFilesystem::open(snapshot).await.expect("open pf_fs failed");
2180                        check_all_volumes(&pf_fs, &vols).await;
2181                        pf_fs.close().await.expect("close pf_fs failed");
2182                    } else {
2183                        if remount_kind == 1 {
2184                            // Unclean synced snapshot remount (replays unflushed journal
2185                            // transactions, locked flushes, and compactions) and continues on the
2186                            // replayed fs.
2187                            fs.journal().stop_compactions().await;
2188                            fs.sync(SyncOptions { flush_device: true, ..Default::default() })
2189                                .await
2190                                .expect("sync failed");
2191                            let snapshot = fs.device().snapshot().expect("snapshot failed");
2192                            let expected_reserved = fs.object_manager().reserved_metadata_space();
2193                            fs.close().await.expect("close old fs failed");
2194                            fs = FxFilesystemBuilder::new()
2195                                .journal_options(JournalOptions {
2196                                    reclaim_size,
2197                                    ..Default::default()
2198                                })
2199                                .open(snapshot)
2200                                .await
2201                                .expect("open snapshot failed");
2202                            assert_eq!(
2203                                fs.object_manager().reserved_metadata_space(),
2204                                expected_reserved,
2205                                "replayed reserved_metadata_space mismatch on snapshot remount \
2206                                 (seed={seed:#x})"
2207                            );
2208                        } else {
2209                            // Clean close and remount.
2210                            fs.close().await.expect("close failed");
2211                            let device = fs.take_device().await;
2212                            device.reopen(false);
2213                            fs = FxFilesystemBuilder::new()
2214                                .journal_options(JournalOptions {
2215                                    reclaim_size,
2216                                    ..Default::default()
2217                                })
2218                                .open(device)
2219                                .await
2220                                .expect("open failed");
2221                        }
2222                        check_all_volumes(&fs, &vols).await;
2223                        for vol in &vols {
2224                            if let Some(crypt) = &vol.crypt
2225                                && rng.random_bool(0.5)
2226                            {
2227                                let store = fs.object_manager().store(vol.store_id).unwrap();
2228                                store.unlock(crypt.clone()).await.expect("unlock failed");
2229                            }
2230                        }
2231                    }
2232                }
2233            }
2234
2235            // End-of-episode full compaction & invariant verification.
2236            fs.graveyard().flush().await;
2237            fs.journal().pause_compactions().await;
2238            for vol in &vols {
2239                if let Some(crypt) = &vol.crypt {
2240                    let store = fs.object_manager().store(vol.store_id).unwrap();
2241                    if store.is_locked() {
2242                        store.unlock(crypt.clone()).await.expect("unlock failed");
2243                    }
2244                }
2245            }
2246            fs.object_manager()
2247                .flush(FlushReason::Journal(ForceMajor::True))
2248                .await
2249                .expect("full flush failed");
2250            fs.journal().force_compact().await.expect("force_compact failed");
2251            fs.journal().force_compact().await.expect("force_compact failed");
2252            let mut layers_before = Vec::with_capacity(vols.len());
2253            for vol in &vols {
2254                let store = fs.object_manager().store(vol.store_id).unwrap();
2255                let info = store.load_store_info().await.unwrap();
2256                assert!(info.layers.len() <= 1);
2257                assert_eq!(info.encrypted_mutations_object_id, INVALID_OBJECT_ID);
2258                let (b, _) = fs.object_manager().store_borrowed(vol.store_id).unwrap();
2259                assert_eq!(b, 0);
2260                layers_before.push((vol.store_id, info.layers));
2261            }
2262            fs.object_manager()
2263                .flush(FlushReason::Journal(ForceMajor::False))
2264                .await
2265                .expect("noop flush failed");
2266            for (store_id, expected_layers) in layers_before {
2267                let store = fs.object_manager().store(store_id).unwrap();
2268                assert_eq!(store.load_store_info().await.unwrap().layers, expected_layers);
2269            }
2270            check_all_volumes(&fs, &vols).await;
2271            fs.close().await.expect("close failed");
2272
2273            episode += 1;
2274            if stress_secs == 0 || start_time.elapsed().as_secs() >= stress_secs {
2275                break;
2276            }
2277        }
2278    }
2279
2280    #[fuchsia::test]
2281    async fn test_required_reservation_earliest_offset_saturating_sub() {
2282        struct CheckRequiredReservation;
2283        impl AssociatedObject for CheckRequiredReservation {
2284            fn will_apply_mutation(
2285                &self,
2286                _mutation: &Mutation,
2287                _object_id: u64,
2288                manager: &ObjectManager,
2289            ) {
2290                let earliest_offset = manager.inner.read().earliest_journal_offset().unwrap();
2291                assert!(earliest_offset > manager.last_end_offset());
2292                let _ = manager.borrowed_metadata_space();
2293            }
2294        }
2295
2296        let device = DeviceHolder::new(FakeDevice::new(8192, 512));
2297        let fs = FxFilesystem::new_empty(device).await.expect("new_empty failed");
2298
2299        fs.journal().force_compact().await.expect("compact");
2300        let allocator_id = fs.allocator().object_id();
2301        assert!(!fs.object_manager().needs_flush(fs.root_store().store_object_id()));
2302
2303        // Clear allocator (5) from `journal_checkpoints` via BeginFlush + EndFlush (simulating a
2304        // clean superblock with empty `journal_file_offsets`).
2305        let txn_opts = Options {
2306            skip_journal_checks: true,
2307            reservation: ReservationOptions::Borrowed,
2308            ..Default::default()
2309        };
2310        for mutation in [Mutation::BeginFlush, Mutation::EndFlush(0)] {
2311            let mut transaction =
2312                fs.root_store().new_transaction(lock_keys![], txn_opts).await.expect("new_txn");
2313            transaction.add(allocator_id, mutation);
2314            transaction.commit().await.expect("commit");
2315        }
2316        assert!(
2317            !fs.object_manager().needs_flush(allocator_id),
2318            "journal_checkpoints should now be completely empty"
2319        );
2320
2321        // Pad the journal to the next 4 KiB block boundary via `sync()`. Because
2322        // `journal_checkpoints` is empty, `needs_borrow_for_journal` must return `false` even
2323        // when the checkpoint advances by more than a block, leaving `last_end_offset` prior to
2324        // the padded block boundary (`checkpoint.file_offset > last_end_offset`).
2325        let checkpoint_before_pad =
2326            fs.journal().sync(SyncOptions::default()).await.expect("sync").unwrap();
2327        let next_block_offset = checkpoint_before_pad.file_offset.next_multiple_of(4096);
2328        assert!(
2329            !fs.object_manager().needs_borrow_for_journal(next_block_offset + 8192),
2330            "needs_borrow_for_journal should return false when journal_checkpoints is empty"
2331        );
2332
2333        let reserved_before = fs.object_manager().metadata_reservation().amount();
2334        let borrowed_before = fs.object_manager().borrowed_metadata_space();
2335        let required_before = reserved_before + borrowed_before;
2336
2337        // Committing a transaction when `min > self.last_end_offset` must not underflow in
2338        // `ObjectManager::Inner::total_journal_usage` and must only charge `txn_space` for journal
2339        // bytes at or after `min`.
2340        let check_obj = CheckRequiredReservation;
2341        let mut transaction =
2342            fs.root_store().new_transaction(lock_keys![], Options::default()).await.expect("txn");
2343        let handle1 = ObjectStore::create_object(
2344            &fs.root_store(),
2345            &mut transaction,
2346            HandleOptions::default(),
2347            None,
2348        )
2349        .await
2350        .expect("create_object");
2351        let handle2 = ObjectStore::create_object(
2352            &fs.root_store(),
2353            &mut transaction,
2354            HandleOptions::default(),
2355            None,
2356        )
2357        .await
2358        .expect("create_object");
2359        let second_id = std::cmp::max(handle1.object_id(), handle2.object_id());
2360        transaction.add_with_object(
2361            fs.root_store().store_object_id(),
2362            Mutation::merge_object(ObjectKey::object(second_id), ObjectValue::None),
2363            AssocObj::Borrowed(&check_obj),
2364        );
2365        transaction.commit().await.expect("commit");
2366
2367        let reserved_after = fs.object_manager().metadata_reservation().amount();
2368        let borrowed_after = fs.object_manager().borrowed_metadata_space();
2369        let required_after = reserved_after + borrowed_after;
2370        assert_eq!(
2371            reserved_after - reserved_before,
2372            required_after - required_before,
2373            "Non-borrowed transaction should increase metadata_reservation by the exact increase in required_reservation"
2374        );
2375
2376        fs.close().await.expect("close failed");
2377    }
2378
2379    #[fuchsia::test]
2380    async fn test_sync_padding_paid_by_next_transaction() {
2381        let device = DeviceHolder::new(FakeDevice::new(8192, 512));
2382        let fs = FxFilesystem::new_empty(device).await.expect("new_empty failed");
2383        fs.journal().force_compact().await.expect("compact");
2384
2385        // Commit a small funded transaction so `root_store` has an active entry in
2386        // `journal_checkpoints` and the journal writer is near the start of a 4 KiB block.
2387        let mut transaction =
2388            fs.root_store().new_transaction(lock_keys![], Options::default()).await.expect("txn");
2389        let handle = ObjectStore::create_object(
2390            &fs.root_store(),
2391            &mut transaction,
2392            HandleOptions::default(),
2393            None,
2394        )
2395        .await
2396        .expect("create_object");
2397        transaction.commit().await.expect("commit");
2398        let borrowed_before = fs.object_manager().borrowed_metadata_space();
2399
2400        // `sync()` pads the remainder of the 4 KiB journal block (> 256 bytes) and queues a
2401        // `DidFlushDevice` record. The next funded transaction should pay for this padding and
2402        // overhead rather than `pre_commit` borrowing it from `b_sys`.
2403        let checkpoint_before_pad = fs
2404            .journal()
2405            .sync(SyncOptions { flush_device: true, ..Default::default() })
2406            .await
2407            .expect("sync")
2408            .unwrap();
2409        let padded_bytes = checkpoint_before_pad.file_offset.next_multiple_of(4096)
2410            - checkpoint_before_pad.file_offset;
2411        assert!(padded_bytes > 256, "expected > 256 bytes of block padding, got {padded_bytes}");
2412
2413        let mut transaction = fs
2414            .root_store()
2415            .new_transaction(
2416                lock_keys![LockKey::object(fs.root_store().store_object_id(), handle.object_id())],
2417                Options::default(),
2418            )
2419            .await
2420            .expect("txn");
2421        transaction.add(
2422            fs.root_store().store_object_id(),
2423            Mutation::merge_object(ObjectKey::object(handle.object_id()), ObjectValue::None),
2424        );
2425        transaction.commit().await.expect("commit");
2426
2427        let borrowed_after = fs.object_manager().borrowed_metadata_space();
2428        assert_eq!(
2429            borrowed_after, borrowed_before,
2430            "Funded transaction after sync() should pay for block padding rather than borrowing from metadata_reservation"
2431        );
2432
2433        fs.close().await.expect("close failed");
2434    }
2435}