1use arena_btreemap::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::<Children>(bytes))
149 }
150 Ok(None) => {
151 debug!("redb get: no data for node_id={}", get.node_id);
152 let mut reply_with_nodes = BTreeMap::default();
154 reply_with_nodes.insert(get.node_id.clone(), BTreeMap::default());
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: Children = BTreeMap::default();
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::default();
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.iter().rev() {
210 if !node_id.is_empty() && node_id.starts_with('_') {
212 continue;
213 }
214
215 let mut children_for_node: Children = match node_table.get(&**node_id)? {
216 Some(access_guard) => {
217 let bytes = access_guard.value();
218 postcard::from_bytes(bytes).unwrap_or_default()
219 }
220 None => BTreeMap::default(),
221 };
222
223 for (child_id, child_data) in update_data {
224 let should_write = !matches!(
225 children_for_node.get(child_id),
226 Some(existing) if existing.updated_at > child_data.updated_at
227 );
228
229 if should_write {
230 children_for_node.insert(child_id.clone(), child_data.clone());
231 }
232 }
233
234 if children_for_node.is_empty() {
235 node_table.remove(&**node_id)?;
236 } else {
237 let bytes = postcard::to_allocvec(&children_for_node).map_err(|e| {
238 redb::Error::Io(std::io::Error::other(format!(
239 "postcard serialize: {:?}",
240 e
241 )))
242 })?;
243 node_table.insert(&**node_id, bytes.as_slice())?;
244 }
245 }
246
247 let now = SystemTime::now()
248 .duration_since(UNIX_EPOCH)
249 .unwrap_or_default()
250 .as_secs();
251 meta_table.insert("_last_write", now)?;
252 Ok(())
253 }
254
255 fn handle_put_internal(&self, put: Put) -> Result<(), redb::Error> {
257 let mut wtxn = self.db.begin_write()?;
258 self.apply_put_to_tables(&mut wtxn, put)?;
259 wtxn.commit()?;
260 Ok(())
261 }
262
263 fn handle_batch_put(&self, batch: BatchPut) -> Result<(), redb::Error> {
267 let mut wtxn = self.db.begin_write()?;
268 for put in batch.puts {
269 self.apply_put_to_tables(&mut wtxn, put)?;
270 }
271 wtxn.commit()?;
272 Ok(())
273 }
274}
275
276#[async_trait]
277impl Actor for RedbStorage {
278 async fn pre_start(&mut self, _ctx: &ActorContext) {
279 debug!("RedbStorage started at {}", self.path);
280 if let Ok(wtxn) = self.db.begin_write() {
282 let _ = wtxn.open_table(BEAM_NODES);
283 let _ = wtxn.open_table(BEAM_META);
284 let _ = wtxn.commit();
285 }
286 }
287
288 async fn stopping(&mut self, _ctx: &ActorContext) {
289 info!(
294 "RedbStorage stopping at {} — all writes committed",
295 self.path
296 );
297 }
298
299 async fn handle(&mut self, message: Arc<Message>, ctx: &ActorContext) {
300 match &*message {
301 Message::Get(get) => self.handle_get(get.clone(), ctx),
302 Message::Put(put) => {
303 let put_id = put.id.clone();
304 let put_from = put.from.clone();
305 let put = put.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 batch = batch.clone();
315 let storage = self.clone();
316 let result =
317 tokio::task::spawn_blocking(move || storage.handle_batch_put(batch)).await;
318 self.send_batch_put_ack_after_commit(&batch_id, &batch_from, &result, ctx);
319 }
320 Message::Flush(flush) => {
321 let flush_id = flush.id.clone();
322 let from_addr = flush.from.clone();
323 let ctx_addr = ctx.addr.clone();
324
325 let mut ack_children = BTreeMap::default();
328 ack_children.insert(
329 "_flushed".to_string(),
330 NodeData {
331 value: Value::Text("true".to_string()),
332 updated_at: SystemTime::now()
333 .duration_since(UNIX_EPOCH)
334 .unwrap_or_default()
335 .as_millis() as f64,
336 },
337 );
338 let mut ack_nodes = BTreeMap::default();
339 ack_nodes.insert("_ack".to_string(), ack_children);
340 let put = Put::new(ack_nodes, Some(flush_id), ctx_addr.clone());
341 put.to_string(); let _ = from_addr.send(Message::Put(put));
343 }
344 _ => {}
345 }
346 }
347
348 fn try_clone_storage(&self) -> Option<Box<dyn Actor>> {
353 Some(Box::new(self.clone()))
354 }
355}
356
357impl RedbStorage {
358 fn send_put_ack_after_commit(
367 &self,
368 put_id: &str,
369 put_from: &Addr,
370 result: &Result<Result<(), redb::Error>, tokio::task::JoinError>,
371 ctx: &ActorContext,
372 ) {
373 let (ack_children, err_msg) = match result {
374 Ok(Ok(())) => (
375 vec![(
376 "_ack".to_string(),
377 NodeData {
378 value: Value::Text("ok".to_string()),
379 updated_at: SystemTime::now()
380 .duration_since(UNIX_EPOCH)
381 .unwrap_or_default()
382 .as_millis() as f64,
383 },
384 )]
385 .into_iter()
386 .collect::<Children>(),
387 None,
388 ),
389 Ok(Err(e)) => {
390 error!("redb put commit failed: {:?}", e);
391 (
392 vec![(
393 "_err".to_string(),
394 NodeData {
395 value: Value::Text(format!("{:?}", e)),
396 updated_at: SystemTime::now()
397 .duration_since(UNIX_EPOCH)
398 .unwrap_or_default()
399 .as_millis() as f64,
400 },
401 )]
402 .into_iter()
403 .collect::<Children>(),
404 Some(format!("redb put commit failed: {:?}", e)),
405 )
406 }
407 Err(e) => {
408 error!("redb put task panicked: {:?}", e);
409 (
410 vec![(
411 "_err".to_string(),
412 NodeData {
413 value: Value::Text(format!("task panicked: {:?}", e)),
414 updated_at: SystemTime::now()
415 .duration_since(UNIX_EPOCH)
416 .unwrap_or_default()
417 .as_millis() as f64,
418 },
419 )]
420 .into_iter()
421 .collect::<Children>(),
422 Some(format!("redb put task panicked: {:?}", e)),
423 )
424 }
425 };
426 let mut nodes = BTreeMap::default();
427 nodes.insert("_ack".to_string(), ack_children);
428 let ack = Put::new(nodes, Some(put_id.to_string()), ctx.addr.clone());
429 let _ = put_from.send(Message::Put(ack));
430 if err_msg.is_some() {
431 debug!("redb put ack sent with _err for {}", put_id);
432 }
433 }
434
435 fn send_batch_put_ack_after_commit(
441 &self,
442 batch_id: &str,
443 batch_from: &Addr,
444 result: &Result<Result<(), redb::Error>, tokio::task::JoinError>,
445 ctx: &ActorContext,
446 ) {
447 let (ack_children, err_msg) = match result {
448 Ok(Ok(())) => (
449 vec![(
450 "_ack".to_string(),
451 NodeData {
452 value: Value::Text("ok".to_string()),
453 updated_at: SystemTime::now()
454 .duration_since(UNIX_EPOCH)
455 .unwrap_or_default()
456 .as_millis() as f64,
457 },
458 )]
459 .into_iter()
460 .collect::<Children>(),
461 None,
462 ),
463 Ok(Err(e)) => {
464 error!("redb batch_put commit failed: {:?}", e);
465 (
466 vec![(
467 "_err".to_string(),
468 NodeData {
469 value: Value::Text(format!("{:?}", e)),
470 updated_at: SystemTime::now()
471 .duration_since(UNIX_EPOCH)
472 .unwrap_or_default()
473 .as_millis() as f64,
474 },
475 )]
476 .into_iter()
477 .collect::<Children>(),
478 Some(format!("redb batch_put commit failed: {:?}", e)),
479 )
480 }
481 Err(e) => {
482 error!("redb batch_put task panicked: {:?}", e);
483 (
484 vec![(
485 "_err".to_string(),
486 NodeData {
487 value: Value::Text(format!("task panicked: {:?}", e)),
488 updated_at: SystemTime::now()
489 .duration_since(UNIX_EPOCH)
490 .unwrap_or_default()
491 .as_millis() as f64,
492 },
493 )]
494 .into_iter()
495 .collect::<Children>(),
496 Some(format!("redb batch_put task panicked: {:?}", e)),
497 )
498 }
499 };
500 let mut nodes = BTreeMap::default();
501 nodes.insert("_ack".to_string(), ack_children);
502 let ack = Put::new(nodes, Some(batch_id.to_string()), ctx.addr.clone());
503 let _ = batch_from.send(Message::Put(ack));
504 if err_msg.is_some() {
505 debug!("redb batch_put ack sent with _err for {}", batch_id);
506 }
507 }
508}
509
510impl Default for RedbStorage {
511 fn default() -> Self {
512 Self::new()
513 }
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519
520 fn create_test_storage(suffix: &str) -> RedbStorage {
521 let path = format!("/tmp/beam-test-{}-{}.redb", std::process::id(), suffix);
522 RedbStorage::new_with_config(Config::default(), &path, None)
523 }
524
525 #[tokio::test]
526 async fn test_redb_storage_creates_db() {
527 let storage = create_test_storage("create");
528 assert!(!storage.path.is_empty());
529 let _ = std::fs::remove_file(&storage.path);
530 }
531
532 #[tokio::test]
533 async fn test_redb_storage_default() {
534 let storage = RedbStorage::default();
535 let _ = std::fs::remove_file(&storage.path);
536 }
537
538 #[tokio::test]
539 async fn test_redb_storage_clone() {
540 let storage = create_test_storage("clone");
541 let cloned = storage.clone();
542 assert_eq!(storage.path, cloned.path);
543 let _ = std::fs::remove_file(&storage.path);
544 }
545
546 #[tokio::test]
562 async fn test_redb_get_always_replies_when_in_response_to_set() {
563 use crate::actor::{Actor, ActorContext};
564 use crate::message::Put;
565 use arena_btreemap::BTreeMap;
566
567 let mut storage = create_test_storage("ack-always");
568 let ctx = ActorContext::new("test".to_string());
569
570 let mut children = BTreeMap::default();
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::default();
580 nodes.insert("n1".to_string(), children.clone());
581 let seed_put = Put::new(nodes, None, ctx.addr.clone());
582 Actor::handle(&mut storage, Arc::new(Message::Put(seed_put)), &ctx).await;
583
584 let (tx, rx) = crate::mailbox::mailbox(16);
586 let from_addr = crate::actor::Addr::new(tx);
587 let mut rx = rx;
588
589 let reply = Put::new(
591 {
592 let mut m = BTreeMap::default();
593 m.insert("n1".to_string(), children.clone());
594 m
595 },
596 Some("get-id-42".to_string()),
597 ctx.addr.clone(),
598 );
599 reply.to_string(); let matching_checksum = reply.checksum;
601
602 let get = Get {
605 id: "get-id-42".to_string(),
606 from: from_addr.clone(),
607 recipients: None,
608 node_id: "n1".to_string(),
609 checksum: matching_checksum,
610 child_key: None,
611 };
612
613 Actor::handle(&mut storage, Arc::new(Message::Get(get)), &ctx).await;
614
615 let received =
618 crate::tokio_time::timeout(web_time::Duration::from_millis(500), rx.recv()).await;
619
620 let _ = std::fs::remove_file(&storage.path);
621
622 match received {
623 Ok(Some(msg)) => match &*msg {
624 Message::Put(reply_put) => {
625 assert_eq!(
626 reply_put.in_response_to.as_deref(),
627 Some("get-id-42"),
628 "reply must carry in_response_to so client can drain sentinel"
629 );
630 }
631 other => panic!("expected Put reply, got {:?}", other),
632 },
633 Ok(None) => panic!("sender closed before reply sent"),
634 Err(_) => panic!(
635 "BUG: redb_storage stayed silent despite matching in_response_to. \
636 This hangs drain_until_sentinel forever."
637 ),
638 }
639 }
640}