Skip to main content

hydro_lang/deploy/maelstrom/
deploy_runtime_maelstrom.rs

1//! Runtime support for Maelstrom deployment backend.
2//!
3//! This module provides the runtime code that runs inside Maelstrom nodes,
4//! handling stdin/stdout JSON message passing according to the Maelstrom protocol.
5
6#![allow(
7    unused,
8    reason = "unused in trybuild but the __staged version is needed"
9)]
10#![allow(missing_docs, reason = "used internally")]
11
12use std::io::{BufRead, Write};
13use std::sync::Arc;
14use std::sync::atomic::{AtomicBool, Ordering};
15
16use futures::{Stream, StreamExt};
17use serde::{Deserialize, Serialize};
18use stageleft::{QuotedWithContext, RuntimeData, q};
19
20use crate::forward_handle::ForwardHandle;
21use crate::live_collections::boundedness::Unbounded;
22use crate::live_collections::keyed_stream::KeyedStream;
23use crate::live_collections::stream::{ExactlyOnce, NoOrder, TotalOrder};
24use crate::location::dynamic::LocationId;
25use crate::location::member_id::TaglessMemberId;
26use crate::location::{Cluster, LocationKey, MembershipEvent};
27use crate::nondet::nondet;
28
29/// Maelstrom message envelope structure.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct MaelstromMessage<T> {
32    pub src: String,
33    pub dest: String,
34    pub body: T,
35}
36
37/// Maelstrom init message body.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct InitBody {
40    #[serde(rename = "type")]
41    pub msg_type: String,
42    pub msg_id: u64,
43    pub node_id: String,
44    pub node_ids: Vec<String>,
45}
46
47/// Maelstrom `init_ok` response body.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct InitOkBody {
50    #[serde(rename = "type")]
51    pub msg_type: String,
52    pub in_reply_to: u64,
53}
54
55/// Metadata for a Maelstrom node, populated from the init message.
56/// Also manages a shared stdin reader that broadcasts lines to multiple subscribers.
57pub struct MaelstromMeta {
58    pub node_id: String,
59    pub node_ids: Vec<String>,
60    stdin_tx: tokio::sync::broadcast::Sender<String>,
61    /// Set once this node has received its first post-`init` message from a client or a
62    /// peer node. Until then, outbound node-to-node messages are dropped; see
63    /// [`MaelstromMeta::peers_ready`] for why this is necessary.
64    peers_ready: Arc<AtomicBool>,
65}
66
67impl MaelstromMeta {
68    /// Subscribe to stdin lines. Each subscriber receives all lines read from stdin.
69    /// Multiple subscribers can be created and each will receive a copy of every line.
70    pub fn subscribe_stdin(&self) -> tokio_stream::wrappers::BroadcastStream<String> {
71        tokio_stream::wrappers::BroadcastStream::new(self.stdin_tx.subscribe())
72    }
73
74    /// A flag that becomes `true` once it is safe to send node-to-node messages.
75    ///
76    /// Maelstrom sets up nodes concurrently: each node's process is launched, registered in
77    /// the simulated network, and sent `init` independently, with no barrier in between. A
78    /// node that answers `init` quickly may therefore start sending to a peer whose process
79    /// Maelstrom has not launched yet. Maelstrom treats a message to an unregistered
80    /// destination as a fatal assertion failure (`Invalid dest for message ...`) and
81    /// permanently stops reading the *sender's* stdout, so every later response from that
82    /// node (e.g. `topology_ok`) is silently lost and the test fails with
83    /// `Client read timeout`.
84    ///
85    /// Jepsen only opens clients after `db/setup!` (launch + `init`) has completed on every
86    /// node, so receiving a client message proves all peers are registered. A message from a
87    /// peer node proves the same thing transitively: peers also hold their outbound messages
88    /// until *they* have been activated, so any peer message we receive was sent after some
89    /// node observed a client message. Dropping outbound messages before activation is
90    /// permitted because node-to-node channels in the Maelstrom backend are lossy.
91    pub fn peers_ready(&self) -> Arc<AtomicBool> {
92        self.peers_ready.clone()
93    }
94
95    /// Start receiving incoming messages from clients and other nodes, after launching the DFIR.
96    pub fn start_receiving(&self) {
97        let tx_clone = self.stdin_tx.clone();
98        let peers_ready = self.peers_ready.clone();
99        let node_ids: std::collections::HashSet<String> = self.node_ids.iter().cloned().collect();
100
101        // Spawn thread to read stdin and broadcast lines
102        std::thread::spawn(move || {
103            let stdin = std::io::stdin();
104            for line in stdin.lock().lines() {
105                match line {
106                    Ok(l) => {
107                        if !peers_ready.load(Ordering::Relaxed)
108                            && message_activates_node(&l, &node_ids)
109                        {
110                            peers_ready.store(true, Ordering::Release);
111                        }
112                        // Ignore send errors (no receivers)
113                        let _ = tx_clone.send(l);
114                    }
115                    Err(_) => break,
116                }
117            }
118        });
119    }
120}
121
122/// Whether receiving `line` proves that every node in the cluster has been registered in the
123/// Maelstrom network (see [`MaelstromMeta::peers_ready`]). This is the case for messages from
124/// peer nodes and from Jepsen clients (`c1`, `c2`, ...). Replies from Maelstrom's built-in
125/// services (`lin-kv`, `seq-kv`, ...) do not count: services are registered before any node is
126/// launched, so a service reply can arrive while peers are still being set up.
127fn message_activates_node(line: &str, node_ids: &std::collections::HashSet<String>) -> bool {
128    let Ok(msg) = serde_json::from_str::<MaelstromMessage<serde_json::Value>>(line) else {
129        return false;
130    };
131    node_ids.contains(&msg.src) || msg.src.starts_with('c')
132}
133
134/// Initialize a Maelstrom node by reading the init message from stdin.
135/// Returns the node metadata and sends `init_ok` response.
136/// Also spawns a background thread to read stdin and broadcast lines to subscribers.
137pub fn maelstrom_init() -> MaelstromMeta {
138    let stdin = std::io::stdin();
139    let mut stdout = std::io::stdout();
140
141    // Read until the init message arrives. Maelstrom delivers node-to-node
142    // messages to a node's stdin as soon as the process starts, so a peer
143    // may already be sending `hydro_data` messages before the init RPC for
144    // this node is sent (e.g. after this node was killed and restarted by a
145    // nemesis) — the first line on stdin is not necessarily `init`. Such
146    // pre-init messages are dropped: inter-node channels in the Maelstrom
147    // backend are lossy, so dropped messages are handled the same as network
148    // loss.
149    let msg: MaelstromMessage<InitBody> = loop {
150        let mut line = String::new();
151        let bytes_read = stdin
152            .lock()
153            .read_line(&mut line)
154            .expect("Failed to read init message");
155
156        if bytes_read == 0 {
157            // stdin reached EOF before an init message arrived. This happens when
158            // Maelstrom tears the node down (e.g. after an init timeout) or during
159            // a warmup invocation with no input. Exit cleanly instead of panicking
160            // so the node is not reported as "crashed", which would mask the real
161            // error (such as the init timeout).
162            eprintln!("stdin closed before an init message was received; exiting");
163            std::process::exit(0);
164        }
165
166        let parsed: MaelstromMessage<serde_json::Value> =
167            serde_json::from_str(&line).expect("Failed to parse message while waiting for init");
168
169        if parsed.body.get("type").and_then(|t| t.as_str()) == Some("init") {
170            let body: InitBody =
171                serde_json::from_value(parsed.body).expect("Failed to parse init message");
172            break MaelstromMessage {
173                src: parsed.src,
174                dest: parsed.dest,
175                body,
176            };
177        }
178        eprintln!(
179            "dropping message received before init (lossy channel): {}",
180            line.trim_end()
181        );
182    };
183
184    // Set up broadcast channel for stdin lines
185    let (stdin_tx, _) = tokio::sync::broadcast::channel::<String>(1024);
186
187    let meta = MaelstromMeta {
188        node_id: msg.body.node_id.clone(),
189        node_ids: msg.body.node_ids.clone(),
190        stdin_tx,
191        peers_ready: Arc::new(AtomicBool::new(false)),
192    };
193
194    // Send init_ok response
195    let response = MaelstromMessage {
196        src: msg.body.node_id,
197        dest: msg.src,
198        body: InitOkBody {
199            msg_type: "init_ok".to_owned(),
200            in_reply_to: msg.body.msg_id,
201        },
202    };
203
204    let response_json = serde_json::to_string(&response).expect("Failed to serialize init_ok");
205    writeln!(stdout, "{}", response_json).expect("Failed to write init_ok");
206    stdout.flush().expect("Failed to flush stdout");
207
208    meta
209}
210
211/// Get the cluster member IDs from the Maelstrom metadata.
212/// The `meta` parameter is a `RuntimeData` reference to the `MaelstromMeta` that will be
213/// available at runtime as `__hydro_lang_maelstrom_meta`.
214pub(super) fn cluster_members<'a>(
215    meta: RuntimeData<&'a MaelstromMeta>,
216    _of_cluster: LocationKey,
217) -> impl QuotedWithContext<'a, &'a [TaglessMemberId], ()> + Clone + 'a {
218    q!({
219        let members: &'static [TaglessMemberId] = Box::leak(
220            meta.node_ids
221                .iter()
222                .map(|id| TaglessMemberId::from_maelstrom_node_id(id.clone()))
223                .collect::<Vec<TaglessMemberId>>()
224                .into_boxed_slice(),
225        );
226        members
227    })
228}
229
230/// Get the self ID for this cluster member.
231pub(super) fn cluster_self_id<'a>(
232    meta: RuntimeData<&'a MaelstromMeta>,
233) -> impl QuotedWithContext<'a, TaglessMemberId, ()> + Clone + 'a {
234    q!(TaglessMemberId::from_maelstrom_node_id(
235        meta.node_id.clone()
236    ))
237}
238
239/// Get the cluster membership stream (static for Maelstrom - all members join at start).
240/// This references `__hydro_lang_maelstrom_meta` which will be in scope at runtime.
241pub(super) fn cluster_membership_stream<'a>(
242    _location_id: &LocationId,
243) -> impl QuotedWithContext<'a, Box<dyn Stream<Item = (TaglessMemberId, MembershipEvent)> + Unpin>, ()>
244{
245    let meta: RuntimeData<&MaelstromMeta> = RuntimeData::new("__hydro_lang_maelstrom_meta");
246    q!(Box::new(futures::stream::iter(
247        meta.node_ids
248            .iter()
249            .map(|id| (
250                TaglessMemberId::from_maelstrom_node_id(id.clone()),
251                MembershipEvent::Joined
252            ))
253            .collect::<Vec<_>>()
254    ))
255        as Box<
256            dyn futures::Stream<Item = (TaglessMemberId, MembershipEvent)> + Unpin,
257        >)
258}
259
260/// Create sink and source for m2m (cluster member to cluster member) communication.
261/// Messages are routed through Maelstrom's network via stdin/stdout.
262pub(super) fn deploy_maelstrom_m2m(meta: RuntimeData<&MaelstromMeta>) -> (syn::Expr, syn::Expr) {
263    // Sink: serialize and write to stdout with Maelstrom message envelope.
264    // Messages are dropped until the node has been activated by an incoming
265    // client or peer message; see `MaelstromMeta::peers_ready`.
266    let sink_expr = q!({
267        let node_id = meta.node_id.clone();
268        let peers_ready = meta.peers_ready();
269        sinktools::map(
270            move |(dest_id, payload): (TaglessMemberId, bytes::Bytes)| {
271                let msg = serde_json::json!({
272                    "src": node_id,
273                    "dest": dest_id.get_maelstrom_node_id(),
274                    "body": {
275                        "type": "hydro_data",
276                        "data": payload.to_vec()
277                    }
278                });
279                let mut line = serde_json::to_string(&msg).unwrap();
280                line.push('\n');
281                line
282            },
283            futures::sink::unfold(
284                peers_ready,
285                |peers_ready: std::sync::Arc<std::sync::atomic::AtomicBool>, line: String| {
286                    Box::pin(async move {
287                        if peers_ready.load(std::sync::atomic::Ordering::Acquire) {
288                            print!("{}", line);
289                            std::io::stdout().flush().ok();
290                        } else {
291                            eprintln!(
292                                "dropping message sent before peers are known to be up (lossy channel): {}",
293                                line.trim_end()
294                            );
295                        }
296                        Ok::<_, std::io::Error>(peers_ready)
297                    })
298                },
299            ),
300        )
301    })
302    .splice_untyped_ctx(&());
303
304    // Source: subscribe to the shared stdin broadcast stream
305    let source_expr = q!({
306        let node_ids: std::collections::HashSet<String> = meta.node_ids.iter().cloned().collect();
307        let lines = meta.subscribe_stdin();
308        futures::StreamExt::filter_map(lines, move |line_result| {
309            let node_ids = node_ids.clone();
310            Box::pin(async move {
311                let line = line_result.ok()?;
312                let mut msg =
313                    serde_json::from_str::<MaelstromMessage<serde_json::Value>>(&line).ok()?;
314                // Only process messages from other nodes (not clients)
315                if msg
316                    .body
317                    .get("type")
318                    .is_some_and(|t| t.as_str() == Some("hydro_data"))
319                {
320                    let deser: Vec<u8> =
321                        serde_json::from_value(msg.body.get_mut("data").unwrap().take()).unwrap();
322                    Some(Ok::<_, std::io::Error>((
323                        TaglessMemberId::from_maelstrom_node_id(msg.src),
324                        bytes::BytesMut::from(&deser[..]),
325                    )))
326                } else {
327                    None
328                }
329            })
330        })
331    })
332    .splice_untyped_ctx(&());
333
334    (sink_expr, source_expr)
335}
336
337/// Creates a stream of client messages from Maelstrom stdin.
338/// Returns tuples of `(client_id, message_body)` where `client_id` is the source client
339/// and `message_body` is the JSON value of the message body.
340///
341/// This function is meant to be used with `source_stream` on a Cluster location.
342pub fn maelstrom_client_source(
343    meta: &MaelstromMeta,
344) -> impl Stream<Item = (String, serde_json::Value)> + Unpin {
345    use std::collections::HashSet;
346
347    let node_ids: HashSet<String> = meta.node_ids.iter().cloned().collect();
348    let lines = meta.subscribe_stdin();
349
350    Box::pin(lines.filter_map(move |line_result| {
351        let node_ids = node_ids.clone();
352        async move {
353            let line = line_result.ok()?;
354            let msg: MaelstromMessage<serde_json::Value> = serde_json::from_str(&line).ok()?;
355            // Only process messages from clients (not other nodes)
356            if !node_ids.contains(&msg.src) {
357                Some((msg.src, msg.body))
358            } else {
359                None
360            }
361        }
362    }))
363}
364
365/// Sends a response to a Maelstrom client via stdout.
366///
367/// This function is meant to be used with `for_each` on a stream of responses.
368pub fn maelstrom_send_response(node_id: &str, client_id: &str, body: serde_json::Value) {
369    use std::io::Write;
370
371    let msg = MaelstromMessage {
372        src: node_id.to_owned(),
373        dest: client_id.to_owned(),
374        body,
375    };
376
377    let json = serde_json::to_string(&msg).expect("Failed to serialize response");
378    println!("{}", json);
379    std::io::stdout().flush().ok();
380}