1use std::cell::RefCell;
8use std::future::Future;
9use std::io::{BufRead, BufReader, Error};
10use std::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 ) -> (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 ) -> (syn::Expr, syn::Expr) {
79 panic!("Maelstrom deployment does not support processes, only clusters")
80 }
81
82 fn o2m_connect(
83 _p1: &Self::Process,
84 _p1_port: &<Self::Process as Node>::Port,
85 _c2: &Self::Cluster,
86 _c2_port: &<Self::Cluster as Node>::Port,
87 ) -> Box<dyn FnOnce()> {
88 panic!("Maelstrom deployment does not support processes, only clusters")
89 }
90
91 fn m2o_sink_source(
92 _env: &mut Self::InstantiateEnv,
93 _c1: &Self::Cluster,
94 _c1_port: &<Self::Cluster as Node>::Port,
95 _p2: &Self::Process,
96 _p2_port: &<Self::Process as Node>::Port,
97 _name: Option<&str>,
98 _networking_info: &crate::networking::NetworkingInfo,
99 ) -> (syn::Expr, syn::Expr) {
100 panic!("Maelstrom deployment does not support processes, only clusters")
101 }
102
103 fn m2o_connect(
104 _c1: &Self::Cluster,
105 _c1_port: &<Self::Cluster as Node>::Port,
106 _p2: &Self::Process,
107 _p2_port: &<Self::Process as Node>::Port,
108 ) -> Box<dyn FnOnce()> {
109 panic!("Maelstrom deployment does not support processes, only clusters")
110 }
111
112 fn m2m_sink_source(
113 env: &mut Self::InstantiateEnv,
114 _c1: &Self::Cluster,
115 _c1_port: &<Self::Cluster as Node>::Port,
116 _c2: &Self::Cluster,
117 _c2_port: &<Self::Cluster as Node>::Port,
118 _name: Option<&str>,
119 networking_info: &crate::networking::NetworkingInfo,
120 ) -> (syn::Expr, syn::Expr) {
121 use crate::networking::{NetworkingInfo, TcpFault};
122 match networking_info {
123 NetworkingInfo::Tcp { fault } => match (fault, env.nemesis.as_deref()) {
124 (TcpFault::Lossy | TcpFault::LossyDelayedForever, _) => {} (_, None) => {} (TcpFault::FailStop, Some("partition")) => {
127 panic!(
128 "Maelstrom partition nemesis requires lossy networking, but fail_stop was used. \
129 Use `TCP.lossy().bincode()` or `TCP.lossy_delayed_forever().bincode()` instead of `TCP.fail_stop().bincode()`."
130 );
131 }
132 (TcpFault::FailStop, Some(_)) => {} },
134 }
135 deploy_maelstrom_m2m(RuntimeData::new("__hydro_lang_maelstrom_meta"))
136 }
137
138 fn m2m_connect(
139 _c1: &Self::Cluster,
140 _c1_port: &<Self::Cluster as Node>::Port,
141 _c2: &Self::Cluster,
142 _c2_port: &<Self::Cluster as Node>::Port,
143 ) -> Box<dyn FnOnce()> {
144 Box::new(|| {})
146 }
147
148 fn e2o_many_source(
149 _extra_stmts: &mut Vec<syn::Stmt>,
150 _p2: &Self::Process,
151 _p2_port: &<Self::Process as Node>::Port,
152 _codec_type: &syn::Type,
153 _shared_handle: String,
154 ) -> syn::Expr {
155 panic!("Maelstrom deployment does not support processes, only clusters")
156 }
157
158 fn e2o_many_sink(_shared_handle: String) -> syn::Expr {
159 panic!("Maelstrom deployment does not support processes, only clusters")
160 }
161
162 fn e2o_source(
163 _extra_stmts: &mut Vec<syn::Stmt>,
164 _p1: &Self::External,
165 _p1_port: &<Self::External as Node>::Port,
166 _p2: &Self::Process,
167 _p2_port: &<Self::Process as Node>::Port,
168 _codec_type: &syn::Type,
169 _shared_handle: String,
170 ) -> syn::Expr {
171 panic!("Maelstrom deployment does not support processes, only clusters")
172 }
173
174 fn e2o_connect(
175 _p1: &Self::External,
176 _p1_port: &<Self::External as Node>::Port,
177 _p2: &Self::Process,
178 _p2_port: &<Self::Process as Node>::Port,
179 _many: bool,
180 _server_hint: NetworkHint,
181 ) -> Box<dyn FnOnce()> {
182 panic!("Maelstrom deployment does not support processes, only clusters")
183 }
184
185 fn o2e_sink(
186 _p1: &Self::Process,
187 _p1_port: &<Self::Process as Node>::Port,
188 _p2: &Self::External,
189 _p2_port: &<Self::External as Node>::Port,
190 _shared_handle: String,
191 ) -> syn::Expr {
192 panic!("Maelstrom deployment does not support processes, only clusters")
193 }
194
195 fn cluster_ids(
196 _of_cluster: LocationKey,
197 ) -> impl QuotedWithContext<'a, &'a [TaglessMemberId], ()> + Clone + 'a {
198 cluster_members(RuntimeData::new("__hydro_lang_maelstrom_meta"), _of_cluster)
199 }
200
201 fn cluster_self_id() -> impl QuotedWithContext<'a, TaglessMemberId, ()> + Clone + 'a {
202 cluster_self_id(RuntimeData::new("__hydro_lang_maelstrom_meta"))
203 }
204
205 fn cluster_membership_stream(
206 _env: &mut Self::InstantiateEnv,
207 _at_location: &LocationId,
208 location_id: &LocationId,
209 ) -> impl QuotedWithContext<'a, Box<dyn Stream<Item = (TaglessMemberId, MembershipEvent)> + Unpin>, ()>
210 {
211 cluster_membership_stream(location_id)
212 }
213}
214
215#[derive(Clone)]
217pub struct MaelstromProcess {
218 _private: (),
219}
220
221impl Node for MaelstromProcess {
222 type Port = String;
223 type Meta = ();
224 type InstantiateEnv = MaelstromDeployment;
225
226 fn next_port(&self) -> Self::Port {
227 panic!("Maelstrom deployment does not support processes")
228 }
229
230 fn update_meta(&self, _meta: &Self::Meta) {}
231
232 fn instantiate(
233 &self,
234 _env: &mut Self::InstantiateEnv,
235 _meta: &mut Self::Meta,
236 _graph: DfirGraph,
237 _extra_stmts: &[syn::Stmt],
238 _sidecars: &[syn::Expr],
239 ) {
240 panic!("Maelstrom deployment does not support processes")
241 }
242}
243
244#[derive(Clone)]
246pub struct MaelstromCluster {
247 next_port: Rc<RefCell<usize>>,
248 name_hint: Option<String>,
249}
250
251impl Node for MaelstromCluster {
252 type Port = String;
253 type Meta = ();
254 type InstantiateEnv = MaelstromDeployment;
255
256 fn next_port(&self) -> Self::Port {
257 let next_port = *self.next_port.borrow();
258 *self.next_port.borrow_mut() += 1;
259 format!("port_{}", next_port)
260 }
261
262 fn update_meta(&self, _meta: &Self::Meta) {}
263
264 fn instantiate(
265 &self,
266 env: &mut Self::InstantiateEnv,
267 _meta: &mut Self::Meta,
268 graph: DfirGraph,
269 extra_stmts: &[syn::Stmt],
270 sidecars: &[syn::Expr],
271 ) {
272 let (bin_name, config) = create_graph_trybuild(
273 graph,
274 extra_stmts,
275 sidecars,
276 self.name_hint.as_deref(),
277 crate::compile::trybuild::generate::DeployMode::Maelstrom,
278 LinkingMode::Dynamic,
279 );
280
281 env.bin_name = Some(bin_name);
282 env.trybuild = Some(config);
283 }
284}
285
286#[derive(Clone)]
288pub enum MaelstromExternal {}
289
290impl Node for MaelstromExternal {
291 type Port = String;
292 type Meta = ();
293 type InstantiateEnv = MaelstromDeployment;
294
295 fn next_port(&self) -> Self::Port {
296 unreachable!()
297 }
298
299 fn update_meta(&self, _meta: &Self::Meta) {}
300
301 fn instantiate(
302 &self,
303 _env: &mut Self::InstantiateEnv,
304 _meta: &mut Self::Meta,
305 _graph: DfirGraph,
306 _extra_stmts: &[syn::Stmt],
307 _sidecars: &[syn::Expr],
308 ) {
309 unreachable!()
310 }
311}
312
313impl<'a> RegisterPort<'a, MaelstromDeploy> for MaelstromExternal {
314 fn register(&self, _external_port_id: ExternalPortId, _port: Self::Port) {
315 unreachable!()
316 }
317
318 #[expect(clippy::manual_async_fn, reason = "false positive, involves lifetimes")]
319 fn as_bytes_bidi(
320 &self,
321 _external_port_id: ExternalPortId,
322 ) -> impl Future<
323 Output = (
324 Pin<Box<dyn Stream<Item = Result<BytesMut, Error>>>>,
325 Pin<Box<dyn Sink<Bytes, Error = Error>>>,
326 ),
327 > + 'a {
328 async move { unreachable!() }
329 }
330
331 #[expect(clippy::manual_async_fn, reason = "false positive, involves lifetimes")]
332 fn as_bincode_bidi<InT, OutT>(
333 &self,
334 _external_port_id: ExternalPortId,
335 ) -> impl Future<
336 Output = (
337 Pin<Box<dyn Stream<Item = OutT>>>,
338 Pin<Box<dyn Sink<InT, Error = Error>>>,
339 ),
340 > + 'a
341 where
342 InT: Serialize + 'static,
343 OutT: DeserializeOwned + 'static,
344 {
345 async move { unreachable!() }
346 }
347
348 #[expect(clippy::manual_async_fn, reason = "false positive, involves lifetimes")]
349 fn as_bincode_sink<T: Serialize + 'static>(
350 &self,
351 _external_port_id: ExternalPortId,
352 ) -> impl Future<Output = Pin<Box<dyn Sink<T, Error = Error>>>> + 'a {
353 async move { unreachable!() }
354 }
355
356 #[expect(clippy::manual_async_fn, reason = "false positive, involves lifetimes")]
357 fn as_bincode_source<T: DeserializeOwned + 'static>(
358 &self,
359 _external_port_id: ExternalPortId,
360 ) -> impl Future<Output = Pin<Box<dyn Stream<Item = T>>>> + 'a {
361 async move { unreachable!() }
362 }
363}
364
365#[derive(Clone)]
367pub struct MaelstromClusterSpec;
368
369impl<'a> ClusterSpec<'a, MaelstromDeploy> for MaelstromClusterSpec {
370 fn build(self, key: LocationKey, name_hint: &str) -> MaelstromCluster {
371 assert_eq!(
372 key,
373 LocationKey::FIRST,
374 "there should only be one location for a Maelstrom deployment"
375 );
376 MaelstromCluster {
377 next_port: Rc::new(RefCell::new(0)),
378 name_hint: Some(name_hint.to_owned()),
379 }
380 }
381}
382
383pub struct MaelstromDeployment {
388 pub node_count: usize,
390 pub maelstrom_path: PathBuf,
392 pub workload: String,
394 pub time_limit: Option<u64>,
396 pub rate: Option<u64>,
398 pub availability: Option<String>,
400 pub nemesis: Option<String>,
402 pub extra_args: Vec<String>,
404
405 pub(crate) bin_name: Option<String>,
407 pub(crate) trybuild: Option<TrybuildConfig>,
408}
409
410impl MaelstromDeployment {
411 pub fn new(workload: impl Into<String>) -> Self {
413 Self {
414 node_count: 1,
415 maelstrom_path: PathBuf::from("maelstrom"),
416 workload: workload.into(),
417 time_limit: None,
418 rate: None,
419 availability: None,
420 nemesis: None,
421 extra_args: vec![],
422 bin_name: None,
423 trybuild: None,
424 }
425 }
426
427 pub fn node_count(mut self, count: usize) -> Self {
429 self.node_count = count;
430 self
431 }
432
433 pub fn maelstrom_path(mut self, path: impl Into<PathBuf>) -> Self {
435 self.maelstrom_path = path.into();
436 self
437 }
438
439 pub fn time_limit(mut self, seconds: u64) -> Self {
441 self.time_limit = Some(seconds);
442 self
443 }
444
445 pub fn rate(mut self, rate: u64) -> Self {
447 self.rate = Some(rate);
448 self
449 }
450
451 pub fn availability(mut self, availability: impl Into<String>) -> Self {
453 self.availability = Some(availability.into());
454 self
455 }
456
457 pub fn nemesis(mut self, nemesis: impl Into<String>) -> Self {
459 self.nemesis = Some(nemesis.into());
460 self
461 }
462
463 pub fn extra_args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
465 self.extra_args.extend(args.into_iter().map(Into::into));
466 self
467 }
468
469 pub fn build(&self) -> Result<PathBuf, Error> {
477 let bin_name = self
478 .bin_name
479 .as_ref()
480 .expect("No binary name set - did you call deploy?");
481 let trybuild = self
482 .trybuild
483 .as_ref()
484 .expect("No trybuild config set - did you call deploy?");
485
486 let out = compile_trybuild_example(ExampleBuildConfig {
487 trybuild: trybuild.clone(),
488 bin_name: bin_name.clone(),
489 runtime_feature: "hydro___feature_maelstrom_runtime",
490 example_name: bin_name.clone(),
492 crate_type: None,
493 set_trybuild_lib_name: false,
494 allow_fuzz: false,
495 })
496 .map_err(|()| Error::other("Maelstrom binary compilation failed"))?;
497
498 out.keep().map_err(|e| Error::other(e.to_string()))
500 }
501
502 pub fn run(self) -> Result<(), Error> {
506 let binary_path = self.build()?;
507
508 let run_dir = tempfile::tempdir().map_err(Error::other)?;
510
511 let mut cmd = std::process::Command::new(&self.maelstrom_path);
512 cmd.arg("test")
513 .arg("-w")
514 .arg(&self.workload)
515 .arg("--bin")
516 .arg(&binary_path)
517 .arg("--node-count")
518 .arg(self.node_count.to_string())
519 .current_dir(run_dir.path())
520 .stdout(Stdio::piped());
521
522 if let Some(time_limit) = self.time_limit {
523 cmd.arg("--time-limit").arg(time_limit.to_string());
524 }
525
526 if let Some(rate) = self.rate {
527 cmd.arg("--rate").arg(rate.to_string());
528 }
529
530 if let Some(availability) = self.availability {
531 cmd.arg("--availability").arg(availability);
532 }
533
534 if let Some(nemesis) = self.nemesis {
535 cmd.arg("--nemesis").arg(nemesis);
536 }
537
538 for arg in &self.extra_args {
539 cmd.arg(arg);
540 }
541
542 let spawned = cmd.spawn()?;
543
544 for line in BufReader::new(spawned.stdout.unwrap()).lines() {
545 let line = line?;
546 eprintln!("{}", &line);
547
548 if line.starts_with("Analysis invalid!") {
549 let path = run_dir.keep();
550 return Err(Error::other(format!(
551 "Analysis was invalid. Maelstrom store at: {}",
552 path.display()
553 )));
554 } else if line.starts_with("Errors occurred during analysis, but no anomalies found.")
555 || line.starts_with("Everything looks good!")
556 {
557 return Ok(());
558 }
559 }
560
561 let path = run_dir.keep();
562 Err(Error::other(format!(
563 "Maelstrom produced an unexpected result. Store at: {}",
564 path.display()
565 )))
566 }
567
568 pub fn binary_path(&self) -> Option<PathBuf> {
570 self.build().ok()
571 }
572}