1use std::collections::BTreeMap;
37use std::path::Path;
38use std::sync::Arc;
39use std::time::{SystemTime, UNIX_EPOCH};
40
41use crate::Config;
42use crate::actor::{Actor, ActorContext, Addr};
43use crate::message::{BatchPut, Get, Message, Put};
44use crate::types::*;
45
46use async_trait::async_trait;
47use log::{debug, error, info};
48use redb::{Database, ReadableDatabase, ReadableTable, TableDefinition};
49
50const BEAM_NODES: TableDefinition<&str, &[u8]> = TableDefinition::new("beam_nodes_v1");
52const BEAM_META: TableDefinition<&str, u64> = TableDefinition::new("beam_meta_v1");
54
55macro_rules! unwrap_or_return {
57 ($e:expr) => {
58 match $e {
59 Ok(v) => v,
60 Err(e) => {
61 error!("redb operation failed: {:?}", e);
62 return;
63 }
64 }
65 };
66}
67
68pub struct RedbStorage {
84 db: Arc<Database>,
85 path: String,
86 _config: Config,
87}
88
89impl Clone for RedbStorage {
90 fn clone(&self) -> Self {
91 Self {
92 db: Arc::clone(&self.db),
93 path: self.path.clone(),
94 _config: self._config.clone(),
95 }
96 }
97}
98
99impl RedbStorage {
100 pub fn new() -> Self {
106 Self::new_with_config(Config::default(), "beam.redb", None)
107 }
108
109 pub fn new_with_config<P: AsRef<Path>>(
121 config: Config,
122 path: P,
123 _max_size: Option<u64>,
124 ) -> Self {
125 let path = path.as_ref().to_string_lossy().into_owned();
126 let db = Database::create(&path).unwrap_or_else(|e| {
127 panic!("Failed to create/open redb at {}: {:?}", path, e);
128 });
129 Self {
130 db: Arc::new(db),
131 path,
132 _config: config,
133 }
134 }
135
136 fn handle_get(&self, get: Get, ctx: &ActorContext) {
142 let read_txn = unwrap_or_return!(self.db.begin_read());
143 let table = unwrap_or_return!(read_txn.open_table(BEAM_NODES));
144
145 let children_for_node = match table.get(&*get.node_id) {
146 Ok(Some(access_guard)) => {
147 let bytes = access_guard.value();
148 unwrap_or_return!(postcard::from_bytes::<BTreeMap<String, NodeData>>(bytes))
149 }
150 Ok(None) => {
151 debug!("redb get: no data for node_id={}", get.node_id);
152 let mut reply_with_nodes = BTreeMap::new();
154 reply_with_nodes.insert(get.node_id.clone(), BTreeMap::new());
155 let mut put = Put::new(reply_with_nodes, Some(get.id.clone()), ctx.addr.clone());
156 put.to_string(); let _ = get.from.send(Message::Put(put));
158 return;
159 }
160 Err(e) => {
161 error!("redb get failed: {:?}", e);
162 return;
163 }
164 };
165
166 let reply_with_children = match &get.child_key {
167 Some(target_key) => {
168 let mut c = BTreeMap::new();
169 if let Some(node_data) = children_for_node.get(target_key) {
170 c.insert(target_key.clone(), node_data.clone());
171 }
172 c
173 }
174 None => children_for_node,
175 };
176
177 let mut reply_with_nodes = BTreeMap::new();
178 reply_with_nodes.insert(get.node_id.clone(), reply_with_children);
179
180 let mut put = Put::new(reply_with_nodes, Some(get.id.clone()), ctx.addr.clone());
181 put.to_string(); let is_ack = put.in_response_to.is_some();
190 if is_ack || put.checksum != get.checksum {
191 let _ = get.from.send(Message::Put(put));
192 } else {
193 debug!("redb get: checksum match, not replying");
194 }
195 }
196
197 fn apply_put_to_tables(
202 &self,
203 wtxn: &mut redb::WriteTransaction,
204 put: Put,
205 ) -> Result<(), redb::Error> {
206 let mut node_table = wtxn.open_table(BEAM_NODES)?;
207 let mut meta_table = wtxn.open_table(BEAM_META)?;
208
209 for (node_id, update_data) in put.updated_nodes.into_iter().rev() {
210 if !node_id.is_empty() && node_id.starts_with('_') {
212 continue;
213 }
214
215 let mut children_for_node: BTreeMap<String, NodeData> =
216 match node_table.get(&*node_id)? {
217 Some(access_guard) => {
218 let bytes = access_guard.value();
219 postcard::from_bytes(bytes).unwrap_or_default()
220 }
221 None => BTreeMap::new(),
222 };
223
224 for (child_id, child_data) in update_data {
225 let should_write = !matches!(
226 children_for_node.get(&child_id),
227 Some(existing) if existing.updated_at > child_data.updated_at
228 );
229
230 if should_write {
231 children_for_node.insert(child_id, child_data);
232 }
233 }
234
235 if children_for_node.is_empty() {
236 node_table.remove(&*node_id)?;
237 } else {
238 let bytes = postcard::to_allocvec(&children_for_node).map_err(|e| {
239 redb::Error::Io(std::io::Error::other(format!(
240 "postcard serialize: {:?}",
241 e
242 )))
243 })?;
244 node_table.insert(&*node_id, bytes.as_slice())?;
245 }
246 }
247
248 let now = SystemTime::now()
249 .duration_since(UNIX_EPOCH)
250 .unwrap_or_default()
251 .as_secs();
252 meta_table.insert("_last_write", now)?;
253 Ok(())
254 }
255
256 fn handle_put_internal(&self, put: Put) -> Result<(), redb::Error> {
258 let mut wtxn = self.db.begin_write()?;
259 self.apply_put_to_tables(&mut wtxn, put)?;
260 wtxn.commit()?;
261 Ok(())
262 }
263
264 fn handle_batch_put(&self, batch: BatchPut) -> Result<(), redb::Error> {
268 let mut wtxn = self.db.begin_write()?;
269 for put in batch.puts {
270 self.apply_put_to_tables(&mut wtxn, put)?;
271 }
272 wtxn.commit()?;
273 Ok(())
274 }
275}
276
277#[async_trait]
278impl Actor for RedbStorage {
279 async fn pre_start(&mut self, _ctx: &ActorContext) {
280 debug!("RedbStorage started at {}", self.path);
281 if let Ok(wtxn) = self.db.begin_write() {
283 let _ = wtxn.open_table(BEAM_NODES);
284 let _ = wtxn.open_table(BEAM_META);
285 let _ = wtxn.commit();
286 }
287 }
288
289 async fn stopping(&mut self, _ctx: &ActorContext) {
290 info!(
295 "RedbStorage stopping at {} — all writes committed",
296 self.path
297 );
298 }
299
300 async fn handle(&mut self, message: Message, ctx: &ActorContext) {
301 match message {
302 Message::Get(get) => self.handle_get(get, ctx),
303 Message::Put(put) => {
304 let put_id = put.id.clone();
305 let put_from = put.from.clone();
306 let storage = self.clone();
307 let result =
308 tokio::task::spawn_blocking(move || storage.handle_put_internal(put)).await;
309 self.send_put_ack_after_commit(&put_id, &put_from, &result, ctx);
310 }
311 Message::BatchPut(batch) => {
312 let batch_id = batch.id.clone();
313 let batch_from = batch.from.clone();
314 let storage = self.clone();
315 let result =
316 tokio::task::spawn_blocking(move || storage.handle_batch_put(batch)).await;
317 self.send_batch_put_ack_after_commit(&batch_id, &batch_from, &result, ctx);
318 }
319 Message::Flush(flush) => {
320 let flush_id = flush.id.clone();
321 let from_addr = flush.from.clone();
322 let ctx_addr = ctx.addr.clone();
323
324 let mut ack_children = BTreeMap::new();
327 ack_children.insert(
328 "_flushed".to_string(),
329 NodeData {
330 value: Value::Text("true".to_string()),
331 updated_at: SystemTime::now()
332 .duration_since(UNIX_EPOCH)
333 .unwrap_or_default()
334 .as_millis() as f64,
335 },
336 );
337 let mut ack_nodes = BTreeMap::new();
338 ack_nodes.insert("_ack".to_string(), ack_children);
339 let mut put = Put::new(ack_nodes, Some(flush_id), ctx_addr.clone());
340 put.to_string(); let _ = from_addr.send(Message::Put(put));
342 }
343 _ => {}
344 }
345 }
346
347 fn try_clone_storage(&self) -> Option<Box<dyn Actor>> {
352 Some(Box::new(self.clone()))
353 }
354}
355
356impl RedbStorage {
357 fn send_put_ack_after_commit(
366 &self,
367 put_id: &str,
368 put_from: &Addr,
369 result: &Result<Result<(), redb::Error>, tokio::task::JoinError>,
370 ctx: &ActorContext,
371 ) {
372 let (ack_children, err_msg) = match result {
373 Ok(Ok(())) => (
374 vec![(
375 "_ack".to_string(),
376 NodeData {
377 value: Value::Text("ok".to_string()),
378 updated_at: SystemTime::now()
379 .duration_since(UNIX_EPOCH)
380 .unwrap_or_default()
381 .as_millis() as f64,
382 },
383 )]
384 .into_iter()
385 .collect::<BTreeMap<_, _>>(),
386 None,
387 ),
388 Ok(Err(e)) => {
389 error!("redb put commit failed: {:?}", e);
390 (
391 vec![(
392 "_err".to_string(),
393 NodeData {
394 value: Value::Text(format!("{:?}", e)),
395 updated_at: SystemTime::now()
396 .duration_since(UNIX_EPOCH)
397 .unwrap_or_default()
398 .as_millis() as f64,
399 },
400 )]
401 .into_iter()
402 .collect(),
403 Some(format!("redb put commit failed: {:?}", e)),
404 )
405 }
406 Err(e) => {
407 error!("redb put task panicked: {:?}", e);
408 (
409 vec![(
410 "_err".to_string(),
411 NodeData {
412 value: Value::Text(format!("task panicked: {:?}", e)),
413 updated_at: SystemTime::now()
414 .duration_since(UNIX_EPOCH)
415 .unwrap_or_default()
416 .as_millis() as f64,
417 },
418 )]
419 .into_iter()
420 .collect(),
421 Some(format!("redb put task panicked: {:?}", e)),
422 )
423 }
424 };
425 let mut nodes = BTreeMap::new();
426 nodes.insert("_ack".to_string(), ack_children);
427 let ack = Put::new(nodes, Some(put_id.to_string()), ctx.addr.clone());
428 let _ = put_from.send(Message::Put(ack));
429 if err_msg.is_some() {
430 debug!("redb put ack sent with _err for {}", put_id);
431 }
432 }
433
434 fn send_batch_put_ack_after_commit(
440 &self,
441 batch_id: &str,
442 batch_from: &Addr,
443 result: &Result<Result<(), redb::Error>, tokio::task::JoinError>,
444 ctx: &ActorContext,
445 ) {
446 let (ack_children, err_msg) = match result {
447 Ok(Ok(())) => (
448 vec![(
449 "_ack".to_string(),
450 NodeData {
451 value: Value::Text("ok".to_string()),
452 updated_at: SystemTime::now()
453 .duration_since(UNIX_EPOCH)
454 .unwrap_or_default()
455 .as_millis() as f64,
456 },
457 )]
458 .into_iter()
459 .collect::<BTreeMap<_, _>>(),
460 None,
461 ),
462 Ok(Err(e)) => {
463 error!("redb batch_put commit failed: {:?}", e);
464 (
465 vec![(
466 "_err".to_string(),
467 NodeData {
468 value: Value::Text(format!("{:?}", e)),
469 updated_at: SystemTime::now()
470 .duration_since(UNIX_EPOCH)
471 .unwrap_or_default()
472 .as_millis() as f64,
473 },
474 )]
475 .into_iter()
476 .collect(),
477 Some(format!("redb batch_put commit failed: {:?}", e)),
478 )
479 }
480 Err(e) => {
481 error!("redb batch_put task panicked: {:?}", e);
482 (
483 vec![(
484 "_err".to_string(),
485 NodeData {
486 value: Value::Text(format!("task panicked: {:?}", e)),
487 updated_at: SystemTime::now()
488 .duration_since(UNIX_EPOCH)
489 .unwrap_or_default()
490 .as_millis() as f64,
491 },
492 )]
493 .into_iter()
494 .collect(),
495 Some(format!("redb batch_put task panicked: {:?}", e)),
496 )
497 }
498 };
499 let mut nodes = BTreeMap::new();
500 nodes.insert("_ack".to_string(), ack_children);
501 let ack = Put::new(nodes, Some(batch_id.to_string()), ctx.addr.clone());
502 let _ = batch_from.send(Message::Put(ack));
503 if err_msg.is_some() {
504 debug!("redb batch_put ack sent with _err for {}", batch_id);
505 }
506 }
507}
508
509impl Default for RedbStorage {
510 fn default() -> Self {
511 Self::new()
512 }
513}
514
515#[cfg(test)]
516mod tests {
517 use super::*;
518
519 fn create_test_storage(suffix: &str) -> RedbStorage {
520 let path = format!("/tmp/beam-test-{}-{}.redb", std::process::id(), suffix);
521 RedbStorage::new_with_config(Config::default(), &path, None)
522 }
523
524 #[tokio::test]
525 async fn test_redb_storage_creates_db() {
526 let storage = create_test_storage("create");
527 assert!(!storage.path.is_empty());
528 let _ = std::fs::remove_file(&storage.path);
529 }
530
531 #[tokio::test]
532 async fn test_redb_storage_default() {
533 let storage = RedbStorage::default();
534 let _ = std::fs::remove_file(&storage.path);
535 }
536
537 #[tokio::test]
538 async fn test_redb_storage_clone() {
539 let storage = create_test_storage("clone");
540 let cloned = storage.clone();
541 assert_eq!(storage.path, cloned.path);
542 let _ = std::fs::remove_file(&storage.path);
543 }
544
545 #[tokio::test]
561 async fn test_redb_get_always_replies_when_in_response_to_set() {
562 use crate::actor::{Actor, ActorContext};
563 use crate::message::Put;
564 use std::collections::BTreeMap;
565 use tokio::sync::mpsc::unbounded_channel;
566
567 let mut storage = create_test_storage("ack-always");
568 let ctx = ActorContext::new("test".to_string());
569
570 let mut children = BTreeMap::new();
572 children.insert(
573 "k".to_string(),
574 NodeData {
575 value: Value::Text("v".to_string()),
576 updated_at: 0.0,
577 },
578 );
579 let mut nodes = BTreeMap::new();
580 nodes.insert("n1".to_string(), children.clone());
581 let seed_put = Put::new(nodes, None, ctx.addr.clone());
582 Actor::handle(&mut storage, Message::Put(seed_put), &ctx).await;
583
584 let (tx, mut rx) = unbounded_channel::<Message>();
586 let from_addr = crate::actor::Addr::new(tx);
587
588 let mut reply = Put::new(
590 {
591 let mut m = BTreeMap::new();
592 m.insert("n1".to_string(), children.clone());
593 m
594 },
595 Some("get-id-42".to_string()),
596 ctx.addr.clone(),
597 );
598 reply.to_string(); let matching_checksum = reply.checksum;
600
601 let get = Get {
604 id: "get-id-42".to_string(),
605 from: from_addr.clone(),
606 recipients: None,
607 node_id: "n1".to_string(),
608 checksum: matching_checksum,
609 child_key: None,
610 json_str: None,
611 };
612
613 Actor::handle(&mut storage, Message::Get(get), &ctx).await;
614
615 let received = tokio::time::timeout(std::time::Duration::from_millis(500), rx.recv()).await;
618
619 let _ = std::fs::remove_file(&storage.path);
620
621 match received {
622 Ok(Some(Message::Put(reply_put))) => {
623 assert_eq!(
624 reply_put.in_response_to.as_deref(),
625 Some("get-id-42"),
626 "reply must carry in_response_to so client can drain sentinel"
627 );
628 }
629 Ok(Some(other)) => panic!("expected Put reply, got {:?}", other),
630 Ok(None) => panic!("sender closed before reply sent"),
631 Err(_) => panic!(
632 "BUG: redb_storage stayed silent despite matching in_response_to. \
633 This hangs drain_until_sentinel forever."
634 ),
635 }
636 }
637}