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 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}