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