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