Skip to main content

velo_common/
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 crate::identity::{InstanceId, WorkerId};
14use crate::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    /// Get the underlying bytes.
93    pub fn as_bytes(&self) -> &[u8] {
94        &self.0
95    }
96
97    /// Get the bytes as a Bytes object.
98    pub fn to_bytes(&self) -> Bytes {
99        self.0.clone()
100    }
101
102    /// Compute a checksum of this address for validation.
103    ///
104    /// This is used to quickly check if an address has changed during re-registration.
105    pub fn checksum(&self) -> u64 {
106        xxh3_64(self.as_bytes())
107    }
108
109    /// Get the list of available transport keys in this address.
110    ///
111    /// Returns the keys from the internal map as `TransportKey` for type-safe efficient
112    /// storage and sharing. This allows callers to see what transport types or endpoints
113    /// are available without exposing the full map.
114    ///
115    /// # Errors
116    ///
117    /// Returns an error if the internal bytes cannot be decoded as a valid MessagePack map.
118    ///
119    /// # Example
120    ///
121    /// ```no_run
122    /// # use velo_common::{WorkerAddress, TransportKey};
123    /// # let address: WorkerAddress = unimplemented!();
124    /// let transports = address.available_transports().unwrap();
125    /// if transports.contains(&TransportKey::from("tcp")) {
126    ///     // TCP transport is available
127    /// }
128    /// ```
129    pub fn available_transports(&self) -> Result<Vec<TransportKey>, WorkerAddressError> {
130        let map = decode_to_map(self.as_bytes())?;
131        Ok(map.keys().cloned().map(TransportKey::from).collect())
132    }
133
134    /// Get a single entry from the internal map.
135    ///
136    /// This decodes the address and extracts the entry for the given key.
137    ///
138    /// Accepts any type that can be converted to a string reference, including
139    /// `&str`, `String`, `&String`, and `TransportKey`.
140    ///
141    /// # Errors
142    ///
143    /// Returns an error if the internal bytes cannot be decoded as a valid MessagePack map.
144    pub fn get_entry(&self, key: impl AsRef<str>) -> Result<Option<Bytes>, WorkerAddressError> {
145        let map = decode_to_map(self.as_bytes())?;
146        Ok(map.get(key.as_ref()).cloned())
147    }
148}
149
150impl fmt::Debug for WorkerAddress {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        f.debug_tuple("WorkerAddress")
153            .field(&format_args!(
154                "len={}, xxh3_64=0x{:016x}",
155                self.0.len(),
156                self.checksum()
157            ))
158            .finish()
159    }
160}
161
162impl fmt::Display for WorkerAddress {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        write!(f, "WorkerAddress(xxh3_64=0x{:016x})", self.checksum())
165    }
166}
167
168// ============================================================================
169// Internal Decoding Helper
170// ============================================================================
171
172/// Decode WorkerAddress bytes from MessagePack into a map.
173fn decode_to_map(bytes: &[u8]) -> Result<HashMap<Arc<str>, Bytes>, WorkerAddressError> {
174    if bytes.is_empty() {
175        return Err(WorkerAddressError::InvalidFormat("Empty bytes".to_string()));
176    }
177
178    // Decode MessagePack
179    let decoded: HashMap<String, Vec<u8>> = rmp_serde::from_slice(bytes)?;
180
181    // Convert to HashMap<Arc<str>, Bytes>
182    Ok(decoded
183        .into_iter()
184        .map(|(k, v)| (Arc::from(k.as_str()), Bytes::from(v)))
185        .collect())
186}
187
188/// Peer information combining instance ID and worker address.
189///
190/// This is the primary type returned by discovery lookups. It contains everything
191/// needed to connect to and identify a peer.
192///
193/// # Example
194///
195/// ```no_run
196/// # // WorkerAddress is created internally, this is simplified for docs
197/// use velo_common::{InstanceId, PeerInfo};
198/// # use velo_common::WorkerAddress;
199/// # let address: WorkerAddress = unimplemented!();
200///
201/// let instance_id = InstanceId::new_v4();
202/// let peer_info = PeerInfo::new(instance_id, address);
203///
204/// assert_eq!(peer_info.instance_id(), instance_id);
205/// assert_eq!(peer_info.worker_id(), instance_id.worker_id());
206/// ```
207#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
208pub struct PeerInfo {
209    /// The instance ID of the peer
210    pub instance_id: InstanceId,
211    /// The worker address for connecting to the peer
212    pub worker_address: WorkerAddress,
213}
214
215impl PeerInfo {
216    /// Create a new PeerInfo.
217    pub fn new(instance_id: InstanceId, worker_address: WorkerAddress) -> Self {
218        Self {
219            instance_id,
220            worker_address,
221        }
222    }
223
224    /// Get the instance ID.
225    pub fn instance_id(&self) -> InstanceId {
226        self.instance_id
227    }
228
229    /// Get the worker ID (derived from instance ID).
230    pub fn worker_id(&self) -> WorkerId {
231        self.instance_id.worker_id()
232    }
233
234    /// Get a reference to the worker address.
235    pub fn worker_address(&self) -> &WorkerAddress {
236        &self.worker_address
237    }
238
239    /// Get the worker address checksum for validation.
240    pub fn address_checksum(&self) -> u64 {
241        self.worker_address.checksum()
242    }
243
244    /// Consume self and return the worker address.
245    pub fn into_address(self) -> WorkerAddress {
246        self.worker_address
247    }
248
249    /// Decompose into instance ID and worker address.
250    pub fn into_parts(self) -> (InstanceId, WorkerAddress) {
251        (self.instance_id, self.worker_address)
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    // Helper to create a test address with MessagePack encoding
260    fn make_test_address(entries: &[(&str, &[u8])]) -> WorkerAddress {
261        let map: HashMap<String, Vec<u8>> = entries
262            .iter()
263            .map(|(k, v)| (k.to_string(), v.to_vec()))
264            .collect();
265        let encoded = rmp_serde::to_vec(&map).unwrap();
266        WorkerAddress::from_encoded(encoded)
267    }
268
269    #[test]
270    fn test_worker_address_from_encoded() {
271        let address = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
272
273        // Verify we can get the entry back
274        let entry = address.get_entry("endpoint").unwrap();
275        assert_eq!(entry, Some(Bytes::from_static(b"tcp://127.0.0.1:5555")));
276    }
277
278    #[test]
279    fn test_worker_address_checksum() {
280        let address1 = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
281        let address2 = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
282        let address3 = make_test_address(&[("endpoint", b"tcp://127.0.0.1:6666")]);
283
284        // Same content = same checksum
285        assert_eq!(address1.checksum(), address2.checksum());
286
287        // Different content = different checksum
288        assert_ne!(address1.checksum(), address3.checksum());
289    }
290
291    #[test]
292    fn test_worker_address_equality() {
293        let address1 = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
294        let address2 = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
295        let address3 = make_test_address(&[("endpoint", b"tcp://127.0.0.1:6666")]);
296
297        assert_eq!(address1, address2);
298        assert_ne!(address1, address3);
299    }
300
301    #[test]
302    fn test_worker_address_debug() {
303        let address = make_test_address(&[("test", b"value")]);
304        let debug_str = format!("{:?}", address);
305
306        assert!(debug_str.contains("WorkerAddress"));
307        assert!(debug_str.contains("len="));
308        assert!(debug_str.contains("xxh3_64="));
309    }
310
311    #[test]
312    fn test_available_transports() {
313        let address = make_test_address(&[
314            ("tcp", b"tcp://127.0.0.1:5555"),
315            ("rdma", b"rdma://10.0.0.1:6666"),
316            ("udp", b"udp://127.0.0.1:7777"),
317        ]);
318
319        let transports = address.available_transports().unwrap();
320        assert_eq!(transports.len(), 3);
321        assert!(transports.contains(&TransportKey::from("tcp")));
322        assert!(transports.contains(&TransportKey::from("rdma")));
323        assert!(transports.contains(&TransportKey::from("udp")));
324    }
325
326    #[test]
327    fn test_available_transports_empty() {
328        let address = make_test_address(&[]);
329        let transports = address.available_transports().unwrap();
330        assert_eq!(transports.len(), 0);
331    }
332
333    #[test]
334    fn test_get_entry() {
335        let address =
336            make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555"), ("protocol", b"tcp")]);
337
338        // Get existing entry
339        assert_eq!(
340            address.get_entry("endpoint").unwrap().unwrap(),
341            Bytes::from_static(b"tcp://127.0.0.1:5555")
342        );
343
344        // Get nonexistent entry
345        assert!(address.get_entry("nonexistent").unwrap().is_none());
346    }
347
348    #[test]
349    fn test_get_entry_with_transport_key() {
350        let address = make_test_address(&[
351            ("tcp", b"tcp://127.0.0.1:5555"),
352            ("rdma", b"rdma://10.0.0.1:6666"),
353        ]);
354
355        // Test get_entry with TransportKey
356        let tcp_key = TransportKey::from("tcp");
357        let result = address.get_entry(tcp_key).unwrap();
358        assert_eq!(result, Some(Bytes::from_static(b"tcp://127.0.0.1:5555")));
359
360        // Test get_entry with String
361        let result = address.get_entry(String::from("rdma")).unwrap();
362        assert_eq!(result, Some(Bytes::from_static(b"rdma://10.0.0.1:6666")));
363    }
364
365    #[test]
366    fn test_peer_info_creation() {
367        let instance_id = InstanceId::new_v4();
368        let address = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
369
370        let peer_info = PeerInfo::new(instance_id, address.clone());
371
372        assert_eq!(peer_info.instance_id(), instance_id);
373        assert_eq!(peer_info.worker_id(), instance_id.worker_id());
374        assert_eq!(peer_info.worker_address(), &address);
375    }
376
377    #[test]
378    fn test_peer_info_checksum() {
379        let instance_id = InstanceId::new_v4();
380        let address = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
381
382        let peer_info = PeerInfo::new(instance_id, address.clone());
383
384        assert_eq!(peer_info.address_checksum(), address.checksum());
385    }
386
387    #[test]
388    fn test_peer_info_into_address() {
389        let instance_id = InstanceId::new_v4();
390        let address = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
391
392        let peer_info = PeerInfo::new(instance_id, address.clone());
393        let extracted_address = peer_info.into_address();
394
395        assert_eq!(extracted_address, address);
396    }
397
398    #[test]
399    fn test_peer_info_into_parts() {
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_id, extracted_address) = peer_info.into_parts();
405
406        assert_eq!(extracted_id, instance_id);
407        assert_eq!(extracted_address, address);
408    }
409
410    #[test]
411    fn test_peer_info_serde() {
412        let instance_id = InstanceId::new_v4();
413        let address = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
414        let peer_info = PeerInfo::new(instance_id, address);
415
416        // Serialize to JSON
417        let json = serde_json::to_string(&peer_info).unwrap();
418
419        // Deserialize back
420        let deserialized: PeerInfo = serde_json::from_str(&json).unwrap();
421
422        assert_eq!(deserialized.instance_id(), instance_id);
423        assert_eq!(deserialized.worker_id(), instance_id.worker_id());
424
425        // Verify the entry is preserved
426        let entry = deserialized.worker_address().get_entry("endpoint").unwrap();
427        assert_eq!(entry, Some(Bytes::from_static(b"tcp://127.0.0.1:5555")));
428    }
429}