1use std::cell::RefCell;
8use std::future::Future;
9use std::io::{BufRead, BufReader, Error};
10use std::path::{Path, PathBuf};
11use std::pin::Pin;
12use std::process::Stdio;
13use std::rc::Rc;
14
15use bytes::{Bytes, BytesMut};
16use dfir_lang::graph::{AsCodeOptions, DfirGraph};
17use futures::{Sink, Stream};
18use serde::Serialize;
19use serde::de::DeserializeOwned;
20use stageleft::{QuotedWithContext, RuntimeData};
21
22use super::deploy_runtime_maelstrom::*;
23use crate::compile::builder::ExternalPortId;
24use crate::compile::deploy_provider::{ClusterSpec, Deploy, Node, RegisterPort};
25use crate::compile::trybuild::generate::{
26 ExampleBuildConfig, LinkingMode, TrybuildConfig, compile_trybuild_example,
27 create_graph_trybuild,
28};
29use crate::location::dynamic::LocationId;
30use crate::location::{LocationKey, NetworkHint};
31
32pub enum MaelstromDeploy {}
39
40impl<'a> Deploy<'a> for MaelstromDeploy {
41 type Meta = ();
42 type InstantiateEnv = MaelstromDeployment;
43
44 type Process = MaelstromProcess;
45 type Cluster = MaelstromCluster;
46 type External = MaelstromExternal;
47
48 fn o2o_sink_source(
49 _env: &mut Self::InstantiateEnv,
50 _p1: &Self::Process,
51 _p1_port: &<Self::Process as Node>::Port,
52 _p2: &Self::Process,
53 _p2_port: &<Self::Process as Node>::Port,
54 _name: Option<&str>,
55 _networking_info: &crate::networking::NetworkingInfo,
56 _external_types: Option<(&syn::Type, &syn::Type)>,
57 ) -> (syn::Expr, syn::Expr) {
58 panic!("Maelstrom deployment does not support processes, only clusters")
59 }
60
61 fn o2o_connect(
62 _p1: &Self::Process,
63 _p1_port: &<Self::Process as Node>::Port,
64 _p2: &Self::Process,
65 _p2_port: &<Self::Process as Node>::Port,
66 ) -> Box<dyn FnOnce()> {
67 panic!("Maelstrom deployment does not support processes, only clusters")
68 }
69
70 fn o2m_sink_source(
71 _env: &mut Self::InstantiateEnv,
72 _p1: &Self::Process,
73 _p1_port: &<Self::Process as Node>::Port,
74 _c2: &Self::Cluster,
75 _c2_port: &<Self::Cluster as Node>::Port,
76 _name: Option<&str>,
77 _networking_info: &crate::networking::NetworkingInfo,
78 _external_types: Option<(&syn::Type, &syn::Type)>,
79 ) -> (syn::Expr, syn::Expr) {
80 panic!("Maelstrom deployment does not support processes, only clusters")
81 }
82
83 fn o2m_connect(
84 _p1: &Self::Process,
85 _p1_port: &<Self::Process as Node>::Port,
86 _c2: &Self::Cluster,
87 _c2_port: &<Self::Cluster as Node>::Port,
88 ) -> Box<dyn FnOnce()> {
89 panic!("Maelstrom deployment does not support processes, only clusters")
90 }
91
92 fn m2o_sink_source(
93 _env: &mut Self::InstantiateEnv,
94 _c1: &Self::Cluster,
95 _c1_port: &<Self::Cluster as Node>::Port,
96 _p2: &Self::Process,
97 _p2_port: &<Self::Process as Node>::Port,
98 _name: Option<&str>,
99 _networking_info: &crate::networking::NetworkingInfo,
100 _external_types: Option<(&syn::Type, &syn::Type)>,
101 ) -> (syn::Expr, syn::Expr) {
102 panic!("Maelstrom deployment does not support processes, only clusters")
103 }
104
105 fn m2o_connect(
106 _c1: &Self::Cluster,
107 _c1_port: &<Self::Cluster as Node>::Port,
108 _p2: &Self::Process,
109 _p2_port: &<Self::Process as Node>::Port,
110 ) -> Box<dyn FnOnce()> {
111 panic!("Maelstrom deployment does not support processes, only clusters")
112 }
113
114 fn m2m_sink_source(
115 env: &mut Self::InstantiateEnv,
116 _c1: &Self::Cluster,
117 _c1_port: &<Self::Cluster as Node>::Port,
118 _c2: &Self::Cluster,
119 _c2_port: &<Self::Cluster as Node>::Port,
120 _name: Option<&str>,
121 networking_info: &crate::networking::NetworkingInfo,
122 _external_types: Option<(&syn::Type, &syn::Type)>,
123 ) -> (syn::Expr, syn::Expr) {
124 use crate::networking::{NetworkingInfo, TcpFault};
125 match networking_info {
126 NetworkingInfo::Tcp { fault } => match (fault, env.nemesis.as_deref()) {
127 (TcpFault::Lossy | TcpFault::LossyDelayedForever, _) => {} (_, None) => {} (TcpFault::FailStop, Some("partition")) => {
130 panic!(
131 "Maelstrom partition nemesis requires lossy networking, but fail_stop was used. \
132 Use `TCP.lossy().bincode()` or `TCP.lossy_delayed_forever().bincode()` instead of `TCP.fail_stop().bincode()`."
133 );
134 }
135 (TcpFault::FailStop, Some(_)) => {} },
137 NetworkingInfo::Udp { .. } => {} NetworkingInfo::Embedded { .. } => {
139 unreachable!("`EMBEDDED` channels are only supported by the embedded backend")
140 }
141 }
142 deploy_maelstrom_m2m(RuntimeData::new("__hydro_lang_maelstrom_meta"))
143 }
144
145 fn m2m_connect(
146 _c1: &Self::Cluster,
147 _c1_port: &<Self::Cluster as Node>::Port,
148 _c2: &Self::Cluster,
149 _c2_port: &<Self::Cluster as Node>::Port,
150 ) -> Box<dyn FnOnce()> {
151 Box::new(|| {})
153 }
154
155 fn e2o_many_source(
156 _extra_stmts: &mut Vec<syn::Stmt>,
157 _p2: &Self::Process,
158 _p2_port: &<Self::Process as Node>::Port,
159 _codec_type: &syn::Type,
160 _shared_handle: String,
161 ) -> syn::Expr {
162 panic!("Maelstrom deployment does not support processes, only clusters")
163 }
164
165 fn e2o_many_sink(_shared_handle: String) -> syn::Expr {
166 panic!("Maelstrom deployment does not support processes, only clusters")
167 }
168
169 fn e2o_source(
170 _extra_stmts: &mut Vec<syn::Stmt>,
171 _p1: &Self::External,
172 _p1_port: &<Self::External as Node>::Port,
173 _p2: &Self::Process,
174 _p2_port: &<Self::Process as Node>::Port,
175 _codec_type: &syn::Type,
176 _shared_handle: String,
177 ) -> syn::Expr {
178 panic!("Maelstrom deployment does not support processes, only clusters")
179 }
180
181 fn e2o_connect(
182 _p1: &Self::External,
183 _p1_port: &<Self::External as Node>::Port,
184 _p2: &Self::Process,
185 _p2_port: &<Self::Process as Node>::Port,
186 _many: bool,
187 _server_hint: NetworkHint,
188 ) -> Box<dyn FnOnce()> {
189 panic!("Maelstrom deployment does not support processes, only clusters")
190 }
191
192 fn o2e_sink(
193 _p1: &Self::Process,
194 _p1_port: &<Self::Process as Node>::Port,
195 _p2: &Self::External,
196 _p2_port: &<Self::External as Node>::Port,
197 _shared_handle: String,
198 ) -> syn::Expr {
199 panic!("Maelstrom deployment does not support processes, only clusters")
200 }
201
202 fn cluster_ids(_of_cluster: LocationKey) -> syn::Expr {
203 cluster_members(RuntimeData::new("__hydro_lang_maelstrom_meta"), _of_cluster)
204 .splice_untyped()
205 }
206
207 fn cluster_self_id() -> syn::Expr {
208 cluster_self_id(RuntimeData::new("__hydro_lang_maelstrom_meta")).splice_untyped()
209 }
210
211 fn cluster_membership_stream(
212 _env: &mut Self::InstantiateEnv,
213 _at_location: &LocationId,
214 location_id: &LocationId,
215 ) -> syn::Expr {
216 cluster_membership_stream(location_id).splice_untyped()
217 }
218}
219
220#[derive(Clone)]
222pub struct MaelstromProcess {
223 _private: (),
224}
225
226impl Node for MaelstromProcess {
227 type Port = String;
228 type Meta = ();
229 type InstantiateEnv = MaelstromDeployment;
230
231 fn next_port(&self) -> Self::Port {
232 panic!("Maelstrom deployment does not support processes")
233 }
234
235 fn update_meta(&self, _meta: &Self::Meta) {}
236
237 fn instantiate(
238 &self,
239 _env: &mut Self::InstantiateEnv,
240 _meta: &mut Self::Meta,
241 _graph: DfirGraph,
242 _extra_stmts: &[syn::Stmt],
243 _sidecars: &[syn::Expr],
244 _as_code_options: &AsCodeOptions,
245 ) {
246 panic!("Maelstrom deployment does not support processes")
247 }
248}
249
250#[derive(Clone)]
252pub struct MaelstromCluster {
253 next_port: Rc<RefCell<usize>>,
254 name_hint: Option<String>,
255}
256
257impl Node for MaelstromCluster {
258 type Port = String;
259 type Meta = ();
260 type InstantiateEnv = MaelstromDeployment;
261
262 fn next_port(&self) -> Self::Port {
263 let next_port = *self.next_port.borrow();
264 *self.next_port.borrow_mut() += 1;
265 format!("port_{}", next_port)
266 }
267
268 fn update_meta(&self, _meta: &Self::Meta) {}
269
270 fn instantiate(
271 &self,
272 env: &mut Self::InstantiateEnv,
273 _meta: &mut Self::Meta,
274 graph: DfirGraph,
275 extra_stmts: &[syn::Stmt],
276 sidecars: &[syn::Expr],
277 as_code_options: &AsCodeOptions,
278 ) {
279 let (bin_name, config) = create_graph_trybuild(
280 graph,
281 extra_stmts,
282 sidecars,
283 as_code_options,
284 self.name_hint.as_deref(),
285 crate::compile::trybuild::generate::DeployMode::Maelstrom,
286 LinkingMode::Dynamic,
287 );
288
289 env.bin_name = Some(bin_name);
290 env.trybuild = Some(config);
291 }
292}
293
294#[derive(Clone)]
296pub enum MaelstromExternal {}
297
298impl Node for MaelstromExternal {
299 type Port = String;
300 type Meta = ();
301 type InstantiateEnv = MaelstromDeployment;
302
303 fn next_port(&self) -> Self::Port {
304 unreachable!()
305 }
306
307 fn update_meta(&self, _meta: &Self::Meta) {}
308
309 fn instantiate(
310 &self,
311 _env: &mut Self::InstantiateEnv,
312 _meta: &mut Self::Meta,
313 _graph: DfirGraph,
314 _extra_stmts: &[syn::Stmt],
315 _sidecars: &[syn::Expr],
316 _as_code_options: &AsCodeOptions,
317 ) {
318 unreachable!()
319 }
320}
321
322impl<'a> RegisterPort<'a, MaelstromDeploy> for MaelstromExternal {
323 fn register(&self, _external_port_id: ExternalPortId, _port: Self::Port) {
324 unreachable!()
325 }
326
327 #[expect(clippy::manual_async_fn, reason = "false positive, involves lifetimes")]
328 fn as_bytes_bidi(
329 &self,
330 _external_port_id: ExternalPortId,
331 ) -> impl Future<
332 Output = (
333 Pin<Box<dyn Stream<Item = Result<BytesMut, Error>>>>,
334 Pin<Box<dyn Sink<Bytes, Error = Error>>>,
335 ),
336 > + 'a {
337 async move { unreachable!() }
338 }
339
340 #[expect(clippy::manual_async_fn, reason = "false positive, involves lifetimes")]
341 fn as_bincode_bidi<InT, OutT>(
342 &self,
343 _external_port_id: ExternalPortId,
344 ) -> impl Future<
345 Output = (
346 Pin<Box<dyn Stream<Item = OutT>>>,
347 Pin<Box<dyn Sink<InT, Error = Error>>>,
348 ),
349 > + 'a
350 where
351 InT: Serialize + 'static,
352 OutT: DeserializeOwned + 'static,
353 {
354 async move { unreachable!() }
355 }
356
357 #[expect(clippy::manual_async_fn, reason = "false positive, involves lifetimes")]
358 fn as_bincode_sink<T: Serialize + 'static>(
359 &self,
360 _external_port_id: ExternalPortId,
361 ) -> impl Future<Output = Pin<Box<dyn Sink<T, Error = Error>>>> + 'a {
362 async move { unreachable!() }
363 }
364
365 #[expect(clippy::manual_async_fn, reason = "false positive, involves lifetimes")]
366 fn as_bincode_source<T: DeserializeOwned + 'static>(
367 &self,
368 _external_port_id: ExternalPortId,
369 ) -> impl Future<Output = Pin<Box<dyn Stream<Item = T>>>> + 'a {
370 async move { unreachable!() }
371 }
372}
373
374#[derive(Clone)]
376pub struct MaelstromClusterSpec;
377
378impl<'a> ClusterSpec<'a, MaelstromDeploy> for MaelstromClusterSpec {
379 fn build(self, key: LocationKey, name_hint: &str) -> MaelstromCluster {
380 assert_eq!(
381 key,
382 LocationKey::FIRST,
383 "there should only be one location for a Maelstrom deployment"
384 );
385 MaelstromCluster {
386 next_port: Rc::new(RefCell::new(0)),
387 name_hint: Some(name_hint.to_owned()),
388 }
389 }
390}
391
392pub struct MaelstromDeployment {
397 pub node_count: usize,
399 pub maelstrom_path: PathBuf,
401 pub workload: String,
403 pub time_limit: Option<u64>,
405 pub rate: Option<u64>,
407 pub availability: Option<String>,
409 pub nemesis: Option<String>,
411 pub extra_args: Vec<String>,
413
414 pub(crate) bin_name: Option<String>,
416 pub(crate) trybuild: Option<TrybuildConfig>,
417}
418
419impl MaelstromDeployment {
420 pub fn new(workload: impl Into<String>) -> Self {
422 Self {
423 node_count: 1,
424 maelstrom_path: PathBuf::from("maelstrom"),
425 workload: workload.into(),
426 time_limit: None,
427 rate: None,
428 availability: None,
429 nemesis: None,
430 extra_args: vec![],
431 bin_name: None,
432 trybuild: None,
433 }
434 }
435
436 #[must_use]
438 pub fn node_count(mut self, count: usize) -> Self {
439 self.node_count = count;
440 self
441 }
442
443 pub fn maelstrom_path(mut self, path: impl Into<PathBuf>) -> Self {
445 self.maelstrom_path = path.into();
446 self
447 }
448
449 #[must_use]
451 pub fn time_limit(mut self, seconds: u64) -> Self {
452 self.time_limit = Some(seconds);
453 self
454 }
455
456 #[must_use]
458 pub fn rate(mut self, rate: u64) -> Self {
459 self.rate = Some(rate);
460 self
461 }
462
463 pub fn availability(mut self, availability: impl Into<String>) -> Self {
465 self.availability = Some(availability.into());
466 self
467 }
468
469 pub fn nemesis(mut self, nemesis: impl Into<String>) -> Self {
471 self.nemesis = Some(nemesis.into());
472 self
473 }
474
475 pub fn extra_args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
477 self.extra_args.extend(args.into_iter().map(Into::into));
478 self
479 }
480
481 pub fn build(&self) -> Result<PathBuf, Error> {
489 let bin_name = self
490 .bin_name
491 .as_ref()
492 .expect("No binary name set - did you call deploy?");
493 let trybuild = self
494 .trybuild
495 .as_ref()
496 .expect("No trybuild config set - did you call deploy?");
497
498 compile_trybuild_example(ExampleBuildConfig {
499 trybuild: trybuild.clone(),
500 bin_name: bin_name.clone(),
501 runtime_feature: "hydro___feature_maelstrom_runtime",
502 example_name: bin_name.clone(),
504 crate_type: None,
505 set_trybuild_lib_name: false,
506 })
507 .map_err(|()| Error::other("Maelstrom binary compilation failed"))
508 }
509
510 pub fn run(self) -> Result<(), Error> {
514 let binary_path = self.build()?;
515
516 std::process::Command::new(&binary_path)
525 .stdin(Stdio::null())
526 .stdout(Stdio::null())
527 .stderr(Stdio::null())
528 .spawn()?
529 .wait()?;
530
531 let run_dir = tempfile::tempdir().map_err(Error::other)?;
533
534 let mut cmd = std::process::Command::new(&self.maelstrom_path);
535 cmd.arg("test")
536 .arg("-w")
537 .arg(&self.workload)
538 .arg("--bin")
539 .arg(&binary_path)
540 .arg("--node-count")
541 .arg(self.node_count.to_string())
542 .current_dir(run_dir.path())
543 .stdout(Stdio::piped());
544
545 if let Some(time_limit) = self.time_limit {
546 cmd.arg("--time-limit").arg(time_limit.to_string());
547 }
548
549 if let Some(rate) = self.rate {
550 cmd.arg("--rate").arg(rate.to_string());
551 }
552
553 if let Some(availability) = self.availability {
554 cmd.arg("--availability").arg(availability);
555 }
556
557 if let Some(nemesis) = self.nemesis {
558 cmd.arg("--nemesis").arg(nemesis);
559 }
560
561 for arg in &self.extra_args {
562 cmd.arg(arg);
563 }
564
565 let spawned = cmd.spawn()?;
566
567 for line in BufReader::new(spawned.stdout.unwrap()).lines() {
568 let line = line?;
569 eprintln!("{}", line);
570
571 if line.starts_with("Analysis invalid!") {
572 let path = run_dir.keep();
573 dump_node_logs(&path);
574 return Err(Error::other(format!(
575 "Analysis was invalid. Maelstrom store at: {}",
576 path.display()
577 )));
578 } else if line.starts_with("Errors occurred during analysis, but no anomalies found.")
579 || line.starts_with("Everything looks good!")
580 {
581 return Ok(());
582 }
583 }
584
585 let path = run_dir.keep();
586 dump_node_logs(&path);
587 Err(Error::other(format!(
588 "Maelstrom produced an unexpected result. Store at: {}",
589 path.display()
590 )))
591 }
592
593 pub fn binary_path(&self) -> Option<PathBuf> {
595 self.build().ok()
596 }
597}
598
599fn dump_node_logs(run_dir: &Path) {
607 fn collect(dir: &Path, out: &mut Vec<PathBuf>) {
608 let Ok(entries) = std::fs::read_dir(dir) else {
609 return;
610 };
611 for entry in entries.flatten() {
612 let path = entry.path();
613 if path.is_symlink() || !path.is_dir() {
615 continue;
616 }
617 if path.file_name().is_some_and(|name| name == "node-logs") {
618 if let Ok(logs) = std::fs::read_dir(&path) {
619 out.extend(logs.flatten().map(|e| e.path()));
620 }
621 } else {
622 collect(&path, out);
623 }
624 }
625 }
626
627 let mut log_files = Vec::new();
628 collect(&run_dir.join("store"), &mut log_files);
629 log_files.sort();
630
631 for log in log_files {
632 eprintln!("==== Maelstrom node log: {} ====", log.display());
633 match std::fs::read_to_string(&log) {
634 Ok(contents) => eprint!("{}", contents),
635 Err(e) => eprintln!("(failed to read log: {})", e),
636 }
637 eprintln!("==== end of node log: {} ====", log.display());
638 }
639}