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