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