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