Skip to main content

fxfs/
hooks.rs

1// Copyright 2026 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//! Thread-safe filesystem test hooks.
6//!
7//! # Design Architecture & Invariants
8//!
9//! - **`Hooks<'a>` & `HooksHandle`**: `Hooks<'a>` is the exclusive mutator handle bound to lifetime
10//!   `'a` (the test context). `HooksHandle` is shared via `Arc` by `FxFilesystem` options to invoke
11//!   registered test callbacks (`on_*` methods) during transaction commits.
12//!
13//! - **Lifetime Safety**: Incoming hook closures bound to lifetime `'a` are transmuted to `'static`
14//!   and stored inside `HooksInner`. `Hooks<'a>` holds the primary `Arc<HooksInner>`, while
15//!   `HooksHandle` holds a `Weak<HooksInner>`.
16//!
17//! - **Cleanup & Synchronization**: When `Hooks<'a>` drops, it drops its `Arc<HooksInner>` and
18//!   waits for a message on an `mpsc` channel. Active readers (`on_*` methods) temporarily upgrade
19//!   the `Weak` reference to an `Arc`, ensuring `HooksInner` remains valid during callback
20//!   execution. When all readers finish and `HooksInner` drops, `SendOnDrop` (the last field of
21//!   `HooksInner`) drops after `HooksTable`, sending the completion signal to `detach()`.
22
23use crate::object_store::transaction::Transaction;
24use anyhow::Error;
25use fuchsia_sync::Mutex;
26use std::sync::{Arc, Weak, mpsc};
27
28type PreCommitHookFn<'a> = dyn Fn(&Transaction<'_>) -> Result<(), Error> + Send + Sync + 'a;
29type SyncHookFn<'a> = dyn Fn() + Send + Sync + 'a;
30
31#[derive(Default)]
32struct HooksTable {
33    pre_commit: Option<Arc<PreCommitHookFn<'static>>>,
34    before_commit: Option<Arc<SyncHookFn<'static>>>,
35    unlock_resources_acquired: Option<Arc<SyncHookFn<'static>>>,
36    waiting_for_journal_space: Option<Arc<SyncHookFn<'static>>>,
37}
38
39struct SendOnDrop(mpsc::Sender<()>);
40
41impl Drop for SendOnDrop {
42    fn drop(&mut self) {
43        let _ = self.0.send(());
44    }
45}
46
47struct HooksInner {
48    table: Mutex<HooksTable>,
49    // `_send_on_drop` MUST be the last field in `HooksInner` so that Rust's struct field drop
50    // order drops `table` (and all registered closures) BEFORE `_send_on_drop` sends the
51    // completion signal.
52    _send_on_drop: SendOnDrop,
53}
54
55/// Used to register test hooks for an `FxFilesystem`.
56pub struct Hooks<'a> {
57    inner: Option<Arc<HooksInner>>,
58    receiver: mpsc::Receiver<()>,
59    _phantom: std::marker::PhantomData<&'a ()>,
60}
61
62impl<'a> Hooks<'a> {
63    /// Creates a new `Hooks` instance and an associated `HooksHandle`.
64    pub fn new() -> (Self, Arc<HooksHandle>) {
65        let (sender, receiver) = mpsc::channel();
66        let inner = Arc::new(HooksInner {
67            table: Mutex::new(HooksTable::default()),
68            _send_on_drop: SendOnDrop(sender),
69        });
70        let handle = Arc::new(HooksHandle { inner: Arc::downgrade(&inner) });
71        (Self { inner: Some(inner), receiver, _phantom: std::marker::PhantomData }, handle)
72    }
73
74    /// Sets the hook executed before committing a transaction.  This hook is called before
75    /// any locks required for the commit are taken, so it's before any transaction locks
76    /// are upgraded to write locks, before any guards that are taken by calling
77    /// `prepare_commit` for objects involved in the transaction, and before the commit mutex
78    /// is acquired.
79    pub fn set_pre_commit(
80        &mut self,
81        hook: impl Fn(&Transaction<'_>) -> Result<(), Error> + Send + Sync + 'a,
82    ) {
83        let boxed: Box<PreCommitHookFn<'a>> = Box::new(hook);
84        // SAFETY: `'a` is transmuted to `'static`. This is safe because `Hooks<'a>` owns the
85        // `Arc<HooksInner>` and its `detach` method (or `drop`) blocks until all `Arc<HooksInner>`
86        // references drop.
87        let static_hook: Box<PreCommitHookFn<'static>> = unsafe { std::mem::transmute(boxed) };
88        if let Some(inner) = &self.inner {
89            inner.table.lock().pre_commit = Some(Arc::from(static_hook));
90        }
91    }
92
93    /// Sets the hook executed before a transaction starts committing.  This hook is called
94    /// after calling `prepare_commit` on all objects involved in the transaction (which
95    /// might acquire some locks/guards), but before acquiring the commit mutex.
96    pub fn set_before_commit(&mut self, hook: impl Fn() + Send + Sync + 'a) {
97        let boxed: Box<SyncHookFn<'a>> = Box::new(hook);
98        // SAFETY: `'a` is transmuted to `'static`. This is safe because `Hooks<'a>` owns the
99        // `Arc<HooksInner>` and its `detach` method (or `drop`) blocks until all `Arc<HooksInner>`
100        // references drop.
101        let static_hook: Box<SyncHookFn<'static>> = unsafe { std::mem::transmute(boxed) };
102        if let Some(inner) = &self.inner {
103            inner.table.lock().before_commit = Some(Arc::from(static_hook));
104        }
105    }
106
107    /// Sets the hook executed when resources are acquired during unlock.  This is called
108    /// after acquiring all the keys that might be required from the crypt service.
109    pub fn set_unlock_resources_acquired(&mut self, hook: impl Fn() + Send + Sync + 'a) {
110        let boxed: Box<SyncHookFn<'a>> = Box::new(hook);
111        // SAFETY: `'a` is transmuted to `'static`. This is safe because `Hooks<'a>` owns the
112        // `Arc<HooksInner>` and its `detach` method (or `drop`) blocks until all `Arc<HooksInner>`
113        // references drop.
114        let static_hook: Box<SyncHookFn<'static>> = unsafe { std::mem::transmute(boxed) };
115        if let Some(inner) = &self.inner {
116            inner.table.lock().unlock_resources_acquired = Some(Arc::from(static_hook));
117        }
118    }
119
120    /// Sets the hook executed when a transaction or flush operation blocks waiting for journal
121    /// space.
122    pub fn set_waiting_for_journal_space(&mut self, hook: impl Fn() + Send + Sync + 'a) {
123        let boxed: Box<SyncHookFn<'a>> = Box::new(hook);
124        // SAFETY: `'a` is transmuted to `'static`. This is safe because `Hooks<'a>` owns the
125        // `Arc<HooksInner>` and its `detach` method (or `drop`) blocks until all `Arc<HooksInner>`
126        // references drop.
127        let static_hook: Box<SyncHookFn<'static>> = unsafe { std::mem::transmute(boxed) };
128        if let Some(inner) = &self.inner {
129            inner.table.lock().waiting_for_journal_space = Some(Arc::from(static_hook));
130        }
131    }
132
133    fn detach(&mut self) {
134        if let Some(inner) = self.inner.take() {
135            drop(inner);
136            let _ = self.receiver.recv();
137        }
138    }
139}
140
141impl Drop for Hooks<'_> {
142    fn drop(&mut self) {
143        self.detach();
144    }
145}
146
147/// Handle stored on `FxFilesystem` options to trigger registered test hooks.
148#[derive(Default)]
149pub struct HooksHandle {
150    inner: Weak<HooksInner>,
151}
152
153impl HooksHandle {
154    /// Invokes the registered `pre_commit` hook, if any.
155    pub fn on_pre_commit(&self, transaction: &Transaction<'_>) -> Result<(), Error> {
156        if let Some(inner) = self.inner.upgrade() {
157            // The strong is held until after the hook is called.
158            let hook = inner.table.lock().pre_commit.clone();
159            if let Some(hook) = hook {
160                return hook(transaction);
161            }
162        }
163        Ok(())
164    }
165
166    /// Invokes the registered `before_commit` hook, if any.
167    pub fn on_before_commit(&self) {
168        if let Some(inner) = self.inner.upgrade() {
169            // The strong is held until after the hook is called.
170            let hook = inner.table.lock().before_commit.clone();
171            if let Some(hook) = hook {
172                hook();
173            }
174        }
175    }
176
177    /// Invokes the registered `unlock_resources_acquired` hook, if any.
178    pub fn on_unlock_resources_acquired(&self) {
179        if let Some(inner) = self.inner.upgrade() {
180            // The strong is held until after the hook is called.
181            let hook = inner.table.lock().unlock_resources_acquired.clone();
182            if let Some(hook) = hook {
183                hook();
184            }
185        }
186    }
187
188    /// Invokes the registered `waiting_for_journal_space` hook, if any.
189    pub fn on_waiting_for_journal_space(&self) {
190        if let Some(inner) = self.inner.upgrade() {
191            // The strong is held until after the hook is called.
192            let hook = inner.table.lock().waiting_for_journal_space.clone();
193            if let Some(hook) = hook {
194                hook();
195            }
196        }
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use std::sync::atomic::{AtomicBool, Ordering};
204
205    #[test]
206    fn test_hooks_basic() {
207        let called = AtomicBool::new(false);
208        let (mut hooks, handle) = Hooks::new();
209
210        let called_ref = &called;
211        hooks.set_before_commit(move || {
212            called_ref.store(true, Ordering::Relaxed);
213        });
214
215        handle.on_before_commit();
216        assert!(called.load(Ordering::Relaxed));
217    }
218
219    #[test]
220    fn test_hooks_detached_on_drop() {
221        let called = AtomicBool::new(false);
222        let handle = {
223            let (mut hooks, handle) = Hooks::new();
224            let called_ref = &called;
225            hooks.set_before_commit(move || {
226                called_ref.store(true, Ordering::Relaxed);
227            });
228            handle
229        };
230
231        // hooks dropped here
232        handle.on_before_commit();
233        assert!(!called.load(Ordering::Relaxed));
234    }
235
236    #[test]
237    fn test_hooks_unlock_resources_acquired() {
238        let called = AtomicBool::new(false);
239        let (mut hooks, handle) = Hooks::new();
240
241        let called_ref = &called;
242        hooks.set_unlock_resources_acquired(move || {
243            called_ref.store(true, Ordering::Relaxed);
244        });
245
246        handle.on_unlock_resources_acquired();
247        assert!(called.load(Ordering::Relaxed));
248    }
249
250    #[test]
251    fn test_hooks_waiting_for_journal_space() {
252        let called = AtomicBool::new(false);
253        let (mut hooks, handle) = Hooks::new();
254
255        let called_ref = &called;
256        hooks.set_waiting_for_journal_space(move || {
257            called_ref.store(true, Ordering::Relaxed);
258        });
259
260        handle.on_waiting_for_journal_space();
261        assert!(called.load(Ordering::Relaxed));
262    }
263}