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