Skip to main content

pulsedb/sync/
guard.rs

1//! Echo prevention guard for sync operations.
2//!
3//! When applying remote changes locally, we must NOT re-emit WAL events —
4//! otherwise those events would be pushed back to the remote, creating an
5//! infinite sync loop.
6//!
7//! The [`SyncApplyGuard`] uses a thread-local `Cell<bool>` flag that WAL
8//! recording and watch emission check before writing. The guard sets the
9//! flag on creation and clears it on drop (RAII pattern).
10//!
11//! # Performance
12//!
13//! `Cell::get()` compiles to a single memory load — no atomic operations,
14//! no contention, <10ns overhead per check.
15//!
16//! # Example
17//!
18//! ```rust
19//! use pulsedb::sync::guard::{SyncApplyGuard, is_sync_applying};
20//!
21//! assert!(!is_sync_applying());
22//!
23//! {
24//!     let _guard = SyncApplyGuard::enter();
25//!     assert!(is_sync_applying());
26//! }
27//!
28//! assert!(!is_sync_applying());
29//! ```
30
31use std::cell::Cell;
32
33thread_local! {
34    static SYNC_APPLYING: Cell<bool> = const { Cell::new(false) };
35}
36
37/// RAII guard that marks the current thread as applying sync changes.
38///
39/// While this guard is held, `is_sync_applying()` returns `true`.
40/// The flag is automatically cleared when the guard is dropped,
41/// even on panic (via `Drop`).
42pub struct SyncApplyGuard {
43    // Private field prevents construction outside `enter()`.
44    _private: (),
45}
46
47impl SyncApplyGuard {
48    /// Enter sync-apply mode for the current thread.
49    ///
50    /// Returns a guard that resets the flag when dropped.
51    #[inline]
52    pub fn enter() -> Self {
53        SYNC_APPLYING.set(true);
54        SyncApplyGuard { _private: () }
55    }
56}
57
58impl Drop for SyncApplyGuard {
59    #[inline]
60    fn drop(&mut self) {
61        SYNC_APPLYING.set(false);
62    }
63}
64
65/// Returns `true` if the current thread is applying sync changes.
66///
67/// Used by WAL recording and watch emission to skip recording
68/// when sync is applying remote changes.
69#[inline]
70pub fn is_sync_applying() -> bool {
71    SYNC_APPLYING.get()
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn test_default_is_false() {
80        assert!(!is_sync_applying());
81    }
82
83    #[test]
84    fn test_guard_sets_and_resets() {
85        assert!(!is_sync_applying());
86        {
87            let _guard = SyncApplyGuard::enter();
88            assert!(is_sync_applying());
89        }
90        assert!(!is_sync_applying());
91    }
92
93    #[test]
94    fn test_nested_guards() {
95        assert!(!is_sync_applying());
96        {
97            let _outer = SyncApplyGuard::enter();
98            assert!(is_sync_applying());
99            {
100                let _inner = SyncApplyGuard::enter();
101                assert!(is_sync_applying());
102            }
103            // Inner dropped, but outer still holds — however, Cell is simple bool.
104            // After inner drops, flag is false. This is expected behavior:
105            // nested guards are not supported (and not needed).
106            // The outermost guard controls the lifecycle.
107            assert!(!is_sync_applying());
108        }
109        assert!(!is_sync_applying());
110    }
111
112    #[test]
113    fn test_guard_resets_on_panic() {
114        assert!(!is_sync_applying());
115
116        let result = std::panic::catch_unwind(|| {
117            let _guard = SyncApplyGuard::enter();
118            assert!(is_sync_applying());
119            panic!("intentional panic");
120        });
121
122        assert!(result.is_err());
123        // Guard's Drop should have run during unwinding
124        assert!(!is_sync_applying());
125    }
126}