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::{LinkingMode, create_graph_trybuild};
26use crate::location::dynamic::LocationId;
27use crate::location::member_id::TaglessMemberId;
28use crate::location::{LocationKey, MembershipEvent, NetworkHint};
29
30pub enum MaelstromDeploy {}
37
38impl<'a> Deploy<'a> for MaelstromDeploy {
39 type Meta = ();
40 type InstantiateEnv = MaelstromDeployment;
41
42 type Process = MaelstromProcess;
43 type Cluster = MaelstromCluster;
44 type External = MaelstromExternal;
45
46 fn o2o_sink_source(
47 _env: &mut Self::InstantiateEnv,
48 _p1: &Self::Process,
49 _p1_port: &<Self::Process as Node>::Port,
50 _p2: &Self::Process,
51 _p2_port: &<Self::Process as Node>::Port,
52 _name: Option<&str>,
53 _networking_info: &crate::networking::NetworkingInfo,
54 _external_types: Option<(&syn::Type, &syn::Type)>,
55 ) -> (syn::Expr, syn::Expr) {
56 panic!("Maelstrom deployment does not support processes, only clusters")
57 }
58
59 fn o2o_connect(
60 _p1: &Self::Process,
61 _p1_port: &<Self::Process as Node>::Port,
62 _p2: &Self::Process,
63 _p2_port: &<Self::Process as Node>::Port,
64 ) -> Box<dyn FnOnce()> {
65 panic!("Maelstrom deployment does not support processes, only clusters")
66 }
67
68 fn o2m_sink_source(
69 _env: &mut Self::InstantiateEnv,
70 _p1: &Self::Process,
71 _p1_port: &<Self::Process as Node>::Port,
72 _c2: &Self::Cluster,
73 _c2_port: &<Self::Cluster as Node>::Port,
74 _name: Option<&str>,
75 _networking_info: &crate::networking::NetworkingInfo,
76 _external_types: Option<(&syn::Type, &syn::Type)>,
77 ) -> (syn::Expr, syn::Expr) {
78 panic!("Maelstrom deployment does not support processes, only clusters")
79 }
80
81 fn o2m_connect(
82 _p1: &Self::Process,
83 _p1_port: &<Self::Process as Node>::Port,
84 _c2: &Self::Cluster,
85 _c2_port: &<Self::Cluster as Node>::Port,
86 ) -> Box<dyn FnOnce()> {
87 panic!("Maelstrom deployment does not support processes, only clusters")
88 }
89
90 fn m2o_sink_source(
91 _env: &mut Self::InstantiateEnv,
92 _c1: &Self::Cluster,
93 _c1_port: &<Self::Cluster as Node>::Port,
94 _p2: &Self::Process,
95 _p2_port: &<Self::Process as Node>::Port,
96 _name: Option<&str>,
97 _networking_info: &crate::networking::NetworkingInfo,
98 _external_types: Option<(&syn::Type, &syn::Type)>,
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 _external_types: Option<(&syn::Type, &syn::Type)>,
121 ) -> (syn::Expr, syn::Expr) {
122 use crate::networking::{NetworkingInfo, TcpFault};
123 match networking_info {
124 NetworkingInfo::Tcp { fault } => match (fault, env.nemesis.as_deref()) {
125 (TcpFault::Lossy | TcpFault::LossyDelayedForever, _) => {} (_, None) => {} (TcpFault::FailStop, Some("partition")) => {
128 panic!(
129 "Maelstrom partition nemesis requires lossy networking, but fail_stop was used. \
130 Use `TCP.lossy().bincode()` or `TCP.lossy_delayed_forever().bincode()` instead of `TCP.fail_stop().bincode()`."
131 );
132 }
133 (TcpFault::FailStop, Some(_)) => {} },
135 }
136 deploy_maelstrom_m2m(RuntimeData::new("__hydro_lang_maelstrom_meta"))
137 }
138
139 fn m2m_connect(
140 _c1: &Self::Cluster,
141 _c1_port: &<Self::Cluster as Node>::Port,
142 _c2: &Self::Cluster,
143 _c2_port: &<Self::Cluster as Node>::Port,
144 ) -> Box<dyn FnOnce()> {
145 Box::new(|| {})
147 }
148
149 fn e2o_many_source(
150 _extra_stmts: &mut Vec<syn::Stmt>,
151 _p2: &Self::Process,
152 _p2_port: &<Self::Process as Node>::Port,
153 _codec_type: &syn::Type,
154 _shared_handle: String,
155 ) -> syn::Expr {
156 panic!("Maelstrom deployment does not support processes, only clusters")
157 }
158
159 fn e2o_many_sink(_shared_handle: String) -> syn::Expr {
160 panic!("Maelstrom deployment does not support processes, only clusters")
161 }
162
163 fn e2o_source(
164 _extra_stmts: &mut Vec<syn::Stmt>,
165 _p1: &Self::External,
166 _p1_port: &<Self::External as Node>::Port,
167 _p2: &Self::Process,
168 _p2_port: &<Self::Process as Node>::Port,
169 _codec_type: &syn::Type,
170 _shared_handle: String,
171 ) -> syn::Expr {
172 panic!("Maelstrom deployment does not support processes, only clusters")
173 }
174
175 fn e2o_connect(
176 _p1: &Self::External,
177 _p1_port: &<Self::External as Node>::Port,
178 _p2: &Self::Process,
179 _p2_port: &<Self::Process as Node>::Port,
180 _many: bool,
181 _server_hint: NetworkHint,
182 ) -> Box<dyn FnOnce()> {
183 panic!("Maelstrom deployment does not support processes, only clusters")
184 }
185
186 fn o2e_sink(
187 _p1: &Self::Process,
188 _p1_port: &<Self::Process as Node>::Port,
189 _p2: &Self::External,
190 _p2_port: &<Self::External as Node>::Port,
191 _shared_handle: String,
192 ) -> syn::Expr {
193 panic!("Maelstrom deployment does not support processes, only clusters")
194 }
195
196 fn cluster_ids(
197 _of_cluster: LocationKey,
198 ) -> impl QuotedWithContext<'a, &'a [TaglessMemberId], ()> + Clone + 'a {
199 cluster_members(RuntimeData::new("__hydro_lang_maelstrom_meta"), _of_cluster)
200 }
201
202 fn cluster_self_id() -> impl QuotedWithContext<'a, TaglessMemberId, ()> + Clone + 'a {
203 cluster_self_id(RuntimeData::new("__hydro_lang_maelstrom_meta"))
204 }
205
206 fn cluster_membership_stream(
207 _env: &mut Self::InstantiateEnv,
208 _at_location: &LocationId,
209 location_id: &LocationId,
210 ) -> impl QuotedWithContext<'a, Box<dyn Stream<Item = (TaglessMemberId, MembershipEvent)> + Unpin>, ()>
211 {
212 cluster_membership_stream(location_id)
213 }
214}
215
216#[derive(Clone)]
218pub struct MaelstromProcess {
219 _private: (),
220}
221
222impl Node for MaelstromProcess {
223 type Port = String;
224 type Meta = ();
225 type InstantiateEnv = MaelstromDeployment;
226
227 fn next_port(&self) -> Self::Port {
228 panic!("Maelstrom deployment does not support processes")
229 }
230
231 fn update_meta(&self, _meta: &Self::Meta) {}
232
233 fn instantiate(
234 &self,
235 _env: &mut Self::InstantiateEnv,
236 _meta: &mut Self::Meta,
237 _graph: DfirGraph,
238 _extra_stmts: &[syn::Stmt],
239 _sidecars: &[syn::Expr],
240 ) {
241 panic!("Maelstrom deployment does not support processes")
242 }
243}
244
245#[derive(Clone)]
247pub struct MaelstromCluster {
248 next_port: Rc<RefCell<usize>>,
249 name_hint: Option<String>,
250}
251
252impl Node for MaelstromCluster {
253 type Port = String;
254 type Meta = ();
255 type InstantiateEnv = MaelstromDeployment;
256
257 fn next_port(&self) -> Self::Port {
258 let next_port = *self.next_port.borrow();
259 *self.next_port.borrow_mut() += 1;
260 format!("port_{}", next_port)
261 }
262
263 fn update_meta(&self, _meta: &Self::Meta) {}
264
265 fn instantiate(
266 &self,
267 env: &mut Self::InstantiateEnv,
268 _meta: &mut Self::Meta,
269 graph: DfirGraph,
270 extra_stmts: &[syn::Stmt],
271 sidecars: &[syn::Expr],
272 ) {
273 let (bin_name, config) = create_graph_trybuild(
274 graph,
275 extra_stmts,
276 sidecars,
277 self.name_hint.as_deref(),
278 crate::compile::trybuild::generate::DeployMode::Maelstrom,
279 LinkingMode::Static,
280 );
281
282 env.bin_name = Some(bin_name);
283 env.project_dir = Some(config.project_dir);
284 env.target_dir = Some(config.target_dir);
285 env.features = config.features;
286 }
287}
288
289#[derive(Clone)]
291pub enum MaelstromExternal {}
292
293impl Node for MaelstromExternal {
294 type Port = String;
295 type Meta = ();
296 type InstantiateEnv = MaelstromDeployment;
297
298 fn next_port(&self) -> Self::Port {
299 unreachable!()
300 }
301
302 fn update_meta(&self, _meta: &Self::Meta) {}
303
304 fn instantiate(
305 &self,
306 _env: &mut Self::InstantiateEnv,
307 _meta: &mut Self::Meta,
308 _graph: DfirGraph,
309 _extra_stmts: &[syn::Stmt],
310 _sidecars: &[syn::Expr],
311 ) {
312 unreachable!()
313 }
314}
315
316impl<'a> RegisterPort<'a, MaelstromDeploy> for MaelstromExternal {
317 fn register(&self, _external_port_id: ExternalPortId, _port: Self::Port) {
318 unreachable!()
319 }
320
321 #[expect(clippy::manual_async_fn, reason = "false positive, involves lifetimes")]
322 fn as_bytes_bidi(
323 &self,
324 _external_port_id: ExternalPortId,
325 ) -> impl Future<
326 Output = (
327 Pin<Box<dyn Stream<Item = Result<BytesMut, Error>>>>,
328 Pin<Box<dyn Sink<Bytes, Error = Error>>>,
329 ),
330 > + 'a {
331 async move { unreachable!() }
332 }
333
334 #[expect(clippy::manual_async_fn, reason = "false positive, involves lifetimes")]
335 fn as_bincode_bidi<InT, OutT>(
336 &self,
337 _external_port_id: ExternalPortId,
338 ) -> impl Future<
339 Output = (
340 Pin<Box<dyn Stream<Item = OutT>>>,
341 Pin<Box<dyn Sink<InT, Error = Error>>>,
342 ),
343 > + 'a
344 where
345 InT: Serialize + 'static,
346 OutT: DeserializeOwned + 'static,
347 {
348 async move { unreachable!() }
349 }
350
351 #[expect(clippy::manual_async_fn, reason = "false positive, involves lifetimes")]
352 fn as_bincode_sink<T: Serialize + 'static>(
353 &self,
354 _external_port_id: ExternalPortId,
355 ) -> impl Future<Output = Pin<Box<dyn Sink<T, Error = Error>>>> + 'a {
356 async move { unreachable!() }
357 }
358
359 #[expect(clippy::manual_async_fn, reason = "false positive, involves lifetimes")]
360 fn as_bincode_source<T: DeserializeOwned + 'static>(
361 &self,
362 _external_port_id: ExternalPortId,
363 ) -> impl Future<Output = Pin<Box<dyn Stream<Item = T>>>> + 'a {
364 async move { unreachable!() }
365 }
366}
367
368#[derive(Clone)]
370pub struct MaelstromClusterSpec;
371
372impl<'a> ClusterSpec<'a, MaelstromDeploy> for MaelstromClusterSpec {
373 fn build(self, key: LocationKey, name_hint: &str) -> MaelstromCluster {
374 assert_eq!(
375 key,
376 LocationKey::FIRST,
377 "there should only be one location for a Maelstrom deployment"
378 );
379 MaelstromCluster {
380 next_port: Rc::new(RefCell::new(0)),
381 name_hint: Some(name_hint.to_owned()),
382 }
383 }
384}
385
386pub struct MaelstromDeployment {
391 pub node_count: usize,
393 pub maelstrom_path: PathBuf,
395 pub workload: String,
397 pub time_limit: Option<u64>,
399 pub rate: Option<u64>,
401 pub availability: Option<String>,
403 pub nemesis: Option<String>,
405 pub extra_args: Vec<String>,
407
408 pub(crate) bin_name: Option<String>,
410 pub(crate) project_dir: Option<PathBuf>,
411 pub(crate) target_dir: Option<PathBuf>,
412 pub(crate) features: Option<Vec<String>>,
413}
414
415impl MaelstromDeployment {
416 pub fn new(workload: impl Into<String>) -> Self {
418 Self {
419 node_count: 1,
420 maelstrom_path: PathBuf::from("maelstrom"),
421 workload: workload.into(),
422 time_limit: None,
423 rate: None,
424 availability: None,
425 nemesis: None,
426 extra_args: vec![],
427 bin_name: None,
428 project_dir: None,
429 target_dir: None,
430 features: None,
431 }
432 }
433
434 pub fn node_count(mut self, count: usize) -> Self {
436 self.node_count = count;
437 self
438 }
439
440 pub fn maelstrom_path(mut self, path: impl Into<PathBuf>) -> Self {
442 self.maelstrom_path = path.into();
443 self
444 }
445
446 pub fn time_limit(mut self, seconds: u64) -> Self {
448 self.time_limit = Some(seconds);
449 self
450 }
451
452 pub fn rate(mut self, rate: u64) -> Self {
454 self.rate = Some(rate);
455 self
456 }
457
458 pub fn availability(mut self, availability: impl Into<String>) -> Self {
460 self.availability = Some(availability.into());
461 self
462 }
463
464 pub fn nemesis(mut self, nemesis: impl Into<String>) -> Self {
466 self.nemesis = Some(nemesis.into());
467 self
468 }
469
470 pub fn extra_args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
472 self.extra_args.extend(args.into_iter().map(Into::into));
473 self
474 }
475
476 pub fn build(&self) -> Result<PathBuf, Error> {
479 let bin_name = self
480 .bin_name
481 .as_ref()
482 .expect("No binary name set - did you call deploy?");
483 let project_dir = self.project_dir.as_ref().expect("No project dir set");
484 let target_dir = self.target_dir.as_ref().expect("No target dir set");
485
486 let mut cmd = std::process::Command::new("cargo");
487 cmd.arg("build")
488 .arg("--example")
489 .arg(bin_name)
490 .arg("--no-default-features")
491 .current_dir(project_dir)
492 .env("CARGO_TARGET_DIR", target_dir)
493 .env("STAGELEFT_TRYBUILD_BUILD_STAGED", "1");
494
495 let mut all_features = vec!["hydro___feature_maelstrom_runtime".to_owned()];
497 if let Some(features) = &self.features {
498 all_features.extend(features.iter().cloned());
499 }
500 if !all_features.is_empty() {
501 cmd.arg("--features").arg(all_features.join(","));
502 }
503
504 let status = cmd.status()?;
505 if !status.success() {
506 return Err(Error::other(format!(
507 "cargo build failed with status: {}",
508 status
509 )));
510 }
511
512 Ok(target_dir.join("debug").join("examples").join(bin_name))
513 }
514
515 pub fn run(self) -> Result<(), Error> {
519 let binary_path = self.build()?;
520
521 let run_dir = tempfile::tempdir().map_err(Error::other)?;
523
524 let mut cmd = std::process::Command::new(&self.maelstrom_path);
525 cmd.arg("test")
526 .arg("-w")
527 .arg(&self.workload)
528 .arg("--bin")
529 .arg(&binary_path)
530 .arg("--node-count")
531 .arg(self.node_count.to_string())
532 .current_dir(run_dir.path())
533 .stdout(Stdio::piped());
534
535 if let Some(time_limit) = self.time_limit {
536 cmd.arg("--time-limit").arg(time_limit.to_string());
537 }
538
539 if let Some(rate) = self.rate {
540 cmd.arg("--rate").arg(rate.to_string());
541 }
542
543 if let Some(availability) = self.availability {
544 cmd.arg("--availability").arg(availability);
545 }
546
547 if let Some(nemesis) = self.nemesis {
548 cmd.arg("--nemesis").arg(nemesis);
549 }
550
551 for arg in &self.extra_args {
552 cmd.arg(arg);
553 }
554
555 let spawned = cmd.spawn()?;
556
557 for line in BufReader::new(spawned.stdout.unwrap()).lines() {
558 let line = line?;
559 eprintln!("{}", &line);
560
561 if line.starts_with("Analysis invalid!") {
562 let path = run_dir.keep();
563 return Err(Error::other(format!(
564 "Analysis was invalid. Maelstrom store at: {}",
565 path.display()
566 )));
567 } else if line.starts_with("Errors occurred during analysis, but no anomalies found.")
568 || line.starts_with("Everything looks good!")
569 {
570 return Ok(());
571 }
572 }
573
574 let path = run_dir.keep();
575 Err(Error::other(format!(
576 "Maelstrom produced an unexpected result. Store at: {}",
577 path.display()
578 )))
579 }
580
581 pub fn binary_path(&self) -> Option<PathBuf> {
583 let bin_name = self.bin_name.as_ref()?;
584 let target_dir = self.target_dir.as_ref()?;
585 Some(target_dir.join("debug").join("examples").join(bin_name))
586 }
587}