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