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};
13
14use futures::{Stream, StreamExt};
15use serde::{Deserialize, Serialize};
16use stageleft::{QuotedWithContext, RuntimeData, q};
17
18use crate::forward_handle::ForwardHandle;
19use crate::live_collections::boundedness::Unbounded;
20use crate::live_collections::keyed_stream::KeyedStream;
21use crate::live_collections::stream::{ExactlyOnce, NoOrder, TotalOrder};
22use crate::location::dynamic::LocationId;
23use crate::location::member_id::TaglessMemberId;
24use crate::location::{Cluster, LocationKey, MembershipEvent};
25use crate::nondet::nondet;
26
27/// Maelstrom message envelope structure.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct MaelstromMessage<T> {
30    pub src: String,
31    pub dest: String,
32    pub body: T,
33}
34
35/// Maelstrom init message body.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct InitBody {
38    #[serde(rename = "type")]
39    pub msg_type: String,
40    pub msg_id: u64,
41    pub node_id: String,
42    pub node_ids: Vec<String>,
43}
44
45/// Maelstrom `init_ok` response body.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct InitOkBody {
48    #[serde(rename = "type")]
49    pub msg_type: String,
50    pub in_reply_to: u64,
51}
52
53/// Metadata for a Maelstrom node, populated from the init message.
54/// Also manages a shared stdin reader that broadcasts lines to multiple subscribers.
55pub struct MaelstromMeta {
56    pub node_id: String,
57    pub node_ids: Vec<String>,
58    stdin_tx: tokio::sync::broadcast::Sender<String>,
59}
60
61impl MaelstromMeta {
62    /// Subscribe to stdin lines. Each subscriber receives all lines read from stdin.
63    /// Multiple subscribers can be created and each will receive a copy of every line.
64    pub fn subscribe_stdin(&self) -> tokio_stream::wrappers::BroadcastStream<String> {
65        tokio_stream::wrappers::BroadcastStream::new(self.stdin_tx.subscribe())
66    }
67
68    /// Start receiving incoming messages from clients and other nodes, after launching the DFIR.
69    pub fn start_receiving(&self) {
70        let tx_clone = self.stdin_tx.clone();
71
72        // Spawn thread to read stdin and broadcast lines
73        std::thread::spawn(move || {
74            let stdin = std::io::stdin();
75            for line in stdin.lock().lines() {
76                match line {
77                    Ok(l) => {
78                        // Ignore send errors (no receivers)
79                        let _ = tx_clone.send(l);
80                    }
81                    Err(_) => break,
82                }
83            }
84        });
85    }
86}
87
88/// Initialize a Maelstrom node by reading the init message from stdin.
89/// Returns the node metadata and sends `init_ok` response.
90/// Also spawns a background thread to read stdin and broadcast lines to subscribers.
91pub fn maelstrom_init() -> MaelstromMeta {
92    let stdin = std::io::stdin();
93    let mut stdout = std::io::stdout();
94
95    // Read until the init message arrives. Maelstrom delivers node-to-node
96    // messages to a node's stdin as soon as the process starts, so a
97    // fast-starting peer may already be broadcasting `hydro_data` messages
98    // before the init RPC for this node is sent — the first line on stdin is
99    // not necessarily `init`. Such pre-init messages are dropped: inter-node
100    // channels in the Maelstrom backend are lossy, so dropped messages are
101    // handled the same as network loss.
102    let msg: MaelstromMessage<InitBody> = loop {
103        let mut line = String::new();
104        let bytes_read = stdin
105            .lock()
106            .read_line(&mut line)
107            .expect("Failed to read init message");
108
109        if bytes_read == 0 {
110            // stdin reached EOF before an init message arrived. This happens when
111            // Maelstrom tears the node down (e.g. after an init timeout) or during
112            // a warmup invocation with no input. Exit cleanly instead of panicking
113            // so the node is not reported as "crashed", which would mask the real
114            // error (such as the init timeout).
115            eprintln!("stdin closed before an init message was received; exiting");
116            std::process::exit(0);
117        }
118
119        let parsed: MaelstromMessage<serde_json::Value> =
120            serde_json::from_str(&line).expect("Failed to parse message while waiting for init");
121
122        if parsed.body.get("type").and_then(|t| t.as_str()) == Some("init") {
123            let body: InitBody =
124                serde_json::from_value(parsed.body).expect("Failed to parse init message");
125            break MaelstromMessage {
126                src: parsed.src,
127                dest: parsed.dest,
128                body,
129            };
130        }
131        eprintln!(
132            "dropping message received before init (lossy channel): {}",
133            line.trim_end()
134        );
135    };
136
137    // Set up broadcast channel for stdin lines
138    let (stdin_tx, _) = tokio::sync::broadcast::channel::<String>(1024);
139
140    let meta = MaelstromMeta {
141        node_id: msg.body.node_id.clone(),
142        node_ids: msg.body.node_ids.clone(),
143        stdin_tx,
144    };
145
146    // Send init_ok response
147    let response = MaelstromMessage {
148        src: msg.body.node_id,
149        dest: msg.src,
150        body: InitOkBody {
151            msg_type: "init_ok".to_owned(),
152            in_reply_to: msg.body.msg_id,
153        },
154    };
155
156    let response_json = serde_json::to_string(&response).expect("Failed to serialize init_ok");
157    writeln!(stdout, "{}", response_json).expect("Failed to write init_ok");
158    stdout.flush().expect("Failed to flush stdout");
159
160    meta
161}
162
163/// Get the cluster member IDs from the Maelstrom metadata.
164/// The `meta` parameter is a `RuntimeData` reference to the `MaelstromMeta` that will be
165/// available at runtime as `__hydro_lang_maelstrom_meta`.
166pub(super) fn cluster_members<'a>(
167    meta: RuntimeData<&'a MaelstromMeta>,
168    _of_cluster: LocationKey,
169) -> impl QuotedWithContext<'a, &'a [TaglessMemberId], ()> + Clone + 'a {
170    q!({
171        let members: &'static [TaglessMemberId] = Box::leak(
172            meta.node_ids
173                .iter()
174                .map(|id| TaglessMemberId::from_maelstrom_node_id(id.clone()))
175                .collect::<Vec<TaglessMemberId>>()
176                .into_boxed_slice(),
177        );
178        members
179    })
180}
181
182/// Get the self ID for this cluster member.
183pub(super) fn cluster_self_id<'a>(
184    meta: RuntimeData<&'a MaelstromMeta>,
185) -> impl QuotedWithContext<'a, TaglessMemberId, ()> + Clone + 'a {
186    q!(TaglessMemberId::from_maelstrom_node_id(
187        meta.node_id.clone()
188    ))
189}
190
191/// Get the cluster membership stream (static for Maelstrom - all members join at start).
192/// This references `__hydro_lang_maelstrom_meta` which will be in scope at runtime.
193pub(super) fn cluster_membership_stream<'a>(
194    _location_id: &LocationId,
195) -> impl QuotedWithContext<'a, Box<dyn Stream<Item = (TaglessMemberId, MembershipEvent)> + Unpin>, ()>
196{
197    let meta: RuntimeData<&MaelstromMeta> = RuntimeData::new("__hydro_lang_maelstrom_meta");
198    q!(Box::new(futures::stream::iter(
199        meta.node_ids
200            .iter()
201            .map(|id| (
202                TaglessMemberId::from_maelstrom_node_id(id.clone()),
203                MembershipEvent::Joined
204            ))
205            .collect::<Vec<_>>()
206    ))
207        as Box<
208            dyn futures::Stream<Item = (TaglessMemberId, MembershipEvent)> + Unpin,
209        >)
210}
211
212/// Create sink and source for m2m (cluster member to cluster member) communication.
213/// Messages are routed through Maelstrom's network via stdin/stdout.
214pub(super) fn deploy_maelstrom_m2m(meta: RuntimeData<&MaelstromMeta>) -> (syn::Expr, syn::Expr) {
215    // Sink: serialize and write to stdout with Maelstrom message envelope
216    let sink_expr = q!({
217        let node_id = meta.node_id.clone();
218        sinktools::map(
219            move |(dest_id, payload): (TaglessMemberId, bytes::Bytes)| {
220                let msg = serde_json::json!({
221                    "src": node_id,
222                    "dest": dest_id.get_maelstrom_node_id(),
223                    "body": {
224                        "type": "hydro_data",
225                        "data": payload.to_vec()
226                    }
227                });
228                let mut line = serde_json::to_string(&msg).unwrap();
229                line.push('\n');
230                line
231            },
232            futures::sink::unfold((), |(), line: String| {
233                Box::pin(async move {
234                    print!("{}", line);
235                    std::io::stdout().flush().ok();
236                    Ok::<_, std::io::Error>(())
237                })
238            }),
239        )
240    })
241    .splice_untyped_ctx(&());
242
243    // Source: subscribe to the shared stdin broadcast stream
244    let source_expr = q!({
245        let node_ids: std::collections::HashSet<String> = meta.node_ids.iter().cloned().collect();
246        let lines = meta.subscribe_stdin();
247        futures::StreamExt::filter_map(lines, move |line_result| {
248            let node_ids = node_ids.clone();
249            Box::pin(async move {
250                let line = line_result.ok()?;
251                let mut msg =
252                    serde_json::from_str::<MaelstromMessage<serde_json::Value>>(&line).ok()?;
253                // Only process messages from other nodes (not clients)
254                if msg
255                    .body
256                    .get("type")
257                    .is_some_and(|t| t.as_str() == Some("hydro_data"))
258                {
259                    let deser: Vec<u8> =
260                        serde_json::from_value(msg.body.get_mut("data").unwrap().take()).unwrap();
261                    Some(Ok::<_, std::io::Error>((
262                        TaglessMemberId::from_maelstrom_node_id(msg.src),
263                        bytes::BytesMut::from(&deser[..]),
264                    )))
265                } else {
266                    None
267                }
268            })
269        })
270    })
271    .splice_untyped_ctx(&());
272
273    (sink_expr, source_expr)
274}
275
276/// Creates a stream of client messages from Maelstrom stdin.
277/// Returns tuples of `(client_id, message_body)` where `client_id` is the source client
278/// and `message_body` is the JSON value of the message body.
279///
280/// This function is meant to be used with `source_stream` on a Cluster location.
281pub fn maelstrom_client_source(
282    meta: &MaelstromMeta,
283) -> impl Stream<Item = (String, serde_json::Value)> + Unpin {
284    use std::collections::HashSet;
285
286    let node_ids: HashSet<String> = meta.node_ids.iter().cloned().collect();
287    let lines = meta.subscribe_stdin();
288
289    Box::pin(lines.filter_map(move |line_result| {
290        let node_ids = node_ids.clone();
291        async move {
292            let line = line_result.ok()?;
293            let msg: MaelstromMessage<serde_json::Value> = serde_json::from_str(&line).ok()?;
294            // Only process messages from clients (not other nodes)
295            if !node_ids.contains(&msg.src) {
296                Some((msg.src, msg.body))
297            } else {
298                None
299            }
300        }
301    }))
302}
303
304/// Sends a response to a Maelstrom client via stdout.
305///
306/// This function is meant to be used with `for_each` on a stream of responses.
307pub fn maelstrom_send_response(node_id: &str, client_id: &str, body: serde_json::Value) {
308    use std::io::Write;
309
310    let msg = MaelstromMessage {
311        src: node_id.to_owned(),
312        dest: client_id.to_owned(),
313        body,
314    };
315
316    let json = serde_json::to_string(&msg).expect("Failed to serialize response");
317    println!("{}", json);
318    std::io::stdout().flush().ok();
319}