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