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}