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