1use std::collections::{HashMap, HashSet};
2use std::io::Error;
3use std::marker::PhantomData;
4use std::pin::Pin;
5
6use bytes::{Bytes, BytesMut};
7use dfir_lang::graph::AsCodeOptions;
8use futures::{Sink, Stream};
9use proc_macro2::Span;
10use serde::Serialize;
11use serde::de::DeserializeOwned;
12use slotmap::{SecondaryMap, SlotMap, SparseSecondaryMap};
13use stageleft::QuotedWithContext;
14
15use super::built::build_inner;
16use super::compiled::CompiledFlow;
17use super::deploy_provider::{
18 ClusterSpec, Deploy, ExternalSpec, IntoProcessSpec, Node, ProcessSpec, RegisterPort,
19};
20use super::ir::HydroRoot;
21use crate::live_collections::stream::{Ordering, Retries};
22use crate::location::dynamic::LocationId;
23use crate::location::external_process::{
24 ExternalBincodeBidi, ExternalBincodeSink, ExternalBincodeStream, ExternalBytesPort,
25};
26use crate::location::{Cluster, External, Location, LocationKey, LocationType, Process};
27use crate::staging_util::Invariant;
28use crate::telemetry::Sidecar;
29
30pub struct DeployFlow<'a, D>
31where
32 D: Deploy<'a>,
33{
34 pub(super) ir: Vec<HydroRoot>,
35
36 pub(super) locations: SlotMap<LocationKey, LocationType>,
37 pub(super) location_names: SecondaryMap<LocationKey, String>,
38
39 pub(super) processes: SparseSecondaryMap<LocationKey, D::Process>,
41 pub(super) clusters: SparseSecondaryMap<LocationKey, D::Cluster>,
42 pub(super) externals: SparseSecondaryMap<LocationKey, D::External>,
43
44 pub(super) sidecars: Vec<super::builder::Sidecar>,
46
47 pub(super) as_code_options: SparseSecondaryMap<LocationKey, AsCodeOptions>,
50
51 pub(super) flow_name: String,
53
54 pub(super) _phantom: Invariant<'a, D>,
55}
56
57impl<'a, D: Deploy<'a>> DeployFlow<'a, D> {
58 pub fn ir(&self) -> &Vec<HydroRoot> {
59 &self.ir
60 }
61
62 pub fn flow_name(&self) -> &str {
64 &self.flow_name
65 }
66
67 pub fn with_process<P>(
68 mut self,
69 process: &Process<'_, P>,
70 spec: impl IntoProcessSpec<'a, D>,
71 ) -> Self {
72 self.processes.insert(
73 process.key,
74 spec.into_process_spec()
75 .build(process.key, &self.location_names[process.key]),
76 );
77 self
78 }
79
80 #[doc(hidden)]
82 pub fn with_process_erased(
83 mut self,
84 process_loc_key: LocationKey,
85 spec: impl IntoProcessSpec<'a, D>,
86 ) -> Self {
87 assert_eq!(
88 Some(&LocationType::Process),
89 self.locations.get(process_loc_key),
90 "No process with the given `LocationKey` was found."
91 );
92 self.processes.insert(
93 process_loc_key,
94 spec.into_process_spec()
95 .build(process_loc_key, &self.location_names[process_loc_key]),
96 );
97 self
98 }
99
100 pub fn with_remaining_processes<S: IntoProcessSpec<'a, D> + 'a>(
101 mut self,
102 spec: impl Fn() -> S,
103 ) -> Self {
104 for (location_key, &location_type) in self.locations.iter() {
105 if LocationType::Process == location_type {
106 self.processes
107 .entry(location_key)
108 .expect("location was removed")
109 .or_insert_with(|| {
110 spec()
111 .into_process_spec()
112 .build(location_key, &self.location_names[location_key])
113 });
114 }
115 }
116 self
117 }
118
119 pub fn with_cluster<C>(
120 mut self,
121 cluster: &Cluster<'_, C>,
122 spec: impl ClusterSpec<'a, D>,
123 ) -> Self {
124 self.clusters.insert(
125 cluster.key,
126 spec.build(cluster.key, &self.location_names[cluster.key]),
127 );
128 self
129 }
130
131 #[doc(hidden)]
133 pub fn with_cluster_erased(
134 mut self,
135 cluster_loc_key: LocationKey,
136 spec: impl ClusterSpec<'a, D>,
137 ) -> Self {
138 assert_eq!(
139 Some(&LocationType::Cluster),
140 self.locations.get(cluster_loc_key),
141 "No cluster with the given `LocationKey` was found."
142 );
143 self.clusters.insert(
144 cluster_loc_key,
145 spec.build(cluster_loc_key, &self.location_names[cluster_loc_key]),
146 );
147 self
148 }
149
150 pub fn with_remaining_clusters<S: ClusterSpec<'a, D> + 'a>(
151 mut self,
152 spec: impl Fn() -> S,
153 ) -> Self {
154 for (location_key, &location_type) in self.locations.iter() {
155 if LocationType::Cluster == location_type {
156 self.clusters
157 .entry(location_key)
158 .expect("location was removed")
159 .or_insert_with(|| {
160 spec().build(location_key, &self.location_names[location_key])
161 });
162 }
163 }
164 self
165 }
166
167 pub fn with_external<P>(
168 mut self,
169 external: &External<'_, P>,
170 spec: impl ExternalSpec<'a, D>,
171 ) -> Self {
172 self.externals.insert(
173 external.key,
174 spec.build(external.key, &self.location_names[external.key]),
175 );
176 self
177 }
178
179 pub fn with_remaining_externals<S: ExternalSpec<'a, D> + 'a>(
180 mut self,
181 spec: impl Fn() -> S,
182 ) -> Self {
183 for (location_key, &location_type) in self.locations.iter() {
184 if LocationType::External == location_type {
185 self.externals
186 .entry(location_key)
187 .expect("location was removed")
188 .or_insert_with(|| {
189 spec().build(location_key, &self.location_names[location_key])
190 });
191 }
192 }
193 self
194 }
195
196 pub fn with_sidecar_all(mut self, sidecar: &impl Sidecar) -> Self {
198 for (location_key, &location_type) in self.locations.iter() {
199 if !matches!(location_type, LocationType::Process | LocationType::Cluster) {
200 continue;
201 }
202
203 let location_name = &self.location_names[location_key];
204
205 sidecar.edit_as_code_options(
206 self.as_code_options
207 .entry(location_key)
208 .expect("location was removed")
209 .or_default(),
210 );
211
212 let future_expr = sidecar.to_expr(
213 self.flow_name(),
214 location_key,
215 location_type,
216 location_name,
217 "e::format_ident!("{}", super::DFIR_IDENT),
218 );
219 self.sidecars.push(super::builder::Sidecar::Simple {
220 location_key,
221 future_expr: Box::new(future_expr),
222 });
223 }
224
225 self
226 }
227
228 pub fn with_sidecar_internal(
230 mut self,
231 location_key: LocationKey,
232 sidecar: &impl Sidecar,
233 ) -> Self {
234 let location_type = self.locations[location_key];
235 let location_name = &self.location_names[location_key];
236 sidecar.edit_as_code_options(
237 self.as_code_options
238 .entry(location_key)
239 .expect("location was removed")
240 .or_default(),
241 );
242 let future_expr = sidecar.to_expr(
243 self.flow_name(),
244 location_key,
245 location_type,
246 location_name,
247 "e::format_ident!("{}", super::DFIR_IDENT),
248 );
249 self.sidecars.push(super::builder::Sidecar::Simple {
250 location_key,
251 future_expr: Box::new(future_expr),
252 });
253 self
254 }
255
256 pub fn with_sidecar_process(self, process: &Process<'_, ()>, sidecar: &impl Sidecar) -> Self {
258 self.with_sidecar_internal(process.key, sidecar)
259 }
260
261 pub fn with_sidecar_cluster(self, cluster: &Cluster<'_, ()>, sidecar: &impl Sidecar) -> Self {
263 self.with_sidecar_internal(cluster.key, sidecar)
264 }
265
266 pub fn preview_compile(&mut self) -> CompiledFlow<'a> {
271 CompiledFlow {
274 dfir: build_inner(&mut self.ir),
275 extra_stmts: SparseSecondaryMap::new(),
276 sidecars: SparseSecondaryMap::new(),
277 as_code_options: SparseSecondaryMap::new(),
278 _phantom: PhantomData,
279 }
280 }
281
282 pub fn compile(mut self) -> CompiledFlow<'a>
286 where
287 D: Deploy<'a, InstantiateEnv = ()>,
288 {
289 self.compile_internal(&mut ())
290 }
291
292 pub(super) fn compile_internal(&mut self, env: &mut D::InstantiateEnv) -> CompiledFlow<'a> {
296 let mut seen_tees: HashMap<_, _> = HashMap::new();
297 let mut seen_cluster_members = HashSet::new();
298 let mut extra_stmts = SparseSecondaryMap::new();
299 for leaf in self.ir.iter_mut() {
300 leaf.compile_network::<D>(
301 &mut extra_stmts,
302 &mut seen_tees,
303 &mut seen_cluster_members,
304 &self.processes,
305 &self.clusters,
306 &self.externals,
307 env,
308 );
309 }
310
311 let mut sidecars: SparseSecondaryMap<LocationKey, Vec<syn::Expr>> =
314 SparseSecondaryMap::new();
315 for decl in std::mem::take(&mut self.sidecars) {
316 match decl {
317 super::builder::Sidecar::Simple {
318 location_key,
319 future_expr,
320 } => {
321 sidecars
322 .entry(location_key)
323 .expect("location was removed")
324 .or_default()
325 .push(*future_expr);
326 }
327 super::builder::Sidecar::Bidi {
328 location_key,
329 sidecar_id,
330 sidecar_closure,
331 } => {
332 use syn::parse_quote;
333
334 let (stream_ident, sink_ident) = sidecar_id.idents();
335
336 let sidecar_closure_expr: &syn::Expr = &sidecar_closure;
337 let setup_stmt: syn::Stmt = parse_quote! {
338 let (#stream_ident, #sink_ident) = (#sidecar_closure_expr)();
339 };
340 extra_stmts
341 .entry(location_key)
342 .expect("location was removed")
343 .or_default()
344 .push(setup_stmt);
345 }
346 }
347 }
348
349 CompiledFlow {
350 dfir: build_inner(&mut self.ir),
351 extra_stmts,
352 sidecars,
353 as_code_options: std::mem::take(&mut self.as_code_options),
354 _phantom: PhantomData,
355 }
356 }
357
358 fn cluster_id_stmts(&self, extra_stmts: &mut SparseSecondaryMap<LocationKey, Vec<syn::Stmt>>) {
360 #[expect(
361 clippy::disallowed_methods,
362 reason = "nondeterministic iteration order, will be sorted"
363 )]
364 let mut all_clusters_sorted = self.clusters.keys().collect::<Vec<_>>();
365 all_clusters_sorted.sort();
366
367 for cluster_key in all_clusters_sorted {
368 let self_id_ident = syn::Ident::new(
369 &format!("__hydro_lang_cluster_self_id_{}", cluster_key),
370 Span::call_site(),
371 );
372 let self_id_expr = D::cluster_self_id().splice_untyped();
373 extra_stmts
374 .entry(cluster_key)
375 .expect("location was removed")
376 .or_default()
377 .push(syn::parse_quote! {
378 let #self_id_ident = &*Box::leak(Box::new(#self_id_expr));
379 });
380
381 let process_cluster_locations = self.location_names.keys().filter(|&location_key| {
382 self.processes.contains_key(location_key)
383 || self.clusters.contains_key(location_key)
384 });
385 for other_location in process_cluster_locations {
386 let other_id_ident = syn::Ident::new(
387 &format!("__hydro_lang_cluster_ids_{}", cluster_key),
388 Span::call_site(),
389 );
390 let other_id_expr = D::cluster_ids(cluster_key).splice_untyped();
391 extra_stmts
392 .entry(other_location)
393 .expect("location was removed")
394 .or_default()
395 .push(syn::parse_quote! {
396 let #other_id_ident = #other_id_expr;
397 });
398 }
399 }
400 }
401
402 #[must_use]
410 pub fn deploy(mut self, env: &mut D::InstantiateEnv) -> DeployResult<'a, D> {
411 let CompiledFlow {
412 dfir,
413 mut extra_stmts,
414 mut sidecars,
415 mut as_code_options,
416 _phantom,
417 } = self.compile_internal(env);
418
419 let mut compiled = dfir;
420 self.cluster_id_stmts(&mut extra_stmts);
421 let mut meta = D::Meta::default();
422
423 let (processes, clusters, externals) = (
424 self.processes
425 .into_iter()
426 .filter(|&(node_key, ref node)| {
427 if let Some(ir) = compiled.remove(node_key) {
428 let ir = ir.unwrap_or_else(|err| {
429 panic!(
430 "Failed to partition DFIR graph for location {node_key}: {}",
431 err.diagnostic
432 )
433 });
434 node.instantiate(
435 env,
436 &mut meta,
437 ir,
438 extra_stmts.remove(node_key).as_deref().unwrap_or_default(),
439 sidecars.remove(node_key).as_deref().unwrap_or_default(),
440 &as_code_options.remove(node_key).unwrap_or_default(),
441 );
442 true
443 } else {
444 false
445 }
446 })
447 .collect::<SparseSecondaryMap<_, _>>(),
448 self.clusters
449 .into_iter()
450 .filter(|&(cluster_key, ref cluster)| {
451 if let Some(ir) = compiled.remove(cluster_key) {
452 let ir = ir.unwrap_or_else(|err| {
453 panic!(
454 "Failed to partition DFIR graph for location {cluster_key}: {}",
455 err.diagnostic
456 )
457 });
458 cluster.instantiate(
459 env,
460 &mut meta,
461 ir,
462 extra_stmts
463 .remove(cluster_key)
464 .as_deref()
465 .unwrap_or_default(),
466 sidecars.remove(cluster_key).as_deref().unwrap_or_default(),
467 &as_code_options.remove(cluster_key).unwrap_or_default(),
468 );
469 true
470 } else {
471 false
472 }
473 })
474 .collect::<SparseSecondaryMap<_, _>>(),
475 self.externals
476 .into_iter()
477 .inspect(|&(external_key, ref external)| {
478 assert!(!extra_stmts.contains_key(external_key));
479 assert!(!sidecars.contains_key(external_key));
480 assert!(!as_code_options.contains_key(external_key));
481 external.instantiate(
482 env,
483 &mut meta,
484 Default::default(),
485 &[],
486 &[],
487 &Default::default(),
488 );
489 })
490 .collect::<SparseSecondaryMap<_, _>>(),
491 );
492
493 for location_key in self.locations.keys() {
494 if let Some(node) = processes.get(location_key) {
495 node.update_meta(&meta);
496 } else if let Some(cluster) = clusters.get(location_key) {
497 cluster.update_meta(&meta);
498 } else if let Some(external) = externals.get(location_key) {
499 external.update_meta(&meta);
500 }
501 }
502
503 let mut seen_tees_connect = HashMap::new();
504 for leaf in self.ir.iter_mut() {
505 leaf.connect_network(&mut seen_tees_connect);
506 }
507
508 DeployResult {
509 location_names: self.location_names,
510 processes,
511 clusters,
512 externals,
513 }
514 }
515}
516
517pub struct DeployResult<'a, D: Deploy<'a>> {
518 location_names: SecondaryMap<LocationKey, String>,
519 processes: SparseSecondaryMap<LocationKey, D::Process>,
520 clusters: SparseSecondaryMap<LocationKey, D::Cluster>,
521 externals: SparseSecondaryMap<LocationKey, D::External>,
522}
523
524impl<'a, D: Deploy<'a>> DeployResult<'a, D> {
525 pub fn get_process<P>(&self, p: &Process<'_, P>) -> &D::Process {
526 let LocationId::Process(location_key) = p.id() else {
527 panic!("Process ID expected")
528 };
529 self.processes.get(location_key).unwrap()
530 }
531
532 pub fn get_cluster<C>(&self, c: &Cluster<'a, C>) -> &D::Cluster {
533 let LocationId::Cluster(location_key) = c.id() else {
534 panic!("Cluster ID expected")
535 };
536 self.clusters.get(location_key).unwrap()
537 }
538
539 pub fn get_external<P>(&self, e: &External<'_, P>) -> &D::External {
540 self.externals.get(e.key).unwrap()
541 }
542
543 pub fn get_all_processes(&self) -> impl Iterator<Item = (LocationId, &str, &D::Process)> {
544 self.location_names
545 .iter()
546 .filter_map(|(location_key, location_name)| {
547 self.processes
548 .get(location_key)
549 .map(|process| (LocationId::Process(location_key), &**location_name, process))
550 })
551 }
552
553 pub fn get_all_clusters(&self) -> impl Iterator<Item = (LocationId, &str, &D::Cluster)> {
554 self.location_names
555 .iter()
556 .filter_map(|(location_key, location_name)| {
557 self.clusters
558 .get(location_key)
559 .map(|cluster| (LocationId::Cluster(location_key), &**location_name, cluster))
560 })
561 }
562
563 #[deprecated(note = "use `connect` instead")]
564 pub async fn connect_bytes<M>(
565 &self,
566 port: ExternalBytesPort<M>,
567 ) -> (
568 Pin<Box<dyn Stream<Item = Result<BytesMut, Error>>>>,
569 Pin<Box<dyn Sink<Bytes, Error = Error>>>,
570 ) {
571 self.connect(port).await
572 }
573
574 #[deprecated(note = "use `connect` instead")]
575 pub async fn connect_sink_bytes<M>(
576 &self,
577 port: ExternalBytesPort<M>,
578 ) -> Pin<Box<dyn Sink<Bytes, Error = Error>>> {
579 self.connect(port).await.1
580 }
581
582 pub async fn connect_bincode<
583 InT: Serialize + 'static,
584 OutT: DeserializeOwned + 'static,
585 Many,
586 >(
587 &self,
588 port: ExternalBincodeBidi<InT, OutT, Many>,
589 ) -> (
590 Pin<Box<dyn Stream<Item = OutT>>>,
591 Pin<Box<dyn Sink<InT, Error = Error>>>,
592 ) {
593 self.externals
594 .get(port.process_key)
595 .unwrap()
596 .as_bincode_bidi(port.port_id)
597 .await
598 }
599
600 #[deprecated(note = "use `connect` instead")]
601 pub async fn connect_sink_bincode<T: Serialize + DeserializeOwned + 'static, Many>(
602 &self,
603 port: ExternalBincodeSink<T, Many>,
604 ) -> Pin<Box<dyn Sink<T, Error = Error>>> {
605 self.connect(port).await
606 }
607
608 #[deprecated(note = "use `connect` instead")]
609 pub async fn connect_source_bytes(
610 &self,
611 port: ExternalBytesPort,
612 ) -> Pin<Box<dyn Stream<Item = Result<BytesMut, Error>>>> {
613 self.connect(port).await.0
614 }
615
616 #[deprecated(note = "use `connect` instead")]
617 pub async fn connect_source_bincode<
618 T: Serialize + DeserializeOwned + 'static,
619 O: Ordering,
620 R: Retries,
621 >(
622 &self,
623 port: ExternalBincodeStream<T, O, R>,
624 ) -> Pin<Box<dyn Stream<Item = T>>> {
625 self.connect(port).await
626 }
627
628 pub async fn connect<'b, P: ConnectableAsync<&'b Self>>(
629 &'b self,
630 port: P,
631 ) -> <P as ConnectableAsync<&'b Self>>::Output {
632 port.connect(self).await
633 }
634}
635
636#[cfg(stageleft_runtime)]
637#[cfg(feature = "deploy")]
638#[cfg_attr(docsrs, doc(cfg(feature = "deploy")))]
639impl DeployResult<'_, crate::deploy::HydroDeploy> {
640 pub fn raw_port<M>(
642 &self,
643 port: ExternalBytesPort<M>,
644 ) -> hydro_deploy::custom_service::CustomClientPort {
645 self.externals
646 .get(port.process_key)
647 .unwrap()
648 .raw_port(port.port_id)
649 }
650}
651
652pub trait ConnectableAsync<Ctx> {
653 type Output;
654
655 fn connect(self, ctx: Ctx) -> impl Future<Output = Self::Output>;
656}
657
658impl<'a, D: Deploy<'a>, M> ConnectableAsync<&DeployResult<'a, D>> for ExternalBytesPort<M> {
659 type Output = (
660 Pin<Box<dyn Stream<Item = Result<BytesMut, Error>>>>,
661 Pin<Box<dyn Sink<Bytes, Error = Error>>>,
662 );
663
664 async fn connect(self, ctx: &DeployResult<'a, D>) -> Self::Output {
665 ctx.externals
666 .get(self.process_key)
667 .unwrap()
668 .as_bytes_bidi(self.port_id)
669 .await
670 }
671}
672
673impl<'a, D: Deploy<'a>, T: DeserializeOwned + 'static, O: Ordering, R: Retries>
674 ConnectableAsync<&DeployResult<'a, D>> for ExternalBincodeStream<T, O, R>
675{
676 type Output = Pin<Box<dyn Stream<Item = T>>>;
677
678 async fn connect(self, ctx: &DeployResult<'a, D>) -> Self::Output {
679 ctx.externals
680 .get(self.process_key)
681 .unwrap()
682 .as_bincode_source(self.port_id)
683 .await
684 }
685}
686
687impl<'a, D: Deploy<'a>, T: Serialize + 'static, Many> ConnectableAsync<&DeployResult<'a, D>>
688 for ExternalBincodeSink<T, Many>
689{
690 type Output = Pin<Box<dyn Sink<T, Error = Error>>>;
691
692 async fn connect(self, ctx: &DeployResult<'a, D>) -> Self::Output {
693 ctx.externals
694 .get(self.process_key)
695 .unwrap()
696 .as_bincode_sink(self.port_id)
697 .await
698 }
699}