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