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::snapshot(ids, nondet_membership);
330            let elements = use::batch(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>(
454        self,
455        other: &External<'_, L2>,
456    ) -> ExternalBincodeStream<T, O, R>
457    where
458        T: Serialize + DeserializeOwned,
459    {
460        let serialize_pipeline = Some(serialize_bincode::<T>(false));
461
462        let mut flow_state_borrow = self.location.flow_state().borrow_mut();
463
464        let external_port_id = flow_state_borrow.next_external_port();
465
466        flow_state_borrow.push_root(HydroRoot::SendExternal {
467            to_external_key: other.key,
468            to_port_id: external_port_id,
469            to_many: false,
470            unpaired: true,
471            serialize_fn: serialize_pipeline.map(|e| e.into()),
472            instantiate_fn: DebugInstantiate::Building,
473            input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
474            op_metadata: HydroIrOpMetadata::new(),
475        });
476
477        ExternalBincodeStream {
478            process_key: other.key,
479            port_id: external_port_id,
480            _phantom: PhantomData,
481        }
482    }
483
484    #[cfg(feature = "sim")]
485    /// Sets up a simulation output port for this stream, allowing test code to receive elements
486    /// sent to this stream during simulation.
487    pub fn sim_output(self) -> SimReceiver<T, O, R>
488    where
489        T: Serialize + DeserializeOwned,
490    {
491        let external_location: External<'a, ()> = External {
492            key: LocationKey::FIRST,
493            flow_state: self.location.flow_state().clone(),
494            _phantom: PhantomData,
495        };
496
497        let external = self.send_bincode_external(&external_location);
498
499        SimReceiver(external.port_id, PhantomData)
500    }
501}
502
503impl<'a, T, L: Location<'a>, B: Boundedness> Stream<T, L, B, TotalOrder, ExactlyOnce> {
504    /// Creates an external output for embedded deployment mode.
505    ///
506    /// The `name` parameter specifies the name of the field in the generated
507    /// `EmbeddedOutputs` struct that will receive elements from this stream.
508    /// The generated function will accept an `EmbeddedOutputs` struct with an
509    /// `impl FnMut(T)` field with this name.
510    pub fn embedded_output(self, name: impl Into<String>) {
511        let ident = syn::Ident::new(&name.into(), proc_macro2::Span::call_site());
512
513        self.location
514            .flow_state()
515            .borrow_mut()
516            .push_root(HydroRoot::EmbeddedOutput {
517                ident,
518                input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
519                op_metadata: HydroIrOpMetadata::new(),
520            });
521    }
522}
523
524impl<'a, T, L, L2, B: Boundedness, O: Ordering, R: Retries>
525    Stream<(MemberId<L2>, T), Process<'a, L>, B, O, R>
526{
527    #[deprecated = "use Stream::demux(..., TCP.fail_stop().bincode()) instead"]
528    /// Sends elements of this stream to specific members of a cluster, identified by a [`MemberId`],
529    /// using [`bincode`] to serialize/deserialize messages.
530    ///
531    /// Each element in the stream must be a tuple `(MemberId<L2>, T)` where the first element
532    /// specifies which cluster member should receive the data. Unlike [`Stream::broadcast_bincode`],
533    /// this API allows precise targeting of specific cluster members rather than broadcasting to
534    /// all members.
535    ///
536    /// # Example
537    /// ```rust
538    /// # #[cfg(feature = "deploy")] {
539    /// # use hydro_lang::prelude::*;
540    /// # use futures::StreamExt;
541    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
542    /// let p1 = flow.process::<()>();
543    /// let workers: Cluster<()> = flow.cluster::<()>();
544    /// let numbers: Stream<_, Process<_>, _> = p1.source_iter(q!(vec![0, 1, 2, 3]));
545    /// let on_worker: Stream<_, Cluster<_>, _> = numbers
546    ///     .map(q!(|x| (hydro_lang::location::MemberId::from_raw_id(x), x)))
547    ///     .demux_bincode(&workers);
548    /// # on_worker.send_bincode(&p2).entries()
549    /// // if there are 4 members in the cluster, each receives one element
550    /// // - MemberId::<()>(0): [0]
551    /// // - MemberId::<()>(1): [1]
552    /// // - MemberId::<()>(2): [2]
553    /// // - MemberId::<()>(3): [3]
554    /// # }, |mut stream| async move {
555    /// # let mut results = Vec::new();
556    /// # for w in 0..4 {
557    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
558    /// # }
559    /// # results.sort();
560    /// # assert_eq!(results, vec!["(MemberId::<()>(0), 0)", "(MemberId::<()>(1), 1)", "(MemberId::<()>(2), 2)", "(MemberId::<()>(3), 3)"]);
561    /// # }));
562    /// # }
563    /// ```
564    pub fn demux_bincode(
565        self,
566        other: &Cluster<'a, L2>,
567    ) -> Stream<T, Cluster<'a, L2>, Unbounded, O, R>
568    where
569        T: Serialize + DeserializeOwned,
570    {
571        self.demux(other, TCP.fail_stop().bincode())
572    }
573
574    /// Sends elements of this stream to specific members of a cluster, identified by a [`MemberId`],
575    /// using the configuration in `via` to set up the message transport.
576    ///
577    /// Each element in the stream must be a tuple `(MemberId<L2>, T)` where the first element
578    /// specifies which cluster member should receive the data. Unlike [`Stream::broadcast`],
579    /// this API allows precise targeting of specific cluster members rather than broadcasting to
580    /// all members.
581    ///
582    /// # Example
583    /// ```rust
584    /// # #[cfg(feature = "deploy")] {
585    /// # use hydro_lang::prelude::*;
586    /// # use futures::StreamExt;
587    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
588    /// let p1 = flow.process::<()>();
589    /// let workers: Cluster<()> = flow.cluster::<()>();
590    /// let numbers: Stream<_, Process<_>, _> = p1.source_iter(q!(vec![0, 1, 2, 3]));
591    /// let on_worker: Stream<_, Cluster<_>, _> = numbers
592    ///     .map(q!(|x| (hydro_lang::location::MemberId::from_raw_id(x), x)))
593    ///     .demux(&workers, TCP.fail_stop().bincode());
594    /// # on_worker.send(&p2, TCP.fail_stop().bincode()).entries()
595    /// // if there are 4 members in the cluster, each receives one element
596    /// // - MemberId::<()>(0): [0]
597    /// // - MemberId::<()>(1): [1]
598    /// // - MemberId::<()>(2): [2]
599    /// // - MemberId::<()>(3): [3]
600    /// # }, |mut stream| async move {
601    /// # let mut results = Vec::new();
602    /// # for w in 0..4 {
603    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
604    /// # }
605    /// # results.sort();
606    /// # assert_eq!(results, vec!["(MemberId::<()>(0), 0)", "(MemberId::<()>(1), 1)", "(MemberId::<()>(2), 2)", "(MemberId::<()>(3), 3)"]);
607    /// # }));
608    /// # }
609    /// ```
610    pub fn demux<N: NetworkFor<T>>(
611        self,
612        to: &Cluster<'a, L2>,
613        via: N,
614    ) -> Stream<
615        T,
616        Cluster<'a, L2, NoConsistency>,
617        Unbounded,
618        <O as MinOrder<N::OrderingGuarantee>>::Min,
619        R,
620    >
621    where
622        T: Serialize + DeserializeOwned,
623        O: MinOrder<N::OrderingGuarantee>,
624    {
625        self.into_keyed().demux(to, via)
626    }
627}
628
629impl<'a, T, L, B: Boundedness> Stream<T, Process<'a, L>, B, TotalOrder, ExactlyOnce> {
630    #[deprecated = "use Stream::round_robin(..., TCP.fail_stop().bincode()) instead"]
631    /// Distributes elements of this stream to cluster members in a round-robin fashion, using
632    /// [`bincode`] to serialize/deserialize messages.
633    ///
634    /// This provides load balancing by evenly distributing work across cluster members. The
635    /// distribution is deterministic based on element order - the first element goes to member 0,
636    /// the second to member 1, and so on, wrapping around when reaching the end of the member list.
637    ///
638    /// # Non-Determinism
639    /// The set of cluster members may asynchronously change over time. Each element is distributed
640    /// based on the current cluster membership _at that point in time_. Depending on when cluster
641    /// members join and leave, the round-robin pattern will change. Furthermore, even when the
642    /// membership is stable, the order of members in the round-robin pattern may change across runs.
643    ///
644    /// # Ordering Requirements
645    /// This method is only available on streams with [`TotalOrder`] and [`ExactlyOnce`], since the
646    /// order of messages and retries affects the round-robin pattern.
647    ///
648    /// # Example
649    /// ```rust
650    /// # #[cfg(feature = "deploy")] {
651    /// # use hydro_lang::prelude::*;
652    /// # use hydro_lang::live_collections::stream::{TotalOrder, ExactlyOnce};
653    /// # use futures::StreamExt;
654    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
655    /// let p1 = flow.process::<()>();
656    /// let workers: Cluster<()> = flow.cluster::<()>();
657    /// let numbers: Stream<_, Process<_>, _, TotalOrder, ExactlyOnce> = p1.source_iter(q!(vec![1, 2, 3, 4]));
658    /// let on_worker: Stream<_, Cluster<_>, _> = numbers.round_robin_bincode(&workers, nondet!(/** assuming stable membership */));
659    /// on_worker.send_bincode(&p2)
660    /// # .first().values() // we use first to assert that each member gets one element
661    /// // with 4 cluster members, elements are distributed (with a non-deterministic round-robin order):
662    /// // - MemberId::<()>(?): [1]
663    /// // - MemberId::<()>(?): [2]
664    /// // - MemberId::<()>(?): [3]
665    /// // - MemberId::<()>(?): [4]
666    /// # }, |mut stream| async move {
667    /// # let mut results = Vec::new();
668    /// # for w in 0..4 {
669    /// #     results.push(stream.next().await.unwrap());
670    /// # }
671    /// # results.sort();
672    /// # assert_eq!(results, vec![1, 2, 3, 4]);
673    /// # }));
674    /// # }
675    /// ```
676    pub fn round_robin_bincode<L2: 'a>(
677        self,
678        other: &Cluster<'a, L2>,
679        nondet_membership: NonDet,
680    ) -> Stream<T, Cluster<'a, L2>, Unbounded, TotalOrder, ExactlyOnce>
681    where
682        T: Serialize + DeserializeOwned,
683    {
684        self.round_robin(other, TCP.fail_stop().bincode(), nondet_membership)
685    }
686
687    /// Distributes elements of this stream to cluster members in a round-robin fashion, using
688    /// the configuration in `via` to set up the message transport.
689    ///
690    /// This provides load balancing by evenly distributing work across cluster members. The
691    /// distribution is deterministic based on element order - the first element goes to member 0,
692    /// the second to member 1, and so on, wrapping around when reaching the end of the member list.
693    ///
694    /// # Non-Determinism
695    /// The set of cluster members may asynchronously change over time. Each element is distributed
696    /// based on the current cluster membership _at that point in time_. Depending on when cluster
697    /// members join and leave, the round-robin pattern will change. Furthermore, even when the
698    /// membership is stable, the order of members in the round-robin pattern may change across runs.
699    ///
700    /// # Ordering Requirements
701    /// This method is only available on streams with [`TotalOrder`] and [`ExactlyOnce`], since the
702    /// order of messages and retries affects the round-robin pattern.
703    ///
704    /// # Example
705    /// ```rust
706    /// # #[cfg(feature = "deploy")] {
707    /// # use hydro_lang::prelude::*;
708    /// # use hydro_lang::live_collections::stream::{TotalOrder, ExactlyOnce};
709    /// # use futures::StreamExt;
710    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
711    /// let p1 = flow.process::<()>();
712    /// let workers: Cluster<()> = flow.cluster::<()>();
713    /// let numbers: Stream<_, Process<_>, _, TotalOrder, ExactlyOnce> = p1.source_iter(q!(vec![1, 2, 3, 4]));
714    /// let on_worker: Stream<_, Cluster<_>, _> = numbers.round_robin(&workers, TCP.fail_stop().bincode(), nondet!(/** assuming stable membership */));
715    /// on_worker.send(&p2, TCP.fail_stop().bincode())
716    /// # .first().values() // we use first to assert that each member gets one element
717    /// // with 4 cluster members, elements are distributed (with a non-deterministic round-robin order):
718    /// // - MemberId::<()>(?): [1]
719    /// // - MemberId::<()>(?): [2]
720    /// // - MemberId::<()>(?): [3]
721    /// // - MemberId::<()>(?): [4]
722    /// # }, |mut stream| async move {
723    /// # let mut results = Vec::new();
724    /// # for w in 0..4 {
725    /// #     results.push(stream.next().await.unwrap());
726    /// # }
727    /// # results.sort();
728    /// # assert_eq!(results, vec![1, 2, 3, 4]);
729    /// # }));
730    /// # }
731    /// ```
732    pub fn round_robin<L2: 'a, N: NetworkFor<T>>(
733        self,
734        to: &Cluster<'a, L2>,
735        via: N,
736        nondet_membership: NonDet,
737    ) -> Stream<T, Cluster<'a, L2>, Unbounded, N::OrderingGuarantee, ExactlyOnce>
738    where
739        T: Serialize + DeserializeOwned,
740    {
741        let ids = track_membership(self.location.source_cluster_membership_stream(
742            to,
743            nondet!(/** dropped prefixes don't affect broadcast */),
744        ));
745        sliced! {
746            let members_snapshot = use::snapshot(ids, nondet_membership);
747            let elements = use::batch(self.enumerate(), nondet_membership);
748
749            let current_members = members_snapshot
750                .filter(q!(|b| *b))
751                .keys()
752                .assume_ordering::<TotalOrder>(nondet_membership)
753                .collect_vec();
754
755            elements
756                .cross_singleton(current_members)
757                .filter_map(q!(|(data, members)| {
758                    if members.is_empty() {
759                        None
760                    } else {
761                        Some((members[data.0 % members.len()].clone(), data.1))
762                    }
763                }))
764        }
765        .demux(to, via)
766    }
767}
768
769impl<'a, T, L, B: Boundedness, C: Consistency>
770    Stream<T, Cluster<'a, L, C>, B, TotalOrder, ExactlyOnce>
771{
772    #[deprecated = "use Stream::round_robin(..., TCP.fail_stop().bincode()) instead"]
773    /// Distributes elements of this stream to cluster members in a round-robin fashion, using
774    /// [`bincode`] to serialize/deserialize messages.
775    ///
776    /// This provides load balancing by evenly distributing work across cluster members. The
777    /// distribution is deterministic based on element order - the first element goes to member 0,
778    /// the second to member 1, and so on, wrapping around when reaching the end of the member list.
779    ///
780    /// # Non-Determinism
781    /// The set of cluster members may asynchronously change over time. Each element is distributed
782    /// based on the current cluster membership _at that point in time_. Depending on when cluster
783    /// members join and leave, the round-robin pattern will change. Furthermore, even when the
784    /// membership is stable, the order of members in the round-robin pattern may change across runs.
785    ///
786    /// # Ordering Requirements
787    /// This method is only available on streams with [`TotalOrder`] and [`ExactlyOnce`], since the
788    /// order of messages and retries affects the round-robin pattern.
789    ///
790    /// # Example
791    /// ```rust
792    /// # #[cfg(feature = "deploy")] {
793    /// # use hydro_lang::prelude::*;
794    /// # use hydro_lang::live_collections::stream::{TotalOrder, ExactlyOnce, NoOrder};
795    /// # use hydro_lang::location::MemberId;
796    /// # use futures::StreamExt;
797    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
798    /// let p1 = flow.process::<()>();
799    /// let workers1: Cluster<()> = flow.cluster::<()>();
800    /// let workers2: Cluster<()> = flow.cluster::<()>();
801    /// let numbers: Stream<_, Process<_>, _, TotalOrder, ExactlyOnce> = p1.source_iter(q!(0..=16));
802    /// let on_worker1: Stream<_, Cluster<_>, _> = numbers.round_robin_bincode(&workers1, nondet!(/** assuming stable membership */));
803    /// let on_worker2: Stream<_, Cluster<_>, _> = on_worker1.round_robin_bincode(&workers2, nondet!(/** assuming stable membership */)).entries().assume_ordering(nondet!(/** assuming stable membership */));
804    /// on_worker2.send_bincode(&p2)
805    /// # .entries()
806    /// # .map(q!(|(w2, (w1, v))| ((w2, w1), v)))
807    /// # }, |mut stream| async move {
808    /// # let mut results = Vec::new();
809    /// # let mut locations = std::collections::HashSet::new();
810    /// # for w in 0..=16 {
811    /// #     let (location, v) = stream.next().await.unwrap();
812    /// #     locations.insert(location);
813    /// #     results.push(v);
814    /// # }
815    /// # results.sort();
816    /// # assert_eq!(results, (0..=16).collect::<Vec<_>>());
817    /// # assert_eq!(locations.len(), 16);
818    /// # }));
819    /// # }
820    /// ```
821    pub fn round_robin_bincode<L2: 'a>(
822        self,
823        other: &Cluster<'a, L2>,
824        nondet_membership: NonDet,
825    ) -> KeyedStream<MemberId<L>, T, Cluster<'a, L2>, Unbounded, TotalOrder, ExactlyOnce>
826    where
827        T: Serialize + DeserializeOwned,
828    {
829        self.round_robin(other, TCP.fail_stop().bincode(), nondet_membership)
830    }
831
832    /// Distributes elements of this stream to cluster members in a round-robin fashion, using
833    /// the configuration in `via` to set up the message transport.
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(&workers1, TCP.fail_stop().bincode(), nondet!(/** assuming stable membership */));
862    /// 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 */));
863    /// on_worker2.send(&p2, TCP.fail_stop().bincode())
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<L2: 'a, N: NetworkFor<T>>(
881        self,
882        to: &Cluster<'a, L2>,
883        via: N,
884        nondet_membership: NonDet,
885    ) -> KeyedStream<MemberId<L>, T, Cluster<'a, L2>, Unbounded, N::OrderingGuarantee, ExactlyOnce>
886    where
887        T: Serialize + DeserializeOwned,
888    {
889        let ids = track_membership(self.location.source_cluster_membership_stream(
890            to,
891            nondet!(/** dropped prefixes don't affect broadcast */),
892        ));
893        sliced! {
894            let members_snapshot = use::snapshot(ids, nondet_membership);
895            let elements = use::batch(self.enumerate(), nondet_membership);
896
897            let current_members = members_snapshot
898                .filter(q!(|b| *b))
899                .keys()
900                .assume_ordering::<TotalOrder>(nondet_membership)
901                .collect_vec();
902
903            elements
904                .cross_singleton(current_members)
905                .filter_map(q!(|(data, members)| {
906                    if members.is_empty() {
907                        None
908                    } else {
909                        Some((members[data.0 % members.len()].clone(), data.1))
910                    }
911                }))
912        }
913        .demux(to, via)
914    }
915}
916
917impl<'a, T, L, B: Boundedness, C: Consistency, O: Ordering, R: Retries>
918    Stream<T, Cluster<'a, L, C>, B, O, R>
919{
920    #[deprecated = "use Stream::send(..., TCP.fail_stop().bincode()) instead"]
921    /// "Moves" elements of this stream from a cluster to a process by sending them over the network,
922    /// using [`bincode`] to serialize/deserialize messages.
923    ///
924    /// Each cluster member sends its local stream elements, and they are collected at the destination
925    /// as a [`KeyedStream`] where keys identify the source cluster member.
926    ///
927    /// # Example
928    /// ```rust
929    /// # #[cfg(feature = "deploy")] {
930    /// # use hydro_lang::prelude::*;
931    /// # use futures::StreamExt;
932    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, process| {
933    /// let workers: Cluster<()> = flow.cluster::<()>();
934    /// let numbers: Stream<_, Cluster<_>, _> = workers.source_iter(q!(vec![1]));
935    /// let all_received = numbers.send_bincode(&process); // KeyedStream<MemberId<()>, i32, ...>
936    /// # all_received.entries()
937    /// # }, |mut stream| async move {
938    /// // if there are 4 members in the cluster, we should receive 4 elements
939    /// // { MemberId::<()>(0): [1], MemberId::<()>(1): [1], MemberId::<()>(2): [1], MemberId::<()>(3): [1] }
940    /// # let mut results = Vec::new();
941    /// # for w in 0..4 {
942    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
943    /// # }
944    /// # results.sort();
945    /// # assert_eq!(results, vec!["(MemberId::<()>(0), 1)", "(MemberId::<()>(1), 1)", "(MemberId::<()>(2), 1)", "(MemberId::<()>(3), 1)"]);
946    /// # }));
947    /// # }
948    /// ```
949    ///
950    /// If you don't need to know the source for each element, you can use `.values()`
951    /// to get just the data:
952    /// ```rust
953    /// # #[cfg(feature = "deploy")] {
954    /// # use hydro_lang::prelude::*;
955    /// # use hydro_lang::live_collections::stream::NoOrder;
956    /// # use futures::StreamExt;
957    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, process| {
958    /// # let workers: Cluster<()> = flow.cluster::<()>();
959    /// # let numbers: Stream<_, Cluster<_>, _> = workers.source_iter(q!(vec![1]));
960    /// let values: Stream<i32, _, _, NoOrder> = numbers.send_bincode(&process).values();
961    /// # values
962    /// # }, |mut stream| async move {
963    /// # let mut results = Vec::new();
964    /// # for w in 0..4 {
965    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
966    /// # }
967    /// # results.sort();
968    /// // if there are 4 members in the cluster, we should receive 4 elements
969    /// // 1, 1, 1, 1
970    /// # assert_eq!(results, vec!["1", "1", "1", "1"]);
971    /// # }));
972    /// # }
973    /// ```
974    pub fn send_bincode<L2>(
975        self,
976        other: &Process<'a, L2>,
977    ) -> KeyedStream<MemberId<L>, T, Process<'a, L2>, Unbounded, O, R>
978    where
979        T: Serialize + DeserializeOwned,
980    {
981        self.send(other, TCP.fail_stop().bincode())
982    }
983
984    /// "Moves" elements of this stream from a cluster to a process by sending them over the network,
985    /// using the configuration in `via` to set up the message transport.
986    ///
987    /// Each cluster member sends its local stream elements, and they are collected at the destination
988    /// as a [`KeyedStream`] where keys identify the source cluster member.
989    ///
990    /// # Example
991    /// ```rust
992    /// # #[cfg(feature = "deploy")] {
993    /// # use hydro_lang::prelude::*;
994    /// # use futures::StreamExt;
995    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, process| {
996    /// let workers: Cluster<()> = flow.cluster::<()>();
997    /// let numbers: Stream<_, Cluster<_>, _> = workers.source_iter(q!(vec![1]));
998    /// let all_received = numbers.send(&process, TCP.fail_stop().bincode()); // KeyedStream<MemberId<()>, i32, ...>
999    /// # all_received.entries()
1000    /// # }, |mut stream| async move {
1001    /// // if there are 4 members in the cluster, we should receive 4 elements
1002    /// // { MemberId::<()>(0): [1], MemberId::<()>(1): [1], MemberId::<()>(2): [1], MemberId::<()>(3): [1] }
1003    /// # let mut results = Vec::new();
1004    /// # for w in 0..4 {
1005    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
1006    /// # }
1007    /// # results.sort();
1008    /// # assert_eq!(results, vec!["(MemberId::<()>(0), 1)", "(MemberId::<()>(1), 1)", "(MemberId::<()>(2), 1)", "(MemberId::<()>(3), 1)"]);
1009    /// # }));
1010    /// # }
1011    /// ```
1012    ///
1013    /// If you don't need to know the source for each element, you can use `.values()`
1014    /// to get just the data:
1015    /// ```rust
1016    /// # #[cfg(feature = "deploy")] {
1017    /// # use hydro_lang::prelude::*;
1018    /// # use hydro_lang::live_collections::stream::NoOrder;
1019    /// # use futures::StreamExt;
1020    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, process| {
1021    /// # let workers: Cluster<()> = flow.cluster::<()>();
1022    /// # let numbers: Stream<_, Cluster<_>, _> = workers.source_iter(q!(vec![1]));
1023    /// let values: Stream<i32, _, _, NoOrder> =
1024    ///     numbers.send(&process, TCP.fail_stop().bincode()).values();
1025    /// # values
1026    /// # }, |mut stream| async move {
1027    /// # let mut results = Vec::new();
1028    /// # for w in 0..4 {
1029    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
1030    /// # }
1031    /// # results.sort();
1032    /// // if there are 4 members in the cluster, we should receive 4 elements
1033    /// // 1, 1, 1, 1
1034    /// # assert_eq!(results, vec!["1", "1", "1", "1"]);
1035    /// # }));
1036    /// # }
1037    /// ```
1038    pub fn send<L2, N: NetworkFor<T>>(
1039        self,
1040        to: &Process<'a, L2>,
1041        via: N,
1042    ) -> KeyedStream<
1043        MemberId<L>,
1044        T,
1045        Process<'a, L2>,
1046        Unbounded,
1047        <O as MinOrder<N::OrderingGuarantee>>::Min,
1048        R,
1049    >
1050    where
1051        T: Serialize + DeserializeOwned,
1052        O: MinOrder<N::OrderingGuarantee>,
1053    {
1054        let name = via.name();
1055        if to.multiversioned() && name.is_none() {
1056            panic!(
1057                "Cannot send to a multiversioned location without a channel name. Please provide a name for the network."
1058            );
1059        }
1060
1061        let (serialize, deserialize) = if N::is_embedded() {
1062            (
1063                NetworkSend::Embedded {
1064                    tag: None,
1065                    element_type: quote_type::<T>().into(),
1066                },
1067                NetworkRecv::Embedded {
1068                    tag: Some(quote_type::<L>().into()),
1069                    element_type: quote_type::<T>().into(),
1070                },
1071            )
1072        } else {
1073            (
1074                NetworkSend::Custom {
1075                    serialize_fn: Some(N::serialize_thunk(false).into()),
1076                },
1077                NetworkRecv::Custom {
1078                    deserialize_fn: Some(N::deserialize_thunk(Some(&quote_type::<L>())).into()),
1079                },
1080            )
1081        };
1082
1083        let raw_stream: Stream<
1084            (MemberId<L>, T),
1085            Process<'a, L2>,
1086            Unbounded,
1087            <O as MinOrder<N::OrderingGuarantee>>::Min,
1088            R,
1089        > = Stream::new(
1090            to.clone(),
1091            HydroNode::Network {
1092                name: name.map(ToOwned::to_owned),
1093                networking_info: N::networking_info(),
1094                serialize,
1095                deserialize,
1096                instantiate_fn: DebugInstantiate::Building,
1097                input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1098                metadata: to.new_node_metadata(Stream::<
1099                    (MemberId<L>, T),
1100                    Process<'a, L2>,
1101                    Unbounded,
1102                    <O as MinOrder<N::OrderingGuarantee>>::Min,
1103                    R,
1104                >::collection_kind()),
1105            },
1106        );
1107
1108        raw_stream.into_keyed()
1109    }
1110
1111    #[deprecated = "use Stream::broadcast(..., TCP.fail_stop().bincode()) instead"]
1112    /// Broadcasts elements of this stream at each source member to all members of a destination
1113    /// cluster, using [`bincode`] to serialize/deserialize messages.
1114    ///
1115    /// Each source member sends each of its stream elements to **every** member of the cluster
1116    /// based on its latest membership information. Unlike [`Stream::demux_bincode`], which requires
1117    /// `(MemberId, T)` tuples to target specific members, `broadcast_bincode` takes a stream of
1118    /// **only data elements** and sends each element to all cluster members.
1119    ///
1120    /// # Non-Determinism
1121    /// The set of cluster members may asynchronously change over time. Each element is only broadcast
1122    /// to the current cluster members known _at that point in time_ at the source member. Depending
1123    /// on when each source member is notified of membership changes, it will broadcast each element
1124    /// to different members.
1125    ///
1126    /// # Example
1127    /// ```rust
1128    /// # #[cfg(feature = "deploy")] {
1129    /// # use hydro_lang::prelude::*;
1130    /// # use hydro_lang::location::MemberId;
1131    /// # use futures::StreamExt;
1132    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
1133    /// # type Source = ();
1134    /// # type Destination = ();
1135    /// let source: Cluster<Source> = flow.cluster::<Source>();
1136    /// let numbers: Stream<_, Cluster<Source>, _> = source.source_iter(q!(vec![123]));
1137    /// let destination: Cluster<Destination> = flow.cluster::<Destination>();
1138    /// let on_destination: KeyedStream<MemberId<Source>, _, Cluster<Destination>, _> = numbers.broadcast_bincode(&destination, nondet!(/** assuming stable membership */));
1139    /// # on_destination.entries().send_bincode(&p2).entries()
1140    /// // if there are 4 members in the desination, each receives one element from each source member
1141    /// // - Destination(0): { Source(0): [123], Source(1): [123], ... }
1142    /// // - Destination(1): { Source(0): [123], Source(1): [123], ... }
1143    /// // - ...
1144    /// # }, |mut stream| async move {
1145    /// # let mut results = Vec::new();
1146    /// # for w in 0..16 {
1147    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
1148    /// # }
1149    /// # results.sort();
1150    /// # assert_eq!(results, vec![
1151    /// #   "(MemberId::<()>(0), (MemberId::<()>(0), 123))", "(MemberId::<()>(0), (MemberId::<()>(1), 123))", "(MemberId::<()>(0), (MemberId::<()>(2), 123))", "(MemberId::<()>(0), (MemberId::<()>(3), 123))",
1152    /// #   "(MemberId::<()>(1), (MemberId::<()>(0), 123))", "(MemberId::<()>(1), (MemberId::<()>(1), 123))", "(MemberId::<()>(1), (MemberId::<()>(2), 123))", "(MemberId::<()>(1), (MemberId::<()>(3), 123))",
1153    /// #   "(MemberId::<()>(2), (MemberId::<()>(0), 123))", "(MemberId::<()>(2), (MemberId::<()>(1), 123))", "(MemberId::<()>(2), (MemberId::<()>(2), 123))", "(MemberId::<()>(2), (MemberId::<()>(3), 123))",
1154    /// #   "(MemberId::<()>(3), (MemberId::<()>(0), 123))", "(MemberId::<()>(3), (MemberId::<()>(1), 123))", "(MemberId::<()>(3), (MemberId::<()>(2), 123))", "(MemberId::<()>(3), (MemberId::<()>(3), 123))"
1155    /// # ]);
1156    /// # }));
1157    /// # }
1158    /// ```
1159    pub fn broadcast_bincode<L2: 'a>(
1160        self,
1161        other: &Cluster<'a, L2>,
1162        nondet_membership: NonDet,
1163    ) -> KeyedStream<MemberId<L>, T, Cluster<'a, L2>, Unbounded, O, R>
1164    where
1165        T: Clone + Serialize + DeserializeOwned,
1166    {
1167        self.broadcast(other, TCP.fail_stop().bincode(), nondet_membership)
1168    }
1169
1170    /// Broadcasts elements of this stream at each source member to all members of a destination
1171    /// cluster, using the configuration in `via` to set up the message transport.
1172    ///
1173    /// Each source member sends each of its stream elements to **every** member of the cluster
1174    /// based on its latest membership information. Unlike [`Stream::demux`], which requires
1175    /// `(MemberId, T)` tuples to target specific members, `broadcast` takes a stream of
1176    /// **only data elements** and sends each element to all cluster members.
1177    ///
1178    /// # Non-Determinism
1179    /// The set of cluster members may asynchronously change over time. Each element is only broadcast
1180    /// to the current cluster members known _at that point in time_ at the source member. Depending
1181    /// on when each source member is notified of membership changes, it will broadcast each element
1182    /// to different members.
1183    ///
1184    /// # Example
1185    /// ```rust
1186    /// # #[cfg(feature = "deploy")] {
1187    /// # use hydro_lang::prelude::*;
1188    /// # use hydro_lang::location::MemberId;
1189    /// # use futures::StreamExt;
1190    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
1191    /// # type Source = ();
1192    /// # type Destination = ();
1193    /// let source: Cluster<Source> = flow.cluster::<Source>();
1194    /// let numbers: Stream<_, Cluster<Source>, _> = source.source_iter(q!(vec![123]));
1195    /// let destination: Cluster<Destination> = flow.cluster::<Destination>();
1196    /// let on_destination: KeyedStream<MemberId<Source>, _, Cluster<Destination>, _> = numbers.broadcast(&destination, TCP.fail_stop().bincode(), nondet!(/** assuming stable membership */));
1197    /// # on_destination.entries().send(&p2, TCP.fail_stop().bincode()).entries()
1198    /// // if there are 4 members in the desination, each receives one element from each source member
1199    /// // - Destination(0): { Source(0): [123], Source(1): [123], ... }
1200    /// // - Destination(1): { Source(0): [123], Source(1): [123], ... }
1201    /// // - ...
1202    /// # }, |mut stream| async move {
1203    /// # let mut results = Vec::new();
1204    /// # for w in 0..16 {
1205    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
1206    /// # }
1207    /// # results.sort();
1208    /// # assert_eq!(results, vec![
1209    /// #   "(MemberId::<()>(0), (MemberId::<()>(0), 123))", "(MemberId::<()>(0), (MemberId::<()>(1), 123))", "(MemberId::<()>(0), (MemberId::<()>(2), 123))", "(MemberId::<()>(0), (MemberId::<()>(3), 123))",
1210    /// #   "(MemberId::<()>(1), (MemberId::<()>(0), 123))", "(MemberId::<()>(1), (MemberId::<()>(1), 123))", "(MemberId::<()>(1), (MemberId::<()>(2), 123))", "(MemberId::<()>(1), (MemberId::<()>(3), 123))",
1211    /// #   "(MemberId::<()>(2), (MemberId::<()>(0), 123))", "(MemberId::<()>(2), (MemberId::<()>(1), 123))", "(MemberId::<()>(2), (MemberId::<()>(2), 123))", "(MemberId::<()>(2), (MemberId::<()>(3), 123))",
1212    /// #   "(MemberId::<()>(3), (MemberId::<()>(0), 123))", "(MemberId::<()>(3), (MemberId::<()>(1), 123))", "(MemberId::<()>(3), (MemberId::<()>(2), 123))", "(MemberId::<()>(3), (MemberId::<()>(3), 123))"
1213    /// # ]);
1214    /// # }));
1215    /// # }
1216    /// ```
1217    pub fn broadcast<L2: 'a, N: NetworkFor<T>>(
1218        self,
1219        to: &Cluster<'a, L2>,
1220        via: N,
1221        nondet_membership: NonDet,
1222    ) -> KeyedStream<
1223        MemberId<L>,
1224        T,
1225        Cluster<'a, L2>,
1226        Unbounded,
1227        <O as MinOrder<N::OrderingGuarantee>>::Min,
1228        R,
1229    >
1230    where
1231        T: Clone + Serialize + DeserializeOwned,
1232        O: MinOrder<N::OrderingGuarantee>,
1233    {
1234        let ids = track_membership(self.location.source_cluster_membership_stream(
1235            to,
1236            nondet!(/** dropped prefixes don't affect broadcast */),
1237        ));
1238        sliced! {
1239            let members_snapshot = use::snapshot(ids, nondet_membership);
1240            let elements = use::batch(self, nondet_membership);
1241
1242            let current_members = members_snapshot.filter(q!(|b| *b));
1243            elements.repeat_with_keys(current_members)
1244        }
1245        .demux(to, via)
1246    }
1247
1248    /// Broadcasts elements of this stream at each source member to all members of a destination
1249    /// cluster, assuming membership is closed (fixed at deploy time).
1250    ///
1251    /// Unlike [`Stream::broadcast`], this does not require a [`NonDet`] guard.
1252    /// The membership set is obtained from deploy metadata via [`ClusterIds`], making the
1253    /// broadcast fully deterministic.
1254    ///
1255    /// The consistency guarantee of the output depends on the network's failure policy
1256    /// ([`NetworkFor::ConsistencyGuarantee`]). Policies like `fail_stop` and
1257    /// `lossy_delayed_forever` guarantee that every live destination member eventually
1258    /// materializes the same elements from each source, so the output is
1259    /// [`EventualConsistency`](crate::location::cluster::EventualConsistency). A plain `lossy`
1260    /// policy can drop individual messages for some
1261    /// members while delivering them to others, so replicas may permanently diverge and the
1262    /// output only has [`NoConsistency`].
1263    ///
1264    /// This is only available in deployment targets with static cluster membership
1265    /// (legacy Hydro Deploy and simulation). On dynamic targets, use [`Stream::broadcast`].
1266    pub fn broadcast_closed<L2: 'a, N: NetworkFor<T>>(
1267        self,
1268        to: &Cluster<'a, L2>,
1269        via: N,
1270    ) -> KeyedStream<
1271        MemberId<L>,
1272        T,
1273        Cluster<'a, L2, N::ConsistencyGuarantee>,
1274        Unbounded,
1275        <O as MinOrder<N::OrderingGuarantee>>::Min,
1276        R,
1277    >
1278    where
1279        T: Clone + Serialize + DeserializeOwned,
1280        O: MinOrder<N::OrderingGuarantee>,
1281    {
1282        let cluster_ids = ClusterIds {
1283            key: to.key,
1284            _phantom: PhantomData,
1285        };
1286        let member_ids = self
1287            .location
1288            .source_iter(q!(cluster_ids
1289                .iter()
1290                .map(|id| MemberId::from_tagless(id.clone()))))
1291            .assert_has_consistency_of_trusted::<Cluster<'a, L, C>>(manual_proof!(
1292                /// ClusterIds is deploy-time metadata, identical on every cluster member.
1293            ));
1294
1295        self.cross_product(member_ids)
1296            .map(q!(|(data, member_id)| (member_id, data)))
1297            .into_keyed()
1298            .demux(to, via)
1299            .assert_has_consistency_of_trusted(manual_proof!(
1300                /// Closed broadcast with fixed membership: every source member sends to every
1301                /// destination member, and the network's failure policy (tracked by
1302                /// `NetworkFor::ConsistencyGuarantee`) delivers the same messages to every live
1303                /// member, so all destinations materialize the same elements.
1304            ))
1305    }
1306
1307    #[cfg(feature = "sim")]
1308    /// Sends elements of this cluster stream to an external location using bincode serialization.
1309    fn send_bincode_external<L2>(self, other: &External<'_, L2>) -> ExternalBincodeStream<T, O, R>
1310    where
1311        T: Serialize + DeserializeOwned,
1312    {
1313        let serialize_pipeline = Some(serialize_bincode::<T>(false));
1314
1315        let mut flow_state_borrow = self.location.flow_state().borrow_mut();
1316
1317        let external_port_id = flow_state_borrow.next_external_port();
1318
1319        flow_state_borrow.push_root(HydroRoot::SendExternal {
1320            to_external_key: other.key,
1321            to_port_id: external_port_id,
1322            to_many: false,
1323            unpaired: true,
1324            serialize_fn: serialize_pipeline.map(|e| e.into()),
1325            instantiate_fn: DebugInstantiate::Building,
1326            input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1327            op_metadata: HydroIrOpMetadata::new(),
1328        });
1329
1330        ExternalBincodeStream {
1331            process_key: other.key,
1332            port_id: external_port_id,
1333            _phantom: PhantomData,
1334        }
1335    }
1336
1337    #[cfg(feature = "sim")]
1338    /// Sets up a simulation output port for this cluster stream, allowing test code
1339    /// to receive `(member_id, T)` pairs during simulation.
1340    pub fn sim_cluster_output(self) -> crate::sim::SimClusterReceiver<T, O, R>
1341    where
1342        T: Serialize + DeserializeOwned,
1343    {
1344        let external_location: External<'a, ()> = External {
1345            key: LocationKey::FIRST,
1346            flow_state: self.location.flow_state().clone(),
1347            _phantom: PhantomData,
1348        };
1349
1350        let external = self.send_bincode_external(&external_location);
1351
1352        crate::sim::SimClusterReceiver(external.port_id, PhantomData)
1353    }
1354}
1355
1356impl<'a, T, L, L2, B: Boundedness, C: Consistency, O: Ordering, R: Retries>
1357    Stream<(MemberId<L2>, T), Cluster<'a, L, C>, B, O, R>
1358{
1359    #[deprecated = "use Stream::demux(..., TCP.fail_stop().bincode()) instead"]
1360    /// Sends elements of this stream at each source member to specific members of a destination
1361    /// cluster, identified by a [`MemberId`], using [`bincode`] to serialize/deserialize messages.
1362    ///
1363    /// Each element in the stream must be a tuple `(MemberId<L2>, T)` where the first element
1364    /// specifies which cluster member should receive the data. Unlike [`Stream::broadcast_bincode`],
1365    /// this API allows precise targeting of specific cluster members rather than broadcasting to
1366    /// all members.
1367    ///
1368    /// Each cluster member sends its local stream elements, and they are collected at each
1369    /// destination member as a [`KeyedStream`] where keys identify the source cluster member.
1370    ///
1371    /// # Example
1372    /// ```rust
1373    /// # #[cfg(feature = "deploy")] {
1374    /// # use hydro_lang::prelude::*;
1375    /// # use futures::StreamExt;
1376    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
1377    /// # type Source = ();
1378    /// # type Destination = ();
1379    /// let source: Cluster<Source> = flow.cluster::<Source>();
1380    /// let to_send: Stream<_, Cluster<_>, _> = source
1381    ///     .source_iter(q!(vec![0, 1, 2, 3]))
1382    ///     .map(q!(|x| (hydro_lang::location::MemberId::from_raw_id(x), x)));
1383    /// let destination: Cluster<Destination> = flow.cluster::<Destination>();
1384    /// let all_received = to_send.demux_bincode(&destination); // KeyedStream<MemberId<Source>, i32, ...>
1385    /// # all_received.entries().send_bincode(&p2).entries()
1386    /// # }, |mut stream| async move {
1387    /// // if there are 4 members in the destination cluster, each receives one message from each source member
1388    /// // - Destination(0): { Source(0): [0], Source(1): [0], ... }
1389    /// // - Destination(1): { Source(0): [1], Source(1): [1], ... }
1390    /// // - ...
1391    /// # let mut results = Vec::new();
1392    /// # for w in 0..16 {
1393    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
1394    /// # }
1395    /// # results.sort();
1396    /// # assert_eq!(results, vec![
1397    /// #   "(MemberId::<()>(0), (MemberId::<()>(0), 0))", "(MemberId::<()>(0), (MemberId::<()>(1), 0))", "(MemberId::<()>(0), (MemberId::<()>(2), 0))", "(MemberId::<()>(0), (MemberId::<()>(3), 0))",
1398    /// #   "(MemberId::<()>(1), (MemberId::<()>(0), 1))", "(MemberId::<()>(1), (MemberId::<()>(1), 1))", "(MemberId::<()>(1), (MemberId::<()>(2), 1))", "(MemberId::<()>(1), (MemberId::<()>(3), 1))",
1399    /// #   "(MemberId::<()>(2), (MemberId::<()>(0), 2))", "(MemberId::<()>(2), (MemberId::<()>(1), 2))", "(MemberId::<()>(2), (MemberId::<()>(2), 2))", "(MemberId::<()>(2), (MemberId::<()>(3), 2))",
1400    /// #   "(MemberId::<()>(3), (MemberId::<()>(0), 3))", "(MemberId::<()>(3), (MemberId::<()>(1), 3))", "(MemberId::<()>(3), (MemberId::<()>(2), 3))", "(MemberId::<()>(3), (MemberId::<()>(3), 3))"
1401    /// # ]);
1402    /// # }));
1403    /// # }
1404    /// ```
1405    pub fn demux_bincode(
1406        self,
1407        other: &Cluster<'a, L2>,
1408    ) -> KeyedStream<MemberId<L>, T, Cluster<'a, L2>, Unbounded, O, R>
1409    where
1410        T: Serialize + DeserializeOwned,
1411    {
1412        self.demux(other, TCP.fail_stop().bincode())
1413    }
1414
1415    /// Sends elements of this stream at each source member to specific members of a destination
1416    /// cluster, identified by a [`MemberId`], using the configuration in `via` to set up the
1417    /// message transport.
1418    ///
1419    /// Each element in the stream must be a tuple `(MemberId<L2>, T)` where the first element
1420    /// specifies which cluster member should receive the data. Unlike [`Stream::broadcast`],
1421    /// this API allows precise targeting of specific cluster members rather than broadcasting to
1422    /// all members.
1423    ///
1424    /// Each cluster member sends its local stream elements, and they are collected at each
1425    /// destination member as a [`KeyedStream`] where keys identify the source cluster member.
1426    ///
1427    /// # Example
1428    /// ```rust
1429    /// # #[cfg(feature = "deploy")] {
1430    /// # use hydro_lang::prelude::*;
1431    /// # use futures::StreamExt;
1432    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
1433    /// # type Source = ();
1434    /// # type Destination = ();
1435    /// let source: Cluster<Source> = flow.cluster::<Source>();
1436    /// let to_send: Stream<_, Cluster<_>, _> = source
1437    ///     .source_iter(q!(vec![0, 1, 2, 3]))
1438    ///     .map(q!(|x| (hydro_lang::location::MemberId::from_raw_id(x), x)));
1439    /// let destination: Cluster<Destination> = flow.cluster::<Destination>();
1440    /// let all_received = to_send.demux(&destination, TCP.fail_stop().bincode()); // KeyedStream<MemberId<Source>, i32, ...>
1441    /// # all_received.entries().send(&p2, TCP.fail_stop().bincode()).entries()
1442    /// # }, |mut stream| async move {
1443    /// // if there are 4 members in the destination cluster, each receives one message from each source member
1444    /// // - Destination(0): { Source(0): [0], Source(1): [0], ... }
1445    /// // - Destination(1): { Source(0): [1], Source(1): [1], ... }
1446    /// // - ...
1447    /// # let mut results = Vec::new();
1448    /// # for w in 0..16 {
1449    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
1450    /// # }
1451    /// # results.sort();
1452    /// # assert_eq!(results, vec![
1453    /// #   "(MemberId::<()>(0), (MemberId::<()>(0), 0))", "(MemberId::<()>(0), (MemberId::<()>(1), 0))", "(MemberId::<()>(0), (MemberId::<()>(2), 0))", "(MemberId::<()>(0), (MemberId::<()>(3), 0))",
1454    /// #   "(MemberId::<()>(1), (MemberId::<()>(0), 1))", "(MemberId::<()>(1), (MemberId::<()>(1), 1))", "(MemberId::<()>(1), (MemberId::<()>(2), 1))", "(MemberId::<()>(1), (MemberId::<()>(3), 1))",
1455    /// #   "(MemberId::<()>(2), (MemberId::<()>(0), 2))", "(MemberId::<()>(2), (MemberId::<()>(1), 2))", "(MemberId::<()>(2), (MemberId::<()>(2), 2))", "(MemberId::<()>(2), (MemberId::<()>(3), 2))",
1456    /// #   "(MemberId::<()>(3), (MemberId::<()>(0), 3))", "(MemberId::<()>(3), (MemberId::<()>(1), 3))", "(MemberId::<()>(3), (MemberId::<()>(2), 3))", "(MemberId::<()>(3), (MemberId::<()>(3), 3))"
1457    /// # ]);
1458    /// # }));
1459    /// # }
1460    /// ```
1461    pub fn demux<N: NetworkFor<T>>(
1462        self,
1463        to: &Cluster<'a, L2>,
1464        via: N,
1465    ) -> KeyedStream<
1466        MemberId<L>,
1467        T,
1468        Cluster<'a, L2, NoConsistency>,
1469        Unbounded,
1470        <O as MinOrder<N::OrderingGuarantee>>::Min,
1471        R,
1472    >
1473    where
1474        T: Serialize + DeserializeOwned,
1475        O: MinOrder<N::OrderingGuarantee>,
1476    {
1477        self.into_keyed().demux(to, via)
1478    }
1479}
1480
1481#[cfg(test)]
1482mod tests {
1483    #[cfg(feature = "sim")]
1484    use stageleft::q;
1485
1486    #[cfg(feature = "sim")]
1487    use crate::live_collections::sliced::sliced;
1488    #[cfg(feature = "sim")]
1489    use crate::location::{Location, MemberId};
1490    #[cfg(feature = "sim")]
1491    use crate::networking::TCP;
1492    #[cfg(feature = "sim")]
1493    use crate::nondet::nondet;
1494    #[cfg(feature = "sim")]
1495    use crate::prelude::FlowBuilder;
1496
1497    #[cfg(feature = "sim")]
1498    #[test]
1499    fn sim_send_bincode_o2o() {
1500        use crate::networking::TCP;
1501
1502        let mut flow = FlowBuilder::new();
1503        let node = flow.process::<()>();
1504        let node2 = flow.process::<()>();
1505
1506        let (in_send, input) = node.sim_input();
1507
1508        let out_recv = input
1509            .send(&node2, TCP.fail_stop().bincode())
1510            .batch(&node2.tick(), nondet!(/** test */))
1511            .count()
1512            .all_ticks()
1513            .sim_output();
1514
1515        let instances = flow.sim().exhaustive(async || {
1516            in_send.send(());
1517            in_send.send(());
1518            in_send.send(());
1519
1520            let received = out_recv.collect::<Vec<_>>().await;
1521            assert!(received.into_iter().sum::<usize>() == 3);
1522        });
1523
1524        assert_eq!(instances, 4); // 2^{3 - 1}
1525    }
1526
1527    #[cfg(feature = "sim")]
1528    #[test]
1529    fn sim_send_bincode_m2o() {
1530        let mut flow = FlowBuilder::new();
1531        let cluster = flow.cluster::<()>();
1532        let node = flow.process::<()>();
1533
1534        let input = cluster.source_iter(q!(vec![1]));
1535
1536        let out_recv = input
1537            .send(&node, TCP.fail_stop().bincode())
1538            .entries()
1539            .batch(&node.tick(), nondet!(/** test */))
1540            .all_ticks()
1541            .sim_output();
1542
1543        let instances = flow
1544            .sim()
1545            .with_cluster_size(&cluster, 4)
1546            .exhaustive(async || {
1547                out_recv
1548                    .assert_yields_only_unordered(vec![
1549                        (MemberId::from_raw_id(0), 1),
1550                        (MemberId::from_raw_id(1), 1),
1551                        (MemberId::from_raw_id(2), 1),
1552                        (MemberId::from_raw_id(3), 1),
1553                    ])
1554                    .await
1555            });
1556
1557        assert_eq!(instances, 75); // ∑ (k=1 to 4) S(4,k) × k! = 75
1558    }
1559
1560    #[cfg(feature = "sim")]
1561    #[test]
1562    fn sim_send_bincode_multiple_m2o() {
1563        let mut flow = FlowBuilder::new();
1564        let cluster1 = flow.cluster::<()>();
1565        let cluster2 = flow.cluster::<()>();
1566        let node = flow.process::<()>();
1567
1568        let out_recv_1 = cluster1
1569            .source_iter(q!(vec![1]))
1570            .send(&node, TCP.fail_stop().bincode())
1571            .entries()
1572            .sim_output();
1573
1574        let out_recv_2 = cluster2
1575            .source_iter(q!(vec![2]))
1576            .send(&node, TCP.fail_stop().bincode())
1577            .entries()
1578            .sim_output();
1579
1580        let instances = flow
1581            .sim()
1582            .with_cluster_size(&cluster1, 3)
1583            .with_cluster_size(&cluster2, 4)
1584            .exhaustive(async || {
1585                out_recv_1
1586                    .assert_yields_only_unordered(vec![
1587                        (MemberId::from_raw_id(0), 1),
1588                        (MemberId::from_raw_id(1), 1),
1589                        (MemberId::from_raw_id(2), 1),
1590                    ])
1591                    .await;
1592
1593                out_recv_2
1594                    .assert_yields_only_unordered(vec![
1595                        (MemberId::from_raw_id(0), 2),
1596                        (MemberId::from_raw_id(1), 2),
1597                        (MemberId::from_raw_id(2), 2),
1598                        (MemberId::from_raw_id(3), 2),
1599                    ])
1600                    .await;
1601            });
1602
1603        assert_eq!(instances, 1);
1604    }
1605
1606    #[cfg(feature = "sim")]
1607    #[test]
1608    fn sim_send_bincode_o2m() {
1609        let mut flow = FlowBuilder::new();
1610        let cluster = flow.cluster::<()>();
1611        let node = flow.process::<()>();
1612
1613        let input = node.source_iter(q!(vec![
1614            (MemberId::from_raw_id(0), 123),
1615            (MemberId::from_raw_id(1), 456),
1616        ]));
1617
1618        let out_recv = input
1619            .demux(&cluster, TCP.fail_stop().bincode())
1620            .map(q!(|x| x + 1))
1621            .send(&node, TCP.fail_stop().bincode())
1622            .entries()
1623            .sim_output();
1624
1625        flow.sim()
1626            .with_cluster_size(&cluster, 4)
1627            .exhaustive(async || {
1628                out_recv
1629                    .assert_yields_only_unordered(vec![
1630                        (MemberId::from_raw_id(0), 124),
1631                        (MemberId::from_raw_id(1), 457),
1632                    ])
1633                    .await
1634            });
1635    }
1636
1637    #[cfg(feature = "sim")]
1638    #[test]
1639    fn sim_broadcast_bincode_o2m() {
1640        let mut flow = FlowBuilder::new();
1641        let cluster = flow.cluster::<()>();
1642        let node = flow.process::<()>();
1643
1644        let input = node.source_iter(q!(vec![123, 456]));
1645
1646        let out_recv = input
1647            .broadcast(&cluster, TCP.fail_stop().bincode(), nondet!(/** test */))
1648            .map(q!(|x| x + 1))
1649            .send(&node, TCP.fail_stop().bincode())
1650            .entries()
1651            .sim_output();
1652
1653        let mut c_1_produced = false;
1654        let mut c_2_produced = false;
1655        let mut c_1_saw_457_but_not_124 = false;
1656
1657        flow.sim()
1658            .with_cluster_size(&cluster, 2)
1659            .exhaustive(async || {
1660                let all_out = out_recv.collect_sorted::<Vec<_>>().await;
1661
1662                // check that order is preserved
1663                if all_out.contains(&(MemberId::from_raw_id(0), 124)) {
1664                    assert!(all_out.contains(&(MemberId::from_raw_id(0), 457)));
1665                    c_1_produced = true;
1666                }
1667
1668                if all_out.contains(&(MemberId::from_raw_id(1), 124)) {
1669                    assert!(all_out.contains(&(MemberId::from_raw_id(1), 457)));
1670                    c_2_produced = true;
1671                }
1672
1673                if all_out.contains(&(MemberId::from_raw_id(0), 457))
1674                    && !all_out.contains(&(MemberId::from_raw_id(0), 124))
1675                {
1676                    c_1_saw_457_but_not_124 = true;
1677                }
1678            });
1679
1680        assert!(c_1_produced && c_2_produced); // in at least one execution each, the cluster member received both messages
1681
1682        // in at least one execution, the cluster member received 457 but not 124, this tests
1683        // that the simulator properly explores dynamic membership additions (a member that joins after 123 is broadcast)
1684        assert!(c_1_saw_457_but_not_124);
1685    }
1686
1687    #[cfg(feature = "sim")]
1688    #[test]
1689    fn sim_send_bincode_m2m() {
1690        let mut flow = FlowBuilder::new();
1691        let cluster = flow.cluster::<()>();
1692        let node = flow.process::<()>();
1693
1694        let input = node.source_iter(q!(vec![
1695            (MemberId::from_raw_id(0), 123),
1696            (MemberId::from_raw_id(1), 456),
1697        ]));
1698
1699        let out_recv = input
1700            .demux(&cluster, TCP.fail_stop().bincode())
1701            .map(q!(|x| x + 1))
1702            .flat_map_ordered(q!(|x| vec![
1703                (MemberId::from_raw_id(0), x),
1704                (MemberId::from_raw_id(1), x),
1705            ]))
1706            .demux(&cluster, TCP.fail_stop().bincode())
1707            .entries()
1708            .send(&node, TCP.fail_stop().bincode())
1709            .entries()
1710            .sim_output();
1711
1712        flow.sim()
1713            .with_cluster_size(&cluster, 4)
1714            .exhaustive(async || {
1715                out_recv
1716                    .assert_yields_only_unordered(vec![
1717                        (MemberId::from_raw_id(0), (MemberId::from_raw_id(0), 124)),
1718                        (MemberId::from_raw_id(0), (MemberId::from_raw_id(1), 457)),
1719                        (MemberId::from_raw_id(1), (MemberId::from_raw_id(0), 124)),
1720                        (MemberId::from_raw_id(1), (MemberId::from_raw_id(1), 457)),
1721                    ])
1722                    .await
1723            });
1724    }
1725
1726    #[cfg(feature = "sim")]
1727    #[test]
1728    fn sim_lossy_delayed_forever_o2o() {
1729        use std::collections::HashSet;
1730
1731        use crate::properties::manual_proof;
1732
1733        let mut flow = FlowBuilder::new();
1734        let node = flow.process::<()>();
1735        let node2 = flow.process::<()>();
1736
1737        let received = node
1738            .source_iter(q!(0..3_u32))
1739            .send(&node2, TCP.lossy_delayed_forever().bincode())
1740            .fold(
1741                q!(|| std::collections::HashSet::<u32>::new()),
1742                q!(
1743                    |set, v| {
1744                        set.insert(v);
1745                    },
1746                    commutative = manual_proof!(/** set insert is commutative */)
1747                ),
1748            );
1749
1750        let out_recv = sliced! {
1751            let snapshot = use::snapshot(received, nondet!(/** test */));
1752            snapshot.into_stream()
1753        }
1754        .sim_output();
1755
1756        let mut saw_non_contiguous = false;
1757
1758        flow.sim().test_safety_only().exhaustive(async || {
1759            let snapshots = out_recv.collect::<Vec<HashSet<u32>>>().await;
1760
1761            // Check each individual snapshot for a non-contiguous subset.
1762            for set in &snapshots {
1763                #[expect(clippy::disallowed_methods, reason = "min / max are deterministic")]
1764                if set.len() >= 2 && set.len() < 3 {
1765                    let min = *set.iter().min().unwrap();
1766                    let max = *set.iter().max().unwrap();
1767                    if set.len() < (max - min + 1) as usize {
1768                        saw_non_contiguous = true;
1769                    }
1770                }
1771            }
1772        });
1773
1774        assert!(
1775            saw_non_contiguous,
1776            "Expected at least one execution with a non-contiguous subset of inputs"
1777        );
1778    }
1779
1780    #[cfg(feature = "sim")]
1781    #[test]
1782    fn sim_udp_lossy_delayed_forever_o2o() {
1783        use std::collections::HashSet;
1784
1785        use crate::networking::UDP;
1786        use crate::properties::manual_proof;
1787
1788        let mut flow = FlowBuilder::new();
1789        let node = flow.process::<()>();
1790        let node2 = flow.process::<()>();
1791
1792        let received = node
1793            .source_iter(q!(0..3_u32))
1794            .send(&node2, UDP.lossy_delayed_forever().bincode())
1795            .fold(
1796                q!(|| std::collections::HashSet::<u32>::new()),
1797                q!(
1798                    |set, v| {
1799                        set.insert(v);
1800                    },
1801                    commutative = manual_proof!(/** set insert is commutative */)
1802                ),
1803            );
1804
1805        let out_recv = sliced! {
1806            let snapshot = use::snapshot(received, nondet!(/** test */));
1807            snapshot.into_stream()
1808        }
1809        .sim_output();
1810
1811        let mut saw_non_contiguous = false;
1812
1813        flow.sim().test_safety_only().exhaustive(async || {
1814            let snapshots = out_recv.collect::<Vec<HashSet<u32>>>().await;
1815
1816            // Check each individual snapshot for a non-contiguous subset.
1817            for set in &snapshots {
1818                #[expect(clippy::disallowed_methods, reason = "min / max are deterministic")]
1819                if set.len() >= 2 && set.len() < 3 {
1820                    let min = *set.iter().min().unwrap();
1821                    let max = *set.iter().max().unwrap();
1822                    if set.len() < (max - min + 1) as usize {
1823                        saw_non_contiguous = true;
1824                    }
1825                }
1826            }
1827        });
1828
1829        assert!(
1830            saw_non_contiguous,
1831            "Expected at least one execution with a non-contiguous subset of inputs"
1832        );
1833    }
1834
1835    #[cfg(feature = "sim")]
1836    #[test]
1837    fn sim_broadcast_closed_o2m() {
1838        let mut flow = FlowBuilder::new();
1839        let cluster = flow.cluster::<()>();
1840        let node = flow.process::<()>();
1841
1842        let input = node.source_iter(q!(vec![123, 456]));
1843
1844        let out_recv = input
1845            .broadcast_closed(&cluster, TCP.fail_stop().bincode())
1846            .send(&node, TCP.fail_stop().bincode())
1847            .entries()
1848            .sim_output();
1849
1850        flow.sim()
1851            .with_cluster_size(&cluster, 2)
1852            .exhaustive(async || {
1853                out_recv
1854                    .assert_yields_only_unordered(vec![
1855                        (MemberId::from_raw_id(0), 123),
1856                        (MemberId::from_raw_id(0), 456),
1857                        (MemberId::from_raw_id(1), 123),
1858                        (MemberId::from_raw_id(1), 456),
1859                    ])
1860                    .await
1861            });
1862    }
1863
1864    #[cfg(feature = "sim")]
1865    #[test]
1866    fn sim_broadcast_closed_m2m() {
1867        let mut flow = FlowBuilder::new();
1868        let source = flow.cluster::<()>();
1869        let dest: crate::location::Cluster<'_, ()> = flow.cluster::<()>();
1870        let node = flow.process::<()>();
1871
1872        let input = source.source_iter(q!(vec![123]));
1873
1874        // Broadcast from source cluster to dest cluster, then collect at a process.
1875        let out_recv = input
1876            .broadcast_closed(&dest, TCP.fail_stop().bincode())
1877            .entries()
1878            .send(&node, TCP.fail_stop().bincode())
1879            .entries()
1880            .sim_output();
1881
1882        flow.sim()
1883            .with_cluster_size(&source, 2)
1884            .with_cluster_size(&dest, 2)
1885            .exhaustive(async || {
1886                // Each source member (0, 1) broadcasts 123 to each dest member (0, 1).
1887                // The dest members then send to the process keyed by dest member id.
1888                // Each dest member receives (source_0, 123) and (source_1, 123).
1889                out_recv
1890                    .assert_yields_only_unordered(vec![
1891                        (MemberId::from_raw_id(0), (MemberId::from_raw_id(0), 123)),
1892                        (MemberId::from_raw_id(0), (MemberId::from_raw_id(1), 123)),
1893                        (MemberId::from_raw_id(1), (MemberId::from_raw_id(0), 123)),
1894                        (MemberId::from_raw_id(1), (MemberId::from_raw_id(1), 123)),
1895                    ])
1896                    .await
1897            });
1898    }
1899
1900    /// Compile-time check that the consistency guarantee of `broadcast_closed` output tracks
1901    /// the network's failure policy: `fail_stop` and `lossy_delayed_forever` preserve
1902    /// [`EventualConsistency`], while plain `lossy` only provides [`NoConsistency`].
1903    #[cfg(feature = "sim")]
1904    #[test]
1905    fn broadcast_closed_consistency_tracks_failure_policy() {
1906        use crate::live_collections::keyed_stream::KeyedStream;
1907        use crate::live_collections::stream::Stream;
1908        use crate::location::Cluster;
1909        use crate::location::cluster::{EventualConsistency, NoConsistency};
1910
1911        let mut flow = FlowBuilder::new();
1912        let cluster = flow.cluster::<()>();
1913        let source = flow.cluster::<()>();
1914        let node = flow.process::<()>();
1915
1916        // `fail_stop` models a failed connection as the recipient having failed, preserving
1917        // eventual consistency across live members.
1918        let _: Stream<u32, Cluster<'_, (), EventualConsistency>, _, _, _> = node
1919            .source_iter(q!(vec![1u32]))
1920            .broadcast_closed(&cluster, TCP.fail_stop().bincode());
1921
1922        // `lossy_delayed_forever` models drops as indefinite delays, preserving eventual
1923        // consistency.
1924        let _: Stream<u32, Cluster<'_, (), EventualConsistency>, _, _, _> = node
1925            .source_iter(q!(vec![1u32]))
1926            .broadcast_closed(&cluster, TCP.lossy_delayed_forever().bincode());
1927
1928        // Plain `lossy` can drop messages for some members while delivering them to others,
1929        // so replicas may permanently diverge.
1930        let _: Stream<u32, Cluster<'_, (), NoConsistency>, _, _, _> = node
1931            .source_iter(q!(vec![1u32]))
1932            .broadcast_closed(&cluster, TCP.lossy(nondet!(/** test */)).bincode());
1933
1934        // The same applies to cluster-to-cluster closed broadcasts.
1935        let _: KeyedStream<MemberId<()>, u32, Cluster<'_, (), EventualConsistency>, _, _, _> =
1936            source
1937                .source_iter(q!(vec![1u32]))
1938                .broadcast_closed(&cluster, TCP.fail_stop().bincode());
1939
1940        let _: KeyedStream<MemberId<()>, u32, Cluster<'_, (), NoConsistency>, _, _, _> = source
1941            .source_iter(q!(vec![1u32]))
1942            .broadcast_closed(&cluster, TCP.lossy(nondet!(/** test */)).bincode());
1943
1944        let _ = flow.finalize();
1945    }
1946}