hydro_lang/networking/mod.rs
1//! Types for configuring network channels with serialization formats, transports, etc.
2
3use std::marker::PhantomData;
4
5use serde::Serialize;
6use serde::de::DeserializeOwned;
7
8use crate::live_collections::stream::networking::{deserialize_bincode, serialize_bincode};
9use crate::live_collections::stream::{NoOrder, TotalOrder};
10use crate::location::cluster::{Consistency, EventualConsistency, NoConsistency};
11use crate::nondet::NonDet;
12
13#[sealed::sealed]
14trait SerKind<T: ?Sized> {
15 fn serialize_thunk(is_demux: bool) -> syn::Expr;
16
17 fn deserialize_thunk(tagged: Option<&syn::Type>) -> syn::Expr;
18
19 /// Whether this serialization backend leaves serialization to code outside of Hydro (see
20 /// [`Embedded`]). When `true`, [`Self::serialize_thunk`] and [`Self::deserialize_thunk`] are
21 /// never called; the raw element type flows across the channel unserialized.
22 fn is_embedded() -> bool {
23 false
24 }
25}
26
27/// Serialize items using the [`bincode`] crate.
28pub enum Bincode {}
29
30#[sealed::sealed]
31impl<T: Serialize + DeserializeOwned> SerKind<T> for Bincode {
32 fn serialize_thunk(is_demux: bool) -> syn::Expr {
33 serialize_bincode::<T>(is_demux)
34 }
35
36 fn deserialize_thunk(tagged: Option<&syn::Type>) -> syn::Expr {
37 deserialize_bincode::<T>(tagged)
38 }
39}
40
41/// Leaves serialization of items to code outside of Hydro.
42///
43/// This serialization backend is only supported by the embedded deployment backend (it will panic
44/// on all other backends). The generated network channel exposes the raw element type `T` to the
45/// developer (rather than serialized bytes), so they can perform custom serialization logic outside
46/// of the Hydro program for that channel.
47pub enum Embedded {}
48
49#[sealed::sealed]
50impl<T> SerKind<T> for Embedded {
51 fn serialize_thunk(_is_demux: bool) -> syn::Expr {
52 unreachable!("embedded serialization does not use a serialize thunk")
53 }
54
55 fn deserialize_thunk(_tagged: Option<&syn::Type>) -> syn::Expr {
56 unreachable!("embedded serialization does not use a deserialize thunk")
57 }
58
59 fn is_embedded() -> bool {
60 true
61 }
62}
63
64/// An unconfigured serialization backend.
65pub enum NoSer {}
66
67/// A transport backend for network channels.
68#[sealed::sealed]
69pub trait TransportKind {
70 /// The ordering guarantee provided by this transport.
71 type OrderingGuarantee: crate::live_collections::stream::Ordering;
72
73 /// The consistency guarantee this transport can preserve for replicated outputs
74 /// (see [`NetworkFor::ConsistencyGuarantee`]).
75 type ConsistencyGuarantee: Consistency;
76
77 /// Returns the [`NetworkingInfo`] describing this transport's configuration.
78 fn networking_info() -> NetworkingInfo;
79}
80
81#[sealed::sealed]
82#[diagnostic::on_unimplemented(
83 message = "TCP transport requires a failure policy. For example, `TCP.fail_stop()` stops sending messages after a failed connection."
84)]
85/// A failure policy for TCP connections, determining how the transport handles
86/// connection failures and what ordering guarantees the output stream provides.
87pub trait TcpFailPolicy {
88 /// The ordering guarantee provided by this failure policy.
89 type OrderingGuarantee: crate::live_collections::stream::Ordering;
90
91 /// The consistency guarantee this failure policy can preserve for replicated outputs
92 /// (see [`NetworkFor::ConsistencyGuarantee`]).
93 type ConsistencyGuarantee: Consistency;
94
95 /// Returns the [`TcpFault`] variant for this failure policy.
96 fn tcp_fault() -> TcpFault;
97}
98
99/// A TCP failure policy that stops sending messages after a failed connection.
100pub enum FailStop {}
101#[sealed::sealed]
102impl TcpFailPolicy for FailStop {
103 type OrderingGuarantee = TotalOrder;
104
105 // A failed connection stops *all* future deliveries to that recipient, which models the
106 // recipient as having failed. Consistency guarantees only apply to live members, so
107 // eventual consistency of replicated outputs is preserved.
108 type ConsistencyGuarantee = EventualConsistency;
109
110 fn tcp_fault() -> TcpFault {
111 TcpFault::FailStop
112 }
113}
114
115/// A failure policy that allows messages to be lost.
116pub enum Lossy {}
117#[sealed::sealed]
118impl TcpFailPolicy for Lossy {
119 type OrderingGuarantee = TotalOrder;
120
121 // A lossy channel can drop an arbitrary message for one recipient while continuing to
122 // deliver later messages, so replicated outputs can diverge across members forever.
123 type ConsistencyGuarantee = NoConsistency;
124
125 fn tcp_fault() -> TcpFault {
126 TcpFault::Lossy
127 }
128}
129
130/// A failure policy that treats dropped messages as indefinitely delayed.
131///
132/// Unlike [`Lossy`], this does not require a [`NonDet`] annotation because the output
133/// stream is always lower in the partial order than the ideal stream (dropped messages
134/// are modeled as infinite delays). The tradeoff is that the output has [`NoOrder`]
135/// guarantees, imposing stricter conditions on downstream consumers.
136///
137/// When using this mode in the Hydro simulator, you must call
138/// [`.test_safety_only()`](crate::sim::flow::SimFlow::test_safety_only): the simulator
139/// will not actually drop packets—it delays "dropped" messages until the end of the
140/// execution, which catches safety bugs but cannot test liveness.
141pub enum LossyDelayedForever {}
142#[sealed::sealed]
143impl TcpFailPolicy for LossyDelayedForever {
144 type OrderingGuarantee = NoOrder;
145
146 // Dropped messages are modeled as indefinitely delayed, so the output on each member is
147 // always a lower bound of the ideal stream that is eventually delivered in full; replicated
148 // outputs therefore remain eventually consistent.
149 type ConsistencyGuarantee = EventualConsistency;
150
151 fn tcp_fault() -> TcpFault {
152 TcpFault::LossyDelayedForever
153 }
154}
155
156#[sealed::sealed]
157#[diagnostic::on_unimplemented(
158 message = "UDP transport requires a failure policy. For example, `UDP.lossy_delayed_forever()` treats dropped messages as indefinitely delayed."
159)]
160/// A failure policy for UDP channels, determining how the transport handles
161/// message loss. Because UDP provides no ordering guarantees, all policies
162/// produce [`NoOrder`] output streams, and there is no `fail_stop` option
163/// (UDP is connectionless, so there is no connection to fail).
164pub trait UdpFailPolicy {
165 /// The consistency guarantee this failure policy can preserve for replicated outputs
166 /// (see [`NetworkFor::ConsistencyGuarantee`]).
167 type ConsistencyGuarantee: Consistency;
168
169 /// Returns the [`UdpFault`] variant for this failure policy.
170 fn udp_fault() -> UdpFault;
171}
172
173#[sealed::sealed]
174impl UdpFailPolicy for Lossy {
175 // A lossy channel can drop an arbitrary message for one recipient while continuing to
176 // deliver later messages, so replicated outputs can diverge across members forever.
177 type ConsistencyGuarantee = NoConsistency;
178
179 fn udp_fault() -> UdpFault {
180 UdpFault::Lossy
181 }
182}
183
184#[sealed::sealed]
185impl UdpFailPolicy for LossyDelayedForever {
186 // Dropped messages are modeled as indefinitely delayed, so the output on each member is
187 // always a lower bound of the ideal stream that is eventually delivered in full; replicated
188 // outputs therefore remain eventually consistent.
189 type ConsistencyGuarantee = EventualConsistency;
190
191 fn udp_fault() -> UdpFault {
192 UdpFault::LossyDelayedForever
193 }
194}
195
196/// Send items across a length-delimited TCP channel.
197pub struct Tcp<F> {
198 _phantom: PhantomData<F>,
199}
200
201#[sealed::sealed]
202impl<F: TcpFailPolicy> TransportKind for Tcp<F> {
203 type OrderingGuarantee = F::OrderingGuarantee;
204
205 type ConsistencyGuarantee = F::ConsistencyGuarantee;
206
207 fn networking_info() -> NetworkingInfo {
208 NetworkingInfo::Tcp {
209 fault: F::tcp_fault(),
210 }
211 }
212}
213
214/// Send items across a UDP channel, which does not guarantee delivery or ordering.
215pub struct Udp<F> {
216 _phantom: PhantomData<F>,
217}
218
219#[sealed::sealed]
220impl<F: UdpFailPolicy> TransportKind for Udp<F> {
221 type OrderingGuarantee = NoOrder;
222
223 type ConsistencyGuarantee = F::ConsistencyGuarantee;
224
225 fn networking_info() -> NetworkingInfo {
226 NetworkingInfo::Udp {
227 fault: F::udp_fault(),
228 }
229 }
230}
231
232/// A networking backend implementation that supports items of type `T`.
233#[sealed::sealed]
234pub trait NetworkFor<T: ?Sized> {
235 /// The ordering guarantee provided by this network configuration.
236 /// When combined with an input stream's ordering `O`, the output ordering
237 /// will be `<O as MinOrder<Self::OrderingGuarantee>>::Min`.
238 type OrderingGuarantee: crate::live_collections::stream::Ordering;
239
240 /// The consistency guarantee this network configuration can preserve when the same data is
241 /// replicated to several recipients (e.g. via
242 /// [`Stream::broadcast_closed`](crate::live_collections::stream::Stream::broadcast_closed)).
243 ///
244 /// Failure policies that guarantee each recipient eventually observes the full stream of
245 /// sent messages, or that model failures as the recipient stopping entirely (such as
246 /// `fail_stop` or `lossy_delayed_forever`), preserve
247 /// [`EventualConsistency`]. Plain `lossy`
248 /// channels can silently drop individual messages for some recipients while others receive
249 /// them, so replicated outputs can permanently diverge and only
250 /// [`NoConsistency`] is guaranteed.
251 type ConsistencyGuarantee: Consistency;
252
253 /// Generates serialization logic for sending `T`.
254 fn serialize_thunk(is_demux: bool) -> syn::Expr;
255
256 /// Generates deserialization logic for receiving `T`.
257 fn deserialize_thunk(tagged: Option<&syn::Type>) -> syn::Expr;
258
259 /// Whether this network channel leaves serialization to code outside of Hydro (see
260 /// [`Embedded`]). When `true`, [`Self::serialize_thunk`] and [`Self::deserialize_thunk`] are
261 /// never called; the raw element type flows across the channel unserialized.
262 fn is_embedded() -> bool {
263 false
264 }
265
266 /// Returns the optional name of the network channel.
267 fn name(&self) -> Option<&str>;
268
269 /// Returns the [`NetworkingInfo`] describing this network channel's transport and fault model.
270 fn networking_info() -> NetworkingInfo;
271}
272
273/// The fault model for a TCP connection.
274#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
275pub enum TcpFault {
276 /// Stops sending messages after a failed connection.
277 FailStop,
278 /// Messages may be lost (e.g. due to network partitions).
279 Lossy,
280 /// Dropped messages are treated as indefinitely delayed with no ordering guarantee.
281 LossyDelayedForever,
282}
283
284/// The fault model for a UDP channel.
285///
286/// UDP is connectionless and never guarantees delivery, so there is no
287/// `FailStop` variant — messages can always be dropped.
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
289pub enum UdpFault {
290 /// Messages may be lost (e.g. due to network partitions or congestion).
291 Lossy,
292 /// Dropped messages are treated as indefinitely delayed.
293 LossyDelayedForever,
294}
295
296/// Describes the networking configuration for a network channel at the IR level.
297#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
298pub enum NetworkingInfo {
299 /// A TCP-based network channel with a specific fault model.
300 Tcp {
301 /// The fault model for this TCP connection.
302 fault: TcpFault,
303 },
304 /// A UDP-based network channel with a specific fault model.
305 Udp {
306 /// The fault model for this UDP channel.
307 fault: UdpFault,
308 },
309}
310
311/// A network channel configuration with `T` as transport backend and `S` as the serialization
312/// backend.
313pub struct NetworkingConfig<Tr: ?Sized, S: ?Sized, Name = ()> {
314 name: Option<Name>,
315 _phantom: (PhantomData<Tr>, PhantomData<S>),
316}
317
318impl<Tr: ?Sized, S: ?Sized> NetworkingConfig<Tr, S> {
319 /// Names the network channel and enables stable communication across multiple service versions.
320 pub fn name(self, name: impl Into<String>) -> NetworkingConfig<Tr, S, String> {
321 NetworkingConfig {
322 name: Some(name.into()),
323 _phantom: (PhantomData, PhantomData),
324 }
325 }
326}
327
328impl<Tr: ?Sized, N> NetworkingConfig<Tr, NoSer, N> {
329 /// Configures the network channel to use [`bincode`] to serialize items.
330 pub const fn bincode(mut self) -> NetworkingConfig<Tr, Bincode, N> {
331 let taken_name = self.name.take();
332 std::mem::forget(self); // nothing else is stored
333 NetworkingConfig {
334 name: taken_name,
335 _phantom: (PhantomData, PhantomData),
336 }
337 }
338
339 /// Configures the network channel to leave serialization to code outside of Hydro.
340 ///
341 /// This is only supported by the embedded deployment backend (it will panic on all other
342 /// backends). The generated network channel exposes the raw element type to the developer
343 /// (rather than serialized bytes), so they can perform custom serialization logic outside of
344 /// the Hydro program for that channel.
345 pub const fn embedded(mut self) -> NetworkingConfig<Tr, Embedded, N> {
346 let taken_name = self.name.take();
347 std::mem::forget(self); // nothing else is stored
348 NetworkingConfig {
349 name: taken_name,
350 _phantom: (PhantomData, PhantomData),
351 }
352 }
353}
354
355impl<S: ?Sized> NetworkingConfig<Tcp<()>, S> {
356 /// Configures the TCP transport to stop sending messages after a failed connection.
357 ///
358 /// Note that the Hydro simulator will not simulate connection failures that impact the
359 /// *liveness* of a program. If an output assertion depends on a `fail_stop` channel
360 /// making progress, that channel will not experience a failure that would cause the test to
361 /// block indefinitely. However, any *safety* issues caused by connection failures will still
362 /// be caught, such as a race condition between a failed connection and some other message.
363 pub const fn fail_stop(self) -> NetworkingConfig<Tcp<FailStop>, S> {
364 NetworkingConfig {
365 name: self.name,
366 _phantom: (PhantomData, PhantomData),
367 }
368 }
369
370 /// Configures the TCP transport to allow messages to be lost.
371 ///
372 /// This is appropriate for networks where messages may be dropped, such as when
373 /// running under a Maelstrom partition nemesis. Unlike `fail_stop`, which guarantees
374 /// a prefix of messages is delivered, `lossy` makes no such guarantee.
375 ///
376 /// # Non-Determinism
377 /// A lossy TCP channel will non-deterministically drop messages during execution.
378 pub const fn lossy(self, nondet: NonDet) -> NetworkingConfig<Tcp<Lossy>, S> {
379 let _ = nondet;
380 NetworkingConfig {
381 name: self.name,
382 _phantom: (PhantomData, PhantomData),
383 }
384 }
385
386 /// Configures the TCP transport to treat dropped messages as indefinitely delayed.
387 ///
388 /// This is appropriate for networks where messages may be dropped, such as when
389 /// running under a Maelstrom partition nemesis. Unlike [`Self::lossy`], this does
390 /// *not* require a [`NonDet`] annotation because the output is always lower in the
391 /// partial order than the ideal stream. However, the output stream will have
392 /// [`NoOrder`] guarantees, imposing stricter conditions on downstream consumers.
393 ///
394 /// Unlike [`Self::lossy`], this mode can easily be simulated in exhaustive mode
395 /// without running into fairness issues.
396 ///
397 /// When using this mode in the Hydro simulator, you must call
398 /// [`.test_safety_only()`](crate::sim::flow::SimFlow::test_safety_only) to opt in:
399 /// the simulator will not actually drop packets—it delays "dropped" messages until
400 /// the end of the execution, which catches safety bugs but cannot test liveness.
401 pub const fn lossy_delayed_forever(self) -> NetworkingConfig<Tcp<LossyDelayedForever>, S> {
402 NetworkingConfig {
403 name: self.name,
404 _phantom: (PhantomData, PhantomData),
405 }
406 }
407}
408
409impl<S: ?Sized> NetworkingConfig<Udp<()>, S> {
410 /// Configures the UDP transport to allow messages to be lost.
411 ///
412 /// UDP never guarantees delivery or ordering, so unlike TCP there is no `fail_stop`
413 /// policy — messages may always be dropped and the output stream always has
414 /// [`NoOrder`] guarantees.
415 ///
416 /// # Non-Determinism
417 /// A lossy UDP channel will non-deterministically drop messages during execution.
418 pub const fn lossy(self, nondet: NonDet) -> NetworkingConfig<Udp<Lossy>, S> {
419 let _ = nondet;
420 NetworkingConfig {
421 name: self.name,
422 _phantom: (PhantomData, PhantomData),
423 }
424 }
425
426 /// Configures the UDP transport to treat dropped messages as indefinitely delayed.
427 ///
428 /// UDP never guarantees delivery or ordering, so unlike TCP there is no `fail_stop`
429 /// policy. Unlike [`Self::lossy`], this does *not* require a [`NonDet`] annotation
430 /// because the output is always lower in the partial order than the ideal stream
431 /// (dropped messages are modeled as infinite delays). The output stream has
432 /// [`NoOrder`] guarantees, imposing stricter conditions on downstream consumers.
433 ///
434 /// Unlike [`Self::lossy`], this mode can easily be simulated in exhaustive mode
435 /// without running into fairness issues.
436 ///
437 /// When using this mode in the Hydro simulator, you must call
438 /// [`.test_safety_only()`](crate::sim::flow::SimFlow::test_safety_only) to opt in:
439 /// the simulator will not actually drop packets—it delays "dropped" messages until
440 /// the end of the execution, which catches safety bugs but cannot test liveness.
441 pub const fn lossy_delayed_forever(self) -> NetworkingConfig<Udp<LossyDelayedForever>, S> {
442 NetworkingConfig {
443 name: self.name,
444 _phantom: (PhantomData, PhantomData),
445 }
446 }
447}
448
449#[sealed::sealed]
450impl<Tr: ?Sized, S: ?Sized, T: ?Sized> NetworkFor<T> for NetworkingConfig<Tr, S>
451where
452 Tr: TransportKind,
453 S: SerKind<T>,
454{
455 type OrderingGuarantee = Tr::OrderingGuarantee;
456
457 type ConsistencyGuarantee = Tr::ConsistencyGuarantee;
458
459 fn serialize_thunk(is_demux: bool) -> syn::Expr {
460 S::serialize_thunk(is_demux)
461 }
462
463 fn deserialize_thunk(tagged: Option<&syn::Type>) -> syn::Expr {
464 S::deserialize_thunk(tagged)
465 }
466
467 fn is_embedded() -> bool {
468 S::is_embedded()
469 }
470
471 fn name(&self) -> Option<&str> {
472 None
473 }
474
475 fn networking_info() -> NetworkingInfo {
476 Tr::networking_info()
477 }
478}
479
480#[sealed::sealed]
481impl<Tr: ?Sized, S: ?Sized, T: ?Sized> NetworkFor<T> for NetworkingConfig<Tr, S, String>
482where
483 Tr: TransportKind,
484 S: SerKind<T>,
485{
486 type OrderingGuarantee = Tr::OrderingGuarantee;
487
488 type ConsistencyGuarantee = Tr::ConsistencyGuarantee;
489
490 fn serialize_thunk(is_demux: bool) -> syn::Expr {
491 S::serialize_thunk(is_demux)
492 }
493
494 fn deserialize_thunk(tagged: Option<&syn::Type>) -> syn::Expr {
495 S::deserialize_thunk(tagged)
496 }
497
498 fn is_embedded() -> bool {
499 S::is_embedded()
500 }
501
502 fn name(&self) -> Option<&str> {
503 self.name.as_deref()
504 }
505
506 fn networking_info() -> NetworkingInfo {
507 Tr::networking_info()
508 }
509}
510
511/// A network channel that uses length-delimited TCP for transport.
512pub const TCP: NetworkingConfig<Tcp<()>, NoSer> = NetworkingConfig {
513 name: None,
514 _phantom: (PhantomData, PhantomData),
515};
516
517/// A network channel that uses UDP for transport.
518///
519/// Unlike [`TCP`], UDP does not guarantee delivery or ordering, so output streams
520/// always have [`NoOrder`] guarantees. Because UDP is connectionless, there is no
521/// `fail_stop` policy; only [`lossy`](NetworkingConfig::lossy) and
522/// [`lossy_delayed_forever`](NetworkingConfig::lossy_delayed_forever) are available.
523///
524/// # Availability
525/// UDP is **not yet available** in "deploy" deployment mode (via Hydro Deploy,
526/// including Docker and ECS deployments); attempting to deploy a UDP channel there
527/// will panic at compile time. Both UDP modes are available for embedded
528/// deployments (the only production deployment option) and Maelstrom testing. In
529/// the Hydro simulator, only `lossy_delayed_forever` is supported, and it requires
530/// [`.test_safety_only()`](crate::sim::flow::SimFlow::test_safety_only): the
531/// simulator will not actually drop packets—it delays "dropped" messages until the
532/// end of the execution, which catches safety bugs but cannot test liveness.
533pub const UDP: NetworkingConfig<Udp<()>, NoSer> = NetworkingConfig {
534 name: None,
535 _phantom: (PhantomData, PhantomData),
536};