Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
OpenDAS
dynamo
Commits
06b0ebef
Unverified
Commit
06b0ebef
authored
Nov 13, 2025
by
Biswa Panda
Committed by
GitHub
Nov 13, 2025
Browse files
feat: transport agnostic request plane for dynamo - natless (#4246)
parent
381c428c
Changes
43
Show whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
4103 additions
and
113 deletions
+4103
-113
lib/runtime/src/logging.rs
lib/runtime/src/logging.rs
+24
-0
lib/runtime/src/pipeline/network.rs
lib/runtime/src/pipeline/network.rs
+7
-1
lib/runtime/src/pipeline/network/codec.rs
lib/runtime/src/pipeline/network/codec.rs
+707
-47
lib/runtime/src/pipeline/network/egress.rs
lib/runtime/src/pipeline/network/egress.rs
+6
-0
lib/runtime/src/pipeline/network/egress/addressed_router.rs
lib/runtime/src/pipeline/network/egress/addressed_router.rs
+26
-35
lib/runtime/src/pipeline/network/egress/http_router.rs
lib/runtime/src/pipeline/network/egress/http_router.rs
+694
-0
lib/runtime/src/pipeline/network/egress/nats_client.rs
lib/runtime/src/pipeline/network/egress/nats_client.rs
+87
-0
lib/runtime/src/pipeline/network/egress/push_router.rs
lib/runtime/src/pipeline/network/egress/push_router.rs
+53
-9
lib/runtime/src/pipeline/network/egress/queue.rs
lib/runtime/src/pipeline/network/egress/queue.rs
+0
-2
lib/runtime/src/pipeline/network/egress/tcp_client.rs
lib/runtime/src/pipeline/network/egress/tcp_client.rs
+918
-0
lib/runtime/src/pipeline/network/egress/unified_client.rs
lib/runtime/src/pipeline/network/egress/unified_client.rs
+193
-0
lib/runtime/src/pipeline/network/ingress.rs
lib/runtime/src/pipeline/network/ingress.rs
+4
-0
lib/runtime/src/pipeline/network/ingress/http_endpoint.rs
lib/runtime/src/pipeline/network/ingress/http_endpoint.rs
+355
-0
lib/runtime/src/pipeline/network/ingress/nats_server.rs
lib/runtime/src/pipeline/network/ingress/nats_server.rs
+216
-0
lib/runtime/src/pipeline/network/ingress/shared_tcp_endpoint.rs
...ntime/src/pipeline/network/ingress/shared_tcp_endpoint.rs
+365
-0
lib/runtime/src/pipeline/network/ingress/unified_server.rs
lib/runtime/src/pipeline/network/ingress/unified_server.rs
+119
-0
lib/runtime/src/pipeline/network/manager.rs
lib/runtime/src/pipeline/network/manager.rs
+327
-0
lib/runtime/src/runtime.rs
lib/runtime/src/runtime.rs
+1
-10
lib/runtime/src/system_status_server.rs
lib/runtime/src/system_status_server.rs
+1
-3
lib/runtime/src/transports/nats.rs
lib/runtime/src/transports/nats.rs
+0
-6
No files found.
lib/runtime/src/logging.rs
View file @
06b0ebef
...
...
@@ -414,6 +414,30 @@ pub fn inject_current_trace_into_nats_headers(headers: &mut async_nats::HeaderMa
inject_otel_context_into_nats_headers
(
headers
,
None
);
}
// Inject trace headers into a generic HashMap for HTTP/TCP transports
pub
fn
inject_trace_headers_into_map
(
headers
:
&
mut
std
::
collections
::
HashMap
<
String
,
String
>
)
{
if
let
Some
(
trace_context
)
=
get_distributed_tracing_context
()
{
// Inject W3C traceparent header
headers
.insert
(
"traceparent"
.to_string
(),
trace_context
.create_traceparent
(),
);
// Inject optional tracestate
if
let
Some
(
tracestate
)
=
trace_context
.tracestate
{
headers
.insert
(
"tracestate"
.to_string
(),
tracestate
);
}
// Inject custom request IDs
if
let
Some
(
x_request_id
)
=
trace_context
.x_request_id
{
headers
.insert
(
"x-request-id"
.to_string
(),
x_request_id
);
}
if
let
Some
(
x_dynamo_request_id
)
=
trace_context
.x_dynamo_request_id
{
headers
.insert
(
"x-dynamo-request-id"
.to_string
(),
x_dynamo_request_id
);
}
}
}
/// Create a client_request span linked to the parent trace context
pub
fn
make_client_request_span
(
operation
:
&
str
,
...
...
lib/runtime/src/pipeline/network.rs
View file @
06b0ebef
// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//! TODO - we need to reconcile what is in this crate with distributed::transports
//! Network layer for distributed communication
//!
//! Provides request distribution across multiple transport protocols:
//! - HTTP/2 for standard deployments
//! - TCP with length-prefixed protocol for high-performance scenarios
//! - NATS for legacy/messaging-based deployments
pub
mod
codec
;
pub
mod
egress
;
pub
mod
ingress
;
pub
mod
manager
;
pub
mod
tcp
;
use
crate
::
SystemHealth
;
...
...
lib/runtime/src/pipeline/network/codec.rs
View file @
06b0ebef
...
...
@@ -8,6 +8,7 @@
//! In this module, we define three primary codec used to issue single, two-part or multi-part messages,
//! on a byte stream.
use
bytes
::
Bytes
;
use
tokio_util
::{
bytes
::{
Buf
,
BufMut
,
BytesMut
},
codec
::{
Decoder
,
Encoder
},
...
...
@@ -17,50 +18,709 @@ 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(())
// }
// }
/// TCP request plane protocol message with endpoint routing
///
/// Wire format:
/// - endpoint_path_len: u16 (big-endian)
/// - endpoint_path: UTF-8 string
/// - payload_len: u32 (big-endian)
/// - payload: bytes
#[derive(Debug,
Clone,
PartialEq,
Eq)]
pub
struct
TcpRequestMessage
{
pub
endpoint_path
:
String
,
pub
payload
:
Bytes
,
}
impl
TcpRequestMessage
{
pub
fn
new
(
endpoint_path
:
String
,
payload
:
Bytes
)
->
Self
{
Self
{
endpoint_path
,
payload
,
}
}
/// Encode message to bytes
pub
fn
encode
(
&
self
)
->
Result
<
Bytes
,
std
::
io
::
Error
>
{
let
endpoint_bytes
=
self
.endpoint_path
.as_bytes
();
let
endpoint_len
=
endpoint_bytes
.len
();
if
endpoint_len
>
u16
::
MAX
as
usize
{
return
Err
(
std
::
io
::
Error
::
new
(
std
::
io
::
ErrorKind
::
InvalidInput
,
format!
(
"Endpoint path too long: {} bytes"
,
endpoint_len
),
));
}
if
self
.payload
.len
()
>
u32
::
MAX
as
usize
{
return
Err
(
std
::
io
::
Error
::
new
(
std
::
io
::
ErrorKind
::
InvalidInput
,
format!
(
"Payload too large: {} bytes"
,
self
.payload
.len
()),
));
}
// Use BytesMut for efficient buffer building
let
mut
buf
=
BytesMut
::
with_capacity
(
2
+
endpoint_len
+
4
+
self
.payload
.len
());
// Write endpoint path length (2 bytes)
buf
.put_u16
(
endpoint_len
as
u16
);
// Write endpoint path
buf
.put_slice
(
endpoint_bytes
);
// Write payload length (4 bytes)
buf
.put_u32
(
self
.payload
.len
()
as
u32
);
// Write payload
buf
.put_slice
(
&
self
.payload
);
// Zero-copy conversion to Bytes
Ok
(
buf
.freeze
())
}
/// Decode message from bytes (for backward compatibility, zero-copy when possible)
pub
fn
decode
(
bytes
:
&
Bytes
)
->
Result
<
Self
,
std
::
io
::
Error
>
{
if
bytes
.len
()
<
2
{
return
Err
(
std
::
io
::
Error
::
new
(
std
::
io
::
ErrorKind
::
UnexpectedEof
,
"Not enough bytes for endpoint path length"
,
));
}
// Read endpoint path length (2 bytes)
let
endpoint_len
=
u16
::
from_be_bytes
([
bytes
[
0
],
bytes
[
1
]])
as
usize
;
let
mut
offset
=
2
;
if
bytes
.len
()
<
offset
+
endpoint_len
{
return
Err
(
std
::
io
::
Error
::
new
(
std
::
io
::
ErrorKind
::
UnexpectedEof
,
"Not enough bytes for endpoint path"
,
));
}
// Read endpoint path (requires copy for UTF-8 validation)
let
endpoint_path
=
String
::
from_utf8
(
bytes
[
offset
..
offset
+
endpoint_len
]
.to_vec
())
.map_err
(|
e
|
{
std
::
io
::
Error
::
new
(
std
::
io
::
ErrorKind
::
InvalidData
,
format!
(
"Invalid UTF-8: {}"
,
e
),
)
})
?
;
offset
+=
endpoint_len
;
if
bytes
.len
()
<
offset
+
4
{
return
Err
(
std
::
io
::
Error
::
new
(
std
::
io
::
ErrorKind
::
UnexpectedEof
,
"Not enough bytes for payload length"
,
));
}
// Read payload length (4 bytes)
let
payload_len
=
u32
::
from_be_bytes
([
bytes
[
offset
],
bytes
[
offset
+
1
],
bytes
[
offset
+
2
],
bytes
[
offset
+
3
],
])
as
usize
;
offset
+=
4
;
if
bytes
.len
()
<
offset
+
payload_len
{
return
Err
(
std
::
io
::
Error
::
new
(
std
::
io
::
ErrorKind
::
UnexpectedEof
,
format!
(
"Not enough bytes for payload: expected {}, got {}"
,
payload_len
,
bytes
.len
()
-
offset
),
));
}
// Read payload (zero-copy slice)
let
payload
=
bytes
.slice
(
offset
..
offset
+
payload_len
);
Ok
(
Self
{
endpoint_path
,
payload
,
})
}
}
/// Codec for encoding/decoding TcpRequestMessage
/// Supports max_message_size enforcement
#[derive(Clone,
Default)]
pub
struct
TcpRequestCodec
{
max_message_size
:
Option
<
usize
>
,
}
impl
TcpRequestCodec
{
pub
fn
new
(
max_message_size
:
Option
<
usize
>
)
->
Self
{
Self
{
max_message_size
}
}
}
impl
Decoder
for
TcpRequestCodec
{
type
Item
=
TcpRequestMessage
;
type
Error
=
std
::
io
::
Error
;
fn
decode
(
&
mut
self
,
src
:
&
mut
BytesMut
)
->
Result
<
Option
<
Self
::
Item
>
,
Self
::
Error
>
{
// Need at least 2 bytes for endpoint_path_len
if
src
.len
()
<
2
{
return
Ok
(
None
);
}
// Peek at endpoint path length without consuming
let
endpoint_len
=
u16
::
from_be_bytes
([
src
[
0
],
src
[
1
]])
as
usize
;
let
header_size
=
2
+
endpoint_len
+
4
;
// path_len + path + payload_len
if
src
.len
()
<
header_size
{
return
Ok
(
None
);
}
// Peek at payload length
let
payload_len_offset
=
2
+
endpoint_len
;
let
payload_len
=
u32
::
from_be_bytes
([
src
[
payload_len_offset
],
src
[
payload_len_offset
+
1
],
src
[
payload_len_offset
+
2
],
src
[
payload_len_offset
+
3
],
])
as
usize
;
let
total_len
=
header_size
+
payload_len
;
// Check max message size
if
let
Some
(
max_size
)
=
self
.max_message_size
&&
total_len
>
max_size
{
return
Err
(
std
::
io
::
Error
::
new
(
std
::
io
::
ErrorKind
::
InvalidData
,
format!
(
"Request too large: {} bytes (max: {} bytes)"
,
total_len
,
max_size
),
));
}
// Check if we have the full message
if
src
.len
()
<
total_len
{
return
Ok
(
None
);
}
// We have a complete message, advance past length prefix
src
.advance
(
2
);
// Read endpoint path
let
endpoint_bytes
=
src
.split_to
(
endpoint_len
);
let
endpoint_path
=
String
::
from_utf8
(
endpoint_bytes
.to_vec
())
.map_err
(|
e
|
{
std
::
io
::
Error
::
new
(
std
::
io
::
ErrorKind
::
InvalidData
,
format!
(
"Invalid UTF-8 in endpoint path: {}"
,
e
),
)
})
?
;
// Advance past payload length
src
.advance
(
4
);
// Read payload
let
payload
=
src
.split_to
(
payload_len
)
.freeze
();
Ok
(
Some
(
TcpRequestMessage
{
endpoint_path
,
payload
,
}))
}
}
impl
Encoder
<
TcpRequestMessage
>
for
TcpRequestCodec
{
type
Error
=
std
::
io
::
Error
;
fn
encode
(
&
mut
self
,
item
:
TcpRequestMessage
,
dst
:
&
mut
BytesMut
)
->
Result
<
(),
Self
::
Error
>
{
let
endpoint_bytes
=
item
.endpoint_path
.as_bytes
();
let
endpoint_len
=
endpoint_bytes
.len
();
if
endpoint_len
>
u16
::
MAX
as
usize
{
return
Err
(
std
::
io
::
Error
::
new
(
std
::
io
::
ErrorKind
::
InvalidInput
,
format!
(
"Endpoint path too long: {} bytes"
,
endpoint_len
),
));
}
if
item
.payload
.len
()
>
u32
::
MAX
as
usize
{
return
Err
(
std
::
io
::
Error
::
new
(
std
::
io
::
ErrorKind
::
InvalidInput
,
format!
(
"Payload too large: {} bytes"
,
item
.payload
.len
()),
));
}
let
total_len
=
2
+
endpoint_len
+
4
+
item
.payload
.len
();
// Check max message size
if
let
Some
(
max_size
)
=
self
.max_message_size
&&
total_len
>
max_size
{
return
Err
(
std
::
io
::
Error
::
new
(
std
::
io
::
ErrorKind
::
InvalidInput
,
format!
(
"Request too large: {} bytes (max: {} bytes)"
,
total_len
,
max_size
),
));
}
// Reserve space
dst
.reserve
(
total_len
);
// Write endpoint path length
dst
.put_u16
(
endpoint_len
as
u16
);
// Write endpoint path
dst
.put_slice
(
endpoint_bytes
);
// Write payload length
dst
.put_u32
(
item
.payload
.len
()
as
u32
);
// Write payload
dst
.put_slice
(
&
item
.payload
);
Ok
(())
}
}
/// TCP response message (acknowledgment or error)
///
/// Wire format:
/// - length: u32 (big-endian)
/// - data: bytes
#[derive(Debug,
Clone,
PartialEq,
Eq)]
pub
struct
TcpResponseMessage
{
pub
data
:
Bytes
,
}
impl
TcpResponseMessage
{
pub
fn
new
(
data
:
Bytes
)
->
Self
{
Self
{
data
}
}
pub
fn
empty
()
->
Self
{
Self
{
data
:
Bytes
::
new
()
}
}
/// Encode response to bytes (for backward compatibility)
pub
fn
encode
(
&
self
)
->
Result
<
Bytes
,
std
::
io
::
Error
>
{
if
self
.data
.len
()
>
u32
::
MAX
as
usize
{
return
Err
(
std
::
io
::
Error
::
new
(
std
::
io
::
ErrorKind
::
InvalidInput
,
format!
(
"Response too large: {} bytes"
,
self
.data
.len
()),
));
}
// Use BytesMut for efficient buffer building
let
mut
buf
=
BytesMut
::
with_capacity
(
4
+
self
.data
.len
());
// Write length (4 bytes)
buf
.put_u32
(
self
.data
.len
()
as
u32
);
// Write data
buf
.put_slice
(
&
self
.data
);
// Zero-copy conversion to Bytes
Ok
(
buf
.freeze
())
}
/// Decode response from bytes (for backward compatibility, zero-copy when possible)
pub
fn
decode
(
bytes
:
&
Bytes
)
->
Result
<
Self
,
std
::
io
::
Error
>
{
if
bytes
.len
()
<
4
{
return
Err
(
std
::
io
::
Error
::
new
(
std
::
io
::
ErrorKind
::
UnexpectedEof
,
"Not enough bytes for response length"
,
));
}
// Read length (4 bytes)
let
len
=
u32
::
from_be_bytes
([
bytes
[
0
],
bytes
[
1
],
bytes
[
2
],
bytes
[
3
]])
as
usize
;
if
bytes
.len
()
<
4
+
len
{
return
Err
(
std
::
io
::
Error
::
new
(
std
::
io
::
ErrorKind
::
UnexpectedEof
,
format!
(
"Not enough bytes for response: expected {}, got {}"
,
len
,
bytes
.len
()
-
4
),
));
}
// Read data (zero-copy slice)
let
data
=
bytes
.slice
(
4
..
4
+
len
);
Ok
(
Self
{
data
})
}
}
/// Codec for encoding/decoding TcpResponseMessage
/// Supports max_message_size enforcement
#[derive(Clone,
Default)]
pub
struct
TcpResponseCodec
{
max_message_size
:
Option
<
usize
>
,
}
impl
TcpResponseCodec
{
pub
fn
new
(
max_message_size
:
Option
<
usize
>
)
->
Self
{
Self
{
max_message_size
}
}
}
impl
Decoder
for
TcpResponseCodec
{
type
Item
=
TcpResponseMessage
;
type
Error
=
std
::
io
::
Error
;
fn
decode
(
&
mut
self
,
src
:
&
mut
BytesMut
)
->
Result
<
Option
<
Self
::
Item
>
,
Self
::
Error
>
{
// Need at least 4 bytes for length
if
src
.len
()
<
4
{
return
Ok
(
None
);
}
// Peek at message length without consuming
let
data_len
=
u32
::
from_be_bytes
([
src
[
0
],
src
[
1
],
src
[
2
],
src
[
3
]])
as
usize
;
let
total_len
=
4
+
data_len
;
// Check max message size
if
let
Some
(
max_size
)
=
self
.max_message_size
&&
total_len
>
max_size
{
return
Err
(
std
::
io
::
Error
::
new
(
std
::
io
::
ErrorKind
::
InvalidData
,
format!
(
"Response too large: {} bytes (max: {} bytes)"
,
total_len
,
max_size
),
));
}
// Check if we have the full message
if
src
.len
()
<
total_len
{
return
Ok
(
None
);
}
// Advance past the length prefix
src
.advance
(
4
);
// Read data
let
data
=
src
.split_to
(
data_len
)
.freeze
();
Ok
(
Some
(
TcpResponseMessage
{
data
}))
}
}
impl
Encoder
<
TcpResponseMessage
>
for
TcpResponseCodec
{
type
Error
=
std
::
io
::
Error
;
fn
encode
(
&
mut
self
,
item
:
TcpResponseMessage
,
dst
:
&
mut
BytesMut
)
->
Result
<
(),
Self
::
Error
>
{
if
item
.data
.len
()
>
u32
::
MAX
as
usize
{
return
Err
(
std
::
io
::
Error
::
new
(
std
::
io
::
ErrorKind
::
InvalidInput
,
format!
(
"Response too large: {} bytes"
,
item
.data
.len
()),
));
}
let
total_len
=
4
+
item
.data
.len
();
// Check max message size
if
let
Some
(
max_size
)
=
self
.max_message_size
&&
total_len
>
max_size
{
return
Err
(
std
::
io
::
Error
::
new
(
std
::
io
::
ErrorKind
::
InvalidInput
,
format!
(
"Response too large: {} bytes (max: {} bytes)"
,
total_len
,
max_size
),
));
}
// Reserve space
dst
.reserve
(
total_len
);
// Write length
dst
.put_u32
(
item
.data
.len
()
as
u32
);
// Write data
dst
.put_slice
(
&
item
.data
);
Ok
(())
}
}
#[cfg(test)]
mod
tests
{
use
super
::
*
;
#[test]
fn
test_tcp_request_encode_decode
()
{
let
msg
=
TcpRequestMessage
::
new
(
"test.endpoint"
.to_string
(),
Bytes
::
from
(
vec!
[
1
,
2
,
3
,
4
,
5
]),
);
let
encoded
=
msg
.encode
()
.unwrap
();
let
decoded
=
TcpRequestMessage
::
decode
(
&
encoded
)
.unwrap
();
assert_eq!
(
decoded
,
msg
);
}
#[test]
fn
test_tcp_request_empty_payload
()
{
let
msg
=
TcpRequestMessage
::
new
(
"test"
.to_string
(),
Bytes
::
new
());
let
encoded
=
msg
.encode
()
.unwrap
();
let
decoded
=
TcpRequestMessage
::
decode
(
&
encoded
)
.unwrap
();
assert_eq!
(
decoded
,
msg
);
}
#[test]
fn
test_tcp_request_large_payload
()
{
let
payload
=
Bytes
::
from
(
vec!
[
42u8
;
1024
*
1024
]);
// 1MB
let
msg
=
TcpRequestMessage
::
new
(
"large"
.to_string
(),
payload
);
let
encoded
=
msg
.encode
()
.unwrap
();
let
decoded
=
TcpRequestMessage
::
decode
(
&
encoded
)
.unwrap
();
assert_eq!
(
decoded
,
msg
);
}
#[test]
fn
test_tcp_request_decode_truncated
()
{
let
msg
=
TcpRequestMessage
::
new
(
"test"
.to_string
(),
Bytes
::
from
(
vec!
[
1
,
2
,
3
,
4
,
5
]));
let
encoded
=
msg
.encode
()
.unwrap
();
// Truncate the encoded message
let
truncated
=
encoded
.slice
(
..
encoded
.len
()
-
2
);
let
result
=
TcpRequestMessage
::
decode
(
&
truncated
);
assert
!
(
result
.is_err
());
}
#[test]
fn
test_tcp_response_encode_decode
()
{
let
msg
=
TcpResponseMessage
::
new
(
Bytes
::
from
(
vec!
[
1
,
2
,
3
,
4
,
5
]));
let
encoded
=
msg
.encode
()
.unwrap
();
let
decoded
=
TcpResponseMessage
::
decode
(
&
encoded
)
.unwrap
();
assert_eq!
(
decoded
,
msg
);
}
#[test]
fn
test_tcp_response_empty
()
{
let
msg
=
TcpResponseMessage
::
empty
();
let
encoded
=
msg
.encode
()
.unwrap
();
let
decoded
=
TcpResponseMessage
::
decode
(
&
encoded
)
.unwrap
();
assert_eq!
(
decoded
,
msg
);
assert_eq!
(
decoded
.data
.len
(),
0
);
}
#[test]
fn
test_tcp_response_decode_truncated
()
{
let
msg
=
TcpResponseMessage
::
new
(
Bytes
::
from
(
vec!
[
1
,
2
,
3
,
4
,
5
]));
let
encoded
=
msg
.encode
()
.unwrap
();
// Truncate the encoded message
let
truncated
=
encoded
.slice
(
..
3
);
let
result
=
TcpResponseMessage
::
decode
(
&
truncated
);
assert
!
(
result
.is_err
());
}
#[test]
fn
test_tcp_request_unicode_endpoint
()
{
let
msg
=
TcpRequestMessage
::
new
(
"тест.端点"
.to_string
(),
Bytes
::
from
(
vec!
[
1
,
2
,
3
]));
let
encoded
=
msg
.encode
()
.unwrap
();
let
decoded
=
TcpRequestMessage
::
decode
(
&
encoded
)
.unwrap
();
assert_eq!
(
decoded
,
msg
);
}
#[test]
fn
test_tcp_request_codec
()
{
use
tokio_util
::
codec
::{
Decoder
,
Encoder
};
let
msg
=
TcpRequestMessage
::
new
(
"test.endpoint"
.to_string
(),
Bytes
::
from
(
vec!
[
1
,
2
,
3
,
4
,
5
]),
);
let
mut
codec
=
TcpRequestCodec
::
new
(
None
);
let
mut
buf
=
BytesMut
::
new
();
// Encode
codec
.encode
(
msg
.clone
(),
&
mut
buf
)
.unwrap
();
// Decode
let
decoded
=
codec
.decode
(
&
mut
buf
)
.unwrap
()
.unwrap
();
assert_eq!
(
decoded
,
msg
);
}
#[test]
fn
test_tcp_request_codec_partial
()
{
use
tokio_util
::
codec
::
Decoder
;
let
msg
=
TcpRequestMessage
::
new
(
"test.endpoint"
.to_string
(),
Bytes
::
from
(
vec!
[
1
,
2
,
3
,
4
,
5
]),
);
let
encoded
=
msg
.encode
()
.unwrap
();
let
mut
codec
=
TcpRequestCodec
::
new
(
None
);
// Feed partial data
let
mut
buf
=
BytesMut
::
from
(
&
encoded
[
..
5
]);
assert
!
(
codec
.decode
(
&
mut
buf
)
.unwrap
()
.is_none
());
// Feed rest of data
buf
.extend_from_slice
(
&
encoded
[
5
..
]);
let
decoded
=
codec
.decode
(
&
mut
buf
)
.unwrap
()
.unwrap
();
assert_eq!
(
decoded
,
msg
);
}
#[test]
fn
test_tcp_request_codec_max_size
()
{
use
tokio_util
::
codec
::
Encoder
;
let
msg
=
TcpRequestMessage
::
new
(
"test"
.to_string
(),
Bytes
::
from
(
vec!
[
1
,
2
,
3
,
4
,
5
]));
let
mut
codec
=
TcpRequestCodec
::
new
(
Some
(
10
));
// Too small
let
mut
buf
=
BytesMut
::
new
();
let
result
=
codec
.encode
(
msg
,
&
mut
buf
);
assert
!
(
result
.is_err
());
}
#[test]
fn
test_tcp_response_codec
()
{
use
tokio_util
::
codec
::{
Decoder
,
Encoder
};
let
msg
=
TcpResponseMessage
::
new
(
Bytes
::
from
(
vec!
[
1
,
2
,
3
,
4
,
5
]));
let
mut
codec
=
TcpResponseCodec
::
new
(
None
);
let
mut
buf
=
BytesMut
::
new
();
// Encode
codec
.encode
(
msg
.clone
(),
&
mut
buf
)
.unwrap
();
// Decode
let
decoded
=
codec
.decode
(
&
mut
buf
)
.unwrap
()
.unwrap
();
assert_eq!
(
decoded
,
msg
);
}
#[test]
fn
test_tcp_response_codec_partial
()
{
use
tokio_util
::
codec
::
Decoder
;
let
msg
=
TcpResponseMessage
::
new
(
Bytes
::
from
(
vec!
[
1
,
2
,
3
,
4
,
5
]));
let
encoded
=
msg
.encode
()
.unwrap
();
let
mut
codec
=
TcpResponseCodec
::
new
(
None
);
// Feed partial data
let
mut
buf
=
BytesMut
::
from
(
&
encoded
[
..
3
]);
assert
!
(
codec
.decode
(
&
mut
buf
)
.unwrap
()
.is_none
());
// Feed rest of data
buf
.extend_from_slice
(
&
encoded
[
3
..
]);
let
decoded
=
codec
.decode
(
&
mut
buf
)
.unwrap
()
.unwrap
();
assert_eq!
(
decoded
,
msg
);
}
#[test]
fn
test_tcp_response_codec_max_size
()
{
use
tokio_util
::
codec
::
Encoder
;
let
msg
=
TcpResponseMessage
::
new
(
Bytes
::
from
(
vec!
[
1
,
2
,
3
,
4
,
5
]));
let
mut
codec
=
TcpResponseCodec
::
new
(
Some
(
5
));
// Too small
let
mut
buf
=
BytesMut
::
new
();
let
result
=
codec
.encode
(
msg
,
&
mut
buf
);
assert
!
(
result
.is_err
());
}
/// Demonstrates how framed codec enables testability without actual TCP connections
#[tokio::test]
async
fn
test_framed_codec_integration
()
{
use
futures
::{
SinkExt
,
StreamExt
};
use
std
::
io
::
Cursor
;
use
tokio_util
::
codec
::{
FramedRead
,
FramedWrite
};
// Simulate a duplex connection using in-memory buffer
let
mut
buffer
=
Vec
::
new
();
// Writer side: encode requests
{
let
cursor
=
Cursor
::
new
(
&
mut
buffer
);
let
mut
writer
=
FramedWrite
::
new
(
cursor
,
TcpRequestCodec
::
new
(
None
));
let
msg1
=
TcpRequestMessage
::
new
(
"endpoint1"
.to_string
(),
Bytes
::
from
(
"data1"
));
let
msg2
=
TcpRequestMessage
::
new
(
"endpoint2"
.to_string
(),
Bytes
::
from
(
"data2"
));
writer
.send
(
msg1
)
.await
.unwrap
();
writer
.send
(
msg2
)
.await
.unwrap
();
}
// Reader side: decode requests
{
let
cursor
=
Cursor
::
new
(
&
buffer
[
..
]);
let
mut
reader
=
FramedRead
::
new
(
cursor
,
TcpRequestCodec
::
new
(
None
));
let
decoded1
=
reader
.next
()
.await
.unwrap
()
.unwrap
();
assert_eq!
(
decoded1
.endpoint_path
,
"endpoint1"
);
assert_eq!
(
decoded1
.payload
,
Bytes
::
from
(
"data1"
));
let
decoded2
=
reader
.next
()
.await
.unwrap
()
.unwrap
();
assert_eq!
(
decoded2
.endpoint_path
,
"endpoint2"
);
assert_eq!
(
decoded2
.payload
,
Bytes
::
from
(
"data2"
));
}
}
/// Demonstrates testing partial message handling
#[tokio::test]
async
fn
test_framed_codec_partial_messages
()
{
use
futures
::
StreamExt
;
use
std
::
io
::
Cursor
;
use
tokio_util
::
codec
::
FramedRead
;
// Create a message and encode it
let
msg
=
TcpRequestMessage
::
new
(
"test"
.to_string
(),
Bytes
::
from
(
"hello"
));
let
encoded
=
msg
.encode
()
.unwrap
();
// Split the encoded message into chunks
let
chunk1
=
&
encoded
[
..
5
];
let
chunk2
=
&
encoded
[
5
..
];
// Create a buffer that simulates receiving data in chunks
let
mut
full_buffer
=
Vec
::
new
();
full_buffer
.extend_from_slice
(
chunk1
);
// Reader can't decode yet (partial data)
{
let
cursor
=
Cursor
::
new
(
&
full_buffer
[
..
]);
let
_
reader
=
FramedRead
::
new
(
cursor
,
TcpRequestCodec
::
new
(
None
));
// In real async, this would return Ok(None) and wait for more data
// For Cursor, it returns None at EOF
}
// Add the rest of the data
full_buffer
.extend_from_slice
(
chunk2
);
// Now decoding succeeds
{
let
cursor
=
Cursor
::
new
(
&
full_buffer
[
..
]);
let
mut
reader
=
FramedRead
::
new
(
cursor
,
TcpRequestCodec
::
new
(
None
));
let
decoded
=
reader
.next
()
.await
.unwrap
()
.unwrap
();
assert_eq!
(
decoded
.endpoint_path
,
"test"
);
assert_eq!
(
decoded
.payload
,
Bytes
::
from
(
"hello"
));
}
}
}
lib/runtime/src/pipeline/network/egress.rs
View file @
06b0ebef
...
...
@@ -2,6 +2,12 @@
// SPDX-License-Identifier: Apache-2.0
pub
mod
addressed_router
;
pub
mod
http_router
;
pub
mod
nats_client
;
pub
mod
push_router
;
// Unified request plane interface and implementations
pub
mod
tcp_client
;
pub
mod
unified_client
;
use
super
::
*
;
lib/runtime/src/pipeline/network/egress/addressed_router.rs
View file @
06b0ebef
...
...
@@ -3,14 +3,13 @@
use
std
::
sync
::
Arc
;
use
super
::
unified_client
::
RequestPlaneClient
;
use
super
::
*
;
use
crate
::
engine
::{
AsyncEngine
,
AsyncEngineContextProvider
,
Data
};
use
crate
::
logging
::
DistributedTraceContext
;
use
crate
::
logging
::
get_distributed_tracing_context
;
use
crate
::
logging
::
inject_otel_context_into_nats_headers
;
use
crate
::
logging
::
inject_trace_headers_into_map
;
use
crate
::
pipeline
::
network
::
ConnectionInfo
;
use
crate
::
pipeline
::
network
::
NetworkStreamWrapper
;
use
crate
::
pipeline
::
network
::
PendingConnections
;
use
crate
::
pipeline
::
network
::
ResponseService
;
use
crate
::
pipeline
::
network
::
STREAM_ERR_MSG
;
use
crate
::
pipeline
::
network
::
StreamOptions
;
use
crate
::
pipeline
::
network
::
TwoPartCodec
;
...
...
@@ -20,8 +19,6 @@ use crate::pipeline::{ManyOut, PipelineError, ResponseStream, SingleIn};
use
crate
::
protocols
::
maybe_error
::
MaybeError
;
use
anyhow
::{
Error
,
Result
};
use
async_nats
::
client
::
Client
;
use
async_nats
::{
HeaderMap
,
HeaderValue
};
use
serde
::
Deserialize
;
use
serde
::
Serialize
;
use
tokio_stream
::{
StreamExt
,
StreamNotifyClose
,
wrappers
::
ReceiverStream
};
...
...
@@ -59,26 +56,30 @@ impl<T> AddressedRequest<T> {
Self
{
request
,
address
}
}
fn
into_parts
(
self
)
->
(
T
,
String
)
{
pub
(
crate
)
fn
into_parts
(
self
)
->
(
T
,
String
)
{
(
self
.request
,
self
.address
)
}
}
pub
struct
AddressedPushRouter
{
//
todo: generalize with a generic
req_
transport
:
Client
,
//
Request transport (unified trait object - works with all transports)
req_
client
:
Arc
<
dyn
RequestPlane
Client
>
,
//
todo: generalize with a generic
//
Response transport (TCP streaming - unchanged)
resp_transport
:
Arc
<
tcp
::
server
::
TcpStreamServer
>
,
}
impl
AddressedPushRouter
{
/// Create a new router with a request plane client
///
/// This is the unified constructor that works with any transport type.
/// The client is provided as a trait object, hiding the specific implementation.
pub
fn
new
(
req_
transport
:
Client
,
req_
client
:
Arc
<
dyn
RequestPlane
Client
>
,
resp_transport
:
Arc
<
tcp
::
server
::
TcpStreamServer
>
,
)
->
Result
<
Arc
<
Self
>>
{
Ok
(
Arc
::
new
(
Self
{
req_
transpor
t
,
req_
clien
t
,
resp_transport
,
}))
}
...
...
@@ -154,32 +155,22 @@ where
// TRANSPORT ABSTRACT REQUIRED - END HERE
tracing
::
trace!
(
request_id
,
"enqueueing two-part message to nats"
);
// Insert Trace Context into Headers
// Enables span to be created in push_endpoint before
// payload is parsed
// Prepare trace headers using the OpenTelemetry injector pattern
// This handles traceparent and tracestate headers according to W3C Trace Context standard
let
mut
headers
=
HeaderMap
::
new
();
inject_otel_context_into_nats_headers
(
&
mut
headers
,
None
);
// Send request using unified client interface
tracing
::
trace!
(
request_id
,
transport
=
self
.req_client
.transport_name
(),
address
=
%
address
,
"Sending request via request plane client"
);
// Add additional custom headers that aren't handled by the OpenTelemetry propagator
if
let
Some
(
trace_context
)
=
get_distributed_tracing_context
()
{
if
let
Some
(
x_request_id
)
=
trace_context
.x_request_id
{
headers
.insert
(
"x-request-id"
,
x_request_id
);
}
if
let
Some
(
x_dynamo_request_id
)
=
trace_context
.x_dynamo_request_id
{
headers
.insert
(
"x-dynamo-request-id"
,
x_dynamo_request_id
);
}
}
// Prepare trace headers using shared helper
let
mut
headers
=
std
::
collections
::
HashMap
::
new
();
inject_trace_headers_into_map
(
&
mut
headers
);
// 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
// Send request (works for all transport types)
let
_
response
=
self
.req_
transpor
t
.request
_with_headers
(
address
.to_string
()
,
headers
,
buffer
)
.req_
clien
t
.
send_
request
(
address
,
buffer
,
headers
)
.await
?
;
tracing
::
trace!
(
request_id
,
"awaiting transport handshake"
);
...
...
lib/runtime/src/pipeline/network/egress/http_router.rs
0 → 100644
View file @
06b0ebef
// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//! HTTP/2 client for request plane
use
super
::
unified_client
::{
Headers
,
RequestPlaneClient
};
use
crate
::
Result
;
use
async_trait
::
async_trait
;
use
bytes
::
Bytes
;
use
std
::
sync
::
Arc
;
use
std
::
time
::
Duration
;
/// Default timeout for HTTP requests (ack only, not full response)
const
DEFAULT_HTTP_REQUEST_TIMEOUT_SECS
:
u64
=
5
;
/// HTTP/2 Performance Configuration Constants
const
DEFAULT_MAX_FRAME_SIZE
:
u32
=
1024
*
1024
;
// 1MB frame size for better throughput
const
DEFAULT_MAX_CONCURRENT_STREAMS
:
u32
=
1000
;
// Allow more concurrent streams
const
DEFAULT_POOL_MAX_IDLE_PER_HOST
:
usize
=
100
;
// Increased connection pool
const
DEFAULT_POOL_IDLE_TIMEOUT_SECS
:
u64
=
90
;
// Keep connections alive longer
const
DEFAULT_HTTP2_KEEP_ALIVE_INTERVAL_SECS
:
u64
=
30
;
// Send pings every 30s
const
DEFAULT_HTTP2_KEEP_ALIVE_TIMEOUT_SECS
:
u64
=
10
;
// Timeout for ping responses
const
DEFAULT_HTTP2_ADAPTIVE_WINDOW
:
bool
=
true
;
// Enable adaptive flow control
/// HTTP/2 Performance Configuration
#[derive(Debug,
Clone)]
pub
struct
Http2Config
{
pub
max_frame_size
:
u32
,
pub
max_concurrent_streams
:
u32
,
pub
pool_max_idle_per_host
:
usize
,
pub
pool_idle_timeout
:
Duration
,
pub
keep_alive_interval
:
Duration
,
pub
keep_alive_timeout
:
Duration
,
pub
adaptive_window
:
bool
,
pub
request_timeout
:
Duration
,
}
impl
Default
for
Http2Config
{
fn
default
()
->
Self
{
Self
{
max_frame_size
:
DEFAULT_MAX_FRAME_SIZE
,
max_concurrent_streams
:
DEFAULT_MAX_CONCURRENT_STREAMS
,
pool_max_idle_per_host
:
DEFAULT_POOL_MAX_IDLE_PER_HOST
,
pool_idle_timeout
:
Duration
::
from_secs
(
DEFAULT_POOL_IDLE_TIMEOUT_SECS
),
keep_alive_interval
:
Duration
::
from_secs
(
DEFAULT_HTTP2_KEEP_ALIVE_INTERVAL_SECS
),
keep_alive_timeout
:
Duration
::
from_secs
(
DEFAULT_HTTP2_KEEP_ALIVE_TIMEOUT_SECS
),
adaptive_window
:
DEFAULT_HTTP2_ADAPTIVE_WINDOW
,
request_timeout
:
Duration
::
from_secs
(
DEFAULT_HTTP_REQUEST_TIMEOUT_SECS
),
}
}
}
impl
Http2Config
{
/// Create configuration from environment variables
pub
fn
from_env
()
->
Self
{
let
mut
config
=
Self
::
default
();
if
let
Ok
(
val
)
=
std
::
env
::
var
(
"DYN_HTTP2_MAX_FRAME_SIZE"
)
&&
let
Ok
(
size
)
=
val
.parse
::
<
u32
>
()
{
config
.max_frame_size
=
size
;
}
if
let
Ok
(
val
)
=
std
::
env
::
var
(
"DYN_HTTP2_MAX_CONCURRENT_STREAMS"
)
&&
let
Ok
(
streams
)
=
val
.parse
::
<
u32
>
()
{
config
.max_concurrent_streams
=
streams
;
}
if
let
Ok
(
val
)
=
std
::
env
::
var
(
"DYN_HTTP2_POOL_MAX_IDLE_PER_HOST"
)
&&
let
Ok
(
pool_size
)
=
val
.parse
::
<
usize
>
()
{
config
.pool_max_idle_per_host
=
pool_size
;
}
if
let
Ok
(
val
)
=
std
::
env
::
var
(
"DYN_HTTP2_POOL_IDLE_TIMEOUT_SECS"
)
&&
let
Ok
(
timeout
)
=
val
.parse
::
<
u64
>
()
{
config
.pool_idle_timeout
=
Duration
::
from_secs
(
timeout
);
}
if
let
Ok
(
val
)
=
std
::
env
::
var
(
"DYN_HTTP2_KEEP_ALIVE_INTERVAL_SECS"
)
&&
let
Ok
(
interval
)
=
val
.parse
::
<
u64
>
()
{
config
.keep_alive_interval
=
Duration
::
from_secs
(
interval
);
}
if
let
Ok
(
val
)
=
std
::
env
::
var
(
"DYN_HTTP2_KEEP_ALIVE_TIMEOUT_SECS"
)
&&
let
Ok
(
timeout
)
=
val
.parse
::
<
u64
>
()
{
config
.keep_alive_timeout
=
Duration
::
from_secs
(
timeout
);
}
if
let
Ok
(
val
)
=
std
::
env
::
var
(
"DYN_HTTP2_ADAPTIVE_WINDOW"
)
{
config
.adaptive_window
=
val
.parse
()
.unwrap_or
(
DEFAULT_HTTP2_ADAPTIVE_WINDOW
);
}
if
let
Ok
(
val
)
=
std
::
env
::
var
(
"DYN_HTTP_REQUEST_TIMEOUT"
)
&&
let
Ok
(
timeout
)
=
val
.parse
::
<
u64
>
()
{
config
.request_timeout
=
Duration
::
from_secs
(
timeout
);
}
config
}
}
/// HTTP/2 request plane client
pub
struct
HttpRequestClient
{
client
:
reqwest
::
Client
,
config
:
Http2Config
,
}
impl
HttpRequestClient
{
/// Create a new HTTP request client with HTTP/2 and default configuration
pub
fn
new
()
->
Result
<
Self
>
{
Self
::
with_config
(
Http2Config
::
default
())
}
/// Create a new HTTP request client with custom timeout (legacy method)
/// Uses HTTP/2 with prior knowledge to avoid ALPN negotiation overhead
pub
fn
with_timeout
(
timeout
:
Duration
)
->
Result
<
Self
>
{
let
config
=
Http2Config
{
request_timeout
:
timeout
,
..
Http2Config
::
default
()
};
Self
::
with_config
(
config
)
}
/// Create a new HTTP request client with basic configuration
///
/// Note: Advanced HTTP/2 configuration methods may not be available in all versions of reqwest.
/// This implementation uses only the stable, widely-supported configuration options.
pub
fn
with_config
(
config
:
Http2Config
)
->
Result
<
Self
>
{
let
builder
=
reqwest
::
Client
::
builder
()
.pool_max_idle_per_host
(
config
.pool_max_idle_per_host
)
.pool_idle_timeout
(
config
.pool_idle_timeout
)
.timeout
(
config
.request_timeout
);
// HTTP/2 is automatically negotiated by reqwest when available
let
client
=
builder
.build
()
?
;
Ok
(
Self
{
client
,
config
})
}
/// Create from environment configuration
pub
fn
from_env
()
->
Result
<
Self
>
{
Self
::
with_config
(
Http2Config
::
from_env
())
}
/// Get the current HTTP/2 configuration
pub
fn
config
(
&
self
)
->
&
Http2Config
{
&
self
.config
}
}
impl
Default
for
HttpRequestClient
{
fn
default
()
->
Self
{
Self
::
new
()
.expect
(
"Failed to create HTTP request client"
)
}
}
#[async_trait]
impl
RequestPlaneClient
for
HttpRequestClient
{
async
fn
send_request
(
&
self
,
address
:
String
,
payload
:
Bytes
,
headers
:
Headers
,
)
->
Result
<
Bytes
>
{
let
mut
req
=
self
.client
.post
(
&
address
)
.header
(
"Content-Type"
,
"application/octet-stream"
)
.body
(
payload
);
// Add custom headers
for
(
key
,
value
)
in
headers
{
req
=
req
.header
(
key
,
value
);
}
let
response
=
req
.send
()
.await
?
;
if
!
response
.status
()
.is_success
()
{
anyhow
::
bail!
(
"HTTP request failed with status {}: {}"
,
response
.status
(),
response
.text
()
.await
.unwrap_or_default
()
);
}
let
body
=
response
.bytes
()
.await
?
;
Ok
(
body
)
}
fn
transport_name
(
&
self
)
->
&
'static
str
{
"http2"
}
fn
is_healthy
(
&
self
)
->
bool
{
// HTTP client is stateless and always healthy if created successfully
true
}
}
#[cfg(test)]
mod
tests
{
use
super
::
*
;
use
axum
::{
Router
,
body
::
Bytes
as
AxumBytes
,
extract
::
State
as
AxumState
,
routing
::
post
};
use
std
::
sync
::
Arc
;
use
tokio
::
sync
::
Mutex
as
TokioMutex
;
#[test]
fn
test_http_client_creation
()
{
let
client
=
HttpRequestClient
::
new
();
assert
!
(
client
.is_ok
());
}
#[test]
fn
test_http_client_with_custom_timeout
()
{
let
client
=
HttpRequestClient
::
with_timeout
(
Duration
::
from_secs
(
10
));
assert
!
(
client
.is_ok
());
assert_eq!
(
client
.unwrap
()
.config.request_timeout
,
Duration
::
from_secs
(
10
)
);
}
#[test]
fn
test_http2_config_from_env
()
{
// Set environment variables
unsafe
{
std
::
env
::
set_var
(
"DYN_HTTP2_MAX_FRAME_SIZE"
,
"2097152"
);
// 2MB
std
::
env
::
set_var
(
"DYN_HTTP2_MAX_CONCURRENT_STREAMS"
,
"2000"
);
std
::
env
::
set_var
(
"DYN_HTTP2_POOL_MAX_IDLE_PER_HOST"
,
"200"
);
std
::
env
::
set_var
(
"DYN_HTTP2_KEEP_ALIVE_INTERVAL_SECS"
,
"60"
);
std
::
env
::
set_var
(
"DYN_HTTP2_ADAPTIVE_WINDOW"
,
"false"
);
}
let
config
=
Http2Config
::
from_env
();
assert_eq!
(
config
.max_frame_size
,
2097152
);
assert_eq!
(
config
.max_concurrent_streams
,
2000
);
assert_eq!
(
config
.pool_max_idle_per_host
,
200
);
assert_eq!
(
config
.keep_alive_interval
,
Duration
::
from_secs
(
60
));
assert
!
(
!
config
.adaptive_window
);
// Clean up
unsafe
{
std
::
env
::
remove_var
(
"DYN_HTTP2_MAX_FRAME_SIZE"
);
std
::
env
::
remove_var
(
"DYN_HTTP2_MAX_CONCURRENT_STREAMS"
);
std
::
env
::
remove_var
(
"DYN_HTTP2_POOL_MAX_IDLE_PER_HOST"
);
std
::
env
::
remove_var
(
"DYN_HTTP2_KEEP_ALIVE_INTERVAL_SECS"
);
std
::
env
::
remove_var
(
"DYN_HTTP2_ADAPTIVE_WINDOW"
);
}
}
#[test]
fn
test_http_client_with_custom_config
()
{
let
config
=
Http2Config
{
max_frame_size
:
512
*
1024
,
// 512KB
max_concurrent_streams
:
500
,
pool_max_idle_per_host
:
75
,
pool_idle_timeout
:
Duration
::
from_secs
(
60
),
keep_alive_interval
:
Duration
::
from_secs
(
45
),
keep_alive_timeout
:
Duration
::
from_secs
(
15
),
adaptive_window
:
false
,
request_timeout
:
Duration
::
from_secs
(
8
),
};
let
client
=
HttpRequestClient
::
with_config
(
config
.clone
());
assert
!
(
client
.is_ok
());
let
client
=
client
.unwrap
();
assert_eq!
(
client
.config.max_frame_size
,
512
*
1024
);
assert_eq!
(
client
.config.max_concurrent_streams
,
500
);
assert_eq!
(
client
.config.pool_max_idle_per_host
,
75
);
assert_eq!
(
client
.config.request_timeout
,
Duration
::
from_secs
(
8
));
}
#[tokio::test]
async
fn
test_http_client_send_request_invalid_url
()
{
let
client
=
HttpRequestClient
::
new
()
.unwrap
();
let
result
=
client
.send_request
(
"http://invalid-host-that-does-not-exist:9999/test"
.to_string
(),
Bytes
::
from
(
"test"
),
std
::
collections
::
HashMap
::
new
(),
)
.await
;
assert
!
(
result
.is_err
());
}
#[tokio::test]
async
fn
test_http2_client_server_integration
()
{
use
hyper_util
::
rt
::{
TokioExecutor
,
TokioIo
};
use
hyper_util
::
server
::
conn
::
auto
::
Builder
as
ConnBuilder
;
use
hyper_util
::
service
::
TowerToHyperService
;
// Create a test server that accepts HTTP/2
#[derive(Clone)]
struct
TestState
{
received
:
Arc
<
TokioMutex
<
Vec
<
Bytes
>>>
,
protocol_version
:
Arc
<
TokioMutex
<
Option
<
String
>>>
,
}
async
fn
test_handler
(
AxumState
(
state
):
AxumState
<
TestState
>
,
body
:
AxumBytes
,
)
->
&
'static
str
{
state
.received
.lock
()
.await
.push
(
body
);
"OK"
}
let
state
=
TestState
{
received
:
Arc
::
new
(
TokioMutex
::
new
(
Vec
::
new
())),
protocol_version
:
Arc
::
new
(
TokioMutex
::
new
(
None
)),
};
let
app
=
Router
::
new
()
.route
(
"/test"
,
post
(
test_handler
))
.with_state
(
state
.clone
());
// Bind to a random port
let
listener
=
tokio
::
net
::
TcpListener
::
bind
(
"127.0.0.1:0"
)
.await
.unwrap
();
let
addr
=
listener
.local_addr
()
.unwrap
();
// Start HTTP/2 server
let
server_handle
=
tokio
::
spawn
(
async
move
{
loop
{
let
Ok
((
stream
,
_
))
=
listener
.accept
()
.await
else
{
break
;
};
let
app
=
app
.clone
();
tokio
::
spawn
(
async
move
{
let
conn_builder
=
ConnBuilder
::
new
(
TokioExecutor
::
new
());
let
io
=
TokioIo
::
new
(
stream
);
let
tower_service
=
app
.into_service
();
let
hyper_service
=
TowerToHyperService
::
new
(
tower_service
);
let
_
=
conn_builder
.serve_connection
(
io
,
hyper_service
)
.await
;
});
}
});
// Give server time to start
tokio
::
time
::
sleep
(
Duration
::
from_millis
(
100
))
.await
;
// Create HTTP/2 client with prior knowledge
let
client
=
HttpRequestClient
::
new
()
.unwrap
();
// Send request
let
test_data
=
Bytes
::
from
(
"test_payload"
);
let
result
=
client
.send_request
(
format!
(
"http://{}/test"
,
addr
),
test_data
.clone
(),
std
::
collections
::
HashMap
::
new
(),
)
.await
;
// Verify request succeeded
assert
!
(
result
.is_ok
(),
"Request failed: {:?}"
,
result
.err
());
// Verify server received the data
tokio
::
time
::
sleep
(
Duration
::
from_millis
(
100
))
.await
;
let
received
=
state
.received
.lock
()
.await
;
assert_eq!
(
received
.len
(),
1
);
assert_eq!
(
received
[
0
],
test_data
);
// Cleanup
server_handle
.abort
();
}
#[tokio::test]
async
fn
test_http2_headers_propagation
()
{
use
hyper_util
::
rt
::{
TokioExecutor
,
TokioIo
};
use
hyper_util
::
server
::
conn
::
auto
::
Builder
as
ConnBuilder
;
use
hyper_util
::
service
::
TowerToHyperService
;
// Create a test server that captures headers
#[derive(Clone)]
struct
HeaderState
{
headers
:
Arc
<
TokioMutex
<
Vec
<
(
String
,
String
)
>>>
,
}
async
fn
header_handler
(
AxumState
(
state
):
AxumState
<
HeaderState
>
,
headers
:
axum
::
http
::
HeaderMap
,
)
->
&
'static
str
{
let
mut
captured
=
state
.headers
.lock
()
.await
;
for
(
name
,
value
)
in
headers
.iter
()
{
if
let
Ok
(
val_str
)
=
value
.to_str
()
{
captured
.push
((
name
.to_string
(),
val_str
.to_string
()));
}
}
"OK"
}
let
state
=
HeaderState
{
headers
:
Arc
::
new
(
TokioMutex
::
new
(
Vec
::
new
())),
};
let
app
=
Router
::
new
()
.route
(
"/test"
,
post
(
header_handler
))
.with_state
(
state
.clone
());
// Bind to a random port
let
listener
=
tokio
::
net
::
TcpListener
::
bind
(
"127.0.0.1:0"
)
.await
.unwrap
();
let
addr
=
listener
.local_addr
()
.unwrap
();
// Start HTTP/2 server
let
server_handle
=
tokio
::
spawn
(
async
move
{
loop
{
let
Ok
((
stream
,
_
))
=
listener
.accept
()
.await
else
{
break
;
};
let
app
=
app
.clone
();
tokio
::
spawn
(
async
move
{
let
conn_builder
=
ConnBuilder
::
new
(
TokioExecutor
::
new
());
let
io
=
TokioIo
::
new
(
stream
);
let
tower_service
=
app
.into_service
();
let
hyper_service
=
TowerToHyperService
::
new
(
tower_service
);
let
_
=
conn_builder
.serve_connection
(
io
,
hyper_service
)
.await
;
});
}
});
// Give server time to start
tokio
::
time
::
sleep
(
Duration
::
from_millis
(
100
))
.await
;
// Create HTTP/2 client
let
client
=
HttpRequestClient
::
new
()
.unwrap
();
// Send request with custom headers
let
mut
headers
=
std
::
collections
::
HashMap
::
new
();
headers
.insert
(
"x-test-header"
.to_string
(),
"test-value"
.to_string
());
headers
.insert
(
"x-request-id"
.to_string
(),
"req-123"
.to_string
());
let
result
=
client
.send_request
(
format!
(
"http://{}/test"
,
addr
),
Bytes
::
from
(
"test"
),
headers
,
)
.await
;
// Verify request succeeded
assert
!
(
result
.is_ok
());
// Verify headers were received
tokio
::
time
::
sleep
(
Duration
::
from_millis
(
100
))
.await
;
let
received_headers
=
state
.headers
.lock
()
.await
;
let
header_map
:
std
::
collections
::
HashMap
<
_
,
_
>
=
received_headers
.iter
()
.map
(|(
k
,
v
)|
(
k
.as_str
(),
v
.as_str
()))
.collect
();
assert
!
(
header_map
.contains_key
(
"x-test-header"
));
assert_eq!
(
header_map
.get
(
"x-test-header"
),
Some
(
&
"test-value"
));
assert
!
(
header_map
.contains_key
(
"x-request-id"
));
assert_eq!
(
header_map
.get
(
"x-request-id"
),
Some
(
&
"req-123"
));
// Cleanup
server_handle
.abort
();
}
#[tokio::test]
async
fn
test_http2_concurrent_requests
()
{
use
hyper_util
::
rt
::{
TokioExecutor
,
TokioIo
};
use
hyper_util
::
server
::
conn
::
auto
::
Builder
as
ConnBuilder
;
use
hyper_util
::
service
::
TowerToHyperService
;
use
std
::
sync
::
atomic
::{
AtomicU64
,
Ordering
};
// Create a test server that counts requests
#[derive(Clone)]
struct
CounterState
{
count
:
Arc
<
AtomicU64
>
,
}
async
fn
counter_handler
(
AxumState
(
state
):
AxumState
<
CounterState
>
)
->
String
{
let
count
=
state
.count
.fetch_add
(
1
,
Ordering
::
SeqCst
);
format!
(
"{}"
,
count
)
}
let
state
=
CounterState
{
count
:
Arc
::
new
(
AtomicU64
::
new
(
0
)),
};
let
app
=
Router
::
new
()
.route
(
"/test"
,
post
(
counter_handler
))
.with_state
(
state
.clone
());
// Bind to a random port
let
listener
=
tokio
::
net
::
TcpListener
::
bind
(
"127.0.0.1:0"
)
.await
.unwrap
();
let
addr
=
listener
.local_addr
()
.unwrap
();
// Start HTTP/2 server
let
server_handle
=
tokio
::
spawn
(
async
move
{
loop
{
let
Ok
((
stream
,
_
))
=
listener
.accept
()
.await
else
{
break
;
};
let
app
=
app
.clone
();
tokio
::
spawn
(
async
move
{
let
conn_builder
=
ConnBuilder
::
new
(
TokioExecutor
::
new
());
let
io
=
TokioIo
::
new
(
stream
);
let
tower_service
=
app
.into_service
();
let
hyper_service
=
TowerToHyperService
::
new
(
tower_service
);
let
_
=
conn_builder
.serve_connection
(
io
,
hyper_service
)
.await
;
});
}
});
// Give server time to start
tokio
::
time
::
sleep
(
Duration
::
from_millis
(
100
))
.await
;
// Create HTTP/2 client
let
client
=
Arc
::
new
(
HttpRequestClient
::
new
()
.unwrap
());
// Send multiple concurrent requests (HTTP/2 multiplexing)
let
mut
handles
=
vec!
[];
for
_
in
0
..
10
{
let
client
=
client
.clone
();
let
handle
=
tokio
::
spawn
(
async
move
{
client
.send_request
(
format!
(
"http://{}/test"
,
addr
),
Bytes
::
from
(
"test"
),
std
::
collections
::
HashMap
::
new
(),
)
.await
});
handles
.push
(
handle
);
}
// Wait for all requests to complete
let
mut
success_count
=
0
;
for
handle
in
handles
{
if
let
Ok
(
Ok
(
_
))
=
handle
.await
{
success_count
+=
1
;
}
}
// Verify all requests succeeded
assert_eq!
(
success_count
,
10
);
// Verify server received all requests
assert_eq!
(
state
.count
.load
(
Ordering
::
SeqCst
),
10
);
// Cleanup
server_handle
.abort
();
}
#[tokio::test]
async
fn
test_http2_performance_benchmark
()
{
use
hyper_util
::
rt
::{
TokioExecutor
,
TokioIo
};
use
hyper_util
::
server
::
conn
::
auto
::
Builder
as
ConnBuilder
;
use
hyper_util
::
service
::
TowerToHyperService
;
use
std
::
sync
::
atomic
::{
AtomicU64
,
Ordering
};
use
std
::
time
::
Instant
;
// Create a test server that measures performance
#[derive(Clone)]
struct
PerfState
{
request_count
:
Arc
<
AtomicU64
>
,
total_bytes
:
Arc
<
AtomicU64
>
,
}
async
fn
perf_handler
(
AxumState
(
state
):
AxumState
<
PerfState
>
,
body
:
AxumBytes
,
)
->
&
'static
str
{
state
.request_count
.fetch_add
(
1
,
Ordering
::
Relaxed
);
state
.total_bytes
.fetch_add
(
body
.len
()
as
u64
,
Ordering
::
Relaxed
);
"OK"
}
let
state
=
PerfState
{
request_count
:
Arc
::
new
(
AtomicU64
::
new
(
0
)),
total_bytes
:
Arc
::
new
(
AtomicU64
::
new
(
0
)),
};
let
app
=
Router
::
new
()
.route
(
"/perf"
,
post
(
perf_handler
))
.with_state
(
state
.clone
());
// Bind to a random port
let
listener
=
tokio
::
net
::
TcpListener
::
bind
(
"127.0.0.1:0"
)
.await
.unwrap
();
let
addr
=
listener
.local_addr
()
.unwrap
();
// Start HTTP/2 server
let
server_handle
=
tokio
::
spawn
(
async
move
{
loop
{
let
Ok
((
stream
,
_
))
=
listener
.accept
()
.await
else
{
break
;
};
let
app
=
app
.clone
();
tokio
::
spawn
(
async
move
{
let
conn_builder
=
ConnBuilder
::
new
(
TokioExecutor
::
new
());
let
io
=
TokioIo
::
new
(
stream
);
let
tower_service
=
app
.into_service
();
let
hyper_service
=
TowerToHyperService
::
new
(
tower_service
);
let
_
=
conn_builder
.serve_connection
(
io
,
hyper_service
)
.await
;
});
}
});
// Give server time to start
tokio
::
time
::
sleep
(
Duration
::
from_millis
(
100
))
.await
;
// Create optimized HTTP/2 client
let
optimized_config
=
Http2Config
{
max_frame_size
:
1024
*
1024
,
// 1MB frames
max_concurrent_streams
:
1000
,
pool_max_idle_per_host
:
100
,
pool_idle_timeout
:
Duration
::
from_secs
(
90
),
keep_alive_interval
:
Duration
::
from_secs
(
30
),
keep_alive_timeout
:
Duration
::
from_secs
(
10
),
adaptive_window
:
true
,
request_timeout
:
Duration
::
from_secs
(
30
),
};
let
client
=
Arc
::
new
(
HttpRequestClient
::
with_config
(
optimized_config
)
.unwrap
());
// Performance test: Send many concurrent requests
let
num_requests
=
100
;
let
payload_size
=
64
*
1024
;
// 64KB payload
let
payload
=
Bytes
::
from
(
vec!
[
0u8
;
payload_size
]);
let
start_time
=
Instant
::
now
();
let
mut
handles
=
vec!
[];
for
_
in
0
..
num_requests
{
let
client
=
client
.clone
();
let
payload
=
payload
.clone
();
let
handle
=
tokio
::
spawn
(
async
move
{
let
headers
=
std
::
collections
::
HashMap
::
new
();
client
.send_request
(
format!
(
"http://{}/perf"
,
addr
),
payload
,
headers
)
.await
});
handles
.push
(
handle
);
}
// Wait for all requests to complete
let
mut
successful_requests
=
0
;
for
handle
in
handles
{
if
handle
.await
.unwrap
()
.is_ok
()
{
successful_requests
+=
1
;
}
}
let
elapsed
=
start_time
.elapsed
();
let
requests_per_sec
=
successful_requests
as
f64
/
elapsed
.as_secs_f64
();
let
throughput_mbps
=
(
successful_requests
*
payload_size
)
as
f64
/
elapsed
.as_secs_f64
()
/
(
1024.0
*
1024.0
);
println!
(
"Performance Results:"
);
println!
(
" Successful requests: {}/{}"
,
successful_requests
,
num_requests
);
println!
(
" Total time: {:?}"
,
elapsed
);
println!
(
" Requests/sec: {:.2}"
,
requests_per_sec
);
println!
(
" Throughput: {:.2} MB/s"
,
throughput_mbps
);
// Verify server received all requests
let
server_count
=
state
.request_count
.load
(
Ordering
::
Relaxed
);
let
server_bytes
=
state
.total_bytes
.load
(
Ordering
::
Relaxed
);
assert_eq!
(
server_count
,
successful_requests
as
u64
);
assert_eq!
(
server_bytes
,
(
successful_requests
*
payload_size
)
as
u64
);
// Performance assertions (adjust based on your requirements)
assert
!
(
successful_requests
>=
num_requests
*
95
/
100
);
// At least 95% success rate
assert
!
(
requests_per_sec
>
50.0
);
// At least 50 requests per second
assert
!
(
throughput_mbps
>
10.0
);
// At least 10 MB/s throughput
// Cleanup
server_handle
.abort
();
}
}
lib/runtime/src/pipeline/network/egress/nats_client.rs
0 → 100644
View file @
06b0ebef
// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//! NATS Request Plane Client
//!
//! Wraps the NATS client to implement the unified RequestPlaneClient trait,
//! providing a consistent interface across all transport types.
use
super
::
unified_client
::{
ClientStats
,
Headers
,
RequestPlaneClient
};
use
anyhow
::
Result
;
use
async_trait
::
async_trait
;
use
bytes
::
Bytes
;
/// NATS implementation of RequestPlaneClient
///
/// This client wraps the async_nats::Client and adapts it to the
/// unified RequestPlaneClient interface.
pub
struct
NatsRequestClient
{
client
:
async_nats
::
Client
,
}
impl
NatsRequestClient
{
/// Create a new NATS request client
///
/// # Arguments
///
/// * `client` - The underlying NATS client
pub
fn
new
(
client
:
async_nats
::
Client
)
->
Self
{
Self
{
client
}
}
}
#[async_trait]
impl
RequestPlaneClient
for
NatsRequestClient
{
async
fn
send_request
(
&
self
,
address
:
String
,
payload
:
Bytes
,
headers
:
Headers
,
)
->
Result
<
Bytes
>
{
// Convert generic headers to NATS headers
let
mut
nats_headers
=
async_nats
::
HeaderMap
::
new
();
for
(
key
,
value
)
in
headers
{
nats_headers
.insert
(
key
.as_str
(),
value
.as_str
());
}
// Send request with headers
let
response
=
self
.client
.request_with_headers
(
address
,
nats_headers
,
payload
)
.await
.map_err
(|
e
|
anyhow
::
anyhow!
(
"NATS request failed: {}"
,
e
))
?
;
Ok
(
response
.payload
)
}
fn
transport_name
(
&
self
)
->
&
'static
str
{
"nats"
}
fn
is_healthy
(
&
self
)
->
bool
{
// Check if NATS client is connected
// NATS client doesn't expose connection state directly, assume healthy
true
}
fn
stats
(
&
self
)
->
ClientStats
{
// NATS client doesn't expose detailed stats
// Return basic health indicator
ClientStats
{
requests_sent
:
0
,
responses_received
:
0
,
errors
:
0
,
bytes_sent
:
0
,
bytes_received
:
0
,
active_connections
:
if
self
.is_healthy
()
{
1
}
else
{
0
},
idle_connections
:
0
,
avg_latency_us
:
0
,
}
}
async
fn
close
(
&
self
)
->
Result
<
()
>
{
// NATS client doesn't have an explicit close method
// Connection is managed by the client lifecycle
Ok
(())
}
}
lib/runtime/src/pipeline/network/egress/push_router.rs
View file @
06b0ebef
...
...
@@ -89,13 +89,17 @@ impl RouterMode {
}
async
fn
addressed_router
(
endpoint
:
&
Endpoint
)
->
anyhow
::
Result
<
Arc
<
AddressedPushRouter
>>
{
let
Some
(
nats_client
)
=
endpoint
.drt
()
.nats_client
()
else
{
anyhow
::
bail!
(
"Missing NATS. Please ensure it is running and accessible."
);
};
AddressedPushRouter
::
new
(
nats_client
.client
()
.clone
(),
endpoint
.drt
()
.tcp_server
()
.await
?
,
)
// Get network manager and create client (no mode checks!)
let
manager
=
endpoint
.drt
()
.network_manager
()
.await
?
;
let
req_client
=
manager
.create_client
()
?
;
let
resp_transport
=
endpoint
.drt
()
.tcp_server
()
.await
?
;
tracing
::
debug!
(
transport
=
req_client
.transport_name
(),
"Creating AddressedPushRouter with request plane client"
);
AddressedPushRouter
::
new
(
req_client
,
resp_transport
)
}
impl
<
T
,
U
>
PushRouter
<
T
,
U
>
...
...
@@ -224,8 +228,48 @@ where
}
}
let
subject
=
self
.client.endpoint
.subject_to
(
instance_id
);
let
request
=
request
.map
(|
req
|
AddressedRequest
::
new
(
req
,
subject
));
// Get the address based on discovered transport type
let
address
=
{
use
crate
::
component
::
TransportType
;
// Get the instance and use its actual transport type
let
instances
=
self
.client
.instances
();
let
instance
=
instances
.iter
()
.find
(|
i
|
i
.instance_id
==
instance_id
)
.ok_or_else
(||
{
anyhow
::
anyhow!
(
"Instance {} not found in available instances"
,
instance_id
)
})
?
;
match
&
instance
.transport
{
TransportType
::
Http
(
http_endpoint
)
=>
{
tracing
::
debug!
(
instance_id
=
instance_id
,
http_endpoint
=
%
http_endpoint
,
"Using HTTP transport for instance"
);
http_endpoint
.clone
()
}
TransportType
::
Tcp
(
tcp_endpoint
)
=>
{
tracing
::
debug!
(
instance_id
=
instance_id
,
tcp_endpoint
=
%
tcp_endpoint
,
"Using TCP transport for instance"
);
tcp_endpoint
.clone
()
}
TransportType
::
Nats
(
subject
)
=>
{
tracing
::
debug!
(
instance_id
=
instance_id
,
subject
=
%
subject
,
"Using NATS transport for instance"
);
subject
.clone
()
}
}
};
let
request
=
request
.map
(|
req
|
AddressedRequest
::
new
(
req
,
address
));
let
stream
:
anyhow
::
Result
<
ManyOut
<
U
>>
=
self
.addressed
.generate
(
request
)
.await
;
match
stream
{
...
...
lib/runtime/src/pipeline/network/egress/queue.rs
deleted
100644 → 0
View file @
381c428c
// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
lib/runtime/src/pipeline/network/egress/tcp_client.rs
0 → 100644
View file @
06b0ebef
// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//! TCP Request Plane Client
//!
use
super
::
unified_client
::{
ClientStats
,
Headers
,
RequestPlaneClient
};
use
anyhow
::
Result
;
use
async_trait
::
async_trait
;
use
bytes
::
Bytes
;
use
dashmap
::
DashMap
;
use
futures
::
StreamExt
;
use
std
::
io
::
IoSlice
;
use
std
::
net
::
SocketAddr
;
use
std
::
sync
::
Arc
;
use
std
::
sync
::
atomic
::{
AtomicBool
,
AtomicU64
,
Ordering
};
use
std
::
time
::
Duration
;
use
tokio
::
io
::{
AsyncReadExt
,
AsyncWriteExt
};
use
tokio
::
net
::
TcpStream
;
use
tokio
::
sync
::{
Mutex
,
mpsc
,
oneshot
};
use
tokio
::
task
::
JoinHandle
;
use
tokio_util
::
codec
::
FramedRead
;
/// Default timeout for TCP request acknowledgment
const
DEFAULT_TCP_REQUEST_TIMEOUT_SECS
:
u64
=
5
;
/// Default connection pool size per host
const
DEFAULT_POOL_SIZE
:
usize
=
100
;
/// Buffer size for request channel per connection (backpressure control)
const
REQUEST_CHANNEL_BUFFER
:
usize
=
50
;
/// Pre-allocated read buffer size (64KB typical message size)
const
READ_BUFFER_SIZE
:
usize
=
65536
;
/// Default maximum message size for TCP client (32 MB)
/// This is the limit for a SINGLE message. For larger data, split into multiple messages.
const
DEFAULT_MAX_MESSAGE_SIZE
:
usize
=
32
*
1024
*
1024
;
/// Get maximum message size from environment or use default
fn
get_max_message_size
()
->
usize
{
std
::
env
::
var
(
"DYN_TCP_MAX_MESSAGE_SIZE"
)
.ok
()
.and_then
(|
s
|
s
.parse
::
<
usize
>
()
.ok
())
.unwrap_or
(
DEFAULT_MAX_MESSAGE_SIZE
)
}
/// TCP request plane configuration
#[derive(Debug,
Clone)]
pub
struct
TcpRequestConfig
{
/// Request timeout
pub
request_timeout
:
Duration
,
/// Maximum connections per host
pub
pool_size
:
usize
,
/// Connect timeout
pub
connect_timeout
:
Duration
,
/// Request channel buffer size
pub
channel_buffer
:
usize
,
}
impl
Default
for
TcpRequestConfig
{
fn
default
()
->
Self
{
Self
{
request_timeout
:
Duration
::
from_secs
(
DEFAULT_TCP_REQUEST_TIMEOUT_SECS
),
pool_size
:
DEFAULT_POOL_SIZE
,
connect_timeout
:
Duration
::
from_secs
(
5
),
channel_buffer
:
REQUEST_CHANNEL_BUFFER
,
}
}
}
impl
TcpRequestConfig
{
/// Create configuration from environment variables
pub
fn
from_env
()
->
Self
{
let
mut
config
=
Self
::
default
();
if
let
Ok
(
val
)
=
std
::
env
::
var
(
"DYN_TCP_REQUEST_TIMEOUT"
)
&&
let
Ok
(
timeout
)
=
val
.parse
::
<
u64
>
()
{
config
.request_timeout
=
Duration
::
from_secs
(
timeout
);
}
if
let
Ok
(
val
)
=
std
::
env
::
var
(
"DYN_TCP_POOL_SIZE"
)
&&
let
Ok
(
size
)
=
val
.parse
::
<
usize
>
()
{
config
.pool_size
=
size
;
}
if
let
Ok
(
val
)
=
std
::
env
::
var
(
"DYN_TCP_CONNECT_TIMEOUT"
)
&&
let
Ok
(
timeout
)
=
val
.parse
::
<
u64
>
()
{
config
.connect_timeout
=
Duration
::
from_secs
(
timeout
);
}
if
let
Ok
(
val
)
=
std
::
env
::
var
(
"DYN_TCP_CHANNEL_BUFFER"
)
&&
let
Ok
(
size
)
=
val
.parse
::
<
usize
>
()
{
config
.channel_buffer
=
size
;
}
config
}
}
/// Request to be sent over TCP
/// Pre-encoded on caller's thread for optimal write performance (hot path optimization)
struct
TcpRequest
{
/// Pre-encoded request data ready to send (zero-copy Bytes)
/// Encoding happens on caller thread to parallelize across multiple request handlers
encoded_data
:
Bytes
,
/// Oneshot channel to send response back to caller
response_tx
:
oneshot
::
Sender
<
Result
<
Bytes
>>
,
}
/// TCP connection with split read/write tasks
///
/// Design: One writer task + one reader task per connection
/// - Writer task receives pre-encoded requests and writes directly (hot path optimized)
/// - Reader task uses framed codec for robust protocol handling
/// - FIFO ordering ensures request/response correlation without explicit IDs
///
/// Performance: Hybrid approach optimizes each path independently:
/// - Write path: Pre-encode on caller thread → direct write (minimal overhead, parallel encoding)
/// - Read path: Framed codec handles partial reads and protocol complexity automatically
struct
TcpConnection
{
addr
:
SocketAddr
,
/// Channel to send requests to the writer task
request_tx
:
mpsc
::
Sender
<
TcpRequest
>
,
/// Writer task handle for cleanup
writer_handle
:
Arc
<
JoinHandle
<
()
>>
,
/// Reader task handle for cleanup
reader_handle
:
Arc
<
JoinHandle
<
()
>>
,
/// Health status (false if tasks have failed)
healthy
:
Arc
<
AtomicBool
>
,
}
impl
TcpConnection
{
/// Create a new connection with split read/write tasks
async
fn
connect
(
addr
:
SocketAddr
,
timeout
:
Duration
,
channel_buffer
:
usize
)
->
Result
<
Self
>
{
let
stream
=
tokio
::
time
::
timeout
(
timeout
,
TcpStream
::
connect
(
addr
))
.await
.map_err
(|
_
|
anyhow
::
anyhow!
(
"TCP connect timeout to {}"
,
addr
))
??
;
// Configure socket for lower latency
Self
::
configure_socket
(
&
stream
)
?
;
let
(
read_half
,
write_half
)
=
tokio
::
io
::
split
(
stream
);
// Channel for writer task to receive requests
let
(
request_tx
,
request_rx
)
=
mpsc
::
channel
::
<
TcpRequest
>
(
channel_buffer
);
// Channel for writer to forward response channels to reader (FIFO correlation)
let
(
response_tx_channel
,
response_rx_channel
)
=
mpsc
::
unbounded_channel
::
<
oneshot
::
Sender
<
Result
<
Bytes
>>>
();
let
healthy
=
Arc
::
new
(
AtomicBool
::
new
(
true
));
// Spawn writer task
let
writer_handle
=
{
let
healthy
=
healthy
.clone
();
tokio
::
spawn
(
async
move
{
if
let
Err
(
e
)
=
Self
::
writer_task
(
write_half
,
request_rx
,
response_tx_channel
)
.await
{
tracing
::
debug!
(
"Writer task failed for {}: {}"
,
addr
,
e
);
healthy
.store
(
false
,
Ordering
::
Relaxed
);
}
})
};
// Spawn reader task
let
reader_handle
=
{
let
healthy
=
healthy
.clone
();
tokio
::
spawn
(
async
move
{
if
let
Err
(
e
)
=
Self
::
reader_task
(
read_half
,
response_rx_channel
)
.await
{
tracing
::
debug!
(
"Reader task failed for {}: {}"
,
addr
,
e
);
healthy
.store
(
false
,
Ordering
::
Relaxed
);
}
})
};
Ok
(
Self
{
addr
,
request_tx
,
writer_handle
:
Arc
::
new
(
writer_handle
),
reader_handle
:
Arc
::
new
(
reader_handle
),
healthy
,
})
}
/// Configure socket for ultra-low latency based on dyn-transports patterns
fn
configure_socket
(
stream
:
&
TcpStream
)
->
Result
<
()
>
{
use
socket2
::{
SockRef
,
Socket
};
let
sock_ref
=
SockRef
::
from
(
stream
);
// TCP_NODELAY - disable Nagle's algorithm for immediate send
sock_ref
.set_nodelay
(
true
)
?
;
// Increase socket buffer sizes for better throughput under load
sock_ref
.set_recv_buffer_size
(
2
*
1024
*
1024
)
?
;
// 2MB
sock_ref
.set_send_buffer_size
(
2
*
1024
*
1024
)
?
;
// 2MB
// Advanced Linux optimizations for ultra-low latency (optional feature)
#[cfg(feature
=
"tcp-low-latency"
)]
{
use
std
::
os
::
unix
::
io
::
AsRawFd
;
unsafe
{
let
fd
=
stream
.as_raw_fd
();
// TCP_QUICKACK - minimize ACK delay
let
quickack
:
libc
::
c_int
=
1
;
libc
::
setsockopt
(
fd
,
libc
::
SOL_TCP
,
libc
::
TCP_QUICKACK
,
&
quickack
as
*
const
_
as
*
const
libc
::
c_void
,
std
::
mem
::
size_of_val
(
&
quickack
)
as
libc
::
socklen_t
,
);
// SO_BUSY_POLL - enable busy polling for lower latency (50 microseconds)
let
busy_poll
:
libc
::
c_int
=
50
;
libc
::
setsockopt
(
fd
,
libc
::
SOL_SOCKET
,
libc
::
SO_BUSY_POLL
,
&
busy_poll
as
*
const
_
as
*
const
libc
::
c_void
,
std
::
mem
::
size_of_val
(
&
busy_poll
)
as
libc
::
socklen_t
,
);
}
tracing
::
debug!
(
"TCP low-latency optimizations enabled (TCP_QUICKACK, SO_BUSY_POLL)"
);
}
Ok
(())
}
/// Writer task: receives pre-encoded requests and writes directly to socket
///
/// Performance optimization: Pre-encoding happens on caller's thread to enable
/// parallel encoding across multiple request handlers, while this task focuses
/// on sequential socket writes with minimal overhead.
async
fn
writer_task
(
mut
write_half
:
tokio
::
io
::
WriteHalf
<
TcpStream
>
,
mut
request_rx
:
mpsc
::
Receiver
<
TcpRequest
>
,
response_tx_channel
:
mpsc
::
UnboundedSender
<
oneshot
::
Sender
<
Result
<
Bytes
>>>
,
)
->
Result
<
()
>
{
while
let
Some
(
req
)
=
request_rx
.recv
()
.await
{
// Direct write of pre-encoded data (hot path)
// With TCP_NODELAY, no need for explicit flush()
match
write_half
.write_all
(
&
req
.encoded_data
)
.await
{
Ok
(())
=>
{
// Forward response channel to reader task (FIFO ordering)
if
response_tx_channel
.send
(
req
.response_tx
)
.is_err
()
{
tracing
::
debug!
(
"Reader task closed, stopping writer"
);
break
;
}
}
Err
(
e
)
=>
{
// Write failed, notify caller and stop
let
err_msg
=
format!
(
"Write failed: {}"
,
e
);
let
_
=
req
.response_tx
.send
(
Err
(
anyhow
::
anyhow!
(
"{}"
,
err_msg
)));
return
Err
(
anyhow
::
anyhow!
(
"{}"
,
err_msg
));
}
}
}
Ok
(())
}
/// Reader task: reads responses using framed codec and sends them back via oneshot channels
/// Protocol framing handled automatically via TcpResponseCodec
async
fn
reader_task
(
read_half
:
tokio
::
io
::
ReadHalf
<
TcpStream
>
,
mut
response_rx_channel
:
mpsc
::
UnboundedReceiver
<
oneshot
::
Sender
<
Result
<
Bytes
>>>
,
)
->
Result
<
()
>
{
use
crate
::
pipeline
::
network
::
codec
::
TcpResponseCodec
;
let
max_message_size
=
get_max_message_size
();
let
codec
=
TcpResponseCodec
::
new
(
Some
(
max_message_size
));
let
mut
framed
=
FramedRead
::
new
(
read_half
,
codec
);
while
let
Some
(
response_tx
)
=
response_rx_channel
.recv
()
.await
{
// Read the next response message from the framed stream
// The codec handles all protocol framing and size checks automatically
match
framed
.next
()
.await
{
Some
(
Ok
(
response_msg
))
=>
{
let
_
=
response_tx
.send
(
Ok
(
response_msg
.data
));
}
Some
(
Err
(
e
))
=>
{
let
err
=
anyhow
::
anyhow!
(
"Failed to decode response: {}"
,
e
);
let
_
=
response_tx
.send
(
Err
(
err
));
return
Err
(
anyhow
::
anyhow!
(
"Failed to decode response"
));
}
None
=>
{
let
err
=
anyhow
::
anyhow!
(
"Connection closed by peer"
);
let
_
=
response_tx
.send
(
Err
(
err
));
return
Err
(
anyhow
::
anyhow!
(
"Connection closed"
));
}
}
}
Ok
(())
}
/// Send a request and wait for response
///
/// Performance: Encoding happens on caller's thread (hot path optimization)
/// to enable parallel encoding across multiple request handlers. The writer
/// task then performs sequential writes with minimal overhead.
async
fn
send_request
(
&
self
,
payload
:
Bytes
,
headers
:
&
Headers
)
->
Result
<
Bytes
>
{
use
crate
::
pipeline
::
network
::
codec
::
TcpRequestMessage
;
// Check health before sending
if
!
self
.healthy
.load
(
Ordering
::
Relaxed
)
{
anyhow
::
bail!
(
"Connection unhealthy (tasks failed)"
);
}
// Extract endpoint path from headers (required for routing)
let
endpoint_path
=
headers
.get
(
"x-endpoint-path"
)
.ok_or_else
(||
anyhow
::
anyhow!
(
"Missing x-endpoint-path header for TCP request"
))
?
.to_string
();
// Encode request on caller's thread (hot path optimization)
// This allows multiple concurrent callers to encode in parallel
// rather than serializing through the writer task
let
request_msg
=
TcpRequestMessage
::
new
(
endpoint_path
,
payload
);
let
encoded_data
=
request_msg
.encode
()
?
;
// Create response channel
let
(
response_tx
,
response_rx
)
=
oneshot
::
channel
();
// Send to writer task (bounded channel provides backpressure)
let
req
=
TcpRequest
{
encoded_data
,
response_tx
,
};
self
.request_tx
.send
(
req
)
.await
.map_err
(|
_
|
anyhow
::
anyhow!
(
"Writer task closed"
))
?
;
// Wait for response from reader task
response_rx
.await
.map_err
(|
_
|
anyhow
::
anyhow!
(
"Reader task closed"
))
?
}
/// Check if connection is healthy
fn
is_healthy
(
&
self
)
->
bool
{
self
.healthy
.load
(
Ordering
::
Relaxed
)
}
}
/// Connection pool with health checking for TCP connections
struct
TcpConnectionPool
{
pools
:
DashMap
<
SocketAddr
,
Arc
<
Mutex
<
Vec
<
TcpConnection
>>>>
,
config
:
TcpRequestConfig
,
}
impl
TcpConnectionPool
{
fn
new
(
config
:
TcpRequestConfig
)
->
Self
{
Self
{
pools
:
DashMap
::
new
(),
config
,
}
}
/// Get a connection from the pool or create a new one
/// Automatically filters out unhealthy connections
async
fn
get_connection
(
&
self
,
addr
:
SocketAddr
)
->
Result
<
TcpConnection
>
{
// Try to get from pool (lock-free read with DashMap)
if
let
Some
(
pool
)
=
self
.pools
.get
(
&
addr
)
{
let
mut
pool
=
pool
.lock
()
.await
;
// Try to get a healthy connection, discard unhealthy ones
while
let
Some
(
conn
)
=
pool
.pop
()
{
if
conn
.is_healthy
()
{
return
Ok
(
conn
);
}
else
{
tracing
::
debug!
(
"Discarding unhealthy connection for {}"
,
addr
);
// Connection will be dropped here, cleaning up tasks
}
}
}
// Create new connection with configured channel buffer
tracing
::
debug!
(
"Creating new TCP connection to {}"
,
addr
);
TcpConnection
::
connect
(
addr
,
self
.config.connect_timeout
,
self
.config.channel_buffer
,
)
.await
}
/// Return a connection to the pool if it's healthy and there's space
async
fn
return_connection
(
&
self
,
conn
:
TcpConnection
)
{
// Only return healthy connections
if
!
conn
.is_healthy
()
{
tracing
::
debug!
(
"Not returning unhealthy connection to pool"
);
return
;
}
let
addr
=
conn
.addr
;
// Get or create pool for this address (lock-free with DashMap)
let
pool
=
self
.pools
.entry
(
addr
)
.or_insert_with
(||
Arc
::
new
(
Mutex
::
new
(
Vec
::
new
())))
.clone
();
let
mut
pool
=
pool
.lock
()
.await
;
if
pool
.len
()
<
self
.config.pool_size
{
pool
.push
(
conn
);
}
else
{
tracing
::
debug!
(
"Connection pool full for {}, dropping connection"
,
addr
);
// Otherwise drop the connection (tasks will be cleaned up)
}
}
}
/// TCP request plane client
pub
struct
TcpRequestClient
{
pool
:
Arc
<
TcpConnectionPool
>
,
config
:
TcpRequestConfig
,
stats
:
Arc
<
TcpClientStats
>
,
}
struct
TcpClientStats
{
requests_sent
:
AtomicU64
,
responses_received
:
AtomicU64
,
errors
:
AtomicU64
,
bytes_sent
:
AtomicU64
,
bytes_received
:
AtomicU64
,
}
impl
TcpRequestClient
{
/// Create a new TCP request client with default configuration
pub
fn
new
()
->
Result
<
Self
>
{
Self
::
with_config
(
TcpRequestConfig
::
default
())
}
/// Create a new TCP request client with custom configuration
pub
fn
with_config
(
config
:
TcpRequestConfig
)
->
Result
<
Self
>
{
Ok
(
Self
{
pool
:
Arc
::
new
(
TcpConnectionPool
::
new
(
config
.clone
())),
config
,
stats
:
Arc
::
new
(
TcpClientStats
{
requests_sent
:
AtomicU64
::
new
(
0
),
responses_received
:
AtomicU64
::
new
(
0
),
errors
:
AtomicU64
::
new
(
0
),
bytes_sent
:
AtomicU64
::
new
(
0
),
bytes_received
:
AtomicU64
::
new
(
0
),
}),
})
}
/// Create from environment configuration
pub
fn
from_env
()
->
Result
<
Self
>
{
Self
::
with_config
(
TcpRequestConfig
::
from_env
())
}
/// Parse TCP address from string
/// Supports formats: "host:port" or "tcp://host:port" or "host:port/endpoint_name"
/// Returns (SocketAddr, Option<endpoint_name>)
fn
parse_address
(
address
:
&
str
)
->
Result
<
(
SocketAddr
,
Option
<
String
>
)
>
{
let
addr_str
=
if
let
Some
(
stripped
)
=
address
.strip_prefix
(
"tcp://"
)
{
stripped
}
else
{
address
};
// Check if endpoint name is included (format: host:port/endpoint_name)
if
let
Some
((
socket_part
,
endpoint_name
))
=
addr_str
.split_once
(
'/'
)
{
let
socket_addr
=
socket_part
.parse
::
<
SocketAddr
>
()
.map_err
(|
e
|
anyhow
::
anyhow!
(
"Invalid TCP address '{}': {}"
,
address
,
e
))
?
;
Ok
((
socket_addr
,
Some
(
endpoint_name
.to_string
())))
}
else
{
let
socket_addr
=
addr_str
.parse
::
<
SocketAddr
>
()
.map_err
(|
e
|
anyhow
::
anyhow!
(
"Invalid TCP address '{}': {}"
,
address
,
e
))
?
;
Ok
((
socket_addr
,
None
))
}
}
}
impl
Default
for
TcpRequestClient
{
fn
default
()
->
Self
{
Self
::
new
()
.expect
(
"Failed to create TCP request client"
)
}
}
#[async_trait]
impl
RequestPlaneClient
for
TcpRequestClient
{
async
fn
send_request
(
&
self
,
address
:
String
,
payload
:
Bytes
,
mut
headers
:
Headers
,
)
->
Result
<
Bytes
>
{
tracing
::
debug!
(
"TCP client sending request to address: {}"
,
address
);
self
.stats.requests_sent
.fetch_add
(
1
,
Ordering
::
Relaxed
);
self
.stats
.bytes_sent
.fetch_add
(
payload
.len
()
as
u64
,
Ordering
::
Relaxed
);
let
(
addr
,
endpoint_name
)
=
Self
::
parse_address
(
&
address
)
?
;
// Add endpoint path to headers if present in address
if
let
Some
(
endpoint_name
)
=
endpoint_name
{
headers
.insert
(
"x-endpoint-path"
.to_string
(),
endpoint_name
.clone
());
}
// Get connection from pool (automatically filters unhealthy connections)
let
conn
=
self
.pool
.get_connection
(
addr
)
.await
?
;
// Send request with timeout
// Note: The connection's send_request now handles all the async I/O via tasks
let
result
=
tokio
::
time
::
timeout
(
self
.config.request_timeout
,
conn
.send_request
(
payload
,
&
headers
),
)
.await
;
match
result
{
Ok
(
Ok
(
response
))
=>
{
self
.stats
.responses_received
.fetch_add
(
1
,
Ordering
::
Relaxed
);
self
.stats
.bytes_received
.fetch_add
(
response
.len
()
as
u64
,
Ordering
::
Relaxed
);
// Return connection to pool (health check happens inside)
self
.pool
.return_connection
(
conn
)
.await
;
Ok
(
response
)
}
Ok
(
Err
(
e
))
=>
{
self
.stats.errors
.fetch_add
(
1
,
Ordering
::
Relaxed
);
tracing
::
warn!
(
"TCP request failed to {}: {}"
,
addr
,
e
);
// Don't return unhealthy connection to pool, let it drop
Err
(
e
)
}
Err
(
_
)
=>
{
self
.stats.errors
.fetch_add
(
1
,
Ordering
::
Relaxed
);
tracing
::
warn!
(
"TCP request timeout to {}"
,
addr
);
// Don't return timed-out connection to pool
Err
(
anyhow
::
anyhow!
(
"TCP request timeout to {}"
,
addr
))
}
}
}
fn
transport_name
(
&
self
)
->
&
'static
str
{
"tcp"
}
fn
is_healthy
(
&
self
)
->
bool
{
true
// TCP client is always healthy if it was created successfully
}
fn
stats
(
&
self
)
->
ClientStats
{
ClientStats
{
requests_sent
:
self
.stats.requests_sent
.load
(
Ordering
::
Relaxed
),
responses_received
:
self
.stats.responses_received
.load
(
Ordering
::
Relaxed
),
errors
:
self
.stats.errors
.load
(
Ordering
::
Relaxed
),
bytes_sent
:
self
.stats.bytes_sent
.load
(
Ordering
::
Relaxed
),
bytes_received
:
self
.stats.bytes_received
.load
(
Ordering
::
Relaxed
),
active_connections
:
0
,
// Could track this if needed
idle_connections
:
0
,
avg_latency_us
:
0
,
}
}
}
#[cfg(test)]
mod
tests
{
use
super
::
*
;
use
std
::
sync
::
atomic
::
AtomicUsize
;
use
tokio
::
net
::
TcpListener
;
#[test]
fn
test_tcp_config_default
()
{
let
config
=
TcpRequestConfig
::
default
();
assert_eq!
(
config
.pool_size
,
DEFAULT_POOL_SIZE
);
assert_eq!
(
config
.request_timeout
,
Duration
::
from_secs
(
DEFAULT_TCP_REQUEST_TIMEOUT_SECS
)
);
assert_eq!
(
config
.channel_buffer
,
REQUEST_CHANNEL_BUFFER
);
}
#[test]
fn
test_tcp_config_from_env
()
{
unsafe
{
std
::
env
::
set_var
(
"DYN_TCP_REQUEST_TIMEOUT"
,
"10"
);
std
::
env
::
set_var
(
"DYN_TCP_POOL_SIZE"
,
"50"
);
std
::
env
::
set_var
(
"DYN_TCP_CONNECT_TIMEOUT"
,
"3"
);
std
::
env
::
set_var
(
"DYN_TCP_CHANNEL_BUFFER"
,
"100"
);
}
let
config
=
TcpRequestConfig
::
from_env
();
assert_eq!
(
config
.request_timeout
,
Duration
::
from_secs
(
10
));
assert_eq!
(
config
.pool_size
,
50
);
assert_eq!
(
config
.connect_timeout
,
Duration
::
from_secs
(
3
));
assert_eq!
(
config
.channel_buffer
,
100
);
// Clean up env vars
unsafe
{
std
::
env
::
remove_var
(
"DYN_TCP_REQUEST_TIMEOUT"
);
std
::
env
::
remove_var
(
"DYN_TCP_POOL_SIZE"
);
std
::
env
::
remove_var
(
"DYN_TCP_CONNECT_TIMEOUT"
);
std
::
env
::
remove_var
(
"DYN_TCP_CHANNEL_BUFFER"
);
}
}
#[test]
fn
test_parse_address
()
{
let
(
addr1
,
_
)
=
TcpRequestClient
::
parse_address
(
"127.0.0.1:8080"
)
.unwrap
();
assert_eq!
(
addr1
.port
(),
8080
);
let
(
addr2
,
_
)
=
TcpRequestClient
::
parse_address
(
"tcp://127.0.0.1:9090"
)
.unwrap
();
assert_eq!
(
addr2
.port
(),
9090
);
let
(
addr3
,
endpoint
)
=
TcpRequestClient
::
parse_address
(
"127.0.0.1:8080/test_endpoint"
)
.unwrap
();
assert_eq!
(
addr3
.port
(),
8080
);
assert_eq!
(
endpoint
,
Some
(
"test_endpoint"
.to_string
()));
assert
!
(
TcpRequestClient
::
parse_address
(
"invalid"
)
.is_err
());
}
#[test]
fn
test_tcp_client_creation
()
{
let
client
=
TcpRequestClient
::
new
();
assert
!
(
client
.is_ok
());
let
client
=
client
.unwrap
();
assert_eq!
(
client
.transport_name
(),
"tcp"
);
assert
!
(
client
.is_healthy
());
}
#[tokio::test]
async
fn
test_connection_health_check
()
{
use
crate
::
pipeline
::
network
::
codec
::{
TcpRequestMessage
,
TcpResponseMessage
};
// Start a mock TCP server
let
listener
=
TcpListener
::
bind
(
"127.0.0.1:0"
)
.await
.unwrap
();
let
addr
=
listener
.local_addr
()
.unwrap
();
// Spawn server that responds to requests
tokio
::
spawn
(
async
move
{
let
(
stream
,
_
)
=
listener
.accept
()
.await
.unwrap
();
let
(
mut
read_half
,
mut
write_half
)
=
tokio
::
io
::
split
(
stream
);
// Read request
let
mut
len_buf
=
[
0u8
;
2
];
read_half
.read_exact
(
&
mut
len_buf
)
.await
.unwrap
();
let
path_len
=
u16
::
from_be_bytes
(
len_buf
)
as
usize
;
let
mut
path_buf
=
vec!
[
0u8
;
path_len
];
read_half
.read_exact
(
&
mut
path_buf
)
.await
.unwrap
();
let
mut
len_buf
=
[
0u8
;
4
];
read_half
.read_exact
(
&
mut
len_buf
)
.await
.unwrap
();
let
payload_len
=
u32
::
from_be_bytes
(
len_buf
)
as
usize
;
let
mut
payload_buf
=
vec!
[
0u8
;
payload_len
];
read_half
.read_exact
(
&
mut
payload_buf
)
.await
.unwrap
();
// Send response
let
response
=
TcpResponseMessage
::
new
(
Bytes
::
from_static
(
b
"pong"
));
let
encoded
=
response
.encode
()
.unwrap
();
write_half
.write_all
(
&
encoded
)
.await
.unwrap
();
});
// Create connection
let
conn
=
TcpConnection
::
connect
(
addr
,
Duration
::
from_secs
(
5
),
10
)
.await
.unwrap
();
assert
!
(
conn
.is_healthy
(),
"New connection should be healthy"
);
// Send a request
let
mut
headers
=
Headers
::
new
();
headers
.insert
(
"x-endpoint-path"
.to_string
(),
"test"
.to_string
());
let
result
=
conn
.send_request
(
Bytes
::
from
(
"ping"
),
&
headers
)
.await
;
assert
!
(
result
.is_ok
(),
"Request should succeed"
);
assert_eq!
(
result
.unwrap
(),
Bytes
::
from
(
"pong"
));
assert
!
(
conn
.is_healthy
(),
"Connection should remain healthy after successful request"
);
}
#[tokio::test]
async
fn
test_concurrent_requests_single_connection
()
{
use
crate
::
pipeline
::
network
::
codec
::{
TcpRequestMessage
,
TcpResponseMessage
};
// Start a mock TCP server that handles multiple requests
let
listener
=
TcpListener
::
bind
(
"127.0.0.1:0"
)
.await
.unwrap
();
let
addr
=
listener
.local_addr
()
.unwrap
();
let
request_count
=
Arc
::
new
(
AtomicUsize
::
new
(
0
));
let
request_count_clone
=
request_count
.clone
();
// Spawn server that responds to multiple requests
tokio
::
spawn
(
async
move
{
let
(
stream
,
_
)
=
listener
.accept
()
.await
.unwrap
();
let
(
mut
read_half
,
mut
write_half
)
=
tokio
::
io
::
split
(
stream
);
// Handle 5 requests
for
_
in
0
..
5
{
// Read request
let
mut
len_buf
=
[
0u8
;
2
];
if
read_half
.read_exact
(
&
mut
len_buf
)
.await
.is_err
()
{
break
;
}
let
path_len
=
u16
::
from_be_bytes
(
len_buf
)
as
usize
;
let
mut
path_buf
=
vec!
[
0u8
;
path_len
];
if
read_half
.read_exact
(
&
mut
path_buf
)
.await
.is_err
()
{
break
;
}
let
mut
len_buf
=
[
0u8
;
4
];
if
read_half
.read_exact
(
&
mut
len_buf
)
.await
.is_err
()
{
break
;
}
let
payload_len
=
u32
::
from_be_bytes
(
len_buf
)
as
usize
;
let
mut
payload_buf
=
vec!
[
0u8
;
payload_len
];
if
read_half
.read_exact
(
&
mut
payload_buf
)
.await
.is_err
()
{
break
;
}
request_count_clone
.fetch_add
(
1
,
Ordering
::
SeqCst
);
// Send response
let
response
=
TcpResponseMessage
::
new
(
Bytes
::
from
(
payload_buf
));
let
encoded
=
response
.encode
()
.unwrap
();
if
write_half
.write_all
(
&
encoded
)
.await
.is_err
()
{
break
;
}
}
});
// Create connection
let
conn
=
Arc
::
new
(
TcpConnection
::
connect
(
addr
,
Duration
::
from_secs
(
5
),
10
)
.await
.unwrap
(),
);
// Send 5 concurrent requests
let
mut
handles
=
vec!
[];
for
i
in
0
..
5
{
let
conn
=
conn
.clone
();
let
handle
=
tokio
::
spawn
(
async
move
{
let
mut
headers
=
Headers
::
new
();
headers
.insert
(
"x-endpoint-path"
.to_string
(),
"test"
.to_string
());
let
payload
=
format!
(
"request_{}"
,
i
);
conn
.send_request
(
Bytes
::
from
(
payload
.clone
()),
&
headers
)
.await
.map
(|
response
|
(
payload
,
response
))
});
handles
.push
(
handle
);
}
// Wait for all requests to complete
let
mut
results
=
vec!
[];
for
handle
in
handles
{
let
result
=
handle
.await
.unwrap
();
assert
!
(
result
.is_ok
(),
"Request should succeed"
);
results
.push
(
result
.unwrap
());
}
// Verify all requests got responses
assert_eq!
(
results
.len
(),
5
);
// Verify server received all requests
assert_eq!
(
request_count
.load
(
Ordering
::
SeqCst
),
5
,
"Server should have received 5 requests"
);
}
#[tokio::test]
async
fn
test_connection_pool_reuse
()
{
use
crate
::
pipeline
::
network
::
codec
::
TcpResponseMessage
;
// Start a mock TCP server
let
listener
=
TcpListener
::
bind
(
"127.0.0.1:0"
)
.await
.unwrap
();
let
addr
=
listener
.local_addr
()
.unwrap
();
let
connection_count
=
Arc
::
new
(
AtomicUsize
::
new
(
0
));
let
connection_count_clone
=
connection_count
.clone
();
// Spawn server that accepts multiple connections
tokio
::
spawn
(
async
move
{
loop
{
let
result
=
listener
.accept
()
.await
;
if
result
.is_err
()
{
break
;
}
let
(
stream
,
_
)
=
result
.unwrap
();
connection_count_clone
.fetch_add
(
1
,
Ordering
::
SeqCst
);
tokio
::
spawn
(
async
move
{
let
(
mut
read_half
,
mut
write_half
)
=
tokio
::
io
::
split
(
stream
);
loop
{
// Read request
let
mut
len_buf
=
[
0u8
;
2
];
if
read_half
.read_exact
(
&
mut
len_buf
)
.await
.is_err
()
{
break
;
}
let
path_len
=
u16
::
from_be_bytes
(
len_buf
)
as
usize
;
let
mut
path_buf
=
vec!
[
0u8
;
path_len
];
if
read_half
.read_exact
(
&
mut
path_buf
)
.await
.is_err
()
{
break
;
}
let
mut
len_buf
=
[
0u8
;
4
];
if
read_half
.read_exact
(
&
mut
len_buf
)
.await
.is_err
()
{
break
;
}
let
payload_len
=
u32
::
from_be_bytes
(
len_buf
)
as
usize
;
let
mut
payload_buf
=
vec!
[
0u8
;
payload_len
];
if
read_half
.read_exact
(
&
mut
payload_buf
)
.await
.is_err
()
{
break
;
}
// Send response
let
response
=
TcpResponseMessage
::
new
(
Bytes
::
from_static
(
b
"ok"
));
let
encoded
=
response
.encode
()
.unwrap
();
if
write_half
.write_all
(
&
encoded
)
.await
.is_err
()
{
break
;
}
}
});
}
});
let
config
=
TcpRequestConfig
{
request_timeout
:
Duration
::
from_secs
(
5
),
connect_timeout
:
Duration
::
from_secs
(
5
),
pool_size
:
2
,
channel_buffer
:
10
,
};
let
pool
=
TcpConnectionPool
::
new
(
config
);
// Get connection twice from pool
let
conn1
=
pool
.get_connection
(
addr
)
.await
.unwrap
();
pool
.return_connection
(
conn1
)
.await
;
// Small delay to ensure connection is returned
tokio
::
time
::
sleep
(
Duration
::
from_millis
(
10
))
.await
;
let
conn2
=
pool
.get_connection
(
addr
)
.await
.unwrap
();
pool
.return_connection
(
conn2
)
.await
;
// Should have created only 1 TCP connection since we reused
assert_eq!
(
connection_count
.load
(
Ordering
::
SeqCst
),
1
,
"Should reuse connection from pool"
);
}
#[tokio::test]
async
fn
test_unhealthy_connection_filtered
()
{
let
listener
=
TcpListener
::
bind
(
"127.0.0.1:0"
)
.await
.unwrap
();
let
addr
=
listener
.local_addr
()
.unwrap
();
// Server that immediately closes connections
tokio
::
spawn
(
async
move
{
while
let
Ok
((
stream
,
_
))
=
listener
.accept
()
.await
{
drop
(
stream
);
// Immediately close
}
});
let
config
=
TcpRequestConfig
{
request_timeout
:
Duration
::
from_secs
(
1
),
connect_timeout
:
Duration
::
from_secs
(
1
),
pool_size
:
2
,
channel_buffer
:
10
,
};
let
pool
=
TcpConnectionPool
::
new
(
config
.clone
());
// Try to get a connection - it will become unhealthy quickly
let
result
=
TcpConnection
::
connect
(
addr
,
config
.connect_timeout
,
config
.channel_buffer
)
.await
;
if
let
Ok
(
conn
)
=
result
{
// Mark as unhealthy by trying to use it
let
mut
headers
=
Headers
::
new
();
headers
.insert
(
"x-endpoint-path"
.to_string
(),
"test"
.to_string
());
let
_
=
conn
.send_request
(
Bytes
::
from
(
"test"
),
&
headers
)
.await
;
// Return to pool
pool
.return_connection
(
conn
)
.await
;
// Try to get from pool again - should get a new connection attempt
let
result2
=
pool
.get_connection
(
addr
)
.await
;
// This might fail or succeed depending on timing, but should not panic
let
_
=
result2
;
}
}
}
lib/runtime/src/pipeline/network/egress/unified_client.rs
0 → 100644
View file @
06b0ebef
// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//! Unified Request Plane Client Interface
//!
//! This module defines a transport-agnostic interface for sending requests
//! in the request plane. All transport implementations (TCP, HTTP, NATS)
//! implement this trait to provide a consistent interface for the egress router.
use
anyhow
::
Result
;
use
async_trait
::
async_trait
;
use
bytes
::
Bytes
;
use
std
::
collections
::
HashMap
;
/// Type alias for request headers
pub
type
Headers
=
HashMap
<
String
,
String
>
;
/// Unified interface for request plane clients
///
/// This trait abstracts over different transport mechanisms (TCP, HTTP, NATS)
/// providing a consistent interface for sending requests and receiving acknowledgments.
///
/// # Design Principles
///
/// 1. **Transport Agnostic**: Implementations can be swapped without changing router logic
/// 2. **Async by Default**: All operations are async to support high concurrency
/// 3. **Headers Support**: All transports must support custom headers for tracing, etc.
/// 4. **Health Checks**: Implementations should provide connection health information
/// 5. **Error Handling**: All errors are wrapped in anyhow::Result for flexibility
///
/// # Example
///
/// ```ignore
/// use dynamo_runtime::pipeline::network::egress::RequestPlaneClient;
///
/// async fn send_request(client: &dyn RequestPlaneClient) -> Result<()> {
/// let mut headers = HashMap::new();
/// headers.insert("x-request-id".to_string(), "123".to_string());
///
/// let response = client.send_request(
/// "service-endpoint".to_string(),
/// Bytes::from("payload"),
/// headers,
/// ).await?;
///
/// Ok(())
/// }
/// ```
#[async_trait]
pub
trait
RequestPlaneClient
:
Send
+
Sync
{
/// Send a request to a specific address and wait for acknowledgment
///
/// # Arguments
///
/// * `address` - Transport-specific address:
/// - HTTP: `http://host:port/path`
/// - TCP: `host:port` or `tcp://host:port`
/// - NATS: `subject.name`
/// * `payload` - Request payload (encoded as bytes)
/// * `headers` - Custom headers for tracing, authentication, etc.
///
/// # Returns
///
/// Returns an acknowledgment response. Note that for streaming responses,
/// the actual response data comes over the TCP response plane, not through
/// this acknowledgment.
///
/// # Errors
///
/// Returns an error if:
/// - Connection to the endpoint fails
/// - Request times out
/// - Transport-specific errors occur (e.g., NATS server unavailable)
async
fn
send_request
(
&
self
,
address
:
String
,
payload
:
Bytes
,
headers
:
Headers
,
)
->
Result
<
Bytes
>
;
/// Get the transport name
///
/// Returns a static string identifier for the transport type.
/// Used for logging and debugging.
///
/// # Examples
///
/// - `"tcp"` - Raw TCP transport
/// - `"http"` or `"http2"` - HTTP/2 transport
/// - `"nats"` - NATS messaging
fn
transport_name
(
&
self
)
->
&
'static
str
;
/// Check connection health
///
/// Returns `true` if the client is healthy and ready to send requests.
/// This is a lightweight check that doesn't perform actual network I/O.
///
/// Implementations should return `false` if:
/// - Connection pool is exhausted
/// - Underlying transport is disconnected
/// - Client has been explicitly closed
fn
is_healthy
(
&
self
)
->
bool
;
/// Get client statistics (optional)
///
/// Returns runtime statistics about the client for monitoring and debugging.
/// Default implementation returns empty statistics.
fn
stats
(
&
self
)
->
ClientStats
{
ClientStats
::
default
()
}
/// Close the client gracefully (optional)
///
/// Implementations should:
/// - Close all active connections
/// - Wait for in-flight requests to complete (or timeout)
/// - Release all resources
///
/// Default implementation does nothing.
async
fn
close
(
&
self
)
->
Result
<
()
>
{
Ok
(())
}
}
/// Client runtime statistics
///
/// Used for monitoring and debugging transport client performance.
#[derive(Debug,
Clone,
Default)]
pub
struct
ClientStats
{
/// Total number of requests sent
pub
requests_sent
:
u64
,
/// Total number of successful responses
pub
responses_received
:
u64
,
/// Total number of errors
pub
errors
:
u64
,
/// Total bytes sent
pub
bytes_sent
:
u64
,
/// Total bytes received
pub
bytes_received
:
u64
,
/// Number of active connections (for connection-pooled transports)
pub
active_connections
:
usize
,
/// Number of idle connections in pool
pub
idle_connections
:
usize
,
/// Average request latency in microseconds (0 if not available)
pub
avg_latency_us
:
u64
,
}
impl
ClientStats
{
/// Create new empty statistics
pub
fn
new
()
->
Self
{
Self
::
default
()
}
/// Check if statistics are available (non-zero)
pub
fn
is_available
(
&
self
)
->
bool
{
self
.requests_sent
>
0
||
self
.active_connections
>
0
}
}
#[cfg(test)]
mod
tests
{
use
super
::
*
;
#[test]
fn
test_client_stats_default
()
{
let
stats
=
ClientStats
::
default
();
assert_eq!
(
stats
.requests_sent
,
0
);
assert_eq!
(
stats
.responses_received
,
0
);
assert
!
(
!
stats
.is_available
());
}
#[test]
fn
test_client_stats_is_available
()
{
let
mut
stats
=
ClientStats
::
default
();
assert
!
(
!
stats
.is_available
());
stats
.requests_sent
=
1
;
assert
!
(
stats
.is_available
());
let
stats2
=
ClientStats
{
active_connections
:
1
,
..
Default
::
default
()
};
assert
!
(
stats2
.is_available
());
}
}
lib/runtime/src/pipeline/network/ingress.rs
View file @
06b0ebef
// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
pub
mod
http_endpoint
;
pub
mod
nats_server
;
pub
mod
push_endpoint
;
pub
mod
push_handler
;
pub
mod
shared_tcp_endpoint
;
pub
mod
unified_server
;
use
super
::
*
;
lib/runtime/src/pipeline/network/ingress/http_endpoint.rs
0 → 100644
View file @
06b0ebef
// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//! HTTP endpoint for receiving requests via Axum/HTTP/2
use
super
::
*
;
use
crate
::
SystemHealth
;
use
crate
::
config
::
HealthStatus
;
use
crate
::
logging
::
TraceParent
;
use
anyhow
::
Result
;
use
axum
::{
Router
,
body
::
Bytes
,
extract
::{
Path
,
State
as
AxumState
},
http
::{
HeaderMap
,
StatusCode
},
response
::
IntoResponse
,
routing
::
post
,
};
use
dashmap
::
DashMap
;
use
hyper_util
::
rt
::{
TokioExecutor
,
TokioIo
};
use
hyper_util
::
server
::
conn
::
auto
::
Builder
as
Http2Builder
;
use
hyper_util
::
service
::
TowerToHyperService
;
use
parking_lot
::
Mutex
;
use
std
::
net
::
SocketAddr
;
use
std
::
sync
::
atomic
::{
AtomicU64
,
Ordering
};
use
tokio
::
sync
::
Notify
;
use
tokio_util
::
sync
::
CancellationToken
;
use
tower_http
::
trace
::
TraceLayer
;
use
tracing
::
Instrument
;
/// Default root path for dynamo RPC endpoints
const
DEFAULT_RPC_ROOT_PATH
:
&
str
=
"/v1/rpc"
;
/// version of crate
pub
const
VERSION
:
&
str
=
env!
(
"CARGO_PKG_VERSION"
);
/// Shared HTTP server that handles multiple endpoints on a single port
pub
struct
SharedHttpServer
{
handlers
:
Arc
<
DashMap
<
String
,
Arc
<
EndpointHandler
>>>
,
bind_addr
:
SocketAddr
,
cancellation_token
:
CancellationToken
,
}
/// Handler for a specific endpoint
struct
EndpointHandler
{
service_handler
:
Arc
<
dyn
PushWorkHandler
>
,
instance_id
:
u64
,
namespace
:
Arc
<
String
>
,
component_name
:
Arc
<
String
>
,
endpoint_name
:
Arc
<
String
>
,
system_health
:
Arc
<
Mutex
<
SystemHealth
>>
,
inflight
:
Arc
<
AtomicU64
>
,
notify
:
Arc
<
Notify
>
,
}
impl
SharedHttpServer
{
pub
fn
new
(
bind_addr
:
SocketAddr
,
cancellation_token
:
CancellationToken
)
->
Arc
<
Self
>
{
Arc
::
new
(
Self
{
handlers
:
Arc
::
new
(
DashMap
::
new
()),
bind_addr
,
cancellation_token
,
})
}
/// Register an endpoint handler with this server
#[allow(clippy::too_many_arguments)]
pub
async
fn
register_endpoint
(
&
self
,
subject
:
String
,
service_handler
:
Arc
<
dyn
PushWorkHandler
>
,
instance_id
:
u64
,
namespace
:
String
,
component_name
:
String
,
endpoint_name
:
String
,
system_health
:
Arc
<
Mutex
<
SystemHealth
>>
,
)
->
Result
<
()
>
{
let
handler
=
Arc
::
new
(
EndpointHandler
{
service_handler
,
instance_id
,
namespace
:
Arc
::
new
(
namespace
),
component_name
:
Arc
::
new
(
component_name
),
endpoint_name
:
Arc
::
new
(
endpoint_name
.clone
()),
system_health
:
system_health
.clone
(),
inflight
:
Arc
::
new
(
AtomicU64
::
new
(
0
)),
notify
:
Arc
::
new
(
Notify
::
new
()),
});
// Set health status
system_health
.lock
()
.set_endpoint_health_status
(
&
endpoint_name
,
HealthStatus
::
Ready
);
let
subject_clone
=
subject
.clone
();
self
.handlers
.insert
(
subject
,
handler
);
tracing
::
debug!
(
"Registered endpoint handler for subject: {}"
,
subject_clone
);
Ok
(())
}
/// Unregister an endpoint handler
pub
async
fn
unregister_endpoint
(
&
self
,
subject
:
&
str
,
endpoint_name
:
&
str
)
{
if
let
Some
((
_
,
handler
))
=
self
.handlers
.remove
(
subject
)
{
handler
.system_health
.lock
()
.set_endpoint_health_status
(
endpoint_name
,
HealthStatus
::
NotReady
);
tracing
::
debug!
(
"Unregistered endpoint handler for subject: {}"
,
subject
);
}
}
/// Start the shared HTTP server
pub
async
fn
start
(
self
:
Arc
<
Self
>
)
->
Result
<
()
>
{
let
rpc_root_path
=
std
::
env
::
var
(
"DYN_HTTP_RPC_ROOT_PATH"
)
.unwrap_or_else
(|
_
|
DEFAULT_RPC_ROOT_PATH
.to_string
());
let
route_pattern
=
format!
(
"{}/{{*endpoint}}"
,
rpc_root_path
);
let
app
=
Router
::
new
()
.route
(
&
route_pattern
,
post
(
handle_shared_request
))
.layer
(
TraceLayer
::
new_for_http
())
.with_state
(
self
.clone
());
tracing
::
info!
(
"Starting shared HTTP/2 endpoint server on {} at path {}/:endpoint"
,
self
.bind_addr
,
rpc_root_path
);
let
listener
=
tokio
::
net
::
TcpListener
::
bind
(
&
self
.bind_addr
)
.await
?
;
let
cancellation_token
=
self
.cancellation_token
.clone
();
loop
{
tokio
::
select!
{
accept_result
=
listener
.accept
()
=>
{
match
accept_result
{
Ok
((
stream
,
_
addr
))
=>
{
let
app_clone
=
app
.clone
();
let
cancel_clone
=
cancellation_token
.clone
();
tokio
::
spawn
(
async
move
{
// Create HTTP/2 connection builder with prior knowledge
let
http2_builder
=
Http2Builder
::
new
(
TokioExecutor
::
new
());
let
io
=
TokioIo
::
new
(
stream
);
let
tower_service
=
app_clone
.into_service
();
// Wrap Tower service for Hyper compatibility
let
hyper_service
=
TowerToHyperService
::
new
(
tower_service
);
tokio
::
select!
{
result
=
http2_builder
.serve_connection
(
io
,
hyper_service
)
=>
{
if
let
Err
(
e
)
=
result
{
tracing
::
debug!
(
"HTTP/2 connection error: {}"
,
e
);
}
}
_
=
cancel_clone
.cancelled
()
=>
{
tracing
::
trace!
(
"Connection cancelled"
);
}
}
});
}
Err
(
e
)
=>
{
tracing
::
error!
(
"Failed to accept connection: {}"
,
e
);
}
}
}
_
=
cancellation_token
.cancelled
()
=>
{
tracing
::
info!
(
"SharedHttpServer received cancellation signal, shutting down"
);
return
Ok
(());
}
}
}
}
/// Wait for all inflight requests across all endpoints
pub
async
fn
wait_for_inflight
(
&
self
)
{
for
handler
in
self
.handlers
.iter
()
{
while
handler
.value
()
.inflight
.load
(
Ordering
::
SeqCst
)
>
0
{
tokio
::
time
::
sleep
(
tokio
::
time
::
Duration
::
from_millis
(
100
))
.await
;
}
}
}
}
/// HTTP handler for the shared server
async
fn
handle_shared_request
(
AxumState
(
server
):
AxumState
<
Arc
<
SharedHttpServer
>>
,
Path
(
endpoint_path
):
Path
<
String
>
,
headers
:
HeaderMap
,
body
:
Bytes
,
)
->
impl
IntoResponse
{
// Look up the handler for this endpoint (lock-free read with DashMap)
let
handler
=
match
server
.handlers
.get
(
&
endpoint_path
)
{
Some
(
h
)
=>
h
.clone
(),
None
=>
{
tracing
::
warn!
(
"No handler found for endpoint: {}"
,
endpoint_path
);
return
(
StatusCode
::
NOT_FOUND
,
"Endpoint not found"
);
}
};
// Increment inflight counter
handler
.inflight
.fetch_add
(
1
,
Ordering
::
SeqCst
);
// Extract tracing headers
let
traceparent
=
TraceParent
::
from_axum_headers
(
&
headers
);
// Spawn async handler
let
service_handler
=
handler
.service_handler
.clone
();
let
inflight
=
handler
.inflight
.clone
();
let
notify
=
handler
.notify
.clone
();
let
namespace
=
handler
.namespace
.clone
();
let
component_name
=
handler
.component_name
.clone
();
let
endpoint_name
=
handler
.endpoint_name
.clone
();
let
instance_id
=
handler
.instance_id
;
tokio
::
spawn
(
async
move
{
tracing
::
trace!
(
instance_id
,
"handling new HTTP request"
);
let
result
=
service_handler
.handle_payload
(
body
)
.instrument
(
tracing
::
info_span!
(
"handle_payload"
,
component
=
component_name
.as_ref
(),
endpoint
=
endpoint_name
.as_ref
(),
namespace
=
namespace
.as_ref
(),
instance_id
=
instance_id
,
trace_id
=
traceparent
.trace_id
,
parent_id
=
traceparent
.parent_id
,
x_request_id
=
traceparent
.x_request_id
,
x_dynamo_request_id
=
traceparent
.x_dynamo_request_id
,
tracestate
=
traceparent
.tracestate
))
.await
;
match
result
{
Ok
(
_
)
=>
{
tracing
::
trace!
(
instance_id
,
"request handled successfully"
);
}
Err
(
e
)
=>
{
tracing
::
warn!
(
"Failed to handle request: {}"
,
e
.to_string
());
}
}
// Decrease inflight counter
inflight
.fetch_sub
(
1
,
Ordering
::
SeqCst
);
notify
.notify_one
();
});
// Return 202 Accepted immediately (like NATS ack)
(
StatusCode
::
ACCEPTED
,
""
)
}
/// Extension trait for TraceParent to support Axum headers
impl
TraceParent
{
pub
fn
from_axum_headers
(
headers
:
&
HeaderMap
)
->
Self
{
let
mut
traceparent
=
TraceParent
::
default
();
if
let
Some
(
value
)
=
headers
.get
(
"traceparent"
)
&&
let
Ok
(
s
)
=
value
.to_str
()
{
traceparent
.trace_id
=
Some
(
s
.to_string
());
}
if
let
Some
(
value
)
=
headers
.get
(
"tracestate"
)
&&
let
Ok
(
s
)
=
value
.to_str
()
{
traceparent
.tracestate
=
Some
(
s
.to_string
());
}
if
let
Some
(
value
)
=
headers
.get
(
"x-request-id"
)
&&
let
Ok
(
s
)
=
value
.to_str
()
{
traceparent
.x_request_id
=
Some
(
s
.to_string
());
}
if
let
Some
(
value
)
=
headers
.get
(
"x-dynamo-request-id"
)
&&
let
Ok
(
s
)
=
value
.to_str
()
{
traceparent
.x_dynamo_request_id
=
Some
(
s
.to_string
());
}
traceparent
}
}
// Implement RequestPlaneServer trait for SharedHttpServer
#[async_trait::async_trait]
impl
super
::
unified_server
::
RequestPlaneServer
for
SharedHttpServer
{
async
fn
register_endpoint
(
&
self
,
endpoint_name
:
String
,
service_handler
:
Arc
<
dyn
PushWorkHandler
>
,
instance_id
:
u64
,
namespace
:
String
,
component_name
:
String
,
system_health
:
Arc
<
Mutex
<
SystemHealth
>>
,
)
->
Result
<
()
>
{
// For HTTP, we use endpoint_name as both the subject (routing key) and endpoint_name
self
.register_endpoint
(
endpoint_name
.clone
(),
service_handler
,
instance_id
,
namespace
,
component_name
,
endpoint_name
,
system_health
,
)
.await
}
async
fn
unregister_endpoint
(
&
self
,
endpoint_name
:
&
str
)
->
Result
<
()
>
{
self
.unregister_endpoint
(
endpoint_name
,
endpoint_name
)
.await
;
Ok
(())
}
fn
address
(
&
self
)
->
String
{
format!
(
"http://{}:{}"
,
self
.bind_addr
.ip
(),
self
.bind_addr
.port
())
}
fn
transport_name
(
&
self
)
->
&
'static
str
{
"http"
}
fn
is_healthy
(
&
self
)
->
bool
{
// Server is healthy if it has been created
// TODO: Add more sophisticated health checks (e.g., check if listener is active)
true
}
}
#[cfg(test)]
mod
tests
{
use
super
::
*
;
#[test]
fn
test_traceparent_from_axum_headers
()
{
let
mut
headers
=
HeaderMap
::
new
();
headers
.insert
(
"traceparent"
,
"test-trace-id"
.parse
()
.unwrap
());
headers
.insert
(
"tracestate"
,
"test-state"
.parse
()
.unwrap
());
headers
.insert
(
"x-request-id"
,
"req-123"
.parse
()
.unwrap
());
headers
.insert
(
"x-dynamo-request-id"
,
"dyn-456"
.parse
()
.unwrap
());
let
traceparent
=
TraceParent
::
from_axum_headers
(
&
headers
);
assert_eq!
(
traceparent
.trace_id
,
Some
(
"test-trace-id"
.to_string
()));
assert_eq!
(
traceparent
.tracestate
,
Some
(
"test-state"
.to_string
()));
assert_eq!
(
traceparent
.x_request_id
,
Some
(
"req-123"
.to_string
()));
assert_eq!
(
traceparent
.x_dynamo_request_id
,
Some
(
"dyn-456"
.to_string
()));
}
#[test]
fn
test_shared_http_server_creation
()
{
use
std
::
net
::{
IpAddr
,
Ipv4Addr
};
let
bind_addr
=
SocketAddr
::
new
(
IpAddr
::
V4
(
Ipv4Addr
::
new
(
127
,
0
,
0
,
1
)),
0
);
let
token
=
CancellationToken
::
new
();
let
server
=
SharedHttpServer
::
new
(
bind_addr
,
token
);
assert
!
(
server
.handlers
.is_empty
());
}
}
lib/runtime/src/pipeline/network/ingress/nats_server.rs
0 → 100644
View file @
06b0ebef
// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//! NATS Multiplexed Server
//!
//! Provides a multiplexed NATS server that handles multiple endpoints on a single
//! NATS service group. This replaces the per-endpoint PushEndpoint pattern with
//! a unified multiplexed approach consistent with HTTP and TCP servers.
use
super
::
*
;
use
crate
::
SystemHealth
;
use
crate
::
config
::
HealthStatus
;
use
crate
::
pipeline
::
network
::
ingress
::
push_endpoint
::
PushEndpoint
;
use
anyhow
::
Result
;
use
async_trait
::
async_trait
;
use
dashmap
::
DashMap
;
use
parking_lot
::
Mutex
;
use
std
::
sync
::
Arc
;
use
tokio_util
::
sync
::
CancellationToken
;
/// Multiplexed NATS server that handles multiple endpoints
///
/// Unlike the previous per-endpoint approach, this server manages multiple
/// endpoints, getting the service group dynamically from the component registry
/// for each endpoint registration.
pub
struct
NatsMultiplexedServer
{
nats_client
:
async_nats
::
Client
,
component_registry
:
crate
::
component
::
Registry
,
handlers
:
Arc
<
DashMap
<
String
,
EndpointTask
>>
,
cancellation_token
:
CancellationToken
,
}
struct
EndpointTask
{
cancel_token
:
CancellationToken
,
_
endpoint_name
:
String
,
}
impl
NatsMultiplexedServer
{
/// Create a new multiplexed NATS server
///
/// # Arguments
///
/// * `nats_client` - NATS client for connection management
/// * `component_registry` - Component registry to get service groups from
/// * `cancellation_token` - Token for graceful shutdown
pub
fn
new
(
nats_client
:
async_nats
::
Client
,
component_registry
:
crate
::
component
::
Registry
,
cancellation_token
:
CancellationToken
,
)
->
Arc
<
Self
>
{
Arc
::
new
(
Self
{
nats_client
,
component_registry
,
handlers
:
Arc
::
new
(
DashMap
::
new
()),
cancellation_token
,
})
}
}
#[async_trait]
impl
super
::
unified_server
::
RequestPlaneServer
for
NatsMultiplexedServer
{
async
fn
register_endpoint
(
&
self
,
endpoint_name
:
String
,
service_handler
:
Arc
<
dyn
PushWorkHandler
>
,
instance_id
:
u64
,
namespace
:
String
,
component_name
:
String
,
system_health
:
Arc
<
Mutex
<
SystemHealth
>>
,
)
->
Result
<
()
>
{
tracing
::
info!
(
endpoint_name
=
%
endpoint_name
,
namespace
=
%
namespace
,
component
=
%
component_name
,
instance_id
=
instance_id
,
"NatsMultiplexedServer::register_endpoint called"
);
// Get the service group from the component registry
// Service name format matches Component::service_name(): "{namespace}_{component}" slugified
use
crate
::
transports
::
nats
::
Slug
;
let
service_name_raw
=
format!
(
"{}_{}"
,
namespace
,
component_name
);
let
service_name
=
Slug
::
slugify
(
&
service_name_raw
)
.to_string
();
tracing
::
debug!
(
service_name_raw
=
%
service_name_raw
,
service_name
=
%
service_name
,
"Looking up service group in registry"
);
let
registry
=
self
.component_registry.inner
.lock
()
.await
;
let
service_group
=
registry
.services
.get
(
&
service_name
)
.map
(|
service
|
service
.group
(
&
service_name
))
.ok_or_else
(||
anyhow
::
anyhow!
(
"Service '{}' not found in registry"
,
service_name
))
?
;
drop
(
registry
);
tracing
::
info!
(
"Successfully retrieved service group"
);
// Construct the full NATS subject with instance ID
// Format: {endpoint_name}-{instance_id_hex}
// This matches Endpoint::name_with_id() and subject_to() format
let
endpoint_with_id
=
format!
(
"{}-{:x}"
,
endpoint_name
,
instance_id
);
// Create NATS service endpoint with the full subject
let
service_endpoint
=
service_group
.endpoint
(
&
endpoint_with_id
)
.await
.map_err
(|
e
|
{
anyhow
::
anyhow!
(
"Failed to create NATS endpoint '{}': {}"
,
endpoint_with_id
,
e
)
})
?
;
tracing
::
info!
(
endpoint_name
=
%
endpoint_name
,
endpoint_with_id
=
%
endpoint_with_id
,
namespace
=
%
namespace
,
component
=
%
component_name
,
instance_id
=
instance_id
,
"Registering NATS endpoint"
);
// Create cancellation token for this specific endpoint
let
endpoint_cancel
=
CancellationToken
::
new
();
let
endpoint_cancel_clone
=
endpoint_cancel
.clone
();
// Build the push endpoint
let
push_endpoint
=
PushEndpoint
::
builder
()
.service_handler
(
service_handler
)
.cancellation_token
(
endpoint_cancel_clone
)
.graceful_shutdown
(
true
)
.build
()
.map_err
(|
e
|
anyhow
::
anyhow!
(
"Failed to build NATS push endpoint: {}"
,
e
))
?
;
tracing
::
info!
(
endpoint_name
=
%
endpoint_name
,
endpoint_with_id
=
%
endpoint_with_id
,
"Starting NATS push endpoint listener (blocking)"
);
// Spawn task to handle this endpoint using PushEndpoint
// Note: PushEndpoint::start() is a blocking loop that runs until cancelled
let
endpoint_name_clone
=
endpoint_name
.clone
();
tokio
::
spawn
(
async
move
{
if
let
Err
(
e
)
=
push_endpoint
.start
(
service_endpoint
,
namespace
,
component_name
,
endpoint_name_clone
.clone
(),
instance_id
,
system_health
,
)
.await
{
tracing
::
error!
(
endpoint_name
=
%
endpoint_name_clone
,
error
=
%
e
,
"NATS endpoint task failed"
);
}
else
{
tracing
::
info!
(
endpoint_name
=
%
endpoint_name_clone
,
"NATS push endpoint listener completed"
);
}
});
// Give the endpoint a moment to start listening
// This prevents a race condition where discovery registers the endpoint
// before NATS is actually ready to receive requests
tokio
::
time
::
sleep
(
tokio
::
time
::
Duration
::
from_millis
(
10
))
.await
;
// Store task info for later cleanup
self
.handlers
.insert
(
endpoint_name
.clone
(),
EndpointTask
{
cancel_token
:
endpoint_cancel
,
_
endpoint_name
:
endpoint_name
,
},
);
Ok
(())
}
async
fn
unregister_endpoint
(
&
self
,
endpoint_name
:
&
str
)
->
Result
<
()
>
{
if
let
Some
((
_
,
task
))
=
self
.handlers
.remove
(
endpoint_name
)
{
tracing
::
info!
(
endpoint_name
=
%
endpoint_name
,
"Unregistering NATS endpoint"
);
task
.cancel_token
.cancel
();
}
Ok
(())
}
fn
address
(
&
self
)
->
String
{
// Return NATS server URL from connection info
// NATS client doesn't expose server info directly, return generic address
"nats://connected"
.to_string
()
}
fn
transport_name
(
&
self
)
->
&
'static
str
{
"nats"
}
fn
is_healthy
(
&
self
)
->
bool
{
// Check if NATS client is connected
// NATS client doesn't expose connection state directly, assume healthy
true
}
}
lib/runtime/src/pipeline/network/ingress/shared_tcp_endpoint.rs
0 → 100644
View file @
06b0ebef
// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//! Shared TCP Server with Endpoint Multiplexing
//!
//! Provides a shared TCP server that can handle multiple endpoints on a single port
//! by adding endpoint routing to the TCP wire protocol.
use
crate
::
SystemHealth
;
use
crate
::
pipeline
::
network
::
PushWorkHandler
;
use
anyhow
::
Result
;
use
bytes
::
Bytes
;
use
dashmap
::
DashMap
;
use
parking_lot
::
Mutex
;
use
std
::
net
::
SocketAddr
;
use
std
::
sync
::
Arc
;
use
std
::
sync
::
atomic
::{
AtomicU64
,
Ordering
};
use
tokio
::
io
::{
AsyncReadExt
,
AsyncWriteExt
};
use
tokio
::
net
::{
TcpListener
,
TcpStream
};
use
tokio
::
sync
::
Notify
;
use
tokio_util
::
bytes
::
BytesMut
;
use
tokio_util
::
sync
::
CancellationToken
;
use
tracing
::
Instrument
;
/// Default maximum message size for TCP server (32 MB)
const
DEFAULT_MAX_MESSAGE_SIZE
:
usize
=
32
*
1024
*
1024
;
/// Get maximum message size from environment or use default
fn
get_max_message_size
()
->
usize
{
std
::
env
::
var
(
"DYN_TCP_MAX_MESSAGE_SIZE"
)
.ok
()
.and_then
(|
s
|
s
.parse
::
<
usize
>
()
.ok
())
.unwrap_or
(
DEFAULT_MAX_MESSAGE_SIZE
)
}
/// Shared TCP server that handles multiple endpoints on a single port
pub
struct
SharedTcpServer
{
handlers
:
Arc
<
DashMap
<
String
,
Arc
<
EndpointHandler
>>>
,
bind_addr
:
SocketAddr
,
cancellation_token
:
CancellationToken
,
}
struct
EndpointHandler
{
service_handler
:
Arc
<
dyn
PushWorkHandler
>
,
instance_id
:
u64
,
namespace
:
String
,
component_name
:
String
,
endpoint_name
:
String
,
system_health
:
Arc
<
Mutex
<
SystemHealth
>>
,
inflight
:
Arc
<
AtomicU64
>
,
notify
:
Arc
<
Notify
>
,
}
impl
SharedTcpServer
{
pub
fn
new
(
bind_addr
:
SocketAddr
,
cancellation_token
:
CancellationToken
)
->
Arc
<
Self
>
{
Arc
::
new
(
Self
{
handlers
:
Arc
::
new
(
DashMap
::
new
()),
bind_addr
,
cancellation_token
,
})
}
#[allow(clippy::too_many_arguments)]
pub
async
fn
register_endpoint
(
&
self
,
endpoint_path
:
String
,
service_handler
:
Arc
<
dyn
PushWorkHandler
>
,
instance_id
:
u64
,
namespace
:
String
,
component_name
:
String
,
endpoint_name
:
String
,
system_health
:
Arc
<
Mutex
<
SystemHealth
>>
,
)
->
Result
<
()
>
{
let
handler
=
Arc
::
new
(
EndpointHandler
{
service_handler
,
instance_id
,
namespace
,
component_name
,
endpoint_name
:
endpoint_name
.clone
(),
system_health
,
inflight
:
Arc
::
new
(
AtomicU64
::
new
(
0
)),
notify
:
Arc
::
new
(
Notify
::
new
()),
});
self
.handlers
.insert
(
endpoint_path
,
handler
);
tracing
::
info!
(
"Registered endpoint '{}' with shared TCP server on {}"
,
endpoint_name
,
self
.bind_addr
);
Ok
(())
}
pub
async
fn
unregister_endpoint
(
&
self
,
endpoint_path
:
&
str
,
endpoint_name
:
&
str
)
{
self
.handlers
.remove
(
endpoint_path
);
tracing
::
info!
(
"Unregistered endpoint '{}' from shared TCP server"
,
endpoint_name
);
}
pub
async
fn
start
(
self
:
Arc
<
Self
>
)
->
Result
<
()
>
{
tracing
::
info!
(
"Starting shared TCP server on {}"
,
self
.bind_addr
);
let
listener
=
TcpListener
::
bind
(
&
self
.bind_addr
)
.await
?
;
let
cancellation_token
=
self
.cancellation_token
.clone
();
loop
{
tokio
::
select!
{
accept_result
=
listener
.accept
()
=>
{
match
accept_result
{
Ok
((
stream
,
peer_addr
))
=>
{
tracing
::
trace!
(
"Accepted TCP connection from {}"
,
peer_addr
);
let
handlers
=
self
.handlers
.clone
();
tokio
::
spawn
(
async
move
{
if
let
Err
(
e
)
=
Self
::
handle_connection
(
stream
,
handlers
)
.await
{
tracing
::
debug!
(
"TCP connection error: {}"
,
e
);
}
});
}
Err
(
e
)
=>
{
tracing
::
error!
(
"Failed to accept TCP connection: {}"
,
e
);
}
}
}
_
=
cancellation_token
.cancelled
()
=>
{
tracing
::
info!
(
"SharedTcpServer received cancellation signal, shutting down"
);
return
Ok
(());
}
}
}
}
async
fn
handle_connection
(
stream
:
TcpStream
,
handlers
:
Arc
<
DashMap
<
String
,
Arc
<
EndpointHandler
>>>
,
)
->
Result
<
()
>
{
use
crate
::
pipeline
::
network
::
codec
::{
TcpRequestMessage
,
TcpResponseMessage
};
// Split stream into read and write halves for concurrent operations
let
(
read_half
,
write_half
)
=
tokio
::
io
::
split
(
stream
);
// Channel for sending responses to the write task (zero-copy Bytes)
let
(
response_tx
,
response_rx
)
=
tokio
::
sync
::
mpsc
::
unbounded_channel
::
<
Bytes
>
();
// Spawn write task
let
write_task
=
tokio
::
spawn
(
Self
::
write_loop
(
write_half
,
response_rx
));
// Run read task in current context
let
read_result
=
Self
::
read_loop
(
read_half
,
handlers
,
response_tx
)
.await
;
// Write task will end when response_tx is dropped
write_task
.await
??
;
read_result
}
async
fn
read_loop
(
mut
read_half
:
tokio
::
io
::
ReadHalf
<
TcpStream
>
,
handlers
:
Arc
<
DashMap
<
String
,
Arc
<
EndpointHandler
>>>
,
response_tx
:
tokio
::
sync
::
mpsc
::
UnboundedSender
<
Bytes
>
,
)
->
Result
<
()
>
{
use
crate
::
pipeline
::
network
::
codec
::{
TcpRequestMessage
,
TcpResponseMessage
};
loop
{
// Read endpoint path length (2 bytes)
let
mut
path_len_buf
=
[
0u8
;
2
];
match
read_half
.read_exact
(
&
mut
path_len_buf
)
.await
{
Ok
(
_
)
=>
{}
Err
(
e
)
if
e
.kind
()
==
std
::
io
::
ErrorKind
::
UnexpectedEof
=>
{
break
;
}
Err
(
e
)
=>
{
return
Err
(
e
.into
());
}
}
let
path_len
=
u16
::
from_be_bytes
(
path_len_buf
)
as
usize
;
// Read endpoint path
let
mut
path_buf
=
vec!
[
0u8
;
path_len
];
read_half
.read_exact
(
&
mut
path_buf
)
.await
?
;
// Read payload length (4 bytes)
let
mut
len_buf
=
[
0u8
;
4
];
read_half
.read_exact
(
&
mut
len_buf
)
.await
?
;
let
payload_len
=
u32
::
from_be_bytes
(
len_buf
)
as
usize
;
// Sanity check - enforce maximum message size
let
max_message_size
=
get_max_message_size
();
if
payload_len
>
max_message_size
{
tracing
::
warn!
(
"Request too large: {} bytes (max: {} bytes), closing connection"
,
payload_len
,
max_message_size
);
// Send error response
let
error_response
=
TcpResponseMessage
::
new
(
Bytes
::
from_static
(
b
"Request too large"
));
if
let
Ok
(
encoded
)
=
error_response
.encode
()
{
let
_
=
response_tx
.send
(
encoded
);
}
break
;
}
// Read request payload
let
mut
payload_buf
=
vec!
[
0u8
;
payload_len
];
read_half
.read_exact
(
&
mut
payload_buf
)
.await
?
;
// Reconstruct the full message buffer for decoding using BytesMut
let
mut
full_msg
=
BytesMut
::
with_capacity
(
2
+
path_len
+
4
+
payload_len
);
full_msg
.extend_from_slice
(
&
path_len_buf
);
full_msg
.extend_from_slice
(
&
path_buf
);
full_msg
.extend_from_slice
(
&
len_buf
);
full_msg
.extend_from_slice
(
&
payload_buf
);
// Decode using codec (zero-copy conversion)
let
full_msg_bytes
=
full_msg
.freeze
();
let
request_msg
=
match
TcpRequestMessage
::
decode
(
&
full_msg_bytes
)
{
Ok
(
msg
)
=>
msg
,
Err
(
e
)
=>
{
tracing
::
warn!
(
"Failed to decode TCP request: {}"
,
e
);
// Send error response
let
error_response
=
TcpResponseMessage
::
new
(
Bytes
::
from
(
format!
(
"Decode error: {}"
,
e
)));
if
let
Ok
(
encoded
)
=
error_response
.encode
()
{
let
_
=
response_tx
.send
(
encoded
);
}
continue
;
}
};
let
endpoint_path
=
request_msg
.endpoint_path
;
let
payload
=
request_msg
.payload
;
// Look up handler (lock-free read with DashMap)
let
handler
=
handlers
.get
(
&
endpoint_path
)
.map
(|
h
|
h
.clone
());
let
handler
=
match
handler
{
Some
(
h
)
=>
h
,
None
=>
{
tracing
::
warn!
(
"No handler found for endpoint: {}"
,
endpoint_path
);
// Send error response using codec
let
error_response
=
TcpResponseMessage
::
new
(
Bytes
::
from
(
format!
(
"Unknown endpoint: {}"
,
endpoint_path
)));
if
let
Ok
(
encoded
)
=
error_response
.encode
()
{
let
_
=
response_tx
.send
(
encoded
);
}
continue
;
}
};
handler
.inflight
.fetch_add
(
1
,
Ordering
::
SeqCst
);
// Send acknowledgment immediately using codec (non-blocking, zero-copy)
let
ack_response
=
TcpResponseMessage
::
empty
();
if
let
Ok
(
encoded_ack
)
=
ack_response
.encode
()
{
// Send to write task without blocking reads
if
response_tx
.send
(
encoded_ack
)
.is_err
()
{
tracing
::
debug!
(
"Write task closed, ending read loop"
);
break
;
}
}
// Process request asynchronously
let
service_handler
=
handler
.service_handler
.clone
();
let
inflight
=
handler
.inflight
.clone
();
let
notify
=
handler
.notify
.clone
();
let
instance_id
=
handler
.instance_id
;
let
namespace
=
handler
.namespace
.clone
();
let
component_name
=
handler
.component_name
.clone
();
let
endpoint_name
=
handler
.endpoint_name
.clone
();
tokio
::
spawn
(
async
move
{
tracing
::
trace!
(
instance_id
,
"handling TCP request"
);
let
result
=
service_handler
.handle_payload
(
payload
)
.instrument
(
tracing
::
info_span!
(
"handle_payload"
,
component
=
component_name
.as_str
(),
endpoint
=
endpoint_name
.as_str
(),
namespace
=
namespace
.as_str
(),
instance_id
=
instance_id
,
))
.await
;
match
result
{
Ok
(
_
)
=>
{
tracing
::
trace!
(
instance_id
,
"TCP request handled successfully"
);
}
Err
(
e
)
=>
{
tracing
::
warn!
(
"Failed to handle TCP request: {}"
,
e
);
}
}
inflight
.fetch_sub
(
1
,
Ordering
::
SeqCst
);
notify
.notify_one
();
});
}
Ok
(())
}
async
fn
write_loop
(
mut
write_half
:
tokio
::
io
::
WriteHalf
<
TcpStream
>
,
mut
response_rx
:
tokio
::
sync
::
mpsc
::
UnboundedReceiver
<
Bytes
>
,
)
->
Result
<
()
>
{
while
let
Some
(
response
)
=
response_rx
.recv
()
.await
{
write_half
.write_all
(
&
response
)
.await
?
;
write_half
.flush
()
.await
?
;
}
Ok
(())
}
}
// Implement RequestPlaneServer trait for SharedTcpServer
#[async_trait::async_trait]
impl
super
::
unified_server
::
RequestPlaneServer
for
SharedTcpServer
{
async
fn
register_endpoint
(
&
self
,
endpoint_name
:
String
,
service_handler
:
Arc
<
dyn
PushWorkHandler
>
,
instance_id
:
u64
,
namespace
:
String
,
component_name
:
String
,
system_health
:
Arc
<
Mutex
<
SystemHealth
>>
,
)
->
Result
<
()
>
{
// For TCP, we use endpoint_name as both the endpoint_path (routing key) and endpoint_name
self
.register_endpoint
(
endpoint_name
.clone
(),
service_handler
,
instance_id
,
namespace
,
component_name
,
endpoint_name
,
system_health
,
)
.await
}
async
fn
unregister_endpoint
(
&
self
,
endpoint_name
:
&
str
)
->
Result
<
()
>
{
self
.unregister_endpoint
(
endpoint_name
,
endpoint_name
)
.await
;
Ok
(())
}
fn
address
(
&
self
)
->
String
{
format!
(
"tcp://{}:{}"
,
self
.bind_addr
.ip
(),
self
.bind_addr
.port
())
}
fn
transport_name
(
&
self
)
->
&
'static
str
{
"tcp"
}
fn
is_healthy
(
&
self
)
->
bool
{
// Server is healthy if it has been created
// TODO: Add more sophisticated health checks (e.g., check if listener is active)
true
}
}
lib/runtime/src/pipeline/network/ingress/unified_server.rs
0 → 100644
View file @
06b0ebef
// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//! Unified Request Plane Server Interface
//!
//! This module defines a transport-agnostic interface for request plane servers.
//! All transport implementations (HTTP, TCP, NATS) implement this trait to provide
//! a consistent interface for endpoint registration and management.
use
super
::
*
;
use
crate
::
SystemHealth
;
use
anyhow
::
Result
;
use
async_trait
::
async_trait
;
use
parking_lot
::
Mutex
;
use
std
::
sync
::
Arc
;
/// Unified interface for request plane servers
///
/// This trait abstracts over different transport mechanisms (HTTP/2, TCP, NATS)
/// providing a consistent interface for registering endpoints and managing server lifecycle.
///
/// # Design Principles
///
/// 1. **Transport Agnostic**: Implementations can be swapped without changing business logic
/// 2. **Multiplexed**: All servers handle multiple endpoints on a single port/connection
/// 3. **Async by Default**: All operations are async to support high concurrency
/// 4. **Health Monitoring**: Servers provide health status for monitoring
///
/// # Example
///
/// ```ignore
/// use dynamo_runtime::pipeline::network::ingress::RequestPlaneServer;
///
/// async fn register(server: &dyn RequestPlaneServer) -> Result<()> {
/// server.register_endpoint(
/// "generate".to_string(),
/// handler,
/// instance_id,
/// "dynamo".to_string(),
/// "backend".to_string(),
/// system_health,
/// ).await?;
/// Ok(())
/// }
/// ```
#[async_trait]
pub
trait
RequestPlaneServer
:
Send
+
Sync
{
/// Register an endpoint handler with the server
///
/// # Arguments
///
/// * `endpoint_name` - Name/path for routing (e.g., "generate", "health")
/// * `service_handler` - Handler that processes incoming requests
/// * `instance_id` - Unique instance identifier for this endpoint
/// * `namespace` - Service namespace (e.g., "dynamo")
/// * `component_name` - Component name (e.g., "backend", "frontend")
/// * `system_health` - Health tracking for this endpoint
///
/// # Returns
///
/// Returns `Ok(())` if registration succeeds, or an error if:
/// - Endpoint name is already registered
/// - Server is not running or has been stopped
/// - Transport-specific errors occur
async
fn
register_endpoint
(
&
self
,
endpoint_name
:
String
,
service_handler
:
Arc
<
dyn
PushWorkHandler
>
,
instance_id
:
u64
,
namespace
:
String
,
component_name
:
String
,
system_health
:
Arc
<
Mutex
<
SystemHealth
>>
,
)
->
Result
<
()
>
;
/// Unregister an endpoint from the server
///
/// # Arguments
///
/// * `endpoint_name` - Name of the endpoint to unregister
///
/// # Returns
///
/// Returns `Ok(())` if unregistration succeeds or endpoint doesn't exist.
/// Errors are only returned for transport-specific failures.
async
fn
unregister_endpoint
(
&
self
,
endpoint_name
:
&
str
)
->
Result
<
()
>
;
/// Get server bind address or identifier
///
/// Returns a transport-specific address string:
/// - HTTP: `"http://0.0.0.0:8888"`
/// - TCP: `"tcp://0.0.0.0:9999"`
/// - NATS: `"nats://localhost:4222"`
///
/// Used for logging, debugging, and service discovery.
fn
address
(
&
self
)
->
String
;
/// Get the transport name
///
/// Returns a static string identifier for the transport type.
/// Used for logging and debugging.
///
/// # Examples
///
/// - `"http"` - HTTP/2 transport
/// - `"tcp"` - Raw TCP transport
/// - `"nats"` - NATS messaging
fn
transport_name
(
&
self
)
->
&
'static
str
;
/// Check if server is healthy and ready to accept requests
///
/// Returns `true` if the server is operational and can handle requests.
/// This is a lightweight check that doesn't perform actual network I/O.
///
/// Implementations should return `false` if:
/// - Server has been explicitly stopped
/// - Underlying transport is disconnected
/// - Server encountered a fatal error
fn
is_healthy
(
&
self
)
->
bool
;
}
lib/runtime/src/pipeline/network/manager.rs
0 → 100644
View file @
06b0ebef
// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//! Network Manager - Single Source of Truth for Network Configuration
//!
//! This module consolidates ALL network-related configuration and creation logic.
//! It is the ONLY place in the codebase that:
//! - Reads environment variables for network configuration
//! - Knows about transport-specific types (SharedHttpServer, TcpRequestClient, etc.)
//! - Performs mode selection based on RequestPlaneMode
//! - Creates servers and clients
//!
//! The rest of the codebase works exclusively with trait objects and never
//! directly accesses transport implementations or configuration.
use
super
::
egress
::
unified_client
::
RequestPlaneClient
;
use
super
::
ingress
::
unified_server
::
RequestPlaneServer
;
use
crate
::
config
::
RequestPlaneMode
;
use
anyhow
::
Result
;
use
async_once_cell
::
OnceCell
;
use
std
::
sync
::
Arc
;
use
tokio_util
::
sync
::
CancellationToken
;
/// Network configuration loaded from environment variables
#[derive(Clone)]
struct
NetworkConfig
{
// HTTP server configuration
http_host
:
String
,
http_port
:
u16
,
http_rpc_root
:
String
,
// TCP server configuration
tcp_host
:
String
,
tcp_port
:
u16
,
// HTTP client configuration
http_client_config
:
super
::
egress
::
http_router
::
Http2Config
,
// TCP client configuration
tcp_client_config
:
super
::
egress
::
tcp_client
::
TcpRequestConfig
,
// NATS configuration (provided externally, not from env)
nats_client
:
Option
<
async_nats
::
Client
>
,
}
impl
NetworkConfig
{
/// Load configuration from environment variables
///
/// This is the ONLY place where network-related environment variables are read.
fn
from_env
(
nats_client
:
Option
<
async_nats
::
Client
>
)
->
Self
{
Self
{
// HTTP server configuration
http_host
:
std
::
env
::
var
(
"DYN_HTTP_RPC_HOST"
)
.unwrap_or_else
(|
_
|
crate
::
utils
::
get_http_rpc_host_from_env
()),
http_port
:
std
::
env
::
var
(
"DYN_HTTP_RPC_PORT"
)
.ok
()
.and_then
(|
p
|
p
.parse
()
.ok
())
.unwrap_or
(
8888
),
http_rpc_root
:
std
::
env
::
var
(
"DYN_HTTP_RPC_ROOT_PATH"
)
.unwrap_or_else
(|
_
|
"/v1/rpc"
.to_string
()),
// TCP server configuration
tcp_host
:
std
::
env
::
var
(
"DYN_TCP_RPC_HOST"
)
.unwrap_or_else
(|
_
|
crate
::
utils
::
get_tcp_rpc_host_from_env
()),
tcp_port
:
std
::
env
::
var
(
"DYN_TCP_RPC_PORT"
)
.ok
()
.and_then
(|
p
|
p
.parse
()
.ok
())
.unwrap_or
(
9999
),
// HTTP client configuration (reads DYN_HTTP2_* env vars)
http_client_config
:
super
::
egress
::
http_router
::
Http2Config
::
from_env
(),
// TCP client configuration (reads DYN_TCP_* env vars)
tcp_client_config
:
super
::
egress
::
tcp_client
::
TcpRequestConfig
::
from_env
(),
// NATS (external)
nats_client
,
}
}
}
/// Network Manager - Central coordinator for all network resources
///
/// # Responsibilities
///
/// 1. **Configuration Management**: Reads and manages all network-related environment variables
/// 2. **Server Creation**: Creates and starts request plane servers based on mode
/// 3. **Client Creation**: Creates request plane clients on demand
/// 4. **Abstraction**: Hides all transport-specific details from the rest of the codebase
///
/// # Design Principles
///
/// - **Single Source of Truth**: All network config and creation logic lives here
/// - **Lazy Initialization**: Servers are created only when first accessed
/// - **Transport Agnostic Interface**: Exposes only trait objects to callers
/// - **No Leaky Abstractions**: Transport types never escape this module
///
/// # Example
///
/// ```ignore
/// // Create manager (typically done once in DistributedRuntime)
/// let manager = NetworkManager::new(cancel_token, nats_client, component_registry);
///
/// // Get server (lazy init, cached)
/// let server = manager.server().await?;
/// server.register_endpoint(...).await?;
///
/// // Create client (not cached, lightweight)
/// let client = manager.create_client()?;
/// client.send_request(...).await?;
/// ```
pub
struct
NetworkManager
{
mode
:
RequestPlaneMode
,
config
:
NetworkConfig
,
server
:
Arc
<
OnceCell
<
Arc
<
dyn
RequestPlaneServer
>>>
,
cancellation_token
:
CancellationToken
,
component_registry
:
crate
::
component
::
Registry
,
}
impl
NetworkManager
{
/// Create a new network manager
///
/// This is the single constructor for NetworkManager. All configuration
/// is loaded from environment variables internally.
///
/// # Arguments
///
/// * `cancellation_token` - Token for graceful shutdown of servers
/// * `nats_client` - Optional NATS client (required only for NATS mode)
/// * `component_registry` - Component registry to get NATS service groups from
///
/// # Returns
///
/// Returns an Arc-wrapped NetworkManager ready to create servers and clients.
pub
fn
new
(
cancellation_token
:
CancellationToken
,
nats_client
:
Option
<
async_nats
::
Client
>
,
component_registry
:
crate
::
component
::
Registry
,
)
->
Arc
<
Self
>
{
let
mode
=
RequestPlaneMode
::
get
();
let
config
=
NetworkConfig
::
from_env
(
nats_client
);
tracing
::
info!
(
mode
=
%
mode
,
http_port
=
config
.http_port
,
tcp_port
=
config
.tcp_port
,
"Initializing NetworkManager"
);
Arc
::
new
(
Self
{
mode
,
config
,
server
:
Arc
::
new
(
OnceCell
::
new
()),
cancellation_token
,
component_registry
,
})
}
/// Get or create the request plane server
///
/// The server is created lazily on first access and cached for subsequent calls.
/// The server is automatically started in the background.
///
/// # Returns
///
/// Returns a trait object that abstracts over HTTP/TCP/NATS implementations.
///
/// # Errors
///
/// Returns an error if:
/// - Server creation fails (e.g., port already in use)
/// - NATS mode is selected but NATS client is not available
/// - Configuration is invalid (e.g., malformed bind address)
pub
async
fn
server
(
&
self
)
->
Result
<
Arc
<
dyn
RequestPlaneServer
>>
{
let
server
=
self
.server
.get_or_try_init
(
async
{
self
.create_server
()
.await
})
.await
?
;
Ok
(
server
.clone
())
}
/// Create a new request plane client
///
/// Clients are lightweight and not cached. Each call creates a new client instance.
///
/// # Returns
///
/// Returns a trait object that abstracts over HTTP/TCP/NATS implementations.
///
/// # Errors
///
/// Returns an error if:
/// - Client creation fails (e.g., invalid configuration)
/// - NATS mode is selected but NATS client is not available
pub
fn
create_client
(
&
self
)
->
Result
<
Arc
<
dyn
RequestPlaneClient
>>
{
match
self
.mode
{
RequestPlaneMode
::
Http
=>
self
.create_http_client
(),
RequestPlaneMode
::
Tcp
=>
self
.create_tcp_client
(),
RequestPlaneMode
::
Nats
=>
self
.create_nats_client
(),
}
}
/// Get the current request plane mode
///
/// This is provided primarily for logging and debugging purposes.
/// Application logic should not branch on mode - use trait objects instead.
pub
fn
mode
(
&
self
)
->
RequestPlaneMode
{
self
.mode
}
// ============================================================================
// PRIVATE: Server Creation
// ============================================================================
async
fn
create_server
(
&
self
)
->
Result
<
Arc
<
dyn
RequestPlaneServer
>>
{
match
self
.mode
{
RequestPlaneMode
::
Http
=>
self
.create_http_server
()
.await
,
RequestPlaneMode
::
Tcp
=>
self
.create_tcp_server
()
.await
,
RequestPlaneMode
::
Nats
=>
self
.create_nats_server
()
.await
,
}
}
async
fn
create_http_server
(
&
self
)
->
Result
<
Arc
<
dyn
RequestPlaneServer
>>
{
use
super
::
ingress
::
http_endpoint
::
SharedHttpServer
;
let
bind_addr
=
format!
(
"{}:{}"
,
self
.config.http_host
,
self
.config.http_port
)
.parse
()
.map_err
(|
e
|
anyhow
::
anyhow!
(
"Invalid HTTP bind address: {}"
,
e
))
?
;
tracing
::
info!
(
bind_addr
=
%
bind_addr
,
rpc_root
=
%
self
.config.http_rpc_root
,
"Creating HTTP request plane server"
);
let
server
=
SharedHttpServer
::
new
(
bind_addr
,
self
.cancellation_token
.clone
());
// Start server in background
let
server_clone
=
server
.clone
();
tokio
::
spawn
(
async
move
{
if
let
Err
(
e
)
=
server_clone
.start
()
.await
{
tracing
::
error!
(
"HTTP request plane server error: {}"
,
e
);
}
});
Ok
(
server
as
Arc
<
dyn
RequestPlaneServer
>
)
}
async
fn
create_tcp_server
(
&
self
)
->
Result
<
Arc
<
dyn
RequestPlaneServer
>>
{
use
super
::
ingress
::
shared_tcp_endpoint
::
SharedTcpServer
;
let
bind_addr
=
format!
(
"{}:{}"
,
self
.config.tcp_host
,
self
.config.tcp_port
)
.parse
()
.map_err
(|
e
|
anyhow
::
anyhow!
(
"Invalid TCP bind address: {}"
,
e
))
?
;
tracing
::
info!
(
bind_addr
=
%
bind_addr
,
"Creating TCP request plane server"
);
let
server
=
SharedTcpServer
::
new
(
bind_addr
,
self
.cancellation_token
.clone
());
// Start server in background
let
server_clone
=
server
.clone
();
tokio
::
spawn
(
async
move
{
if
let
Err
(
e
)
=
server_clone
.start
()
.await
{
tracing
::
error!
(
"TCP request plane server error: {}"
,
e
);
}
});
Ok
(
server
as
Arc
<
dyn
RequestPlaneServer
>
)
}
async
fn
create_nats_server
(
&
self
)
->
Result
<
Arc
<
dyn
RequestPlaneServer
>>
{
use
super
::
ingress
::
nats_server
::
NatsMultiplexedServer
;
let
nats_client
=
self
.config
.nats_client
.as_ref
()
.ok_or_else
(||
anyhow
::
anyhow!
(
"NATS client required for NATS mode"
))
?
;
tracing
::
info!
(
"Creating NATS request plane server"
);
Ok
(
NatsMultiplexedServer
::
new
(
nats_client
.clone
(),
self
.component_registry
.clone
(),
self
.cancellation_token
.clone
(),
)
as
Arc
<
dyn
RequestPlaneServer
>
)
}
// ============================================================================
// PRIVATE: Client Creation
// ============================================================================
fn
create_http_client
(
&
self
)
->
Result
<
Arc
<
dyn
RequestPlaneClient
>>
{
use
super
::
egress
::
http_router
::
HttpRequestClient
;
tracing
::
debug!
(
"Creating HTTP request plane client with config from NetworkManager"
);
Ok
(
Arc
::
new
(
HttpRequestClient
::
with_config
(
self
.config.http_client_config
.clone
(),
)
?
))
}
fn
create_tcp_client
(
&
self
)
->
Result
<
Arc
<
dyn
RequestPlaneClient
>>
{
use
super
::
egress
::
tcp_client
::
TcpRequestClient
;
tracing
::
debug!
(
"Creating TCP request plane client with config from NetworkManager"
);
Ok
(
Arc
::
new
(
TcpRequestClient
::
with_config
(
self
.config.tcp_client_config
.clone
(),
)
?
))
}
fn
create_nats_client
(
&
self
)
->
Result
<
Arc
<
dyn
RequestPlaneClient
>>
{
use
super
::
egress
::
nats_client
::
NatsRequestClient
;
let
nats_client
=
self
.config
.nats_client
.as_ref
()
.ok_or_else
(||
anyhow
::
anyhow!
(
"NATS client required for NATS mode"
))
?
;
tracing
::
debug!
(
"Creating NATS request plane client"
);
Ok
(
Arc
::
new
(
NatsRequestClient
::
new
(
nats_client
.clone
())))
}
}
lib/runtime/src/runtime.rs
View file @
06b0ebef
...
...
@@ -27,7 +27,7 @@ use tokio::{signal, sync::Mutex, task::JoinHandle};
pub
use
tokio_util
::
sync
::
CancellationToken
;
/// Types of Tokio runtimes that can be used to construct a Dynamo [Runtime].
#[derive(Clone)]
#[derive(Clone
,
Debug
)]
enum
RuntimeType
{
Shared
(
Arc
<
tokio
::
runtime
::
Runtime
>
),
External
(
tokio
::
runtime
::
Handle
),
...
...
@@ -339,12 +339,3 @@ impl RuntimeType {
}
}
}
impl
std
::
fmt
::
Debug
for
RuntimeType
{
fn
fmt
(
&
self
,
f
:
&
mut
std
::
fmt
::
Formatter
<
'_
>
)
->
std
::
fmt
::
Result
{
match
self
{
RuntimeType
::
External
(
_
)
=>
write!
(
f
,
"RuntimeType::External"
),
RuntimeType
::
Shared
(
_
)
=>
write!
(
f
,
"RuntimeType::Shared"
),
}
}
}
lib/runtime/src/system_status_server.rs
View file @
06b0ebef
...
...
@@ -798,9 +798,7 @@ mod integration_tests {
endpoint
:
"health"
.to_string
(),
namespace
:
"test_namespace"
.to_string
(),
instance_id
:
1
,
transport
:
crate
::
component
::
TransportType
::
NatsTcp
(
endpoint
.to_string
(),
),
transport
:
crate
::
component
::
TransportType
::
Nats
(
endpoint
.to_string
()),
},
health_check_payload
.clone
(),
);
...
...
lib/runtime/src/transports/nats.rs
View file @
06b0ebef
...
...
@@ -400,12 +400,6 @@ impl Default for NatsAuth {
}
}
/// Is this file name / url in the NATS object store?
/// Checks the name only, does not go to the store.
pub
fn
is_nats_url
(
s
:
&
str
)
->
bool
{
s
.starts_with
(
URL_PREFIX
)
}
/// Extract NATS bucket and key from a nats URL of the form:
/// nats://host[:port]/bucket/key
pub
fn
url_to_bucket_and_key
(
url
:
&
Url
)
->
anyhow
::
Result
<
(
String
,
String
)
>
{
...
...
Prev
1
2
3
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment