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