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 TCP 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 TCP 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) because the
121/// simulator models dropped messages as indefinitely delayed, which only tests safety
122/// properties (not 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/// Send items across a length-delimited TCP channel.
134pub struct Tcp<F> {
135    _phantom: PhantomData<F>,
136}
137
138#[sealed::sealed]
139impl<F: TcpFailPolicy> TransportKind for Tcp<F> {
140    type OrderingGuarantee = F::OrderingGuarantee;
141
142    fn networking_info() -> NetworkingInfo {
143        NetworkingInfo::Tcp {
144            fault: F::tcp_fault(),
145        }
146    }
147}
148
149/// A networking backend implementation that supports items of type `T`.
150#[sealed::sealed]
151pub trait NetworkFor<T: ?Sized> {
152    /// The ordering guarantee provided by this network configuration.
153    /// When combined with an input stream's ordering `O`, the output ordering
154    /// will be `<O as MinOrder<Self::OrderingGuarantee>>::Min`.
155    type OrderingGuarantee: crate::live_collections::stream::Ordering;
156
157    /// Generates serialization logic for sending `T`.
158    fn serialize_thunk(is_demux: bool) -> syn::Expr;
159
160    /// Generates deserialization logic for receiving `T`.
161    fn deserialize_thunk(tagged: Option<&syn::Type>) -> syn::Expr;
162
163    /// Whether this network channel leaves serialization to code outside of Hydro (see
164    /// [`Embedded`]). When `true`, [`Self::serialize_thunk`] and [`Self::deserialize_thunk`] are
165    /// never called; the raw element type flows across the channel unserialized.
166    fn is_embedded() -> bool {
167        false
168    }
169
170    /// Returns the optional name of the network channel.
171    fn name(&self) -> Option<&str>;
172
173    /// Returns the [`NetworkingInfo`] describing this network channel's transport and fault model.
174    fn networking_info() -> NetworkingInfo;
175}
176
177/// The fault model for a TCP connection.
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
179pub enum TcpFault {
180    /// Stops sending messages after a failed connection.
181    FailStop,
182    /// Messages may be lost (e.g. due to network partitions).
183    Lossy,
184    /// Dropped messages are treated as indefinitely delayed with no ordering guarantee.
185    LossyDelayedForever,
186}
187
188/// Describes the networking configuration for a network channel at the IR level.
189#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
190pub enum NetworkingInfo {
191    /// A TCP-based network channel with a specific fault model.
192    Tcp {
193        /// The fault model for this TCP connection.
194        fault: TcpFault,
195    },
196}
197
198/// A network channel configuration with `T` as transport backend and `S` as the serialization
199/// backend.
200pub struct NetworkingConfig<Tr: ?Sized, S: ?Sized, Name = ()> {
201    name: Option<Name>,
202    _phantom: (PhantomData<Tr>, PhantomData<S>),
203}
204
205impl<Tr: ?Sized, S: ?Sized> NetworkingConfig<Tr, S> {
206    /// Names the network channel and enables stable communication across multiple service versions.
207    pub fn name(self, name: impl Into<String>) -> NetworkingConfig<Tr, S, String> {
208        NetworkingConfig {
209            name: Some(name.into()),
210            _phantom: (PhantomData, PhantomData),
211        }
212    }
213}
214
215impl<Tr: ?Sized, N> NetworkingConfig<Tr, NoSer, N> {
216    /// Configures the network channel to use [`bincode`] to serialize items.
217    pub const fn bincode(mut self) -> NetworkingConfig<Tr, Bincode, N> {
218        let taken_name = self.name.take();
219        std::mem::forget(self); // nothing else is stored
220        NetworkingConfig {
221            name: taken_name,
222            _phantom: (PhantomData, PhantomData),
223        }
224    }
225
226    /// Configures the network channel to leave serialization to code outside of Hydro.
227    ///
228    /// This is only supported by the embedded deployment backend (it will panic on all other
229    /// backends). The generated network channel exposes the raw element type to the developer
230    /// (rather than serialized bytes), so they can perform custom serialization logic outside of
231    /// the Hydro program for that channel.
232    pub const fn embedded(mut self) -> NetworkingConfig<Tr, Embedded, N> {
233        let taken_name = self.name.take();
234        std::mem::forget(self); // nothing else is stored
235        NetworkingConfig {
236            name: taken_name,
237            _phantom: (PhantomData, PhantomData),
238        }
239    }
240}
241
242impl<S: ?Sized> NetworkingConfig<Tcp<()>, S> {
243    /// Configures the TCP transport to stop sending messages after a failed connection.
244    ///
245    /// Note that the Hydro simulator will not simulate connection failures that impact the
246    /// *liveness* of a program. If an output assertion depends on a `fail_stop` channel
247    /// making progress, that channel will not experience a failure that would cause the test to
248    /// block indefinitely. However, any *safety* issues caused by connection failures will still
249    /// be caught, such as a race condition between a failed connection and some other message.
250    pub const fn fail_stop(self) -> NetworkingConfig<Tcp<FailStop>, S> {
251        NetworkingConfig {
252            name: self.name,
253            _phantom: (PhantomData, PhantomData),
254        }
255    }
256
257    /// Configures the TCP transport to allow messages to be lost.
258    ///
259    /// This is appropriate for networks where messages may be dropped, such as when
260    /// running under a Maelstrom partition nemesis. Unlike `fail_stop`, which guarantees
261    /// a prefix of messages is delivered, `lossy` makes no such guarantee.
262    ///
263    /// # Non-Determinism
264    /// A lossy TCP channel will non-deterministically drop messages during execution.
265    pub const fn lossy(self, nondet: NonDet) -> NetworkingConfig<Tcp<Lossy>, S> {
266        let _ = nondet;
267        NetworkingConfig {
268            name: self.name,
269            _phantom: (PhantomData, PhantomData),
270        }
271    }
272
273    /// Configures the TCP transport to treat dropped messages as indefinitely delayed.
274    ///
275    /// This is appropriate for networks where messages may be dropped, such as when
276    /// running under a Maelstrom partition nemesis. Unlike [`Self::lossy`], this does
277    /// *not* require a [`NonDet`] annotation because the output is always lower in the
278    /// partial order than the ideal stream. However, the output stream will have
279    /// [`NoOrder`] guarantees, imposing stricter conditions on downstream consumers.
280    ///
281    /// Unlike [`Self::lossy`], this mode can easily be simulated in exhaustive mode
282    /// without running into fairness issues.
283    ///
284    /// When using this mode in the Hydro simulator, you must call
285    /// [`.test_safety_only()`](crate::sim::flow::SimFlow::test_safety_only) to opt in,
286    /// because the simulator models dropped messages as indefinitely delayed, which only
287    /// tests safety properties (not liveness).
288    pub const fn lossy_delayed_forever(self) -> NetworkingConfig<Tcp<LossyDelayedForever>, S> {
289        NetworkingConfig {
290            name: self.name,
291            _phantom: (PhantomData, PhantomData),
292        }
293    }
294}
295
296#[sealed::sealed]
297impl<Tr: ?Sized, S: ?Sized, T: ?Sized> NetworkFor<T> for NetworkingConfig<Tr, S>
298where
299    Tr: TransportKind,
300    S: SerKind<T>,
301{
302    type OrderingGuarantee = Tr::OrderingGuarantee;
303
304    fn serialize_thunk(is_demux: bool) -> syn::Expr {
305        S::serialize_thunk(is_demux)
306    }
307
308    fn deserialize_thunk(tagged: Option<&syn::Type>) -> syn::Expr {
309        S::deserialize_thunk(tagged)
310    }
311
312    fn is_embedded() -> bool {
313        S::is_embedded()
314    }
315
316    fn name(&self) -> Option<&str> {
317        None
318    }
319
320    fn networking_info() -> NetworkingInfo {
321        Tr::networking_info()
322    }
323}
324
325#[sealed::sealed]
326impl<Tr: ?Sized, S: ?Sized, T: ?Sized> NetworkFor<T> for NetworkingConfig<Tr, S, String>
327where
328    Tr: TransportKind,
329    S: SerKind<T>,
330{
331    type OrderingGuarantee = Tr::OrderingGuarantee;
332
333    fn serialize_thunk(is_demux: bool) -> syn::Expr {
334        S::serialize_thunk(is_demux)
335    }
336
337    fn deserialize_thunk(tagged: Option<&syn::Type>) -> syn::Expr {
338        S::deserialize_thunk(tagged)
339    }
340
341    fn is_embedded() -> bool {
342        S::is_embedded()
343    }
344
345    fn name(&self) -> Option<&str> {
346        self.name.as_deref()
347    }
348
349    fn networking_info() -> NetworkingInfo {
350        Tr::networking_info()
351    }
352}
353
354/// A network channel that uses length-delimited TCP for transport.
355pub const TCP: NetworkingConfig<Tcp<()>, NoSer> = NetworkingConfig {
356    name: None,
357    _phantom: (PhantomData, PhantomData),
358};