1use std::collections::BTreeMap;
37use std::path::Path;
38use std::sync::Arc;
39use web_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 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 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: Arc<Message>, ctx: &ActorContext) {
301 match &*message {
302 Message::Get(get) => self.handle_get(get.clone(), ctx),
303 Message::Put(put) => {
304 let put_id = put.id.clone();
305 let put_from = put.from.clone();
306 let put = put.clone();
307 let storage = self.clone();
308 let result =
309 tokio::task::spawn_blocking(move || storage.handle_put_internal(put)).await;
310 self.send_put_ack_after_commit(&put_id, &put_from, &result, ctx);
311 }
312 Message::BatchPut(batch) => {
313 let batch_id = batch.id.clone();
314 let batch_from = batch.from.clone();
315 let batch = batch.clone();
316 let storage = self.clone();
317 let result =
318 tokio::task::spawn_blocking(move || storage.handle_batch_put(batch)).await;
319 self.send_batch_put_ack_after_commit(&batch_id, &batch_from, &result, ctx);
320 }
321 Message::Flush(flush) => {
322 let flush_id = flush.id.clone();
323 let from_addr = flush.from.clone();
324 let ctx_addr = ctx.addr.clone();
325
326 let mut ack_children = BTreeMap::new();
329 ack_children.insert(
330 "_flushed".to_string(),
331 NodeData {
332 value: Value::Text("true".to_string()),
333 updated_at: SystemTime::now()
334 .duration_since(UNIX_EPOCH)
335 .unwrap_or_default()
336 .as_millis() as f64,
337 },
338 );
339 let mut ack_nodes = BTreeMap::new();
340 ack_nodes.insert("_ack".to_string(), ack_children);
341 let put = Put::new(ack_nodes, Some(flush_id), ctx_addr.clone());
342 put.to_string(); let _ = from_addr.send(Message::Put(put));
344 }
345 _ => {}
346 }
347 }
348
349 fn try_clone_storage(&self) -> Option<Box<dyn Actor>> {
354 Some(Box::new(self.clone()))
355 }
356}
357
358impl RedbStorage {
359 fn send_put_ack_after_commit(
368 &self,
369 put_id: &str,
370 put_from: &Addr,
371 result: &Result<Result<(), redb::Error>, tokio::task::JoinError>,
372 ctx: &ActorContext,
373 ) {
374 let (ack_children, err_msg) = match result {
375 Ok(Ok(())) => (
376 vec![(
377 "_ack".to_string(),
378 NodeData {
379 value: Value::Text("ok".to_string()),
380 updated_at: SystemTime::now()
381 .duration_since(UNIX_EPOCH)
382 .unwrap_or_default()
383 .as_millis() as f64,
384 },
385 )]
386 .into_iter()
387 .collect::<BTreeMap<_, _>>(),
388 None,
389 ),
390 Ok(Err(e)) => {
391 error!("redb put commit failed: {:?}", e);
392 (
393 vec![(
394 "_err".to_string(),
395 NodeData {
396 value: Value::Text(format!("{:?}", e)),
397 updated_at: SystemTime::now()
398 .duration_since(UNIX_EPOCH)
399 .unwrap_or_default()
400 .as_millis() as f64,
401 },
402 )]
403 .into_iter()
404 .collect(),
405 Some(format!("redb put commit failed: {:?}", e)),
406 )
407 }
408 Err(e) => {
409 error!("redb put task panicked: {:?}", e);
410 (
411 vec![(
412 "_err".to_string(),
413 NodeData {
414 value: Value::Text(format!("task panicked: {:?}", e)),
415 updated_at: SystemTime::now()
416 .duration_since(UNIX_EPOCH)
417 .unwrap_or_default()
418 .as_millis() as f64,
419 },
420 )]
421 .into_iter()
422 .collect(),
423 Some(format!("redb put task panicked: {:?}", e)),
424 )
425 }
426 };
427 let mut nodes = BTreeMap::new();
428 nodes.insert("_ack".to_string(), ack_children);
429 let ack = Put::new(nodes, Some(put_id.to_string()), ctx.addr.clone());
430 let _ = put_from.send(Message::Put(ack));
431 if err_msg.is_some() {
432 debug!("redb put ack sent with _err for {}", put_id);
433 }
434 }
435
436 fn send_batch_put_ack_after_commit(
442 &self,
443 batch_id: &str,
444 batch_from: &Addr,
445 result: &Result<Result<(), redb::Error>, tokio::task::JoinError>,
446 ctx: &ActorContext,
447 ) {
448 let (ack_children, err_msg) = match result {
449 Ok(Ok(())) => (
450 vec![(
451 "_ack".to_string(),
452 NodeData {
453 value: Value::Text("ok".to_string()),
454 updated_at: SystemTime::now()
455 .duration_since(UNIX_EPOCH)
456 .unwrap_or_default()
457 .as_millis() as f64,
458 },
459 )]
460 .into_iter()
461 .collect::<BTreeMap<_, _>>(),
462 None,
463 ),
464 Ok(Err(e)) => {
465 error!("redb batch_put commit failed: {:?}", e);
466 (
467 vec![(
468 "_err".to_string(),
469 NodeData {
470 value: Value::Text(format!("{:?}", e)),
471 updated_at: SystemTime::now()
472 .duration_since(UNIX_EPOCH)
473 .unwrap_or_default()
474 .as_millis() as f64,
475 },
476 )]
477 .into_iter()
478 .collect(),
479 Some(format!("redb batch_put commit failed: {:?}", e)),
480 )
481 }
482 Err(e) => {
483 error!("redb batch_put task panicked: {:?}", e);
484 (
485 vec![(
486 "_err".to_string(),
487 NodeData {
488 value: Value::Text(format!("task panicked: {:?}", e)),
489 updated_at: SystemTime::now()
490 .duration_since(UNIX_EPOCH)
491 .unwrap_or_default()
492 .as_millis() as f64,
493 },
494 )]
495 .into_iter()
496 .collect(),
497 Some(format!("redb batch_put task panicked: {:?}", e)),
498 )
499 }
500 };
501 let mut nodes = BTreeMap::new();
502 nodes.insert("_ack".to_string(), ack_children);
503 let ack = Put::new(nodes, Some(batch_id.to_string()), ctx.addr.clone());
504 let _ = batch_from.send(Message::Put(ack));
505 if err_msg.is_some() {
506 debug!("redb batch_put ack sent with _err for {}", batch_id);
507 }
508 }
509}
510
511impl Default for RedbStorage {
512 fn default() -> Self {
513 Self::new()
514 }
515}
516
517#[cfg(test)]
518mod tests {
519 use super::*;
520
521 fn create_test_storage(suffix: &str) -> RedbStorage {
522 let path = format!("/tmp/beam-test-{}-{}.redb", std::process::id(), suffix);
523 RedbStorage::new_with_config(Config::default(), &path, None)
524 }
525
526 #[tokio::test]
527 async fn test_redb_storage_creates_db() {
528 let storage = create_test_storage("create");
529 assert!(!storage.path.is_empty());
530 let _ = std::fs::remove_file(&storage.path);
531 }
532
533 #[tokio::test]
534 async fn test_redb_storage_default() {
535 let storage = RedbStorage::default();
536 let _ = std::fs::remove_file(&storage.path);
537 }
538
539 #[tokio::test]
540 async fn test_redb_storage_clone() {
541 let storage = create_test_storage("clone");
542 let cloned = storage.clone();
543 assert_eq!(storage.path, cloned.path);
544 let _ = std::fs::remove_file(&storage.path);
545 }
546
547 #[tokio::test]
563 async fn test_redb_get_always_replies_when_in_response_to_set() {
564 use crate::actor::{Actor, ActorContext};
565 use crate::message::Put;
566 use std::collections::BTreeMap;
567
568 let mut storage = create_test_storage("ack-always");
569 let ctx = ActorContext::new("test".to_string());
570
571 let mut children = BTreeMap::new();
573 children.insert(
574 "k".to_string(),
575 NodeData {
576 value: Value::Text("v".to_string()),
577 updated_at: 0.0,
578 },
579 );
580 let mut nodes = BTreeMap::new();
581 nodes.insert("n1".to_string(), children.clone());
582 let seed_put = Put::new(nodes, None, ctx.addr.clone());
583 Actor::handle(&mut storage, Arc::new(Message::Put(seed_put)), &ctx).await;
584
585 let (tx, rx) = crate::mailbox::mailbox(16);
587 let from_addr = crate::actor::Addr::new(tx);
588 let mut rx = rx;
589
590 let reply = Put::new(
592 {
593 let mut m = BTreeMap::new();
594 m.insert("n1".to_string(), children.clone());
595 m
596 },
597 Some("get-id-42".to_string()),
598 ctx.addr.clone(),
599 );
600 reply.to_string(); let matching_checksum = reply.checksum;
602
603 let get = Get {
606 id: "get-id-42".to_string(),
607 from: from_addr.clone(),
608 recipients: None,
609 node_id: "n1".to_string(),
610 checksum: matching_checksum,
611 child_key: None,
612 };
613
614 Actor::handle(&mut storage, Arc::new(Message::Get(get)), &ctx).await;
615
616 let received =
619 crate::tokio_time::timeout(web_time::Duration::from_millis(500), rx.recv()).await;
620
621 let _ = std::fs::remove_file(&storage.path);
622
623 match received {
624 Ok(Some(msg)) => match &*msg {
625 Message::Put(reply_put) => {
626 assert_eq!(
627 reply_put.in_response_to.as_deref(),
628 Some("get-id-42"),
629 "reply must carry in_response_to so client can drain sentinel"
630 );
631 }
632 other => panic!("expected Put reply, got {:?}", other),
633 },
634 Ok(None) => panic!("sender closed before reply sent"),
635 Err(_) => panic!(
636 "BUG: redb_storage stayed silent despite matching in_response_to. \
637 This hangs drain_until_sentinel forever."
638 ),
639 }
640 }
641}