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