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    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: web_time::SystemTime::now()
152                            .duration_since(web_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: web_time::SystemTime::now()
164                            .duration_since(web_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: web_time::SystemTime::now()
211                            .duration_since(web_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: web_time::SystemTime::now()
223                            .duration_since(web_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: web_time::SystemTime::now()
257                            .duration_since(web_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}