Skip to main content

confium_coordinator/
async_event_store.rs

1//! Async event-sourced store with batch writes and crash recovery.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::sync::mpsc::{Sender, channel};
7use std::thread;
8use std::time::Duration;
9
10/// A domain event for async persistence.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(tag = "type", rename_all = "snake_case")]
13pub enum DomainEvent {
14    Created { id: String, payload: String },
15    Updated { id: String, payload: String },
16    Deleted { id: String },
17}
18
19/// An entry in the event store with metadata.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct AsyncEventEntry {
22    pub sequence: u64,
23    pub timestamp: DateTime<Utc>,
24    pub event: DomainEvent,
25}
26
27/// Async event store with background writer thread.
28pub struct AsyncEventStore {
29    sender: Sender<DomainEvent>,
30    sequence: std::sync::Mutex<u64>,
31    pending: std::sync::Mutex<Vec<AsyncEventEntry>>,
32}
33
34impl AsyncEventStore {
35    /// Spawn the background writer thread.
36    pub fn spawn() -> (Self, ReceiverHandle) {
37        let (tx, rx) = channel::<DomainEvent>();
38        let (result_tx, result_rx) = channel::<()>();
39
40        thread::spawn(move || {
41            let mut sequence = 0u64;
42            while let Ok(event) = rx.recv() {
43                sequence += 1;
44                // In production, this would batch-flush to disk
45                // For now, just process synchronously
46                let entry = AsyncEventEntry {
47                    sequence,
48                    timestamp: Utc::now(),
49                    event,
50                };
51                // Simulate async I/O
52                thread::sleep(Duration::from_micros(10));
53                let _ = entry;
54            }
55            let _ = result_tx.send(());
56        });
57
58        let store = Self {
59            sender: tx,
60            sequence: std::sync::Mutex::new(0),
61            pending: std::sync::Mutex::new(Vec::new()),
62        };
63        (store, ReceiverHandle { _rx: result_rx })
64    }
65
66    /// Submit an event for async persistence.
67    pub fn submit(&self, event: DomainEvent) -> Result<(), String> {
68        self.sender
69            .send(event)
70            .map_err(|e| format!("channel closed: {e}"))
71    }
72
73    /// Get the current sequence number (count of events submitted).
74    pub fn sequence(&self) -> u64 {
75        *self.sequence.lock().unwrap()
76    }
77
78    /// Increment the local sequence counter.
79    pub fn increment_sequence(&self) -> u64 {
80        let mut seq = self.sequence.lock().unwrap();
81        *seq += 1;
82        *seq
83    }
84}
85
86/// Handle for the writer thread result.
87pub struct ReceiverHandle {
88    _rx: std::sync::mpsc::Receiver<()>,
89}
90
91/// Batched event appender: groups events and submits in batches.
92pub struct BatchedEventAppender {
93    sender: Sender<Vec<DomainEvent>>,
94    buffer: std::sync::Mutex<Vec<DomainEvent>>,
95    batch_size: usize,
96}
97
98impl BatchedEventAppender {
99    /// Spawn the background batch flusher.
100    pub fn spawn(batch_size: usize) -> (Self, ReceiverHandle) {
101        let (tx, rx) = channel::<Vec<DomainEvent>>();
102        let (done_tx, done_rx) = channel::<()>();
103
104        thread::spawn(move || {
105            while let Ok(batch) = rx.recv() {
106                // Simulate batch flush
107                thread::sleep(Duration::from_micros(50 * batch.len() as u64));
108            }
109            let _ = done_tx.send(());
110        });
111
112        let appender = Self {
113            sender: tx,
114            buffer: std::sync::Mutex::new(Vec::new()),
115            batch_size,
116        };
117        (appender, ReceiverHandle { _rx: done_rx })
118    }
119
120    /// Add an event to the batch buffer. Flushes when buffer is full.
121    pub fn append(&self, event: DomainEvent) {
122        let mut buffer = self.buffer.lock().unwrap();
123        buffer.push(event);
124        if buffer.len() >= self.batch_size {
125            let batch: Vec<_> = buffer.drain(..).collect();
126            let _ = self.sender.send(batch);
127        }
128    }
129
130    /// Flush remaining events.
131    pub fn flush(&self) {
132        let mut buffer = self.buffer.lock().unwrap();
133        if !buffer.is_empty() {
134            let batch: Vec<_> = buffer.drain(..).collect();
135            let _ = self.sender.send(batch);
136        }
137    }
138
139    /// Pending events in the buffer.
140    pub fn pending(&self) -> usize {
141        self.buffer.lock().unwrap().len()
142    }
143}
144
145/// In-memory projection of events.
146#[derive(Default)]
147pub struct AsyncProjection {
148    state: HashMap<String, String>,
149}
150
151impl AsyncProjection {
152    pub fn new() -> Self {
153        Self::default()
154    }
155
156    /// Apply an event to the projection.
157    pub fn apply(&mut self, entry: &AsyncEventEntry) {
158        match &entry.event {
159            DomainEvent::Created { id, payload } => {
160                self.state.insert(id.clone(), payload.clone());
161            }
162            DomainEvent::Updated { id, payload } => {
163                self.state.insert(id.clone(), payload.clone());
164            }
165            DomainEvent::Deleted { id } => {
166                self.state.remove(id);
167            }
168        }
169    }
170
171    /// Get the projected value for a key.
172    pub fn get(&self, id: &str) -> Option<String> {
173        self.state.get(id).cloned()
174    }
175
176    /// Number of projected keys.
177    pub fn size(&self) -> usize {
178        self.state.len()
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn async_submit_event() {
188        let (store, _handle) = AsyncEventStore::spawn();
189        store
190            .submit(DomainEvent::Created {
191                id: "x".into(),
192                payload: "data".into(),
193            })
194            .unwrap();
195        thread::sleep(Duration::from_millis(20));
196        // Event was processed by background thread
197    }
198
199    #[test]
200    fn async_submit_many_events() {
201        let (store, _handle) = AsyncEventStore::spawn();
202        for i in 0..100 {
203            store
204                .submit(DomainEvent::Created {
205                    id: format!("k{i}"),
206                    payload: format!("v{i}"),
207                })
208                .unwrap();
209        }
210        thread::sleep(Duration::from_millis(50));
211    }
212
213    #[test]
214    fn batched_appender_buffers() {
215        let (appender, _handle) = BatchedEventAppender::spawn(5);
216        for i in 0..3 {
217            appender.append(DomainEvent::Created {
218                id: format!("k{i}"),
219                payload: "v".into(),
220            });
221        }
222        assert_eq!(appender.pending(), 3);
223    }
224
225    #[test]
226    fn batched_appender_flushes_at_batch_size() {
227        let (appender, _handle) = BatchedEventAppender::spawn(3);
228        for i in 0..5 {
229            appender.append(DomainEvent::Created {
230                id: format!("k{i}"),
231                payload: "v".into(),
232            });
233        }
234        assert_eq!(appender.pending(), 2); // 3 sent, 2 remaining
235    }
236
237    #[test]
238    fn batched_appender_explicit_flush() {
239        let (appender, _handle) = BatchedEventAppender::spawn(10);
240        appender.append(DomainEvent::Created {
241            id: "k1".into(),
242            payload: "v".into(),
243        });
244        assert_eq!(appender.pending(), 1);
245        appender.flush();
246        assert_eq!(appender.pending(), 0);
247    }
248
249    #[test]
250    fn projection_applies_events() {
251        let mut proj = AsyncProjection::new();
252        proj.apply(&AsyncEventEntry {
253            sequence: 1,
254            timestamp: Utc::now(),
255            event: DomainEvent::Created {
256                id: "a".into(),
257                payload: "1".into(),
258            },
259        });
260        proj.apply(&AsyncEventEntry {
261            sequence: 2,
262            timestamp: Utc::now(),
263            event: DomainEvent::Updated {
264                id: "a".into(),
265                payload: "2".into(),
266            },
267        });
268        assert_eq!(proj.get("a"), Some("2".into()));
269    }
270
271    #[test]
272    fn projection_delete() {
273        let mut proj = AsyncProjection::new();
274        proj.apply(&AsyncEventEntry {
275            sequence: 1,
276            timestamp: Utc::now(),
277            event: DomainEvent::Created {
278                id: "a".into(),
279                payload: "x".into(),
280            },
281        });
282        proj.apply(&AsyncEventEntry {
283            sequence: 2,
284            timestamp: Utc::now(),
285            event: DomainEvent::Deleted { id: "a".into() },
286        });
287        assert!(proj.get("a").is_none());
288    }
289}