Commit 5ed8c1c0 authored by Ryan Olson's avatar Ryan Olson Committed by GitHub
Browse files

feat: rust - initial commit

the journey begins
parent 4017bd18
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "addr2line"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "anyhow"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
[[package]]
name = "arrayref"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "assert_matches"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9"
[[package]]
name = "async-nats"
version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76433c4de73442daedb3a59e991d94e85c14ebfc33db53dfcd347a21cd6ef4f8"
dependencies = [
"base64",
"bytes",
"futures",
"memchr",
"nkeys",
"nuid",
"once_cell",
"pin-project",
"portable-atomic",
"rand",
"regex",
"ring",
"rustls-native-certs 0.7.3",
"rustls-pemfile",
"rustls-webpki",
"serde",
"serde_json",
"serde_nanos",
"serde_repr",
"thiserror",
"time",
"tokio",
"tokio-rustls",
"tokio-util",
"tokio-websockets",
"tracing",
"tryhard",
"url",
]
[[package]]
name = "async-once-cell"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a"
[[package]]
name = "async-stream"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
dependencies = [
"async-stream-impl",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-stream-impl"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "async-trait"
version = "0.1.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "atomic"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994"
dependencies = [
"bytemuck",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "axum"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
dependencies = [
"async-trait",
"axum-core",
"bytes",
"futures-util",
"http",
"http-body",
"http-body-util",
"itoa",
"matchit",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"rustversion",
"serde",
"sync_wrapper",
"tower 0.5.2",
"tower-layer",
"tower-service",
]
[[package]]
name = "axum-core"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
dependencies = [
"async-trait",
"bytes",
"futures-util",
"http",
"http-body",
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"sync_wrapper",
"tower-layer",
"tower-service",
]
[[package]]
name = "backtrace"
version = "0.3.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets",
]
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bitflags"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
[[package]]
name = "blake3"
version = "1.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8ee0c1824c4dea5b5f81736aff91bae041d2c07ee1192bec91054e10e3e601e"
dependencies = [
"arrayref",
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bytemuck"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
dependencies = [
"serde",
]
[[package]]
name = "cc"
version = "1.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "constant_time_eq"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
"digest",
"fiat-crypto",
"rustc_version",
"subtle",
]
[[package]]
name = "curve25519-dalek-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "darling"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.98",
]
[[package]]
name = "darling_macro"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
dependencies = [
"darling_core",
"quote",
"syn 2.0.98",
]
[[package]]
name = "data-encoding"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f"
[[package]]
name = "der"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
dependencies = [
"const-oid",
"pem-rfc7468",
"zeroize",
]
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
"serde",
]
[[package]]
name = "derive-getters"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74ef43543e701c01ad77d3a5922755c6a1d71b22d942cb8042be4994b380caff"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "derive_builder"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "derive_builder_macro"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
dependencies = [
"derive_builder_core",
"syn 2.0.98",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "ed25519"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [
"signature",
]
[[package]]
name = "ed25519-dalek"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871"
dependencies = [
"curve25519-dalek",
"ed25519",
"sha2",
"signature",
"subtle",
]
[[package]]
name = "educe"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417"
dependencies = [
"enum-ordinalize",
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "either"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
dependencies = [
"serde",
]
[[package]]
name = "enum-ordinalize"
version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5"
dependencies = [
"enum-ordinalize-derive",
]
[[package]]
name = "enum-ordinalize-derive"
version = "4.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
[[package]]
name = "etcd-client"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0452bcc559431b16f472b7ab86e2f9ccd5f3c2da3795afbd6b773665e047fe"
dependencies = [
"http",
"prost",
"tokio",
"tokio-stream",
"tonic",
"tonic-build",
"tower 0.4.13",
"tower-service",
]
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "fiat-crypto"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "figment"
version = "0.10.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3"
dependencies = [
"atomic",
"parking_lot",
"pear",
"serde",
"serde_json",
"tempfile",
"toml",
"uncased",
"version_check",
]
[[package]]
name = "fixedbitset"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
dependencies = [
"percent-encoding",
]
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-util"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
dependencies = [
"cfg-if",
"libc",
"wasi 0.13.3+wasi-0.2.2",
"windows-targets",
]
[[package]]
name = "gimli"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "h2"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e"
dependencies = [
"atomic-waker",
"bytes",
"fnv",
"futures-core",
"futures-sink",
"http",
"indexmap 2.7.1",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "http"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "http-body"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http",
]
[[package]]
name = "http-body-util"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
dependencies = [
"bytes",
"futures-util",
"http",
"http-body",
"pin-project-lite",
]
[[package]]
name = "httparse"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"h2",
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"smallvec",
"tokio",
"want",
]
[[package]]
name = "hyper-timeout"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
dependencies = [
"hyper",
"hyper-util",
"pin-project-lite",
"tokio",
"tower-service",
]
[[package]]
name = "hyper-util"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http",
"http-body",
"hyper",
"pin-project-lite",
"socket2",
"tokio",
"tower-service",
"tracing",
]
[[package]]
name = "icu_collections"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locid"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_locid_transform"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
dependencies = [
"displaydoc",
"icu_locid",
"icu_locid_transform_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_locid_transform_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
[[package]]
name = "icu_normalizer"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
dependencies = [
"displaydoc",
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec",
"utf16_iter",
"utf8_iter",
"write16",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
[[package]]
name = "icu_properties"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
dependencies = [
"displaydoc",
"icu_collections",
"icu_locid_transform",
"icu_properties_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
[[package]]
name = "icu_provider"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
dependencies = [
"displaydoc",
"icu_locid",
"icu_provider_macros",
"stable_deref_trait",
"tinystr",
"writeable",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_provider_macros"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
dependencies = [
"idna_adapter",
"smallvec",
"utf8_iter",
]
[[package]]
name = "idna_adapter"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
dependencies = [
"icu_normalizer",
"icu_properties",
]
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
]
[[package]]
name = "indexmap"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
dependencies = [
"equivalent",
"hashbrown 0.15.2",
]
[[package]]
name = "inlinable_string"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
name = "litemap"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
[[package]]
name = "local-ip-address"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3669cf5561f8d27e8fc84cc15e58350e70f557d4d65f70e3154e54cd2f8e1782"
dependencies = [
"libc",
"neli",
"thiserror",
"windows-sys 0.59.0",
]
[[package]]
name = "lock_api"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
[[package]]
name = "matchit"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "miniz_oxide"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
dependencies = [
"adler2",
]
[[package]]
name = "mio"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0",
]
[[package]]
name = "multimap"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03"
[[package]]
name = "neli"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93062a0dce6da2517ea35f301dfc88184ce18d3601ec786a727a87bf535deca9"
dependencies = [
"byteorder",
"libc",
"log",
"neli-proc-macros",
]
[[package]]
name = "neli-proc-macros"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c8034b7fbb6f9455b2a96c19e6edf8dc9fc34c70449938d8ee3b4df363f61fe"
dependencies = [
"either",
"proc-macro2",
"quote",
"serde",
"syn 1.0.109",
]
[[package]]
name = "nid"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4abdf1789932b85dc39446e27f45a1064a30f9e19a2b872b1d09bd59283f85f3"
dependencies = [
"rand",
"serde",
"thiserror",
]
[[package]]
name = "nix"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "nkeys"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f49e787f4c61cbd0f9320b31cc26e58719f6aa5068e34697dd3aea361412fe3"
dependencies = [
"data-encoding",
"ed25519",
"ed25519-dalek",
"getrandom 0.2.15",
"log",
"rand",
"signatory",
]
[[package]]
name = "nuid"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc895af95856f929163a0aa20c26a78d26bfdc839f51b9d5aa7a5b79e52b7e83"
dependencies = [
"rand",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "object"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "parking_lot"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-targets",
]
[[package]]
name = "pear"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467"
dependencies = [
"inlinable_string",
"pear_codegen",
"yansi",
]
[[package]]
name = "pear_codegen"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147"
dependencies = [
"proc-macro2",
"proc-macro2-diagnostics",
"quote",
"syn 2.0.98",
]
[[package]]
name = "pem-rfc7468"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
dependencies = [
"base64ct",
]
[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "petgraph"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
dependencies = [
"fixedbitset",
"indexmap 2.7.1",
]
[[package]]
name = "pin-project"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkcs8"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"spki",
]
[[package]]
name = "portable-atomic"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [
"zerocopy",
]
[[package]]
name = "prettyplease"
version = "0.2.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac"
dependencies = [
"proc-macro2",
"syn 2.0.98",
]
[[package]]
name = "proc-macro-error-attr2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "proc-macro-error2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
dependencies = [
"proc-macro-error-attr2",
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "proc-macro2"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
dependencies = [
"unicode-ident",
]
[[package]]
name = "proc-macro2-diagnostics"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"version_check",
"yansi",
]
[[package]]
name = "prometheus"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d33c28a30771f7f96db69893f78b857f7450d7e0237e9c8fc6427a81bae7ed1"
dependencies = [
"cfg-if",
"fnv",
"lazy_static",
"memchr",
"parking_lot",
"protobuf",
"thiserror",
]
[[package]]
name = "prost"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec"
dependencies = [
"bytes",
"prost-derive",
]
[[package]]
name = "prost-build"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0f3e5beed80eb580c68e2c600937ac2c4eedabdfd5ef1e5b7ea4f3fba84497b"
dependencies = [
"heck",
"itertools",
"log",
"multimap",
"once_cell",
"petgraph",
"prettyplease",
"prost",
"prost-types",
"regex",
"syn 2.0.98",
"tempfile",
]
[[package]]
name = "prost-derive"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3"
dependencies = [
"anyhow",
"itertools",
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "prost-types"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc2f1e56baa61e93533aebc21af4d2134b70f66275e0fcdf3cbe43d77ff7e8fc"
dependencies = [
"prost",
]
[[package]]
name = "protobuf"
version = "2.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94"
[[package]]
name = "quote"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.15",
]
[[package]]
name = "redox_syscall"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
dependencies = [
"bitflags",
]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "ring"
version = "0.17.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.15",
"libc",
"spin",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.59.0",
]
[[package]]
name = "rustls"
version = "0.23.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb9263ab4eb695e42321db096e3b8fbd715a59b154d5c88d82db2175b681ba7"
dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5"
dependencies = [
"openssl-probe",
"rustls-pemfile",
"rustls-pki-types",
"schannel",
"security-framework 2.11.1",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3"
dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
"security-framework 3.2.0",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
[[package]]
name = "rustls-webpki"
version = "0.102.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
[[package]]
name = "ryu"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
[[package]]
name = "schannel"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags",
"core-foundation 0.9.4",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316"
dependencies = [
"bitflags",
"core-foundation 0.10.0",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "semver"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03"
[[package]]
name = "serde"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "serde_json"
version = "1.0.138"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serde_nanos"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a93142f0367a4cc53ae0fead1bcda39e85beccfad3dcd717656cacab94b12985"
dependencies = [
"serde",
]
[[package]]
name = "serde_repr"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "serde_spanned"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
dependencies = [
"serde",
]
[[package]]
name = "sha2"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
dependencies = [
"libc",
]
[[package]]
name = "signatory"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1e303f8205714074f6068773f0e29527e0453937fe837c9717d066635b65f31"
dependencies = [
"pkcs8",
"rand_core",
"signature",
"zeroize",
]
[[package]]
name = "signature"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest",
"rand_core",
]
[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]]
name = "smallvec"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "socket2"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
name = "spki"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
[[package]]
name = "synstructure"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "tempfile"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91"
dependencies = [
"cfg-if",
"fastrand",
"getrandom 0.3.1",
"once_cell",
"rustix",
"windows-sys 0.59.0",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "time"
version = "0.3.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tinystr"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
dependencies = [
"displaydoc",
"zerovec",
]
[[package]]
name = "tokio"
version = "1.43.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.52.0",
]
[[package]]
name = "tokio-macros"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "tokio-rustls"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37"
dependencies = [
"rustls",
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio",
]
[[package]]
name = "tokio-websockets"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f591660438b3038dd04d16c938271c79e7e06260ad2ea2885a4861bfb238605d"
dependencies = [
"base64",
"bytes",
"futures-core",
"futures-sink",
"http",
"httparse",
"rand",
"ring",
"rustls-native-certs 0.8.1",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tokio-util",
]
[[package]]
name = "toml"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee"
dependencies = [
"indexmap 2.7.1",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "tonic"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52"
dependencies = [
"async-stream",
"async-trait",
"axum",
"base64",
"bytes",
"h2",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-timeout",
"hyper-util",
"percent-encoding",
"pin-project",
"prost",
"socket2",
"tokio",
"tokio-stream",
"tower 0.4.13",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tonic-build"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11"
dependencies = [
"prettyplease",
"proc-macro2",
"prost-build",
"prost-types",
"quote",
"syn 2.0.98",
]
[[package]]
name = "tower"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
"futures-core",
"futures-util",
"indexmap 1.9.3",
"pin-project",
"pin-project-lite",
"rand",
"slab",
"tokio",
"tokio-util",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
"sync_wrapper",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]]
name = "tower-service"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "tracing-core"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [
"once_cell",
]
[[package]]
name = "triton-distributed"
version = "0.1.1"
dependencies = [
"anyhow",
"assert_matches",
"async-nats",
"async-once-cell",
"async-stream",
"async-trait",
"blake3",
"bytes",
"derive-getters",
"derive_builder",
"educe",
"either",
"etcd-client",
"figment",
"futures",
"local-ip-address",
"nid",
"nix",
"nuid",
"once_cell",
"prometheus",
"rand",
"regex",
"serde",
"serde_json",
"thiserror",
"tokio",
"tokio-stream",
"tokio-util",
"tracing",
"uuid",
"validator",
"xxhash-rust",
]
[[package]]
name = "try-lock"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tryhard"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9f0a709784e86923586cff0d872dba54cd2d2e116b3bc57587d15737cfce9d"
dependencies = [
"futures",
"pin-project-lite",
"tokio",
]
[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "uncased"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697"
dependencies = [
"version_check",
]
[[package]]
name = "unicode-ident"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
]
[[package]]
name = "utf16_iter"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b"
dependencies = [
"getrandom 0.2.15",
"serde",
]
[[package]]
name = "validator"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa"
dependencies = [
"idna",
"once_cell",
"regex",
"serde",
"serde_derive",
"serde_json",
"url",
"validator_derive",
]
[[package]]
name = "validator_derive"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca"
dependencies = [
"darling",
"once_cell",
"proc-macro-error2",
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "want"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
dependencies = [
"try-lock",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
version = "0.13.3+wasi-0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f"
dependencies = [
"memchr",
]
[[package]]
name = "wit-bindgen-rt"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
dependencies = [
"bitflags",
]
[[package]]
name = "write16"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
[[package]]
name = "writeable"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "xxhash-rust"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "yoke"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
dependencies = [
"serde",
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"synstructure",
]
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"byteorder",
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "zerofrom"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
[[package]]
name = "zerovec"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[package]
name = "triton-distributed"
version = "0.1.1"
edition = "2021"
authors = ["NVIDIA"]
homepage = "https://github.com/triton-inference-server/triton_distributed"
[dependencies]
# workspace - when we expand to multiple crates; put these in the workspace
anyhow = { version = "1" }
async-nats = { version = "0.38", features = ["service"] }
async-stream = { version = "0.3" }
async-trait = { version = "0.1" }
blake3 = "1"
bytes = "1"
derive_builder = "0.20"
derive-getters = "0.5"
either = { version = "1.13", features = ["serde"] }
figment = { version = "0.10.19", features = ["env", "json", "toml", "test"] }
futures = { version = "0.3" }
once_cell = "1"
prometheus = { version = "0.13" }
regex = { version = "1" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = { version = "1" }
tokio = { version = "1", features = ["full"] }
tokio-stream = { version = "0.1" }
tokio-util = { version = "0.7", features = ["codec", "net"] }
tracing = { version = "0.1" }
uuid = { version = "1", features = ["v4", "serde"] }
validator = { version = "0.20", features = ["derive"] }
xxhash-rust = { version = "0.8", features = ["xxh3", "const_xxh3"] }
# non-workspace
async-once-cell = "0.5.4"
educe = "0.6.0"
etcd-client = "0.14"
local-ip-address = { version = "0.6.3" }
nid = { version = "3.0.0", features = ["serde"] }
nix = { version = "0.29", features = ["signal"] }
nuid = { version = "0.5" }
rand = { version = "0.8"}
[dev-dependencies]
assert_matches = "1.5.0"
/*
* Copyright 2024-2025 NVIDIA CORPORATION & AFFILIATES
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
//! The [Component] module defines the top-level API for building distributed applications.
//!
//! A distributed application consists of a set of [Component][Component] that can host one
//! or more [Endpoint][Endpoint]. Each [Endpoint][Endpoint] is a network-accessible service
//! that can be accessed by other [Component][Component] in the distributed application.
//!
//! A [Component] is made discoverable by registering it with the distributed runtime under
//! a [`Namespace`].
//!
//! A [`Namespace`] is a logical grouping of [Component][Component] that are grouped together.
//!
//! We might extend namespace to include grouping behavior, which would define groups of
//! components that are tightly coupled.
//!
//! A [Component] is the core building block of a distributed application. It is a logical
//! unit of work such as a `Preprocessor` or `SmartRouter` that has a well-defined role in the
//! distributed application.
//!
//! A [Component] can present to the distributed application one or more configuration files
//! which define how that component was constructed/configured and what capabilities it can
//! provide.
//!
//! Other [Component][Component] can write to watching locations within a [Component] etcd
//! path. This allows the [Component] to take dynamic actions depending on the watch
//! triggers.
//!
//! TODO: Top-level Overview of Endpoints/Functions
use crate::discovery::Lease;
use super::{error, log, transports::nats::Slug, DistributedRuntime, Result};
use crate::pipeline::network::{ingress::push_endpoint::PushEndpoint, PushWorkHandler};
use async_nats::{
rustls::quic,
service::{Service, ServiceExt},
};
use derive_builder::Builder;
use derive_getters::Getters;
use educe::Educe;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, sync::Arc};
use validator::{Validate, ValidationError};
mod client;
mod endpoint;
mod registry;
mod service;
pub use client::Client;
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum TransportType {
NatsTcp(String),
}
#[derive(Clone)]
pub struct Registry {
services: Arc<tokio::sync::Mutex<HashMap<String, Service>>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComponentEndpointInfo {
pub component: String,
pub endpoint: String,
pub namespace: String,
pub lease_id: i64,
pub transport: TransportType,
}
/// A [Component] a discoverable entity in the distributed runtime.
/// You can host [Endpoint][Endpoint] on a [Component] by first creating
/// a [Service] then adding one or more [Endpoint][Endpoint] to the [Service].
///
/// You can also issue a request to a [Component]'s [Endpoint] by creating a [Client].
#[derive(Educe, Builder, Clone)]
#[educe(Debug)]
#[builder(pattern = "owned")]
pub struct Component {
#[builder(private)]
#[educe(Debug(ignore))]
drt: DistributedRuntime,
// todo - restrict the namespace to a-z0-9-_A-Z
/// Name of the component
#[builder(setter(into))]
name: String,
// todo - restrict the namespace to a-z0-9-_A-Z
/// Namespace
#[builder(setter(into))]
namespace: String,
}
impl Component {
pub fn etcd_path(&self) -> String {
format!("{}/components/{}", self.namespace, self.name)
}
fn slug(&self) -> Slug {
Slug::from_string(self.etcd_path())
}
pub fn endpoint(&self, endpoint: impl Into<String>) -> Endpoint {
Endpoint {
component: self.clone(),
name: endpoint.into(),
}
}
/// Get keys from etcd on the slug, splitting the endpoints and only returning the
/// set of unique endpoints.
pub async fn list_endpoints(&self) -> Vec<Endpoint> {
unimplemented!("endpoints")
}
/// This method will scrape the stats for all available services
/// Returns a stream of [`ServiceInfo`] objects.
/// This should be consumed by a `[tokio::time::timeout_at`] because each services
/// will only respond once, but there is no way to know when all services have responded.
pub async fn stats_stream(&self) -> Result<()> {
unimplemented!("collect_stats")
}
pub fn service_builder(&self) -> service::ServiceConfigBuilder {
service::ServiceConfigBuilder::from_component(self.clone())
}
}
impl ComponentBuilder {
pub fn from_runtime(drt: DistributedRuntime) -> Self {
Self::default().drt(drt)
}
}
#[derive(Debug, Clone)]
pub struct Endpoint {
component: Component,
// todo - restrict alphabet
/// Endpoint name
name: String,
}
impl Endpoint {
pub fn name(&self) -> &str {
&self.name
}
pub fn etcd_path(&self) -> String {
format!("{}/{}", self.component.etcd_path(), self.name)
}
pub fn etcd_path_with_id(&self, lease_id: i64) -> String {
format!("{}:{:x}", self.etcd_path(), lease_id)
}
pub fn name_with_id(&self, lease_id: i64) -> String {
format!("{}-{:x}", self.name, lease_id)
}
pub fn subject(&self, lease_id: i64) -> String {
format!("{}.{}", self.component.slug(), self.name_with_id(lease_id))
}
pub async fn client<Req, Resp>(&self) -> Result<client::Client<Req, Resp>>
where
Req: Serialize + Send + Sync + 'static,
Resp: for<'de> Deserialize<'de> + Send + Sync + 'static,
{
client::Client::new(self.clone()).await
}
pub fn endpoint_builder(&self) -> endpoint::EndpointConfigBuilder {
endpoint::EndpointConfigBuilder::from_endpoint(self.clone())
}
}
#[derive(Educe, Builder, Clone, Validate)]
#[educe(Debug)]
#[builder(pattern = "owned")]
pub struct Namespace {
#[builder(private)]
#[educe(Debug(ignore))]
runtime: DistributedRuntime,
#[validate()]
name: String,
}
impl Namespace {
pub(crate) fn new(runtime: DistributedRuntime, name: String) -> Result<Self> {
Ok(NamespaceBuilder::default()
.runtime(runtime)
.name(name)
.build()?)
}
/// Create a [`Component`] in the namespace
pub fn component(&self, name: impl Into<String>) -> Result<Component> {
Ok(ComponentBuilder::from_runtime(self.runtime.clone())
.name(name)
.namespace(self.name.clone())
.build()?)
}
}
// Custom validator function
fn validate_allowed_chars(input: &str) -> Result<(), ValidationError> {
// Define the allowed character set using a regex
let regex = regex::Regex::new(r"^[a-z0-9-_]+$").unwrap();
if regex.is_match(input) {
Ok(())
} else {
Err(ValidationError::new("invalid_characters"))
}
}
// TODO - enable restrictions to the character sets allowed for namespaces,
// components, and endpoints.
//
// Put Validate traits on the struct and use the `validate_allowed_chars` method
// to validate the fields.
// #[cfg(test)]
// mod tests {
// use super::*;
// use validator::Validate;
// #[test]
// fn test_valid_names() {
// // Valid strings
// let valid_inputs = vec![
// "abc", // Lowercase letters
// "abc123", // Letters and numbers
// "a-b-c", // Letters with hyphens
// "a_b_c", // Letters with underscores
// "a-b_c-123", // Mixed valid characters
// "a", // Single character
// "a_b", // Short valid pattern
// "123456", // Only numbers
// "a---b_c123", // Repeated hyphens/underscores
// ];
// for input in valid_inputs {
// let result = validate_allowed_chars(input);
// assert!(result.is_ok(), "Expected '{}' to be valid", input);
// }
// }
// #[test]
// fn test_invalid_names() {
// // Invalid strings
// let invalid_inputs = vec![
// "abc!", // Invalid character `!`
// "abc@", // Invalid character `@`
// "123$", // Invalid character `$`
// "foo.bar", // Invalid character `.`
// "foo/bar", // Invalid character `/`
// "foo\\bar", // Invalid character `\`
// "abc#", // Invalid character `#`
// "abc def", // Spaces are not allowed
// "foo,", // Invalid character `,`
// "", // Empty string
// ];
// for input in invalid_inputs {
// let result = validate_allowed_chars(input);
// assert!(result.is_err(), "Expected '{}' to be invalid", input);
// }
// }
// // #[test]
// // fn test_struct_validation_valid() {
// // // Struct with valid data
// // let valid_data = InputData {
// // name: "valid-name_123".to_string(),
// // };
// // assert!(valid_data.validate().is_ok());
// // }
// // #[test]
// // fn test_struct_validation_invalid() {
// // // Struct with invalid data
// // let invalid_data = InputData {
// // name: "invalid!name".to_string(),
// // };
// // let result = invalid_data.validate();
// // assert!(result.is_err());
// // if let Err(errors) = result {
// // let error_map = errors.field_errors();
// // assert!(error_map.contains_key("name"));
// // let name_errors = &error_map["name"];
// // assert_eq!(name_errors[0].code, "invalid_characters");
// // }
// // }
// #[test]
// fn test_edge_cases() {
// // Edge cases
// let edge_inputs = vec![
// ("-", true), // Single hyphen
// ("_", true), // Single underscore
// ("a-", true), // Letter with hyphen
// ("-", false), // Repeated hyphens
// ("-a", false), // Hyphen at the beginning
// ("a-", false), // Hyphen at the end
// ];
// for (input, expected_validity) in edge_inputs {
// let result = validate_allowed_chars(input);
// if expected_validity {
// assert!(result.is_ok(), "Expected '{}' to be valid", input);
// } else {
// assert!(result.is_err(), "Expected '{}' to be invalid", input);
// }
// }
// }
// }
/*
* Copyright 2024-2025 NVIDIA CORPORATION & AFFILIATES
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
use crate::pipeline::{
network::egress::push::{AddressedPushRouter, AddressedRequest, PushRouter},
AsyncEngine, Data, ManyOut, SingleIn,
};
use rand::Rng;
use std::collections::HashMap;
use std::sync::{
atomic::{AtomicU64, Ordering},
Arc,
};
use tokio::{net::unix::pipe::Receiver, sync::Mutex};
use crate::{pipeline::async_trait, transports::etcd::WatchEvent, Error};
use super::*;
/// Each state will be have a nonce associated with it
/// The state will be emitted in a watch channel, so we can observe the
/// critical state transitions.
enum MapState {
/// The map is empty; value = nonce
Empty(u64),
/// The map is not-empty; values are (nonce, count)
NonEmpty(u64, u64),
/// The watcher has finished, no more events will be emitted
Finished,
}
enum EndpointEvent {
Put(String, i64),
Delete(String),
}
#[derive(Clone)]
pub struct Client<T: Data, U: Data> {
endpoint: Endpoint,
router: PushRouter<T, U>,
watch_rx: tokio::sync::watch::Receiver<Vec<i64>>,
counter: Arc<AtomicU64>,
}
impl<T, U> Client<T, U>
where
T: Data + Serialize,
U: Data + for<'de> Deserialize<'de>,
{
pub(crate) async fn new(endpoint: Endpoint) -> Result<Self> {
let router = AddressedPushRouter::new(
endpoint.component.drt.nats_client.client().clone(),
endpoint.component.drt.tcp_server().await?,
)?;
// create live endpoint watcher
let prefix_watcher = endpoint
.component
.drt
.etcd_client
.kv_get_and_watch_prefix(endpoint.etcd_path())
.await?;
let (prefix, _watcher, mut kv_event_rx) = prefix_watcher.dissolve();
let (watch_tx, watch_rx) = tokio::sync::watch::channel(vec![]);
let secondary = endpoint.component.drt.runtime.secondary().clone();
// this task should be included in the registry
// currently this is created once per client, but this object/task should only be instantiated
// once per worker/instance
secondary.spawn(async move {
log::debug!("Starting endpoint watcher for prefix: {}", prefix);
let mut map = HashMap::new();
loop {
let kv_event = tokio::select! {
_ = watch_tx.closed() => {
log::debug!("all watchers have closed; shutting down endpoint watcher for prefix: {}", prefix);
break;
}
kv_event = kv_event_rx.recv() => {
match kv_event {
Some(kv_event) => kv_event,
None => {
log::debug!("watch stream has closed; shutting down endpoint watcher for prefix: {}", prefix);
break;
}
}
}
};
match kv_event {
WatchEvent::Put(kv) => {
let key = String::from_utf8(kv.key().to_vec());
let val = serde_json::from_slice::<ComponentEndpointInfo>(kv.value());
if let (Ok(key), Ok(val)) = (key, val) {
map.insert(key.clone(), val.lease_id);
} else {
log::error!("Unable to parse put endpoint event; shutting down endpoint watcher for prefix: {}", prefix);
break;
}
}
WatchEvent::Delete(kv) => {
match String::from_utf8(kv.key().to_vec()) {
Ok(key) => { map.remove(&key); }
Err(_) => {
log::error!("Unable to parse delete endpoint event; shutting down endpoint watcher for prefix: {}", prefix);
break;
}
}
}
}
let endpoint_ids: Vec<i64> = map.values().cloned().collect();
if watch_tx.send(endpoint_ids).is_err() {
log::debug!("Unable to send watch updates; shutting down endpoint watcher for prefix: {}", prefix);
break;
}
}
log::debug!("Completed endpoint watcher for prefix: {}", prefix);
let _ = watch_tx.send(vec![]);
});
Ok(Client {
endpoint,
router,
watch_rx,
counter: Arc::new(AtomicU64::new(0)),
})
}
pub fn endpoint_ids(&self) -> &tokio::sync::watch::Receiver<Vec<i64>> {
&self.watch_rx
}
/// Wait for at least one [`Endpoint`] to be available
pub async fn wait_for_endpoints(&self) -> Result<()> {
let mut rx = self.watch_rx.clone();
// wait for there to be 1 or more endpoints
loop {
if rx.borrow_and_update().is_empty() {
rx.changed().await?;
} else {
break;
}
}
Ok(())
}
/// Issue a request to the next available endpoint in a round-robin fashion
pub async fn round_robin(&self, request: SingleIn<T>) -> Result<ManyOut<U>> {
let counter = self.counter.fetch_add(1, Ordering::Relaxed);
let endpoint_id = {
let endpoints = self.watch_rx.borrow();
let count = endpoints.len();
if count == 0 {
return Err(error!(
"no endpoints found for endpoint {:?}",
self.endpoint.etcd_path()
));
}
let offset = counter % count as u64;
endpoints[offset as usize]
};
let subject = self.endpoint.subject(endpoint_id);
let request = request.map(|req| AddressedRequest::new(req, subject));
self.router.generate(request).await
}
/// Issue a request to a random endpoint
pub async fn random(&self, request: SingleIn<T>) -> Result<ManyOut<U>> {
let endpoint_id = {
let endpoints = self.watch_rx.borrow();
let count = endpoints.len();
if count == 0 {
return Err(error!(
"no endpoints found for endpoint {:?}",
self.endpoint.etcd_path()
));
}
let counter = rand::thread_rng().gen::<u64>();
let offset = counter % count as u64;
endpoints[offset as usize]
};
let subject = self.endpoint.subject(endpoint_id);
let request = request.map(|req| AddressedRequest::new(req, subject));
self.router.generate(request).await
}
/// Issue a request to a specific endpoint
pub async fn direct(&self, request: SingleIn<T>, endpoint_id: i64) -> Result<ManyOut<U>> {
let found = {
let endpoints = self.watch_rx.borrow();
endpoints.contains(&endpoint_id)
};
if !found {
return Err(error!(
"endpoint_id={} not found for endpoint {:?}",
endpoint_id,
self.endpoint.etcd_path()
));
}
let subject = self.endpoint.subject(endpoint_id);
let request = request.map(|req| AddressedRequest::new(req, subject));
self.router.generate(request).await
}
}
#[async_trait]
impl<T, U> AsyncEngine<SingleIn<T>, ManyOut<U>, Error> for Client<T, U>
where
T: Data + Serialize,
U: Data + for<'de> Deserialize<'de>,
{
async fn generate(&self, request: SingleIn<T>) -> Result<ManyOut<U>, Error> {
self.random(request).await
}
}
/*
* Copyright 2024-2025 NVIDIA CORPORATION & AFFILIATES
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
use derive_getters::Dissolve;
use super::*;
#[derive(Educe, Builder, Dissolve)]
#[educe(Debug)]
#[builder(pattern = "owned", build_fn(private, name = "build_internal"))]
pub struct EndpointConfig {
#[builder(private)]
endpoint: Endpoint,
/// Lease
#[educe(Debug(ignore))]
#[builder(default)]
lease: Option<Lease>,
/// Endpoint handler
#[educe(Debug(ignore))]
handler: Arc<dyn PushWorkHandler>,
}
impl EndpointConfigBuilder {
pub(crate) fn from_endpoint(endpoint: Endpoint) -> Self {
Self::default().endpoint(endpoint)
}
pub async fn start(self) -> Result<()> {
let (endpoint, lease, handler) = self.build_internal()?.dissolve();
let lease = lease.unwrap_or(endpoint.component.drt.primary_lease());
log::debug!(
"Starting endpoint: {}",
endpoint.etcd_path_with_id(lease.id())
);
let group = endpoint
.component
.drt
.component_registry
.services
.lock()
.await
.get(&endpoint.component.etcd_path())
.map(|service| service.group(endpoint.component.slug()))
.ok_or(error!("Service not found"))?;
// let group = service.group(service_name.as_str());
// creates an endpoint for the service
let service_endpoint = group
.endpoint(&endpoint.name_with_id(lease.id()))
.await
.map_err(|e| anyhow::anyhow!("Failed to start endpoint: {e}"))?;
let cancel_token = lease.child_token();
let push_endpoint = PushEndpoint::builder()
.service_handler(handler)
.cancellation_token(cancel_token.clone())
.build()
.map_err(|e| anyhow::anyhow!("Failed to build push endpoint: {e}"))?;
// launch in primary runtime
let task = tokio::spawn(push_endpoint.start(service_endpoint));
// log::debug!(worker_id, "endpoint subject: {}", subject);
// make the components service endpoint discovery in etcd
// client.register_service()
let info = ComponentEndpointInfo {
component: endpoint.component.name.clone(),
endpoint: endpoint.name.clone(),
namespace: endpoint.component.namespace.clone(),
lease_id: lease.id(),
transport: TransportType::NatsTcp(endpoint.subject(lease.id())),
};
let info = serde_json::to_vec_pretty(&info)?;
if let Err(e) = endpoint
.component
.drt
.etcd_client
.kv_create(
endpoint.etcd_path_with_id(lease.id()),
info,
Some(lease.id()),
)
.await
{
log::error!("Failed to register discoverable service: {:?}", e);
cancel_token.cancel();
return Err(error!("Failed to register discoverable service"));
}
task.await??;
Ok(())
}
}
/*
* Copyright 2024-2025 NVIDIA CORPORATION & AFFILIATES
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
use super::{Component, Registry, Result};
use async_once_cell::OnceCell;
use std::{
collections::HashMap,
sync::{Arc, Weak},
};
use tokio::sync::Mutex;
impl Default for Registry {
fn default() -> Self {
Self::new()
}
}
impl Registry {
pub fn new() -> Self {
Self {
services: Arc::new(Mutex::new(HashMap::new())),
}
}
}
// impl ComponentRegistry {
// pub fn new() -> Self {
// Self {
// clients: Arc::new(Mutex::new(HashMap::new())),
// }
// }
// pub async fn get_or_create(&mut self, component: Component) -> Result<Arc<Client>> {
// // Lock the clients HashMap for thread-safe access
// let mut guard = self.clients.lock().await;
// // Check if the component already exists in the registry
// if let Some(weak) = guard.get(&component.slug()) {
// // Attempt to upgrade the Weak pointer
// if let Some(client) = weak.upgrade() {
// return Ok(client);
// }
// }
// // Fallback: Create a new Client
// let client = component.client().await?;
// // Insert a Weak reference to the new client into the map
// guard.insert(component.slug(), Arc::downgrade(&client));
// Ok(client)
// }
// }
// #[derive(Clone)]
// pub struct ServiceRegistry {
// clients: Arc<Mutex<HashMap<String, Arc<Service>>>>,
// }
// impl ServiceRegistry {
// pub fn new() -> Self {
// Self {
// clients: Arc::new(Mutex::new(HashMap::new())),
// }
// }
// pub async fn get_or_create(&mut self, component: Component) -> Result<Arc<Client>> {
// // Lock the clients HashMap for thread-safe access
// let mut guard = self.clients.lock().await;
// // Check if the component already exists in the registry
// if let Some(weak) = guard.get(&component.slug()) {
// // Attempt to upgrade the Weak pointer
// if let Some(client) = weak.upgrade() {
// return Ok(client);
// }
// }
// // Fallback: Create a new Client
// let client = component.client().await?;
// // Insert a Weak reference to the new client into the map
// guard.insert(component.slug(), Arc::downgrade(&client));
// Ok(client)
// }
// }
/*
* Copyright 2024-2025 NVIDIA CORPORATION & AFFILIATES
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
use derive_getters::Dissolve;
use super::*;
use async_nats::service::{endpoint, Service};
pub type StatsHandler =
Box<dyn FnMut(String, endpoint::Stats) -> serde_json::Value + Send + Sync + 'static>;
#[derive(Educe, Builder, Dissolve)]
#[educe(Debug)]
#[builder(pattern = "owned", build_fn(private, name = "build_internal"))]
pub struct ServiceConfig {
#[builder(private)]
component: Component,
/// Description
#[builder(default)]
description: Option<String>,
// todo - make optional - if None, then skip making the endpoint
// and skip making the service-endpoint discoverable.
/// Endpoint handler
#[educe(Debug(ignore))]
#[builder(default)]
stats_handler: Option<StatsHandler>,
}
impl ServiceConfigBuilder {
/// Create the [`Component`]'s service and store it in the registry.
pub async fn create(self) -> Result<Component> {
let version = "0.0.1".to_string();
let (component, description, stat_handler) = self.build_internal()?.dissolve();
let service_name = component.slug();
let description = description.unwrap_or(format!(
"Triton Component {} in {}",
component.name, component.namespace
));
let mut guard = component.drt.component_registry.services.lock().await;
if guard.contains_key(&component.etcd_path()) {
return Err(anyhow::anyhow!("Service already exists"));
}
// create service on the secondary runtime
let secondary = component.drt.runtime.secondary.clone();
let builder = component.drt.nats_client.client().service_builder();
let service = secondary
.spawn(async move {
// unwrap the stats handler
let builder = match stat_handler {
Some(handler) => builder.stats_handler(handler),
None => builder,
};
log::debug!("Starting service: {}", service_name);
builder
.description(description)
.start(service_name.to_string(), version)
.await
})
.await?
.map_err(|e| anyhow::anyhow!("Failed to start service: {e}"))?;
guard.insert(component.etcd_path(), service);
drop(guard);
Ok(component)
}
}
impl ServiceConfigBuilder {
pub(crate) fn from_component(component: Component) -> Self {
Self::default().component(component)
}
}
/*
* Copyright 2024-2025 NVIDIA CORPORATION & AFFILIATES
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
use super::Result;
use derive_builder::Builder;
use figment::{
providers::{Env, Format, Serialized, Toml},
Figment,
};
use serde::{Deserialize, Serialize};
use validator::Validate;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkerConfig {
/// Grace shutdown period for http-service.
pub graceful_shutdown_timeout: u64,
}
impl WorkerConfig {
pub fn from_settings() -> Self {
// Instantiates and reads server configurations from appropriate sources.
// All calls should be global and thread safe.
Figment::new()
.merge(Serialized::defaults(Self::default()))
.merge(Env::prefixed("TRITON_WORKER_"))
.extract()
.unwrap()
}
}
impl Default for WorkerConfig {
fn default() -> Self {
WorkerConfig {
graceful_shutdown_timeout: if cfg!(debug_assertions) {
1 // Debug build: 1 second
} else {
30 // Release build: 30 seconds
},
}
}
}
/// Runtime configuration
/// Defines the configuration for Tokio runtimes
#[derive(Serialize, Deserialize, Validate, Debug, Builder, Clone)]
#[builder(build_fn(private, name = "build_internal"), derive(Debug, Serialize))]
pub struct RuntimeConfig {
/// Maximum number of async worker threads
/// If set to 1, the runtime will run in single-threaded mode
#[validate(range(min = 1))]
#[builder(default = "16")]
#[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
pub max_worker_threads: usize,
/// Maximum number of blocking threads
/// Blocking threads are used for blocking operations, this value must be greater than 0.
#[validate(range(min = 1))]
#[builder(default = "16")]
#[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
pub max_blocking_threads: usize,
}
impl RuntimeConfig {
pub fn builder() -> RuntimeConfigBuilder {
RuntimeConfigBuilder::default()
}
pub(crate) fn figment() -> Figment {
Figment::new()
.merge(Serialized::defaults(RuntimeConfig::default()))
.merge(Toml::file("/opt/triton/defaults/runtime.toml"))
.merge(Toml::file("/opt/triton/etc/runtime.toml"))
.merge(Env::prefixed("TRITON_RUNTIME_"))
}
/// Load the runtime configuration from the environment and configuration files
/// Configuration is priorities in the following order, where the last has the lowest priority:
/// 1. Environment variables (top priority)
/// 2. /opt/triton/etc/runtime.toml
/// 3. /opt/triton/defaults/runtime.toml (lowest priority)
///
/// Environment variables are prefixed with `TRITON_RUNTIME_`
pub fn from_settings() -> Result<RuntimeConfig> {
let config: RuntimeConfig = Self::figment().extract()?;
config.validate()?;
Ok(config)
}
pub fn single_threaded() -> Self {
RuntimeConfig {
max_worker_threads: 1,
max_blocking_threads: 1,
}
}
/// Create a new default runtime configuration
pub(crate) fn create_runtime(&self) -> Result<tokio::runtime::Runtime> {
Ok(tokio::runtime::Builder::new_multi_thread()
.worker_threads(self.max_worker_threads)
.max_blocking_threads(self.max_blocking_threads)
.enable_all()
.build()?)
}
}
impl Default for RuntimeConfig {
fn default() -> Self {
Self::single_threaded()
}
}
impl RuntimeConfigBuilder {
/// Build and validate the runtime configuration
pub fn build(&self) -> Result<RuntimeConfig> {
let config = self.build_internal()?;
config.validate()?;
Ok(config)
}
}
/*
* Copyright 2024-2025 NVIDIA CORPORATION & AFFILIATES
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
use crate::{transports::etcd, Result};
pub use etcd::Lease;
pub struct DiscoveryClient {
namespace: String,
etcd_client: etcd::Client,
}
impl DiscoveryClient {
/// Create a new [`DiscoveryClient`]
///
/// This will establish a connection to the etcd server, create a primary lease,
/// and spawn a task to keep the lease alive and tie the lifetime of the [`Runtime`]
/// to the lease.
///
/// If the lease expires, the [`Runtime`] will be shutdown.
/// If the [`Runtime`] is shutdown, the lease will be revoked.
pub(crate) fn new(namespace: String, etcd_client: etcd::Client) -> Self {
DiscoveryClient {
namespace,
etcd_client,
}
}
/// Get the primary lease ID
pub fn primary_lease_id(&self) -> i64 {
self.etcd_client.lease_id()
}
/// Create a [`Lease`] with a given time-to-live (TTL).
/// This [`Lease`] will be tied to the [`Runtime`], but has its own independent [`crate::CancellationToken`].
pub async fn create_lease(&self, ttl: i64) -> Result<Lease> {
self.etcd_client.create_lease(ttl).await
}
// the following two commented out codes are not implemented, but are placeholders for proposed ectd usage patterns
// /// Create an ephemeral key/value pair tied to a lease_id.
// /// This is an atomic create. If the key already exists, this will fail.
// /// The [`etcd_client::KeyValue`] will be removed when the lease expires or is revoked.
// pub async fn create_ephemerial_key(&self, key: &str, value: &str, lease_id: i64) -> Result<()> {
// // self.etcd_client.create_ephemeral_key(key, value, lease_id).await
// unimplemented!()
// }
// /// Create a shared [`etcd_client::KeyValue`] which behaves similar to a C++ `std::shared_ptr` or a
// /// Rust [std::sync::Arc]. Instead of having one owner of the lease, multiple owners participate in
// /// maintaining the lease. In this manner, when the last member of the group sharing the lease is gone,
// /// the lease will be expired.
// ///
// /// Implementation notes: At the time of writing, it is unclear if we have atomics that control leases,
// /// so in our initial implementation, the last member of the group will not revoke the lease, so the object
// /// will live for upto the TTL after the last member is gone.
// ///
// /// Notes
// /// -----
// ///
// /// - Multiple members sharing the lease and contributing to the heartbeat might cause some overheads.
// /// The implementation will try to randomize the heartbeat intervals to avoid thundering herd problem,
// /// and with any luck, the heartbeat watchers will be able to detect when if a external member triggered
// /// the heartbeat checking this interval and skip unnecessary heartbeat messages.
// ///
// /// A new lease will be created for this object. If you wish to add an object to a shared group s
// ///
// /// The [`etcd_client::KeyValue`] will be removed when the lease expires or is revoked.
// pub async fn create_shared_key(&self, key: &str, value: &str, lease_id: i64) -> Result<()> {
// // self.etcd_client.create_ephemeral_key(key, value, lease_id).await
// unimplemented!()
// }
}
/*
* Copyright 2024-2025 NVIDIA CORPORATION & AFFILIATES
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
pub use crate::component::Component;
use crate::{
component::{self, ComponentBuilder, Namespace},
discovery::DiscoveryClient,
service::ServiceClient,
transports::{etcd, nats, tcp},
ErrorContext,
};
use super::{error, Arc, DistributedRuntime, OnceCell, Result, Runtime, OK};
use derive_getters::Dissolve;
use figment::error;
impl DistributedRuntime {
pub async fn new(runtime: Runtime, config: DistributedConfig) -> Result<Self> {
let secondary = runtime.secondary();
let (etcd_config, nats_config) = config.dissolve();
let runtime_clone = runtime.clone();
let etcd_client = secondary
.spawn(async move {
let client = etcd::Client::new(etcd_config.clone(), runtime_clone)
.await
.context(format!(
"Failed to connect to etcd server with config {:?}",
etcd_config
))?;
OK(client)
})
.await??;
let nats_client = secondary
.spawn(async move {
let client = nats_config.clone().connect().await.context(format!(
"Failed to connect to NATS server with config {:?}",
nats_config
))?;
anyhow::Ok(client)
})
.await??;
Ok(Self {
runtime,
etcd_client,
nats_client,
tcp_server: Arc::new(OnceCell::new()),
component_registry: component::Registry::new(),
})
}
pub async fn from_settings(runtime: Runtime) -> Result<Self> {
let config = DistributedConfig::from_settings();
Self::new(runtime, config).await
}
pub fn runtime(&self) -> &Runtime {
&self.runtime
}
pub fn primary_lease(&self) -> etcd::Lease {
self.etcd_client.primary_lease()
}
pub fn shutdown(&self) {
self.runtime.shutdown();
}
/// Create a [`Namespace`]
pub fn namespace(&self, name: impl Into<String>) -> Result<Namespace> {
Namespace::new(self.clone(), name.into())
}
// /// Create a [`Component`]
// pub fn component(
// &self,
// name: impl Into<String>,
// namespace: impl Into<String>,
// ) -> Result<Component> {
// Ok(ComponentBuilder::from_runtime(self.clone())
// .name(name.into())
// .namespace(namespace.into())
// .build()?)
// }
pub(crate) fn discovery_client(&self, namespace: impl Into<String>) -> DiscoveryClient {
DiscoveryClient::new(namespace.into(), self.etcd_client.clone())
}
pub(crate) fn service_client(&self) -> ServiceClient {
ServiceClient::new(self.nats_client.clone())
}
pub(crate) async fn tcp_server(&self) -> Result<Arc<tcp::server::TcpStreamServer>> {
Ok(self
.tcp_server
.get_or_try_init(async move {
let options = tcp::server::ServerOptions::default();
let server = tcp::server::TcpStreamServer::new(options).await?;
OK(server)
})
.await?
.clone())
}
pub fn nats_client(&self) -> nats::Client {
self.nats_client.clone()
}
pub fn etcd_client(&self) -> etcd::Client {
self.etcd_client.clone()
}
}
#[derive(Dissolve)]
pub struct DistributedConfig {
pub etcd_config: etcd::ClientOptions,
pub nats_config: nats::ClientOptions,
}
impl DistributedConfig {
pub fn from_settings() -> DistributedConfig {
DistributedConfig {
etcd_config: etcd::ClientOptions::default(),
nats_config: nats::ClientOptions::default(),
}
}
}
/*
* Copyright 2024-2025 NVIDIA CORPORATION & AFFILIATES
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
use std::{fmt::Debug, future::Future, pin::Pin, sync::Arc};
pub use async_trait::async_trait;
use futures::stream::Stream;
/// All [`Send`] + [`Sync`] + `'static` types can be used as [`AsyncEngine`] request and response types.
pub trait Data: Send + Sync + 'static {}
impl<T: Send + Sync + 'static> Data for T {}
/// [`DataStream`] is a type alias for a stream of [`Data`] items. This can be adapted to a [`ResponseStream`]
/// by associating it with a [`AsyncEngineContext`].
pub type DataUnary<T> = Pin<Box<dyn Future<Output = T> + Send + Sync>>;
pub type DataStream<T> = Pin<Box<dyn Stream<Item = T> + Send + Sync>>;
pub type Engine<Req, Resp, E> = Arc<dyn AsyncEngine<Req, Resp, E>>;
pub type EngineUnary<Resp> = Pin<Box<dyn AsyncEngineUnary<Resp>>>;
pub type EngineStream<Resp> = Pin<Box<dyn AsyncEngineStream<Resp>>>;
pub type Context = Arc<dyn AsyncEngineContext>;
impl<T: Data> From<EngineStream<T>> for DataStream<T> {
fn from(stream: EngineStream<T>) -> Self {
Box::pin(stream)
}
}
// The Controller and the Context when https://github.com/rust-lang/rust/issues/65991 becomes stable
pub trait AsyncEngineController: Send + Sync {}
/// The [`AsyncEngineContext`] trait defines the interface to control the resulting stream
/// produced by the engine.
#[async_trait]
pub trait AsyncEngineContext: Send + Sync + Debug {
/// Unique ID for the Stream
fn id(&self) -> &str;
/// Returns true if `stop_generating()` has been called; otherwise, false.
fn is_stopped(&self) -> bool;
/// Returns true if `kill()` has been called; otherwise, false.
/// This can be used with a `.take_while()` stream combinator to immediately terminate
/// the stream.
///
/// An ideal location for a `[.take_while(!ctx.is_killed())]` stream combinator is on
/// the most downstream return stream.
fn is_killed(&self) -> bool;
/// Calling this method when [`AsyncEngineContext::is_stopped`] is `true` will return
/// immediately; otherwise, it will [`AsyncEngineContext::is_stopped`] will return true.
async fn stopped(&self);
/// Calling this method when [`AsyncEngineContext::is_killed`] is `true` will return
/// immediately; otherwise, it will [`AsyncEngineContext::is_killed`] will return true.
async fn killed(&self);
// Controller
/// Informs the [`AsyncEngine`] to stop producing results for this particular stream.
/// This method is idempotent. This method does not invalidate results current in the
/// stream. It might take some time for the engine to stop producing results. The caller
/// can decided to drain the stream or drop the stream.
fn stop_generating(&self);
/// See [`AsyncEngineContext::stop_generating`].
fn stop(&self);
/// Extends the [`AsyncEngineContext::stop_generating`] also indicates a preference to
/// terminate without draining the remaining items in the stream. This is implementation
/// specific and may not be supported by all engines.
fn kill(&self);
}
pub trait AsyncEngineContextProvider: Send + Sync + Debug {
fn context(&self) -> Arc<dyn AsyncEngineContext>;
}
pub trait AsyncEngineUnary<Resp: Data>:
Future<Output = Resp> + AsyncEngineContextProvider + Send + Sync
{
}
pub trait AsyncEngineStream<Resp: Data>:
Stream<Item = Resp> + AsyncEngineContextProvider + Send + Sync
{
}
/// Engine is a trait that defines the interface for a steaming LLM completion engine.
/// The synchronous Engine version is does not need to be awaited.
#[async_trait]
pub trait AsyncEngine<Req: Data, Resp: Data + AsyncEngineContextProvider, E: Data>:
Send + Sync
{
/// Generate a stream of completion responses.
async fn generate(&self, request: Req) -> Result<Resp, E>;
}
/// Adapter for a [`DataStream`] to a [`ResponseStream`].
///
/// A common pattern is to consume the [`ResponseStream`] with standard stream combinators
/// which produces a [`DataStream`] stream, then form a [`ResponseStream`] by propagating the
/// original [`AsyncEngineContext`].
pub struct ResponseStream<R: Data> {
stream: DataStream<R>,
ctx: Arc<dyn AsyncEngineContext>,
}
impl<R: Data> ResponseStream<R> {
pub fn new(stream: DataStream<R>, ctx: Arc<dyn AsyncEngineContext>) -> Pin<Box<Self>> {
Box::pin(Self { stream, ctx })
}
}
impl<R: Data> Stream for ResponseStream<R> {
type Item = R;
#[inline]
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
Pin::new(&mut self.stream).poll_next(cx)
}
}
impl<R: Data> AsyncEngineStream<R> for ResponseStream<R> {}
impl<R: Data> AsyncEngineContextProvider for ResponseStream<R> {
fn context(&self) -> Arc<dyn AsyncEngineContext> {
self.ctx.clone()
}
}
impl<R: Data> Debug for ResponseStream<R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ResponseStream")
// todo: add debug for stream - possibly propagate some information about what
// engine created the stream
// .field("stream", &self.stream)
.field("ctx", &self.ctx)
.finish()
}
}
impl<T: Data> AsyncEngineContextProvider for Pin<Box<dyn AsyncEngineUnary<T>>> {
fn context(&self) -> Arc<dyn AsyncEngineContext> {
AsyncEngineContextProvider::context(&**self)
}
}
impl<T: Data> AsyncEngineContextProvider for Pin<Box<dyn AsyncEngineStream<T>>> {
fn context(&self) -> Arc<dyn AsyncEngineContext> {
AsyncEngineContextProvider::context(&**self)
}
}
/*
* Copyright 2024-2025 NVIDIA CORPORATION & AFFILIATES
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
//! Triton
#![allow(dead_code)]
#![allow(unused_imports)]
use std::sync::{Arc, Mutex};
pub use anyhow::{anyhow as error, Context as ErrorContext, Error, Ok as OK, Result};
use async_once_cell::OnceCell;
use tracing as log;
mod config;
pub use config::RuntimeConfig;
pub mod component;
pub mod discovery;
pub mod engine;
pub mod pipeline;
pub mod protocols;
pub mod runtime;
pub mod service;
pub mod transports;
pub mod worker;
pub mod distributed;
pub use tokio_util::sync::CancellationToken;
pub use worker::Worker;
/// Types of Tokio runtimes that can be used to construct a Triton [Runtime].
#[derive(Clone)]
enum RuntimeType {
Shared(Arc<tokio::runtime::Runtime>),
External(tokio::runtime::Handle),
}
/// Local [Runtime] which provides access to shared resources local to the physical node/machine.
#[derive(Debug, Clone)]
pub struct Runtime {
id: Arc<String>,
primary: RuntimeType,
secondary: Arc<tokio::runtime::Runtime>,
cancellation_token: CancellationToken,
}
/// Distributed [Runtime] which provides access to shared resources across the cluster, this includes
/// communication protocols and transports.
#[derive(Clone)]
pub struct DistributedRuntime {
// local runtime
runtime: Runtime,
// we might consider a unifed transport manager here
etcd_client: transports::etcd::Client,
nats_client: transports::nats::Client,
tcp_server: Arc<OnceCell<Arc<transports::tcp::server::TcpStreamServer>>>,
// local registry for components
// the registry allows us to use share runtime resources across instances of the same component object.
// take fo example two instances of a client to the same remote component. The registry allows us to use
// a single endpoint watcher for both clients, this keeps the number background tasking watching specific
// paths in etcd to a minimum.
component_registry: component::Registry,
}
/*
* Copyright 2024-2025 NVIDIA CORPORATION & AFFILIATES
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
/// In a Pipeline, the [`AsyncEngine`] is constrained to take a [`Context`] as input and return
/// a [`super::engine::ResponseStream`] as output.
use serde::{Deserialize, Serialize};
mod nodes;
pub use nodes::{
Operator, PipelineNode, PipelineOperator, SegmentSink, SegmentSource, Service, ServiceBackend,
ServiceFrontend, Sink, Source,
};
pub mod context;
pub mod error;
pub mod network;
pub mod registry;
pub use crate::engine::{
self as engine, async_trait, AsyncEngine, AsyncEngineContext, AsyncEngineContextProvider, Data,
DataStream, Engine, EngineStream, EngineUnary, ResponseStream,
};
pub use anyhow::Error;
pub use context::Context;
pub use error::{PipelineError, PipelineErrorExt, TwoPartCodecError};
/// Pipeline inputs carry a [`Context`] which can be used to carry metadata or additional information
/// about the request. This information propagates through the stages, both local and distributed.
pub type SingleIn<T> = Context<T>;
/// Pipeline inputs carry a [`Context`] which can be used to carry metadata or additional information
/// about the request. This information propagates through the stages, both local and distributed.
pub type ManyIn<T> = Context<DataStream<T>>;
/// Type alias for the output of pipeline that returns a single value
pub type SingleOut<T> = EngineUnary<T>;
/// Type alias for the output of pipeline that returns multiple values
pub type ManyOut<T> = EngineStream<T>;
pub type ServiceEngine<T, U> = Engine<T, U, Error>;
/// Unary Engine is a pipeline that takes a single input and returns a single output
pub type UnaryEngine<T, U> = ServiceEngine<SingleIn<T>, SingleOut<U>>;
/// `ClientStreaming` Engine is a pipeline that takes multiple inputs and returns a single output
/// Typically the engine will consume the entire input stream; however, it can also decided to exit
/// early and emit a response without consuming the entire input stream.
pub type ClientStreamingEngine<T, U> = ServiceEngine<ManyIn<T>, SingleOut<U>>;
/// `ServerStreaming` takes a single input and returns multiple outputs.
pub type ServerStreamingEngine<T, U> = ServiceEngine<SingleIn<T>, ManyOut<U>>;
/// `BidirectionalStreaming` takes multiple inputs and returns multiple outputs. Input and output values
/// are considered independent of each other; however, they could be constrained to be related.
pub type BidirectionalStreamingEngine<T, U> = ServiceEngine<ManyIn<T>, ManyOut<U>>;
pub trait AsyncTransportEngine<T: PipelineIO, U: PipelineIO>:
AsyncEngine<T, U, Error> + Send + Sync + 'static
{
}
// pub type TransportEngine<T, U> = Arc<dyn AsyncTransportEngine<T, U>>;
mod sealed {
use super::*;
#[allow(dead_code)]
pub struct Token;
pub trait Connectable {
type DataType: Data;
}
impl<T: Data> Connectable for Context<T> {
type DataType = T;
}
impl<T: Data> Connectable for EngineUnary<T> {
type DataType = T;
}
impl<T: Data> Connectable for EngineStream<T> {
type DataType = T;
}
}
pub trait PipelineIO: Data + sealed::Connectable + AsyncEngineContextProvider {
fn id(&self) -> String;
}
impl<T: Data> PipelineIO for Context<T> {
fn id(&self) -> String {
self.id().to_string()
}
}
impl<T: Data> PipelineIO for EngineUnary<T> {
fn id(&self) -> String {
self.context().id().to_string()
}
}
impl<T: Data> PipelineIO for EngineStream<T> {
fn id(&self) -> String {
self.context().id().to_string()
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Event {
pub id: String,
}
/*
* Copyright 2024-2025 NVIDIA CORPORATION & AFFILIATES
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
//! Context Module
//!
//! There are two context object defined in this module:
//!
//! - [`Context`] is an input context which is propagated through the processing pipeline,
//! up to the point where the input is pass to an [`nim_llm_async_engine::AsyncEngine`] for processing.
//! - [`StreamContext`] is the input context transformed into to a type erased context that maintains the inputs
//! registry and visitors. `StreamAdaptors` will amend themselves to the [`StreamContext`] to allow for the
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use super::{AsyncEngineContext, AsyncEngineContextProvider, Data};
use crate::engine::AsyncEngineController;
use async_trait::async_trait;
use super::registry::Registry;
pub struct Context<T: Data> {
current: T,
controller: Arc<Controller>, //todo: hold this as an arc
registry: Registry,
stages: Vec<String>,
}
impl<T: Send + Sync + 'static> Context<T> {
// Create a new context with initial data
pub fn new(current: T) -> Self {
Context {
current,
controller: Arc::new(Controller::default()),
registry: Registry::new(),
stages: Vec::new(),
}
}
pub fn with_controller(current: T, controller: Controller) -> Self {
Context {
current,
controller: Arc::new(controller),
registry: Registry::new(),
stages: Vec::new(),
}
}
pub fn with_id(current: T, id: String) -> Self {
Context {
current,
controller: Arc::new(Controller::new(id)),
registry: Registry::new(),
stages: Vec::new(),
}
}
pub fn id(&self) -> &str {
self.controller.id()
}
pub fn controller(&self) -> &Controller {
&self.controller
}
/// Insert an object into the registry with a specific key.
pub fn insert<K: ToString, U: Send + Sync + 'static>(&mut self, key: K, value: U) {
self.registry.insert_shared(key, value);
}
/// Insert a unique and takable object into the registry with a specific key.
pub fn insert_unique<K: ToString, U: Send + Sync + 'static>(&mut self, key: K, value: U) {
self.registry.insert_unique(key, value);
}
/// Retrieve an object from the registry by key and type.
pub fn get<V: Send + Sync + 'static>(&self, key: &str) -> Result<Arc<V>, String> {
self.registry.get_shared(key)
}
/// Clone a unique object from the registry by key and type.
pub fn clone_unique<V: Clone + Send + Sync + 'static>(&self, key: &str) -> Result<V, String> {
self.registry.clone_unique(key)
}
/// Take a unique object from the registry by key and type.
pub fn take_unique<V: Send + Sync + 'static>(&mut self, key: &str) -> Result<V, String> {
self.registry.take_unique(key)
}
/// Transfer the Context to a new Object without updating the registry
/// This returns a tuple of the previous object and the new Context
pub fn transfer<U: Send + Sync + 'static>(self, new_current: U) -> (T, Context<U>) {
(
self.current,
Context {
current: new_current,
controller: self.controller,
registry: self.registry,
stages: self.stages,
},
)
}
/// Separate out the current object and context
pub fn into_parts(self) -> (T, Context<()>) {
self.transfer(())
}
pub fn stages(&self) -> &Vec<String> {
&self.stages
}
pub fn add_stage(&mut self, stage: &str) {
self.stages.push(stage.to_string());
}
/// Transforms the current context to another type using a provided function.
pub fn map<U: Send + Sync + 'static, F>(self, f: F) -> Context<U>
where
F: FnOnce(T) -> U,
{
// Use the transfer method to move the current value out
let (current, temp_context) = self.transfer(());
// Apply the transformation function to the current value
let new_current = f(current);
// Use transfer again to create the new context with the transformed type
temp_context.transfer(new_current).1
}
pub fn try_map<U, F, E>(self, f: F) -> Result<Context<U>, E>
where
F: FnOnce(T) -> Result<U, E>,
U: Send + Sync + 'static,
{
// Use the transfer method to move the current value out
let (current, temp_context) = self.transfer(());
// Apply the transformation function to the current value
let new_current = f(current)?;
// Use transfer again to create the new context with the transformed type
Ok(temp_context.transfer(new_current).1)
}
}
impl<T: Data> std::fmt::Debug for Context<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Context")
.field("id", &self.controller.id())
.finish()
}
}
// Implement Deref to allow Context<T> to act like &T
impl<T: Data> Deref for Context<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.current
}
}
// Implement DerefMut to allow Context<T> to act like &mut T
impl<T: Data> DerefMut for Context<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.current
}
}
// Implement the custom trait for Context<T>
impl<T> From<T> for Context<T>
where
T: Send + Sync + 'static,
{
fn from(current: T) -> Self {
Context::new(current)
}
}
// Define a custom trait for conversion from Context<T> to Context<U>
pub trait IntoContext<U: Data> {
fn into_context(self) -> Context<U>;
}
// Implement the custom trait for converting Context<T> to Context<U>
impl<T, U> IntoContext<U> for Context<T>
where
T: Send + Sync + 'static + Into<U>,
U: Send + Sync + 'static,
{
fn into_context(self) -> Context<U> {
self.map(|current| current.into())
}
}
impl<T: Data> AsyncEngineContextProvider for Context<T> {
fn context(&self) -> Arc<dyn AsyncEngineContext> {
self.controller.clone()
}
}
#[derive(Debug, Clone)]
pub struct StreamContext {
controller: Arc<Controller>,
registry: Arc<Registry>,
stages: Vec<String>,
}
impl StreamContext {
fn new(controller: Arc<Controller>, registry: Registry) -> Self {
StreamContext {
controller,
registry: Arc::new(registry),
stages: Vec::new(),
}
}
/// Retrieve an object from the registry by key and type.
pub fn get<V: Send + Sync + 'static>(&self, key: &str) -> Result<Arc<V>, String> {
self.registry.get_shared(key)
}
/// Clone a unique object from the registry by key and type.
pub fn clone_unique<V: Clone + Send + Sync + 'static>(&self, key: &str) -> Result<V, String> {
self.registry.clone_unique(key)
}
pub fn registry(&self) -> Arc<Registry> {
self.registry.clone()
}
pub fn stages(&self) -> &Vec<String> {
&self.stages
}
pub fn add_stage(&mut self, stage: &str) {
self.stages.push(stage.to_string());
}
}
#[async_trait]
impl AsyncEngineContext for StreamContext {
fn id(&self) -> &str {
self.controller.id()
}
fn stop(&self) {
self.controller.stop();
}
fn kill(&self) {
self.controller.kill();
}
fn stop_generating(&self) {
self.controller.stop_generating();
}
fn is_stopped(&self) -> bool {
self.controller.is_stopped()
}
fn is_killed(&self) -> bool {
self.controller.is_killed()
}
async fn stopped(&self) {
self.controller.stopped().await
}
async fn killed(&self) {
self.controller.killed().await
}
}
impl AsyncEngineContextProvider for StreamContext {
fn context(&self) -> Arc<dyn AsyncEngineContext> {
self.controller.clone()
}
}
impl<T: Send + Sync + 'static> From<Context<T>> for StreamContext {
fn from(value: Context<T>) -> Self {
StreamContext::new(value.controller, value.registry)
}
}
// TODO - refactor here - this came from the nim-llm-async-engine crate
use tokio::sync::watch::{channel, Receiver, Sender};
#[derive(Debug, Eq, PartialEq)]
enum State {
Live,
Stopped,
Killed,
}
/// A context implementation with cancellation propagation.
#[derive(Debug)]
pub struct Controller {
id: String,
tx: Sender<State>,
rx: Receiver<State>,
}
impl Controller {
pub fn new(id: String) -> Self {
let (tx, rx) = channel(State::Live);
Self { id, tx, rx }
}
pub fn id(&self) -> &str {
&self.id
}
}
impl Default for Controller {
fn default() -> Self {
Self::new(uuid::Uuid::new_v4().to_string())
}
}
impl AsyncEngineController for Controller {}
#[async_trait]
impl AsyncEngineContext for Controller {
fn id(&self) -> &str {
&self.id
}
fn is_stopped(&self) -> bool {
*self.rx.borrow() != State::Live
}
fn is_killed(&self) -> bool {
*self.rx.borrow() == State::Killed
}
async fn stopped(&self) {
let mut rx = self.rx.clone();
if *rx.borrow_and_update() != State::Live {
return;
}
let _ = rx.changed().await;
}
async fn killed(&self) {
let mut rx = self.rx.clone();
if *rx.borrow_and_update() == State::Killed {
return;
}
let _ = rx.changed().await;
}
fn stop_generating(&self) {
self.stop();
}
fn stop(&self) {
let _ = self.tx.send(State::Stopped);
}
fn kill(&self) {
let _ = self.tx.send(State::Killed);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone)]
struct Input {
value: String,
}
#[derive(Debug, Clone)]
struct Processed {
length: usize,
}
#[derive(Debug, Clone)]
struct Final {
message: String,
}
impl From<Input> for Processed {
fn from(input: Input) -> Self {
Processed {
length: input.value.len(),
}
}
}
impl From<Processed> for Final {
fn from(processed: Processed) -> Self {
Final {
message: format!("Processed length: {}", processed.length),
}
}
}
#[test]
fn test_insert_and_get() {
let mut ctx = Context::new(Input {
value: "Hello".to_string(),
});
ctx.insert("key1", 42);
ctx.insert("key2", "some data".to_string());
assert_eq!(*ctx.get::<i32>("key1").unwrap(), 42);
assert_eq!(*ctx.get::<String>("key2").unwrap(), "some data");
assert!(ctx.get::<f64>("key1").is_err()); // Testing a downcast failure
}
#[test]
fn test_transfer() {
let ctx = Context::new(Input {
value: "Hello".to_string(),
});
let (input, ctx) = ctx.transfer(Processed { length: 5 });
assert_eq!(input.value, "Hello");
assert_eq!(ctx.length, 5);
}
#[test]
fn test_map() {
let ctx = Context::new(Input {
value: "Hello".to_string(),
});
let ctx: Context<Processed> = ctx.map(|input| input.into());
let ctx: Context<Final> = ctx.map(|processed| processed.into());
assert_eq!(ctx.current.message, "Processed length: 5");
}
#[test]
fn test_into_context() {
let ctx = Context::new(Input {
value: "Hello".to_string(),
});
let ctx: Context<Processed> = ctx.into_context();
let ctx: Context<Final> = ctx.into_context();
assert_eq!(ctx.current.message, "Processed length: 5");
}
}
/*
* Copyright 2024-2025 NVIDIA CORPORATION & AFFILIATES
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
//! Pipeline Error
//
use async_nats::error::Error as NatsError;
pub use anyhow::{anyhow, anyhow as error, bail, ensure, Context, Error, Result};
pub trait PipelineErrorExt {
/// Downcast the [`Error`] to a [`PipelineError`]
fn try_into_pipeline_error(self) -> Result<PipelineError, Error>;
/// If the [`Error`] can be downcast to a [`PipelineError`], then the left variant is returned,
/// otherwise the right variant is returned.
fn either_pipeline_error(self) -> either::Either<PipelineError, Error>;
}
impl PipelineErrorExt for Error {
fn try_into_pipeline_error(self) -> Result<PipelineError, Error> {
self.downcast::<PipelineError>()
}
fn either_pipeline_error(self) -> either::Either<PipelineError, Error> {
match self.downcast::<PipelineError>() {
Ok(err) => either::Left(err),
Err(err) => either::Right(err),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum PipelineError {
/// For starter, to remove as code matures.
#[error("Generic error: {0}")]
Generic(String),
/// Edges can only be set once. This error is thrown on subsequent attempts to set an edge.s
#[error("Link failed: Edge already set")]
EdgeAlreadySet,
/// The source node is not connected to an edge.
#[error("Disconnected source; no edge on which to send data")]
NoEdge,
#[error("SegmentSink is not connected to an EgressPort")]
NoNetworkEdge,
/// In the interim between when a request was made and when the stream was received, the
/// requesting task was dropped. This maybe a logic error in the pipeline; and become a
/// panic/fatal error in the future. This error is thrown when the `on_data` method of a
/// terminating sink either cannot find the `oneshot` channel sender or the corresponding
/// receiver was dropped
#[error("Unlinked request; initiating request task was dropped or cancelled")]
DetatchedStreamReceiver,
// In the interim between when a response was made and when the stream was received, the
// Sender for the stream was dropped. This maybe a logic error in the pipeline; and become a
// panic/fatal error in the future.
#[error("Unlinked response; response task was dropped or cancelled")]
DetatchedStreamSender,
#[error("Serialzation Error: {0}")]
SerializationError(String),
#[error("Deserialization Error: {0}")]
DeserializationError(String),
#[error("Failed to issue request to the control plane: {0}")]
ControlPlaneRequestError(String),
#[error("Failed to establish a streaming connection: {0}")]
ConnectionFailed(String),
#[error("Generate Error: {0}")]
GenerateError(Error),
#[error("NATS Request Error: {0}")]
NatsRequestError(#[from] NatsError<async_nats::jetstream::context::RequestErrorKind>),
#[error("NATS Get Stream Error: {0}")]
NatsGetStreamError(#[from] NatsError<async_nats::jetstream::context::GetStreamErrorKind>),
#[error("NATS Create Stream Error: {0}")]
NatsCreateStreamError(#[from] NatsError<async_nats::jetstream::context::CreateStreamErrorKind>),
#[error("NATS Consumer Error: {0}")]
NatsConsumerError(#[from] NatsError<async_nats::jetstream::stream::ConsumerErrorKind>),
#[error("NATS Batch Error: {0}")]
NatsBatchError(#[from] NatsError<async_nats::jetstream::consumer::pull::BatchErrorKind>),
#[error("NATS Publish Error: {0}")]
NatsPublishError(#[from] NatsError<async_nats::client::PublishErrorKind>),
#[error("NATS Connect Error: {0}")]
NatsConnectError(#[from] NatsError<async_nats::ConnectErrorKind>),
#[error("NATS Subscriber Error: {0}")]
NatsSubscriberError(#[from] async_nats::SubscribeError),
#[error("Local IP Address Error: {0}")]
LocalIpAddressError(#[from] local_ip_address::Error),
#[error("Prometheus Error: {0}")]
PrometheusError(#[from] prometheus::Error),
#[error("Other NATS Error: {0}")]
NatsError(#[from] Box<dyn std::error::Error + Send + Sync>),
#[error("Two Part Codec Error: {0}")]
TwoPartCodec(#[from] TwoPartCodecError),
#[error("Serde Json Error: {0}")]
SerdeJsonError(#[from] serde_json::Error),
#[error("NATS KV Err: {0} for bucket '{1}")]
KeyValueError(String, String),
}
#[derive(Debug, thiserror::Error)]
pub enum TwoPartCodecError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Message size {0} exceeds the maximum allowed size of {1} bytes")]
MessageTooLarge(usize, usize),
#[error("Invalid message: {0}")]
InvalidMessage(String),
#[error("Checksum mismatch")]
ChecksumMismatch,
}
/*
* Copyright 2024-2025 NVIDIA CORPORATION & AFFILIATES
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
//! TODO - we need to reconcile what is in this crate with distributed::transports
pub mod codec;
pub mod egress;
pub mod ingress;
pub mod tcp;
use std::sync::{Arc, OnceLock};
use anyhow::Result;
use async_trait::async_trait;
use bytes::Bytes;
use codec::{TwoPartCodec, TwoPartMessage, TwoPartMessageType};
use derive_builder::Builder;
use futures::StreamExt;
// io::Cursor, TryStreamExt
use super::{AsyncEngine, AsyncEngineContext, AsyncEngineContextProvider, ResponseStream};
use serde::{Deserialize, Serialize};
use super::{
context, AsyncTransportEngine, Context, Data, Error, ManyOut, PipelineError, PipelineIO,
SegmentSource, ServiceBackend, ServiceEngine, SingleIn, Source,
};
pub trait Codable: PipelineIO + Serialize + for<'de> Deserialize<'de> {}
impl<T: PipelineIO + Serialize + for<'de> Deserialize<'de>> Codable for T {}
/// `WorkQueueConsumer` is a generic interface for a work queue that can be used to send and receive
#[async_trait]
pub trait WorkQueueConsumer {
async fn dequeue(&self) -> Result<Bytes, String>;
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum StreamType {
Request,
Response,
}
/// This is the first message in a `ResponseStream`. This is not a message that gets process
/// by the general pipeline, but is a control message that is awaited before the
/// [`AsyncEngine::generate`] method is allowed to return.
///
/// If an error is present, the [`AsyncEngine::generate`] method will return the error instead
/// of returning the `ResponseStream`.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ResponseStreamPrologue {
error: Option<String>,
}
pub type StreamProvider<T> = tokio::sync::oneshot::Receiver<Result<T, String>>;
/// The [`RegisteredStream`] object is acquired from a [`StreamProvider`] and is used to provide
/// an awaitable receiver which will the `T` which is either a stream writer for a request stream
/// or a stream reader for a response stream.
///
/// make this an raii object linked to some stream provider
/// if the object has not been awaited an the type T unwrapped, the registered stream
/// on the stream provider will be informed and can clean up a stream that will never
/// be connected.
#[derive(Debug)]
pub struct RegisteredStream<T> {
pub connection_info: ConnectionInfo,
pub stream_provider: StreamProvider<T>,
}
impl<T> RegisteredStream<T> {
pub fn into_parts(self) -> (ConnectionInfo, StreamProvider<T>) {
(self.connection_info, self.stream_provider)
}
}
/// After registering a stream, the [`PendingConnections`] object is returned to the caller. This
/// object can be used to await the connection to be established.
pub struct PendingConnections {
pub send_stream: Option<RegisteredStream<StreamSender>>,
pub recv_stream: Option<RegisteredStream<StreamReceiver>>,
}
impl PendingConnections {
pub fn into_parts(
self,
) -> (
Option<RegisteredStream<StreamSender>>,
Option<RegisteredStream<StreamReceiver>>,
) {
(self.send_stream, self.recv_stream)
}
}
/// A [`ResponseService`] implements a services in which a context a specific subject with will
/// be associated with a stream of responses. The key difference between a [`ResponseService`]
/// and a [`RequestService`] is that the [`ResponseService`] is the awaits an explicit connection
/// to be established, where as a [`RequestService`] has no known knowledge about incoming
/// connections. All [`ResponseService`] connections are expected, all [`RequestService`] connections
/// are unexpected.
#[async_trait::async_trait]
pub trait ResponseService {
async fn register(&self, options: StreamOptions) -> PendingConnections;
}
// #[derive(Debug, Clone, Serialize, Deserialize)]
// struct Handshake {
// request_id: String,
// worker_id: Option<String>,
// error: Option<String>,
// }
// impl Handshake {
// pub fn validate(&self) -> Result<(), String> {
// if let Some(e) = &self.error {
// return Err(e.clone());
// }
// Ok(())
// }
// }
// this probably needs to be come a ResponseStreamSender
// since the prologue in this scenario sender telling the receiver
// that all is good and it's ready to send
//
// in the RequestStreamSender, the prologue would be coming from the
// receiver, so the sender would have to await the prologue which if
// was not an error, would indicate the RequestStreamReceiver is read
// to receive data.
pub struct StreamSender {
tx: tokio::sync::mpsc::Sender<TwoPartMessage>,
prologue: Option<ResponseStreamPrologue>,
}
impl StreamSender {
pub async fn send(&self, data: Bytes) -> Result<(), String> {
self.tx
.send(TwoPartMessage::from_data(data))
.await
.map_err(|e| e.to_string())
}
#[allow(clippy::needless_update)]
pub async fn send_prologue(&mut self, error: Option<String>) -> Result<(), String> {
if let Some(prologue) = self.prologue.take() {
let prologue = ResponseStreamPrologue { error, ..prologue };
self.tx
.send(TwoPartMessage::from_header(
serde_json::to_vec(&prologue).unwrap().into(),
))
.await
.map_err(|e| e.to_string())?;
} else {
panic!("Prologue already sent; or not set; logic error");
}
Ok(())
}
}
pub struct StreamReceiver {
rx: tokio::sync::mpsc::Receiver<Bytes>,
}
/// Connection Info is encoded as JSON and then again serialized has part of the Transport
/// Layer. The double serialization is not performance critical as it is only done once per
/// connection. The primary reason storing the ConnecitonInfo has a JSON string is for type
/// erasure. The Transport Layer will check the [`ConnectionInfo::transport`] type and then
/// route it to the appropriate instance of the Transport, which will then deserialize the
/// [`ConnectionInfo::info`] field to its internal connection info object.
///
/// Optionally, this object could become strongly typed for which all possible combinations
/// of transport and connection info would need to be enumerated.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectionInfo {
pub transport: String,
pub info: String,
}
/// When registering a new TransportStream on the server, the caller specifies if the
/// stream is a sender, receiver or both.
///
/// Senders and Receivers are with share a Context, but result in separate tcp socket
/// connections to the server. Internally, we may use bcast channels to coordinate the
/// internal control messages between the sender and receiver socket connections.
#[derive(Clone, Builder)]
pub struct StreamOptions {
/// Context
pub context: Arc<dyn AsyncEngineContext>,
/// Register with the server that this connection will have a server-side Sender
/// that can be picked up by the Request/Forward pipeline
///
/// TODO - note, this option is currently not implemented and will cause a panic
pub enable_request_stream: bool,
/// Register with the server that this connection will have a server-side Receiver
/// that can be picked up by the Response/Reverse pipeline
pub enable_response_stream: bool,
/// The number of messages to buffer before blocking
#[builder(default = "8")]
pub send_buffer_count: usize,
/// The number of messages to buffer before blocking
#[builder(default = "8")]
pub recv_buffer_count: usize,
}
impl StreamOptions {
pub fn builder() -> StreamOptionsBuilder {
StreamOptionsBuilder::default()
}
}
pub struct Egress<Req: PipelineIO, Resp: PipelineIO> {
transport_engine: Arc<dyn AsyncTransportEngine<Req, Resp>>,
}
#[async_trait]
impl<T: Data, U: Data> AsyncEngine<SingleIn<T>, ManyOut<U>, Error>
for Egress<SingleIn<T>, ManyOut<U>>
where
T: Data + Serialize,
U: for<'de> Deserialize<'de> + Data,
{
async fn generate(&self, request: SingleIn<T>) -> Result<ManyOut<U>, Error> {
self.transport_engine.generate(request).await
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
enum RequestType {
SingleIn,
ManyIn,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
enum ResponseType {
SingleOut,
ManyOut,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct RequestControlMessage {
id: String,
request_type: RequestType,
response_type: ResponseType,
connection_info: ConnectionInfo,
}
pub struct Ingress<Req: PipelineIO, Resp: PipelineIO> {
segment: OnceLock<Arc<SegmentSource<Req, Resp>>>,
}
impl<Req: PipelineIO, Resp: PipelineIO> Ingress<Req, Resp> {
pub fn new() -> Arc<Self> {
Arc::new(Self {
segment: OnceLock::new(),
})
}
pub fn attach(&self, segment: Arc<SegmentSource<Req, Resp>>) -> Result<()> {
self.segment
.set(segment)
.map_err(|_| anyhow::anyhow!("Segment already set"))
}
pub fn link(segment: Arc<SegmentSource<Req, Resp>>) -> Result<Arc<Self>> {
let ingress = Ingress::new();
ingress.attach(segment)?;
Ok(ingress)
}
pub fn for_pipeline(segment: Arc<SegmentSource<Req, Resp>>) -> Result<Arc<Self>> {
let ingress = Ingress::new();
ingress.attach(segment)?;
Ok(ingress)
}
pub fn for_engine(engine: ServiceEngine<Req, Resp>) -> Result<Arc<Self>> {
let frontend = SegmentSource::<Req, Resp>::new();
let backend = ServiceBackend::from_engine(engine);
// create the pipeline
let pipeline = frontend.link(backend)?.link(frontend)?;
let ingress = Ingress::new();
ingress.attach(pipeline)?;
Ok(ingress)
}
}
#[async_trait]
pub trait PushWorkHandler: Send + Sync {
async fn handle_payload(&self, payload: Bytes) -> Result<(), PipelineError>;
}
/*
* Copyright 2024-2025 NVIDIA CORPORATION & AFFILIATES
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
//! Codec Module
//!
//! Codec map structure into blobs of bytes and streams of bytes.
//!
//! In this module, we define three primary codec used to issue single, two-part or multi-part messages,
//! on a byte stream.
use tokio_util::{
bytes::{Buf, BufMut, BytesMut},
codec::{Decoder, Encoder},
};
mod two_part;
pub use two_part::{TwoPartCodec, TwoPartMessage, TwoPartMessageType};
// // Custom codec that reads a u64 length header and the message of that length
// #[derive(Default)]
// pub struct LengthPrefixedCodec;
// impl LengthPrefixedCodec {
// pub fn new() -> Self {
// LengthPrefixedCodec {}
// }
// }
// impl Decoder for LengthPrefixedCodec {
// type Item = Vec<u8>;
// type Error = tokio::io::Error;
// fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
// // Check if enough bytes are available to read the length (u64 = 8 bytes)
// if src.len() < 8 {
// return Ok(None); // Not enough data to read the length
// }
// // Read the u64 length header
// let len = src.get_u64() as usize;
// // Check if enough bytes are available to read the full message
// if src.len() < len {
// src.reserve(len - src.len()); // Reserve space for the remaining bytes
// return Ok(None);
// }
// // Read the actual message bytes of the specified length
// let data = src.split_to(len).to_vec();
// Ok(Some(data))
// }
// }
// impl Encoder<Vec<u8>> for LengthPrefixedCodec {
// type Error = tokio::io::Error;
// fn encode(&mut self, item: Vec<u8>, dst: &mut BytesMut) -> Result<(), Self::Error> {
// // Write the length of the message as a u64 header
// dst.put_u64(item.len() as u64);
// // Write the actual message bytes
// dst.put_slice(&item);
// Ok(())
// }
// }
/*
* Copyright 2024-2025 NVIDIA CORPORATION & AFFILIATES
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
use bytes::{Buf, BufMut, Bytes, BytesMut};
use tokio_util::codec::{Decoder, Encoder};
use xxhash_rust::xxh3::xxh3_64;
use crate::pipeline::error::TwoPartCodecError;
#[derive(Clone, Default)]
pub struct TwoPartCodec {
max_message_size: Option<usize>,
}
impl TwoPartCodec {
pub fn new(max_message_size: Option<usize>) -> Self {
TwoPartCodec { max_message_size }
}
/// Encodes a `TwoPartMessage` into `Bytes`, enforcing `max_message_size`.
pub fn encode_message(&self, msg: TwoPartMessage) -> Result<Bytes, TwoPartCodecError> {
let mut buf = BytesMut::new();
let mut codec = self.clone();
codec.encode(msg, &mut buf)?;
Ok(buf.freeze())
}
/// Decodes a `TwoPartMessage` from `Bytes`, enforcing `max_message_size`.
pub fn decode_message(&self, data: Bytes) -> Result<TwoPartMessage, TwoPartCodecError> {
let mut buf = BytesMut::from(&data[..]);
let mut codec = self.clone();
match codec.decode(&mut buf)? {
Some(msg) => Ok(msg),
None => Err(TwoPartCodecError::InvalidMessage(
"No message decoded".to_string(),
)),
}
}
}
impl Decoder for TwoPartCodec {
type Item = TwoPartMessage;
type Error = TwoPartCodecError;
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
// Need at least 24 bytes (header_len, body_len, checksum)
if src.len() < 24 {
return Ok(None);
}
// Use a cursor to read lengths and checksum without modifying the buffer
let mut cursor = &src[..];
let header_len = cursor.get_u64() as usize;
let body_len = cursor.get_u64() as usize;
let checksum = cursor.get_u64();
let total_len = 24 + header_len + body_len;
// Check if total_len exceeds max_message_size
if let Some(max_size) = self.max_message_size {
if total_len > max_size {
return Err(TwoPartCodecError::MessageTooLarge(total_len, max_size));
}
}
// Check if enough data is available
if src.len() < total_len {
return Ok(None);
}
// Advance the buffer past the lengths and checksum
src.advance(24);
let bytes_to_hash = header_len + body_len;
let data_to_hash = &src[..bytes_to_hash];
let computed_checksum = xxh3_64(data_to_hash);
// Compare checksums
if checksum != computed_checksum {
return Err(TwoPartCodecError::ChecksumMismatch);
}
// Read header and body data
let header = src.split_to(header_len).freeze();
let data = src.split_to(body_len).freeze();
Ok(Some(TwoPartMessage { header, data }))
}
}
impl Encoder<TwoPartMessage> for TwoPartCodec {
type Error = TwoPartCodecError;
fn encode(&mut self, item: TwoPartMessage, dst: &mut BytesMut) -> Result<(), Self::Error> {
let header_len = item.header.len();
let body_len = item.data.len();
let total_len = 24 + header_len + body_len; // 24 bytes for lengths and checksum
// Check if total_len exceeds max_message_size
if let Some(max_size) = self.max_message_size {
if total_len > max_size {
return Err(TwoPartCodecError::MessageTooLarge(total_len, max_size));
}
}
// Compute checksum of the data
let mut data_to_hash = BytesMut::with_capacity(header_len + body_len);
data_to_hash.extend_from_slice(&item.header);
data_to_hash.extend_from_slice(&item.data);
let checksum = xxh3_64(&data_to_hash);
// Write header and body sizes and checksum
dst.put_u64(header_len as u64);
dst.put_u64(body_len as u64);
dst.put_u64(checksum);
// Write header and body
dst.put_slice(&item.header);
dst.put_slice(&item.data);
Ok(())
}
}
pub enum TwoPartMessageType {
HeaderOnly(Bytes),
DataOnly(Bytes),
HeaderAndData(Bytes, Bytes),
Empty,
}
#[derive(Clone, Debug)]
pub struct TwoPartMessage {
pub header: Bytes,
pub data: Bytes,
}
impl TwoPartMessage {
pub fn new(header: Bytes, data: Bytes) -> Self {
TwoPartMessage { header, data }
}
pub fn from_header(header: Bytes) -> Self {
TwoPartMessage {
header,
data: Bytes::new(),
}
}
pub fn from_data(data: Bytes) -> Self {
TwoPartMessage {
header: Bytes::new(),
data,
}
}
pub fn from_parts(header: Bytes, data: Bytes) -> Self {
TwoPartMessage { header, data }
}
pub fn parts(&self) -> (&Bytes, &Bytes) {
(&self.header, &self.data)
}
pub fn optional_parts(&self) -> (Option<&Bytes>, Option<&Bytes>) {
(self.header(), self.data())
}
pub fn into_parts(self) -> (Bytes, Bytes) {
(self.header, self.data)
}
pub fn header(&self) -> Option<&Bytes> {
if self.header.is_empty() {
None
} else {
Some(&self.header)
}
}
pub fn data(&self) -> Option<&Bytes> {
if self.data.is_empty() {
None
} else {
Some(&self.data)
}
}
pub fn into_message_type(self) -> TwoPartMessageType {
if self.header.is_empty() && self.data.is_empty() {
TwoPartMessageType::Empty
} else if self.header.is_empty() {
TwoPartMessageType::DataOnly(self.data)
} else if self.data.is_empty() {
TwoPartMessageType::HeaderOnly(self.header)
} else {
TwoPartMessageType::HeaderAndData(self.header, self.data)
}
}
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use std::pin::Pin;
use std::task::{Context, Poll};
use bytes::{Bytes, BytesMut};
use futures::StreamExt;
use tokio::io::AsyncRead;
use tokio::io::ReadBuf;
use tokio_util::codec::{Decoder, FramedRead};
use super::*;
/// Test encoding and decoding of a message with both header and data.
#[test]
fn test_message_with_header_and_data() {
// Create a message with both header and data.
let header_data = Bytes::from("header data");
let data = Bytes::from("body data");
let message = TwoPartMessage::from_parts(header_data.clone(), data.clone());
let codec = TwoPartCodec::new(None);
// Encode the message.
let encoded = codec.encode_message(message).unwrap();
// Decode the message.
let decoded = codec.decode_message(encoded).unwrap();
// Verify the decoded message.
assert_eq!(decoded.header, header_data);
assert_eq!(decoded.data, data);
}
/// Test encoding and decoding of a message with only header.
#[test]
fn test_message_with_only_header() {
let header_data = Bytes::from("header only");
let message = TwoPartMessage::from_header(header_data.clone());
let codec = TwoPartCodec::new(None);
// Encode the message.
let encoded = codec.encode_message(message).unwrap();
// Decode the message.
let decoded = codec.decode_message(encoded).unwrap();
// Verify the decoded message.
assert_eq!(decoded.header, header_data);
assert!(decoded.data.is_empty());
}
/// Test encoding and decoding of a message with only data.
#[test]
fn test_message_with_only_data() {
let data = Bytes::from("data only");
let message = TwoPartMessage::from_data(data.clone());
let codec = TwoPartCodec::new(None);
// Encode the message.
let encoded = codec.encode_message(message).unwrap();
// Decode the message.
let decoded = codec.decode_message(encoded).unwrap();
// Verify the decoded message.
assert!(decoded.header.is_empty());
assert_eq!(decoded.data, data);
}
/// Test encoding and decoding of an empty message.
#[test]
fn test_empty_message() {
let message = TwoPartMessage::from_parts(Bytes::new(), Bytes::new());
let codec = TwoPartCodec::new(None);
// Encode the message.
let encoded = codec.encode_message(message).unwrap();
// Decode the message.
let decoded = codec.decode_message(encoded).unwrap();
// Verify the decoded message.
assert!(decoded.header.is_empty());
assert!(decoded.data.is_empty());
}
/// Test encoding and decoding of a message under max_message_size.
#[test]
fn test_message_under_max_size() {
let max_size = 1024; // Set max_message_size to 1024 bytes
// Create a message smaller than max_size
let header_data = Bytes::from(vec![b'h'; 100]);
let body_data = Bytes::from(vec![b'd'; 200]);
let message = TwoPartMessage::from_parts(header_data.clone(), body_data.clone());
let codec = TwoPartCodec::new(Some(max_size));
// Encode the message
let encoded = codec.encode_message(message.clone()).unwrap();
// Decode the message
let decoded = codec.decode_message(encoded).unwrap();
// Verify the decoded message
assert_eq!(decoded.header, header_data);
assert_eq!(decoded.data, body_data);
}
/// Test encoding and decoding of a message exactly at max_message_size.
#[test]
fn test_message_exactly_at_max_size() {
let max_size = 1024; // Set max_message_size to 1024 bytes
// Calculate the sizes
let lengths_size = 24; // 8 bytes for header_len, 8 bytes for body_len, 8 bytes for checksum
let data_size = max_size - lengths_size; // Total data size to reach max_size
// Split data_size between header and body
let header_size = data_size / 2;
let body_size = data_size - header_size;
// Create header and body data
let header_data = Bytes::from(vec![b'h'; header_size]);
let body_data = Bytes::from(vec![b'd'; body_size]);
let message = TwoPartMessage::from_parts(header_data.clone(), body_data.clone());
let codec = TwoPartCodec::new(Some(max_size));
// Encode the message
let encoded = codec.encode_message(message.clone()).unwrap();
// The length of encoded should be exactly max_size
assert_eq!(encoded.len(), max_size);
// Decode the message
let decoded = codec.decode_message(encoded).unwrap();
// Verify the decoded message
assert_eq!(decoded.header, header_data);
assert_eq!(decoded.data, body_data);
}
/// Test encoding of a message over max_message_size.
#[test]
fn test_message_over_max_size() {
let max_size = 1024; // Set max_message_size to 1024 bytes
// Create a message larger than max_size
let data_size = max_size - 24 + 1; // Exceed max_size by 1 byte
let header_size = data_size / 2;
let body_size = data_size - header_size;
let header_data = Bytes::from(vec![b'h'; header_size]);
let body_data = Bytes::from(vec![b'd'; body_size]);
let message = TwoPartMessage::from_parts(header_data, body_data);
let codec = TwoPartCodec::new(Some(max_size));
// Attempt to encode the message
let result = codec.encode_message(message);
// Expect an error
assert!(result.is_err());
// Verify the error is MessageTooLarge
if let Err(TwoPartCodecError::MessageTooLarge(size, max)) = result {
assert_eq!(size, data_size + 24); // Total size including lengths and checksum
assert_eq!(max, max_size);
} else {
panic!("Expected MessageTooLarge error");
}
}
/// Test decoding of a message over max_message_size.
#[test]
fn test_decoding_message_over_max_size() {
let max_size = 1024; // Set max_message_size to 1024 bytes
// Create a message larger than max_size
let data_size = max_size - 24 + 1; // Exceed max_size by 1 byte
let header_size = data_size / 2;
let body_size = data_size - header_size;
let header_data = Bytes::from(vec![b'h'; header_size]);
let body_data = Bytes::from(vec![b'd'; body_size]);
let message = TwoPartMessage::from_parts(header_data.clone(), body_data.clone());
let codec = TwoPartCodec::new(None); // No size limit during encoding
// Encode the message
let encoded = codec.encode_message(message).unwrap();
let codec_with_limit = TwoPartCodec::new(Some(max_size));
// Attempt to decode the message with max_message_size limit
let result = codec_with_limit.decode_message(encoded);
// Expect an error
assert!(result.is_err());
// Verify the error is MessageTooLarge
if let Err(TwoPartCodecError::MessageTooLarge(size, max)) = result {
assert_eq!(size, data_size + 24); // Total size including lengths and checksum
assert_eq!(max, max_size);
} else {
panic!("Expected MessageTooLarge error");
}
}
/// Test decoding of a message with checksum mismatch.
#[test]
fn test_checksum_mismatch() {
// Create a message
let header_data = Bytes::from("header data");
let data = Bytes::from("body data");
let message = TwoPartMessage::from_parts(header_data.clone(), data.clone());
let codec = TwoPartCodec::new(None);
// Encode the message
let encoded = codec.encode_message(message).unwrap();
// Corrupt the data to cause checksum mismatch
let mut encoded = BytesMut::from(encoded);
let len = encoded.len();
encoded[len - 1] ^= 0xFF; // Flip the last byte
// Attempt to decode
let result = codec.decode_message(encoded.into());
// Expect an error
assert!(result.is_err());
// Verify the error is ChecksumMismatch
if let Err(TwoPartCodecError::ChecksumMismatch) = result {
// Test passed
} else {
panic!("Expected ChecksumMismatch error");
}
}
/// Test partial data arrival and ensure decoder waits for full message.
#[test]
fn test_partial_data() {
let header_data = Bytes::from("header data");
let data = Bytes::from("body data");
let message = TwoPartMessage::from_parts(header_data.clone(), data.clone());
let codec = TwoPartCodec::new(None);
// Encode the message
let encoded = codec.encode_message(message).unwrap();
// Simulate partial data arrival
let partial_len = encoded.len() - 5;
let partial_encoded = encoded.slice(0..partial_len);
// Attempt to decode
let result = codec.decode_message(partial_encoded);
// Should return InvalidMessage error
assert!(result.is_err());
if let Err(TwoPartCodecError::InvalidMessage(_)) = result {
// Test passed
} else {
panic!("Expected InvalidMessage error");
}
}
/// Test multiple messages concatenated in the same buffer.
#[test]
fn test_multiple_messages_in_buffer() {
let header_data1 = Bytes::from("header1");
let data1 = Bytes::from("data1");
let message1 = TwoPartMessage::from_parts(header_data1.clone(), data1.clone());
let header_data2 = Bytes::from("header2");
let data2 = Bytes::from("data2");
let message2 = TwoPartMessage::from_parts(header_data2.clone(), data2.clone());
let codec = TwoPartCodec::new(None);
// Encode messages
let encoded1 = codec.encode_message(message1).unwrap();
let encoded2 = codec.encode_message(message2).unwrap();
// Concatenate messages into one buffer
let mut combined = BytesMut::new();
combined.extend_from_slice(&encoded1);
combined.extend_from_slice(&encoded2);
// Decode messages
let mut decode_buf = combined;
let mut codec = codec.clone();
let decoded_msg1 = codec.decode(&mut decode_buf).unwrap().unwrap();
let decoded_msg2 = codec.decode(&mut decode_buf).unwrap().unwrap();
// Verify messages
assert_eq!(decoded_msg1.header, header_data1);
assert_eq!(decoded_msg1.data, data1);
assert_eq!(decoded_msg2.header, header_data2);
assert_eq!(decoded_msg2.data, data2);
}
/// Test simulating reading from a byte stream like a TCP socket.
#[tokio::test]
async fn test_streaming_read() {
// Create messages
let header_data = Bytes::from("header data");
let data = Bytes::from("body data");
let message = TwoPartMessage::from_parts(header_data.clone(), data.clone());
let codec = TwoPartCodec::new(None);
// Encode the message
let encoded = codec.encode_message(message.clone()).unwrap();
// Simulate reading from a TCP socket
// We'll use a Cursor over the encoded data to simulate an AsyncRead
let reader = Cursor::new(encoded.clone());
// Wrap the reader with the codec
let mut framed_read = FramedRead::new(reader, codec.clone());
// Read the message
if let Some(Ok(decoded_message)) = framed_read.next().await {
// Verify the decoded message
assert_eq!(decoded_message.header, header_data);
assert_eq!(decoded_message.data, data);
} else {
panic!("Failed to decode message from stream");
}
}
/// Test simulating partial reads from a TCP socket
#[tokio::test]
async fn test_streaming_partial_reads() {
// Create messages
let header_data = Bytes::from("header data");
let data = Bytes::from("body data");
let message = TwoPartMessage::from_parts(header_data.clone(), data.clone());
let codec = TwoPartCodec::new(None);
// Encode the message
let encoded = codec.encode_message(message.clone()).unwrap();
// Simulate partial reads
// We'll create a custom AsyncRead that returns data in small chunks
struct ChunkedReader {
data: Bytes,
pos: usize,
chunk_size: usize,
}
impl AsyncRead for ChunkedReader {
fn poll_read(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
if self.pos >= self.data.len() {
return Poll::Ready(Ok(()));
}
let end = std::cmp::min(self.pos + self.chunk_size, self.data.len());
let bytes_to_read = &self.data[self.pos..end];
buf.put_slice(bytes_to_read);
self.pos = end;
// if self.pos >= self.data.len() {
// Poll::Ready(Ok(()))
// } else {
// Poll::Ready(Ok(()))
// }
Poll::Ready(Ok(()))
}
}
let reader = ChunkedReader {
data: encoded.clone(),
pos: 0,
chunk_size: 5, // Read in chunks of 5 bytes
};
let mut framed_read = FramedRead::new(reader, codec.clone());
// Read the message
if let Some(Ok(decoded_message)) = framed_read.next().await {
// Verify the decoded message
assert_eq!(decoded_message.header, header_data);
assert_eq!(decoded_message.data, data);
} else {
panic!("Failed to decode message from stream");
}
}
/// Test handling of corrupted data in a stream
#[tokio::test]
async fn test_streaming_corrupted_data() {
// Create messages
let header_data = Bytes::from("header data");
let data = Bytes::from("body data");
let message = TwoPartMessage::from_parts(header_data.clone(), data.clone());
let codec = TwoPartCodec::new(None);
// Encode the message
let encoded = codec.encode_message(message.clone()).unwrap();
// Corrupt the data
let mut encoded = BytesMut::from(encoded);
encoded[30] ^= 0xFF; // Flip a byte in the data
// Simulate reading from a TCP socket
let reader = Cursor::new(encoded.clone());
let mut framed_read = FramedRead::new(reader, codec.clone());
// Read the message
if let Some(result) = framed_read.next().await {
assert!(result.is_err());
// Verify the error is ChecksumMismatch
if let Err(TwoPartCodecError::ChecksumMismatch) = result {
// Test passed
} else {
panic!("Expected ChecksumMismatch error");
}
} else {
panic!("Failed to read message from stream");
}
}
/// Test handling of empty streams
#[tokio::test]
async fn test_empty_stream() {
let codec = TwoPartCodec::new(None);
// Empty reader
let reader = Cursor::new(Vec::new());
let mut framed_read = FramedRead::new(reader, codec.clone());
// Try to read from empty stream
if let Some(result) = framed_read.next().await {
panic!("Expected no messages, but got {:?}", result);
} else {
// Test passed
}
}
/// Test decoding of multiple messages from a stream
#[tokio::test]
async fn test_streaming_multiple_messages() {
let header_data1 = Bytes::from("header1");
let data1 = Bytes::from("data1");
let message1 = TwoPartMessage::from_parts(header_data1.clone(), data1.clone());
let header_data2 = Bytes::from("header2");
let data2 = Bytes::from("data2");
let message2 = TwoPartMessage::from_parts(header_data2.clone(), data2.clone());
let codec = TwoPartCodec::new(None);
// Encode messages
let encoded1 = codec.encode_message(message1.clone()).unwrap();
let encoded2 = codec.encode_message(message2.clone()).unwrap();
// Concatenate messages into one buffer
let mut combined = BytesMut::new();
combined.extend_from_slice(&encoded1);
combined.extend_from_slice(&encoded2);
// Simulate reading from a TCP socket
let reader = Cursor::new(combined.freeze());
let mut framed_read = FramedRead::new(reader, codec.clone());
// Read first message
if let Some(Ok(decoded_message)) = framed_read.next().await {
assert_eq!(decoded_message.header, header_data1);
assert_eq!(decoded_message.data, data1);
} else {
panic!("Failed to decode first message from stream");
}
// Read second message
if let Some(Ok(decoded_message)) = framed_read.next().await {
assert_eq!(decoded_message.header, header_data2);
assert_eq!(decoded_message.data, data2);
} else {
panic!("Failed to decode second message from stream");
}
// Ensure no more messages
if let Some(result) = framed_read.next().await {
panic!("Expected no more messages, but got {:?}", result);
}
}
/// Test encoding and decoding without max_message_size.
#[test]
fn test_message_without_max_size() {
// Create a large message
let header_data = Bytes::from(vec![b'h'; 1024 * 1024]); // 1 MB
let body_data = Bytes::from(vec![b'd'; 1024 * 1024]); // 1 MB
let message = TwoPartMessage::from_parts(header_data.clone(), body_data.clone());
let codec = TwoPartCodec::new(None);
// Encode the message without max_message_size
let encoded = codec.encode_message(message).unwrap();
// Decode the message without max_message_size
let decoded = codec.decode_message(encoded).unwrap();
// Verify the decoded message
assert_eq!(decoded.header, header_data);
assert_eq!(decoded.data, body_data);
}
}
/*
* Copyright 2024-2025 NVIDIA CORPORATION & AFFILIATES
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
pub mod push;
use super::*;
/*
* Copyright 2024-2025 NVIDIA CORPORATION & AFFILIATES
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
use anyhow::Result;
use async_nats::client::Client;
use tracing as log;
use super::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
enum RequestType {
SingleIn,
ManyIn,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
enum ResponseType {
SingleOut,
ManyOut,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct RequestControlMessage {
id: String,
request_type: RequestType,
response_type: ResponseType,
connection_info: ConnectionInfo,
}
pub type PushRouter<In, Out> =
Arc<dyn AsyncEngine<SingleIn<AddressedRequest<In>>, ManyOut<Out>, Error>>;
pub struct AddressedRequest<T> {
request: T,
address: String,
}
impl<T> AddressedRequest<T> {
pub fn new(request: T, address: String) -> Self {
Self { request, address }
}
fn into_parts(self) -> (T, String) {
(self.request, self.address)
}
}
pub struct AddressedPushRouter {
// todo: generalize with a generic
req_transport: Client,
// todo: generalize with a generic
resp_transport: Arc<tcp::server::TcpStreamServer>,
}
impl AddressedPushRouter {
pub fn new(
req_transport: Client,
resp_transport: Arc<tcp::server::TcpStreamServer>,
) -> Result<Arc<Self>> {
Ok(Arc::new(Self {
req_transport,
resp_transport,
}))
}
}
#[async_trait]
impl<T, U> AsyncEngine<SingleIn<AddressedRequest<T>>, ManyOut<U>, Error> for AddressedPushRouter
where
T: Data + Serialize,
U: Data + for<'de> Deserialize<'de>,
{
async fn generate(&self, request: SingleIn<AddressedRequest<T>>) -> Result<ManyOut<U>, Error> {
let id = request.context().id().to_string();
let (addressed_request, context) = request.transfer(());
let (request, address) = addressed_request.into_parts();
let engine_ctx = context.context();
// registration options for the data plane in a singe in / many out configuration
let options = StreamOptions::builder()
.context(engine_ctx.clone())
.enable_request_stream(false)
.enable_response_stream(true)
.build()
.unwrap();
// register our needs with the data plane
// todo - generalize this with a generic data plane object which hides the specific transports
let pending_connections: PendingConnections = self.resp_transport.register(options).await;
// validate and unwrap the RegisteredStream object
let pending_response_stream = match pending_connections.into_parts() {
(None, Some(recv_stream)) => recv_stream,
_ => {
panic!("Invalid data plane registration for a SingleIn/ManyOut transport");
}
};
// separate out the the connection info and the stream provider from the registered stream
let (connection_info, response_stream_provider) = pending_response_stream.into_parts();
// package up the connection info as part of the "header" component of the two part message
// used to issue the request on the
// todo -- this object should be automatically created by the register call, and achieved by to the two into_parts()
// calls. all the information here is provided by the [`StreamOptions`] object and/or the dataplane object
let control_message = RequestControlMessage {
id: engine_ctx.id().to_string(),
request_type: RequestType::SingleIn,
response_type: ResponseType::ManyOut,
connection_info,
};
// next build the two part message where we package the connection info and the request into
// a single Vec<u8> that can be sent over the wire.
// --- package this up in the WorkQueuePublisher ---
let ctrl = serde_json::to_vec(&control_message).unwrap();
let data = serde_json::to_vec(&request).unwrap();
log::trace!(
"[req: {}] packaging two-part message; ctrl: {} bytes, data: {} bytes",
id,
ctrl.len(),
data.len()
);
let msg = TwoPartMessage::from_parts(ctrl.into(), data.into());
// the request plane / work queue should provide a two part message codec that can be used
// or it should take a two part message directly
// todo - update this
let codec = TwoPartCodec::default();
let buffer = codec.encode_message(msg).unwrap();
// TRANSPORT ABSTRACT REQUIRED - END HERE
log::trace!("[req: {}] enqueueing two-part message to nats", id);
// we might need to add a timeout on this if there is no subscriber to the subject; however, I think nats
// will handle this for us
let _response = self
.req_transport
.request(address.to_string(), buffer)
.await?;
log::trace!("[req: {}] awaiting transport handshake", id);
let response_stream = response_stream_provider
.await
.map_err(|_| PipelineError::DetatchedStreamReceiver)?
.map_err(PipelineError::ConnectionFailed)?;
let stream = tokio_stream::wrappers::ReceiverStream::new(response_stream.rx);
let stream = stream.map(|msg| {
let resp: U = serde_json::from_slice(&msg).unwrap();
resp
});
Ok(ResponseStream::new(Box::pin(stream), engine_ctx))
}
}
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