Skip to main content

cellz/cell/
actor.rs

1use std::collections::HashMap;
2use std::path::Path;
3use std::time::Instant;
4
5use anyhow::{Context, Result};
6use serde_json::Value;
7use tokio::sync::{broadcast, mpsc, oneshot};
8use tracing::{info, warn};
9
10use crate::cell::db::CellDb;
11use crate::model::event::{EventRecord, Message};
12use crate::model::state::{CellMeta, CellStatus, CheckpointRecord};
13
14pub enum ActorMessage {
15    AppendEvent {
16        turn_id: Option<String>,
17        event_type: String,
18        payload: Value,
19        reply: oneshot::Sender<Result<EventRecord>>,
20    },
21    AppendEventsBatch {
22        requests: Vec<crate::model::event::AppendEventRequest>,
23        reply: oneshot::Sender<Result<Vec<EventRecord>>>,
24    },
25    GetEvents {
26        since_seq: Option<i64>,
27        limit: Option<i64>,
28        reply: oneshot::Sender<Result<Vec<EventRecord>>>,
29    },
30    Export {
31        reply: oneshot::Sender<Result<crate::model::state::CellExport>>,
32    },
33    GetMessages {
34        reply: oneshot::Sender<Result<Vec<Message>>>,
35    },
36    GetMeta {
37        reply: oneshot::Sender<Result<CellMeta>>,
38    },
39    SetKV {
40        key: String,
41        value: Value,
42        reply: oneshot::Sender<Result<()>>,
43    },
44    GetKV {
45        key: String,
46        reply: oneshot::Sender<Result<Option<Value>>>,
47    },
48    ListKV {
49        reply: oneshot::Sender<Result<HashMap<String, Value>>>,
50    },
51    CreateCheckpoint {
52        label: String,
53        reply: oneshot::Sender<Result<CheckpointRecord>>,
54    },
55    RestoreCheckpoint {
56        checkpoint_id: String,
57        reply: oneshot::Sender<Result<i64>>,
58    },
59    CheckpointWal {
60        reply: oneshot::Sender<Result<()>>,
61    },
62    Backup {
63        reply: oneshot::Sender<Result<Vec<u8>>>,
64    },
65    Fence,
66    GetIdleDuration {
67        reply: oneshot::Sender<std::time::Duration>,
68    },
69    Shutdown {
70        reply: oneshot::Sender<()>,
71    },
72}
73
74#[derive(Clone)]
75pub struct CellHandle {
76    pub cell_id: String,
77    tx: mpsc::Sender<ActorMessage>,
78    event_bus: broadcast::Sender<EventRecord>,
79}
80
81impl CellHandle {
82    pub async fn append_event(
83        &self,
84        turn_id: Option<String>,
85        event_type: impl Into<String>,
86        payload: Value,
87    ) -> Result<EventRecord> {
88        let (reply, rx) = oneshot::channel();
89        self.tx
90            .send(ActorMessage::AppendEvent {
91                turn_id,
92                event_type: event_type.into(),
93                payload,
94                reply,
95            })
96            .await
97            .map_err(|_| anyhow::anyhow!("Cell actor mailbox closed"))?;
98        rx.await
99            .map_err(|_| anyhow::anyhow!("Cell actor dropped reply"))?
100    }
101
102    pub async fn append_events_batch(
103        &self,
104        requests: Vec<crate::model::event::AppendEventRequest>,
105    ) -> Result<Vec<EventRecord>> {
106        let (reply, rx) = oneshot::channel();
107        self.tx
108            .send(ActorMessage::AppendEventsBatch { requests, reply })
109            .await
110            .map_err(|_| anyhow::anyhow!("Cell actor mailbox closed"))?;
111        rx.await
112            .map_err(|_| anyhow::anyhow!("Cell actor dropped reply"))?
113    }
114
115    pub async fn export(&self) -> Result<crate::model::state::CellExport> {
116        let (reply, rx) = oneshot::channel();
117        self.tx
118            .send(ActorMessage::Export { reply })
119            .await
120            .map_err(|_| anyhow::anyhow!("Cell actor mailbox closed"))?;
121        rx.await
122            .map_err(|_| anyhow::anyhow!("Cell actor dropped reply"))?
123    }
124
125    pub async fn get_events(
126        &self,
127        since_seq: Option<i64>,
128        limit: Option<i64>,
129    ) -> Result<Vec<EventRecord>> {
130        let (reply, rx) = oneshot::channel();
131        self.tx
132            .send(ActorMessage::GetEvents {
133                since_seq,
134                limit,
135                reply,
136            })
137            .await
138            .map_err(|_| anyhow::anyhow!("Cell actor mailbox closed"))?;
139        rx.await
140            .map_err(|_| anyhow::anyhow!("Cell actor dropped reply"))?
141    }
142
143    pub async fn get_messages(&self) -> Result<Vec<Message>> {
144        let (reply, rx) = oneshot::channel();
145        self.tx
146            .send(ActorMessage::GetMessages { reply })
147            .await
148            .map_err(|_| anyhow::anyhow!("Cell actor mailbox closed"))?;
149        rx.await
150            .map_err(|_| anyhow::anyhow!("Cell actor dropped reply"))?
151    }
152
153    pub async fn get_meta(&self) -> Result<CellMeta> {
154        let (reply, rx) = oneshot::channel();
155        self.tx
156            .send(ActorMessage::GetMeta { reply })
157            .await
158            .map_err(|_| anyhow::anyhow!("Cell actor mailbox closed"))?;
159        rx.await
160            .map_err(|_| anyhow::anyhow!("Cell actor dropped reply"))?
161    }
162
163    pub async fn set_kv(&self, key: impl Into<String>, value: Value) -> Result<()> {
164        let (reply, rx) = oneshot::channel();
165        self.tx
166            .send(ActorMessage::SetKV {
167                key: key.into(),
168                value,
169                reply,
170            })
171            .await
172            .map_err(|_| anyhow::anyhow!("Cell actor mailbox closed"))?;
173        rx.await
174            .map_err(|_| anyhow::anyhow!("Cell actor dropped reply"))?
175    }
176
177    pub async fn get_kv(&self, key: impl Into<String>) -> Result<Option<Value>> {
178        let (reply, rx) = oneshot::channel();
179        self.tx
180            .send(ActorMessage::GetKV {
181                key: key.into(),
182                reply,
183            })
184            .await
185            .map_err(|_| anyhow::anyhow!("Cell actor mailbox closed"))?;
186        rx.await
187            .map_err(|_| anyhow::anyhow!("Cell actor dropped reply"))?
188    }
189
190    pub async fn list_kv(&self) -> Result<HashMap<String, Value>> {
191        let (reply, rx) = oneshot::channel();
192        self.tx
193            .send(ActorMessage::ListKV { reply })
194            .await
195            .map_err(|_| anyhow::anyhow!("Cell actor mailbox closed"))?;
196        rx.await
197            .map_err(|_| anyhow::anyhow!("Cell actor dropped reply"))?
198    }
199
200    pub async fn create_checkpoint(&self, label: impl Into<String>) -> Result<CheckpointRecord> {
201        let (reply, rx) = oneshot::channel();
202        self.tx
203            .send(ActorMessage::CreateCheckpoint {
204                label: label.into(),
205                reply,
206            })
207            .await
208            .map_err(|_| anyhow::anyhow!("Cell actor mailbox closed"))?;
209        rx.await
210            .map_err(|_| anyhow::anyhow!("Cell actor dropped reply"))?
211    }
212
213    pub async fn restore_checkpoint(&self, checkpoint_id: impl Into<String>) -> Result<i64> {
214        let (reply, rx) = oneshot::channel();
215        self.tx
216            .send(ActorMessage::RestoreCheckpoint {
217                checkpoint_id: checkpoint_id.into(),
218                reply,
219            })
220            .await
221            .map_err(|_| anyhow::anyhow!("Cell actor mailbox closed"))?;
222        rx.await
223            .map_err(|_| anyhow::anyhow!("Cell actor dropped reply"))?
224    }
225
226    pub async fn checkpoint_wal(&self) -> Result<()> {
227        let (reply, rx) = oneshot::channel();
228        self.tx
229            .send(ActorMessage::CheckpointWal { reply })
230            .await
231            .map_err(|_| anyhow::anyhow!("Cell actor mailbox closed"))?;
232        rx.await
233            .map_err(|_| anyhow::anyhow!("Cell actor dropped reply"))?
234    }
235
236    pub async fn backup(&self) -> Result<Vec<u8>> {
237        let (reply, rx) = oneshot::channel();
238        self.tx
239            .send(ActorMessage::Backup { reply })
240            .await
241            .map_err(|_| anyhow::anyhow!("Cell actor mailbox closed"))?;
242        rx.await
243            .map_err(|_| anyhow::anyhow!("Cell actor dropped reply"))?
244    }
245
246    pub async fn fence(&self) {
247        let _ = self.tx.send(ActorMessage::Fence).await;
248    }
249
250    pub async fn idle_duration(&self) -> Result<std::time::Duration> {
251        let (reply, rx) = oneshot::channel();
252        self.tx
253            .send(ActorMessage::GetIdleDuration { reply })
254            .await
255            .map_err(|_| anyhow::anyhow!("Cell actor mailbox closed"))?;
256        rx.await
257            .map_err(|_| anyhow::anyhow!("Cell actor dropped reply"))
258    }
259
260    pub async fn shutdown(&self) {
261        let (reply, rx) = oneshot::channel();
262        if self.tx.send(ActorMessage::Shutdown { reply }).await.is_ok() {
263            let _ = rx.await;
264        }
265    }
266
267    pub fn subscribe(&self) -> broadcast::Receiver<EventRecord> {
268        self.event_bus.subscribe()
269    }
270}
271
272pub struct CellActor {
273    cell_id: String,
274    db: CellDb,
275    event_bus: broadcast::Sender<EventRecord>,
276    rx: mpsc::Receiver<ActorMessage>,
277    last_active: Instant,
278}
279
280impl CellActor {
281    /// Spawn a dedicated OS thread that owns the cell's rusqlite connection.
282    pub fn spawn(
283        cell_id: impl Into<String>,
284        db_path: impl AsRef<Path>,
285        initial_name: Option<String>,
286    ) -> Result<CellHandle> {
287        let cell_id = cell_id.into();
288        let db_path = db_path.as_ref().to_path_buf();
289        let (tx, rx) = mpsc::channel(128);
290        let (event_bus, _) = broadcast::channel(256);
291        let (ready_tx, ready_rx) = std::sync::mpsc::sync_channel(1);
292
293        let thread_cell_id = cell_id.clone();
294        let event_bus_for_actor = event_bus.clone();
295        let short: String = cell_id.chars().take(8).collect();
296        let thread_name = format!("cell-{short}");
297
298        std::thread::Builder::new()
299            .name(thread_name)
300            .spawn(
301                move || match CellDb::open(&thread_cell_id, &db_path, initial_name.as_deref()) {
302                    Ok(db) => {
303                        let actor = CellActor {
304                            cell_id: thread_cell_id,
305                            db,
306                            event_bus: event_bus_for_actor,
307                            rx,
308                            last_active: Instant::now(),
309                        };
310                        let _ = ready_tx.send(Ok(()));
311                        actor.run();
312                    }
313                    Err(e) => {
314                        let _ = ready_tx.send(Err(e));
315                    }
316                },
317            )
318            .context("failed to spawn cell actor thread")?;
319
320        ready_rx
321            .recv()
322            .context("cell actor thread dropped before ready")??;
323
324        Ok(CellHandle {
325            cell_id,
326            tx,
327            event_bus,
328        })
329    }
330
331    fn run(mut self) {
332        info!("CellActor [{}] spawned and running", self.cell_id);
333
334        while let Some(msg) = self.rx.blocking_recv() {
335            self.last_active = Instant::now();
336
337            match msg {
338                ActorMessage::AppendEvent {
339                    turn_id,
340                    event_type,
341                    payload,
342                    reply,
343                } => {
344                    let res = self.db.append_event(turn_id, &event_type, payload);
345                    if let Ok(ref record) = res {
346                        let _ = self.event_bus.send(record.clone());
347                    }
348                    let _ = reply.send(res);
349                }
350                ActorMessage::AppendEventsBatch { requests, reply } => {
351                    let res = self.db.append_events_batch(requests);
352                    if let Ok(ref records) = res {
353                        for rec in records {
354                            let _ = self.event_bus.send(rec.clone());
355                        }
356                    }
357                    let _ = reply.send(res);
358                }
359                ActorMessage::Export { reply } => {
360                    let res = self.db.export_cell();
361                    let _ = reply.send(res);
362                }
363                ActorMessage::GetEvents {
364                    since_seq,
365                    limit,
366                    reply,
367                } => {
368                    let res = self.db.get_events(since_seq, limit);
369                    let _ = reply.send(res);
370                }
371                ActorMessage::GetMessages { reply } => {
372                    let res = self.db.get_messages();
373                    let _ = reply.send(res);
374                }
375                ActorMessage::GetMeta { reply } => {
376                    let res = self.db.get_meta();
377                    let _ = reply.send(res);
378                }
379                ActorMessage::SetKV { key, value, reply } => {
380                    let res = self.db.set_kv(&key, &value);
381                    let _ = reply.send(res);
382                }
383                ActorMessage::GetKV { key, reply } => {
384                    let res = self.db.get_kv(&key);
385                    let _ = reply.send(res);
386                }
387                ActorMessage::ListKV { reply } => {
388                    let res = self.db.list_kv();
389                    let _ = reply.send(res);
390                }
391                ActorMessage::CreateCheckpoint { label, reply } => {
392                    let res = self.db.create_checkpoint(&label);
393                    let _ = reply.send(res);
394                }
395                ActorMessage::RestoreCheckpoint {
396                    checkpoint_id,
397                    reply,
398                } => {
399                    let res = self.db.restore_checkpoint(&checkpoint_id);
400                    let _ = reply.send(res);
401                }
402                ActorMessage::CheckpointWal { reply } => {
403                    let res = self.db.checkpoint_wal();
404                    let _ = reply.send(res);
405                }
406                ActorMessage::Backup { reply } => {
407                    let res = (|| {
408                        self.db.checkpoint_wal()?;
409                        std::fs::read(self.db.db_path()).map_err(Into::into)
410                    })();
411                    let _ = reply.send(res);
412                }
413                ActorMessage::Fence => {
414                    warn!(
415                        "CellActor [{}] fenced due to lease expiration or takeover",
416                        self.cell_id
417                    );
418                    let _ = self.db.update_status(CellStatus::Suspended);
419                    break;
420                }
421                ActorMessage::GetIdleDuration { reply } => {
422                    let _ = reply.send(self.last_active.elapsed());
423                }
424                ActorMessage::Shutdown { reply } => {
425                    info!("CellActor [{}] received shutdown signal", self.cell_id);
426                    let _ = self.db.update_status(CellStatus::Suspended);
427                    let _ = self.db.checkpoint_wal();
428                    let _ = reply.send(());
429                    break;
430                }
431            }
432        }
433
434        info!("CellActor [{}] terminated", self.cell_id);
435    }
436}