Skip to main content

moonpool_sim/chaos/
state_handle.rs

1//! Type-safe shared state for workload communication and invariant checking.
2//!
3//! `StateHandle` replaces the JSON-based `StateRegistry` with a type-safe
4//! `Rc<RefCell<HashMap>>` backed store using `Box<dyn Any>`.
5//!
6//! # Usage
7//!
8//! ```ignore
9//! use moonpool_sim::StateHandle;
10//!
11//! let state = StateHandle::new();
12//! state.publish("counter", 42u64);
13//! let val: Option<u64> = state.get("counter");
14//! assert_eq!(val, Some(42));
15//! ```
16
17use std::any::Any;
18use std::cell::{Cell, Ref, RefCell};
19use std::collections::HashMap;
20use std::rc::Rc;
21
22/// A single entry in an append-only event timeline.
23#[derive(Debug, Clone)]
24pub struct TimelineEntry<T> {
25    /// The event payload.
26    pub event: T,
27    /// Simulation time in milliseconds when the event was emitted.
28    pub time_ms: u64,
29    /// IP address of the emitter.
30    pub source: String,
31    /// Global sequence number (monotonically increasing across all timelines).
32    pub seq: u64,
33}
34
35/// Handle to a typed, append-only event timeline.
36///
37/// Clone is cheap (Rc-based). All clones share the same underlying storage.
38#[derive(Clone)]
39pub struct Timeline<T: 'static> {
40    inner: Rc<RefCell<Vec<TimelineEntry<T>>>>,
41}
42
43impl<T: 'static> Timeline<T> {
44    /// Number of entries in this timeline.
45    pub fn len(&self) -> usize {
46        self.inner.borrow().len()
47    }
48
49    /// Whether this timeline has no entries.
50    pub fn is_empty(&self) -> bool {
51        self.inner.borrow().is_empty()
52    }
53
54    /// Borrow all entries (zero-copy).
55    pub fn all(&self) -> Ref<'_, Vec<TimelineEntry<T>>> {
56        self.inner.borrow()
57    }
58}
59
60impl<T: Clone + 'static> Timeline<T> {
61    /// Get entries from `index` onward (for cursor-based incremental invariants).
62    pub fn since(&self, index: usize) -> Vec<TimelineEntry<T>> {
63        self.inner.borrow()[index..].to_vec()
64    }
65
66    /// Get the last entry, if any.
67    pub fn last(&self) -> Option<TimelineEntry<T>> {
68        self.inner.borrow().last().cloned()
69    }
70}
71
72/// Shared state handle for cross-workload communication and invariant checking.
73///
74/// Provides type-safe publish/get semantics using `std::any::Any` for type erasure.
75/// Clone is cheap (Rc-based) — all clones share the same underlying storage.
76#[derive(Clone)]
77pub struct StateHandle {
78    inner: Rc<RefCell<HashMap<String, Box<dyn Any>>>>,
79    timelines: Rc<RefCell<HashMap<String, Box<dyn Any>>>>,
80    next_seq: Rc<Cell<u64>>,
81}
82
83impl Default for StateHandle {
84    fn default() -> Self {
85        Self {
86            inner: Rc::default(),
87            timelines: Rc::default(),
88            next_seq: Rc::new(Cell::new(0)),
89        }
90    }
91}
92
93impl StateHandle {
94    /// Create a new empty state handle.
95    pub fn new() -> Self {
96        Self::default()
97    }
98
99    /// Publish a value under a key, replacing any existing value.
100    pub fn publish<T: Any + 'static>(&self, key: &str, value: T) {
101        self.inner
102            .borrow_mut()
103            .insert(key.to_string(), Box::new(value));
104    }
105
106    /// Get a cloned copy of the value under a key, if it exists and matches the type.
107    ///
108    /// Returns `None` if the key doesn't exist or the type doesn't match.
109    pub fn get<T: Any + Clone + 'static>(&self, key: &str) -> Option<T> {
110        self.inner
111            .borrow()
112            .get(key)
113            .and_then(|v| v.downcast_ref::<T>())
114            .cloned()
115    }
116
117    /// Check whether a key exists in the state.
118    pub fn contains(&self, key: &str) -> bool {
119        self.inner.borrow().contains_key(key)
120    }
121
122    /// Append an event to a named timeline with explicit metadata.
123    ///
124    /// Prefer [`SimContext::emit`] which auto-captures time and source IP.
125    pub fn emit_raw<T: Any + 'static>(&self, key: &str, event: T, time_ms: u64, source: &str) {
126        let seq = self.next_seq.get();
127        self.next_seq.set(seq + 1);
128        let entry = TimelineEntry {
129            event,
130            time_ms,
131            source: source.to_string(),
132            seq,
133        };
134
135        // Get or create the Rc<RefCell<Vec>> for this key, then drop the map borrow
136        // before pushing to avoid nested RefCell borrows.
137        let rc = {
138            let mut map = self.timelines.borrow_mut();
139            let boxed = map
140                .entry(key.to_string())
141                .or_insert_with(|| Box::new(Rc::new(RefCell::new(Vec::<TimelineEntry<T>>::new()))));
142            boxed
143                .downcast_ref::<Rc<RefCell<Vec<TimelineEntry<T>>>>>()
144                .expect("timeline type mismatch: key already used with a different type")
145                .clone()
146        };
147        rc.borrow_mut().push(entry);
148    }
149
150    /// Get a typed timeline by name.
151    ///
152    /// Returns `None` if no events have been emitted to this key,
153    /// or if the type doesn't match.
154    pub fn timeline<T: Any + 'static>(&self, key: &str) -> Option<Timeline<T>> {
155        let map = self.timelines.borrow();
156        let boxed = map.get(key)?;
157        let rc = boxed.downcast_ref::<Rc<RefCell<Vec<TimelineEntry<T>>>>>()?;
158        Some(Timeline { inner: rc.clone() })
159    }
160}
161
162impl std::fmt::Debug for StateHandle {
163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164        let state_count = self.inner.borrow().len();
165        let timeline_count = self.timelines.borrow().len();
166        f.debug_struct("StateHandle")
167            .field("state_keys", &state_count)
168            .field("timeline_keys", &timeline_count)
169            .finish()
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_publish_and_get() {
179        let state = StateHandle::new();
180        state.publish("count", 42u64);
181        assert_eq!(state.get::<u64>("count"), Some(42));
182    }
183
184    #[test]
185    fn test_get_wrong_type_returns_none() {
186        let state = StateHandle::new();
187        state.publish("count", 42u64);
188        assert_eq!(state.get::<String>("count"), None);
189    }
190
191    #[test]
192    fn test_get_missing_key_returns_none() {
193        let state = StateHandle::new();
194        assert_eq!(state.get::<u64>("missing"), None);
195    }
196
197    #[test]
198    fn test_contains() {
199        let state = StateHandle::new();
200        assert!(!state.contains("key"));
201        state.publish("key", "value".to_string());
202        assert!(state.contains("key"));
203    }
204
205    #[test]
206    fn test_publish_replaces() {
207        let state = StateHandle::new();
208        state.publish("x", 1u64);
209        state.publish("x", 2u64);
210        assert_eq!(state.get::<u64>("x"), Some(2));
211    }
212
213    #[test]
214    fn test_clone_shares_state() {
215        let state = StateHandle::new();
216        let clone = state.clone();
217        state.publish("shared", true);
218        assert_eq!(clone.get::<bool>("shared"), Some(true));
219    }
220
221    #[test]
222    fn test_emit_and_read_timeline() {
223        let state = StateHandle::new();
224        state.emit_raw("ops", "first", 100, "10.0.1.1");
225        state.emit_raw("ops", "second", 200, "10.0.1.2");
226        state.emit_raw("ops", "third", 300, "10.0.1.1");
227
228        let tl = state
229            .timeline::<&str>("ops")
230            .expect("timeline should exist");
231        assert_eq!(tl.len(), 3);
232
233        let entries = tl.all();
234        assert_eq!(entries[0].event, "first");
235        assert_eq!(entries[0].time_ms, 100);
236        assert_eq!(entries[0].source, "10.0.1.1");
237        assert_eq!(entries[0].seq, 0);
238        assert_eq!(entries[1].event, "second");
239        assert_eq!(entries[1].seq, 1);
240        assert_eq!(entries[2].event, "third");
241        assert_eq!(entries[2].seq, 2);
242    }
243
244    #[test]
245    fn test_timeline_missing_key_returns_none() {
246        let state = StateHandle::new();
247        assert!(state.timeline::<u64>("missing").is_none());
248    }
249
250    #[test]
251    fn test_timeline_wrong_type_returns_none() {
252        let state = StateHandle::new();
253        state.emit_raw("ops", 42u64, 0, "10.0.1.1");
254        assert!(state.timeline::<String>("ops").is_none());
255    }
256
257    #[test]
258    fn test_timeline_since() {
259        let state = StateHandle::new();
260        for i in 0..5u64 {
261            state.emit_raw("events", i, i * 10, "10.0.1.1");
262        }
263
264        let tl = state.timeline::<u64>("events").expect("should exist");
265        let tail = tl.since(3);
266        assert_eq!(tail.len(), 2);
267        assert_eq!(tail[0].event, 3);
268        assert_eq!(tail[1].event, 4);
269    }
270
271    #[test]
272    fn test_timeline_clone_shares_data() {
273        let state = StateHandle::new();
274        state.emit_raw("log", "a", 0, "10.0.1.1");
275
276        let tl = state.timeline::<&str>("log").expect("should exist");
277        let tl2 = tl.clone();
278
279        state.emit_raw("log", "b", 10, "10.0.1.2");
280        assert_eq!(tl.len(), 2);
281        assert_eq!(tl2.len(), 2);
282    }
283
284    #[test]
285    fn test_seq_spans_timelines() {
286        let state = StateHandle::new();
287        state.emit_raw("a", 1u64, 0, "10.0.1.1");
288        state.emit_raw("b", 2u64, 0, "10.0.1.1");
289
290        let tl_a = state.timeline::<u64>("a").expect("a");
291        let tl_b = state.timeline::<u64>("b").expect("b");
292        assert_eq!(tl_a.all()[0].seq, 0);
293        assert_eq!(tl_b.all()[0].seq, 1);
294    }
295
296    #[test]
297    fn test_timeline_last() {
298        let state = StateHandle::new();
299        state.emit_raw("log", "first", 0, "10.0.1.1");
300        state.emit_raw("log", "second", 10, "10.0.1.1");
301
302        let tl = state.timeline::<&str>("log").expect("should exist");
303        let last = tl.last().expect("should have last");
304        assert_eq!(last.event, "second");
305        assert_eq!(last.time_ms, 10);
306    }
307}