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}
37
38struct SendOnDrop(mpsc::Sender<()>);
39
40impl Drop for SendOnDrop {
41    fn drop(&mut self) {
42        let _ = self.0.send(());
43    }
44}
45
46struct HooksInner {
47    table: Mutex<HooksTable>,
48    // `_send_on_drop` MUST be the last field in `HooksInner` so that Rust's struct field drop
49    // order drops `table` (and all registered closures) BEFORE `_send_on_drop` sends the
50    // completion signal.
51    _send_on_drop: SendOnDrop,
52}
53
54/// Used to register test hooks for an `FxFilesystem`.
55pub struct Hooks<'a> {
56    inner: Option<Arc<HooksInner>>,
57    receiver: mpsc::Receiver<()>,
58    _phantom: std::marker::PhantomData<&'a ()>,
59}
60
61impl<'a> Hooks<'a> {
62    /// Creates a new `Hooks` instance and an associated `HooksHandle`.
63    pub fn new() -> (Self, Arc<HooksHandle>) {
64        let (sender, receiver) = mpsc::channel();
65        let inner = Arc::new(HooksInner {
66            table: Mutex::new(HooksTable::default()),
67            _send_on_drop: SendOnDrop(sender),
68        });
69        let handle = Arc::new(HooksHandle { inner: Arc::downgrade(&inner) });
70        (Self { inner: Some(inner), receiver, _phantom: std::marker::PhantomData }, handle)
71    }
72
73    /// Sets the hook executed before committing a transaction.  This hook is called before
74    /// any locks required for the commit are taken, so it's before any transaction locks
75    /// are upgraded to write locks, before any guards that are taken by calling
76    /// `prepare_commit` for objects involved in the transaction, and before the commit mutex
77    /// is acquired.
78    pub fn set_pre_commit(
79        &mut self,
80        hook: impl Fn(&Transaction<'_>) -> Result<(), Error> + Send + Sync + 'a,
81    ) {
82        let boxed: Box<PreCommitHookFn<'a>> = Box::new(hook);
83        // SAFETY: `'a` is transmuted to `'static`. This is safe because `Hooks<'a>` owns the
84        // `Arc<HooksInner>` and its `detach` method (or `drop`) blocks until all `Arc<HooksInner>`
85        // references drop.
86        let static_hook: Box<PreCommitHookFn<'static>> = unsafe { std::mem::transmute(boxed) };
87        if let Some(inner) = &self.inner {
88            inner.table.lock().pre_commit = Some(Arc::from(static_hook));
89        }
90    }
91
92    /// Sets the hook executed before a transaction starts committing.  This hook is called
93    /// after calling `prepare_commit` on all objects involved in the transaction (which
94    /// might acquire some locks/guards), but before acquiring the commit mutex.
95    pub fn set_before_commit(&mut self, hook: impl Fn() + Send + Sync + 'a) {
96        let boxed: Box<SyncHookFn<'a>> = Box::new(hook);
97        // SAFETY: `'a` is transmuted to `'static`. This is safe because `Hooks<'a>` owns the
98        // `Arc<HooksInner>` and its `detach` method (or `drop`) blocks until all `Arc<HooksInner>`
99        // references drop.
100        let static_hook: Box<SyncHookFn<'static>> = unsafe { std::mem::transmute(boxed) };
101        if let Some(inner) = &self.inner {
102            inner.table.lock().before_commit = Some(Arc::from(static_hook));
103        }
104    }
105
106    /// Sets the hook executed when resources are acquired during unlock.  This is called
107    /// after acquiring all the keys that might be required from the crypt service.
108    pub fn set_unlock_resources_acquired(&mut self, hook: impl Fn() + Send + Sync + 'a) {
109        let boxed: Box<SyncHookFn<'a>> = Box::new(hook);
110        // SAFETY: `'a` is transmuted to `'static`. This is safe because `Hooks<'a>` owns the
111        // `Arc<HooksInner>` and its `detach` method (or `drop`) blocks until all `Arc<HooksInner>`
112        // references drop.
113        let static_hook: Box<SyncHookFn<'static>> = unsafe { std::mem::transmute(boxed) };
114        if let Some(inner) = &self.inner {
115            inner.table.lock().unlock_resources_acquired = Some(Arc::from(static_hook));
116        }
117    }
118
119    fn detach(&mut self) {
120        if let Some(inner) = self.inner.take() {
121            drop(inner);
122            let _ = self.receiver.recv();
123        }
124    }
125}
126
127impl Drop for Hooks<'_> {
128    fn drop(&mut self) {
129        self.detach();
130    }
131}
132
133/// Handle stored on `FxFilesystem` options to trigger registered test hooks.
134#[derive(Default)]
135pub struct HooksHandle {
136    inner: Weak<HooksInner>,
137}
138
139impl HooksHandle {
140    /// Invokes the registered `pre_commit` hook, if any.
141    pub fn on_pre_commit(&self, transaction: &Transaction<'_>) -> Result<(), Error> {
142        if let Some(inner) = self.inner.upgrade() {
143            // The strong is held until after the hook is called.
144            let hook = inner.table.lock().pre_commit.clone();
145            if let Some(hook) = hook {
146                return hook(transaction);
147            }
148        }
149        Ok(())
150    }
151
152    /// Invokes the registered `before_commit` hook, if any.
153    pub fn on_before_commit(&self) {
154        if let Some(inner) = self.inner.upgrade() {
155            // The strong is held until after the hook is called.
156            let hook = inner.table.lock().before_commit.clone();
157            if let Some(hook) = hook {
158                hook();
159            }
160        }
161    }
162
163    /// Invokes the registered `unlock_resources_acquired` hook, if any.
164    pub fn on_unlock_resources_acquired(&self) {
165        if let Some(inner) = self.inner.upgrade() {
166            // The strong is held until after the hook is called.
167            let hook = inner.table.lock().unlock_resources_acquired.clone();
168            if let Some(hook) = hook {
169                hook();
170            }
171        }
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use std::sync::atomic::{AtomicBool, Ordering};
179
180    #[test]
181    fn test_hooks_basic() {
182        let called = AtomicBool::new(false);
183        let (mut hooks, handle) = Hooks::new();
184
185        let called_ref = &called;
186        hooks.set_before_commit(move || {
187            called_ref.store(true, Ordering::Relaxed);
188        });
189
190        handle.on_before_commit();
191        assert!(called.load(Ordering::Relaxed));
192    }
193
194    #[test]
195    fn test_hooks_detached_on_drop() {
196        let called = AtomicBool::new(false);
197        let handle = {
198            let (mut hooks, handle) = Hooks::new();
199            let called_ref = &called;
200            hooks.set_before_commit(move || {
201                called_ref.store(true, Ordering::Relaxed);
202            });
203            handle
204        };
205
206        // hooks dropped here
207        handle.on_before_commit();
208        assert!(!called.load(Ordering::Relaxed));
209    }
210
211    #[test]
212    fn test_hooks_unlock_resources_acquired() {
213        let called = AtomicBool::new(false);
214        let (mut hooks, handle) = Hooks::new();
215
216        let called_ref = &called;
217        hooks.set_unlock_resources_acquired(move || {
218            called_ref.store(true, Ordering::Relaxed);
219        });
220
221        handle.on_unlock_resources_acquired();
222        assert!(called.load(Ordering::Relaxed));
223    }
224}