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