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 /// Handles a `Get` by reading from the in-memory store and replying with
65 /// a `Put` containing the requested node data. Takes `&Get` to avoid
66 /// cloning the request — the reply is constructed from borrows.
67 fn handle_get(&self, get: &Get, ctx: &ActorContext) {
68 if let Some(children) = self.store.read().get(&get.node_id).cloned() {
69 debug!("have {}: {:?}", get.node_id, children);
70 let reply_with_children = match &get.child_key {
71 Some(child_key) => {
72 // Reply with specific child if it's found
73 match children.get(child_key) {
74 Some(child_val) => {
75 let mut r = BTreeMap::new();
76 r.insert(child_key.clone(), child_val.clone());
77 r
78 }
79 None => {
80 return;
81 }
82 }
83 }
84 None => children.clone(), // Reply with all children of this node
85 };
86 let mut reply_with_nodes = BTreeMap::new();
87 reply_with_nodes.insert(get.node_id.clone(), reply_with_children);
88 let mut recipients = HashSet::new();
89 recipients.insert(get.from.clone());
90 let my_addr = ctx.addr.clone();
91 let put = Put::new(reply_with_nodes, Some(get.id.clone()), my_addr);
92 let _ = get.from.send(Message::Put(put));
93 } else {
94 debug!("have not {}", get.node_id);
95 // Empty set: still a valid replay. Emit sentinel so `.map()` doesn't hang.
96 let mut reply_with_nodes = BTreeMap::new();
97 reply_with_nodes.insert(get.node_id.clone(), BTreeMap::new());
98 let put = Put::new(reply_with_nodes, Some(get.id.clone()), ctx.addr.clone());
99 let _ = get.from.send(Message::Put(put));
100 }
101 }
102
103 /// Handles a `Put` by merging `updated_nodes` into the store.
104 ///
105 /// For each node, existing children are compared by `updated_at` —
106 /// a child is only overwritten if the incoming `updated_at` is >= the
107 /// existing one (last-write-wins).
108 ///
109 /// After a successful merge, sends an ack `Put` directly back to the
110 /// originating node (NOT through the router — that would route through
111 /// `seen_get_messages` and be silently dropped). The ack payload uses
112 /// the same `_ack`/`_err` sentinel children as `Flush`, so the originating
113 /// `Node::handle_put` can drain `pending_puts` and resolve the awaiter.
114 /// Handles a `Put` by merging `updated_nodes` into the store, then sending
115 /// an ack back to the originating node. Takes `&Put` — the data is read
116 /// for merging and ack construction, never consumed for ownership.
117 fn handle_put(&self, put: &Put, ctx: &ActorContext) {
118 let put_result = self.apply_put(put);
119 self.send_put_ack(put, &put_result, ctx);
120 }
121
122 /// Applies a put to the in-memory store, returning Ok or an error string.
123 fn apply_put(&self, put: &Put) -> Result<(), String> {
124 for (node_id, update_data) in put.updated_nodes.iter().rev() {
125 debug!("saving k-v {}: {:?}", node_id, update_data);
126 let mut write = self.store.write();
127 if let Some(children) = write.get_mut(node_id) {
128 for (child_id, child_data) in update_data {
129 if let Some(existing) = children.get(child_id) {
130 if child_data.updated_at >= existing.updated_at {
131 children.insert(child_id.clone(), child_data.clone());
132 }
133 } else {
134 children.insert(child_id.clone(), child_data.clone());
135 }
136 }
137 } else {
138 write.insert(node_id.to_string(), update_data.clone());
139 }
140 }
141 Ok(())
142 }
143
144 /// Sends a put-ack message directly to the originating node's addr.
145 ///
146 /// Uses the same sentinel convention as the Flush ack:
147 /// - `_ack` child → success
148 /// - `_err` child carrying the message → failure
149 fn send_put_ack(&self, put: &Put, result: &Result<(), String>, ctx: &ActorContext) {
150 let mut ack_children = BTreeMap::new();
151 match result {
152 Ok(()) => {
153 ack_children.insert(
154 "_ack".to_string(),
155 NodeData {
156 value: Value::Text("ok".to_string()),
157 updated_at: web_time::SystemTime::now()
158 .duration_since(web_time::UNIX_EPOCH)
159 .unwrap_or_default()
160 .as_millis() as f64,
161 },
162 );
163 }
164 Err(msg) => {
165 ack_children.insert(
166 "_err".to_string(),
167 NodeData {
168 value: Value::Text(msg.clone()),
169 updated_at: web_time::SystemTime::now()
170 .duration_since(web_time::UNIX_EPOCH)
171 .unwrap_or_default()
172 .as_millis() as f64,
173 },
174 );
175 }
176 }
177 let mut nodes = BTreeMap::new();
178 nodes.insert("_ack".to_string(), ack_children);
179 let ack = Put::new(nodes, Some(put.id.clone()), ctx.addr.clone());
180 let _ = put.from.send(Message::Put(ack));
181 }
182
183 /// Handles a `BatchPut` by applying each constituent Put and sending
184 /// a single batch ack back to the originating node.
185 /// Handles a `BatchPut` by applying each constituent `Put` and sending
186 /// a single batch ack. Takes `&BatchPut` — puts are iterated by reference.
187 fn handle_batch_put(&self, batch: &BatchPut, ctx: &ActorContext) {
188 let mut last_err: Option<String> = None;
189 for put in batch.puts.iter() {
190 if let Err(e) = self.apply_put(put) {
191 last_err = Some(e);
192 }
193 }
194 let result = last_err.map(Err).unwrap_or(Ok(()));
195 self.send_batch_put_ack(batch, &result, ctx);
196 }
197
198 /// Sends a `BatchPut` ack back to the originating node.
199 ///
200 /// Mirrors the Put ack pattern but uses the BatchPut message type. The
201 /// originating node's `handle_put` only drains on `Message::Put` acks —
202 /// we still need to drain `pending_puts` for the batch case. The cleanest
203 /// way: send the batch ack back as a single Put ack message keyed on
204 /// `batch.id`, reusing the same routing.
205 fn send_batch_put_ack(
206 &self,
207 batch: &BatchPut,
208 result: &Result<(), String>,
209 ctx: &ActorContext,
210 ) {
211 let mut ack_children = BTreeMap::new();
212 match result {
213 Ok(()) => {
214 ack_children.insert(
215 "_ack".to_string(),
216 NodeData {
217 value: Value::Text("ok".to_string()),
218 updated_at: web_time::SystemTime::now()
219 .duration_since(web_time::UNIX_EPOCH)
220 .unwrap_or_default()
221 .as_millis() as f64,
222 },
223 );
224 }
225 Err(msg) => {
226 ack_children.insert(
227 "_err".to_string(),
228 NodeData {
229 value: Value::Text(msg.clone()),
230 updated_at: web_time::SystemTime::now()
231 .duration_since(web_time::UNIX_EPOCH)
232 .unwrap_or_default()
233 .as_millis() as f64,
234 },
235 );
236 }
237 }
238 let mut nodes = BTreeMap::new();
239 nodes.insert("_ack".to_string(), ack_children);
240 // Send as Put ack keyed on batch.id so Node::handle_put drains it.
241 let ack = Put::new(nodes, Some(batch.id.clone()), ctx.addr.clone());
242 let _ = batch.from.send(Message::Put(ack));
243 }
244}
245
246#[async_trait]
247impl Actor for MemoryStorage {
248 async fn pre_start(&mut self, _ctx: &ActorContext) {
249 info!("MemoryStorage adapter starting");
250 }
251
252 async fn handle(&mut self, message: Arc<Message>, ctx: &ActorContext) {
253 match &*message {
254 Message::Get(get) => self.handle_get(get, ctx),
255 Message::Put(put) => {
256 self.handle_put(put, ctx);
257 }
258 Message::Flush(flush) => {
259 // Memory storage has no disk state; flush is a no-op.
260 // Ack the barrier so callers never hang.
261 let mut ack = BTreeMap::new();
262 ack.insert(
263 "_flushed".to_string(),
264 NodeData {
265 value: Value::Text("true".to_string()),
266 updated_at: web_time::SystemTime::now()
267 .duration_since(web_time::UNIX_EPOCH)
268 .unwrap_or_default()
269 .as_millis() as f64,
270 },
271 );
272 let mut nodes = BTreeMap::new();
273 nodes.insert("_ack".to_string(), ack);
274 let put = Put::new(nodes, Some(flush.id.clone()), ctx.addr.clone());
275 put.to_string(); // compute checksum
276 let _ = flush.from.send(Message::Put(put));
277 }
278 Message::BatchPut(batch) => self.handle_batch_put(batch, ctx),
279 _ => {}
280 }
281 }
282
283 /// MemoryStorage does not split into read/write actors.
284 ///
285 /// In-memory writes are synchronous (no fsync), so there is no benefit
286 /// to splitting reads from writes. Keeping a single actor preserves
287 /// read-after-write ordering for tests and synchronous use cases.
288 fn try_clone_storage(&self) -> Option<Box<dyn Actor>> {
289 None
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[tokio::test]
298 async fn test_memory_storage_new() {
299 let storage = MemoryStorage::new();
300 assert!(storage.store.read().is_empty());
301 }
302
303 #[tokio::test]
304 async fn test_memory_storage_default() {
305 let storage = MemoryStorage::default();
306 assert!(storage.store.read().is_empty());
307 }
308}