Unverified Commit 63d7c01c authored by Ryan Olson's avatar Ryan Olson Committed by GitHub
Browse files

feat: velo-backend (#6547)


Signed-off-by: default avatarRyan Olson <rolson@nvidia.com>
parent 2cc92bfa
......@@ -306,6 +306,25 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "async-ucx"
version = "0.2.0"
source = "git+https://github.com/ryanolson/async-ucx.git?branch=ryan%2Fucx#fbcb8e3d281d7c910e3dcd84f7eb3e8ee253ac6e"
dependencies = [
"bytes",
"crossbeam",
"derivative",
"futures",
"futures-lite",
"lazy_static",
"log",
"pin-project",
"socket2 0.4.10",
"thiserror 1.0.69",
"tokio",
"ucx1-sys",
]
[[package]]
name = "async_zmq"
version = "0.4.0"
......@@ -584,7 +603,7 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef"
dependencies = [
"fastrand",
"fastrand 2.3.0",
"gloo-timers",
"tokio",
]
......@@ -642,6 +661,29 @@ dependencies = [
"virtue",
]
[[package]]
name = "bindgen"
version = "0.66.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b84e06fc203107bfbad243f4aba2af864eb7db3b1cf46ea0a023b0b433d2a7"
dependencies = [
"bitflags 2.11.0",
"cexpr",
"clang-sys",
"lazy_static",
"lazycell",
"log",
"peeking_take_while",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash 1.1.0",
"shlex",
"syn 2.0.117",
"which",
]
[[package]]
name = "bindgen"
version = "0.70.1"
......@@ -1621,6 +1663,17 @@ dependencies = [
"serde_core",
]
[[package]]
name = "derivative"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "derive-getters"
version = "0.5.0"
......@@ -2385,6 +2438,15 @@ dependencies = [
"regex-syntax",
]
[[package]]
name = "fastrand"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
dependencies = [
"instant",
]
[[package]]
name = "fastrand"
version = "2.3.0"
......@@ -2511,7 +2573,7 @@ version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be"
dependencies = [
"fastrand",
"fastrand 2.3.0",
"futures-core",
"futures-sink",
"spin",
......@@ -2643,6 +2705,21 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
[[package]]
name = "futures-lite"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
dependencies = [
"fastrand 1.9.0",
"futures-core",
"futures-io",
"memchr",
"parking",
"pin-project-lite",
"waker-fn",
]
[[package]]
name = "futures-macro"
version = "0.3.32"
......@@ -3580,9 +3657,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "jiff"
version = "0.2.22"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819b44bc7c87d9117eb522f14d46e918add69ff12713c475946b0a29363ed1c2"
checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359"
dependencies = [
"jiff-static",
"jiff-tzdb-platform",
......@@ -3595,9 +3672,9 @@ dependencies = [
[[package]]
name = "jiff-static"
version = "0.2.22"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "470252db18ecc35fd766c0891b1e3ec6cbbcd62507e85276c01bf75d8e94d4a1"
checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4"
dependencies = [
"proc-macro2",
"quote",
......@@ -3606,9 +3683,9 @@ dependencies = [
[[package]]
name = "jiff-tzdb"
version = "0.1.5"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68971ebff725b9e2ca27a601c5eb38a4c5d64422c4cbab0c535f248087eda5c2"
checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076"
[[package]]
name = "jiff-tzdb-platform"
......@@ -3969,6 +4046,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "leb128fmt"
version = "0.1.0"
......@@ -4311,9 +4394,9 @@ dependencies = [
[[package]]
name = "minijinja"
version = "2.16.0"
version = "2.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c54f3bcc034dd74496b5ca929fd0b710186672d5ff0b0f255a9ceb259042ece"
checksum = "65ab6f50e4e8fb40bd21f527066bd019f5b029035b4e5ac9b9f9ba526c6bd87b"
dependencies = [
"memo-map",
"self_cell",
......@@ -4322,9 +4405,9 @@ dependencies = [
[[package]]
name = "minijinja-contrib"
version = "2.16.0"
version = "2.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86201a560fde90bf270d3c9d5da0c2a5a0c25bd3242d674fcf03e6a35fcfa7ba"
checksum = "2db46838f53a8c6e1d19135432e6b9bdbf4f0382c4fbf163f7f9dc24e31800e1"
dependencies = [
"minijinja",
"serde",
......@@ -5343,6 +5426,12 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "peeking_take_while"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
[[package]]
name = "pem"
version = "3.0.6"
......@@ -5972,9 +6061,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.44"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
......@@ -6941,6 +7030,16 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_bytes"
version = "0.11.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"
dependencies = [
"serde",
"serde_core",
]
[[package]]
name = "serde_cbor"
version = "0.11.2"
......@@ -7241,6 +7340,16 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d"
dependencies = [
"libc",
"winapi 0.3.9",
]
[[package]]
name = "socket2"
version = "0.5.10"
......@@ -7483,7 +7592,7 @@ version = "3.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0"
dependencies = [
"fastrand",
"fastrand 2.3.0",
"getrandom 0.4.2",
"once_cell",
"rustix 1.1.4",
......@@ -8281,6 +8390,15 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "ucx1-sys"
version = "0.2.0"
source = "git+https://github.com/ryanolson/async-ucx.git?branch=ryan%2Fucx#fbcb8e3d281d7c910e3dcd84f7eb3e8ee253ac6e"
dependencies = [
"bindgen 0.66.1",
"pkg-config",
]
[[package]]
name = "unarray"
version = "0.1.4"
......@@ -8616,6 +8734,20 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "velo-common"
version = "1.0.0"
dependencies = [
"bytes",
"rmp-serde",
"serde",
"serde_bytes",
"serde_json",
"thiserror 2.0.18",
"uuid",
"xxhash-rust",
]
[[package]]
name = "velo-events"
version = "1.0.0"
......@@ -8632,6 +8764,43 @@ dependencies = [
"xxhash-rust",
]
[[package]]
name = "velo-transports"
version = "1.0.0"
dependencies = [
"anyhow",
"async-nats",
"async-ucx",
"axum 0.8.4",
"bs58",
"bytes",
"dashmap 6.1.0",
"flume",
"futures",
"http 1.4.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.8.1",
"hyper-util",
"nix 0.30.1",
"parking_lot",
"prost 0.13.5",
"reqwest 0.12.28",
"rmp-serde",
"serde_json",
"socket2 0.6.2",
"thiserror 2.0.18",
"tokio",
"tokio-stream",
"tokio-util",
"tonic 0.13.1",
"tonic-build 0.13.1",
"tower 0.5.3",
"tracing",
"tracing-subscriber",
"velo-common",
]
[[package]]
name = "version-compare"
version = "0.2.1"
......@@ -8670,6 +8839,12 @@ dependencies = [
"libc",
]
[[package]]
name = "waker-fn"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7"
[[package]]
name = "walkdir"
version = "2.5.0"
......@@ -8863,6 +9038,18 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]]
name = "which"
version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
dependencies = [
"either",
"home",
"once_cell",
"rustix 0.38.44",
]
[[package]]
name = "winapi"
version = "0.2.8"
......
......@@ -19,7 +19,8 @@ members = [
"lib/bench",
"lib/bindings/c",
"lib/bindings/python/codegen",
"lib/config",
"lib/velo-common",
"lib/velo-transports",
"lib/velo-events",
]
resolver = "3"
......@@ -53,6 +54,8 @@ kvbm-logical = { path = "lib/kvbm-logical", version = "1.0.0" }
kvbm-physical = { path = "lib/kvbm-physical", version = "1.0.0" }
# velo
velo-common = { path = "lib/velo-common", version = "1.0.0" }
velo-transports = { path = "lib/velo-transports", version = "1.0.0" }
velo-events = { path = "lib/velo-events", version = "1.0.0" }
# External dependencies
......@@ -99,6 +102,7 @@ reqwest = { version = "0.12.24", default-features = false, features = [
"rustls-tls",
] }
rmp-serde = { version = "1" }
serde_bytes = { version = "0.11" }
# "rc" is for async-openai. Allows serializing Rc and Arc. Generally avoid doing that.
serde = { version = "1", features = ["derive", "rc"] }
serde_json = { version = "1" }
......
......@@ -2,9 +2,6 @@
"folders": [
{
"path": "."
},
{
"path": "../dynamo-tpm"
}
],
"settings": {
......
......@@ -2965,9 +2965,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "jiff"
version = "0.2.22"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819b44bc7c87d9117eb522f14d46e918add69ff12713c475946b0a29363ed1c2"
checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359"
dependencies = [
"jiff-static",
"jiff-tzdb-platform",
......@@ -2980,9 +2980,9 @@ dependencies = [
[[package]]
name = "jiff-static"
version = "0.2.22"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "470252db18ecc35fd766c0891b1e3ec6cbbcd62507e85276c01bf75d8e94d4a1"
checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4"
dependencies = [
"proc-macro2",
"quote",
......@@ -2991,9 +2991,9 @@ dependencies = [
[[package]]
name = "jiff-tzdb"
version = "0.1.5"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68971ebff725b9e2ca27a601c5eb38a4c5d64422c4cbab0c535f248087eda5c2"
checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076"
[[package]]
name = "jiff-tzdb-platform"
......@@ -3584,9 +3584,9 @@ dependencies = [
[[package]]
name = "minijinja"
version = "2.16.0"
version = "2.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c54f3bcc034dd74496b5ca929fd0b710186672d5ff0b0f255a9ceb259042ece"
checksum = "65ab6f50e4e8fb40bd21f527066bd019f5b029035b4e5ac9b9f9ba526c6bd87b"
dependencies = [
"memo-map",
"self_cell",
......@@ -3595,9 +3595,9 @@ dependencies = [
[[package]]
name = "minijinja-contrib"
version = "2.16.0"
version = "2.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86201a560fde90bf270d3c9d5da0c2a5a0c25bd3242d674fcf03e6a35fcfa7ba"
checksum = "2db46838f53a8c6e1d19135432e6b9bdbf4f0382c4fbf163f7f9dc24e31800e1"
dependencies = [
"minijinja",
"serde",
......@@ -5186,9 +5186,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.44"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
......
......@@ -3026,9 +3026,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "jiff"
version = "0.2.22"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819b44bc7c87d9117eb522f14d46e918add69ff12713c475946b0a29363ed1c2"
checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359"
dependencies = [
"jiff-static",
"jiff-tzdb-platform",
......@@ -3041,9 +3041,9 @@ dependencies = [
[[package]]
name = "jiff-static"
version = "0.2.22"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "470252db18ecc35fd766c0891b1e3ec6cbbcd62507e85276c01bf75d8e94d4a1"
checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4"
dependencies = [
"proc-macro2",
"quote",
......@@ -3052,9 +3052,9 @@ dependencies = [
[[package]]
name = "jiff-tzdb"
version = "0.1.5"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68971ebff725b9e2ca27a601c5eb38a4c5d64422c4cbab0c535f248087eda5c2"
checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076"
[[package]]
name = "jiff-tzdb-platform"
......@@ -3633,9 +3633,9 @@ dependencies = [
[[package]]
name = "minijinja"
version = "2.16.0"
version = "2.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c54f3bcc034dd74496b5ca929fd0b710186672d5ff0b0f255a9ceb259042ece"
checksum = "65ab6f50e4e8fb40bd21f527066bd019f5b029035b4e5ac9b9f9ba526c6bd87b"
dependencies = [
"memo-map",
"self_cell",
......@@ -3644,9 +3644,9 @@ dependencies = [
[[package]]
name = "minijinja-contrib"
version = "2.16.0"
version = "2.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86201a560fde90bf270d3c9d5da0c2a5a0c25bd3242d674fcf03e6a35fcfa7ba"
checksum = "2db46838f53a8c6e1d19135432e6b9bdbf4f0382c4fbf163f7f9dc24e31800e1"
dependencies = [
"minijinja",
"serde",
......@@ -5245,9 +5245,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.44"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
......
......@@ -2713,9 +2713,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.44"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
......
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
[package]
name = "velo-common"
version.workspace = true
edition.workspace = true
description = "Common types for Velo distributed systems (identity, address, transport)"
authors.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
keywords.workspace = true
[dependencies]
bytes = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
uuid = { workspace = true, features = ["serde", "v4"] }
xxhash-rust = { workspace = true }
rmp-serde = { workspace = true }
serde_bytes = { workspace = true }
# velo-common
Common types for the Velo distributed systems stack.
## Overview
This crate provides the foundational types used across Velo for identity and addressing. The design prioritizes:
- **Compact representations** for embedding in fixed-size handles
- **Transport-agnostic addressing** without enumerating all possible transports
- **KV-store friendly** serialization using opaque `Bytes`
## Identity Types
### InstanceId
`InstanceId` is a UUID-based identifier that serves as the **source of truth** for identifying a running Velo instance. It is used for:
- Transport-level routing
- Discovery registration
- Peer management
```rust
use velo_common::InstanceId;
let instance_id = InstanceId::new_v4();
let uuid = instance_id.as_uuid();
```
### WorkerId
`WorkerId` is a deterministic 64-bit identifier derived from `InstanceId` via xxh3 hash. The compact representation enables embedding worker identity into fixed-size handles.
**Design rationale**: A `u128` handle can encode:
- 64 bits for `WorkerId`
- 64 bits for additional data (sequence numbers, flags, etc.)
This value-semantics approach simplifies passing identity through systems that work with fixed-size integers.
```rust
use velo_common::InstanceId;
let instance_id = InstanceId::new_v4();
let worker_id = instance_id.worker_id(); // Deterministic derivation
// Embed in a u128 handle
let other_data: u128 = 42;
let handle: u128 = (worker_id.as_u64() as u128) << 64 | other_data;
```
The derivation is always consistent—calling `worker_id()` multiple times returns the same value.
## Address Types
### WorkerAddress
`WorkerAddress` is an opaque byte container holding transport endpoint information. Internally, it's a MessagePack-encoded map of `TransportKey -> Bytes`, but this structure is intentionally hidden from consumers.
**Key design decisions**:
1. **Opaque values**: Transport endpoints are stored as raw bytes. They could be simple strings (`"tcp://127.0.0.1:5555"`) or complex serialized objects. The interpretation is left to the transport implementation.
2. **No transport enum**: Rather than defining an enum of all possible transports with their configurations, we use string keys (`"tcp"`, `"rdma"`, `"grpc"`, etc.). This allows transports to be added without modifying the common types.
3. **KV-store friendly**: The entire address serializes to a `Bytes` blob, suitable for storage in etcd, Redis, or any key-value store without schema changes.
```rust
use velo_common::WorkerAddress;
use std::collections::HashMap;
// Addresses are typically constructed by velo-transports transport builders,
// but here we demonstrate the consumer API:
let mut map = HashMap::new();
map.insert("tcp".to_string(), b"tcp://127.0.0.1:5555".to_vec());
map.insert("rdma".to_string(), b"rdma://10.0.0.1:6666".to_vec());
let encoded = rmp_serde::to_vec(&map).unwrap();
let address = WorkerAddress::from_encoded(encoded);
// Reading an address (consumer perspective)
let transports = address.available_transports().unwrap(); // ["tcp", "rdma"]
let tcp_endpoint = address.get_entry("tcp").unwrap(); // Some(Bytes)
```
### TransportKey
A type-safe wrapper around transport identifiers. Provides zero-cost abstraction over `Arc<str>` with efficient cloning and HashMap compatibility.
```rust
use velo_common::TransportKey;
use std::collections::HashMap;
let key = TransportKey::from("tcp");
let key2: TransportKey = "rdma".into();
// Works with HashMap lookups via Borrow<str>
let mut map = HashMap::new();
map.insert(TransportKey::from("tcp"), "127.0.0.1:5555");
assert!(map.get("tcp").is_some()); // &str lookup works
```
### PeerInfo
Combines `InstanceId` and `WorkerAddress` into a single structure representing a discoverable peer. This is the primary type exchanged during peer discovery and registration.
```rust
use velo_common::{InstanceId, PeerInfo, WorkerAddress};
use std::collections::HashMap;
let instance_id = InstanceId::new_v4();
let map: HashMap<String, Vec<u8>> = HashMap::new();
let encoded = rmp_serde::to_vec(&map).unwrap();
let worker_address = WorkerAddress::from_encoded(encoded);
let peer_info = PeerInfo::new(instance_id, worker_address);
assert_eq!(peer_info.instance_id(), instance_id);
```
## Address Construction
`WorkerAddress` instances are constructed by transport builders in `velo-transports`. Each transport (TCP, gRPC, NATS, UCX, etc.) contributes its endpoint data:
```text
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ TCP Transport │ │ gRPC Transport │ │ UCX Transport │
│ Builder │ │ Builder │ │ Builder │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
│ "tcp" -> endpoint │ "grpc" -> endpoint │ "ucx" -> blob
│ │ │
└───────────────────────┼───────────────────────┘
┌────────────────────────┐
│ WorkerAddress │
│ (MessagePack map) │
│ │
│ tcp -> bytes │
│ grpc -> bytes │
│ ucx -> bytes │
└────────────────────────┘
```
When a Velo client receives a `PeerInfo`, it can:
1. Check `available_transports()` to see what's supported
2. Extract the relevant endpoint via `get_entry(key)`
3. Register the peer with its own transports
This design decouples the common types from specific transport implementations.
## Wire Format
- **InstanceId**: Serializes as a UUID string (JSON) or 16 bytes (binary)
- **WorkerId**: Serializes as a u64
- **WorkerAddress**: Serializes as a byte array (the MessagePack-encoded map)
- **PeerInfo**: Serializes as a struct with `instance_id` and `worker_address` fields
All types implement `serde::Serialize` and `serde::Deserialize`.
// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//! Address types for peer discovery.
//!
//! This module provides types for representing worker addresses and peer information:
//! - [`WorkerAddress`]: Opaque byte representation of a peer's network address
//! - [`PeerInfo`]: Combined instance ID and worker address for a discovered peer
//!
//! These types are intentionally transport-agnostic, storing addresses as opaque bytes.
//! The interpretation of these bytes is left to the active message runtime.
use crate::identity::{InstanceId, WorkerId};
use crate::transport::TransportKey;
use bytes::Bytes;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
use xxhash_rust::xxh3::xxh3_64;
/// Errors that can occur when working with WorkerAddress.
#[derive(Debug, thiserror::Error)]
pub enum WorkerAddressError {
/// Attempted to add a key that already exists
#[error("Key already exists: {0}")]
KeyExists(String),
/// Attempted to access or remove a key that doesn't exist
#[error("Key not found: {0}")]
KeyNotFound(String),
/// Failed to encode the map to bytes
#[error("Encoding error: {0}")]
EncodingError(#[from] rmp_serde::encode::Error),
/// Failed to decode bytes to map
#[error("Decoding error: {0}")]
DecodingError(#[from] rmp_serde::decode::Error),
/// Encountered an unsupported format version
#[error("Unsupported format version: {0}")]
UnsupportedVersion(u8),
/// The data format is invalid
#[error("Invalid format: {0}")]
InvalidFormat(String),
}
/// Opaque worker address for discovery.
///
/// This is a transport-agnostic representation of a peer's network address.
/// The bytes are opaque to discovery and are interpreted by the active message runtime.
///
/// # Checksum
///
/// WorkerAddress implements a checksum via xxh3_64 for quick comparison during
/// re-registration validation.
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct WorkerAddress(Bytes);
// Custom Serialize/Deserialize to handle Bytes
impl Serialize for WorkerAddress {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serde_bytes::serialize(self.0.as_ref(), serializer)
}
}
impl<'de> Deserialize<'de> for WorkerAddress {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let bytes: Vec<u8> = serde_bytes::deserialize(deserializer)?;
Ok(WorkerAddress(Bytes::from(bytes)))
}
}
impl WorkerAddress {
/// Create a WorkerAddress from pre-encoded bytes.
///
/// This is used by transport implementations to construct addresses from
/// MessagePack-encoded map data. The bytes are assumed to be valid MessagePack.
pub fn from_encoded(bytes: impl Into<Bytes>) -> Self {
Self(bytes.into())
}
/// Get the underlying bytes.
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
/// Get the bytes as a Bytes object.
pub fn to_bytes(&self) -> Bytes {
self.0.clone()
}
/// Compute a checksum of this address for validation.
///
/// This is used to quickly check if an address has changed during re-registration.
pub fn checksum(&self) -> u64 {
xxh3_64(self.as_bytes())
}
/// Get the list of available transport keys in this address.
///
/// Returns the keys from the internal map as `TransportKey` for type-safe efficient
/// storage and sharing. This allows callers to see what transport types or endpoints
/// are available without exposing the full map.
///
/// # Errors
///
/// Returns an error if the internal bytes cannot be decoded as a valid MessagePack map.
///
/// # Example
///
/// ```no_run
/// # use velo_common::{WorkerAddress, TransportKey};
/// # let address: WorkerAddress = unimplemented!();
/// let transports = address.available_transports().unwrap();
/// if transports.contains(&TransportKey::from("tcp")) {
/// // TCP transport is available
/// }
/// ```
pub fn available_transports(&self) -> Result<Vec<TransportKey>, WorkerAddressError> {
let map = decode_to_map(self.as_bytes())?;
Ok(map.keys().cloned().map(TransportKey::from).collect())
}
/// Get a single entry from the internal map.
///
/// This decodes the address and extracts the entry for the given key.
///
/// Accepts any type that can be converted to a string reference, including
/// `&str`, `String`, `&String`, and `TransportKey`.
///
/// # Errors
///
/// Returns an error if the internal bytes cannot be decoded as a valid MessagePack map.
pub fn get_entry(&self, key: impl AsRef<str>) -> Result<Option<Bytes>, WorkerAddressError> {
let map = decode_to_map(self.as_bytes())?;
Ok(map.get(key.as_ref()).cloned())
}
}
impl fmt::Debug for WorkerAddress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("WorkerAddress")
.field(&format_args!(
"len={}, xxh3_64=0x{:016x}",
self.0.len(),
self.checksum()
))
.finish()
}
}
impl fmt::Display for WorkerAddress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "WorkerAddress(xxh3_64=0x{:016x})", self.checksum())
}
}
// ============================================================================
// Internal Decoding Helper
// ============================================================================
/// Decode WorkerAddress bytes from MessagePack into a map.
fn decode_to_map(bytes: &[u8]) -> Result<HashMap<Arc<str>, Bytes>, WorkerAddressError> {
if bytes.is_empty() {
return Err(WorkerAddressError::InvalidFormat("Empty bytes".to_string()));
}
// Decode MessagePack
let decoded: HashMap<String, Vec<u8>> = rmp_serde::from_slice(bytes)?;
// Convert to HashMap<Arc<str>, Bytes>
Ok(decoded
.into_iter()
.map(|(k, v)| (Arc::from(k.as_str()), Bytes::from(v)))
.collect())
}
/// Peer information combining instance ID and worker address.
///
/// This is the primary type returned by discovery lookups. It contains everything
/// needed to connect to and identify a peer.
///
/// # Example
///
/// ```no_run
/// # // WorkerAddress is created internally, this is simplified for docs
/// use velo_common::{InstanceId, PeerInfo};
/// # use velo_common::WorkerAddress;
/// # let address: WorkerAddress = unimplemented!();
///
/// let instance_id = InstanceId::new_v4();
/// let peer_info = PeerInfo::new(instance_id, address);
///
/// assert_eq!(peer_info.instance_id(), instance_id);
/// assert_eq!(peer_info.worker_id(), instance_id.worker_id());
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PeerInfo {
/// The instance ID of the peer
pub instance_id: InstanceId,
/// The worker address for connecting to the peer
pub worker_address: WorkerAddress,
}
impl PeerInfo {
/// Create a new PeerInfo.
pub fn new(instance_id: InstanceId, worker_address: WorkerAddress) -> Self {
Self {
instance_id,
worker_address,
}
}
/// Get the instance ID.
pub fn instance_id(&self) -> InstanceId {
self.instance_id
}
/// Get the worker ID (derived from instance ID).
pub fn worker_id(&self) -> WorkerId {
self.instance_id.worker_id()
}
/// Get a reference to the worker address.
pub fn worker_address(&self) -> &WorkerAddress {
&self.worker_address
}
/// Get the worker address checksum for validation.
pub fn address_checksum(&self) -> u64 {
self.worker_address.checksum()
}
/// Consume self and return the worker address.
pub fn into_address(self) -> WorkerAddress {
self.worker_address
}
/// Decompose into instance ID and worker address.
pub fn into_parts(self) -> (InstanceId, WorkerAddress) {
(self.instance_id, self.worker_address)
}
}
#[cfg(test)]
mod tests {
use super::*;
// Helper to create a test address with MessagePack encoding
fn make_test_address(entries: &[(&str, &[u8])]) -> WorkerAddress {
let map: HashMap<String, Vec<u8>> = entries
.iter()
.map(|(k, v)| (k.to_string(), v.to_vec()))
.collect();
let encoded = rmp_serde::to_vec(&map).unwrap();
WorkerAddress::from_encoded(encoded)
}
#[test]
fn test_worker_address_from_encoded() {
let address = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
// Verify we can get the entry back
let entry = address.get_entry("endpoint").unwrap();
assert_eq!(entry, Some(Bytes::from_static(b"tcp://127.0.0.1:5555")));
}
#[test]
fn test_worker_address_checksum() {
let address1 = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
let address2 = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
let address3 = make_test_address(&[("endpoint", b"tcp://127.0.0.1:6666")]);
// Same content = same checksum
assert_eq!(address1.checksum(), address2.checksum());
// Different content = different checksum
assert_ne!(address1.checksum(), address3.checksum());
}
#[test]
fn test_worker_address_equality() {
let address1 = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
let address2 = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
let address3 = make_test_address(&[("endpoint", b"tcp://127.0.0.1:6666")]);
assert_eq!(address1, address2);
assert_ne!(address1, address3);
}
#[test]
fn test_worker_address_debug() {
let address = make_test_address(&[("test", b"value")]);
let debug_str = format!("{:?}", address);
assert!(debug_str.contains("WorkerAddress"));
assert!(debug_str.contains("len="));
assert!(debug_str.contains("xxh3_64="));
}
#[test]
fn test_available_transports() {
let address = make_test_address(&[
("tcp", b"tcp://127.0.0.1:5555"),
("rdma", b"rdma://10.0.0.1:6666"),
("udp", b"udp://127.0.0.1:7777"),
]);
let transports = address.available_transports().unwrap();
assert_eq!(transports.len(), 3);
assert!(transports.contains(&TransportKey::from("tcp")));
assert!(transports.contains(&TransportKey::from("rdma")));
assert!(transports.contains(&TransportKey::from("udp")));
}
#[test]
fn test_available_transports_empty() {
let address = make_test_address(&[]);
let transports = address.available_transports().unwrap();
assert_eq!(transports.len(), 0);
}
#[test]
fn test_get_entry() {
let address =
make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555"), ("protocol", b"tcp")]);
// Get existing entry
assert_eq!(
address.get_entry("endpoint").unwrap().unwrap(),
Bytes::from_static(b"tcp://127.0.0.1:5555")
);
// Get nonexistent entry
assert!(address.get_entry("nonexistent").unwrap().is_none());
}
#[test]
fn test_get_entry_with_transport_key() {
let address = make_test_address(&[
("tcp", b"tcp://127.0.0.1:5555"),
("rdma", b"rdma://10.0.0.1:6666"),
]);
// Test get_entry with TransportKey
let tcp_key = TransportKey::from("tcp");
let result = address.get_entry(tcp_key).unwrap();
assert_eq!(result, Some(Bytes::from_static(b"tcp://127.0.0.1:5555")));
// Test get_entry with String
let result = address.get_entry(String::from("rdma")).unwrap();
assert_eq!(result, Some(Bytes::from_static(b"rdma://10.0.0.1:6666")));
}
#[test]
fn test_peer_info_creation() {
let instance_id = InstanceId::new_v4();
let address = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
let peer_info = PeerInfo::new(instance_id, address.clone());
assert_eq!(peer_info.instance_id(), instance_id);
assert_eq!(peer_info.worker_id(), instance_id.worker_id());
assert_eq!(peer_info.worker_address(), &address);
}
#[test]
fn test_peer_info_checksum() {
let instance_id = InstanceId::new_v4();
let address = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
let peer_info = PeerInfo::new(instance_id, address.clone());
assert_eq!(peer_info.address_checksum(), address.checksum());
}
#[test]
fn test_peer_info_into_address() {
let instance_id = InstanceId::new_v4();
let address = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
let peer_info = PeerInfo::new(instance_id, address.clone());
let extracted_address = peer_info.into_address();
assert_eq!(extracted_address, address);
}
#[test]
fn test_peer_info_into_parts() {
let instance_id = InstanceId::new_v4();
let address = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
let peer_info = PeerInfo::new(instance_id, address.clone());
let (extracted_id, extracted_address) = peer_info.into_parts();
assert_eq!(extracted_id, instance_id);
assert_eq!(extracted_address, address);
}
#[test]
fn test_peer_info_serde() {
let instance_id = InstanceId::new_v4();
let address = make_test_address(&[("endpoint", b"tcp://127.0.0.1:5555")]);
let peer_info = PeerInfo::new(instance_id, address);
// Serialize to JSON
let json = serde_json::to_string(&peer_info).unwrap();
// Deserialize back
let deserialized: PeerInfo = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.instance_id(), instance_id);
assert_eq!(deserialized.worker_id(), instance_id.worker_id());
// Verify the entry is preserved
let entry = deserialized.worker_address().get_entry("endpoint").unwrap();
assert_eq!(entry, Some(Bytes::from_static(b"tcp://127.0.0.1:5555")));
}
}
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//! Identity types for the active message system.
//!
//! This module provides strongly-typed wrappers for instance and worker identifiers:
//! - [`InstanceId`]: Unique runtime instance identifier (wraps UUID)
//! - [`WorkerId`]: Deterministic 64-bit worker identifier derived from InstanceId
//!
//! # Design Principles
//!
//! 1. **Type Safety**: InstanceId cannot be confused with message IDs or other UUIDs
//! 2. **Deterministic Derivation**: WorkerId is always computed from InstanceId (xxh3_64 hash)
//! 3. **Single Source of Truth**: InstanceId is the primary identifier, WorkerId is derived
use serde::{Deserialize, Serialize};
use std::fmt;
use uuid::Uuid;
use xxhash_rust::xxh3::xxh3_64;
/// Unique identifier for a runtime instance.
///
/// This is a UUID-based identifier that uniquely identifies a running instance
/// of the active message runtime. It is used for:
/// - Transport-level addressing
/// - Discovery registration
/// - Routing table management
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct InstanceId(Uuid);
impl InstanceId {
/// Create a new random v4 InstanceId.
///
/// This is exposed for testing and special cases. In production, use
/// [`InstanceFactory::create()`] instead.
pub fn new_v4() -> Self {
loop {
let instance_id = InstanceId(Uuid::new_v4());
let worker_id = WorkerId::from(&instance_id);
if worker_id.as_u64() != 0 {
return instance_id;
}
}
}
/// Derive the deterministic WorkerId from this InstanceId.
///
/// WorkerId is computed using xxh3_64 hash of the UUID bytes.
/// This ensures a 1:1 mapping between InstanceId and WorkerId.
pub fn worker_id(&self) -> WorkerId {
WorkerId::from(self)
}
/// Get a reference to the underlying UUID.
pub fn as_uuid(&self) -> &Uuid {
&self.0
}
/// Get the underlying UUID as a u128.
pub fn as_u128(&self) -> u128 {
self.0.as_u128()
}
/// Get the underlying UUID as bytes.
pub fn as_bytes(&self) -> &[u8; 16] {
self.0.as_bytes()
}
}
impl fmt::Display for InstanceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<Uuid> for InstanceId {
fn from(uuid: Uuid) -> Self {
Self(uuid)
}
}
impl From<InstanceId> for Uuid {
fn from(id: InstanceId) -> Self {
id.0
}
}
impl AsRef<Uuid> for InstanceId {
fn as_ref(&self) -> &Uuid {
&self.0
}
}
/// Deterministic 64-bit worker identifier derived from InstanceId.
///
/// WorkerId enables embedding instance identity into fixed-size handles that can be
/// passed with value semantics. A `u128` is the largest integer that can be passed
/// by value, making it ideal for handles that encode both routing and event information.
///
/// WorkerId is used in:
/// - `EventHandle` (velo): Uses 64 bits for WorkerId + 64 bits for event details
/// - `EventRoutingTable` (velo): Maps worker_id → instance_id for event routing
/// - Discovery systems: Lookup key for peer information
///
/// WorkerId is **always derived** from InstanceId using xxh3_64 hash.
/// This ensures consistency across the system without needing to store both values.
///
/// # Example
///
/// ```ignore
/// use velo_common::{InstanceId, WorkerId};
///
/// # fn get_instance_id() -> InstanceId { unimplemented!() }
/// let instance_id = get_instance_id(); // From ActiveMessageClient
/// let worker_id = instance_id.worker_id();
///
/// // WorkerId is deterministic
/// assert_eq!(worker_id, instance_id.worker_id());
/// ```
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
#[serde(transparent)]
pub struct WorkerId(u64);
impl WorkerId {
/// Create a WorkerId from a raw u64 value.
///
/// This is used when decoding WorkerIds from event handles or wire formats.
/// External users should always derive WorkerId via `instance_id.worker_id()`.
pub fn from_u64(value: u64) -> Self {
Self(value)
}
/// Get the underlying u64 value.
#[inline(always)]
pub fn as_u64(&self) -> u64 {
self.0
}
}
impl fmt::Display for WorkerId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<&InstanceId> for WorkerId {
/// Derive WorkerId from InstanceId using xxh3_64 hash.
///
/// This is the canonical way to compute WorkerId - it should never be
/// constructed any other way to ensure consistency.
fn from(id: &InstanceId) -> Self {
Self(xxh3_64(id.as_uuid().as_bytes()))
}
}
impl From<InstanceId> for WorkerId {
fn from(id: InstanceId) -> Self {
Self::from(&id)
}
}
impl From<WorkerId> for u64 {
fn from(id: WorkerId) -> Self {
id.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_instance_id_creation() {
let id1 = InstanceId::new_v4();
let id2 = InstanceId::new_v4();
// Different instances have different IDs
assert_ne!(id1, id2);
// Can convert to/from UUID
let uuid: Uuid = id1.into();
let id3 = InstanceId::from(uuid);
assert_eq!(id1, id3);
}
#[test]
fn test_worker_id_deterministic() {
let instance_id = InstanceId::new_v4();
// WorkerId is deterministic
let worker_id1 = instance_id.worker_id();
let worker_id2 = instance_id.worker_id();
assert_eq!(worker_id1, worker_id2);
// Different instances have different worker IDs
let other_instance = InstanceId::new_v4();
let other_worker = other_instance.worker_id();
assert_ne!(worker_id1, other_worker);
}
#[test]
fn test_worker_id_from_conversion() {
let instance_id = InstanceId::new_v4();
// Both From implementations work
let worker_id1 = WorkerId::from(&instance_id);
let worker_id2 = WorkerId::from(instance_id);
assert_eq!(worker_id1, worker_id2);
// Matches .worker_id() method
assert_eq!(worker_id1, instance_id.worker_id());
}
#[test]
fn test_instance_id_display() {
let instance_id = InstanceId::new_v4();
let display = format!("{}", instance_id);
let uuid_display = format!("{}", instance_id.as_uuid());
assert_eq!(display, uuid_display);
}
#[test]
fn test_worker_id_display() {
let instance_id = InstanceId::new_v4();
let worker_id = instance_id.worker_id();
let display = format!("{}", worker_id);
let u64_display = format!("{}", worker_id.as_u64());
assert_eq!(display, u64_display);
}
#[test]
fn test_instance_id_serde() {
let instance_id = InstanceId::new_v4();
// Serialize as JSON
let json = serde_json::to_string(&instance_id).unwrap();
// Should be a plain UUID string
let uuid_json = serde_json::to_string(instance_id.as_uuid()).unwrap();
assert_eq!(json, uuid_json);
// Deserialize back
let deserialized: InstanceId = serde_json::from_str(&json).unwrap();
assert_eq!(instance_id, deserialized);
}
#[test]
fn test_worker_id_serde() {
let worker_id = InstanceId::new_v4().worker_id();
// Serialize as JSON
let json = serde_json::to_string(&worker_id).unwrap();
// Should be a plain u64
let u64_json = serde_json::to_string(&worker_id.as_u64()).unwrap();
assert_eq!(json, u64_json);
// Deserialize back
let deserialized: WorkerId = serde_json::from_str(&json).unwrap();
assert_eq!(worker_id, deserialized);
}
#[test]
fn test_worker_id_u64_conversion() {
let instance_id = InstanceId::new_v4();
let worker_id = instance_id.worker_id();
let raw_u64 = worker_id.as_u64();
let reconstructed = WorkerId::from_u64(raw_u64);
assert_eq!(worker_id, reconstructed);
}
}
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
#![doc = include_str!("../README.md")]
mod address;
mod identity;
mod transport;
// Re-export all public types
pub use address::{PeerInfo, WorkerAddress, WorkerAddressError};
pub use identity::{InstanceId, WorkerId};
pub use transport::TransportKey;
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//! Transport key type for type-safe transport identification.
use std::fmt;
use std::sync::Arc;
/// A type-safe wrapper around transport keys for WorkerAddress.
///
/// This provides a zero-cost abstraction over `Arc<str>` with type safety
/// to prevent accidentally mixing transport keys with other string types.
///
/// # Examples
///
/// ```
/// use velo_common::TransportKey;
///
/// let key = TransportKey::new("tcp");
/// assert_eq!(key.as_str(), "tcp");
///
/// // Ergonomic conversions
/// let key2: TransportKey = "rdma".into();
/// let key3 = TransportKey::from("udp");
///
/// // Use in collections
/// use std::collections::HashMap;
/// let mut transports = HashMap::new();
/// transports.insert(TransportKey::from("tcp"), "tcp://127.0.0.1:5555");
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TransportKey(Arc<str>);
impl TransportKey {
/// Create a new TransportKey from any type that can be converted into Arc<str>.
pub fn new(key: impl Into<Arc<str>>) -> Self {
Self(key.into())
}
/// Get the key as a string slice.
pub fn as_str(&self) -> &str {
&self.0
}
}
// Deref to str for ergonomic usage
impl std::ops::Deref for TransportKey {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
// AsRef for flexible parameter types
impl AsRef<str> for TransportKey {
fn as_ref(&self) -> &str {
&self.0
}
}
// From conversions for ergonomic construction
impl From<&str> for TransportKey {
fn from(s: &str) -> Self {
Self(Arc::from(s))
}
}
impl From<String> for TransportKey {
fn from(s: String) -> Self {
Self(Arc::from(s))
}
}
impl From<Arc<str>> for TransportKey {
fn from(s: Arc<str>) -> Self {
Self(s)
}
}
impl From<&String> for TransportKey {
fn from(s: &String) -> Self {
Self(Arc::from(s.as_str()))
}
}
impl From<TransportKey> for String {
fn from(val: TransportKey) -> Self {
val.0.to_string()
}
}
// Borrow trait for HashMap lookups with &str
impl std::borrow::Borrow<str> for TransportKey {
fn borrow(&self) -> &str {
&self.0
}
}
// Display for printing
impl fmt::Display for TransportKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::{HashMap, HashSet};
#[test]
fn test_transport_key_creation() {
// Test new() method
let key1 = TransportKey::new("tcp");
assert_eq!(key1.as_str(), "tcp");
// Test From<&str>
let key2: TransportKey = "rdma".into();
assert_eq!(key2.as_str(), "rdma");
// Test From<String>
let key3 = TransportKey::from(String::from("udp"));
assert_eq!(key3.as_str(), "udp");
// Test From<&String>
let s = String::from("grpc");
let key4 = TransportKey::from(&s);
assert_eq!(key4.as_str(), "grpc");
// Test From<Arc<str>>
let arc_str: Arc<str> = Arc::from("http");
let key5 = TransportKey::from(arc_str);
assert_eq!(key5.as_str(), "http");
}
#[test]
fn test_transport_key_deref() {
let key = TransportKey::from("tcp");
// Deref to str methods should work
assert_eq!(key.len(), 3);
assert_eq!(key.chars().count(), 3);
assert!(key.starts_with("tc"));
assert!(key.ends_with("cp"));
// Can use str slicing through Deref
assert_eq!(&key[0..2], "tc");
}
#[test]
fn test_transport_key_as_ref() {
let key = TransportKey::from("tcp");
// AsRef<str> allows passing to functions expecting &str
fn takes_str_ref(s: &str) -> usize {
s.len()
}
assert_eq!(takes_str_ref(&key), 3);
assert_eq!(takes_str_ref(key.as_ref()), 3);
}
#[test]
fn test_transport_key_display() {
let key = TransportKey::from("tcp");
assert_eq!(format!("{}", key), "tcp");
assert_eq!(key.to_string(), "tcp");
}
#[test]
fn test_transport_key_debug() {
let key = TransportKey::from("tcp");
let debug_str = format!("{:?}", key);
assert!(debug_str.contains("TransportKey"));
assert!(debug_str.contains("tcp"));
}
#[test]
fn test_transport_key_equality() {
let key1 = TransportKey::from("tcp");
let key2 = TransportKey::from("tcp");
let key3 = TransportKey::from("rdma");
assert_eq!(key1, key2);
assert_ne!(key1, key3);
// Test with different source types
let key4: TransportKey = String::from("tcp").into();
assert_eq!(key1, key4);
}
#[test]
fn test_transport_key_ordering() {
let mut keys = [
TransportKey::from("udp"),
TransportKey::from("tcp"),
TransportKey::from("rdma"),
TransportKey::from("grpc"),
];
keys.sort();
assert_eq!(keys[0], TransportKey::from("grpc"));
assert_eq!(keys[1], TransportKey::from("rdma"));
assert_eq!(keys[2], TransportKey::from("tcp"));
assert_eq!(keys[3], TransportKey::from("udp"));
}
#[test]
fn test_transport_key_hash() {
let mut set = HashSet::new();
set.insert(TransportKey::from("tcp"));
set.insert(TransportKey::from("rdma"));
set.insert(TransportKey::from("tcp")); // Duplicate
assert_eq!(set.len(), 2);
assert!(set.contains(&TransportKey::from("tcp")));
assert!(set.contains(&TransportKey::from("rdma")));
assert!(!set.contains(&TransportKey::from("udp")));
}
#[test]
fn test_transport_key_in_hashmap() {
let mut map = HashMap::new();
map.insert(TransportKey::from("tcp"), "tcp://127.0.0.1:5555");
map.insert(TransportKey::from("rdma"), "rdma://10.0.0.1:6666");
// Can lookup with TransportKey
assert_eq!(
map.get(&TransportKey::from("tcp")),
Some(&"tcp://127.0.0.1:5555")
);
// Can lookup with &str via Borrow trait
assert_eq!(map.get("tcp"), Some(&"tcp://127.0.0.1:5555"));
assert_eq!(map.get("rdma"), Some(&"rdma://10.0.0.1:6666"));
assert_eq!(map.get("udp"), None);
}
#[test]
fn test_transport_key_clone() {
let key1 = TransportKey::from("tcp");
let key2 = key1.clone();
assert_eq!(key1, key2);
assert_eq!(key1.as_str(), key2.as_str());
// Verify Arc is shared (same pointer)
let ptr1 = key1.as_str().as_ptr();
let ptr2 = key2.as_str().as_ptr();
assert_eq!(ptr1, ptr2);
}
}
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
[package]
name = "velo-transports"
version.workspace = true
edition.workspace = true
description.workspace = true
authors.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
keywords.workspace = true
[features]
default = ["http", "nats", "grpc"]
ucx = ["dep:async-ucx"]
http = ["dep:axum", "dep:reqwest"]
nats = ["dep:async-nats", "dep:bs58"]
grpc = ["dep:tonic", "dep:prost", "dep:tower", "dep:hyper", "dep:http", "dep:http-body", "dep:http-body-util", "dep:hyper-util", "dep:tokio-stream"]
[dependencies]
velo-common = { workspace = true }
anyhow = { workspace = true }
bytes = { workspace = true }
dashmap = { workspace = true }
# derive_builder = { workspace = true }
parking_lot.workspace = true
# serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
# uuid = { workspace = true, features = ["serde", "v4"] }
# xxhash-rust = { workspace = true }
# base64 = "0.22"
flume = "0.12.0"
futures = "0.3"
# hashbrown = "0.16"
# lru = { version = "0.16", features = ["hashbrown"]}
rmp-serde = "1.1"
# serde_bytes = "0.11"
socket2 = "0.6"
tokio-util = { version = "0.7", features = ["codec"] }
[target.'cfg(target_os = "linux")'.dependencies]
nix = { version = "0.30", features = ["sched"] }
# optional dependencies
async-ucx = { git = "https://github.com/ryanolson/async-ucx.git", branch = "ryan/ucx", optional = true, features = ["am", "event"] }
axum = { version = "0.8", optional = true }
reqwest = { workspace = true , optional = true }
async-nats = { workspace = true, optional = true }
bs58 = { version = "0.5", optional = true }
prost = { version = "0.13", optional = true }
tonic = { version = "0.13.1", optional = true }
tower = { version = "0.5", optional = true }
hyper = { version = "1.0", optional = true }
http = { version = "1.0", optional = true }
http-body = { version = "1.0", optional = true }
http-body-util = { version = "0.1", optional = true }
hyper-util = { version = "0.1", optional = true, features = ["tokio", "server", "server-auto"] }
tokio-stream = { version = "0.1", optional = true, features = ["sync"] }
[dev-dependencies]
tokio = { workspace = true, features = ["test-util", "macros"] }
tower = "0.5"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[build-dependencies]
tonic-build = "0.13.1"
# velo-transports
Multi-transport active message routing for distributed systems.
## Overview
`velo-transports` abstracts TCP, HTTP, NATS, gRPC, and UCX behind a unified `Transport` trait. It provides:
- **Zero-copy `Bytes`** — inbound frames share the receive buffer via `bytes::Bytes` slicing
- **Fire-and-forget with error callbacks**`send_message` is non-blocking; failures are reported through `TransportErrorHandler`
- **Priority-based peer routing** — each peer is registered with all compatible transports; the highest-priority one becomes the primary
- **3-phase graceful shutdown** — Gate → Drain → Teardown with RAII in-flight tracking
- **Peer discovery via `WorkerAddress`** — MessagePack-encoded map of transport key → endpoint bytes
## Architecture
```mermaid
graph TD
VB[VeloBackend] -->|priority routing| TCP[TcpTransport]
VB -->|priority routing| HTTP[HttpTransport]
VB -->|priority routing| NATS[NatsTransport]
VB -->|priority routing| GRPC[GrpcTransport]
VB -->|priority routing| UCX[UcxTransport]
TCP -->|inbound| DS[DataStreams]
HTTP -->|inbound| DS
NATS -->|inbound| DS
GRPC -->|inbound| DS
UCX -->|inbound| DS
DS -->|message_stream| APP[Application]
DS -->|response_stream| APP
DS -->|event_stream| APP
```
`VeloBackend` is the central orchestrator. It starts each transport, builds a composite `WorkerAddress` advertising all endpoints, and manages the peer registry. Inbound frames arrive via three independent `flume` channels in `DataStreams`.
## Feature Flags
| Feature | Default | Dependencies | Description |
|---------|---------|-------------|-------------|
| `http` | ✓ | axum, reqwest | HTTP transport with Axum server |
| `nats` | ✓ | async-nats, bs58 | NATS pub-sub transport |
| `grpc` | ✓ | tonic, prost, tower | gRPC bidirectional streaming |
| `ucx` | ✗ | async-ucx | UCX/RDMA transport (requires UCX libraries) |
TCP transport is always available (no feature gate).
## Transport Summary
| Transport | Protocol | Framing | Key Properties |
|-----------|----------|---------|----------------|
| **TCP** | Raw TCP | 11-byte preamble + header + payload | Zero-copy codec, DashMap connection pool, CPU pinning, keepalive |
| **HTTP** | HTTP/1.1 POST | Base64 header in `X-Transport-Header`, raw body | Fire-and-forget (202 Accepted), Axum server, drain → 503 |
| **NATS** | NATS pub-sub | Base64 header in NATS HeaderMap, raw payload | Subject scheme `velo.{b58}.{type}`, request/reply health, drain via unsub |
| **gRPC** | HTTP/2 streaming | Protobuf `FramedData` wrapper (preamble + header + payload) | Bidirectional streaming, tonic channels, exponential backoff reconnect |
| **UCX** | RDMA Active Messages | 3 fixed lanes (msg/resp/event) | `LocalSet` thread for `Rc<Worker>`, lazy endpoints, zero-copy RDMA |
## Shutdown Model
Graceful shutdown follows three phases:
1. **Gate**`begin_drain()` flips an atomic flag. Transports reject new inbound `Message` frames (TCP sends `ShuttingDown` response, HTTP returns 503, NATS unsubscribes from message subject).
2. **Drain**`wait_for_drain()` blocks until all `InFlightGuard`s are dropped. Policy is either `WaitForever` or `Timeout(Duration)`.
3. **Teardown** — Cancel the teardown token, stopping all listener loops and writer tasks. Call `shutdown()` on each transport to clean up connections.
Response, Ack, and Event frames continue flowing during drain so in-flight work can complete.
## Wire Format (TCP)
```text
┌──────────────┬───────────┬──────────────┬───────────────┬────────┬─────────┐
│ version (2B) │ type (1B) │ hdr_len (4B) │ pay_len (4B) │ header │ payload │
│ u16 BE │ u8 │ u32 BE │ u32 BE │ bytes │ bytes │
└──────────────┴───────────┴──────────────┴───────────────┴────────┴─────────┘
```
- **version**: Schema version (currently 1)
- **type**: `Message(0)`, `Response(1)`, `Ack(2)`, `Event(3)`, `ShuttingDown(4)`
- **hdr_len / pay_len**: Lengths of the following header and payload sections
- **Max frame size**: 16 MB
The gRPC transport wraps the same preamble + header + payload in a Protobuf `FramedData` message.
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Only compile proto files if grpc feature is enabled
#[cfg(feature = "grpc")]
{
tonic_build::compile_protos("proto/velo.proto")?;
}
Ok(())
}
# TCP Transport Design
## Overview
The TCP transport provides high-performance, zero-copy message delivery over raw TCP connections. It uses a custom 11-byte frame preamble for minimal overhead and supports CPU pinning for predictable latency.
## Connection Management
### DashMap Connection Pool
Peer connections are managed via two `DashMap` instances:
- `peers: DashMap<InstanceId, SocketAddr>` — registered peer addresses
- `connections: DashMap<InstanceId, ConnectionHandle>` — active writer task handles
Connections are established lazily on first `send_message()` call. Each connection spawns a dedicated writer task that owns the TCP stream.
### Writer Tasks
Each `ConnectionHandle` wraps a bounded `flume::Sender<SendTask>` (default capacity: 256). The send path:
1. **Fast path**: `try_send()` on existing connection — non-blocking, no allocation
2. **Slow path (full)**: `send_async()` via spawned task — applies backpressure
3. **Slow path (new)**: `get_or_create_connection()` — establishes TCP connection and spawns writer
The writer task loop:
```
recv_async(SendTask) → encode_frame(&mut stream, ...) → loop
```
## TcpFrameCodec
### Wire Format
```
[u16 BE: schema_version(1)] [u8: frame_type] [u32 BE: header_len] [u32 BE: payload_len] [header] [payload]
```
Total preamble: 11 bytes. Maximum frame: 16 MB.
### Decoder State Machine
The codec uses a two-state decoder for streaming TCP:
```
AwaitingHeader ──(11 bytes available)──→ AwaitingData ──(data available)──→ emit frame, reset
```
Zero-copy is achieved via `BytesMut::split_to().freeze()` — the output `Bytes` share the underlying receive buffer.
### Encoder
`encode_frame()` writes three segments via `write_all()`:
1. Preamble (11 bytes)
2. Header bytes
3. Payload bytes
`write_vectored()` is intentionally not used because it doesn't guarantee writing all bytes for payloads exceeding the kernel send buffer (~128KB).
## TCP Listener
### Frame Routing
Incoming frames are routed based on `MessageType`:
| MessageType | Target Stream |
|------------|---------------|
| Message | `message_stream` |
| Response | `response_stream` |
| Ack, Event | `event_stream` |
| ShuttingDown | `response_stream` (for correlation) |
### Drain Behavior
During drain (`ShutdownState::is_draining()`):
- **Message** frames are rejected: a `ShuttingDown` frame is sent back with the original header for correlation
- **Response/Ack/Event** frames pass through normally
### CPU Pinning (Linux)
`RuntimeConfig::CpuPin(cpu_id)` creates a single-threaded tokio runtime with the thread pinned to the specified CPU core via `nix::sched::sched_setaffinity`. This provides predictable latency by avoiding context switches.
On non-Linux platforms, `CpuPin` falls back to a regular single-threaded runtime with a warning.
## Socket Configuration
Both listener and writer sides configure:
- `TCP_NODELAY` — disable Nagle's algorithm for low-latency framing
- `SO_LINGER(1s)` — ensure clean socket shutdown
- `TCP_KEEPALIVE` — 60s idle time, 10s probe interval
- **Buffer sizes** — 1 MB send/receive buffers for high throughput
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
syntax = "proto3";
package velo.streaming.v1;
// Velo streaming service for active messages
//
// This service uses bidirectional streaming to send pre-framed messages.
// Messages are wrapped in FramedData which contains our custom TCP frame format.
service VeloStreaming
{
// Bidirectional streaming RPC
// Client sends framed messages, server receives them
// The response stream is unused (empty) in our implementation
rpc Stream(stream FramedData) returns (stream FramedData);
}
// Wrapper for pre-framed message data
//
// The fields map to TCP frame segments:
// preamble: [u16: version][u8: msg_type][u32: header_len][u32: payload_len]
// header: [header bytes]
// payload: [payload bytes]
message FramedData
{
bytes preamble = 1;
bytes header = 2;
bytes payload = 3;
}
// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//! Internal address builder for constructing WorkerAddress instances.
//!
//! This module provides the builder pattern for creating WorkerAddress instances
//! from transport-specific endpoint data. It is internal to velo-transports.
use bytes::Bytes;
use std::collections::HashMap;
use std::sync::Arc;
use velo_common::{WorkerAddress, WorkerAddressError};
/// Builder for constructing WorkerAddress instances.
///
/// This provides a mutable interface for collecting transport endpoints
/// before encoding them into the immutable WorkerAddress format.
#[derive(Debug, Clone, Default)]
pub(crate) struct WorkerAddressBuilder {
entries: HashMap<String, Bytes>,
}
impl WorkerAddressBuilder {
/// Create a new empty builder.
pub fn new() -> Self {
Self {
entries: HashMap::new(),
}
}
/// Add a new entry to the map.
///
/// Returns an error if the key already exists.
pub fn add_entry(
&mut self,
key: impl Into<String>,
value: impl Into<Bytes>,
) -> Result<(), WorkerAddressError> {
let key = key.into();
if self.entries.contains_key(&key) {
return Err(WorkerAddressError::KeyExists(key));
}
self.entries.insert(key, value.into());
Ok(())
}
/// Check if a key exists in the map.
#[allow(dead_code)]
pub fn has_entry(&self, key: &str) -> bool {
self.entries.contains_key(key)
}
/// Get a reference to an entry's value.
#[allow(dead_code)]
pub fn get_entry(&self, key: &str) -> Option<&Bytes> {
self.entries.get(key)
}
/// Merge another WorkerAddress into this builder.
///
/// This decodes the other address and attempts to add all its entries to this builder.
/// If any key from the other address already exists in this builder, returns an error
/// and leaves the builder unchanged.
pub fn merge(&mut self, other: &WorkerAddress) -> Result<(), WorkerAddressError> {
let map = decode_to_map(other.as_bytes())?;
// First check if any keys would conflict
for key in map.keys() {
if self.entries.contains_key(key.as_ref()) {
return Err(WorkerAddressError::KeyExists(key.to_string()));
}
}
// All keys are unique, now add them
for (key, value) in map {
self.entries.insert(key.to_string(), value);
}
Ok(())
}
/// Build the WorkerAddress from this builder.
///
/// This encodes the map into MessagePack binary format.
pub fn build(self) -> Result<WorkerAddress, WorkerAddressError> {
// Convert HashMap<String, Bytes> to HashMap<String, Vec<u8>> for MessagePack
let serializable: HashMap<String, Vec<u8>> = self
.entries
.into_iter()
.map(|(k, v)| (k, v.to_vec()))
.collect();
// Encode to MessagePack
let encoded = rmp_serde::to_vec(&serializable)?;
Ok(WorkerAddress::from_encoded(encoded))
}
}
/// Decode WorkerAddress bytes from MessagePack into a map.
fn decode_to_map(bytes: &[u8]) -> Result<HashMap<Arc<str>, Bytes>, WorkerAddressError> {
if bytes.is_empty() {
return Err(WorkerAddressError::InvalidFormat("Empty bytes".to_string()));
}
// Decode MessagePack
let decoded: HashMap<String, Vec<u8>> = rmp_serde::from_slice(bytes)?;
// Convert to HashMap<Arc<str>, Bytes>
Ok(decoded
.into_iter()
.map(|(k, v)| (Arc::from(k.as_str()), Bytes::from(v)))
.collect())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_basic() {
let mut builder = WorkerAddressBuilder::new();
builder
.add_entry("endpoint", Bytes::from_static(b"tcp://127.0.0.1:5555"))
.unwrap();
builder
.add_entry("protocol", Bytes::from_static(b"tcp"))
.unwrap();
assert!(builder.has_entry("endpoint"));
assert!(builder.has_entry("protocol"));
assert!(!builder.has_entry("nonexistent"));
let address = builder.build().unwrap();
assert!(!address.as_bytes().is_empty());
// Verify we can read the entries back
let entry = address.get_entry("endpoint").unwrap();
assert_eq!(entry, Some(Bytes::from_static(b"tcp://127.0.0.1:5555")));
}
#[test]
fn test_builder_add_duplicate_key() {
let mut builder = WorkerAddressBuilder::new();
builder
.add_entry("key", Bytes::from_static(b"value1"))
.unwrap();
let result = builder.add_entry("key", Bytes::from_static(b"value2"));
assert!(matches!(result, Err(WorkerAddressError::KeyExists(_))));
}
#[test]
fn test_builder_merge() {
// Build first address
let mut builder1 = WorkerAddressBuilder::new();
builder1
.add_entry("tcp", Bytes::from_static(b"tcp://127.0.0.1:5555"))
.unwrap();
let address1 = builder1.build().unwrap();
// Build second address
let mut builder2 = WorkerAddressBuilder::new();
builder2
.add_entry("rdma", Bytes::from_static(b"rdma://10.0.0.1:6666"))
.unwrap();
let address2 = builder2.build().unwrap();
// Merge both into a new builder
let mut builder3 = WorkerAddressBuilder::new();
builder3.merge(&address1).unwrap();
builder3.merge(&address2).unwrap();
let final_address = builder3.build().unwrap();
// Verify both entries are present
assert_eq!(
final_address.get_entry("tcp").unwrap(),
Some(Bytes::from_static(b"tcp://127.0.0.1:5555"))
);
assert_eq!(
final_address.get_entry("rdma").unwrap(),
Some(Bytes::from_static(b"rdma://10.0.0.1:6666"))
);
}
#[test]
fn test_builder_merge_with_conflict() {
let mut builder1 = WorkerAddressBuilder::new();
builder1
.add_entry("tcp", Bytes::from_static(b"tcp://127.0.0.1:5555"))
.unwrap();
let address1 = builder1.build().unwrap();
let mut builder2 = WorkerAddressBuilder::new();
builder2
.add_entry("tcp", Bytes::from_static(b"tcp://different:5555"))
.unwrap();
let address2 = builder2.build().unwrap();
// Merge first address
let mut builder3 = WorkerAddressBuilder::new();
builder3.merge(&address1).unwrap();
// Try to merge conflicting address - should fail
let result = builder3.merge(&address2);
assert!(matches!(result, Err(WorkerAddressError::KeyExists(_))));
// Builder should be unchanged
assert!(builder3.has_entry("tcp"));
assert_eq!(
builder3.get_entry("tcp").unwrap(),
&Bytes::from_static(b"tcp://127.0.0.1:5555")
);
}
#[test]
fn test_empty_builder() {
let builder = WorkerAddressBuilder::new();
let address = builder.build().unwrap();
// Empty address should still be valid
let transports = address.available_transports().unwrap();
assert_eq!(transports.len(), 0);
}
// ========================================================================
// Integration tests: Verify WorkerAddressBuilder (velo-transports) produces
// addresses that WorkerAddress (velo-common) can correctly decode.
// These tests ensure the two crates stay in sync on the wire format.
// ========================================================================
#[test]
fn test_builder_address_integration_get_entry() {
// Build an address with multiple entries
let mut builder = WorkerAddressBuilder::new();
builder
.add_entry("tcp", Bytes::from_static(b"tcp://127.0.0.1:5555"))
.unwrap();
builder
.add_entry("rdma", Bytes::from_static(b"rdma://10.0.0.1:6666"))
.unwrap();
builder
.add_entry("binary_data", Bytes::from_static(&[0x00, 0x01, 0x02, 0xFF]))
.unwrap();
let address = builder.build().unwrap();
// Verify WorkerAddress::get_entry() correctly decodes each entry
assert_eq!(
address.get_entry("tcp").unwrap(),
Some(Bytes::from_static(b"tcp://127.0.0.1:5555"))
);
assert_eq!(
address.get_entry("rdma").unwrap(),
Some(Bytes::from_static(b"rdma://10.0.0.1:6666"))
);
assert_eq!(
address.get_entry("binary_data").unwrap(),
Some(Bytes::from_static(&[0x00, 0x01, 0x02, 0xFF]))
);
assert_eq!(address.get_entry("nonexistent").unwrap(), None);
}
#[test]
fn test_builder_address_integration_available_transports() {
let mut builder = WorkerAddressBuilder::new();
builder
.add_entry("tcp", Bytes::from_static(b"tcp://127.0.0.1:5555"))
.unwrap();
builder
.add_entry("rdma", Bytes::from_static(b"rdma://10.0.0.1:6666"))
.unwrap();
builder
.add_entry("grpc", Bytes::from_static(b"grpc://localhost:9000"))
.unwrap();
let address = builder.build().unwrap();
// Verify WorkerAddress::available_transports() returns all keys
let transports = address.available_transports().unwrap();
assert_eq!(transports.len(), 3);
assert!(transports.contains(&velo_common::TransportKey::from("tcp")));
assert!(transports.contains(&velo_common::TransportKey::from("rdma")));
assert!(transports.contains(&velo_common::TransportKey::from("grpc")));
}
#[test]
fn test_builder_address_integration_checksum_stability() {
// Build same address twice - checksums should match
let mut builder1 = WorkerAddressBuilder::new();
builder1
.add_entry("key", Bytes::from_static(b"value"))
.unwrap();
let address1 = builder1.build().unwrap();
let mut builder2 = WorkerAddressBuilder::new();
builder2
.add_entry("key", Bytes::from_static(b"value"))
.unwrap();
let address2 = builder2.build().unwrap();
// Same content should produce same checksum
assert_eq!(address1.checksum(), address2.checksum());
// Different content should produce different checksum
let mut builder3 = WorkerAddressBuilder::new();
builder3
.add_entry("key", Bytes::from_static(b"different"))
.unwrap();
let address3 = builder3.build().unwrap();
assert_ne!(address1.checksum(), address3.checksum());
}
#[test]
fn test_builder_address_integration_bytes_roundtrip() {
// Build an address
let mut builder = WorkerAddressBuilder::new();
builder
.add_entry("endpoint", Bytes::from_static(b"test://value"))
.unwrap();
let address = builder.build().unwrap();
// Get raw bytes and create new address via from_encoded
let raw_bytes = address.to_bytes();
let address2 = WorkerAddress::from_encoded(raw_bytes);
// Both should be equal and decode the same
assert_eq!(address, address2);
assert_eq!(address.checksum(), address2.checksum());
assert_eq!(
address.get_entry("endpoint").unwrap(),
address2.get_entry("endpoint").unwrap()
);
}
#[test]
fn test_builder_address_integration_serde_roundtrip() {
// Build an address
let mut builder = WorkerAddressBuilder::new();
builder
.add_entry("tcp", Bytes::from_static(b"tcp://127.0.0.1:5555"))
.unwrap();
let address = builder.build().unwrap();
// Serialize to JSON and back
let json = serde_json::to_string(&address).unwrap();
let deserialized: WorkerAddress = serde_json::from_str(&json).unwrap();
// Should be equal and decode correctly
assert_eq!(address, deserialized);
assert_eq!(
address.get_entry("tcp").unwrap(),
deserialized.get_entry("tcp").unwrap()
);
}
}
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment