Skip to main content

lattice_mode/
guards.rs

1//! `GuardStore`: type-erased storage for per-`(buffer, mode)`
2//! Guards returned by [`Mode::on_activate`](crate::Mode::on_activate).
3//!
4//! The dispatcher stashes each successful activation's Guard in
5//! this store, keyed by `(BufferId, ModeId)`. On deactivation,
6//! the dispatcher removes the entry and drops the boxed Guard;
7//! the Guard's `Drop` impl performs cleanup (unsubscribe,
8//! restore prior option value, drop supervisor handle, ...).
9//!
10//! Storage is `Box<dyn Any + Send>` because each mode's Guard
11//! type is different. The dispatcher never downcasts -- dropping
12//! through the `dyn Any` trait object correctly invokes the
13//! original type's `Drop` via the vtable.
14//!
15//! M-async.2: the store is accessed from two threads -- the App
16//! thread (synchronous deactivate path; activation's sync
17//! prefix) and the tokio worker that runs the spawned lifecycle
18//! future (inserts the Guard when `on_activate` resolves). The
19//! [`GuardStoreHandle`] wraps the store in `Arc<Mutex<...>>` so
20//! both threads can lock briefly without `&mut` lifetime
21//! gymnastics. The App owns one handle; the dispatcher clones
22//! it into each spawned task.
23
24use std::any::Any;
25use std::collections::HashMap;
26use std::sync::{Arc, Mutex};
27
28use lattice_protocol::ids::BufferId;
29
30use crate::mode::ModeId;
31
32/// Type-erased per-`(buffer, mode)` Guard storage.
33///
34/// One per App, and in practice always behind a [`GuardStoreHandle`]
35/// (the registry's activation / deactivation methods take the handle, so
36/// the spawned lifecycle task can insert into the same store). Default is
37/// empty.
38///
39/// Not `Clone` -- `Box<dyn Any>` is not `Clone`.
40///
41/// **M-async.4 epoch counter:** each `(buffer, mode)` key
42/// carries a `u64` epoch that monotonically increments on every
43/// activate begin + every deactivate. The dispatcher's spawn
44/// task captures the epoch when it queues, then validates
45/// against the current epoch via [`Self::try_insert`] before
46/// stashing its Guard. A mismatch means a deactivate (or a
47/// later activate) arrived while the spawn was in flight; the
48/// returned `Err(stale_guard)` lets the spawn drop the Guard
49/// (firing its Drop for out-of-band cleanup) instead of
50/// stashing it in a logically-inactive store slot.
51#[derive(Default)]
52pub struct GuardStore {
53    map: HashMap<(BufferId, ModeId), Box<dyn Any + Send>>,
54    epochs: HashMap<(BufferId, ModeId), u64>,
55}
56
57impl std::fmt::Debug for GuardStore {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        f.debug_struct("GuardStore")
60            .field("count", &self.map.len())
61            .finish_non_exhaustive()
62    }
63}
64
65impl GuardStore {
66    /// Empty store.
67    pub fn new() -> Self {
68        Self::default()
69    }
70
71    /// Stash a Guard for the given `(buffer, mode)`. Replaces
72    /// any existing entry (the old Guard is dropped, firing its
73    /// `Drop` impl). Used in tests + the rare reload path; the
74    /// production dispatcher routes through [`Self::try_insert`]
75    /// to respect the epoch invariant.
76    pub fn insert(&mut self, buffer: BufferId, mode: ModeId, guard: Box<dyn Any + Send>) {
77        self.map.insert((buffer, mode), guard);
78    }
79
80    /// Bump the epoch for `(buffer, mode)` and return the new
81    /// value. The dispatcher's sync prefix calls this when
82    /// queueing a step; the spawn task captures the returned
83    /// value and passes it to [`Self::try_insert`] on
84    /// completion. Wraps on overflow (`u64::MAX → 0`); the
85    /// dispatcher tolerates this because consecutive bumps
86    /// always advance by 1, so a wrap that happens to land on
87    /// a stale spawn's captured epoch would require 2^64
88    /// activate / deactivate cycles in flight -- not a
89    /// realistic concern.
90    pub fn bump_epoch(&mut self, buffer: BufferId, mode: ModeId) -> u64 {
91        let entry = self.epochs.entry((buffer, mode)).or_insert(0);
92        *entry = entry.wrapping_add(1);
93        *entry
94    }
95
96    /// Current epoch for `(buffer, mode)`. `0` if the pair has
97    /// never had an activation queued. Used by tests + the
98    /// dispatcher's spawn task to validate before stashing.
99    pub fn current_epoch(&self, buffer: BufferId, mode: ModeId) -> u64 {
100        self.epochs.get(&(buffer, mode)).copied().unwrap_or(0)
101    }
102
103    /// Insert `guard` only if `my_epoch` still matches the
104    /// store's current epoch for `(buffer, mode)`. Returns
105    /// `Ok(())` on success; on epoch mismatch returns
106    /// `Err(guard)` so the caller can drop the Guard outside
107    /// the lock (the Box's `Drop` then fires the original
108    /// type's cleanup).
109    ///
110    /// Used by the M-async.4 spawn-task path: a deactivate
111    /// (or a subsequent activate) arriving while a spawn was
112    /// in flight bumps the epoch via [`Self::remove`] /
113    /// [`Self::bump_epoch`]; the spawn's late `try_insert`
114    /// then fails the match and drops the Guard instead of
115    /// stashing it in a logically-inactive store slot.
116    pub fn try_insert(
117        &mut self,
118        buffer: BufferId,
119        mode: ModeId,
120        my_epoch: u64,
121        guard: Box<dyn Any + Send>,
122    ) -> Result<(), Box<dyn Any + Send>> {
123        if self.current_epoch(buffer, mode) == my_epoch {
124            self.map.insert((buffer, mode), guard);
125            Ok(())
126        } else {
127            Err(guard)
128        }
129    }
130
131    /// Take ownership of the Guard for `(buffer, mode)`,
132    /// bumping the epoch so any in-flight spawn that hasn't
133    /// inserted yet fails its [`Self::try_insert`] check.
134    /// Returns `None` if no Guard was stashed.
135    pub fn remove(&mut self, buffer: BufferId, mode: ModeId) -> Option<Box<dyn Any + Send>> {
136        // Bump first so a spawn task's later try_insert (after
137        // its on_activate.await resolves) sees the mismatch
138        // regardless of whether a Guard was already present.
139        self.bump_epoch(buffer, mode);
140        self.map.remove(&(buffer, mode))
141    }
142
143    /// Drop every Guard belonging to `buffer`. Call when a
144    /// buffer is deleted -- the dispatcher's normal
145    /// deactivation path may not run if the buffer vanishes
146    /// before the App can deactivate its modes. Bumps the
147    /// epoch for every `(buffer, *)` entry so any in-flight
148    /// spawn for the purged buffer fails its later
149    /// [`Self::try_insert`].
150    pub fn purge_buffer(&mut self, buffer: BufferId) {
151        self.map.retain(|(b, _), _| *b != buffer);
152        for ((b, _), epoch) in self.epochs.iter_mut() {
153            if *b == buffer {
154                *epoch = epoch.wrapping_add(1);
155            }
156        }
157    }
158
159    /// Number of stashed Guards.
160    pub fn len(&self) -> usize {
161        self.map.len()
162    }
163
164    /// True when no Guard is stashed.
165    pub fn is_empty(&self) -> bool {
166        self.map.is_empty()
167    }
168
169    /// True iff a Guard is stashed for `(buffer, mode)`.
170    pub fn contains(&self, buffer: BufferId, mode: ModeId) -> bool {
171        self.map.contains_key(&(buffer, mode))
172    }
173}
174
175/// Cheap-clone, thread-safe handle to a [`GuardStore`]. The App
176/// owns one; the dispatcher clones it into each spawned
177/// lifecycle task so the task can lock + insert the Guard on
178/// completion. Locks are held briefly (single map mutation per
179/// activation / deactivation); `std::sync::Mutex` is correct
180/// because no `.await` happens inside the lock.
181#[derive(Clone, Default)]
182pub struct GuardStoreHandle {
183    inner: Arc<Mutex<GuardStore>>,
184}
185
186impl std::fmt::Debug for GuardStoreHandle {
187    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188        let guard = self.inner.lock();
189        match guard {
190            Ok(g) => f
191                .debug_struct("GuardStoreHandle")
192                .field("count", &g.len())
193                .finish_non_exhaustive(),
194            Err(_) => f.debug_struct("GuardStoreHandle").finish_non_exhaustive(),
195        }
196    }
197}
198
199impl GuardStoreHandle {
200    /// Fresh empty handle.
201    pub fn new() -> Self {
202        Self::default()
203    }
204
205    /// Stash a Guard unconditionally (skipping the epoch
206    /// check). Used in tests + the rare reload path; the
207    /// production dispatcher routes through
208    /// [`Self::try_insert`].
209    pub fn insert(&self, buffer: BufferId, mode: ModeId, guard: Box<dyn Any + Send>) {
210        if let Ok(mut store) = self.inner.lock() {
211            store.insert(buffer, mode, guard);
212        }
213    }
214
215    /// Bump + return the new epoch for `(buffer, mode)`. The
216    /// dispatcher's sync prefix calls this when queueing each
217    /// cascade step; the spawn task captures it and passes
218    /// back to [`Self::try_insert`].
219    pub fn bump_epoch(&self, buffer: BufferId, mode: ModeId) -> u64 {
220        self.inner
221            .lock()
222            .map(|mut store| store.bump_epoch(buffer, mode))
223            .unwrap_or(0)
224    }
225
226    /// Insert iff `my_epoch` still matches the current
227    /// epoch. Returns `Err(guard)` on stale; the caller drops
228    /// the Guard outside the lock so the original type's
229    /// `Drop` fires.
230    pub fn try_insert(
231        &self,
232        buffer: BufferId,
233        mode: ModeId,
234        my_epoch: u64,
235        guard: Box<dyn Any + Send>,
236    ) -> Result<(), Box<dyn Any + Send>> {
237        match self.inner.lock() {
238            Ok(mut store) => store.try_insert(buffer, mode, my_epoch, guard),
239            // Poisoned mutex: treat as "stale" so caller drops.
240            Err(_) => Err(guard),
241        }
242    }
243
244    /// Take ownership of the Guard. Bumps the epoch
245    /// (invalidating any in-flight spawn) then removes; the
246    /// caller drops the returned `Box`, firing the Guard's
247    /// `Drop` impl *outside* the lock.
248    pub fn remove(&self, buffer: BufferId, mode: ModeId) -> Option<Box<dyn Any + Send>> {
249        self.inner.lock().ok()?.remove(buffer, mode)
250    }
251
252    /// Drop every Guard for `buffer`. Used when a buffer is
253    /// deleted.
254    pub fn purge_buffer(&self, buffer: BufferId) {
255        if let Ok(mut store) = self.inner.lock() {
256            store.purge_buffer(buffer);
257        }
258    }
259
260    /// True iff a Guard is stashed for `(buffer, mode)`.
261    pub fn contains(&self, buffer: BufferId, mode: ModeId) -> bool {
262        self.inner
263            .lock()
264            .map(|s| s.contains(buffer, mode))
265            .unwrap_or(false)
266    }
267
268    /// Number of stashed Guards.
269    pub fn len(&self) -> usize {
270        self.inner.lock().map(|s| s.len()).unwrap_or(0)
271    }
272
273    /// True when no Guard is stashed (or the lock is poisoned).
274    pub fn is_empty(&self) -> bool {
275        self.len() == 0
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    #![allow(clippy::unwrap_used)]
282    use super::*;
283    use std::sync::Arc;
284    use std::sync::atomic::{AtomicU32, Ordering};
285
286    /// Test Guard that increments a counter on drop, proving
287    /// `Box<dyn Any + Send>::drop` correctly invokes the
288    /// original type's `Drop` via the vtable.
289    struct DropCounter {
290        count: Arc<AtomicU32>,
291    }
292    impl Drop for DropCounter {
293        fn drop(&mut self) {
294            self.count.fetch_add(1, Ordering::SeqCst);
295        }
296    }
297
298    #[test]
299    fn insert_then_remove_drops_guard() {
300        let count = Arc::new(AtomicU32::new(0));
301        let mut store = GuardStore::new();
302        store.insert(
303            BufferId::new(1),
304            ModeId::new("x-mode"),
305            Box::new(DropCounter {
306                count: count.clone(),
307            }),
308        );
309        // Drop hasn't fired yet -- Guard is owned by the store.
310        assert_eq!(count.load(Ordering::SeqCst), 0);
311        let removed = store.remove(BufferId::new(1), ModeId::new("x-mode"));
312        // Box is now in `removed`; still hasn't dropped.
313        assert!(removed.is_some());
314        // Dropping the removed Box fires the original Guard's Drop.
315        drop(removed);
316        assert_eq!(count.load(Ordering::SeqCst), 1);
317    }
318
319    #[test]
320    fn remove_missing_returns_none() {
321        let mut store = GuardStore::new();
322        assert!(
323            store
324                .remove(BufferId::new(1), ModeId::new("x-mode"))
325                .is_none()
326        );
327    }
328
329    #[test]
330    fn purge_buffer_drops_every_mode_for_that_buffer() {
331        let c1 = Arc::new(AtomicU32::new(0));
332        let c2 = Arc::new(AtomicU32::new(0));
333        let other = Arc::new(AtomicU32::new(0));
334        let mut store = GuardStore::new();
335        store.insert(
336            BufferId::new(1),
337            ModeId::new("a-mode"),
338            Box::new(DropCounter { count: c1.clone() }),
339        );
340        store.insert(
341            BufferId::new(1),
342            ModeId::new("b-mode"),
343            Box::new(DropCounter { count: c2.clone() }),
344        );
345        store.insert(
346            BufferId::new(2),
347            ModeId::new("a-mode"),
348            Box::new(DropCounter {
349                count: other.clone(),
350            }),
351        );
352        store.purge_buffer(BufferId::new(1));
353        // Buffer 1's two Guards dropped; buffer 2 intact.
354        assert_eq!(c1.load(Ordering::SeqCst), 1);
355        assert_eq!(c2.load(Ordering::SeqCst), 1);
356        assert_eq!(other.load(Ordering::SeqCst), 0);
357        assert_eq!(store.len(), 1);
358        assert!(store.contains(BufferId::new(2), ModeId::new("a-mode")));
359    }
360
361    #[test]
362    fn insert_replaces_existing_and_drops_old() {
363        let old = Arc::new(AtomicU32::new(0));
364        let new = Arc::new(AtomicU32::new(0));
365        let mut store = GuardStore::new();
366        store.insert(
367            BufferId::new(1),
368            ModeId::new("x-mode"),
369            Box::new(DropCounter { count: old.clone() }),
370        );
371        store.insert(
372            BufferId::new(1),
373            ModeId::new("x-mode"),
374            Box::new(DropCounter { count: new.clone() }),
375        );
376        // Old Guard dropped at replacement time.
377        assert_eq!(old.load(Ordering::SeqCst), 1);
378        assert_eq!(new.load(Ordering::SeqCst), 0);
379    }
380
381    #[test]
382    fn unit_guard_works() {
383        // Marker modes use `Guard = ()`; storing/removing it
384        // must not panic.
385        let mut store = GuardStore::new();
386        store.insert(BufferId::new(1), ModeId::new("marker-mode"), Box::new(()));
387        let g = store.remove(BufferId::new(1), ModeId::new("marker-mode"));
388        assert!(g.is_some());
389    }
390}