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