Skip to main content

hydro_lang/live_collections/stream/
networking.rs

1//! Networking APIs for [`Stream`].
2
3use std::marker::PhantomData;
4
5use serde::Serialize;
6use serde::de::DeserializeOwned;
7use stageleft::{q, quote_type};
8use syn::parse_quote;
9
10use super::{ExactlyOnce, MinOrder, MinRetries, Ordering, Stream, TotalOrder};
11use crate::compile::builder::ExternalPortId;
12use crate::compile::ir::{
13    DebugInstantiate, HydroIrOpMetadata, HydroNode, HydroRoot, NetworkRecv, NetworkSend,
14};
15use crate::live_collections::boundedness::{Boundedness, Unbounded};
16use crate::live_collections::keyed_singleton::{KeyedSingleton, MonotonicKeys};
17use crate::live_collections::keyed_stream::KeyedStream;
18use crate::live_collections::sliced::sliced;
19use crate::live_collections::stream::Retries;
20#[cfg(feature = "sim")]
21use crate::location::LocationKey;
22use crate::location::cluster::{ClusterIds, Consistency, NoConsistency};
23#[cfg(stageleft_runtime)]
24use crate::location::dynamic::DynLocation;
25use crate::location::external_process::ExternalBincodeStream;
26use crate::location::{Cluster, External, Location, MemberId, MembershipEvent, Process};
27use crate::networking::{NetworkFor, TCP};
28use crate::nondet::{NonDet, nondet};
29use crate::properties::manual_proof;
30#[cfg(feature = "sim")]
31use crate::sim::SimReceiver;
32use crate::staging_util::get_this_crate;
33
34// same as the one in `hydro_std`, but internal use only
35fn track_membership<'a, C, L: Location<'a>>(
36    membership: KeyedStream<MemberId<C>, MembershipEvent, L, Unbounded>,
37) -> KeyedSingleton<MemberId<C>, bool, L, MonotonicKeys> {
38    membership.fold(
39        q!(|| false),
40        q!(|present, event| {
41            match event {
42                MembershipEvent::Joined => *present = true,
43                MembershipEvent::Left => *present = false,
44            }
45        }),
46    )
47}
48
49fn serialize_bincode_with_type(is_demux: bool, t_type: &syn::Type) -> syn::Expr {
50    let root = get_this_crate();
51
52    if is_demux {
53        parse_quote! {
54            #root::runtime_support::stageleft::runtime_support::fn1_type_hint::<(#root::__staged::location::MemberId<_>, #t_type), _>(
55                |(id, data)| {
56                    (id.into_tagless(), #root::runtime_support::bincode::serialize(&data).unwrap().into())
57                }
58            )
59        }
60    } else {
61        parse_quote! {
62            #root::runtime_support::stageleft::runtime_support::fn1_type_hint::<#t_type, _>(
63                |data| {
64                    #root::runtime_support::bincode::serialize(&data).unwrap().into()
65                }
66            )
67        }
68    }
69}
70
71pub(crate) fn serialize_bincode<T: Serialize>(is_demux: bool) -> syn::Expr {
72    serialize_bincode_with_type(is_demux, &quote_type::<T>())
73}
74
75fn deserialize_bincode_with_type(tagged: Option<&syn::Type>, t_type: &syn::Type) -> syn::Expr {
76    let root = get_this_crate();
77    if let Some(c_type) = tagged {
78        parse_quote! {
79            |res| {
80                let (id, b) = res.unwrap();
81                (#root::__staged::location::MemberId::<#c_type>::from_tagless(id as #root::__staged::location::TaglessMemberId), #root::runtime_support::bincode::deserialize::<#t_type>(&b).unwrap())
82            }
83        }
84    } else {
85        parse_quote! {
86            |res| {
87                #root::runtime_support::bincode::deserialize::<#t_type>(&res.unwrap()).unwrap()
88            }
89        }
90    }
91}
92
93pub(crate) fn deserialize_bincode<T: DeserializeOwned>(tagged: Option<&syn::Type>) -> syn::Expr {
94    deserialize_bincode_with_type(tagged, &quote_type::<T>())
95}
96
97impl<'a, T, L, B: Boundedness, O: Ordering, R: Retries> Stream<T, Process<'a, L>, B, O, R> {
98    #[deprecated = "use Stream::send(..., TCP.fail_stop().bincode()) instead"]
99    /// "Moves" elements of this stream to a new distributed location by sending them over the network,
100    /// using [`bincode`] to serialize/deserialize messages.
101    ///
102    /// The returned stream captures the elements received at the destination, where values will
103    /// asynchronously arrive over the network. Sending from a [`Process`] to another [`Process`]
104    /// preserves ordering and retries guarantees by using a single TCP channel to send the values. The
105    /// recipient is guaranteed to receive a _prefix_ or the sent messages; if the TCP connection is
106    /// dropped no further messages will be sent.
107    ///
108    /// # Example
109    /// ```rust
110    /// # #[cfg(feature = "deploy")] {
111    /// # use hydro_lang::prelude::*;
112    /// # use futures::StreamExt;
113    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p_out| {
114    /// let p1 = flow.process::<()>();
115    /// let numbers: Stream<_, Process<_>, Bounded> = p1.source_iter(q!(vec![1, 2, 3]));
116    /// let p2 = flow.process::<()>();
117    /// let on_p2: Stream<_, Process<_>, Unbounded> = numbers.send_bincode(&p2);
118    /// // 1, 2, 3
119    /// # on_p2.send_bincode(&p_out)
120    /// # }, |mut stream| async move {
121    /// # for w in 1..=3 {
122    /// #     assert_eq!(stream.next().await, Some(w));
123    /// # }
124    /// # }));
125    /// # }
126    /// ```
127    pub fn send_bincode<L2>(
128        self,
129        other: &Process<'a, L2>,
130    ) -> Stream<T, Process<'a, L2>, Unbounded, O, R>
131    where
132        T: Serialize + DeserializeOwned,
133    {
134        self.send(other, TCP.fail_stop().bincode())
135    }
136
137    /// "Moves" elements of this stream to a new distributed location by sending them over the network,
138    /// using the configuration in `via` to set up the message transport.
139    ///
140    /// The returned stream captures the elements received at the destination, where values will
141    /// asynchronously arrive over the network. Sending from a [`Process`] to another [`Process`]
142    /// preserves ordering and retries guarantees when using a single TCP channel to send the values.
143    /// The recipient is guaranteed to receive a _prefix_ or the sent messages; if the connection is
144    /// dropped no further messages will be sent.
145    ///
146    /// # Example
147    /// ```rust
148    /// # #[cfg(feature = "deploy")] {
149    /// # use hydro_lang::prelude::*;
150    /// # use futures::StreamExt;
151    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p_out| {
152    /// let p1 = flow.process::<()>();
153    /// let numbers: Stream<_, Process<_>, Bounded> = p1.source_iter(q!(vec![1, 2, 3]));
154    /// let p2 = flow.process::<()>();
155    /// let on_p2: Stream<_, Process<_>, Unbounded> = numbers.send(&p2, TCP.fail_stop().bincode());
156    /// // 1, 2, 3
157    /// # on_p2.send(&p_out, TCP.fail_stop().bincode())
158    /// # }, |mut stream| async move {
159    /// # for w in 1..=3 {
160    /// #     assert_eq!(stream.next().await, Some(w));
161    /// # }
162    /// # }));
163    /// # }
164    /// ```
165    pub fn send<L2, N: NetworkFor<T>>(
166        self,
167        to: &Process<'a, L2>,
168        via: N,
169    ) -> Stream<
170        T,
171        Process<'a, L2>,
172        Unbounded,
173        <O as MinOrder<N::OrderingGuarantee>>::Min,
174        <R as MinRetries<N::RetriesGuarantee>>::Min,
175    >
176    where
177        O: MinOrder<N::OrderingGuarantee>,
178        R: MinRetries<N::RetriesGuarantee>,
179    {
180        let name = via.name();
181        assert!(
182            !to.multiversioned() || name.is_some(),
183            "Cannot send to a multiversioned location without a channel name. Please provide a name for the network."
184        );
185
186        let (serialize, deserialize) = if N::is_embedded() {
187            (
188                NetworkSend::Embedded {
189                    tag: None,
190                    element_type: quote_type::<T>().into(),
191                },
192                NetworkRecv::Embedded {
193                    tag: None,
194                    element_type: quote_type::<T>().into(),
195                },
196            )
197        } else {
198            (
199                NetworkSend::Custom {
200                    serialize_fn: Some(N::serialize_thunk(false).into()),
201                },
202                NetworkRecv::Custom {
203                    deserialize_fn: Some(N::deserialize_thunk(None).into()),
204                },
205            )
206        };
207
208        Stream::new(
209            to.clone(),
210            HydroNode::Network {
211                name: name.map(ToOwned::to_owned),
212                networking_info: N::networking_info(),
213                serialize,
214                deserialize,
215                instantiate_fn: DebugInstantiate::Building,
216                input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
217                metadata: to.new_node_metadata(Stream::<
218                    T,
219                    Process<'a, L2>,
220                    Unbounded,
221                    <O as MinOrder<N::OrderingGuarantee>>::Min,
222                    <R as MinRetries<N::RetriesGuarantee>>::Min,
223                >::collection_kind()),
224            },
225        )
226    }
227
228    #[deprecated = "use Stream::broadcast(..., TCP.fail_stop().bincode()) instead"]
229    /// Broadcasts elements of this stream to all members of a cluster by sending them over the network,
230    /// using [`bincode`] to serialize/deserialize messages.
231    ///
232    /// Each element in the stream will be sent to **every** member of the cluster based on the latest
233    /// membership information. This is a common pattern in distributed systems for broadcasting data to
234    /// all nodes in a cluster. Unlike [`Stream::demux_bincode`], which requires `(MemberId, T)` tuples to
235    /// target specific members, `broadcast_bincode` takes a stream of **only data elements** and sends
236    /// each element to all cluster members.
237    ///
238    /// # Non-Determinism
239    /// The set of cluster members may asynchronously change over time. Each element is only broadcast
240    /// to the current cluster members _at that point in time_. Depending on when we are notified of
241    /// membership changes, we will broadcast each element to different members.
242    ///
243    /// # Example
244    /// ```rust
245    /// # #[cfg(feature = "deploy")] {
246    /// # use hydro_lang::prelude::*;
247    /// # use futures::StreamExt;
248    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
249    /// let p1 = flow.process::<()>();
250    /// let workers: Cluster<()> = flow.cluster::<()>();
251    /// let numbers: Stream<_, Process<_>, _> = p1.source_iter(q!(vec![123]));
252    /// let on_worker: Stream<_, Cluster<_>, _> = numbers.broadcast_bincode(&workers, nondet!(/** assuming stable membership */));
253    /// # on_worker.send_bincode(&p2).entries()
254    /// // if there are 4 members in the cluster, each receives one element
255    /// // - MemberId::<()>(0): [123]
256    /// // - MemberId::<()>(1): [123]
257    /// // - MemberId::<()>(2): [123]
258    /// // - MemberId::<()>(3): [123]
259    /// # }, |mut stream| async move {
260    /// # let mut results = Vec::new();
261    /// # for w in 0..4 {
262    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
263    /// # }
264    /// # results.sort();
265    /// # assert_eq!(results, vec!["(MemberId::<()>(0), 123)", "(MemberId::<()>(1), 123)", "(MemberId::<()>(2), 123)", "(MemberId::<()>(3), 123)"]);
266    /// # }));
267    /// # }
268    /// ```
269    pub fn broadcast_bincode<L2: 'a>(
270        self,
271        other: &Cluster<'a, L2>,
272        nondet_membership: NonDet,
273    ) -> Stream<T, Cluster<'a, L2>, Unbounded, O, R>
274    where
275        T: Clone + Serialize + DeserializeOwned,
276    {
277        self.broadcast(other, TCP.fail_stop().bincode(), nondet_membership)
278    }
279
280    /// Broadcasts elements of this stream to all members of a cluster by sending them over the network,
281    /// using the configuration in `via` to set up the message transport.
282    ///
283    /// Each element in the stream will be sent to **every** member of the cluster based on the latest
284    /// membership information. This is a common pattern in distributed systems for broadcasting data to
285    /// all nodes in a cluster. Unlike [`Stream::demux`], which requires `(MemberId, T)` tuples to
286    /// target specific members, `broadcast` takes a stream of **only data elements** and sends
287    /// each element to all cluster members.
288    ///
289    /// # Non-Determinism
290    /// The set of cluster members may asynchronously change over time. Each element is only broadcast
291    /// to the current cluster members _at that point in time_. Depending on when we are notified of
292    /// membership changes, we will broadcast each element to different members.
293    ///
294    /// # Example
295    /// ```rust
296    /// # #[cfg(feature = "deploy")] {
297    /// # use hydro_lang::prelude::*;
298    /// # use futures::StreamExt;
299    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
300    /// let p1 = flow.process::<()>();
301    /// let workers: Cluster<()> = flow.cluster::<()>();
302    /// let numbers: Stream<_, Process<_>, _> = p1.source_iter(q!(vec![123]));
303    /// let on_worker: Stream<_, Cluster<_>, _> = numbers.broadcast(&workers, TCP.fail_stop().bincode(), nondet!(/** assuming stable membership */));
304    /// # on_worker.send(&p2, TCP.fail_stop().bincode()).entries()
305    /// // if there are 4 members in the cluster, each receives one element
306    /// // - MemberId::<()>(0): [123]
307    /// // - MemberId::<()>(1): [123]
308    /// // - MemberId::<()>(2): [123]
309    /// // - MemberId::<()>(3): [123]
310    /// # }, |mut stream| async move {
311    /// # let mut results = Vec::new();
312    /// # for w in 0..4 {
313    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
314    /// # }
315    /// # results.sort();
316    /// # assert_eq!(results, vec!["(MemberId::<()>(0), 123)", "(MemberId::<()>(1), 123)", "(MemberId::<()>(2), 123)", "(MemberId::<()>(3), 123)"]);
317    /// # }));
318    /// # }
319    /// ```
320    pub fn broadcast<L2: 'a, N: NetworkFor<T>>(
321        self,
322        to: &Cluster<'a, L2>,
323        via: N,
324        nondet_membership: NonDet,
325    ) -> Stream<
326        T,
327        Cluster<'a, L2>,
328        Unbounded,
329        <O as MinOrder<N::OrderingGuarantee>>::Min,
330        <R as MinRetries<N::RetriesGuarantee>>::Min,
331    >
332    where
333        T: Clone,
334        O: MinOrder<N::OrderingGuarantee>,
335        R: MinRetries<N::RetriesGuarantee>,
336    {
337        // TODO(#1875): the membership snapshot below is over a `KeyedSingleton`, and keyed
338        // sim hooks do not exist yet. Once they do, expose a composite hook payload here
339        // (`NonDet<(Option<KeyedSnapshotHook<..>>, Option<BatchHook<T, O, R>>)>`) so tests
340        // can script the membership snapshot and the element batching independently.
341        let ids = track_membership(self.location.source_cluster_membership_stream(
342            to,
343            nondet!(/** dropped prefixes don't affect broadcast */),
344        ));
345        sliced! {
346            let members_snapshot = use::snapshot(ids, nondet!(
347                /// membership timing is captured by the caller's guard
348                nondet_membership
349            ));
350            let elements = use::batch(self, nondet!(
351                /// batching timing is captured by the caller's guard
352                nondet_membership
353            ));
354
355            let current_members = members_snapshot.filter(q!(|b| *b));
356            elements.repeat_with_keys(current_members)
357        }
358        .demux(to, via)
359    }
360
361    /// Broadcasts elements of this stream to all members of a cluster,
362    /// assuming membership is closed (fixed at deploy time).
363    ///
364    /// Unlike [`Stream::broadcast`], this does not require a [`NonDet`] guard.
365    /// The membership set is obtained from deploy metadata via
366    /// [`ClusterIds`], producing a
367    /// `Bounded` stream. The cross-product of data × members is fully
368    /// deterministic.
369    ///
370    /// The consistency guarantee of the output depends on the network's failure policy
371    /// ([`NetworkFor::ConsistencyGuarantee`]). Policies like `fail_stop` and
372    /// `lossy_delayed_forever` guarantee that every live member eventually materializes the same
373    /// elements, so the output is
374    /// [`EventualConsistency`](crate::location::cluster::EventualConsistency). A plain `lossy`
375    /// policy can drop individual messages for some members while delivering them to others, so
376    /// replicas may permanently diverge and the output only has
377    /// [`NoConsistency`].
378    ///
379    /// This is only available in deployment targets with static cluster
380    /// membership (legacy Hydro Deploy and simulation). There are no late
381    /// joiners in that context, so broadcast receivers are guaranteed to
382    /// get data from the start of the stream. On dynamic targets
383    /// (e.g. ECS), use [`Stream::broadcast`] instead.
384    ///
385    /// # Example
386    /// ```rust
387    /// # #[cfg(feature = "deploy")] {
388    /// # use hydro_lang::prelude::*;
389    /// # use futures::StreamExt;
390    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
391    /// let p1 = flow.process::<()>();
392    /// let workers: Cluster<()> = flow.cluster::<()>();
393    /// let numbers: Stream<_, Process<_>, _> = p1.source_iter(q!(vec![123]));
394    /// let on_worker = numbers.broadcast_closed(&workers, TCP.fail_stop().bincode());
395    /// # on_worker.send(&p2, TCP.fail_stop().bincode()).entries()
396    /// // each of the 4 cluster members receives 123
397    /// # }, |mut stream| async move {
398    /// # let mut results = Vec::new();
399    /// # for _ in 0..4 {
400    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
401    /// # }
402    /// # results.sort();
403    /// # assert_eq!(results, vec!["(MemberId::<()>(0), 123)", "(MemberId::<()>(1), 123)", "(MemberId::<()>(2), 123)", "(MemberId::<()>(3), 123)"]);
404    /// # }));
405    /// # }
406    /// ```
407    pub fn broadcast_closed<L2: 'a, N: NetworkFor<T>>(
408        self,
409        to: &Cluster<'a, L2>,
410        via: N,
411    ) -> Stream<
412        T,
413        Cluster<'a, L2, N::ConsistencyGuarantee>,
414        Unbounded,
415        <O as MinOrder<N::OrderingGuarantee>>::Min,
416        <R as MinRetries<N::RetriesGuarantee>>::Min,
417    >
418    where
419        T: Clone,
420        O: MinOrder<N::OrderingGuarantee>,
421        R: MinRetries<N::RetriesGuarantee>,
422    {
423        let cluster_ids = ClusterIds {
424            key: to.key,
425            _phantom: PhantomData,
426        };
427        let member_ids = self.location.source_iter(q!(cluster_ids
428            .iter()
429            .map(|id| MemberId::from_tagless(id.clone()))));
430
431        // Late joiners will receive no data from this broadcast, which is
432        // future-monotone and eventually consistent (a safe under-approximation).
433        self.cross_product(member_ids)
434            .map(q!(|(data, member_id)| (member_id, data)))
435            .into_keyed()
436            .demux(to, via)
437            .assert_has_consistency_of_trusted(manual_proof!(
438                /// With a network whose failure policy delivers the same messages to every live
439                /// member (tracked by `NetworkFor::ConsistencyGuarantee`), a closed broadcast
440                /// will materialize the same elements on each member.
441            ))
442    }
443
444    /// Sends the elements of this stream to an external (non-Hydro) process, using [`bincode`]
445    /// serialization. The external process can receive these elements by establishing a TCP
446    /// connection and decoding using [`tokio_util::codec::LengthDelimitedCodec`].
447    ///
448    /// # Example
449    /// ```rust
450    /// # #[cfg(feature = "deploy")] {
451    /// # use hydro_lang::prelude::*;
452    /// # use futures::StreamExt;
453    /// # tokio_test::block_on(async move {
454    /// let mut flow = FlowBuilder::new();
455    /// let process = flow.process::<()>();
456    /// let numbers: Stream<_, Process<_>, Bounded> = process.source_iter(q!(vec![1, 2, 3]));
457    /// let external = flow.external::<()>();
458    /// let external_handle = numbers.send_bincode_external(&external);
459    ///
460    /// let mut deployment = hydro_deploy::Deployment::new();
461    /// let nodes = flow
462    ///     .with_process(&process, deployment.Localhost())
463    ///     .with_external(&external, deployment.Localhost())
464    ///     .deploy(&mut deployment);
465    ///
466    /// deployment.deploy().await.unwrap();
467    /// // establish the TCP connection
468    /// let mut external_recv_stream = nodes.connect(external_handle).await;
469    /// deployment.start().await.unwrap();
470    ///
471    /// for w in 1..=3 {
472    ///     assert_eq!(external_recv_stream.next().await, Some(w));
473    /// }
474    /// # });
475    /// # }
476    /// ```
477    pub fn send_bincode_external<L2>(
478        self,
479        other: &External<'_, L2>,
480    ) -> ExternalBincodeStream<T, O, R>
481    where
482        T: Serialize + DeserializeOwned,
483    {
484        let external_port_id =
485            self.register_serialized_external_port(other, serialize_bincode::<T>(false));
486
487        ExternalBincodeStream {
488            process_key: other.key,
489            port_id: external_port_id,
490            _phantom: PhantomData,
491        }
492    }
493
494    // TODO: Add a codec-parameterized external stream handle once deployment supports custom codecs.
495    fn register_serialized_external_port<L2>(
496        self,
497        other: &External<'_, L2>,
498        serialize_pipeline: syn::Expr,
499    ) -> ExternalPortId {
500        let mut flow_state_borrow = self.location.flow_state().borrow_mut();
501
502        let external_port_id = flow_state_borrow.next_external_port();
503
504        flow_state_borrow.push_root(HydroRoot::SendExternal {
505            to_external_key: other.key,
506            to_port_id: external_port_id,
507            to_many: false,
508            unpaired: true,
509            serialize_fn: Some(serialize_pipeline.into()),
510            instantiate_fn: DebugInstantiate::Building,
511            input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
512            op_metadata: HydroIrOpMetadata::new(),
513        });
514
515        external_port_id
516    }
517
518    #[cfg(feature = "sim")]
519    /// Sets up a MessagePack-encoded simulation output port for this stream, allowing test code to
520    /// receive elements sent to this stream during simulation. Use [`Stream::sim_output_with`] to
521    /// select another codec.
522    pub fn sim_output(self) -> SimReceiver<T, O, R>
523    where
524        T: Serialize + DeserializeOwned,
525    {
526        self.sim_output_with::<crate::sim::codec::RmpCodec>()
527    }
528
529    #[cfg(feature = "sim")]
530    /// Sets up a simulation output port using the codec `C`, allowing test code to receive
531    /// elements sent to this stream during simulation: `stream.sim_output_with::<MyCodec>()`.
532    /// Custom codecs implement [`SimCodec`](crate::sim::codec::SimCodec), which documents
533    /// where they must be defined.
534    pub fn sim_output_with<C>(self) -> SimReceiver<T, O, R>
535    where
536        C: crate::sim::codec::SimCodec<T>,
537    {
538        let external_location: External<'a, ()> = External {
539            key: LocationKey::FIRST,
540            flow_state: self.location.flow_state().clone(),
541            _phantom: PhantomData,
542        };
543
544        let external_port_id = self.register_serialized_external_port(
545            &external_location,
546            crate::sim::codec::staged_serialize::<T, C>(),
547        );
548
549        SimReceiver(external_port_id, PhantomData, C::decode)
550    }
551}
552
553impl<'a, T, L: Location<'a>, B: Boundedness, O: Ordering, R: Retries> Stream<T, L, B, O, R> {
554    /// Creates an external output for embedded deployment mode.
555    ///
556    /// The `name` parameter specifies the name of the field in the generated
557    /// `EmbeddedOutputs` struct that will receive elements from this stream: an
558    /// [`EmbeddedOutputChannel`](crate::compile::embedded_runtime::EmbeddedOutputChannel) around the host's
559    /// `FnMut(T)` callback. The stream's ordering `O` and retries `R` are what the host
560    /// receives; the host must acknowledge each way they may be weaker than `TotalOrder` and
561    /// `ExactlyOnce`, or it does not compile.
562    pub fn embedded_output(self, name: impl Into<String>) {
563        let ident = syn::Ident::new(&name.into(), proc_macro2::Span::call_site());
564
565        self.location
566            .flow_state()
567            .borrow_mut()
568            .push_root(HydroRoot::EmbeddedOutput {
569                ident,
570                input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
571                op_metadata: HydroIrOpMetadata::new(),
572            });
573    }
574}
575
576impl<'a, T, L, L2, B: Boundedness, O: Ordering, R: Retries>
577    Stream<(MemberId<L2>, T), Process<'a, L>, B, O, R>
578{
579    #[deprecated = "use Stream::demux(..., TCP.fail_stop().bincode()) instead"]
580    /// Sends elements of this stream to specific members of a cluster, identified by a [`MemberId`],
581    /// using [`bincode`] to serialize/deserialize messages.
582    ///
583    /// Each element in the stream must be a tuple `(MemberId<L2>, T)` where the first element
584    /// specifies which cluster member should receive the data. Unlike [`Stream::broadcast_bincode`],
585    /// this API allows precise targeting of specific cluster members rather than broadcasting to
586    /// all members.
587    ///
588    /// # Example
589    /// ```rust
590    /// # #[cfg(feature = "deploy")] {
591    /// # use hydro_lang::prelude::*;
592    /// # use futures::StreamExt;
593    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
594    /// let p1 = flow.process::<()>();
595    /// let workers: Cluster<()> = flow.cluster::<()>();
596    /// let numbers: Stream<_, Process<_>, _> = p1.source_iter(q!(vec![0, 1, 2, 3]));
597    /// let on_worker: Stream<_, Cluster<_>, _> = numbers
598    ///     .map(q!(|x| (hydro_lang::location::MemberId::from_raw_id(x), x)))
599    ///     .demux_bincode(&workers);
600    /// # on_worker.send_bincode(&p2).entries()
601    /// // if there are 4 members in the cluster, each receives one element
602    /// // - MemberId::<()>(0): [0]
603    /// // - MemberId::<()>(1): [1]
604    /// // - MemberId::<()>(2): [2]
605    /// // - MemberId::<()>(3): [3]
606    /// # }, |mut stream| async move {
607    /// # let mut results = Vec::new();
608    /// # for w in 0..4 {
609    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
610    /// # }
611    /// # results.sort();
612    /// # assert_eq!(results, vec!["(MemberId::<()>(0), 0)", "(MemberId::<()>(1), 1)", "(MemberId::<()>(2), 2)", "(MemberId::<()>(3), 3)"]);
613    /// # }));
614    /// # }
615    /// ```
616    pub fn demux_bincode(
617        self,
618        other: &Cluster<'a, L2>,
619    ) -> Stream<T, Cluster<'a, L2>, Unbounded, O, R>
620    where
621        T: Serialize + DeserializeOwned,
622    {
623        self.demux(other, TCP.fail_stop().bincode())
624    }
625
626    /// Sends elements of this stream to specific members of a cluster, identified by a [`MemberId`],
627    /// using the configuration in `via` to set up the message transport.
628    ///
629    /// Each element in the stream must be a tuple `(MemberId<L2>, T)` where the first element
630    /// specifies which cluster member should receive the data. Unlike [`Stream::broadcast`],
631    /// this API allows precise targeting of specific cluster members rather than broadcasting to
632    /// all members.
633    ///
634    /// # Example
635    /// ```rust
636    /// # #[cfg(feature = "deploy")] {
637    /// # use hydro_lang::prelude::*;
638    /// # use futures::StreamExt;
639    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
640    /// let p1 = flow.process::<()>();
641    /// let workers: Cluster<()> = flow.cluster::<()>();
642    /// let numbers: Stream<_, Process<_>, _> = p1.source_iter(q!(vec![0, 1, 2, 3]));
643    /// let on_worker: Stream<_, Cluster<_>, _> = numbers
644    ///     .map(q!(|x| (hydro_lang::location::MemberId::from_raw_id(x), x)))
645    ///     .demux(&workers, TCP.fail_stop().bincode());
646    /// # on_worker.send(&p2, TCP.fail_stop().bincode()).entries()
647    /// // if there are 4 members in the cluster, each receives one element
648    /// // - MemberId::<()>(0): [0]
649    /// // - MemberId::<()>(1): [1]
650    /// // - MemberId::<()>(2): [2]
651    /// // - MemberId::<()>(3): [3]
652    /// # }, |mut stream| async move {
653    /// # let mut results = Vec::new();
654    /// # for w in 0..4 {
655    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
656    /// # }
657    /// # results.sort();
658    /// # assert_eq!(results, vec!["(MemberId::<()>(0), 0)", "(MemberId::<()>(1), 1)", "(MemberId::<()>(2), 2)", "(MemberId::<()>(3), 3)"]);
659    /// # }));
660    /// # }
661    /// ```
662    pub fn demux<N: NetworkFor<T>>(
663        self,
664        to: &Cluster<'a, L2>,
665        via: N,
666    ) -> Stream<
667        T,
668        Cluster<'a, L2, NoConsistency>,
669        Unbounded,
670        <O as MinOrder<N::OrderingGuarantee>>::Min,
671        <R as MinRetries<N::RetriesGuarantee>>::Min,
672    >
673    where
674        O: MinOrder<N::OrderingGuarantee>,
675        R: MinRetries<N::RetriesGuarantee>,
676    {
677        self.into_keyed().demux(to, via)
678    }
679}
680
681impl<'a, T, L, B: Boundedness> Stream<T, Process<'a, L>, B, TotalOrder, ExactlyOnce> {
682    #[deprecated = "use Stream::round_robin(..., TCP.fail_stop().bincode()) instead"]
683    /// Distributes elements of this stream to cluster members in a round-robin fashion, using
684    /// [`bincode`] to serialize/deserialize messages.
685    ///
686    /// This provides load balancing by evenly distributing work across cluster members. The
687    /// distribution is deterministic based on element order - the first element goes to member 0,
688    /// the second to member 1, and so on, wrapping around when reaching the end of the member list.
689    ///
690    /// # Non-Determinism
691    /// The set of cluster members may asynchronously change over time. Each element is distributed
692    /// based on the current cluster membership _at that point in time_. Depending on when cluster
693    /// members join and leave, the round-robin pattern will change. Furthermore, even when the
694    /// membership is stable, the order of members in the round-robin pattern may change across runs.
695    ///
696    /// # Ordering Requirements
697    /// This method is only available on streams with [`TotalOrder`] and [`ExactlyOnce`], since the
698    /// order of messages and retries affects the round-robin pattern.
699    ///
700    /// # Example
701    /// ```rust
702    /// # #[cfg(feature = "deploy")] {
703    /// # use hydro_lang::prelude::*;
704    /// # use hydro_lang::live_collections::stream::{TotalOrder, ExactlyOnce};
705    /// # use futures::StreamExt;
706    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
707    /// let p1 = flow.process::<()>();
708    /// let workers: Cluster<()> = flow.cluster::<()>();
709    /// let numbers: Stream<_, Process<_>, _, TotalOrder, ExactlyOnce> = p1.source_iter(q!(vec![1, 2, 3, 4]));
710    /// let on_worker: Stream<_, Cluster<_>, _> = numbers.round_robin_bincode(&workers, nondet!(/** assuming stable membership */));
711    /// on_worker.send_bincode(&p2)
712    /// # .first().values() // we use first to assert that each member gets one element
713    /// // with 4 cluster members, elements are distributed (with a non-deterministic round-robin order):
714    /// // - MemberId::<()>(?): [1]
715    /// // - MemberId::<()>(?): [2]
716    /// // - MemberId::<()>(?): [3]
717    /// // - MemberId::<()>(?): [4]
718    /// # }, |mut stream| async move {
719    /// # let mut results = Vec::new();
720    /// # for w in 0..4 {
721    /// #     results.push(stream.next().await.unwrap());
722    /// # }
723    /// # results.sort();
724    /// # assert_eq!(results, vec![1, 2, 3, 4]);
725    /// # }));
726    /// # }
727    /// ```
728    pub fn round_robin_bincode<L2: 'a>(
729        self,
730        other: &Cluster<'a, L2>,
731        nondet_membership: NonDet,
732    ) -> Stream<T, Cluster<'a, L2>, Unbounded, TotalOrder, ExactlyOnce>
733    where
734        T: Serialize + DeserializeOwned,
735    {
736        self.round_robin(other, TCP.fail_stop().bincode(), nondet_membership)
737    }
738
739    /// Distributes elements of this stream to cluster members in a round-robin fashion, using
740    /// the configuration in `via` to set up the message transport.
741    ///
742    /// This provides load balancing by evenly distributing work across cluster members. The
743    /// distribution is deterministic based on element order - the first element goes to member 0,
744    /// the second to member 1, and so on, wrapping around when reaching the end of the member list.
745    ///
746    /// # Non-Determinism
747    /// The set of cluster members may asynchronously change over time. Each element is distributed
748    /// based on the current cluster membership _at that point in time_. Depending on when cluster
749    /// members join and leave, the round-robin pattern will change. Furthermore, even when the
750    /// membership is stable, the order of members in the round-robin pattern may change across runs.
751    ///
752    /// # Ordering Requirements
753    /// This method is only available on streams with [`TotalOrder`] and [`ExactlyOnce`], since the
754    /// order of messages and retries affects the round-robin pattern.
755    ///
756    /// # Example
757    /// ```rust
758    /// # #[cfg(feature = "deploy")] {
759    /// # use hydro_lang::prelude::*;
760    /// # use hydro_lang::live_collections::stream::{TotalOrder, ExactlyOnce};
761    /// # use futures::StreamExt;
762    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
763    /// let p1 = flow.process::<()>();
764    /// let workers: Cluster<()> = flow.cluster::<()>();
765    /// let numbers: Stream<_, Process<_>, _, TotalOrder, ExactlyOnce> = p1.source_iter(q!(vec![1, 2, 3, 4]));
766    /// let on_worker: Stream<_, Cluster<_>, _> = numbers.round_robin(&workers, TCP.fail_stop().bincode(), nondet!(/** assuming stable membership */));
767    /// on_worker.send(&p2, TCP.fail_stop().bincode())
768    /// # .first().values() // we use first to assert that each member gets one element
769    /// // with 4 cluster members, elements are distributed (with a non-deterministic round-robin order):
770    /// // - MemberId::<()>(?): [1]
771    /// // - MemberId::<()>(?): [2]
772    /// // - MemberId::<()>(?): [3]
773    /// // - MemberId::<()>(?): [4]
774    /// # }, |mut stream| async move {
775    /// # let mut results = Vec::new();
776    /// # for w in 0..4 {
777    /// #     results.push(stream.next().await.unwrap());
778    /// # }
779    /// # results.sort();
780    /// # assert_eq!(results, vec![1, 2, 3, 4]);
781    /// # }));
782    /// # }
783    /// ```
784    pub fn round_robin<L2: 'a, N: NetworkFor<T>>(
785        self,
786        to: &Cluster<'a, L2>,
787        via: N,
788        nondet_membership: NonDet,
789    ) -> Stream<T, Cluster<'a, L2>, Unbounded, N::OrderingGuarantee, N::RetriesGuarantee> {
790        // TODO(#1875): the membership snapshot below is over a `KeyedSingleton`, and keyed
791        // sim hooks do not exist yet. Once they do, expose a composite hook payload here
792        // (`NonDet<(Option<KeyedSnapshotHook<..>>, Option<BatchHook<T, O, R>>)>`) so tests
793        // can script the membership snapshot and the element batching independently.
794        let ids = track_membership(self.location.source_cluster_membership_stream(
795            to,
796            nondet!(/** dropped prefixes don't affect broadcast */),
797        ));
798        sliced! {
799            let members_snapshot = use::snapshot(ids, nondet!(
800                /// membership timing is captured by the caller's guard
801                nondet_membership
802            ));
803            let elements = use::batch(self.enumerate(), nondet!(
804                /// batching timing is captured by the caller's guard
805                nondet_membership
806            ));
807
808            let current_members = members_snapshot
809                .filter(q!(|b| *b))
810                .keys()
811                .assume_ordering::<TotalOrder>(nondet!(/** membership timing is captured by the caller guard */ nondet_membership))
812                .collect_vec();
813
814            elements
815                .cross_singleton(current_members)
816                .filter_map(q!(|(data, members)| {
817                    if members.is_empty() {
818                        None
819                    } else {
820                        Some((members[data.0 % members.len()].clone(), data.1))
821                    }
822                }))
823        }
824        .demux(to, via)
825    }
826}
827
828impl<'a, T, L, B: Boundedness, C: Consistency>
829    Stream<T, Cluster<'a, L, C>, B, TotalOrder, ExactlyOnce>
830{
831    #[deprecated = "use Stream::round_robin(..., TCP.fail_stop().bincode()) instead"]
832    /// Distributes elements of this stream to cluster members in a round-robin fashion, using
833    /// [`bincode`] to serialize/deserialize messages.
834    ///
835    /// This provides load balancing by evenly distributing work across cluster members. The
836    /// distribution is deterministic based on element order - the first element goes to member 0,
837    /// the second to member 1, and so on, wrapping around when reaching the end of the member list.
838    ///
839    /// # Non-Determinism
840    /// The set of cluster members may asynchronously change over time. Each element is distributed
841    /// based on the current cluster membership _at that point in time_. Depending on when cluster
842    /// members join and leave, the round-robin pattern will change. Furthermore, even when the
843    /// membership is stable, the order of members in the round-robin pattern may change across runs.
844    ///
845    /// # Ordering Requirements
846    /// This method is only available on streams with [`TotalOrder`] and [`ExactlyOnce`], since the
847    /// order of messages and retries affects the round-robin pattern.
848    ///
849    /// # Example
850    /// ```rust
851    /// # #[cfg(feature = "deploy")] {
852    /// # use hydro_lang::prelude::*;
853    /// # use hydro_lang::live_collections::stream::{TotalOrder, ExactlyOnce, NoOrder};
854    /// # use hydro_lang::location::MemberId;
855    /// # use futures::StreamExt;
856    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
857    /// let p1 = flow.process::<()>();
858    /// let workers1: Cluster<()> = flow.cluster::<()>();
859    /// let workers2: Cluster<()> = flow.cluster::<()>();
860    /// let numbers: Stream<_, Process<_>, _, TotalOrder, ExactlyOnce> = p1.source_iter(q!(0..=16));
861    /// let on_worker1: Stream<_, Cluster<_>, _> = numbers.round_robin_bincode(&workers1, nondet!(/** assuming stable membership */));
862    /// let on_worker2: Stream<_, Cluster<_>, _> = on_worker1.round_robin_bincode(&workers2, nondet!(/** assuming stable membership */)).entries().assume_ordering(nondet!(/** assuming stable membership */));
863    /// on_worker2.send_bincode(&p2)
864    /// # .entries()
865    /// # .map(q!(|(w2, (w1, v))| ((w2, w1), v)))
866    /// # }, |mut stream| async move {
867    /// # let mut results = Vec::new();
868    /// # let mut locations = std::collections::HashSet::new();
869    /// # for w in 0..=16 {
870    /// #     let (location, v) = stream.next().await.unwrap();
871    /// #     locations.insert(location);
872    /// #     results.push(v);
873    /// # }
874    /// # results.sort();
875    /// # assert_eq!(results, (0..=16).collect::<Vec<_>>());
876    /// # assert_eq!(locations.len(), 16);
877    /// # }));
878    /// # }
879    /// ```
880    pub fn round_robin_bincode<L2: 'a>(
881        self,
882        other: &Cluster<'a, L2>,
883        nondet_membership: NonDet,
884    ) -> KeyedStream<MemberId<L>, T, Cluster<'a, L2>, Unbounded, TotalOrder, ExactlyOnce>
885    where
886        T: Serialize + DeserializeOwned,
887    {
888        self.round_robin(other, TCP.fail_stop().bincode(), nondet_membership)
889    }
890
891    /// Distributes elements of this stream to cluster members in a round-robin fashion, using
892    /// the configuration in `via` to set up the message transport.
893    ///
894    /// This provides load balancing by evenly distributing work across cluster members. The
895    /// distribution is deterministic based on element order - the first element goes to member 0,
896    /// the second to member 1, and so on, wrapping around when reaching the end of the member list.
897    ///
898    /// # Non-Determinism
899    /// The set of cluster members may asynchronously change over time. Each element is distributed
900    /// based on the current cluster membership _at that point in time_. Depending on when cluster
901    /// members join and leave, the round-robin pattern will change. Furthermore, even when the
902    /// membership is stable, the order of members in the round-robin pattern may change across runs.
903    ///
904    /// # Ordering Requirements
905    /// This method is only available on streams with [`TotalOrder`] and [`ExactlyOnce`], since the
906    /// order of messages and retries affects the round-robin pattern.
907    ///
908    /// # Example
909    /// ```rust
910    /// # #[cfg(feature = "deploy")] {
911    /// # use hydro_lang::prelude::*;
912    /// # use hydro_lang::live_collections::stream::{TotalOrder, ExactlyOnce, NoOrder};
913    /// # use hydro_lang::location::MemberId;
914    /// # use futures::StreamExt;
915    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
916    /// let p1 = flow.process::<()>();
917    /// let workers1: Cluster<()> = flow.cluster::<()>();
918    /// let workers2: Cluster<()> = flow.cluster::<()>();
919    /// let numbers: Stream<_, Process<_>, _, TotalOrder, ExactlyOnce> = p1.source_iter(q!(0..=16));
920    /// let on_worker1: Stream<_, Cluster<_>, _> = numbers.round_robin(&workers1, TCP.fail_stop().bincode(), nondet!(/** assuming stable membership */));
921    /// let on_worker2: Stream<_, Cluster<_>, _> = on_worker1.round_robin(&workers2, TCP.fail_stop().bincode(), nondet!(/** assuming stable membership */)).entries().assume_ordering(nondet!(/** assuming stable membership */));
922    /// on_worker2.send(&p2, TCP.fail_stop().bincode())
923    /// # .entries()
924    /// # .map(q!(|(w2, (w1, v))| ((w2, w1), v)))
925    /// # }, |mut stream| async move {
926    /// # let mut results = Vec::new();
927    /// # let mut locations = std::collections::HashSet::new();
928    /// # for w in 0..=16 {
929    /// #     let (location, v) = stream.next().await.unwrap();
930    /// #     locations.insert(location);
931    /// #     results.push(v);
932    /// # }
933    /// # results.sort();
934    /// # assert_eq!(results, (0..=16).collect::<Vec<_>>());
935    /// # assert_eq!(locations.len(), 16);
936    /// # }));
937    /// # }
938    /// ```
939    pub fn round_robin<L2: 'a, N: NetworkFor<T>>(
940        self,
941        to: &Cluster<'a, L2>,
942        via: N,
943        nondet_membership: NonDet,
944    ) -> KeyedStream<
945        MemberId<L>,
946        T,
947        Cluster<'a, L2>,
948        Unbounded,
949        N::OrderingGuarantee,
950        N::RetriesGuarantee,
951    > {
952        // TODO(#1875): the membership snapshot below is over a `KeyedSingleton`, and keyed
953        // sim hooks do not exist yet. Once they do, expose a composite hook payload here
954        // (`NonDet<(Option<KeyedSnapshotHook<..>>, Option<BatchHook<T, O, R>>)>`) so tests
955        // can script the membership snapshot and the element batching independently.
956        let ids = track_membership(self.location.source_cluster_membership_stream(
957            to,
958            nondet!(/** dropped prefixes don't affect broadcast */),
959        ));
960        sliced! {
961            let members_snapshot = use::snapshot(ids, nondet!(
962                /// membership timing is captured by the caller's guard
963                nondet_membership
964            ));
965            let elements = use::batch(self.enumerate(), nondet!(
966                /// batching timing is captured by the caller's guard
967                nondet_membership
968            ));
969
970            let current_members = members_snapshot
971                .filter(q!(|b| *b))
972                .keys()
973                .assume_ordering::<TotalOrder>(nondet!(/** membership timing is captured by the caller guard */ nondet_membership))
974                .collect_vec();
975
976            elements
977                .cross_singleton(current_members)
978                .filter_map(q!(|(data, members)| {
979                    if members.is_empty() {
980                        None
981                    } else {
982                        Some((members[data.0 % members.len()].clone(), data.1))
983                    }
984                }))
985        }
986        .demux(to, via)
987    }
988}
989
990impl<'a, T, L, B: Boundedness, C: Consistency, O: Ordering, R: Retries>
991    Stream<T, Cluster<'a, L, C>, B, O, R>
992{
993    #[deprecated = "use Stream::send(..., TCP.fail_stop().bincode()) instead"]
994    /// "Moves" elements of this stream from a cluster to a process by sending them over the network,
995    /// using [`bincode`] to serialize/deserialize messages.
996    ///
997    /// Each cluster member sends its local stream elements, and they are collected at the destination
998    /// as a [`KeyedStream`] where keys identify the source cluster member.
999    ///
1000    /// # Example
1001    /// ```rust
1002    /// # #[cfg(feature = "deploy")] {
1003    /// # use hydro_lang::prelude::*;
1004    /// # use futures::StreamExt;
1005    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, process| {
1006    /// let workers: Cluster<()> = flow.cluster::<()>();
1007    /// let numbers: Stream<_, Cluster<_>, _> = workers.source_iter(q!(vec![1]));
1008    /// let all_received = numbers.send_bincode(&process); // KeyedStream<MemberId<()>, i32, ...>
1009    /// # all_received.entries()
1010    /// # }, |mut stream| async move {
1011    /// // if there are 4 members in the cluster, we should receive 4 elements
1012    /// // { MemberId::<()>(0): [1], MemberId::<()>(1): [1], MemberId::<()>(2): [1], MemberId::<()>(3): [1] }
1013    /// # let mut results = Vec::new();
1014    /// # for w in 0..4 {
1015    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
1016    /// # }
1017    /// # results.sort();
1018    /// # assert_eq!(results, vec!["(MemberId::<()>(0), 1)", "(MemberId::<()>(1), 1)", "(MemberId::<()>(2), 1)", "(MemberId::<()>(3), 1)"]);
1019    /// # }));
1020    /// # }
1021    /// ```
1022    ///
1023    /// If you don't need to know the source for each element, you can use `.values()`
1024    /// to get just the data:
1025    /// ```rust
1026    /// # #[cfg(feature = "deploy")] {
1027    /// # use hydro_lang::prelude::*;
1028    /// # use hydro_lang::live_collections::stream::NoOrder;
1029    /// # use futures::StreamExt;
1030    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, process| {
1031    /// # let workers: Cluster<()> = flow.cluster::<()>();
1032    /// # let numbers: Stream<_, Cluster<_>, _> = workers.source_iter(q!(vec![1]));
1033    /// let values: Stream<i32, _, _, NoOrder> = numbers.send_bincode(&process).values();
1034    /// # values
1035    /// # }, |mut stream| async move {
1036    /// # let mut results = Vec::new();
1037    /// # for w in 0..4 {
1038    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
1039    /// # }
1040    /// # results.sort();
1041    /// // if there are 4 members in the cluster, we should receive 4 elements
1042    /// // 1, 1, 1, 1
1043    /// # assert_eq!(results, vec!["1", "1", "1", "1"]);
1044    /// # }));
1045    /// # }
1046    /// ```
1047    pub fn send_bincode<L2>(
1048        self,
1049        other: &Process<'a, L2>,
1050    ) -> KeyedStream<MemberId<L>, T, Process<'a, L2>, Unbounded, O, R>
1051    where
1052        T: Serialize + DeserializeOwned,
1053    {
1054        self.send(other, TCP.fail_stop().bincode())
1055    }
1056
1057    /// "Moves" elements of this stream from a cluster to a process by sending them over the network,
1058    /// using the configuration in `via` to set up the message transport.
1059    ///
1060    /// Each cluster member sends its local stream elements, and they are collected at the destination
1061    /// as a [`KeyedStream`] where keys identify the source cluster member.
1062    ///
1063    /// # Example
1064    /// ```rust
1065    /// # #[cfg(feature = "deploy")] {
1066    /// # use hydro_lang::prelude::*;
1067    /// # use futures::StreamExt;
1068    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, process| {
1069    /// let workers: Cluster<()> = flow.cluster::<()>();
1070    /// let numbers: Stream<_, Cluster<_>, _> = workers.source_iter(q!(vec![1]));
1071    /// let all_received = numbers.send(&process, TCP.fail_stop().bincode()); // KeyedStream<MemberId<()>, i32, ...>
1072    /// # all_received.entries()
1073    /// # }, |mut stream| async move {
1074    /// // if there are 4 members in the cluster, we should receive 4 elements
1075    /// // { MemberId::<()>(0): [1], MemberId::<()>(1): [1], MemberId::<()>(2): [1], MemberId::<()>(3): [1] }
1076    /// # let mut results = Vec::new();
1077    /// # for w in 0..4 {
1078    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
1079    /// # }
1080    /// # results.sort();
1081    /// # assert_eq!(results, vec!["(MemberId::<()>(0), 1)", "(MemberId::<()>(1), 1)", "(MemberId::<()>(2), 1)", "(MemberId::<()>(3), 1)"]);
1082    /// # }));
1083    /// # }
1084    /// ```
1085    ///
1086    /// If you don't need to know the source for each element, you can use `.values()`
1087    /// to get just the data:
1088    /// ```rust
1089    /// # #[cfg(feature = "deploy")] {
1090    /// # use hydro_lang::prelude::*;
1091    /// # use hydro_lang::live_collections::stream::NoOrder;
1092    /// # use futures::StreamExt;
1093    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, process| {
1094    /// # let workers: Cluster<()> = flow.cluster::<()>();
1095    /// # let numbers: Stream<_, Cluster<_>, _> = workers.source_iter(q!(vec![1]));
1096    /// let values: Stream<i32, _, _, NoOrder> =
1097    ///     numbers.send(&process, TCP.fail_stop().bincode()).values();
1098    /// # values
1099    /// # }, |mut stream| async move {
1100    /// # let mut results = Vec::new();
1101    /// # for w in 0..4 {
1102    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
1103    /// # }
1104    /// # results.sort();
1105    /// // if there are 4 members in the cluster, we should receive 4 elements
1106    /// // 1, 1, 1, 1
1107    /// # assert_eq!(results, vec!["1", "1", "1", "1"]);
1108    /// # }));
1109    /// # }
1110    /// ```
1111    pub fn send<L2, N: NetworkFor<T>>(
1112        self,
1113        to: &Process<'a, L2>,
1114        via: N,
1115    ) -> KeyedStream<
1116        MemberId<L>,
1117        T,
1118        Process<'a, L2>,
1119        Unbounded,
1120        <O as MinOrder<N::OrderingGuarantee>>::Min,
1121        <R as MinRetries<N::RetriesGuarantee>>::Min,
1122    >
1123    where
1124        O: MinOrder<N::OrderingGuarantee>,
1125        R: MinRetries<N::RetriesGuarantee>,
1126    {
1127        let name = via.name();
1128        assert!(
1129            !to.multiversioned() || name.is_some(),
1130            "Cannot send to a multiversioned location without a channel name. Please provide a name for the network."
1131        );
1132
1133        let (serialize, deserialize) = if N::is_embedded() {
1134            (
1135                NetworkSend::Embedded {
1136                    tag: None,
1137                    element_type: quote_type::<T>().into(),
1138                },
1139                NetworkRecv::Embedded {
1140                    tag: Some(quote_type::<L>().into()),
1141                    element_type: quote_type::<T>().into(),
1142                },
1143            )
1144        } else {
1145            (
1146                NetworkSend::Custom {
1147                    serialize_fn: Some(N::serialize_thunk(false).into()),
1148                },
1149                NetworkRecv::Custom {
1150                    deserialize_fn: Some(N::deserialize_thunk(Some(&quote_type::<L>())).into()),
1151                },
1152            )
1153        };
1154
1155        let raw_stream: Stream<
1156            (MemberId<L>, T),
1157            Process<'a, L2>,
1158            Unbounded,
1159            <O as MinOrder<N::OrderingGuarantee>>::Min,
1160            <R as MinRetries<N::RetriesGuarantee>>::Min,
1161        > = Stream::new(
1162            to.clone(),
1163            HydroNode::Network {
1164                name: name.map(ToOwned::to_owned),
1165                networking_info: N::networking_info(),
1166                serialize,
1167                deserialize,
1168                instantiate_fn: DebugInstantiate::Building,
1169                input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1170                metadata: to.new_node_metadata(Stream::<
1171                    (MemberId<L>, T),
1172                    Process<'a, L2>,
1173                    Unbounded,
1174                    <O as MinOrder<N::OrderingGuarantee>>::Min,
1175                    <R as MinRetries<N::RetriesGuarantee>>::Min,
1176                >::collection_kind()),
1177            },
1178        );
1179
1180        raw_stream.into_keyed()
1181    }
1182
1183    #[deprecated = "use Stream::broadcast(..., TCP.fail_stop().bincode()) instead"]
1184    /// Broadcasts elements of this stream at each source member to all members of a destination
1185    /// cluster, using [`bincode`] to serialize/deserialize messages.
1186    ///
1187    /// Each source member sends each of its stream elements to **every** member of the cluster
1188    /// based on its latest membership information. Unlike [`Stream::demux_bincode`], which requires
1189    /// `(MemberId, T)` tuples to target specific members, `broadcast_bincode` takes a stream of
1190    /// **only data elements** and sends each element to all cluster members.
1191    ///
1192    /// # Non-Determinism
1193    /// The set of cluster members may asynchronously change over time. Each element is only broadcast
1194    /// to the current cluster members known _at that point in time_ at the source member. Depending
1195    /// on when each source member is notified of membership changes, it will broadcast each element
1196    /// to different members.
1197    ///
1198    /// # Example
1199    /// ```rust
1200    /// # #[cfg(feature = "deploy")] {
1201    /// # use hydro_lang::prelude::*;
1202    /// # use hydro_lang::location::MemberId;
1203    /// # use futures::StreamExt;
1204    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
1205    /// # type Source = ();
1206    /// # type Destination = ();
1207    /// let source: Cluster<Source> = flow.cluster::<Source>();
1208    /// let numbers: Stream<_, Cluster<Source>, _> = source.source_iter(q!(vec![123]));
1209    /// let destination: Cluster<Destination> = flow.cluster::<Destination>();
1210    /// let on_destination: KeyedStream<MemberId<Source>, _, Cluster<Destination>, _> = numbers.broadcast_bincode(&destination, nondet!(/** assuming stable membership */));
1211    /// # on_destination.entries().send_bincode(&p2).entries()
1212    /// // if there are 4 members in the desination, each receives one element from each source member
1213    /// // - Destination(0): { Source(0): [123], Source(1): [123], ... }
1214    /// // - Destination(1): { Source(0): [123], Source(1): [123], ... }
1215    /// // - ...
1216    /// # }, |mut stream| async move {
1217    /// # let mut results = Vec::new();
1218    /// # for w in 0..16 {
1219    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
1220    /// # }
1221    /// # results.sort();
1222    /// # assert_eq!(results, vec![
1223    /// #   "(MemberId::<()>(0), (MemberId::<()>(0), 123))", "(MemberId::<()>(0), (MemberId::<()>(1), 123))", "(MemberId::<()>(0), (MemberId::<()>(2), 123))", "(MemberId::<()>(0), (MemberId::<()>(3), 123))",
1224    /// #   "(MemberId::<()>(1), (MemberId::<()>(0), 123))", "(MemberId::<()>(1), (MemberId::<()>(1), 123))", "(MemberId::<()>(1), (MemberId::<()>(2), 123))", "(MemberId::<()>(1), (MemberId::<()>(3), 123))",
1225    /// #   "(MemberId::<()>(2), (MemberId::<()>(0), 123))", "(MemberId::<()>(2), (MemberId::<()>(1), 123))", "(MemberId::<()>(2), (MemberId::<()>(2), 123))", "(MemberId::<()>(2), (MemberId::<()>(3), 123))",
1226    /// #   "(MemberId::<()>(3), (MemberId::<()>(0), 123))", "(MemberId::<()>(3), (MemberId::<()>(1), 123))", "(MemberId::<()>(3), (MemberId::<()>(2), 123))", "(MemberId::<()>(3), (MemberId::<()>(3), 123))"
1227    /// # ]);
1228    /// # }));
1229    /// # }
1230    /// ```
1231    pub fn broadcast_bincode<L2: 'a>(
1232        self,
1233        other: &Cluster<'a, L2>,
1234        nondet_membership: NonDet,
1235    ) -> KeyedStream<MemberId<L>, T, Cluster<'a, L2>, Unbounded, O, R>
1236    where
1237        T: Clone + Serialize + DeserializeOwned,
1238    {
1239        self.broadcast(other, TCP.fail_stop().bincode(), nondet_membership)
1240    }
1241
1242    /// Broadcasts elements of this stream at each source member to all members of a destination
1243    /// cluster, using the configuration in `via` to set up the message transport.
1244    ///
1245    /// Each source member sends each of its stream elements to **every** member of the cluster
1246    /// based on its latest membership information. Unlike [`Stream::demux`], which requires
1247    /// `(MemberId, T)` tuples to target specific members, `broadcast` takes a stream of
1248    /// **only data elements** and sends each element to all cluster members.
1249    ///
1250    /// # Non-Determinism
1251    /// The set of cluster members may asynchronously change over time. Each element is only broadcast
1252    /// to the current cluster members known _at that point in time_ at the source member. Depending
1253    /// on when each source member is notified of membership changes, it will broadcast each element
1254    /// to different members.
1255    ///
1256    /// # Example
1257    /// ```rust
1258    /// # #[cfg(feature = "deploy")] {
1259    /// # use hydro_lang::prelude::*;
1260    /// # use hydro_lang::location::MemberId;
1261    /// # use futures::StreamExt;
1262    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
1263    /// # type Source = ();
1264    /// # type Destination = ();
1265    /// let source: Cluster<Source> = flow.cluster::<Source>();
1266    /// let numbers: Stream<_, Cluster<Source>, _> = source.source_iter(q!(vec![123]));
1267    /// let destination: Cluster<Destination> = flow.cluster::<Destination>();
1268    /// let on_destination: KeyedStream<MemberId<Source>, _, Cluster<Destination>, _> = numbers.broadcast(&destination, TCP.fail_stop().bincode(), nondet!(/** assuming stable membership */));
1269    /// # on_destination.entries().send(&p2, TCP.fail_stop().bincode()).entries()
1270    /// // if there are 4 members in the desination, each receives one element from each source member
1271    /// // - Destination(0): { Source(0): [123], Source(1): [123], ... }
1272    /// // - Destination(1): { Source(0): [123], Source(1): [123], ... }
1273    /// // - ...
1274    /// # }, |mut stream| async move {
1275    /// # let mut results = Vec::new();
1276    /// # for w in 0..16 {
1277    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
1278    /// # }
1279    /// # results.sort();
1280    /// # assert_eq!(results, vec![
1281    /// #   "(MemberId::<()>(0), (MemberId::<()>(0), 123))", "(MemberId::<()>(0), (MemberId::<()>(1), 123))", "(MemberId::<()>(0), (MemberId::<()>(2), 123))", "(MemberId::<()>(0), (MemberId::<()>(3), 123))",
1282    /// #   "(MemberId::<()>(1), (MemberId::<()>(0), 123))", "(MemberId::<()>(1), (MemberId::<()>(1), 123))", "(MemberId::<()>(1), (MemberId::<()>(2), 123))", "(MemberId::<()>(1), (MemberId::<()>(3), 123))",
1283    /// #   "(MemberId::<()>(2), (MemberId::<()>(0), 123))", "(MemberId::<()>(2), (MemberId::<()>(1), 123))", "(MemberId::<()>(2), (MemberId::<()>(2), 123))", "(MemberId::<()>(2), (MemberId::<()>(3), 123))",
1284    /// #   "(MemberId::<()>(3), (MemberId::<()>(0), 123))", "(MemberId::<()>(3), (MemberId::<()>(1), 123))", "(MemberId::<()>(3), (MemberId::<()>(2), 123))", "(MemberId::<()>(3), (MemberId::<()>(3), 123))"
1285    /// # ]);
1286    /// # }));
1287    /// # }
1288    /// ```
1289    pub fn broadcast<L2: 'a, N: NetworkFor<T>>(
1290        self,
1291        to: &Cluster<'a, L2>,
1292        via: N,
1293        nondet_membership: NonDet,
1294    ) -> KeyedStream<
1295        MemberId<L>,
1296        T,
1297        Cluster<'a, L2>,
1298        Unbounded,
1299        <O as MinOrder<N::OrderingGuarantee>>::Min,
1300        <R as MinRetries<N::RetriesGuarantee>>::Min,
1301    >
1302    where
1303        T: Clone,
1304        O: MinOrder<N::OrderingGuarantee>,
1305        R: MinRetries<N::RetriesGuarantee>,
1306    {
1307        // TODO(#1875): the membership snapshot below is over a `KeyedSingleton`, and keyed
1308        // sim hooks do not exist yet. Once they do, expose a composite hook payload here
1309        // (`NonDet<(Option<KeyedSnapshotHook<..>>, Option<BatchHook<T, O, R>>)>`) so tests
1310        // can script the membership snapshot and the element batching independently.
1311        let ids = track_membership(self.location.source_cluster_membership_stream(
1312            to,
1313            nondet!(/** dropped prefixes don't affect broadcast */),
1314        ));
1315        sliced! {
1316            let members_snapshot = use::snapshot(ids, nondet!(
1317                /// membership timing is captured by the caller's guard
1318                nondet_membership
1319            ));
1320            let elements = use::batch(self, nondet!(
1321                /// batching timing is captured by the caller's guard
1322                nondet_membership
1323            ));
1324
1325            let current_members = members_snapshot.filter(q!(|b| *b));
1326            elements.repeat_with_keys(current_members)
1327        }
1328        .demux(to, via)
1329    }
1330
1331    /// Broadcasts elements of this stream at each source member to all members of a destination
1332    /// cluster, assuming membership is closed (fixed at deploy time).
1333    ///
1334    /// Unlike [`Stream::broadcast`], this does not require a [`NonDet`] guard.
1335    /// The membership set is obtained from deploy metadata via [`ClusterIds`], making the
1336    /// broadcast fully deterministic.
1337    ///
1338    /// The consistency guarantee of the output depends on the network's failure policy
1339    /// ([`NetworkFor::ConsistencyGuarantee`]). Policies like `fail_stop` and
1340    /// `lossy_delayed_forever` guarantee that every live destination member eventually
1341    /// materializes the same elements from each source, so the output is
1342    /// [`EventualConsistency`](crate::location::cluster::EventualConsistency). A plain `lossy`
1343    /// policy can drop individual messages for some
1344    /// members while delivering them to others, so replicas may permanently diverge and the
1345    /// output only has [`NoConsistency`].
1346    ///
1347    /// This is only available in deployment targets with static cluster membership
1348    /// (legacy Hydro Deploy and simulation). On dynamic targets, use [`Stream::broadcast`].
1349    pub fn broadcast_closed<L2: 'a, N: NetworkFor<T>>(
1350        self,
1351        to: &Cluster<'a, L2>,
1352        via: N,
1353    ) -> KeyedStream<
1354        MemberId<L>,
1355        T,
1356        Cluster<'a, L2, N::ConsistencyGuarantee>,
1357        Unbounded,
1358        <O as MinOrder<N::OrderingGuarantee>>::Min,
1359        <R as MinRetries<N::RetriesGuarantee>>::Min,
1360    >
1361    where
1362        T: Clone,
1363        O: MinOrder<N::OrderingGuarantee>,
1364        R: MinRetries<N::RetriesGuarantee>,
1365    {
1366        let cluster_ids = ClusterIds {
1367            key: to.key,
1368            _phantom: PhantomData,
1369        };
1370        let member_ids = self
1371            .location
1372            .source_iter(q!(cluster_ids
1373                .iter()
1374                .map(|id| MemberId::from_tagless(id.clone()))))
1375            .assert_has_consistency_of_trusted::<Cluster<'a, L, C>>(manual_proof!(
1376                /// ClusterIds is deploy-time metadata, identical on every cluster member.
1377            ));
1378
1379        self.cross_product(member_ids)
1380            .map(q!(|(data, member_id)| (member_id, data)))
1381            .into_keyed()
1382            .demux(to, via)
1383            .assert_has_consistency_of_trusted(manual_proof!(
1384                /// Closed broadcast with fixed membership: every source member sends to every
1385                /// destination member, and the network's failure policy (tracked by
1386                /// `NetworkFor::ConsistencyGuarantee`) delivers the same messages to every live
1387                /// member, so all destinations materialize the same elements.
1388            ))
1389    }
1390
1391    #[cfg(feature = "sim")]
1392    fn register_serialized_external_port<L2>(
1393        self,
1394        other: &External<'_, L2>,
1395        serialize_pipeline: syn::Expr,
1396    ) -> ExternalPortId {
1397        let mut flow_state_borrow = self.location.flow_state().borrow_mut();
1398
1399        let external_port_id = flow_state_borrow.next_external_port();
1400
1401        flow_state_borrow.push_root(HydroRoot::SendExternal {
1402            to_external_key: other.key,
1403            to_port_id: external_port_id,
1404            to_many: false,
1405            unpaired: true,
1406            serialize_fn: Some(serialize_pipeline.into()),
1407            instantiate_fn: DebugInstantiate::Building,
1408            input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1409            op_metadata: HydroIrOpMetadata::new(),
1410        });
1411
1412        external_port_id
1413    }
1414
1415    #[cfg(feature = "sim")]
1416    /// Sets up a MessagePack-encoded simulation output port for this cluster stream, allowing test
1417    /// code to receive `(member_id, T)` pairs during simulation. Use
1418    /// [`Stream::sim_cluster_output_with`] to select another codec.
1419    pub fn sim_cluster_output(self) -> crate::sim::SimClusterReceiver<T, O, R>
1420    where
1421        T: Serialize + DeserializeOwned,
1422    {
1423        self.sim_cluster_output_with::<crate::sim::codec::RmpCodec>()
1424    }
1425
1426    #[cfg(feature = "sim")]
1427    /// Sets up a simulation output port for this cluster stream using the codec `Codec`,
1428    /// allowing test code to receive `(member_id, T)` pairs during simulation:
1429    /// `stream.sim_cluster_output_with::<MyCodec>()`. Custom codecs implement
1430    /// [`SimCodec`](crate::sim::codec::SimCodec), which documents where they must be defined.
1431    pub fn sim_cluster_output_with<Codec>(self) -> crate::sim::SimClusterReceiver<T, O, R>
1432    where
1433        Codec: crate::sim::codec::SimCodec<T>,
1434    {
1435        let external_location: External<'a, ()> = External {
1436            key: LocationKey::FIRST,
1437            flow_state: self.location.flow_state().clone(),
1438            _phantom: PhantomData,
1439        };
1440
1441        let external_port_id = self.register_serialized_external_port(
1442            &external_location,
1443            crate::sim::codec::staged_serialize::<T, Codec>(),
1444        );
1445
1446        crate::sim::SimClusterReceiver(external_port_id, PhantomData, Codec::decode)
1447    }
1448}
1449
1450impl<'a, T, L, L2, B: Boundedness, C: Consistency, O: Ordering, R: Retries>
1451    Stream<(MemberId<L2>, T), Cluster<'a, L, C>, B, O, R>
1452{
1453    #[deprecated = "use Stream::demux(..., TCP.fail_stop().bincode()) instead"]
1454    /// Sends elements of this stream at each source member to specific members of a destination
1455    /// cluster, identified by a [`MemberId`], using [`bincode`] to serialize/deserialize messages.
1456    ///
1457    /// Each element in the stream must be a tuple `(MemberId<L2>, T)` where the first element
1458    /// specifies which cluster member should receive the data. Unlike [`Stream::broadcast_bincode`],
1459    /// this API allows precise targeting of specific cluster members rather than broadcasting to
1460    /// all members.
1461    ///
1462    /// Each cluster member sends its local stream elements, and they are collected at each
1463    /// destination member as a [`KeyedStream`] where keys identify the source cluster member.
1464    ///
1465    /// # Example
1466    /// ```rust
1467    /// # #[cfg(feature = "deploy")] {
1468    /// # use hydro_lang::prelude::*;
1469    /// # use futures::StreamExt;
1470    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
1471    /// # type Source = ();
1472    /// # type Destination = ();
1473    /// let source: Cluster<Source> = flow.cluster::<Source>();
1474    /// let to_send: Stream<_, Cluster<_>, _> = source
1475    ///     .source_iter(q!(vec![0, 1, 2, 3]))
1476    ///     .map(q!(|x| (hydro_lang::location::MemberId::from_raw_id(x), x)));
1477    /// let destination: Cluster<Destination> = flow.cluster::<Destination>();
1478    /// let all_received = to_send.demux_bincode(&destination); // KeyedStream<MemberId<Source>, i32, ...>
1479    /// # all_received.entries().send_bincode(&p2).entries()
1480    /// # }, |mut stream| async move {
1481    /// // if there are 4 members in the destination cluster, each receives one message from each source member
1482    /// // - Destination(0): { Source(0): [0], Source(1): [0], ... }
1483    /// // - Destination(1): { Source(0): [1], Source(1): [1], ... }
1484    /// // - ...
1485    /// # let mut results = Vec::new();
1486    /// # for w in 0..16 {
1487    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
1488    /// # }
1489    /// # results.sort();
1490    /// # assert_eq!(results, vec![
1491    /// #   "(MemberId::<()>(0), (MemberId::<()>(0), 0))", "(MemberId::<()>(0), (MemberId::<()>(1), 0))", "(MemberId::<()>(0), (MemberId::<()>(2), 0))", "(MemberId::<()>(0), (MemberId::<()>(3), 0))",
1492    /// #   "(MemberId::<()>(1), (MemberId::<()>(0), 1))", "(MemberId::<()>(1), (MemberId::<()>(1), 1))", "(MemberId::<()>(1), (MemberId::<()>(2), 1))", "(MemberId::<()>(1), (MemberId::<()>(3), 1))",
1493    /// #   "(MemberId::<()>(2), (MemberId::<()>(0), 2))", "(MemberId::<()>(2), (MemberId::<()>(1), 2))", "(MemberId::<()>(2), (MemberId::<()>(2), 2))", "(MemberId::<()>(2), (MemberId::<()>(3), 2))",
1494    /// #   "(MemberId::<()>(3), (MemberId::<()>(0), 3))", "(MemberId::<()>(3), (MemberId::<()>(1), 3))", "(MemberId::<()>(3), (MemberId::<()>(2), 3))", "(MemberId::<()>(3), (MemberId::<()>(3), 3))"
1495    /// # ]);
1496    /// # }));
1497    /// # }
1498    /// ```
1499    pub fn demux_bincode(
1500        self,
1501        other: &Cluster<'a, L2>,
1502    ) -> KeyedStream<MemberId<L>, T, Cluster<'a, L2>, Unbounded, O, R>
1503    where
1504        T: Serialize + DeserializeOwned,
1505    {
1506        self.demux(other, TCP.fail_stop().bincode())
1507    }
1508
1509    /// Sends elements of this stream at each source member to specific members of a destination
1510    /// cluster, identified by a [`MemberId`], using the configuration in `via` to set up the
1511    /// message transport.
1512    ///
1513    /// Each element in the stream must be a tuple `(MemberId<L2>, T)` where the first element
1514    /// specifies which cluster member should receive the data. Unlike [`Stream::broadcast`],
1515    /// this API allows precise targeting of specific cluster members rather than broadcasting to
1516    /// all members.
1517    ///
1518    /// Each cluster member sends its local stream elements, and they are collected at each
1519    /// destination member as a [`KeyedStream`] where keys identify the source cluster member.
1520    ///
1521    /// # Example
1522    /// ```rust
1523    /// # #[cfg(feature = "deploy")] {
1524    /// # use hydro_lang::prelude::*;
1525    /// # use futures::StreamExt;
1526    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
1527    /// # type Source = ();
1528    /// # type Destination = ();
1529    /// let source: Cluster<Source> = flow.cluster::<Source>();
1530    /// let to_send: Stream<_, Cluster<_>, _> = source
1531    ///     .source_iter(q!(vec![0, 1, 2, 3]))
1532    ///     .map(q!(|x| (hydro_lang::location::MemberId::from_raw_id(x), x)));
1533    /// let destination: Cluster<Destination> = flow.cluster::<Destination>();
1534    /// let all_received = to_send.demux(&destination, TCP.fail_stop().bincode()); // KeyedStream<MemberId<Source>, i32, ...>
1535    /// # all_received.entries().send(&p2, TCP.fail_stop().bincode()).entries()
1536    /// # }, |mut stream| async move {
1537    /// // if there are 4 members in the destination cluster, each receives one message from each source member
1538    /// // - Destination(0): { Source(0): [0], Source(1): [0], ... }
1539    /// // - Destination(1): { Source(0): [1], Source(1): [1], ... }
1540    /// // - ...
1541    /// # let mut results = Vec::new();
1542    /// # for w in 0..16 {
1543    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
1544    /// # }
1545    /// # results.sort();
1546    /// # assert_eq!(results, vec![
1547    /// #   "(MemberId::<()>(0), (MemberId::<()>(0), 0))", "(MemberId::<()>(0), (MemberId::<()>(1), 0))", "(MemberId::<()>(0), (MemberId::<()>(2), 0))", "(MemberId::<()>(0), (MemberId::<()>(3), 0))",
1548    /// #   "(MemberId::<()>(1), (MemberId::<()>(0), 1))", "(MemberId::<()>(1), (MemberId::<()>(1), 1))", "(MemberId::<()>(1), (MemberId::<()>(2), 1))", "(MemberId::<()>(1), (MemberId::<()>(3), 1))",
1549    /// #   "(MemberId::<()>(2), (MemberId::<()>(0), 2))", "(MemberId::<()>(2), (MemberId::<()>(1), 2))", "(MemberId::<()>(2), (MemberId::<()>(2), 2))", "(MemberId::<()>(2), (MemberId::<()>(3), 2))",
1550    /// #   "(MemberId::<()>(3), (MemberId::<()>(0), 3))", "(MemberId::<()>(3), (MemberId::<()>(1), 3))", "(MemberId::<()>(3), (MemberId::<()>(2), 3))", "(MemberId::<()>(3), (MemberId::<()>(3), 3))"
1551    /// # ]);
1552    /// # }));
1553    /// # }
1554    /// ```
1555    pub fn demux<N: NetworkFor<T>>(
1556        self,
1557        to: &Cluster<'a, L2>,
1558        via: N,
1559    ) -> KeyedStream<
1560        MemberId<L>,
1561        T,
1562        Cluster<'a, L2, NoConsistency>,
1563        Unbounded,
1564        <O as MinOrder<N::OrderingGuarantee>>::Min,
1565        <R as MinRetries<N::RetriesGuarantee>>::Min,
1566    >
1567    where
1568        O: MinOrder<N::OrderingGuarantee>,
1569        R: MinRetries<N::RetriesGuarantee>,
1570    {
1571        self.into_keyed().demux(to, via)
1572    }
1573}
1574
1575#[cfg(test)]
1576mod tests {
1577    #[cfg(feature = "sim")]
1578    use stageleft::q;
1579
1580    #[cfg(feature = "sim")]
1581    use crate::live_collections::sliced::sliced;
1582    #[cfg(feature = "sim")]
1583    use crate::location::{Location, MemberId};
1584    #[cfg(feature = "sim")]
1585    use crate::networking::TCP;
1586    #[cfg(feature = "sim")]
1587    use crate::nondet::nondet;
1588    #[cfg(feature = "sim")]
1589    use crate::prelude::FlowBuilder;
1590
1591    #[cfg(feature = "sim")]
1592    #[test]
1593    fn sim_send_bincode_o2o() {
1594        use crate::networking::TCP;
1595
1596        let mut flow = FlowBuilder::new();
1597        let node = flow.process::<()>();
1598        let node2 = flow.process::<()>();
1599
1600        let (in_send, input) = node.sim_input();
1601
1602        let out_recv = input
1603            .send(&node2, TCP.fail_stop().bincode())
1604            .batch(&node2.tick(), nondet!(/** test */))
1605            .count()
1606            .all_ticks()
1607            .sim_output();
1608
1609        let instances = flow.sim().exhaustive(async || {
1610            in_send.send(());
1611            in_send.send(());
1612            in_send.send(());
1613
1614            let received = out_recv.collect::<Vec<_>>().await;
1615            assert_eq!(received.into_iter().sum::<usize>(), 3);
1616        });
1617
1618        assert_eq!(instances, 4); // 2^{3 - 1}
1619    }
1620
1621    #[cfg(feature = "sim")]
1622    #[test]
1623    fn sim_send_bincode_m2o() {
1624        let mut flow = FlowBuilder::new();
1625        let cluster = flow.cluster::<()>();
1626        let node = flow.process::<()>();
1627
1628        let input = cluster.source_iter(q!(vec![1]));
1629
1630        let out_recv = input
1631            .send(&node, TCP.fail_stop().bincode())
1632            .entries()
1633            .batch(&node.tick(), nondet!(/** test */))
1634            .all_ticks()
1635            .sim_output();
1636
1637        let instances = flow
1638            .sim()
1639            .with_cluster_size(&cluster, 4)
1640            .exhaustive(async || {
1641                out_recv
1642                    .assert_yields_only_unordered(vec![
1643                        (MemberId::from_raw_id(0), 1),
1644                        (MemberId::from_raw_id(1), 1),
1645                        (MemberId::from_raw_id(2), 1),
1646                        (MemberId::from_raw_id(3), 1),
1647                    ])
1648                    .await
1649            });
1650
1651        assert_eq!(instances, 75); // ∑ (k=1 to 4) S(4,k) × k! = 75
1652    }
1653
1654    #[cfg(feature = "sim")]
1655    #[test]
1656    fn sim_send_bincode_multiple_m2o() {
1657        let mut flow = FlowBuilder::new();
1658        let cluster1 = flow.cluster::<()>();
1659        let cluster2 = flow.cluster::<()>();
1660        let node = flow.process::<()>();
1661
1662        let out_recv_1 = cluster1
1663            .source_iter(q!(vec![1]))
1664            .send(&node, TCP.fail_stop().bincode())
1665            .entries()
1666            .sim_output();
1667
1668        let out_recv_2 = cluster2
1669            .source_iter(q!(vec![2]))
1670            .send(&node, TCP.fail_stop().bincode())
1671            .entries()
1672            .sim_output();
1673
1674        let instances = flow
1675            .sim()
1676            .with_cluster_size(&cluster1, 3)
1677            .with_cluster_size(&cluster2, 4)
1678            .exhaustive(async || {
1679                out_recv_1
1680                    .assert_yields_only_unordered(vec![
1681                        (MemberId::from_raw_id(0), 1),
1682                        (MemberId::from_raw_id(1), 1),
1683                        (MemberId::from_raw_id(2), 1),
1684                    ])
1685                    .await;
1686
1687                out_recv_2
1688                    .assert_yields_only_unordered(vec![
1689                        (MemberId::from_raw_id(0), 2),
1690                        (MemberId::from_raw_id(1), 2),
1691                        (MemberId::from_raw_id(2), 2),
1692                        (MemberId::from_raw_id(3), 2),
1693                    ])
1694                    .await;
1695            });
1696
1697        assert_eq!(instances, 1);
1698    }
1699
1700    #[cfg(feature = "sim")]
1701    #[test]
1702    fn sim_send_bincode_o2m() {
1703        let mut flow = FlowBuilder::new();
1704        let cluster = flow.cluster::<()>();
1705        let node = flow.process::<()>();
1706
1707        let input = node.source_iter(q!(vec![
1708            (MemberId::from_raw_id(0), 123),
1709            (MemberId::from_raw_id(1), 456),
1710        ]));
1711
1712        let out_recv = input
1713            .demux(&cluster, TCP.fail_stop().bincode())
1714            .map(q!(|x| x + 1))
1715            .send(&node, TCP.fail_stop().bincode())
1716            .entries()
1717            .sim_output();
1718
1719        flow.sim()
1720            .with_cluster_size(&cluster, 4)
1721            .exhaustive(async || {
1722                out_recv
1723                    .assert_yields_only_unordered(vec![
1724                        (MemberId::from_raw_id(0), 124),
1725                        (MemberId::from_raw_id(1), 457),
1726                    ])
1727                    .await
1728            });
1729    }
1730
1731    #[cfg(feature = "sim")]
1732    #[test]
1733    fn sim_broadcast_bincode_o2m() {
1734        let mut flow = FlowBuilder::new();
1735        let cluster = flow.cluster::<()>();
1736        let node = flow.process::<()>();
1737
1738        let input = node.source_iter(q!(vec![123, 456]));
1739
1740        let out_recv = input
1741            .broadcast(&cluster, TCP.fail_stop().bincode(), nondet!(/** test */))
1742            .map(q!(|x| x + 1))
1743            .send(&node, TCP.fail_stop().bincode())
1744            .entries()
1745            .sim_output();
1746
1747        let mut c_1_produced = false;
1748        let mut c_2_produced = false;
1749        let mut c_1_saw_457_but_not_124 = false;
1750
1751        flow.sim()
1752            .with_cluster_size(&cluster, 2)
1753            .exhaustive(async || {
1754                let all_out = out_recv.collect_sorted::<Vec<_>>().await;
1755
1756                // check that order is preserved
1757                if all_out.contains(&(MemberId::from_raw_id(0), 124)) {
1758                    assert!(all_out.contains(&(MemberId::from_raw_id(0), 457)));
1759                    c_1_produced = true;
1760                }
1761
1762                if all_out.contains(&(MemberId::from_raw_id(1), 124)) {
1763                    assert!(all_out.contains(&(MemberId::from_raw_id(1), 457)));
1764                    c_2_produced = true;
1765                }
1766
1767                if all_out.contains(&(MemberId::from_raw_id(0), 457))
1768                    && !all_out.contains(&(MemberId::from_raw_id(0), 124))
1769                {
1770                    c_1_saw_457_but_not_124 = true;
1771                }
1772            });
1773
1774        assert!(c_1_produced && c_2_produced); // in at least one execution each, the cluster member received both messages
1775
1776        // in at least one execution, the cluster member received 457 but not 124, this tests
1777        // that the simulator properly explores dynamic membership additions (a member that joins after 123 is broadcast)
1778        assert!(c_1_saw_457_but_not_124);
1779    }
1780
1781    #[cfg(feature = "sim")]
1782    #[test]
1783    fn sim_send_bincode_m2m() {
1784        let mut flow = FlowBuilder::new();
1785        let cluster = flow.cluster::<()>();
1786        let node = flow.process::<()>();
1787
1788        let input = node.source_iter(q!(vec![
1789            (MemberId::from_raw_id(0), 123),
1790            (MemberId::from_raw_id(1), 456),
1791        ]));
1792
1793        let out_recv = input
1794            .demux(&cluster, TCP.fail_stop().bincode())
1795            .map(q!(|x| x + 1))
1796            .flat_map_ordered(q!(|x| vec![
1797                (MemberId::from_raw_id(0), x),
1798                (MemberId::from_raw_id(1), x),
1799            ]))
1800            .demux(&cluster, TCP.fail_stop().bincode())
1801            .entries()
1802            .send(&node, TCP.fail_stop().bincode())
1803            .entries()
1804            .sim_output();
1805
1806        flow.sim()
1807            .with_cluster_size(&cluster, 4)
1808            .exhaustive(async || {
1809                out_recv
1810                    .assert_yields_only_unordered(vec![
1811                        (MemberId::from_raw_id(0), (MemberId::from_raw_id(0), 124)),
1812                        (MemberId::from_raw_id(0), (MemberId::from_raw_id(1), 457)),
1813                        (MemberId::from_raw_id(1), (MemberId::from_raw_id(0), 124)),
1814                        (MemberId::from_raw_id(1), (MemberId::from_raw_id(1), 457)),
1815                    ])
1816                    .await
1817            });
1818    }
1819
1820    #[cfg(feature = "sim")]
1821    #[test]
1822    fn sim_lossy_delayed_forever_o2o() {
1823        use std::collections::HashSet;
1824
1825        use crate::properties::manual_proof;
1826
1827        let mut flow = FlowBuilder::new();
1828        let node = flow.process::<()>();
1829        let node2 = flow.process::<()>();
1830
1831        let received = node
1832            .source_iter(q!(0..3_u32))
1833            .send(&node2, TCP.lossy_delayed_forever().bincode())
1834            .fold(
1835                q!(|| std::collections::HashSet::<u32>::new()),
1836                q!(
1837                    |set, v| {
1838                        set.insert(v);
1839                    },
1840                    commutative = manual_proof!(/** set insert is commutative */)
1841                ),
1842            );
1843
1844        let out_recv = sliced! {
1845            let snapshot = use::snapshot(received, nondet!(/** test */));
1846            snapshot.into_stream()
1847        }
1848        .sim_output();
1849
1850        let mut saw_non_contiguous = false;
1851
1852        flow.sim().test_safety_only().exhaustive(async || {
1853            let snapshots = out_recv.collect::<Vec<HashSet<u32>>>().await;
1854
1855            // Check each individual snapshot for a non-contiguous subset.
1856            for set in &snapshots {
1857                #[expect(clippy::disallowed_methods, reason = "min / max are deterministic")]
1858                if set.len() >= 2 && set.len() < 3 {
1859                    let min = *set.iter().min().unwrap();
1860                    let max = *set.iter().max().unwrap();
1861                    if set.len() < (max - min + 1) as usize {
1862                        saw_non_contiguous = true;
1863                    }
1864                }
1865            }
1866        });
1867
1868        assert!(
1869            saw_non_contiguous,
1870            "Expected at least one execution with a non-contiguous subset of inputs"
1871        );
1872    }
1873
1874    #[cfg(feature = "sim")]
1875    #[test]
1876    fn sim_udp_lossy_delayed_forever_o2o() {
1877        use std::collections::HashSet;
1878
1879        use crate::networking::UDP;
1880        use crate::properties::manual_proof;
1881
1882        let mut flow = FlowBuilder::new();
1883        let node = flow.process::<()>();
1884        let node2 = flow.process::<()>();
1885
1886        let received = node
1887            .source_iter(q!(0..3_u32))
1888            .send(&node2, UDP.lossy_delayed_forever().bincode())
1889            .fold(
1890                q!(|| std::collections::HashSet::<u32>::new()),
1891                q!(
1892                    |set, v| {
1893                        set.insert(v);
1894                    },
1895                    commutative = manual_proof!(/** set insert is commutative */)
1896                ),
1897            );
1898
1899        let out_recv = sliced! {
1900            let snapshot = use::snapshot(received, nondet!(/** test */));
1901            snapshot.into_stream()
1902        }
1903        .sim_output();
1904
1905        let mut saw_non_contiguous = false;
1906
1907        flow.sim().test_safety_only().exhaustive(async || {
1908            let snapshots = out_recv.collect::<Vec<HashSet<u32>>>().await;
1909
1910            // Check each individual snapshot for a non-contiguous subset.
1911            for set in &snapshots {
1912                #[expect(clippy::disallowed_methods, reason = "min / max are deterministic")]
1913                if set.len() >= 2 && set.len() < 3 {
1914                    let min = *set.iter().min().unwrap();
1915                    let max = *set.iter().max().unwrap();
1916                    if set.len() < (max - min + 1) as usize {
1917                        saw_non_contiguous = true;
1918                    }
1919                }
1920            }
1921        });
1922
1923        assert!(
1924            saw_non_contiguous,
1925            "Expected at least one execution with a non-contiguous subset of inputs"
1926        );
1927    }
1928
1929    #[cfg(feature = "sim")]
1930    #[test]
1931    fn sim_broadcast_closed_o2m() {
1932        let mut flow = FlowBuilder::new();
1933        let cluster = flow.cluster::<()>();
1934        let node = flow.process::<()>();
1935
1936        let input = node.source_iter(q!(vec![123, 456]));
1937
1938        let out_recv = input
1939            .broadcast_closed(&cluster, TCP.fail_stop().bincode())
1940            .send(&node, TCP.fail_stop().bincode())
1941            .entries()
1942            .sim_output();
1943
1944        flow.sim()
1945            .with_cluster_size(&cluster, 2)
1946            .exhaustive(async || {
1947                out_recv
1948                    .assert_yields_only_unordered(vec![
1949                        (MemberId::from_raw_id(0), 123),
1950                        (MemberId::from_raw_id(0), 456),
1951                        (MemberId::from_raw_id(1), 123),
1952                        (MemberId::from_raw_id(1), 456),
1953                    ])
1954                    .await
1955            });
1956    }
1957
1958    #[cfg(feature = "sim")]
1959    #[test]
1960    fn sim_broadcast_closed_m2m() {
1961        let mut flow = FlowBuilder::new();
1962        let source = flow.cluster::<()>();
1963        let dest: crate::location::Cluster<'_, ()> = flow.cluster::<()>();
1964        let node = flow.process::<()>();
1965
1966        let input = source.source_iter(q!(vec![123]));
1967
1968        // Broadcast from source cluster to dest cluster, then collect at a process.
1969        let out_recv = input
1970            .broadcast_closed(&dest, TCP.fail_stop().bincode())
1971            .entries()
1972            .send(&node, TCP.fail_stop().bincode())
1973            .entries()
1974            .sim_output();
1975
1976        flow.sim()
1977            .with_cluster_size(&source, 2)
1978            .with_cluster_size(&dest, 2)
1979            .exhaustive(async || {
1980                // Each source member (0, 1) broadcasts 123 to each dest member (0, 1).
1981                // The dest members then send to the process keyed by dest member id.
1982                // Each dest member receives (source_0, 123) and (source_1, 123).
1983                out_recv
1984                    .assert_yields_only_unordered(vec![
1985                        (MemberId::from_raw_id(0), (MemberId::from_raw_id(0), 123)),
1986                        (MemberId::from_raw_id(0), (MemberId::from_raw_id(1), 123)),
1987                        (MemberId::from_raw_id(1), (MemberId::from_raw_id(0), 123)),
1988                        (MemberId::from_raw_id(1), (MemberId::from_raw_id(1), 123)),
1989                    ])
1990                    .await
1991            });
1992    }
1993
1994    /// Compile-time check that the consistency guarantee of `broadcast_closed` output tracks
1995    /// the network's failure policy: `fail_stop` and `lossy_delayed_forever` preserve
1996    /// [`EventualConsistency`], while plain `lossy` only provides [`NoConsistency`].
1997    #[cfg(feature = "sim")]
1998    #[test]
1999    fn broadcast_closed_consistency_tracks_failure_policy() {
2000        use crate::live_collections::keyed_stream::KeyedStream;
2001        use crate::live_collections::stream::Stream;
2002        use crate::location::Cluster;
2003        use crate::location::cluster::{EventualConsistency, NoConsistency};
2004
2005        let mut flow = FlowBuilder::new();
2006        let cluster = flow.cluster::<()>();
2007        let source = flow.cluster::<()>();
2008        let node = flow.process::<()>();
2009
2010        // `fail_stop` models a failed connection as the recipient having failed, preserving
2011        // eventual consistency across live members.
2012        let _: Stream<u32, Cluster<'_, (), EventualConsistency>, _, _, _> = node
2013            .source_iter(q!(vec![1u32]))
2014            .broadcast_closed(&cluster, TCP.fail_stop().bincode());
2015
2016        // `lossy_delayed_forever` models drops as indefinite delays, preserving eventual
2017        // consistency.
2018        let _: Stream<u32, Cluster<'_, (), EventualConsistency>, _, _, _> = node
2019            .source_iter(q!(vec![1u32]))
2020            .broadcast_closed(&cluster, TCP.lossy_delayed_forever().bincode());
2021
2022        // Plain `lossy` can drop messages for some members while delivering them to others,
2023        // so replicas may permanently diverge.
2024        let _: Stream<u32, Cluster<'_, (), NoConsistency>, _, _, _> = node
2025            .source_iter(q!(vec![1u32]))
2026            .broadcast_closed(&cluster, TCP.lossy(nondet!(/** test */)).bincode());
2027
2028        // The same applies to cluster-to-cluster closed broadcasts.
2029        let _: KeyedStream<MemberId<()>, u32, Cluster<'_, (), EventualConsistency>, _, _, _> =
2030            source
2031                .source_iter(q!(vec![1u32]))
2032                .broadcast_closed(&cluster, TCP.fail_stop().bincode());
2033
2034        let _: KeyedStream<MemberId<()>, u32, Cluster<'_, (), NoConsistency>, _, _, _> = source
2035            .source_iter(q!(vec![1u32]))
2036            .broadcast_closed(&cluster, TCP.lossy(nondet!(/** test */)).bincode());
2037
2038        let _ = flow.finalize();
2039    }
2040}