use socket2::{Domain, SockAddr, SockRef, Socket, Type};
use std::{
collections::{HashMap, HashSet},
net::{IpAddr, SocketAddr},
os::fd::{AsFd, FromRawFd},
sync::Arc,
time::Duration,
};
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::time::Instant;
use tokio_rustls::TlsAcceptor;
const TOMBSTONE_TTL: Duration = Duration::from_secs(5);
const HANDSHAKE_CANCEL_CLEANUP_TIMEOUT: Duration = Duration::from_secs(1);
use bytes::Bytes;
use derive_builder::Builder;
use futures::{SinkExt, StreamExt};
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use tokio::{
io::AsyncWriteExt,
sync::{mpsc, oneshot},
time,
};
use tokio_util::codec::{FramedRead, FramedWrite};
use super::{
CallHomeHandshake, ControlMessage, PendingConnections, RegisteredStream, StreamOptions,
StreamReceiver, StreamSender, TcpStreamConnectionInfo, TwoPartCodec, bind_listener,
};
use crate::discovery::EndpointInstanceId;
use crate::engine::AsyncEngineContext;
use crate::error::{DynamoError, ErrorType};
use crate::pipeline::{
PipelineError,
network::{
ResponseService, ResponseStreamPrologue, StreamPrologueError,
codec::{TwoPartMessage, TwoPartMessageType},
tcp::StreamType,
},
};
use crate::utils::ip_resolver::{resolve_host_or_interface, resolve_local_host};
use anyhow::{Result, anyhow as error};
pub use crate::utils::ip_resolver::{DefaultIpResolver, IpResolver};
#[allow(dead_code)]
type ResponseType = TwoPartMessage;
#[derive(Debug, Serialize, Deserialize, Clone, Builder, Default)]
pub struct ServerOptions {
#[builder(default = "0")]
pub port: u16,
#[builder(default)]
pub interface: Option<String>,
}
impl ServerOptions {
pub fn builder() -> ServerOptionsBuilder {
ServerOptionsBuilder::default()
}
}
pub struct TcpStreamServer {
address: String,
state: Arc<Mutex<State>>,
}
#[allow(dead_code)]
struct RequestedSendConnection {
context: Arc<dyn AsyncEngineContext>,
connection: oneshot::Sender<Result<StreamSender, StreamPrologueError>>,
send_buffer_count: usize,
}
struct RequestedRecvConnection {
context: Arc<dyn AsyncEngineContext>,
connection: oneshot::Sender<Result<StreamReceiver, StreamPrologueError>>,
cancellation: tokio_util::sync::CancellationToken,
send_buffer_count: usize,
defer_cancellation_until_prologue: bool,
}
fn data_plane_channel<T>(send_buffer_count: usize) -> (mpsc::Sender<T>, mpsc::Receiver<T>) {
mpsc::channel(send_buffer_count.max(1))
}
#[derive(Default)]
struct State {
tx_subjects: HashMap<String, RequestedSendConnection>,
rx_subjects: HashMap<String, RequestedRecvConnection>,
rx_cancellations: HashMap<String, tokio_util::sync::CancellationToken>,
subject_instance: HashMap<String, EndpointInstanceId>,
instance_subjects: HashMap<EndpointInstanceId, HashSet<(StreamType, String)>>,
removed_instances: HashMap<EndpointInstanceId, Instant>,
handle: Option<tokio::task::JoinHandle<Result<()>>>,
}
struct ResponseRegistrationGuard {
state: Arc<Mutex<State>>,
subject: String,
is_active: bool,
}
impl ResponseRegistrationGuard {
fn accept(mut self) -> bool {
self.is_active = false;
TcpStreamServer::finish_response_stream(&self.state, &self.subject)
}
}
impl Drop for ResponseRegistrationGuard {
fn drop(&mut self) {
if self.is_active {
TcpStreamServer::finish_response_stream(&self.state, &self.subject);
}
}
}
fn prune_tombstones(tombstones: &mut HashMap<EndpointInstanceId, Instant>, now: Instant) {
tombstones.retain(|_, ts| now.saturating_duration_since(*ts) < TOMBSTONE_TTL);
}
impl TcpStreamServer {
pub fn local_address(&self) -> Result<SocketAddr> {
Ok(self.address.parse()?)
}
pub fn options_builder() -> ServerOptionsBuilder {
ServerOptionsBuilder::default()
}
pub async fn new(options: ServerOptions) -> Result<Arc<Self>, PipelineError> {
Self::new_with_resolver(options, DefaultIpResolver).await
}
pub async fn new_with_resolver<R: IpResolver>(
options: ServerOptions,
resolver: R,
) -> Result<Arc<Self>, PipelineError> {
let resolved_host = if let Some(host_or_interface) = options.interface.as_deref() {
resolve_host_or_interface(host_or_interface, &resolver).map_err(|error| {
PipelineError::Generic(format!(
"Failed to resolve configured TCP host '{host_or_interface}': {error}"
))
})?
} else {
resolve_local_host(&resolver).map_err(|error| {
PipelineError::Generic(format!("Failed to resolve local IP address: {error}"))
})?
};
if resolved_host.used_loopback_fallback() {
tracing::warn!(
bind_ip = %resolved_host.bind_ip(),
advertise_ip = %resolved_host.advertise_ip(),
"No usable local IP address found; advertising loopback"
);
}
let state = Arc::new(Mutex::new(State::default()));
let tls_acceptor = Self::build_tls_acceptor().map_err(|e| {
PipelineError::Generic(format!("Failed to build TCP TLS acceptor: {}", e))
})?;
let bind_address = SocketAddr::new(resolved_host.bind_ip(), options.port);
let local_address = Self::start(bind_address, state.clone(), tls_acceptor)
.await
.map_err(|error| {
PipelineError::Generic(format!("Failed to start TcpStreamServer: {error}"))
})?;
let advertised_address =
SocketAddr::new(resolved_host.advertise_ip(), local_address.port());
let address = advertised_address.to_string();
tracing::debug!(%local_address, %advertised_address, "tcp transport service started");
Ok(Arc::new(Self { address, state }))
}
pub async fn associate_instance(
&self,
recv_subject: &str,
send_subject: Option<&str>,
id: &EndpointInstanceId,
) -> bool {
let mut state = self.state.lock();
let now = Instant::now();
prune_tombstones(&mut state.removed_instances, now);
if state.removed_instances.contains_key(id) {
tracing::warn!(
recv_subject,
send_subject,
namespace = %id.namespace,
component = %id.component,
endpoint = %id.endpoint,
instance_id = id.instance_id,
"Cancelling subject immediately: instance already removed (tombstoned)"
);
state.rx_subjects.remove(recv_subject);
if let Some(token) = state.rx_cancellations.remove(recv_subject) {
token.cancel();
}
if let Some(s) = send_subject {
state.tx_subjects.remove(s);
}
return false;
}
state
.subject_instance
.insert(recv_subject.to_string(), id.clone());
if let Some(s) = send_subject {
state.subject_instance.insert(s.to_string(), id.clone());
}
let entry = state.instance_subjects.entry(id.clone()).or_default();
entry.insert((StreamType::Response, recv_subject.to_string()));
if let Some(s) = send_subject {
entry.insert((StreamType::Request, s.to_string()));
}
true
}
pub async fn associate_request_instance(&self, subject: &str, id: &EndpointInstanceId) -> bool {
let mut state = self.state.lock();
let now = Instant::now();
prune_tombstones(&mut state.removed_instances, now);
if state.removed_instances.contains_key(id) {
state.tx_subjects.remove(subject);
return false;
}
state
.subject_instance
.insert(subject.to_string(), id.clone());
state
.instance_subjects
.entry(id.clone())
.or_default()
.insert((StreamType::Request, subject.to_string()));
true
}
pub async fn cancel_recv_stream(&self, subject: &str) {
let mut state = self.state.lock();
state.rx_subjects.remove(subject);
if let Some(token) = state.rx_cancellations.remove(subject) {
token.cancel();
}
if let Some(key) = state.subject_instance.remove(subject)
&& let Some(subjects) = state.instance_subjects.get_mut(&key)
{
subjects.remove(&(StreamType::Response, subject.to_string()));
if subjects.is_empty() {
state.instance_subjects.remove(&key);
}
}
}
pub async fn cancel_send_stream(&self, subject: &str) {
let mut state = self.state.lock();
state.tx_subjects.remove(subject);
if let Some(key) = state.subject_instance.remove(subject)
&& let Some(subjects) = state.instance_subjects.get_mut(&key)
{
subjects.remove(&(StreamType::Request, subject.to_string()));
if subjects.is_empty() {
state.instance_subjects.remove(&key);
}
}
}
pub async fn cancel_instance_streams(&self, id: &EndpointInstanceId) -> usize {
let mut state = self.state.lock();
let now = Instant::now();
prune_tombstones(&mut state.removed_instances, now);
state.removed_instances.insert(id.clone(), now);
let subjects = match state.instance_subjects.remove(id) {
Some(subjects) => subjects,
None => return 0,
};
let count = subjects.len();
for (kind, subject) in &subjects {
match kind {
StreamType::Response => {
state.rx_subjects.remove(subject);
if let Some(token) = state.rx_cancellations.remove(subject) {
token.cancel();
}
}
StreamType::Request => {
state.tx_subjects.remove(subject);
}
}
state.subject_instance.remove(subject);
}
count
}
pub async fn clear_instance_tombstone(&self, id: &EndpointInstanceId) {
let mut state = self.state.lock();
state.removed_instances.remove(id);
}
fn build_tls_acceptor() -> anyhow::Result<Option<TlsAcceptor>> {
use crate::config::environment_names::tcp_response_stream::tls as env;
let cert_path = std::env::var(env::DYN_TCP_TLS_CERT_PATH).ok();
let key_path = std::env::var(env::DYN_TCP_TLS_KEY_PATH).ok();
let client_ca = std::env::var(env::DYN_TCP_TLS_CLIENT_CA_CERT_PATH).ok();
Ok(crate::tls_utils::server_tls_acceptor_config(
"TCP server",
cert_path.as_deref().map(std::path::Path::new),
key_path.as_deref().map(std::path::Path::new),
client_ca.as_deref().map(std::path::Path::new),
)?
.map(|config| TlsAcceptor::from(Arc::new(config))))
}
async fn start(
bind_address: SocketAddr,
state: Arc<Mutex<State>>,
tls_acceptor: Option<TlsAcceptor>,
) -> Result<SocketAddr> {
let state_clone = state.clone();
let (ready_tx, ready_rx) = tokio::sync::oneshot::channel::<Result<SocketAddr>>();
{
let mut guard = state.lock();
if guard.handle.is_some() {
panic!("TcpStreamServer already started");
}
guard.handle = Some(tokio::spawn(tcp_listener(
bind_address,
state_clone,
tls_acceptor,
ready_tx,
)));
}
ready_rx.await?
}
fn insert_request_stream(&self, subject: String, connection: RequestedSendConnection) {
self.state.lock().tx_subjects.insert(subject, connection);
}
fn insert_response_stream(&self, subject: String, connection: RequestedRecvConnection) {
let cancellation = connection.cancellation.clone();
let mut state = self.state.lock();
state.rx_subjects.insert(subject.clone(), connection);
state.rx_cancellations.insert(subject, cancellation);
}
fn take_request_stream(state: &Mutex<State>, subject: &str) -> Option<RequestedSendConnection> {
let mut state = state.lock();
let connection = state.tx_subjects.remove(subject);
if let Some(key) = state.subject_instance.remove(subject)
&& let Some(subjects) = state.instance_subjects.get_mut(&key)
{
subjects.remove(&(StreamType::Request, subject.to_string()));
if subjects.is_empty() {
state.instance_subjects.remove(&key);
}
}
connection
}
fn take_response_stream(
state: &Mutex<State>,
subject: &str,
) -> Option<RequestedRecvConnection> {
state.lock().rx_subjects.remove(subject)
}
fn finish_response_stream(state: &Mutex<State>, subject: &str) -> bool {
let mut state = state.lock();
let is_pending = state.rx_cancellations.remove(subject).is_some();
if let Some(key) = state.subject_instance.remove(subject)
&& let Some(subjects) = state.instance_subjects.get_mut(&key)
{
subjects.remove(&(StreamType::Response, subject.to_string()));
if subjects.is_empty() {
state.instance_subjects.remove(&key);
}
}
is_pending
}
}
#[async_trait::async_trait]
impl ResponseService for TcpStreamServer {
async fn register(&self, options: StreamOptions) -> PendingConnections {
let address = self.address.clone();
tracing::debug!("Registering new TcpStream on {address}");
let send_stream = if options.enable_request_stream {
let sender_subject = uuid::Uuid::new_v4().to_string();
let registry_subject = sender_subject.clone();
let (pending_sender_tx, pending_sender_rx) = oneshot::channel();
let connection_info = RequestedSendConnection {
context: options.context.clone(),
connection: pending_sender_tx,
send_buffer_count: options.send_buffer_count,
};
let cleanup_subject = sender_subject.clone();
let cleanup_state = self.state.clone();
let registered_stream = RegisteredStream::new(
TcpStreamConnectionInfo {
address: address.clone(),
subject: sender_subject,
context: options.context.id().to_string(),
stream_type: StreamType::Request,
}
.into(),
pending_sender_rx,
)
.with_cleanup(move || {
tokio::spawn(async move {
let mut state = cleanup_state.lock();
state.tx_subjects.remove(&cleanup_subject);
if let Some(key) = state.subject_instance.remove(&cleanup_subject)
&& let Some(subjects) = state.instance_subjects.get_mut(&key)
{
subjects.remove(&(StreamType::Request, cleanup_subject.clone()));
if subjects.is_empty() {
state.instance_subjects.remove(&key);
}
}
});
});
self.insert_request_stream(registry_subject, connection_info);
Some(registered_stream)
} else {
None
};
let recv_stream = if options.enable_response_stream {
let (pending_recver_tx, pending_recver_rx) = oneshot::channel();
let receiver_subject = uuid::Uuid::new_v4().to_string();
let registry_subject = receiver_subject.clone();
let connection_info = RequestedRecvConnection {
context: options.context.clone(),
connection: pending_recver_tx,
cancellation: tokio_util::sync::CancellationToken::new(),
send_buffer_count: options.send_buffer_count,
defer_cancellation_until_prologue: options.defer_cancellation_until_prologue,
};
let cleanup_subject = receiver_subject.clone();
let cleanup_state = self.state.clone();
let registered_stream = RegisteredStream::new(
TcpStreamConnectionInfo {
address: address.clone(),
subject: receiver_subject,
context: options.context.id().to_string(),
stream_type: StreamType::Response,
}
.into(),
pending_recver_rx,
)
.with_cleanup(move || {
tokio::spawn(async move {
let mut state = cleanup_state.lock();
state.rx_subjects.remove(&cleanup_subject);
if let Some(token) = state.rx_cancellations.remove(&cleanup_subject) {
token.cancel();
}
if let Some(key) = state.subject_instance.remove(&cleanup_subject)
&& let Some(subjects) = state.instance_subjects.get_mut(&key)
{
subjects.remove(&(StreamType::Response, cleanup_subject.clone()));
if subjects.is_empty() {
state.instance_subjects.remove(&key);
}
}
});
});
self.insert_response_stream(registry_subject, connection_info);
Some(registered_stream)
} else {
None
};
PendingConnections {
send_stream,
recv_stream,
}
}
}
const ACCEPT_BACKOFF_INITIAL_DELAY: Duration = Duration::from_millis(5);
const ACCEPT_BACKOFF_MAX_DELAY: Duration = Duration::from_secs(1);
const ACCEPT_BACKOFF_LOG_INTERVAL: Duration = Duration::from_secs(5);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AcceptFailure {
Exhaustion,
Ordinary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct AcceptBackoffAction {
delay: Duration,
log_suppressed: Option<u64>,
}
#[derive(Debug)]
struct AcceptBackoff {
initial_delay: Duration,
max_delay: Duration,
log_interval: Duration,
current_delay: Duration,
suppressed: u64,
last_log_at: Option<std::time::Instant>,
in_backoff: bool,
}
impl Default for AcceptBackoff {
fn default() -> Self {
Self {
initial_delay: ACCEPT_BACKOFF_INITIAL_DELAY,
max_delay: ACCEPT_BACKOFF_MAX_DELAY,
log_interval: ACCEPT_BACKOFF_LOG_INTERVAL,
current_delay: ACCEPT_BACKOFF_INITIAL_DELAY,
suppressed: 0,
last_log_at: None,
in_backoff: false,
}
}
}
impl AcceptBackoff {
fn classify(err: &std::io::Error) -> AcceptFailure {
#[cfg(unix)]
{
if matches!(
err.raw_os_error(),
Some(libc::EMFILE) | Some(libc::ENFILE) | Some(libc::ENOBUFS) | Some(libc::ENOMEM)
) {
return AcceptFailure::Exhaustion;
}
}
#[cfg(not(unix))]
{
let _ = err;
}
AcceptFailure::Ordinary
}
fn record_exhaustion(&mut self, now: std::time::Instant) -> AcceptBackoffAction {
self.in_backoff = true;
let delay = self.current_delay;
self.current_delay = self.current_delay.saturating_mul(2).min(self.max_delay);
let due = match self.last_log_at {
None => true,
Some(prev) => now.saturating_duration_since(prev) >= self.log_interval,
};
let log_suppressed = if due {
self.last_log_at = Some(now);
Some(std::mem::take(&mut self.suppressed))
} else {
self.suppressed += 1;
None
};
AcceptBackoffAction {
delay,
log_suppressed,
}
}
fn record_success(&mut self, now: impl FnOnce() -> std::time::Instant) -> Option<u64> {
if !self.in_backoff {
return None;
}
self.current_delay = self.initial_delay;
self.in_backoff = false;
let now = now();
let due = match self.last_log_at {
None => true,
Some(prev) => now.saturating_duration_since(prev) >= self.log_interval,
};
if !due {
return None;
}
self.last_log_at = Some(now);
Some(std::mem::take(&mut self.suppressed))
}
}
async fn handle_accept_error(err: &std::io::Error, backoff: &mut AcceptBackoff) -> Duration {
match AcceptBackoff::classify(err) {
AcceptFailure::Ordinary => {
tracing::warn!(error = %err, "failed to accept tcp connection");
#[cfg(debug_assertions)]
eprintln!("failed to accept tcp connection: {}", err);
Duration::ZERO
}
AcceptFailure::Exhaustion => {
crate::metrics::transport_metrics::TCP_ACCEPT_BACKOFF_TOTAL.inc();
let action = backoff.record_exhaustion(std::time::Instant::now());
if let Some(suppressed) = action.log_suppressed {
tracing::warn!(
error = %err,
retry_delay_ms = action.delay.as_millis() as u64,
suppressed_failures = suppressed,
"tcp accept failed: out of file descriptors or kernel memory; backing off before retry"
);
}
time::sleep(action.delay).await;
action.delay
}
}
}
type BoxRead = Box<dyn tokio::io::AsyncRead + Unpin + Send>;
type BoxWrite = Box<dyn tokio::io::AsyncWrite + Unpin + Send>;
async fn cancel_response_handshake(
connection: oneshot::Sender<Result<StreamReceiver, StreamPrologueError>>,
registration_guard: Option<ResponseRegistrationGuard>,
mut writer: FramedWrite<BoxWrite, TwoPartCodec>,
) {
drop(registration_guard);
drop(connection);
let cleanup = async move {
if let Ok(bytes) = serde_json::to_vec(&ControlMessage::Kill) {
let _ = writer.send(TwoPartMessage::from_header(bytes.into())).await;
}
let _ = writer.into_inner().shutdown().await;
};
if time::timeout(HANDSHAKE_CANCEL_CLEANUP_TIMEOUT, cleanup)
.await
.is_err()
{
tracing::debug!("timed out notifying worker of cancelled response handshake");
}
}
async fn tcp_listener(
addr: SocketAddr,
state: Arc<Mutex<State>>,
tls_acceptor: Option<TlsAcceptor>,
read_tx: tokio::sync::oneshot::Sender<Result<SocketAddr>>,
) -> Result<()> {
let listener = bind_listener(addr)
.map_err(|e| anyhow::anyhow!("Failed to start TcpListender on {}: {}", addr, e));
let listener = match listener {
Ok(listener) => {
let addr = listener
.local_addr()
.map_err(|e| anyhow::anyhow!("Failed get SocketAddr: {:?}", e))
.unwrap();
read_tx.send(Ok(addr)).expect("Failed to send ready signal");
listener
}
Err(e) => {
read_tx.send(Err(e)).expect("Failed to send ready signal");
return Err(anyhow::anyhow!("Failed to start TcpListender on {}", addr));
}
};
let mut accept_backoff = AcceptBackoff::default();
loop {
let (stream, _addr) = match listener.accept().await {
Ok((stream, _addr)) => {
if let Some(suppressed) = accept_backoff.record_success(std::time::Instant::now) {
tracing::warn!(
suppressed_failures = suppressed,
"tcp accept recovered from resource exhaustion"
);
}
(stream, _addr)
}
Err(e) => {
handle_accept_error(&e, &mut accept_backoff).await;
continue;
}
};
match stream.set_nodelay(true) {
Ok(_) => (),
Err(e) => {
tracing::warn!("failed to set tcp stream to nodelay: {e}");
}
}
match SockRef::from(&stream).set_linger(Some(std::time::Duration::from_secs(0))) {
Ok(_) => (),
Err(e) => {
tracing::warn!("failed to set tcp stream to linger: {e}");
}
}
let state_clone = state.clone();
let tls_acceptor_clone = tls_acceptor.clone();
tokio::spawn(async move {
let (reader, writer) = if let Some(ref tls) = tls_acceptor_clone {
match tokio::time::timeout(
crate::tls_utils::handshake_timeout(),
tls.accept(stream),
)
.await
{
Ok(Ok(tls_stream)) => {
let (r, w) = tokio::io::split(tls_stream);
(Box::new(r) as BoxRead, Box::new(w) as BoxWrite)
}
Ok(Err(e)) => {
tracing::warn!("TLS handshake failed: {e}");
return;
}
Err(_) => {
tracing::warn!("TLS handshake timed out");
return;
}
}
} else {
let (r, w) = tokio::io::split(stream);
(Box::new(r) as BoxRead, Box::new(w) as BoxWrite)
};
handle_connection(reader, writer, state_clone).await;
});
}
async fn handle_connection(reader: BoxRead, writer: BoxWrite, state: Arc<Mutex<State>>) {
let result = process_stream(reader, writer, state).await;
match result {
Ok(_) => tracing::trace!("successfully processed tcp connection"),
Err(e) => {
tracing::warn!("failed to handle tcp connection: {e}");
#[cfg(debug_assertions)]
eprintln!("failed to handle tcp connection: {}", e);
}
}
}
async fn process_stream(
read_half: BoxRead,
write_half: BoxWrite,
state: Arc<Mutex<State>>,
) -> Result<()> {
let mut framed_reader = FramedRead::new(read_half, TwoPartCodec::default());
let framed_writer = FramedWrite::new(write_half, TwoPartCodec::default());
let first_message =
tokio::time::timeout(std::time::Duration::from_secs(10), framed_reader.next())
.await
.map_err(|_| error!("Timed out waiting for CallHomeHandshake"))?
.ok_or(error!("Connection closed without a ControlMessage"))??;
let handshake: CallHomeHandshake = match first_message.header() {
Some(header) => serde_json::from_slice(header).map_err(|e| {
error!(
"Failed to deserialize the first message as a valid `CallHomeHandshake`: {e}",
)
})?,
None => {
return Err(error!("Expected ControlMessage, got DataMessage"));
}
};
match handshake.stream_type {
StreamType::Request => {
process_request_stream(handshake.subject, state, framed_reader, framed_writer).await
}
StreamType::Response => {
process_response_stream(handshake.subject, state, framed_reader, framed_writer)
.await
}
}
}
async fn process_request_stream(
subject: String,
state: Arc<Mutex<State>>,
reader: FramedRead<BoxRead, TwoPartCodec>,
writer: FramedWrite<BoxWrite, TwoPartCodec>,
) -> Result<()> {
drop(reader);
let request_stream = TcpStreamServer::take_request_stream(&state, &subject).ok_or_else(|| {
error!(
"Subject not found: {}; downstream subscriber specified a subject unknown to the upstream publisher",
subject
)
})?;
let RequestedSendConnection {
context,
connection,
send_buffer_count,
} = request_stream;
let (request_tx, request_rx) = data_plane_channel(send_buffer_count);
if connection
.send(Ok(crate::pipeline::network::StreamSender {
tx: request_tx,
prologue: None,
}))
.is_err()
{
return Err(error!(
"The requester of the request stream has been dropped before the connection was established"
));
}
request_stream_send_handler(writer, request_rx, context).await;
Ok(())
}
async fn request_stream_send_handler(
mut framed_writer: FramedWrite<BoxWrite, TwoPartCodec>,
mut request_rx: mpsc::Receiver<TwoPartMessage>,
context: Arc<dyn AsyncEngineContext>,
) {
let killed = context.killed();
let stopped = context.stopped();
tokio::pin!(killed, stopped);
let closing_msg: Option<ControlMessage> = loop {
tokio::select! {
biased;
_ = &mut killed => {
tracing::trace!("context kill received in request-stream send handler");
break Some(ControlMessage::Kill);
}
_ = &mut stopped => {
tracing::trace!("context stop received in request-stream send handler");
break Some(ControlMessage::Stop);
}
msg = request_rx.recv() => {
match msg {
Some(msg) => {
if let Err(e) = framed_writer.send(msg).await {
tracing::trace!(
"failed to send request-stream frame to downstream: {:?}",
e
);
break None;
}
}
None => {
tracing::trace!("upstream request-stream sender closed; sending sentinel");
break Some(ControlMessage::Sentinel);
}
}
}
}
};
if let Some(ctrl) = closing_msg
&& let Ok(bytes) = serde_json::to_vec(&ctrl)
&& let Err(err) = framed_writer
.send(TwoPartMessage::from_header(bytes.into()))
.await
{
tracing::trace!(?err, ?ctrl, "request-stream closing-frame send failed");
}
let mut inner = framed_writer.into_inner();
if let Err(err) = inner.flush().await {
tracing::trace!(?err, "request-stream socket flush failed");
}
if let Err(err) = inner.shutdown().await {
tracing::trace!(?err, "request-stream socket shutdown failed");
}
}
async fn process_response_stream(
subject: String,
state: Arc<Mutex<State>>,
mut reader: FramedRead<BoxRead, TwoPartCodec>,
mut writer: FramedWrite<BoxWrite, TwoPartCodec>,
) -> Result<()> {
let response_stream = TcpStreamServer::take_response_stream(&state, &subject).ok_or_else(|| {
error!("Subject not found: {}; upstream publisher specified a subject unknown to the downsteam subscriber", subject)
})?;
let registration_guard = ResponseRegistrationGuard {
state,
subject,
is_active: true,
};
let RequestedRecvConnection {
context,
mut connection,
cancellation,
send_buffer_count,
defer_cancellation_until_prologue,
} = response_stream;
let prologue = tokio::select! {
biased;
_ = cancellation.cancelled() => {
cancel_response_handshake(connection, Some(registration_guard), writer).await;
return Ok(());
}
_ = context.killed(), if !defer_cancellation_until_prologue => Err(ControlMessage::Kill),
_ = connection.closed() => Err(ControlMessage::Kill),
_ = context.stopped(), if !defer_cancellation_until_prologue => Err(ControlMessage::Stop),
prologue = reader.next() => Ok(prologue),
};
let prologue = match prologue {
Ok(prologue) => {
prologue.ok_or(error!("Connection closed without a ControlMessage"))??
}
Err(control) => {
drop(registration_guard);
let message = "Request cancelled before response stream was established";
let error = DynamoError::builder()
.error_type(ErrorType::Cancelled)
.message(message)
.build();
let _ = connection.send(Err(StreamPrologueError::new(message, error)));
writer
.send(TwoPartMessage::from_header(
serde_json::to_vec(&control)?.into(),
))
.await?;
writer.close().await?;
return Ok(());
}
};
let prologue = match prologue.into_message_type() {
TwoPartMessageType::HeaderOnly(header) => {
match serde_json::from_slice::<ResponseStreamPrologue>(&header) {
Ok(prologue) => prologue,
Err(e) => {
let msg = format!("malformed prologue: {e}");
let _ =
connection.send(Err(StreamPrologueError::from_message(msg.clone())));
return Err(error!(msg));
}
}
}
_ => {
let msg = "malformed prologue: expected HeaderOnly ControlMessage";
let _ = connection.send(Err(StreamPrologueError::from_message(msg)));
return Err(error!(msg));
}
};
if let Some(error) = prologue.error {
let returned = error!("Received error prologue: {error}");
let _ = connection.send(Err(StreamPrologueError {
message: error,
typed_error: prologue.typed_error,
}));
return Err(returned);
}
if !registration_guard.accept() {
cancel_response_handshake(connection, None, writer).await;
return Ok(());
}
let (response_tx, response_rx) = data_plane_channel(send_buffer_count);
if connection
.send(Ok(crate::pipeline::network::StreamReceiver {
rx: response_rx,
}))
.is_err()
{
return Err(error!(
"The requester of the stream has been dropped before the connection was established"
));
}
let (control_tx, control_rx) = mpsc::channel::<ControlMessage>(1);
let send_task = tokio::spawn(network_send_handler(writer, control_rx));
let recv_task = tokio::spawn(network_receive_handler(
reader,
response_tx,
control_tx,
context.clone(),
));
let (monitor_result, forward_result) = tokio::join!(send_task, recv_task);
monitor_result?;
forward_result?;
Ok(())
}
async fn network_receive_handler(
mut framed_reader: FramedRead<BoxRead, TwoPartCodec>,
response_tx: mpsc::Sender<Bytes>,
control_tx: mpsc::Sender<ControlMessage>,
context: Arc<dyn AsyncEngineContext>,
) {
let response_closed = response_tx.closed();
let killed = context.killed();
let stopped = context.stopped();
tokio::pin!(response_closed, killed, stopped);
let mut can_stop = true;
loop {
tokio::select! {
biased;
_ = &mut response_closed => {
tracing::trace!("response channel closed before the client finished writing data");
let _ = control_tx.send(ControlMessage::Kill).await;
break;
}
_ = &mut killed => {
tracing::trace!("context kill signal received; shutting down");
let _ = control_tx.send(ControlMessage::Kill).await;
break;
}
_ = &mut stopped, if can_stop => {
tracing::trace!("context stop signal received; shutting down");
can_stop = false;
let _ = control_tx.send(ControlMessage::Stop).await;
}
msg = framed_reader.next() => {
match msg {
Some(Ok(msg)) => {
let (header, data) = msg.into_parts();
if !header.is_empty() {
match process_control_message(header) {
Ok(ControlAction::Continue) => {}
Ok(ControlAction::Shutdown) => {
if !data.is_empty() {
tracing::warn!(
data_len = data.len(),
"client sent Sentinel with data (protocol violation); killing stream"
);
let _ = control_tx.send(ControlMessage::Kill).await;
break;
}
tracing::trace!("received sentinel message; shutting down");
break;
}
Err(e) => {
tracing::warn!(err = ?e, "malformed control message, closing connection");
let _ = control_tx.send(ControlMessage::Kill).await;
break;
}
}
}
if !data.is_empty()
&& let Err(err) = response_tx.send(data).await {
tracing::debug!(?err, "forwarding body/data to response channel failed");
let _ = control_tx.send(ControlMessage::Kill).await;
break;
};
}
Some(Err(e)) => {
tracing::warn!(err = ?e, "tcp stream read error from worker, closing connection");
let _ = control_tx.send(ControlMessage::Kill).await;
break;
}
None => {
tracing::trace!("tcp stream was closed by client");
break;
}
}
}
}
}
}
async fn network_send_handler(
socket_tx: FramedWrite<BoxWrite, TwoPartCodec>,
control_rx: mpsc::Receiver<ControlMessage>,
) {
let mut socket_tx = socket_tx;
let mut control_rx = control_rx;
while let Some(control_msg) = control_rx.recv().await {
if matches!(control_msg, ControlMessage::Sentinel) {
tracing::warn!("received sentinel on send-side control channel; dropping");
continue;
}
let bytes = match serde_json::to_vec(&control_msg) {
Ok(b) => b,
Err(e) => {
tracing::warn!(err = ?e, ?control_msg, "failed to serialize control message");
continue;
}
};
let message = TwoPartMessage::from_header(bytes.into());
match socket_tx.send(message).await {
Ok(_) => tracing::debug!(?control_msg, "issued control message"),
Err(e) => {
tracing::debug!(err = ?e, ?control_msg, "failed to send control message")
}
}
}
let mut inner = socket_tx.into_inner();
if let Err(e) = inner.flush().await {
tracing::debug!("failed to flush socket: {e}");
}
if let Err(e) = inner.shutdown().await {
tracing::debug!("failed to shutdown socket: {e}");
}
}
}
enum ControlAction {
Continue,
Shutdown,
}
fn process_control_message(message: Bytes) -> Result<ControlAction> {
match serde_json::from_slice::<ControlMessage>(&message)? {
ControlMessage::Sentinel => {
tracing::trace!("sentinel received; shutting down");
Ok(ControlAction::Shutdown)
}
ControlMessage::Kill | ControlMessage::Stop => {
anyhow::bail!("unexpected control message on response stream");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::AsyncEngineContextProvider;
use crate::error::{BackendError, DynamoError, ErrorType};
use crate::pipeline::Context;
use crate::pipeline::network::DEFAULT_SEND_BUFFER_COUNT;
use crate::pipeline::network::tcp::client::TcpClient;
use crate::tls_utils::test_certs::self_signed_pair;
use crate::utils::ip_resolver::test_support::{ProbeOutcome, StubResolver};
use tokio::io::{AsyncWriteExt, ReadHalf, WriteHalf};
use tokio::net::TcpStream;
#[test]
fn build_tls_acceptor_no_env_vars_is_plaintext() {
if crate::test_utils::run_isolated(
concat!(
module_path!(),
"::build_tls_acceptor_no_env_vars_is_plaintext"
),
&[],
) {
return;
}
temp_env::with_vars_unset(
[
"DYN_TCP_TLS_CERT_PATH",
"DYN_TCP_TLS_KEY_PATH",
"DYN_TCP_TLS_CLIENT_CA_CERT_PATH",
],
|| {
assert!(TcpStreamServer::build_tls_acceptor().unwrap().is_none());
},
);
}
#[test]
fn build_tls_acceptor_partial_config_errors() {
if crate::test_utils::run_isolated(
concat!(module_path!(), "::build_tls_acceptor_partial_config_errors"),
&[],
) {
return;
}
let (cert, key) = self_signed_pair();
let cert_str = cert.path().to_str().unwrap();
let key_str = key.path().to_str().unwrap();
temp_env::with_vars(
[
("DYN_TCP_TLS_CERT_PATH", Some(cert_str)),
("DYN_TCP_TLS_KEY_PATH", None),
],
|| assert!(TcpStreamServer::build_tls_acceptor().is_err()),
);
temp_env::with_vars(
[
("DYN_TCP_TLS_CERT_PATH", None),
("DYN_TCP_TLS_KEY_PATH", Some(key_str)),
],
|| assert!(TcpStreamServer::build_tls_acceptor().is_err()),
);
}
#[test]
fn build_tls_acceptor_both_paths_is_tls() {
if crate::test_utils::run_isolated(
concat!(module_path!(), "::build_tls_acceptor_both_paths_is_tls"),
&[],
) {
return;
}
let (cert, key) = self_signed_pair();
temp_env::with_vars(
[
("DYN_TCP_TLS_CERT_PATH", Some(cert.path().to_str().unwrap())),
("DYN_TCP_TLS_KEY_PATH", Some(key.path().to_str().unwrap())),
],
|| assert!(TcpStreamServer::build_tls_acceptor().unwrap().is_some()),
);
}
#[test]
fn build_tls_acceptor_with_client_ca_is_mtls() {
if crate::test_utils::run_isolated(
concat!(
module_path!(),
"::build_tls_acceptor_with_client_ca_is_mtls"
),
&[],
) {
return;
}
let (cert, key) = self_signed_pair();
temp_env::with_vars(
[
("DYN_TCP_TLS_CERT_PATH", Some(cert.path().to_str().unwrap())),
("DYN_TCP_TLS_KEY_PATH", Some(key.path().to_str().unwrap())),
(
"DYN_TCP_TLS_CLIENT_CA_CERT_PATH",
Some(cert.path().to_str().unwrap()),
),
],
|| assert!(TcpStreamServer::build_tls_acceptor().unwrap().is_some()),
);
}
#[test]
fn build_tls_acceptor_client_ca_without_server_identity_errors() {
if crate::test_utils::run_isolated(
concat!(
module_path!(),
"::build_tls_acceptor_client_ca_without_server_identity_errors"
),
&[],
) {
return;
}
let (cert, _key) = self_signed_pair();
temp_env::with_vars(
[
("DYN_TCP_TLS_CERT_PATH", None),
("DYN_TCP_TLS_KEY_PATH", None),
(
"DYN_TCP_TLS_CLIENT_CA_CERT_PATH",
Some(cert.path().to_str().unwrap()),
),
],
|| assert!(TcpStreamServer::build_tls_acceptor().is_err()),
);
}
async fn registered_tcp_info(server: &TcpStreamServer) -> TcpStreamConnectionInfo {
let context = Context::new(());
let stream_options = StreamOptions::builder()
.context(context.context())
.enable_request_stream(false)
.enable_response_stream(true)
.build()
.unwrap();
let pending_connection = server.register(stream_options).await;
let connection_info = pending_connection
.recv_stream
.as_ref()
.unwrap()
.connection_info
.clone();
connection_info.try_into().unwrap()
}
#[tokio::test]
async fn wildcard_bind_advertises_concrete_ipv4_with_bound_port() {
for host in ["0.0.0.0", "::ffff:0.0.0.0", " [::ffff:0.0.0.0] "] {
let mut resolver = StubResolver::not_found();
resolver
.interfaces
.push(("eth0", "192.0.2.20".parse().unwrap()));
let options = ServerOptions::builder()
.port(0)
.interface(Some(host.to_string()))
.build()
.unwrap();
let server = TcpStreamServer::new_with_resolver(options, resolver)
.await
.unwrap();
let tcp_info = registered_tcp_info(&server).await;
let socket_addr: SocketAddr = tcp_info.address.parse().unwrap();
assert_eq!(socket_addr.ip(), "192.0.2.20".parse::<IpAddr>().unwrap());
assert_ne!(socket_addr.port(), 0);
assert!(!socket_addr.ip().is_unspecified());
let _connection = tokio::net::TcpStream::connect(SocketAddr::new(
std::net::Ipv4Addr::LOCALHOST.into(),
socket_addr.port(),
))
.await
.unwrap();
}
}
#[tokio::test]
async fn real_bracketed_ipv6_host_binds_and_registers() {
if let Err(error) = std::net::TcpListener::bind("[::1]:0") {
eprintln!("Skipping IPv6 loopback bind test: {error}");
return;
}
let options = ServerOptions::builder()
.port(0)
.interface(Some("[::1]".to_string()))
.build()
.unwrap();
let server = TcpStreamServer::new_with_resolver(options, StubResolver::not_found())
.await
.unwrap();
let tcp_info = registered_tcp_info(&server).await;
let socket_addr: SocketAddr = tcp_info.address.parse().unwrap();
assert_eq!(socket_addr.ip(), IpAddr::V6(std::net::Ipv6Addr::LOCALHOST));
assert_ne!(socket_addr.port(), 0);
assert!(
tcp_info.address.starts_with("[::1]:"),
"{}",
tcp_info.address
);
}
#[tokio::test]
async fn tcp_server_preserves_interface_enumeration_error() {
let mut resolver = StubResolver::not_found();
resolver.interface_error = Some(ProbeOutcome::Platform("test-platform"));
let options = ServerOptions::builder().port(0).build().unwrap();
let error = TcpStreamServer::new_with_resolver(options, resolver)
.await
.err()
.expect("an interface enumeration failure must be returned");
let error = error.to_string();
assert!(
error.contains("failed to enumerate network interfaces"),
"{error}"
);
assert!(error.contains("test-platform"), "{error}");
}
#[tokio::test]
async fn test_tcp_stream_server_default_behavior() {
let options = ServerOptions::default();
let result = TcpStreamServer::new(options).await;
assert!(
result.is_ok(),
"TcpStreamServer::new should succeed with default options"
);
let server = result.unwrap();
let tcp_info = registered_tcp_info(&server).await;
let socket_addr = tcp_info.address.parse::<std::net::SocketAddr>().unwrap();
assert!(
socket_addr.port() > 0,
"Server should be assigned a valid port number"
);
println!(
"Server created successfully with address: {}",
tcp_info.address
);
}
#[test]
fn data_plane_channel_capacity_matches_send_buffer_count() {
let (tx, _rx) = data_plane_channel::<()>(7);
assert_eq!(tx.max_capacity(), 7);
let (tx, _rx) = data_plane_channel::<()>(DEFAULT_SEND_BUFFER_COUNT);
assert_eq!(tx.max_capacity(), 64);
let (tx, _rx) = data_plane_channel::<()>(0);
assert_eq!(tx.max_capacity(), 1);
}
#[tokio::test]
async fn register_threads_send_buffer_count_into_connection_structs() {
let server = TcpStreamServer::new(ServerOptions::default())
.await
.expect("server");
let context = Context::new(());
let options = StreamOptions::builder()
.context(context.context())
.enable_request_stream(true)
.enable_response_stream(true)
.send_buffer_count(7)
.build()
.unwrap();
let _pending = server.register(options).await;
let state = server.state.lock();
assert_eq!(state.tx_subjects.len(), 1, "one request stream registered");
assert_eq!(state.rx_subjects.len(), 1, "one response stream registered");
assert!(
state.tx_subjects.values().all(|c| c.send_buffer_count == 7),
"send_buffer_count must reach RequestedSendConnection"
);
assert!(
state.rx_subjects.values().all(|c| c.send_buffer_count == 7),
"send_buffer_count must reach RequestedRecvConnection"
);
}
#[tokio::test]
async fn test_tcp_stream_server_fallback_to_loopback() {
let options = ServerOptions::builder().port(0).build().unwrap();
let result = TcpStreamServer::new_with_resolver(options, StubResolver::not_found()).await;
assert!(
result.is_ok(),
"Server creation should succeed with fallback even when IP detection fails"
);
let server = result.unwrap();
let tcp_info = registered_tcp_info(&server).await;
let socket_addr = tcp_info.address.parse::<std::net::SocketAddr>().unwrap();
let ip = socket_addr.ip();
assert!(
ip.is_loopback(),
"Should use loopback when IP detection fails"
);
assert_eq!(
ip,
std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)),
"Fallback should use exactly 127.0.0.1, got: {}",
ip
);
println!("SUCCESS: Fallback to 127.0.0.1 was confirmed: {}", ip);
assert!(socket_addr.port() > 0, "Server should have a valid port");
}
#[tokio::test]
async fn configured_ip_literals_bind_and_format_addresses() {
let ipv6_available = std::net::TcpListener::bind("[::1]:0").is_ok();
for (host, expected_ip) in [
("127.0.0.1", "127.0.0.1".parse::<IpAddr>().unwrap()),
("::1", "::1".parse::<IpAddr>().unwrap()),
(" 127.0.0.1 ", "127.0.0.1".parse::<IpAddr>().unwrap()),
(" [::1]\t", "::1".parse::<IpAddr>().unwrap()),
("::ffff:127.0.0.1", "127.0.0.1".parse::<IpAddr>().unwrap()),
(" lo ", "127.0.0.1".parse::<IpAddr>().unwrap()),
(" lo:1 ", "127.0.0.1".parse::<IpAddr>().unwrap()),
] {
if expected_ip.is_ipv6() && !ipv6_available {
eprintln!("skipping IPv6 bind because this host does not support IPv6 loopback");
continue;
}
let mut resolver = StubResolver::not_found();
resolver
.interfaces
.push(("lo:1", "127.0.0.1".parse().unwrap()));
let server = TcpStreamServer::new_with_resolver(
ServerOptions {
port: 0,
interface: Some(host.to_string()),
},
resolver,
)
.await
.unwrap();
let context = Context::new(());
let pending = server
.register(
StreamOptions::builder()
.context(context.context())
.enable_request_stream(false)
.enable_response_stream(true)
.build()
.unwrap(),
)
.await;
let connection_info = pending.recv_stream.unwrap().connection_info;
let tcp_info: TcpStreamConnectionInfo = connection_info.try_into().unwrap();
let address = tcp_info.address.parse::<SocketAddr>().unwrap();
assert_eq!(address, server.local_address().unwrap());
assert_eq!(address.ip(), expected_ip);
assert_ne!(address.port(), 0);
if expected_ip.is_ipv6() {
assert!(tcp_info.address.starts_with('['));
}
}
}
async fn test_server() -> Arc<TcpStreamServer> {
TcpStreamServer::new_with_resolver(
ServerOptions::builder().port(0).build().unwrap(),
StubResolver::not_found(),
)
.await
.unwrap()
}
async fn register_and_get_subject(
server: &TcpStreamServer,
) -> (
String,
tokio::sync::oneshot::Receiver<Result<super::StreamReceiver, StreamPrologueError>>,
) {
let context = Context::new(());
let options = StreamOptions::builder()
.context(context.context())
.enable_request_stream(false)
.enable_response_stream(true)
.build()
.unwrap();
let pending = server.register(options).await;
let recv_stream = pending.recv_stream.unwrap();
let (conn_info, provider) = recv_stream.into_parts();
let tcp_info: TcpStreamConnectionInfo = conn_info.try_into().unwrap();
(tcp_info.subject, provider)
}
fn make_eid(
namespace: &str,
component: &str,
endpoint: &str,
instance_id: u64,
) -> EndpointInstanceId {
EndpointInstanceId {
namespace: namespace.to_string(),
component: component.to_string(),
endpoint: endpoint.to_string(),
instance_id,
}
}
async fn register_and_get_bidi_subjects(
server: &TcpStreamServer,
) -> (
String,
tokio::sync::oneshot::Receiver<Result<super::StreamSender, StreamPrologueError>>,
String,
tokio::sync::oneshot::Receiver<Result<super::StreamReceiver, StreamPrologueError>>,
) {
let context = Context::new(());
let options = StreamOptions::builder()
.context(context.context())
.enable_request_stream(true)
.enable_response_stream(true)
.build()
.unwrap();
let pending = server.register(options).await;
let send_stream = pending.send_stream.unwrap();
let recv_stream = pending.recv_stream.unwrap();
let (send_info, send_provider) = send_stream.into_parts();
let (recv_info, recv_provider) = recv_stream.into_parts();
let send_tcp_info: TcpStreamConnectionInfo = send_info.try_into().unwrap();
let recv_tcp_info: TcpStreamConnectionInfo = recv_info.try_into().unwrap();
(
send_tcp_info.subject,
send_provider,
recv_tcp_info.subject,
recv_provider,
)
}
#[tokio::test]
async fn test_cancel_instance_streams_drops_both_bidi_halves() {
let server = test_server().await;
let (send_subj, send_provider, recv_subj, recv_provider) =
register_and_get_bidi_subjects(&server).await;
let id = make_eid("ns", "comp", "generate", 7);
assert!(
server
.associate_instance(&recv_subj, Some(&send_subj), &id)
.await,
"fresh instance must not be tombstoned"
);
let cancelled = server.cancel_instance_streams(&id).await;
assert_eq!(cancelled, 2, "both request + response halves must count");
assert!(
recv_provider.await.is_err(),
"recv provider should resolve with RecvError"
);
assert!(
send_provider.await.is_err(),
"send provider should resolve with RecvError after instance cancellation"
);
}
#[tokio::test]
async fn test_associate_instance_tombstone_cancels_both_bidi_halves() {
let server = test_server().await;
let id = make_eid("ns", "comp", "generate", 8);
server.cancel_instance_streams(&id).await;
let (send_subj, send_provider, recv_subj, recv_provider) =
register_and_get_bidi_subjects(&server).await;
assert!(
!server
.associate_instance(&recv_subj, Some(&send_subj), &id)
.await,
"tombstoned instance must reject association"
);
assert!(recv_provider.await.is_err());
assert!(send_provider.await.is_err());
}
#[tokio::test]
async fn test_cancel_instance_streams_unblocks_receiver() {
let server = test_server().await;
let (subject, provider) = register_and_get_subject(&server).await;
let id = make_eid("ns", "comp", "generate", 42);
assert!(server.associate_instance(&subject, None, &id).await);
let cancelled = server.cancel_instance_streams(&id).await;
assert_eq!(cancelled, 1);
let result = provider.await;
assert!(result.is_err(), "Expected RecvError after cancellation");
}
#[tokio::test]
async fn test_cancel_instance_streams_multiple_subjects() {
let server = test_server().await;
let (subj1, prov1) = register_and_get_subject(&server).await;
let (subj2, prov2) = register_and_get_subject(&server).await;
let (subj3, prov3) = register_and_get_subject(&server).await;
let id10 = make_eid("ns", "comp", "generate", 10);
let id20 = make_eid("ns", "comp", "generate", 20);
assert!(server.associate_instance(&subj1, None, &id10).await);
assert!(server.associate_instance(&subj2, None, &id10).await);
assert!(server.associate_instance(&subj3, None, &id20).await);
let cancelled = server.cancel_instance_streams(&id10).await;
assert_eq!(cancelled, 2);
assert!(prov1.await.is_err());
assert!(prov2.await.is_err());
let cancelled = server.cancel_instance_streams(&id20).await;
assert_eq!(cancelled, 1);
assert!(prov3.await.is_err());
}
#[tokio::test]
async fn test_cancel_instance_streams_nonexistent_instance() {
let server = test_server().await;
let id = make_eid("ns", "comp", "generate", 999);
let cancelled = server.cancel_instance_streams(&id).await;
assert_eq!(cancelled, 0);
}
#[tokio::test]
async fn test_cancel_recv_stream_cleans_up_instance_tracking() {
let server = test_server().await;
let (subject, _provider) = register_and_get_subject(&server).await;
let id = make_eid("ns", "comp", "generate", 42);
assert!(server.associate_instance(&subject, None, &id).await);
server.cancel_recv_stream(&subject).await;
let cancelled = server.cancel_instance_streams(&id).await;
assert_eq!(
cancelled, 0,
"Instance tracking should have been cleaned up"
);
}
#[tokio::test]
async fn test_registered_stream_drop_runs_cleanup() {
let server = test_server().await;
let context = Context::new(());
let options = StreamOptions::builder()
.context(context.context())
.enable_request_stream(false)
.enable_response_stream(true)
.build()
.unwrap();
let pending = server.register(options).await;
let recv_stream = pending.recv_stream.unwrap();
let tcp_info: TcpStreamConnectionInfo =
recv_stream.connection_info.clone().try_into().unwrap();
let subject = tcp_info.subject.clone();
{
let state = server.state.lock();
assert!(state.rx_subjects.contains_key(&subject));
}
drop(recv_stream);
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
{
let state = server.state.lock();
assert!(
!state.rx_subjects.contains_key(&subject),
"RAII cleanup should have removed the rx_subjects entry"
);
}
}
#[tokio::test]
async fn test_registered_stream_into_parts_disarms_cleanup() {
let server = test_server().await;
let context = Context::new(());
let options = StreamOptions::builder()
.context(context.context())
.enable_request_stream(false)
.enable_response_stream(true)
.build()
.unwrap();
let pending = server.register(options).await;
let recv_stream = pending.recv_stream.unwrap();
let tcp_info: TcpStreamConnectionInfo =
recv_stream.connection_info.clone().try_into().unwrap();
let subject = tcp_info.subject.clone();
let (_conn_info, _provider) = recv_stream.into_parts();
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
{
let state = server.state.lock();
assert!(
state.rx_subjects.contains_key(&subject),
"into_parts() should disarm the RAII cleanup"
);
}
}
#[tokio::test]
async fn test_dropped_registered_stream_wait_cleans_instance_indexes() {
let server = test_server().await;
let context = Context::new(());
let options = StreamOptions::builder()
.context(context.context())
.enable_request_stream(false)
.enable_response_stream(true)
.build()
.unwrap();
let pending = server.register(options).await;
let recv_stream = pending.recv_stream.unwrap();
let tcp_info: TcpStreamConnectionInfo =
recv_stream.connection_info.clone().try_into().unwrap();
let subject = tcp_info.subject;
let instance = make_eid("ns", "comp", "generate", 42);
assert!(server.associate_instance(&subject, None, &instance).await);
let mut wait = Box::pin(recv_stream.wait());
assert!(futures::poll!(wait.as_mut()).is_pending());
drop(wait);
tokio::time::timeout(std::time::Duration::from_secs(1), async {
loop {
let cleaned = {
let state = server.state.lock();
!state.rx_subjects.contains_key(&subject)
&& !state.rx_cancellations.contains_key(&subject)
&& !state.subject_instance.contains_key(&subject)
&& !state.instance_subjects.contains_key(&instance)
};
if cleaned {
break;
}
tokio::task::yield_now().await;
}
})
.await
.expect("cancelled wait did not clean response registration indexes");
}
#[tokio::test]
async fn test_associate_after_cancel_is_immediately_cancelled() {
let server = test_server().await;
let id = make_eid("ns", "comp", "generate", 42);
let cancelled = server.cancel_instance_streams(&id).await;
assert_eq!(cancelled, 0);
let (subject, provider) = register_and_get_subject(&server).await;
let associated = server.associate_instance(&subject, None, &id).await;
assert!(
!associated,
"associate_instance on a tombstoned instance should return false"
);
let result = provider.await;
assert!(
result.is_err(),
"Late associate_instance on a tombstoned instance should immediately cancel"
);
}
#[tokio::test]
async fn test_clear_tombstone_allows_new_associations() {
let server = test_server().await;
let id = make_eid("ns", "comp", "generate", 42);
server.cancel_instance_streams(&id).await;
server.clear_instance_tombstone(&id).await;
let (subject, _provider) = register_and_get_subject(&server).await;
assert!(server.associate_instance(&subject, None, &id).await);
let cancelled = server.cancel_instance_streams(&id).await;
assert_eq!(
cancelled, 1,
"After clearing tombstone, subjects should be tracked normally"
);
}
#[tokio::test]
async fn test_cancel_does_not_affect_sibling_endpoint() {
let server = test_server().await;
let (gen_subj, gen_prov) = register_and_get_subject(&server).await;
let (pre_subj, pre_prov) = register_and_get_subject(&server).await;
let gen_id = make_eid("ns", "comp", "generate", 42);
let pre_id = make_eid("ns", "comp", "prefill", 42);
assert!(server.associate_instance(&gen_subj, None, &gen_id).await);
assert!(server.associate_instance(&pre_subj, None, &pre_id).await);
let cancelled = server.cancel_instance_streams(&gen_id).await;
assert_eq!(
cancelled, 1,
"Only the generate subject should be cancelled"
);
assert!(gen_prov.await.is_err());
let still_pending = server.cancel_instance_streams(&pre_id).await;
assert_eq!(still_pending, 1, "prefill subject should still be tracked");
assert!(pre_prov.await.is_err());
}
#[tokio::test]
async fn test_tombstone_is_endpoint_scoped() {
let server = test_server().await;
let gen_id = make_eid("ns", "comp", "generate", 42);
let pre_id = make_eid("ns", "comp", "prefill", 42);
server.cancel_instance_streams(&gen_id).await;
let (gen_subj, gen_prov) = register_and_get_subject(&server).await;
assert!(
!server.associate_instance(&gen_subj, None, &gen_id).await,
"generate should be tombstoned"
);
assert!(gen_prov.await.is_err());
let (pre_subj, _pre_prov) = register_and_get_subject(&server).await;
assert!(
server.associate_instance(&pre_subj, None, &pre_id).await,
"prefill tombstone is independent; subject should be tracked"
);
let count = server.cancel_instance_streams(&pre_id).await;
assert_eq!(count, 1, "prefill subject should be tracked normally");
}
#[tokio::test]
async fn test_cancel_does_not_affect_different_component() {
let server = test_server().await;
let (subj_a, prov_a) = register_and_get_subject(&server).await;
let (subj_b, prov_b) = register_and_get_subject(&server).await;
let id_a = make_eid("ns-a", "comp-a", "generate", 42);
let id_b = make_eid("ns-b", "comp-b", "generate", 42);
assert!(server.associate_instance(&subj_a, None, &id_a).await);
assert!(server.associate_instance(&subj_b, None, &id_b).await);
let cancelled = server.cancel_instance_streams(&id_a).await;
assert_eq!(cancelled, 1, "Only service-A subject should be cancelled");
assert!(prov_a.await.is_err());
let still_tracked = server.cancel_instance_streams(&id_b).await;
assert_eq!(still_tracked, 1, "Service-B subject should be unaffected");
assert!(prov_b.await.is_err());
}
#[tokio::test(start_paused = true)]
async fn test_tombstone_expires_after_ttl() {
let server = test_server().await;
let id = make_eid("ns", "comp", "generate", 42);
server.cancel_instance_streams(&id).await;
{
let state = server.state.lock();
assert!(state.removed_instances.contains_key(&id));
}
tokio::time::advance(TOMBSTONE_TTL + Duration::from_secs(1)).await;
let (subject, _provider) = register_and_get_subject(&server).await;
assert!(
server.associate_instance(&subject, None, &id).await,
"tombstone older than TTL should not block association"
);
{
let state = server.state.lock();
assert!(
!state.removed_instances.contains_key(&id),
"expired tombstone should be pruned, not retained"
);
}
}
#[tokio::test(start_paused = true)]
async fn test_tombstone_within_ttl_blocks_associate() {
let server = test_server().await;
let id = make_eid("ns", "comp", "generate", 42);
server.cancel_instance_streams(&id).await;
tokio::time::advance(Duration::from_secs(1)).await;
let (subject, provider) = register_and_get_subject(&server).await;
assert!(
!server.associate_instance(&subject, None, &id).await,
"tombstone within TTL must still block association"
);
assert!(provider.await.is_err());
}
#[tokio::test(start_paused = true)]
async fn test_tombstone_lazy_prune_on_cancel() {
let server = test_server().await;
let id_old = make_eid("ns", "comp", "generate", 1);
let id_new = make_eid("ns", "comp", "generate", 2);
server.cancel_instance_streams(&id_old).await;
tokio::time::advance(TOMBSTONE_TTL + Duration::from_secs(1)).await;
server.cancel_instance_streams(&id_new).await;
let state = server.state.lock();
assert!(
!state.removed_instances.contains_key(&id_old),
"old tombstone should be pruned by the next cancel_instance_streams call"
);
assert!(
state.removed_instances.contains_key(&id_new),
"fresh tombstone should be retained"
);
assert_eq!(state.removed_instances.len(), 1);
}
#[tokio::test]
async fn test_clear_tombstone_only_affects_named_identity() {
let server = test_server().await;
let id_a = make_eid("ns", "comp", "generate", 1);
let id_b = make_eid("ns", "comp", "generate", 2);
server.cancel_instance_streams(&id_a).await;
server.clear_instance_tombstone(&id_b).await;
let state = server.state.lock();
assert!(
state.removed_instances.contains_key(&id_a),
"clearing a different identity must not remove id_a's tombstone"
);
}
#[tokio::test]
async fn test_tombstone_scoped_to_full_identity() {
let server = test_server().await;
let id_a = make_eid("ns-a", "comp-a", "generate", 42);
let id_b = make_eid("ns-b", "comp-b", "generate", 42);
server.cancel_instance_streams(&id_a).await;
let (subj_a, prov_a) = register_and_get_subject(&server).await;
assert!(!server.associate_instance(&subj_a, None, &id_a).await);
assert!(prov_a.await.is_err());
let (subj_b, _prov_b) = register_and_get_subject(&server).await;
assert!(
server.associate_instance(&subj_b, None, &id_b).await,
"Different namespace/component must not be tombstoned"
);
assert_eq!(server.cancel_instance_streams(&id_b).await, 1);
}
type TestFramedRead = FramedRead<ReadHalf<TcpStream>, TwoPartCodec>;
type TestFramedWrite = FramedWrite<WriteHalf<TcpStream>, TwoPartCodec>;
type TestResponseStream = (TestFramedRead, TestFramedWrite, StreamReceiver);
async fn open_registered_response_stream() -> TestResponseStream {
let options = ServerOptions::builder().port(0).build().unwrap();
let server = TcpStreamServer::new_with_resolver(options, StubResolver::not_found())
.await
.unwrap();
let context = Context::new(());
let stream_options = StreamOptions::builder()
.context(context.context())
.enable_request_stream(false)
.enable_response_stream(true)
.build()
.unwrap();
let pending_connection = server.register(stream_options).await;
let registered_stream = pending_connection.recv_stream.unwrap();
let (connection_info, stream_provider) = registered_stream.into_parts();
let tcp_info: TcpStreamConnectionInfo = connection_info.try_into().unwrap();
let stream = TcpStream::connect(&tcp_info.address).await.unwrap();
let (read_half, write_half) = tokio::io::split(stream);
let framed_reader = FramedRead::new(read_half, TwoPartCodec::default());
let mut framed_writer = FramedWrite::new(write_half, TwoPartCodec::default());
let handshake = CallHomeHandshake {
subject: tcp_info.subject,
stream_type: StreamType::Response,
};
framed_writer
.send(TwoPartMessage::from_header(
serde_json::to_vec(&handshake).unwrap().into(),
))
.await
.unwrap();
framed_writer
.send(TwoPartMessage::from_header(
serde_json::to_vec(&ResponseStreamPrologue {
error: None,
typed_error: None,
})
.unwrap()
.into(),
))
.await
.unwrap();
let receiver = tokio::time::timeout(std::time::Duration::from_secs(1), stream_provider)
.await
.expect("server should establish response stream within timeout")
.expect("stream provider should not be dropped")
.expect("response stream should be accepted");
(framed_reader, framed_writer, receiver)
}
async fn recv_control_message(framed_reader: &mut TestFramedRead) -> ControlMessage {
let message = tokio::time::timeout(std::time::Duration::from_secs(1), framed_reader.next())
.await
.expect("server should send a control message within timeout")
.expect("server should not close before sending control")
.expect("control message should decode");
let (header, data) = message.optional_parts();
assert!(data.is_none(), "control message should not contain data");
serde_json::from_slice(header.expect("control header missing").as_ref()).unwrap()
}
#[tokio::test]
async fn test_tcp_stream_server_sends_kill_on_unexpected_control_message() {
let (mut framed_reader, mut framed_writer, _receiver) =
open_registered_response_stream().await;
framed_writer
.send(TwoPartMessage::from_header(
serde_json::to_vec(&ControlMessage::Stop).unwrap().into(),
))
.await
.unwrap();
assert_eq!(
recv_control_message(&mut framed_reader).await,
ControlMessage::Kill,
"unexpected control message should kill only this stream"
);
}
#[tokio::test]
async fn test_tcp_stream_server_sends_kill_when_prologue_wait_is_cancelled() {
let server = test_server().await;
let context = Context::new(());
let options = StreamOptions::builder()
.context(context.context())
.enable_request_stream(false)
.enable_response_stream(true)
.build()
.unwrap();
let pending = server.register(options).await;
let registered_stream = pending.recv_stream.unwrap();
let (connection_info, stream_provider) = registered_stream.into_parts();
let tcp_info: TcpStreamConnectionInfo = connection_info.try_into().unwrap();
let subject = tcp_info.subject.clone();
let instance = make_eid("ns", "comp", "generate", 42);
assert!(server.associate_instance(&subject, None, &instance).await);
let stream = TcpStream::connect(&tcp_info.address).await.unwrap();
let (read_half, write_half) = tokio::io::split(stream);
let mut framed_reader = FramedRead::new(read_half, TwoPartCodec::default());
let mut framed_writer = FramedWrite::new(write_half, TwoPartCodec::default());
let handshake = CallHomeHandshake {
subject: subject.clone(),
stream_type: StreamType::Response,
};
framed_writer
.send(TwoPartMessage::from_header(
serde_json::to_vec(&handshake).unwrap().into(),
))
.await
.unwrap();
tokio::time::timeout(std::time::Duration::from_secs(1), async {
while server.state.lock().rx_subjects.contains_key(&subject) {
tokio::task::yield_now().await;
}
})
.await
.expect("server did not accept response call-home");
assert_eq!(server.cancel_instance_streams(&instance).await, 1);
assert!(
time::timeout(Duration::from_secs(1), stream_provider)
.await
.expect("worker removal must unblock the response handshake")
.is_err()
);
assert_eq!(
recv_control_message(&mut framed_reader).await,
ControlMessage::Kill,
"cancelling the prologue wait should stop worker generation"
);
let state = server.state.lock();
assert!(!state.rx_subjects.contains_key(&subject));
assert!(!state.rx_cancellations.contains_key(&subject));
assert!(!state.subject_instance.contains_key(&subject));
assert!(!state.instance_subjects.contains_key(&instance));
}
#[tokio::test]
async fn response_prologue_acceptance_is_ordered_with_worker_removal() {
let server = test_server().await;
for removal_first in [true, false] {
let instance = make_eid("ns", "comp", "generate", u64::from(removal_first));
let (subject, _provider) = register_and_get_subject(&server).await;
assert!(server.associate_instance(&subject, None, &instance).await);
let pending = TcpStreamServer::take_response_stream(&server.state, &subject).unwrap();
let guard = ResponseRegistrationGuard {
state: server.state.clone(),
subject: subject.clone(),
is_active: true,
};
if removal_first {
assert_eq!(server.cancel_instance_streams(&instance).await, 1);
assert!(
!guard.accept(),
"a cancelled handshake must not be accepted"
);
assert!(pending.cancellation.is_cancelled());
} else {
assert!(guard.accept());
assert_eq!(server.cancel_instance_streams(&instance).await, 0);
assert!(!pending.cancellation.is_cancelled());
}
let state = server.state.lock();
assert!(!state.rx_cancellations.contains_key(&subject));
assert!(!state.subject_instance.contains_key(&subject));
assert!(!state.instance_subjects.contains_key(&instance));
}
}
#[tokio::test(start_paused = true)]
async fn cancelled_handshake_unblocks_before_stalled_cleanup() {
let server = test_server().await;
let instance = make_eid("ns", "comp", "generate", 42);
let (subject, provider) = register_and_get_subject(&server).await;
assert!(server.associate_instance(&subject, None, &instance).await);
let pending = TcpStreamServer::take_response_stream(&server.state, &subject).unwrap();
let guard = ResponseRegistrationGuard {
state: server.state.clone(),
subject: subject.clone(),
is_active: true,
};
let (socket, _peer) = tokio::io::duplex(1);
let writer = FramedWrite::new(Box::new(socket) as BoxWrite, TwoPartCodec::default());
let cleanup = tokio::spawn(cancel_response_handshake(
pending.connection,
Some(guard),
writer,
));
assert!(
time::timeout(Duration::from_millis(50), provider)
.await
.expect("requester must be released before network cleanup")
.is_err()
);
assert!(
!cleanup.is_finished(),
"peer should still be blocking the Kill write"
);
{
let state = server.state.lock();
assert!(!state.rx_cancellations.contains_key(&subject));
assert!(!state.subject_instance.contains_key(&subject));
assert!(!state.instance_subjects.contains_key(&instance));
}
time::advance(HANDSHAKE_CANCEL_CLEANUP_TIMEOUT).await;
time::timeout(Duration::from_millis(50), cleanup)
.await
.expect("network cleanup must stop at its deadline")
.unwrap();
}
#[tokio::test]
async fn test_tcp_stream_server_sends_kill_on_read_error() {
let (mut framed_reader, framed_writer, _receiver) = open_registered_response_stream().await;
let mut raw_writer = framed_writer.into_inner();
raw_writer.write_all(&[0u8; 8]).await.unwrap();
raw_writer.shutdown().await.unwrap();
assert_eq!(
recv_control_message(&mut framed_reader).await,
ControlMessage::Kill,
"framing read error should kill only this stream"
);
}
#[tokio::test]
async fn test_tcp_stream_server_sends_kill_on_sentinel_with_data() {
let (mut framed_reader, mut framed_writer, _receiver) =
open_registered_response_stream().await;
let header = serde_json::to_vec(&ControlMessage::Sentinel)
.unwrap()
.into();
framed_writer
.send(TwoPartMessage::from_parts(
header,
Bytes::from_static(b"unexpected payload"),
))
.await
.unwrap();
assert_eq!(
recv_control_message(&mut framed_reader).await,
ControlMessage::Kill,
"Sentinel with data should kill only this stream"
);
}
#[tokio::test]
async fn test_tcp_stream_server_returns_error_on_invalid_prologue() {
let options = ServerOptions::builder().port(0).build().unwrap();
let server = TcpStreamServer::new_with_resolver(options, StubResolver::not_found())
.await
.unwrap();
let context = Context::new(());
let stream_options = StreamOptions::builder()
.context(context.context())
.enable_request_stream(false)
.enable_response_stream(true)
.build()
.unwrap();
let pending_connection = server.register(stream_options).await;
let registered_stream = pending_connection.recv_stream.unwrap();
let (connection_info, stream_provider) = registered_stream.into_parts();
let tcp_info: TcpStreamConnectionInfo = connection_info.try_into().unwrap();
let stream = TcpStream::connect(&tcp_info.address).await.unwrap();
let (_read_half, write_half) = tokio::io::split(stream);
let mut framed_writer = FramedWrite::new(write_half, TwoPartCodec::default());
let handshake = CallHomeHandshake {
subject: tcp_info.subject,
stream_type: StreamType::Response,
};
framed_writer
.send(TwoPartMessage::from_header(
serde_json::to_vec(&handshake).unwrap().into(),
))
.await
.unwrap();
framed_writer
.send(TwoPartMessage::from_data(Bytes::from_static(
b"not a prologue",
)))
.await
.unwrap();
let outcome = tokio::time::timeout(std::time::Duration::from_secs(1), stream_provider)
.await
.expect("stream provider should resolve quickly")
.expect("stream provider channel should not be dropped");
match outcome {
Err(err) => assert!(
err.contains("malformed prologue"),
"expected malformed-prologue error, got: {err}"
),
Ok(_) => panic!("invalid prologue should produce an error, but got Ok"),
}
}
#[tokio::test]
async fn test_unknown_typed_error_preserves_the_legacy_prologue_error() {
let options = ServerOptions::builder().port(0).build().unwrap();
let server = TcpStreamServer::new_with_resolver(options, StubResolver::not_found())
.await
.unwrap();
let context = Context::new(());
let stream_options = StreamOptions::builder()
.context(context.context())
.enable_request_stream(false)
.enable_response_stream(true)
.build()
.unwrap();
let pending_connection = server.register(stream_options).await;
let (connection_info, stream_provider) =
pending_connection.recv_stream.unwrap().into_parts();
let tcp_info: TcpStreamConnectionInfo = connection_info.try_into().unwrap();
let stream = TcpStream::connect(&tcp_info.address).await.unwrap();
let (_read_half, write_half) = tokio::io::split(stream);
let mut framed_writer = FramedWrite::new(write_half, TwoPartCodec::default());
let handshake = CallHomeHandshake {
subject: tcp_info.subject,
stream_type: StreamType::Response,
};
framed_writer
.send(TwoPartMessage::from_header(
serde_json::to_vec(&handshake).unwrap().into(),
))
.await
.unwrap();
framed_writer
.send(TwoPartMessage::from_header(Bytes::from_static(
br#"{"error":"Generate Error: boom","typed_error":{"error_type":"VariantFromTheFuture","message":"boom"}}"#,
)))
.await
.unwrap();
let outcome = tokio::time::timeout(std::time::Duration::from_secs(5), stream_provider)
.await
.expect("stream provider should resolve quickly")
.expect("the oneshot must be notified, not dropped");
match outcome {
Err(err) => {
assert_eq!(err.message, "Generate Error: boom");
assert!(err.typed_error.is_none());
}
Ok(_) => panic!("an error prologue must not yield a usable stream"),
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_concurrent_response_registration_and_call_home() {
const STREAMS: usize = 128;
let result = time::timeout(Duration::from_secs(20), async {
let server = test_server().await;
let mut pending_streams = Vec::with_capacity(STREAMS);
let mut client_tasks = Vec::with_capacity(STREAMS);
for idx in 0..STREAMS {
let context = Context::new(());
let options = StreamOptions::builder()
.context(context.context())
.enable_request_stream(false)
.enable_response_stream(true)
.build()
.unwrap();
let pending = server.register(options).await;
let registered_stream = pending.recv_stream.unwrap();
let (connection_info, stream_provider) = registered_stream.into_parts();
let client_context =
Context::with_id_and_metadata((), context.id().to_string(), Default::default());
let payload = Bytes::from(format!("payload-{idx}"));
pending_streams.push((idx, payload.clone(), stream_provider));
client_tasks.push(tokio::spawn(async move {
let mut sender = TcpClient::create_response_stream(
client_context.context(),
connection_info,
None,
)
.await
.unwrap();
sender.send_prologue(None).await.unwrap();
sender.send(payload).await.unwrap();
}));
}
for task in client_tasks {
task.await.unwrap();
}
for (idx, expected, stream_provider) in pending_streams {
let mut stream = stream_provider.await.unwrap().unwrap();
let actual = stream.rx.recv().await.unwrap();
assert_eq!(actual, expected, "payload mismatch for stream {idx}");
}
})
.await;
assert!(
result.is_ok(),
"concurrent response registration and call-home timed out"
);
}
#[tokio::test]
async fn test_response_stream_cancellation_before_prologue() {
temp_env::async_with_vars(
[
("DYN_TCP_TLS_CERT_PATH", None::<&str>),
("DYN_TCP_TLS_KEY_PATH", None),
("DYN_TCP_TLS_CLIENT_CA_CERT_PATH", None),
("DYN_TCP_TLS_CA_CERT_PATH", None),
("DYN_TCP_TLS_INSECURE", None),
("DYN_TCP_TLS_CLIENT_CERT_PATH", None),
("DYN_TCP_TLS_CLIENT_KEY_PATH", None),
],
async {
let server = test_server().await;
for action in [Some(ControlMessage::Stop), Some(ControlMessage::Kill), None] {
tokio::time::timeout(Duration::from_secs(5), async {
let context = Context::new(()).context();
let options = StreamOptions::builder()
.context(context.clone())
.enable_request_stream(false)
.enable_response_stream(true)
.build()
.unwrap();
let pending = server.register(options).await;
let (connection_info, stream_provider) =
pending.recv_stream.unwrap().into_parts();
let worker_context = Context::with_id_and_metadata(
(),
context.id().to_string(),
Default::default(),
)
.context();
let sender = TcpClient::create_response_stream(
worker_context.clone(),
connection_info,
None,
)
.await
.unwrap();
let stream_provider = match action {
Some(ControlMessage::Stop) => {
context.stop_generating();
Some(stream_provider)
}
Some(ControlMessage::Kill) => {
context.kill();
Some(stream_provider)
}
None => {
drop(stream_provider);
assert!(!context.is_stopped());
None
}
Some(ControlMessage::Sentinel) => unreachable!(),
};
if let Some(stream_provider) = stream_provider {
let failure = match stream_provider.await.unwrap() {
Err(failure) => failure,
Ok(_) => {
panic!("cancellation must not establish a response stream")
}
};
assert_eq!(
failure.typed_error.as_ref().map(|error| error.error_type()),
Some(ErrorType::Cancelled)
);
}
worker_context.stopped().await;
sender.tx.closed().await;
assert_eq!(
worker_context.is_killed(),
action != Some(ControlMessage::Stop)
);
})
.await
.unwrap_or_else(|_| {
panic!("cancellation before prologue timed out: {action:?}")
});
}
},
)
.await;
}
#[tokio::test]
async fn test_typed_prologue_error_survives_to_requester() {
let server = test_server().await;
let context = Context::new(());
let options = StreamOptions::builder()
.context(context.context())
.enable_request_stream(false)
.enable_response_stream(true)
.build()
.unwrap();
let pending = server.register(options).await;
let (connection_info, stream_provider) = pending.recv_stream.unwrap().into_parts();
let client_context =
Context::with_id_and_metadata((), context.id().to_string(), Default::default());
let worker_error = DynamoError::builder()
.error_type(ErrorType::Backend(BackendError::InvalidArgument))
.message("multimodal input is not supported by this backend")
.build();
let mut sender =
TcpClient::create_response_stream(client_context.context(), connection_info, None)
.await
.unwrap();
sender
.send_prologue_typed(Some(StreamPrologueError::new(
"Generate Error: multimodal input is not supported by this backend",
worker_error,
)))
.await
.unwrap();
let outcome = tokio::time::timeout(std::time::Duration::from_secs(5), stream_provider)
.await
.expect("stream provider should resolve quickly")
.expect("stream provider channel should not be dropped");
let prologue_error = match outcome {
Err(err) => err,
Ok(_) => panic!("an error prologue must not yield a usable stream"),
};
assert_eq!(
prologue_error.typed_error.as_ref().map(|e| e.error_type()),
Some(ErrorType::Backend(BackendError::InvalidArgument)),
"the worker's error type must survive the prologue round trip"
);
}
use futures::SinkExt;
async fn register_and_dial_request_stream(
server: &TcpStreamServer,
) -> (
FramedRead<tokio::io::ReadHalf<TcpStream>, TwoPartCodec>,
super::StreamSender,
Arc<dyn AsyncEngineContext>,
) {
let upstream_ctx = Context::new(()).context();
let options = StreamOptions::builder()
.context(upstream_ctx.clone())
.enable_request_stream(true)
.enable_response_stream(false)
.build()
.unwrap();
let pending = server.register(options).await;
let send_stream = pending.send_stream.unwrap();
let (conn_info, send_provider) = send_stream.into_parts();
let tcp_info: TcpStreamConnectionInfo = conn_info.try_into().unwrap();
let raw = TcpStream::connect(&tcp_info.address).await.unwrap();
let (read_half, write_half) = tokio::io::split(raw);
let framed_reader = FramedRead::new(read_half, TwoPartCodec::default());
let mut framed_writer = FramedWrite::new(write_half, TwoPartCodec::default());
let handshake = super::CallHomeHandshake {
subject: tcp_info.subject.clone(),
stream_type: StreamType::Request,
};
let handshake_bytes = serde_json::to_vec(&handshake).unwrap();
framed_writer
.send(TwoPartMessage::from_header(handshake_bytes.into()))
.await
.unwrap();
drop(framed_writer);
let sender = send_provider.await.unwrap().unwrap();
(framed_reader, sender, upstream_ctx)
}
async fn next_control_message(
reader: &mut FramedRead<tokio::io::ReadHalf<TcpStream>, TwoPartCodec>,
) -> ControlMessage {
loop {
let frame = reader
.next()
.await
.expect("socket closed before control message arrived")
.expect("decode error");
if let Some(header) = frame.header() {
return serde_json::from_slice::<ControlMessage>(header)
.expect("invalid control message bytes");
}
}
}
#[tokio::test]
async fn test_request_stream_sends_sentinel_on_clean_drop() {
let server = test_server().await;
let (mut reader, sender, _ctx) = register_and_dial_request_stream(&server).await;
drop(sender);
let ctrl = next_control_message(&mut reader).await;
assert!(
matches!(ctrl, ControlMessage::Sentinel),
"clean drain should emit Sentinel, got {ctrl:?}"
);
}
#[tokio::test]
async fn test_request_stream_sends_kill_on_context_killed() {
let server = test_server().await;
let (mut reader, _sender, ctx) = register_and_dial_request_stream(&server).await;
ctx.kill();
let ctrl = next_control_message(&mut reader).await;
assert!(
matches!(ctrl, ControlMessage::Kill),
"context.kill() should emit Kill, got {ctrl:?}"
);
}
#[tokio::test]
async fn test_request_stream_sends_stop_on_context_stopped() {
let server = test_server().await;
let (mut reader, _sender, ctx) = register_and_dial_request_stream(&server).await;
ctx.stop();
let ctrl = next_control_message(&mut reader).await;
assert!(
matches!(ctrl, ControlMessage::Stop),
"context.stop() should emit Stop, got {ctrl:?}"
);
}
#[cfg(unix)]
fn emfile_error() -> std::io::Error {
std::io::Error::from_raw_os_error(libc::EMFILE)
}
#[cfg(unix)]
#[test]
fn accept_backoff_classifies_all_exhaustion_errnos() {
for errno in [libc::EMFILE, libc::ENFILE, libc::ENOBUFS, libc::ENOMEM] {
let err = std::io::Error::from_raw_os_error(errno);
assert_eq!(
AcceptBackoff::classify(&err),
AcceptFailure::Exhaustion,
"errno {errno} must classify as exhaustion"
);
}
let ordinary = std::io::Error::from(std::io::ErrorKind::ConnectionAborted);
assert_eq!(AcceptBackoff::classify(&ordinary), AcceptFailure::Ordinary,);
}
#[cfg(unix)]
#[test]
fn accept_backoff_grows_resets_and_rate_limits() {
let mut backoff = AcceptBackoff::default();
let now = std::time::Instant::now();
let delays: Vec<Duration> = (0..12)
.map(|_| backoff.record_exhaustion(now).delay)
.collect();
assert!(delays[0] > Duration::ZERO);
let sat = delays
.iter()
.position(|d| *d == ACCEPT_BACKOFF_MAX_DELAY)
.expect("delay reaches the ceiling");
for pair in delays[..=sat].windows(2) {
assert!(
pair[1] > pair[0],
"delay must grow: {:?} -> {:?}",
pair[0],
pair[1]
);
}
assert!(delays[sat..].iter().all(|d| *d == ACCEPT_BACKOFF_MAX_DELAY));
backoff.record_success(|| now);
assert_eq!(
backoff.record_exhaustion(now).delay,
ACCEPT_BACKOFF_INITIAL_DELAY,
);
let t0 = std::time::Instant::now();
let mut backoff = AcceptBackoff::default();
let emitted: Vec<Option<u64>> = (0..50)
.map(|_| backoff.record_exhaustion(t0).log_suppressed)
.collect();
assert_eq!(emitted.iter().filter(|e| e.is_some()).count(), 1);
assert_eq!(emitted[0], Some(0));
let t1 = t0 + ACCEPT_BACKOFF_LOG_INTERVAL + Duration::from_millis(1);
assert_eq!(
backoff.record_exhaustion(t1).log_suppressed,
Some(49),
"next emission reports every failure suppressed since the last one",
);
let mut backoff = AcceptBackoff::default();
let t0 = std::time::Instant::now();
assert_eq!(backoff.record_exhaustion(t0).log_suppressed, Some(0));
backoff.record_exhaustion(t0);
backoff.record_exhaustion(t0);
assert_eq!(
backoff.record_success(|| t0),
None,
"recovery inside the log window must not emit",
);
backoff.record_exhaustion(t0);
assert_eq!(
backoff.record_success(|| t0 + ACCEPT_BACKOFF_LOG_INTERVAL),
Some(3),
"after the window elapses the recovery emits with the rolled-forward count",
);
}
#[cfg(unix)]
#[tokio::test]
async fn accept_backoff_socket_recovers_after_injected_exhaustion() {
use crate::metrics::transport_metrics::TCP_ACCEPT_BACKOFF_TOTAL;
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind ephemeral listener");
let addr = listener.local_addr().expect("local_addr");
let mut backoff = AcceptBackoff::default();
let counter_before = TCP_ACCEPT_BACKOFF_TOTAL.get();
let started = std::time::Instant::now();
let mut expected_total = Duration::ZERO;
for _ in 0..3 {
expected_total += handle_accept_error(&emfile_error(), &mut backoff).await;
}
assert!(expected_total >= ACCEPT_BACKOFF_INITIAL_DELAY * 3);
assert!(
started.elapsed() >= expected_total,
"the accept loop must sleep, not spin; elapsed {:?}",
started.elapsed(),
);
assert!(TCP_ACCEPT_BACKOFF_TOTAL.get() >= counter_before + 3.0);
let client = tokio::spawn(async move { tokio::net::TcpStream::connect(addr).await });
let (accepted, _peer) = tokio::time::timeout(Duration::from_secs(5), listener.accept())
.await
.expect("listener should still accept after backing off")
.expect("accept should succeed");
let _client = client.await.expect("client task").expect("client connect");
drop(accepted);
backoff.record_success(std::time::Instant::now);
assert_eq!(
backoff.record_exhaustion(std::time::Instant::now()).delay,
ACCEPT_BACKOFF_INITIAL_DELAY,
);
}
}