Skip to main content

hydro_lang/compile/
deploy.rs

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    /// Deployed instances of each process in the flow
39    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    /// Compile-time sidecar directives (both simple futures and external TCP sidecars).
44    pub(super) sidecars: Vec<super::builder::Sidecar>,
45
46    /// Application name used in telemetry.
47    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    /// Application name used in telemetry.
58    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    /// TODO(mingwei): unstable API
76    #[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    /// TODO(mingwei): unstable API
127    #[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    /// Adds a [`Sidecar`] to all processes and clusters in the flow.
192    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                &quote::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    /// Adds a [`Sidecar`] to the given location.
217    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            &quote::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    /// Adds a [`Sidecar`] to a specific process in the flow.
239    pub fn with_sidecar_process(self, process: &Process<'_, ()>, sidecar: &impl Sidecar) -> Self {
240        self.with_sidecar_internal(process.key, sidecar)
241    }
242
243    /// Adds a [`Sidecar`] to a specific cluster in the flow.
244    pub fn with_sidecar_cluster(self, cluster: &Cluster<'_, ()>, sidecar: &impl Sidecar) -> Self {
245        self.with_sidecar_internal(cluster.key, sidecar)
246    }
247
248    /// Compiles the flow into DFIR ([`dfir_lang::graph::DfirGraph`]) without networking.
249    /// Useful for generating Mermaid diagrams of the DFIR.
250    ///
251    /// (This returned DFIR will not compile due to the networking missing).
252    pub fn preview_compile(&mut self) -> CompiledFlow<'a> {
253        // NOTE: `build_inner` does not actually mutate the IR, but `&mut` is required
254        // only because the shared traversal logic requires it
255        CompiledFlow {
256            dfir: build_inner(&mut self.ir),
257            extra_stmts: SparseSecondaryMap::new(),
258            sidecars: SparseSecondaryMap::new(),
259            _phantom: PhantomData,
260        }
261    }
262
263    /// Compiles the flow into DFIR ([`dfir_lang::graph::DfirGraph`]) including networking.
264    ///
265    /// (This does not compile the DFIR itself, instead use [`Self::deploy`] to compile & deploy the DFIR).
266    pub fn compile(mut self) -> CompiledFlow<'a>
267    where
268        D: Deploy<'a, InstantiateEnv = ()>,
269    {
270        self.compile_internal(&mut ())
271    }
272
273    /// Same as [`Self::compile`] but does not invalidate `self`, for internal use.
274    ///
275    /// Empties `self.sidecars` and modifies `self.ir`, leaving `self` in a partial state.
276    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        // Process sidecar declarations — compile-time directives that
293        // produce futures to spawn on each location's LocalSet.
294        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    /// Creates the variables for cluster IDs and adds them into `extra_stmts`.
339    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    /// Compiles and deploys the flow.
383    ///
384    /// Rough outline of steps:
385    /// * Compiles the Hydro into DFIR.
386    /// * Instantiates nodes as configured.
387    /// * Compiles the corresponding DFIR into binaries for nodes as needed.
388    /// * Connects up networking as needed.
389    #[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    /// Get the raw port handle.
610    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}