Skip to main content

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}