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