Skip to main content

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};