Skip to main content

dynamo_runtime/transports/event_plane/
codec.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Event plane codec for serializing/deserializing envelopes and payloads.
5
6use anyhow::{Result, anyhow, bail};
7use bytes::Bytes;
8use serde::{Deserialize, Serialize, de::DeserializeOwned};
9
10use super::EventEnvelope;
11
12/// Codec for serializing and deserializing event envelopes and payloads.
13///
14/// Currently only supports MessagePack for all transports.
15#[derive(Debug, Clone, Copy)]
16pub enum Codec {
17    Msgpack(MsgpackCodec),
18}
19
20impl Default for Codec {
21    fn default() -> Self {
22        Codec::Msgpack(MsgpackCodec)
23    }
24}
25
26impl Codec {
27    /// Encode an EventEnvelope to wire bytes
28    pub fn encode_envelope(&self, envelope: &EventEnvelope) -> Result<Bytes> {
29        match self {
30            Codec::Msgpack(c) => c.encode_envelope(envelope),
31        }
32    }
33
34    /// Encode an event envelope while borrowing its immutable topic and payload.
35    pub fn encode_envelope_parts(
36        &self,
37        publisher_id: u64,
38        sequence: u64,
39        published_at: u64,
40        topic: &str,
41        payload: &[u8],
42    ) -> Result<Bytes> {
43        match self {
44            Codec::Msgpack(c) => {
45                c.encode_envelope_parts(publisher_id, sequence, published_at, topic, payload)
46            }
47        }
48    }
49
50    /// Decode wire bytes to an EventEnvelope
51    pub fn decode_envelope(&self, bytes: &Bytes) -> Result<EventEnvelope> {
52        match self {
53            Codec::Msgpack(c) => c.decode_envelope(bytes),
54        }
55    }
56
57    /// Decode only the envelope identity without allocating its topic or payload.
58    pub(crate) fn decode_envelope_identity(&self, bytes: &Bytes) -> Result<(u64, u64)> {
59        match self {
60            Codec::Msgpack(c) => c.decode_envelope_identity(bytes),
61        }
62    }
63
64    /// Encode a typed payload to bytes (for embedding in envelope)
65    pub fn encode_payload<T: Serialize>(&self, payload: &T) -> Result<Bytes> {
66        match self {
67            Codec::Msgpack(c) => c.encode_payload(payload),
68        }
69    }
70
71    /// Decode payload bytes to a typed value
72    pub fn decode_payload<T: DeserializeOwned>(&self, bytes: &Bytes) -> Result<T> {
73        match self {
74            Codec::Msgpack(c) => c.decode_payload(bytes),
75        }
76    }
77
78    /// Codec name for debugging
79    pub fn name(&self) -> &'static str {
80        match self {
81            Codec::Msgpack(c) => c.name(),
82        }
83    }
84}
85
86#[derive(Debug, Clone, Copy, Default)]
87pub struct MsgpackCodec;
88
89#[derive(Deserialize, Serialize)]
90struct BorrowedEventEnvelope<'a> {
91    publisher_id: u64,
92    sequence: u64,
93    published_at: u64,
94    #[serde(borrow)]
95    topic: &'a str,
96    #[serde(borrow, serialize_with = "serialize_bytes")]
97    payload: &'a [u8],
98}
99
100fn serialize_bytes<S>(bytes: &&[u8], serializer: S) -> std::result::Result<S::Ok, S::Error>
101where
102    S: serde::Serializer,
103{
104    serializer.serialize_bytes(bytes)
105}
106
107impl MsgpackCodec {
108    pub fn encode_envelope(&self, envelope: &EventEnvelope) -> Result<Bytes> {
109        Ok(Bytes::from(rmp_serde::to_vec_named(envelope)?))
110    }
111
112    pub fn encode_envelope_parts(
113        &self,
114        publisher_id: u64,
115        sequence: u64,
116        published_at: u64,
117        topic: &str,
118        payload: &[u8],
119    ) -> Result<Bytes> {
120        let envelope = BorrowedEventEnvelope {
121            publisher_id,
122            sequence,
123            published_at,
124            topic,
125            payload,
126        };
127        Ok(Bytes::from(rmp_serde::to_vec_named(&envelope)?))
128    }
129
130    pub fn decode_envelope(&self, bytes: &Bytes) -> Result<EventEnvelope> {
131        let envelope: BorrowedEventEnvelope<'_> = rmp_serde::from_slice(bytes)?;
132        let payload_start = (envelope.payload.as_ptr() as usize)
133            .checked_sub(bytes.as_ptr() as usize)
134            .ok_or_else(|| {
135                anyhow!("MessagePack envelope payload is not borrowed from its frame")
136            })?;
137        let payload_end = payload_start
138            .checked_add(envelope.payload.len())
139            .ok_or_else(|| anyhow!("MessagePack envelope payload range overflowed"))?;
140
141        if payload_end > bytes.len() {
142            bail!("MessagePack envelope payload exceeds its frame");
143        }
144
145        Ok(EventEnvelope {
146            publisher_id: envelope.publisher_id,
147            sequence: envelope.sequence,
148            published_at: envelope.published_at,
149            topic: envelope.topic.to_owned(),
150            payload: bytes.slice(payload_start..payload_end),
151        })
152    }
153
154    pub(crate) fn decode_envelope_identity(&self, bytes: &Bytes) -> Result<(u64, u64)> {
155        let envelope: BorrowedEventEnvelope<'_> = rmp_serde::from_slice(bytes)?;
156        Ok((envelope.publisher_id, envelope.sequence))
157    }
158
159    pub fn encode_payload<T: Serialize>(&self, payload: &T) -> Result<Bytes> {
160        Ok(Bytes::from(rmp_serde::to_vec_named(payload)?))
161    }
162
163    pub fn decode_payload<T: DeserializeOwned>(&self, bytes: &Bytes) -> Result<T> {
164        Ok(rmp_serde::from_slice(bytes)?)
165    }
166
167    pub fn name(&self) -> &'static str {
168        "msgpack"
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[derive(Debug, Clone, PartialEq, Serialize, serde::Deserialize)]
177    struct TestEvent {
178        worker_id: u64,
179        message: String,
180    }
181
182    #[test]
183    fn test_msgpack_codec_envelope_roundtrip() {
184        let codec = MsgpackCodec;
185
186        let envelope = EventEnvelope {
187            publisher_id: 12345,
188            sequence: 42,
189            published_at: 1700000000000,
190            topic: "test-topic".to_string(),
191            payload: Bytes::from("test payload"),
192        };
193
194        let encoded = codec.encode_envelope(&envelope).unwrap();
195        let decoded = codec.decode_envelope(&encoded).unwrap();
196
197        assert_eq!(decoded.publisher_id, envelope.publisher_id);
198        assert_eq!(decoded.sequence, envelope.sequence);
199        assert_eq!(decoded.published_at, envelope.published_at);
200        assert_eq!(decoded.topic, envelope.topic);
201        assert_eq!(decoded.payload, envelope.payload);
202
203        let frame_start = encoded.as_ptr() as usize;
204        let frame_end = frame_start + encoded.len();
205        let payload_start = decoded.payload.as_ptr() as usize;
206        let payload_end = payload_start + decoded.payload.len();
207        assert!(
208            (frame_start..=frame_end).contains(&payload_start) && payload_end <= frame_end,
209            "decoded payload must share the encoded frame's backing storage"
210        );
211    }
212
213    #[test]
214    fn test_msgpack_codec_borrowed_envelope_roundtrip() {
215        let codec = MsgpackCodec;
216        let payload = b"borrowed payload";
217
218        let borrowed = codec
219            .encode_envelope_parts(12345, 42, 1700000000000, "test-topic", payload)
220            .unwrap();
221        let owned = codec
222            .encode_envelope(&EventEnvelope {
223                publisher_id: 12345,
224                sequence: 42,
225                published_at: 1700000000000,
226                topic: "test-topic".to_string(),
227                payload: Bytes::from_static(payload),
228            })
229            .unwrap();
230        assert_eq!(borrowed, owned);
231
232        let decoded = codec.decode_envelope(&borrowed).unwrap();
233
234        assert_eq!(decoded.publisher_id, 12345);
235        assert_eq!(decoded.sequence, 42);
236        assert_eq!(decoded.published_at, 1700000000000);
237        assert_eq!(decoded.topic, "test-topic");
238        assert_eq!(decoded.payload.as_ref(), payload);
239    }
240
241    #[test]
242    fn test_msgpack_codec_envelope_identity_decode() {
243        let codec = MsgpackCodec;
244        let encoded = codec
245            .encode_envelope_parts(12345, 42, 1700000000000, "test-topic", b"payload")
246            .unwrap();
247
248        assert_eq!(
249            codec.decode_envelope_identity(&encoded).unwrap(),
250            (12345, 42)
251        );
252    }
253
254    #[test]
255    fn test_msgpack_codec_rejects_malformed_envelope() {
256        let codec = MsgpackCodec;
257
258        assert!(codec.decode_envelope(&Bytes::from_static(&[0xc1])).is_err());
259        assert!(
260            codec
261                .decode_envelope_identity(&Bytes::from_static(&[0xc1]))
262                .is_err()
263        );
264    }
265
266    #[test]
267    fn test_msgpack_codec_payload_roundtrip() {
268        let codec = MsgpackCodec;
269
270        let event = TestEvent {
271            worker_id: 123,
272            message: "hello world".to_string(),
273        };
274
275        let encoded = codec.encode_payload(&event).unwrap();
276        let decoded: TestEvent = codec.decode_payload(&encoded).unwrap();
277
278        assert_eq!(decoded, event);
279    }
280}