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, "e_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, "e_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("e_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}