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 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    /// Deployed instances of each process in the flow
40    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    /// Compile-time sidecar directives (both simple futures and external TCP sidecars).
45    pub(super) sidecars: Vec<super::builder::Sidecar>,
46
47    /// Clusters whose code reads their own member ID.
48    pub(super) clusters_reading_self_id: HashSet<LocationKey>,
49
50    /// Per-location codegen options, edited by sidecars via [`Sidecar::edit_as_code_options`].
51    /// Locations without an entry use [`AsCodeOptions::default`].
52    pub(super) as_code_options: SparseSecondaryMap<LocationKey, AsCodeOptions>,
53
54    /// Application name used in telemetry.
55    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    /// Application name used in telemetry.
66    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    /// TODO(mingwei): unstable API
84    #[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    /// TODO(mingwei): unstable API
135    #[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    /// Adds a [`Sidecar`] to all processes and clusters in the flow.
200    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                &quote::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    /// Adds a [`Sidecar`] to the given location.
232    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            &quote::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    /// Adds a [`Sidecar`] to a specific process in the flow.
260    pub fn with_sidecar_process(self, process: &Process<'_, ()>, sidecar: &impl Sidecar) -> Self {
261        self.with_sidecar_internal(process.key, sidecar)
262    }
263
264    /// Adds a [`Sidecar`] to a specific cluster in the flow.
265    pub fn with_sidecar_cluster(self, cluster: &Cluster<'_, ()>, sidecar: &impl Sidecar) -> Self {
266        self.with_sidecar_internal(cluster.key, sidecar)
267    }
268
269    /// Compiles the flow into DFIR ([`dfir_lang::graph::DfirGraph`]) without networking.
270    /// Useful for generating Mermaid diagrams of the DFIR.
271    ///
272    /// (This returned DFIR will not compile due to the networking missing).
273    pub fn preview_compile(&mut self) -> CompiledFlow<'a> {
274        // NOTE: `build_inner` does not actually mutate the IR, but `&mut` is required
275        // only because the shared traversal logic requires it
276        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    /// Compiles the flow into DFIR ([`dfir_lang::graph::DfirGraph`]) including networking.
286    ///
287    /// (This does not compile the DFIR itself, instead use [`Self::deploy`] to compile & deploy the DFIR).
288    pub fn compile(mut self) -> CompiledFlow<'a>
289    where
290        D: Deploy<'a, InstantiateEnv = ()>,
291    {
292        self.compile_internal(&mut ())
293    }
294
295    /// Same as [`Self::compile`] but does not invalidate `self`, for internal use.
296    ///
297    /// Empties `self.sidecars` and `self.as_code_options` and modifies `self.ir`, leaving `self` in a partial state.
298    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        // Process sidecar declarations — compile-time directives that
315        // produce futures to spawn on each location's LocalSet.
316        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    /// Creates the variables for cluster IDs and adds them into `extra_stmts`.
362    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    /// Compiles and deploys the flow.
406    ///
407    /// Rough outline of steps:
408    /// * Compiles the Hydro into DFIR.
409    /// * Instantiates nodes as configured.
410    /// * Compiles the corresponding DFIR into binaries for nodes as needed.
411    /// * Connects up networking as needed.
412    #[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    /// Get the raw port handle.
644    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}