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