beam/adapters/memory_storage.rs
1//! In-memory storage adapter — the default storage backend for BEAM.
2//!
3//! [`MemoryStorage`] stores all graph data in a `HashMap` protected by a
4//! `parking_lot::RwLock`. It is the simplest storage adapter and is used
5//! by default when no persistent storage is configured.
6//!
7//! # Semantics
8//!
9//! - **Get**: Returns the stored children for a node ID. If the node has no
10//! children, an empty `Put` reply is sent (so `.map()` listeners don't hang).
11//! - **Put**: Merges incoming data with existing children using `updated_at`
12//! timestamps for conflict resolution (last-write-wins per child).
13//! - **Flush**: No-op (memory storage has no disk state). Sends an immediate
14//! ack so callers never hang waiting for a barrier.
15//! - **BatchPut**: Processes each constituent `Put` sequentially.
16//!
17//! # Thread Safety
18//!
19//! The store is `Arc<RwLock<HashMap>>`, allowing concurrent reads and
20//! exclusive writes. The actor model ensures messages are processed
21//! sequentially within the actor's task.
22
23#![allow(clippy::mutable_key_type)] // Addr hashes by id field, not interior-mutable sender
24
25use std::collections::{BTreeMap, HashMap, HashSet};
26
27use crate::actor::{Actor, ActorContext};
28use crate::message::{BatchPut, Get, Message, Put};
29use crate::types::*;
30
31use async_trait::async_trait;
32use log::{debug, info};
33use parking_lot::RwLock;
34use std::sync::Arc;
35
36/// In-memory storage adapter backed by `HashMap<String, Children>`.
37///
38/// See the [module docs](self) for semantics.
39#[derive(Clone)]
40pub struct MemoryStorage {
41 store: Arc<RwLock<HashMap<String, Children>>>,
42}
43
44impl Default for MemoryStorage {
45 fn default() -> Self {
46 Self::new()
47 }
48}
49
50impl MemoryStorage {
51 /// Creates a new empty in-memory storage adapter.
52 pub fn new() -> Self {
53 MemoryStorage {
54 store: Arc::new(RwLock::new(HashMap::new())),
55 }
56 }
57
58 /// Handles a `Get` request by looking up the node ID and replying with
59 /// its children (or an empty reply if not found).
60 ///
61 /// If `child_key` is specified in the `Get`, only that specific child is
62 /// returned. If the child doesn't exist, no reply is sent (the requester
63 /// simply doesn't receive data).
64 fn handle_get(&self, get: Get, ctx: &ActorContext) {
65 if let Some(children) = self.store.read().get(&get.node_id).cloned() {
66 debug!("have {}: {:?}", get.node_id, children);
67 let reply_with_children = match &get.child_key {
68 Some(child_key) => {
69 // Reply with specific child if it's found
70 match children.get(child_key) {
71 Some(child_val) => {
72 let mut r = BTreeMap::new();
73 r.insert(child_key.clone(), child_val.clone());
74 r
75 }
76 None => {
77 return;
78 }
79 }
80 }
81 None => children.clone(), // Reply with all children of this node
82 };
83 let mut reply_with_nodes = BTreeMap::new();
84 reply_with_nodes.insert(get.node_id.clone(), reply_with_children);
85 let mut recipients = HashSet::new();
86 recipients.insert(get.from.clone());
87 let my_addr = ctx.addr.clone();
88 let put = Put::new(reply_with_nodes, Some(get.id.clone()), my_addr);
89 let _ = get.from.send(Message::Put(put));
90 } else {
91 debug!("have not {}", get.node_id);
92 // Empty set: still a valid replay. Emit sentinel so `.map()` doesn't hang.
93 let mut reply_with_nodes = BTreeMap::new();
94 reply_with_nodes.insert(get.node_id.clone(), BTreeMap::new());
95 let put = Put::new(reply_with_nodes, Some(get.id.clone()), ctx.addr.clone());
96 let _ = get.from.send(Message::Put(put));
97 }
98 }
99
100 /// Handles a `Put` by merging `updated_nodes` into the store.
101 ///
102 /// For each node, existing children are compared by `updated_at` —
103 /// a child is only overwritten if the incoming `updated_at` is >= the
104 /// existing one (last-write-wins).
105 ///
106 /// After a successful merge, sends an ack `Put` directly back to the
107 /// originating node (NOT through the router — that would route through
108 /// `seen_get_messages` and be silently dropped). The ack payload uses
109 /// the same `_ack`/`_err` sentinel children as `Flush`, so the originating
110 /// `Node::handle_put` can drain `pending_puts` and resolve the awaiter.
111 fn handle_put(&self, put: Put, ctx: &ActorContext) {
112 let put_result = self.apply_put(&put);
113 self.send_put_ack(&put, &put_result, ctx);
114 }
115
116 /// Applies a put to the in-memory store, returning Ok or an error string.
117 fn apply_put(&self, put: &Put) -> Result<(), String> {
118 for (node_id, update_data) in put.updated_nodes.iter().rev() {
119 debug!("saving k-v {}: {:?}", node_id, update_data);
120 let mut write = self.store.write();
121 if let Some(children) = write.get_mut(node_id) {
122 for (child_id, child_data) in update_data {
123 if let Some(existing) = children.get(child_id) {
124 if child_data.updated_at >= existing.updated_at {
125 children.insert(child_id.clone(), child_data.clone());
126 }
127 } else {
128 children.insert(child_id.clone(), child_data.clone());
129 }
130 }
131 } else {
132 write.insert(node_id.to_string(), update_data.clone());
133 }
134 }
135 Ok(())
136 }
137
138 /// Sends a put-ack message directly to the originating node's addr.
139 ///
140 /// Uses the same sentinel convention as the Flush ack:
141 /// - `_ack` child → success
142 /// - `_err` child carrying the message → failure
143 fn send_put_ack(&self, put: &Put, result: &Result<(), String>, ctx: &ActorContext) {
144 let mut ack_children = BTreeMap::new();
145 match result {
146 Ok(()) => {
147 ack_children.insert(
148 "_ack".to_string(),
149 NodeData {
150 value: Value::Text("ok".to_string()),
151 updated_at: std::time::SystemTime::now()
152 .duration_since(std::time::UNIX_EPOCH)
153 .unwrap_or_default()
154 .as_millis() as f64,
155 },
156 );
157 }
158 Err(msg) => {
159 ack_children.insert(
160 "_err".to_string(),
161 NodeData {
162 value: Value::Text(msg.clone()),
163 updated_at: std::time::SystemTime::now()
164 .duration_since(std::time::UNIX_EPOCH)
165 .unwrap_or_default()
166 .as_millis() as f64,
167 },
168 );
169 }
170 }
171 let mut nodes = BTreeMap::new();
172 nodes.insert("_ack".to_string(), ack_children);
173 let ack = Put::new(nodes, Some(put.id.clone()), ctx.addr.clone());
174 let _ = put.from.send(Message::Put(ack));
175 }
176
177 /// Handles a `BatchPut` by applying each constituent Put and sending
178 /// a single batch ack back to the originating node.
179 fn handle_batch_put(&self, batch: BatchPut, ctx: &ActorContext) {
180 let mut last_err: Option<String> = None;
181 for put in batch.puts.iter() {
182 if let Err(e) = self.apply_put(put) {
183 last_err = Some(e);
184 }
185 }
186 let result = last_err.map(Err).unwrap_or(Ok(()));
187 self.send_batch_put_ack(&batch, &result, ctx);
188 }
189
190 /// Sends a `BatchPut` ack back to the originating node.
191 ///
192 /// Mirrors the Put ack pattern but uses the BatchPut message type. The
193 /// originating node's `handle_put` only drains on `Message::Put` acks —
194 /// we still need to drain `pending_puts` for the batch case. The cleanest
195 /// way: send the batch ack back as a single Put ack message keyed on
196 /// `batch.id`, reusing the same routing.
197 fn send_batch_put_ack(
198 &self,
199 batch: &BatchPut,
200 result: &Result<(), String>,
201 ctx: &ActorContext,
202 ) {
203 let mut ack_children = BTreeMap::new();
204 match result {
205 Ok(()) => {
206 ack_children.insert(
207 "_ack".to_string(),
208 NodeData {
209 value: Value::Text("ok".to_string()),
210 updated_at: std::time::SystemTime::now()
211 .duration_since(std::time::UNIX_EPOCH)
212 .unwrap_or_default()
213 .as_millis() as f64,
214 },
215 );
216 }
217 Err(msg) => {
218 ack_children.insert(
219 "_err".to_string(),
220 NodeData {
221 value: Value::Text(msg.clone()),
222 updated_at: std::time::SystemTime::now()
223 .duration_since(std::time::UNIX_EPOCH)
224 .unwrap_or_default()
225 .as_millis() as f64,
226 },
227 );
228 }
229 }
230 let mut nodes = BTreeMap::new();
231 nodes.insert("_ack".to_string(), ack_children);
232 // Send as Put ack keyed on batch.id so Node::handle_put drains it.
233 let ack = Put::new(nodes, Some(batch.id.clone()), ctx.addr.clone());
234 let _ = batch.from.send(Message::Put(ack));
235 }
236}
237
238#[async_trait]
239impl Actor for MemoryStorage {
240 async fn pre_start(&mut self, _ctx: &ActorContext) {
241 info!("MemoryStorage adapter starting");
242 }
243
244 async fn handle(&mut self, message: Message, ctx: &ActorContext) {
245 match message {
246 Message::Get(get) => self.handle_get(get, ctx),
247 Message::Put(put) => self.handle_put(put, ctx),
248 Message::Flush(flush) => {
249 // Memory storage has no disk state; flush is a no-op.
250 // Ack the barrier so callers never hang.
251 let mut ack = BTreeMap::new();
252 ack.insert(
253 "_flushed".to_string(),
254 NodeData {
255 value: Value::Text("true".to_string()),
256 updated_at: std::time::SystemTime::now()
257 .duration_since(std::time::UNIX_EPOCH)
258 .unwrap_or_default()
259 .as_millis() as f64,
260 },
261 );
262 let mut nodes = BTreeMap::new();
263 nodes.insert("_ack".to_string(), ack);
264 let mut put = Put::new(nodes, Some(flush.id), ctx.addr.clone());
265 put.to_string(); // compute checksum
266 let _ = flush.from.send(Message::Put(put));
267 }
268 Message::BatchPut(batch) => self.handle_batch_put(batch, ctx),
269 _ => {}
270 }
271 }
272
273 /// MemoryStorage does not split into read/write actors.
274 ///
275 /// In-memory writes are synchronous (no fsync), so there is no benefit
276 /// to splitting reads from writes. Keeping a single actor preserves
277 /// read-after-write ordering for tests and synchronous use cases.
278 fn try_clone_storage(&self) -> Option<Box<dyn Actor>> {
279 None
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[tokio::test]
288 async fn test_memory_storage_new() {
289 let storage = MemoryStorage::new();
290 assert!(storage.store.read().is_empty());
291 }
292
293 #[tokio::test]
294 async fn test_memory_storage_default() {
295 let storage = MemoryStorage::default();
296 assert!(storage.store.read().is_empty());
297 }
298}