Skip to main content

velo_ext/id/
address.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Address types for peer discovery.
5//!
6//! This module provides types for representing worker addresses and peer information:
7//! - [`WorkerAddress`]: Opaque byte representation of a peer's network address
8//! - [`PeerInfo`]: Combined instance ID and worker address for a discovered peer
9//!
10//! These types are intentionally transport-agnostic, storing addresses as opaque bytes.
11//! The interpretation of these bytes is left to the active message runtime.
12
13use super::identity::{InstanceId, WorkerId};
14use super::transport::TransportKey;
15
16use bytes::Bytes;
17use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19use std::fmt;
20use std::sync::Arc;
21use xxhash_rust::xxh3::xxh3_64;
22
23/// Errors that can occur when working with WorkerAddress.
24#[derive(Debug, thiserror::Error)]
25pub enum WorkerAddressError {
26    /// Attempted to add a key that already exists
27    #[error("Key already exists: {0}")]
28    KeyExists(String),
29
30    /// Attempted to access or remove a key that doesn't exist
31    #[error("Key not found: {0}")]
32    KeyNotFound(String),
33
34    /// Failed to encode the map to bytes
35    #[error("Encoding error: {0}")]
36    EncodingError(#[from] rmp_serde::encode::Error),
37
38    /// Failed to decode bytes to map
39    #[error("Decoding error: {0}")]
40    DecodingError(#[from] rmp_serde::decode::Error),
41
42    /// Encountered an unsupported format version
43    #[error("Unsupported format version: {0}")]
44    UnsupportedVersion(u8),
45
46    /// The data format is invalid
47    #[error("Invalid format: {0}")]
48    InvalidFormat(String),
49}
50
51/// Opaque worker address for discovery.
52///
53/// This is a transport-agnostic representation of a peer's network address.
54/// The bytes are opaque to discovery and are interpreted by the active message runtime.
55///
56/// # Checksum
57///
58/// WorkerAddress implements a checksum via xxh3_64 for quick comparison during
59/// re-registration validation.
60#[derive(Clone, PartialEq, Eq, Hash)]
61pub struct WorkerAddress(Bytes);
62
63// Custom Serialize/Deserialize to handle Bytes
64impl Serialize for WorkerAddress {
65    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
66    where
67        S: serde::Serializer,
68    {
69        serde_bytes::serialize(self.0.as_ref(), serializer)
70    }
71}
72
73impl<'de> Deserialize<'de> for WorkerAddress {
74    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
75    where
76        D: serde::Deserializer<'de>,
77    {
78        let bytes: Vec<u8> = serde_bytes::deserialize(deserializer)?;
79        Ok(WorkerAddress(Bytes::from(bytes)))
80    }
81}
82
83impl WorkerAddress {
84    /// Create a WorkerAddress from pre-encoded bytes.
85    ///
86    /// This is used by transport implementations to construct addresses from
87    /// MessagePack-encoded map data. The bytes are assumed to be valid MessagePack.
88    pub fn from_encoded(bytes: impl Into<Bytes>) -> Self {
89        Self(bytes.into())
90    }
91
92    /// Create an empty WorkerAddress.
93    ///
94    /// Encodes an empty `HashMap<String, Vec<u8>>` as MessagePack. Useful for
95    /// transports that do not advertise their own endpoint into WorkerAddress
96    /// (e.g., a streaming transport that piggybacks on the messenger).
97    pub fn empty() -> Self {
98        let empty: HashMap<String, Vec<u8>> = HashMap::new();
99        let encoded = rmp_serde::to_vec(&empty).expect("encoding empty HashMap cannot fail");
100        Self(Bytes::from(encoded))
101    }
102
103    /// Get the underlying bytes.
104    pub fn as_bytes(&self) -> &[u8] {
105        &self.0
106    }
107
108    /// Get the bytes as a Bytes object.
109    pub fn to_bytes(&self) -> Bytes {
110        self.0.clone()
111    }
112
113    /// Compute a checksum of this address for validation.
114    ///
115    /// This is used to quickly check if an address has changed during re-registration.
116    pub fn checksum(&self) -> u64 {
117        xxh3_64(self.as_bytes())
118    }
119
120    /// Get the list of available transport keys in this address.
121    ///
122    /// Returns the keys from the internal map as `TransportKey` for type-safe efficient
123    /// storage and sharing. This allows callers to see what transport types or endpoints
124    /// are available without exposing the full map.
125    ///
126    /// # Errors
127    ///
128    /// Returns an error if the internal bytes cannot be decoded as a valid MessagePack map.
129    ///
130    /// # Example
131    ///
132    /// ```no_run
133    /// # use velo_ext::{WorkerAddress, TransportKey};
134    /// # let address: WorkerAddress = unimplemented!();
135    /// let transports = address.available_transports().unwrap();
136    /// if transports.contains(&TransportKey::from("tcp")) {
137    ///     // TCP transport is available
138    /// }
139    /// ```
140    pub fn available_transports(&self) -> Result<Vec<TransportKey>, WorkerAddressError> {
141        let map = decode_to_map(self.as_bytes())?;
142        Ok(map.keys().cloned().map(TransportKey::from).collect())
143    }
144
145    /// Get a single entry from the internal map.
146    ///
147    /// This decodes the address and extracts the entry for the given key.
148    ///
149    /// Accepts any type that can be converted to a string reference, including
150    /// `&str`, `String`, `&String`, and `TransportKey`.
151    ///
152    /// # Errors
153    ///
154    /// Returns an error if the internal bytes cannot be decoded as a valid MessagePack map.
155    pub fn get_entry(&self, key: impl AsRef<str>) -> Result<Option<Bytes>, WorkerAddressError> {
156        let map = decode_to_map(self.as_bytes())?;
157        Ok(map.get(key.as_ref()).cloned())
158    }
159}
160
161impl fmt::Debug for WorkerAddress {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        f.debug_tuple("WorkerAddress")
164            .field(&format_args!(
165                "len={}, xxh3_64=0x{:016x}",
166                self.0.len(),
167                self.checksum()
168            ))
169            .finish()
170    }
171}
172
173impl fmt::Display for WorkerAddress {
174    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175        write!(f, "WorkerAddress(xxh3_64=0x{:016x})", self.checksum())
176    }
177}
178
179// ============================================================================
180// Internal Decoding Helper
181// ============================================================================
182
183/// Decode WorkerAddress bytes from MessagePack into a map.
184fn decode_to_map(bytes: &[u8]) -> Result<HashMap<Arc<str>, Bytes>, WorkerAddressError> {
185    if bytes.is_empty() {
186        return Err(WorkerAddressError::InvalidFormat("Empty bytes".to_string()));
187    }
188
189    // Decode MessagePack
190    let decoded: HashMap<String, Vec<u8>> = rmp_serde::from_slice(bytes)?;
191
192    // Convert to HashMap<Arc<str>, Bytes>
193    Ok(decoded
194        .into_iter()
195        .map(|(k, v)| (Arc::from(k.as_str()), Bytes::from(v)))
196        .collect())
197}
198
199/// Peer information combining instance ID and worker address.
200///
201/// This is the primary type returned by discovery lookups. It contains everything
202/// needed to connect to and identify a peer.
203///
204/// # Example
205///
206/// ```no_run
207/// # // WorkerAddress is created internally, this is simplified for docs
208/// use velo_ext::{InstanceId, PeerInfo};
209/// # use velo_ext::WorkerAddress;
210/// # let address: WorkerAddress = unimplemented!();
211///
212/// let instance_id = InstanceId::new_v4();
213/// let peer_info = PeerInfo::new(instance_id, address);
214///
215/// assert_eq!(peer_info.instance_id(), instance_id);
216/// assert_eq!(peer_info.worker_id(), instance_id.worker_id());
217/// ```
218#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
219pub struct PeerInfo {
220    /// The instance ID of the peer
221    pub instance_id: InstanceId,
222    /// The worker address for connecting to the peer
223    pub worker_address: WorkerAddress,
224}
225
226impl PeerInfo {
227    /// Create a new PeerInfo.
228    pub fn new(instance_id: InstanceId, worker_address: WorkerAddress) -> Self {
229        Self {
230            instance_id,
231            worker_address,
232        }
233    }
234
235    /// Get the instance ID.
236    pub fn instance_id(&self) -> InstanceId {
237        self.instance_id
238    }
239
240    /// Get the worker ID (derived from instance ID).
241    pub fn worker_id(&self) -> WorkerId {
242        self.instance_id.worker_id()
243    }
244
245    /// Get a reference to the worker address.
246    pub fn worker_address(&self) -> &WorkerAddress {
247        &self.worker_address
248    }
249
250    /// Get the worker address checksum for validation.
251    pub fn address_checksum(&self) -> u64 {
252        self.worker_address.checksum()
253    }
254
255    /// Consume self and return the worker address.
256    pub fn into_address(self) -> WorkerAddress {
257        self.worker_address
258    }
259
260    /// Decompose into instance ID and worker address.
261    pub fn into_parts(self) -> (InstanceId, WorkerAddress) {
262        (self.instance_id, self.worker_address)
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269
270    // Helper to create a test address with MessagePack encoding
271    fn make_test_address(entries: &[(&str, &[u8])]) -> WorkerAddress {
272        let map: HashMap<String, Vec<u8>> = entries
273            .iter()
274            .map(|(k, v)| (k.to_string(), v.to_vec()))
275            .collect();
276        let encoded = rmp_serde::to_vec(&map).unwrap();
277        WorkerAddress::from_encoded(encoded)
278    }
279
280    #[test]
281    fn test_worker_address_from_encoded() {
282        let address = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
283
284        // Verify we can get the entry back
285        let entry = address.get_entry("endpoint").unwrap();
286        assert_eq!(entry, Some(Bytes::from_static(b"tcp://127.0.0.1:5555")));
287    }
288
289    #[test]
290    fn test_worker_address_checksum() {
291        let address1 = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
292        let address2 = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
293        let address3 = make_test_address(&[("endpoint", b"tcp://127.0.0.1:6666")]);
294
295        // Same content = same checksum
296        assert_eq!(address1.checksum(), address2.checksum());
297
298        // Different content = different checksum
299        assert_ne!(address1.checksum(), address3.checksum());
300    }
301
302    #[test]
303    fn test_worker_address_equality() {
304        let address1 = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
305        let address2 = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
306        let address3 = make_test_address(&[("endpoint", b"tcp://127.0.0.1:6666")]);
307
308        assert_eq!(address1, address2);
309        assert_ne!(address1, address3);
310    }
311
312    #[test]
313    fn test_worker_address_debug() {
314        let address = make_test_address(&[("test", b"value")]);
315        let debug_str = format!("{:?}", address);
316
317        assert!(debug_str.contains("WorkerAddress"));
318        assert!(debug_str.contains("len="));
319        assert!(debug_str.contains("xxh3_64="));
320    }
321
322    #[test]
323    fn test_available_transports() {
324        let address = make_test_address(&[
325            ("tcp", b"tcp://127.0.0.1:5555"),
326            ("rdma", b"rdma://10.0.0.1:6666"),
327            ("udp", b"udp://127.0.0.1:7777"),
328        ]);
329
330        let transports = address.available_transports().unwrap();
331        assert_eq!(transports.len(), 3);
332        assert!(transports.contains(&TransportKey::from("tcp")));
333        assert!(transports.contains(&TransportKey::from("rdma")));
334        assert!(transports.contains(&TransportKey::from("udp")));
335    }
336
337    #[test]
338    fn test_available_transports_empty() {
339        let address = make_test_address(&[]);
340        let transports = address.available_transports().unwrap();
341        assert_eq!(transports.len(), 0);
342    }
343
344    #[test]
345    fn test_get_entry() {
346        let address =
347            make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555"), ("protocol", b"tcp")]);
348
349        // Get existing entry
350        assert_eq!(
351            address.get_entry("endpoint").unwrap().unwrap(),
352            Bytes::from_static(b"tcp://127.0.0.1:5555")
353        );
354
355        // Get nonexistent entry
356        assert!(address.get_entry("nonexistent").unwrap().is_none());
357    }
358
359    #[test]
360    fn test_get_entry_with_transport_key() {
361        let address = make_test_address(&[
362            ("tcp", b"tcp://127.0.0.1:5555"),
363            ("rdma", b"rdma://10.0.0.1:6666"),
364        ]);
365
366        // Test get_entry with TransportKey
367        let tcp_key = TransportKey::from("tcp");
368        let result = address.get_entry(tcp_key).unwrap();
369        assert_eq!(result, Some(Bytes::from_static(b"tcp://127.0.0.1:5555")));
370
371        // Test get_entry with String
372        let result = address.get_entry(String::from("rdma")).unwrap();
373        assert_eq!(result, Some(Bytes::from_static(b"rdma://10.0.0.1:6666")));
374    }
375
376    #[test]
377    fn test_peer_info_creation() {
378        let instance_id = InstanceId::new_v4();
379        let address = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
380
381        let peer_info = PeerInfo::new(instance_id, address.clone());
382
383        assert_eq!(peer_info.instance_id(), instance_id);
384        assert_eq!(peer_info.worker_id(), instance_id.worker_id());
385        assert_eq!(peer_info.worker_address(), &address);
386    }
387
388    #[test]
389    fn test_peer_info_checksum() {
390        let instance_id = InstanceId::new_v4();
391        let address = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
392
393        let peer_info = PeerInfo::new(instance_id, address.clone());
394
395        assert_eq!(peer_info.address_checksum(), address.checksum());
396    }
397
398    #[test]
399    fn test_peer_info_into_address() {
400        let instance_id = InstanceId::new_v4();
401        let address = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
402
403        let peer_info = PeerInfo::new(instance_id, address.clone());
404        let extracted_address = peer_info.into_address();
405
406        assert_eq!(extracted_address, address);
407    }
408
409    #[test]
410    fn test_peer_info_into_parts() {
411        let instance_id = InstanceId::new_v4();
412        let address = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
413
414        let peer_info = PeerInfo::new(instance_id, address.clone());
415        let (extracted_id, extracted_address) = peer_info.into_parts();
416
417        assert_eq!(extracted_id, instance_id);
418        assert_eq!(extracted_address, address);
419    }
420
421    #[test]
422    fn test_peer_info_serde() {
423        let instance_id = InstanceId::new_v4();
424        let address = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
425        let peer_info = PeerInfo::new(instance_id, address);
426
427        // Serialize to JSON
428        let json = serde_json::to_string(&peer_info).unwrap();
429
430        // Deserialize back
431        let deserialized: PeerInfo = serde_json::from_str(&json).unwrap();
432
433        assert_eq!(deserialized.instance_id(), instance_id);
434        assert_eq!(deserialized.worker_id(), instance_id.worker_id());
435
436        // Verify the entry is preserved
437        let entry = deserialized.worker_address().get_entry("endpoint").unwrap();
438        assert_eq!(entry, Some(Bytes::from_static(b"tcp://127.0.0.1:5555")));
439    }
440
441    #[test]
442    fn test_worker_address_empty() {
443        let empty = WorkerAddress::empty();
444        // get_entry on any key returns Ok(None).
445        assert!(empty.get_entry("anything").unwrap().is_none());
446        // available_transports decodes to an empty list.
447        assert!(empty.available_transports().unwrap().is_empty());
448        // Two empties round-trip equal (deterministic encoding).
449        assert_eq!(empty, WorkerAddress::empty());
450        assert_eq!(empty.checksum(), WorkerAddress::empty().checksum());
451    }
452}