Skip to main content

reovim_kernel/ipc/
scope.rs

1//! Event scope for lifecycle tracking.
2//!
3//! `EventScope` provides GC-like synchronization for tracking event lifecycles.
4//! It enables callers to wait until all events dispatched within a scope have
5//! been fully processed.
6//!
7//! # Design Philosophy
8//!
9//! This is the **only** synchronization mechanism for event lifecycle tracking.
10//! It uses a simple reference-counting pattern:
11//! - `increment()` when an event is emitted
12//! - `decrement()` when event dispatch completes
13//! - `wait()` blocks until counter reaches zero
14//!
15//! # Use Case
16//!
17//! The primary use case is RPC key injection, where the caller needs to wait
18//! for all effects of injected keys to complete before returning a response.
19//!
20//! # Example
21//!
22//! ```
23//! use reovim_kernel::api::v1::*;
24//! use std::time::Duration;
25//!
26//! let scope = EventScope::new();
27//!
28//! // Simulate emitting events
29//! scope.increment();
30//! scope.increment();
31//! assert_eq!(scope.in_flight(), 2);
32//!
33//! // Simulate event completion
34//! scope.decrement();
35//! scope.decrement();
36//! assert!(scope.is_complete());
37//!
38//! // wait() returns immediately when counter is 0
39//! scope.wait();
40//! ```
41
42use std::{
43    fmt,
44    sync::{
45        Arc,
46        atomic::{AtomicU64, AtomicUsize, Ordering},
47    },
48    time::Duration,
49};
50
51use reovim_arch::sync::{Condvar, Mutex};
52
53/// Unique identifier for an `EventScope`.
54///
55/// Used for debugging and tracing to distinguish between concurrent scopes.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57pub struct ScopeId(u64);
58
59impl ScopeId {
60    /// Create a new unique `ScopeId`.
61    pub(crate) fn new() -> Self {
62        static COUNTER: AtomicU64 = AtomicU64::new(1);
63        Self(COUNTER.fetch_add(1, Ordering::Relaxed))
64    }
65
66    /// Get the raw ID value.
67    #[inline]
68    #[must_use]
69    pub const fn as_u64(&self) -> u64 {
70        self.0
71    }
72}
73
74impl fmt::Display for ScopeId {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        write!(f, "scope-{}", self.0)
77    }
78}
79
80/// Internal state shared between `EventScope` clones.
81struct ScopeInner {
82    /// Unique identifier for this scope.
83    id: ScopeId,
84
85    /// Count of in-flight events.
86    in_flight: AtomicUsize,
87
88    /// Condition variable for waiting.
89    /// The Mutex holds a dummy value; we only need the condvar.
90    condvar: (Mutex<()>, Condvar),
91}
92
93/// GC-like synchronization for event lifecycle tracking.
94///
95/// `EventScope` tracks in-flight events and allows callers to wait until
96/// all events within the scope have been fully processed.
97///
98/// # Thread Safety
99///
100/// `EventScope` is `Clone`, `Send`, and `Sync`. Cloning creates a new handle
101/// to the same underlying scope - all clones share the same counter.
102///
103/// # Timeout Behavior
104///
105/// The proven production timeout is **3 seconds** (see `DEFAULT_TIMEOUT`).
106/// This is long enough to handle slow handlers while preventing deadlocks.
107///
108/// # Example
109///
110/// ```
111/// use reovim_kernel::api::v1::*;
112/// use std::thread;
113/// use std::time::Duration;
114///
115/// let scope = EventScope::new();
116/// let scope2 = scope.clone(); // Same underlying scope
117///
118/// // Simulate async event processing
119/// scope.increment();
120///
121/// let handle = thread::spawn(move || {
122///     thread::sleep(Duration::from_millis(10));
123///     scope2.decrement(); // Signal completion
124/// });
125///
126/// // Wait for all events to complete
127/// let completed = scope.wait_timeout(Duration::from_secs(1));
128/// assert!(completed);
129///
130/// handle.join().unwrap();
131/// ```
132#[derive(Clone)]
133pub struct EventScope {
134    inner: Arc<ScopeInner>,
135}
136
137/// Default timeout for scope waiting (3 seconds).
138///
139/// This is the proven production value, long enough for slow handlers
140/// (like tree-sitter compilation) while preventing infinite waits.
141pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(3);
142
143impl EventScope {
144    /// Create a new `EventScope` with counter initialized to 0.
145    ///
146    /// The scope is considered complete when `in_flight() == 0`.
147    #[must_use]
148    pub fn new() -> Self {
149        Self {
150            inner: Arc::new(ScopeInner {
151                id: ScopeId::new(),
152                in_flight: AtomicUsize::new(0),
153                condvar: (Mutex::new(()), Condvar::new()),
154            }),
155        }
156    }
157
158    /// Get the unique ID of this scope.
159    #[inline]
160    #[must_use]
161    pub fn id(&self) -> ScopeId {
162        self.inner.id
163    }
164
165    /// Increment the in-flight counter.
166    ///
167    /// Call this when emitting an event within the scope.
168    #[inline]
169    pub fn increment(&self) {
170        self.inner.in_flight.fetch_add(1, Ordering::SeqCst);
171    }
172
173    /// Decrement the in-flight counter.
174    ///
175    /// Call this when event dispatch completes. If the counter reaches 0,
176    /// all waiters are notified.
177    ///
178    /// # Panics
179    ///
180    /// Panics in debug mode if called when counter is already 0.
181    #[inline]
182    pub fn decrement(&self) {
183        let prev = self.inner.in_flight.fetch_sub(1, Ordering::SeqCst);
184        debug_assert!(prev > 0, "EventScope::decrement called when counter is 0");
185
186        // Notify waiters if we just reached 0
187        if prev == 1 {
188            let (_lock, condvar) = &self.inner.condvar;
189            condvar.notify_all();
190        }
191    }
192
193    /// Get the current in-flight count.
194    #[inline]
195    #[must_use]
196    pub fn in_flight(&self) -> usize {
197        self.inner.in_flight.load(Ordering::SeqCst)
198    }
199
200    /// Check if the scope is complete (no in-flight events).
201    #[inline]
202    #[must_use]
203    pub fn is_complete(&self) -> bool {
204        self.in_flight() == 0
205    }
206
207    /// Block until the in-flight counter reaches 0.
208    ///
209    /// If the counter is already 0, returns immediately.
210    ///
211    /// # Warning
212    ///
213    /// This can block indefinitely if events are never decremented.
214    /// Prefer `wait_timeout()` in production code.
215    pub fn wait(&self) {
216        let (mutex, condvar) = &self.inner.condvar;
217        let mut guard = mutex.lock();
218
219        while self.in_flight() > 0 {
220            condvar.wait(&mut guard);
221        }
222    }
223
224    /// Block until the in-flight counter reaches 0, with timeout.
225    ///
226    /// Returns `true` if the scope completed, `false` if the timeout elapsed.
227    ///
228    /// # Example
229    ///
230    /// ```
231    /// use reovim_kernel::api::v1::*;
232    /// use std::time::Duration;
233    ///
234    /// let scope = EventScope::new();
235    ///
236    /// // Already complete - returns immediately
237    /// assert!(scope.wait_timeout(Duration::from_millis(100)));
238    ///
239    /// // With in-flight event that never completes
240    /// scope.increment();
241    /// assert!(!scope.wait_timeout(Duration::from_millis(10)));
242    /// ```
243    #[must_use]
244    #[allow(clippy::significant_drop_tightening)] // Guard must be held for the wait loop
245    #[cfg_attr(coverage_nightly, coverage(off))]
246    pub fn wait_timeout(&self, timeout: Duration) -> bool {
247        let (mutex, condvar) = &self.inner.condvar;
248        let mut guard = mutex.lock();
249
250        let deadline = std::time::Instant::now() + timeout;
251
252        while self.in_flight() > 0 {
253            let remaining = deadline.saturating_duration_since(std::time::Instant::now());
254            if remaining.is_zero() {
255                return false;
256            }
257            let result = condvar.wait_for(&mut guard, remaining);
258            if result.timed_out() && self.in_flight() > 0 {
259                return false;
260            }
261        }
262
263        true
264    }
265
266    /// Block until complete, with default timeout and warning logging.
267    ///
268    /// Uses `DEFAULT_TIMEOUT` (3 seconds). Returns `true` if completed,
269    /// `false` if timed out.
270    ///
271    /// In production, this logs a warning if the timeout is reached.
272    /// For kernel-level code, we just return the result.
273    #[must_use]
274    pub fn wait_with_default_timeout(&self) -> bool {
275        self.wait_timeout(DEFAULT_TIMEOUT)
276    }
277}
278
279impl Default for EventScope {
280    fn default() -> Self {
281        Self::new()
282    }
283}
284
285impl fmt::Debug for EventScope {
286    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287        f.debug_struct("EventScope")
288            .field("id", &self.inner.id)
289            .field("in_flight", &self.in_flight())
290            .finish()
291    }
292}