use crate::protocol::{RequestContext, RequestKind, RequestResponseMetadata, StatusCode};
use crate::router::{BackendSelector, CommandGuard};
use crate::session::SessionError;
use crate::session::response_transfer::{ResponseConnectionReuse, ResponseTransferError};
use crate::session::retry::retry_once;
use crate::session::routing::{
CacheAction, MetricsAction, determine_cache_action_for_request,
determine_metrics_action_for_request,
};
use crate::session::{ClientSession, backend};
use crate::types::{BackendId, BackendToClientBytes, ClientToBackendBytes};
use anyhow::Result;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use tokio::io::{AsyncWrite, AsyncWriteExt};
use tracing::{debug, warn};
const BACKEND_TIMING_SAMPLE_MASK: u64 = 0x0f;
static BACKEND_TIMING_SAMPLE_COUNTER: AtomicU64 = AtomicU64::new(0);
#[inline]
pub fn should_sample_backend_timing() -> bool {
BACKEND_TIMING_SAMPLE_COUNTER.fetch_add(1, Ordering::Relaxed) & BACKEND_TIMING_SAMPLE_MASK == 0
}
pub(super) enum BackendAttemptResult {
Success,
ArticleNotFound { backend_id: BackendId },
BackendUnavailable,
NoRetryableBackend,
}
impl BackendAttemptResult {
#[must_use]
const fn success(
request: &mut RequestContext,
backend_id: BackendId,
response: RequestResponseMetadata,
) -> Self {
request.record_backend_response(backend_id, response);
Self::Success
}
}
pub(super) struct ArticleAttemptState<'a> {
pub availability: &'a mut crate::cache::ArticleAvailability,
pub client_to_backend_bytes: &'a mut ClientToBackendBytes,
pub backend_connection: &'a mut Option<(BackendId, crate::pool::ConnectionGuard)>,
pub unavailable_backends: &'a mut u8,
}
#[derive(Clone, Copy)]
pub(super) enum RetryAttemptKind {
Direct,
OrderedPipeline,
}
impl RetryAttemptKind {
const fn as_str(self) -> &'static str {
match self {
Self::Direct => "direct retry",
Self::OrderedPipeline => "ordered pipeline retry",
}
}
}
#[derive(Clone, Copy)]
pub(super) struct AuthoritativeArticleMissing {
backend_id: BackendId,
}
impl AuthoritativeArticleMissing {
pub(super) fn from_status_code(backend_id: BackendId, status_code: StatusCode) -> Option<Self> {
(status_code.as_u16() == 430).then_some(Self { backend_id })
}
pub(super) const fn backend_id(self) -> BackendId {
self.backend_id
}
}
type BackendTimings = (u64, u64, u64);
#[derive(Clone, Copy)]
pub(super) struct ResponseWriteParams<'a> {
pub(super) request: &'a RequestContext,
pub(super) msg_id: Option<&'a crate::types::MessageId<'a>>,
pub(super) status_code: StatusCode,
}
struct InvalidBackendResponseContext<'a> {
provider: &'a crate::pool::DeadpoolConnectionProvider,
request: &'a RequestContext,
buffer: &'a crate::pool::PooledBuffer,
conn: crate::pool::ConnectionGuard,
}
pub(super) type PreparedBackendAttempt = Option<(
crate::pool::ConnectionGuard,
StatusCode,
crate::pool::PooledBuffer,
)>;
type ExecutedBackendAttempt = (
crate::pool::ConnectionGuard,
crate::session::backend::BackendReadResult,
crate::pool::PooledBuffer,
Option<BackendTimings>,
);
type BackendReadAttempt = (
crate::session::backend::BackendReadResult,
crate::pool::PooledBuffer,
Option<BackendTimings>,
);
enum BackendReadAttemptError {
Backend(anyhow::Error),
}
const fn classify_response_write_err(e: std::io::Error) -> ResponseTransferError {
ResponseTransferError::ClientDisconnect(e)
}
const fn can_discard_response_after_write(
request: &RequestContext,
cache_action: CacheAction,
) -> bool {
matches!(
request.kind(),
RequestKind::Article | RequestKind::Body | RequestKind::Head
) && matches!(
cache_action,
CacheAction::TrackAvailability | CacheAction::None
)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ResponseRetention {
DiscardAfterWrite,
RetainAfterWrite,
}
impl ResponseRetention {
const fn for_request(request: &RequestContext, cache_action: CacheAction) -> Self {
match can_discard_response_after_write(request, cache_action) {
true => Self::DiscardAfterWrite,
false => Self::RetainAfterWrite,
}
}
}
impl ClientSession {
pub(super) fn retry_backend_provider<'a>(
&self,
router: &'a BackendSelector,
backend_id: BackendId,
request: &RequestContext,
attempt: RetryAttemptKind,
) -> Option<&'a crate::pool::DeadpoolConnectionProvider> {
let provider = router.backend_provider(backend_id);
if provider.is_none() {
debug!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?request.verb(),
msg_id = ?request.message_id_value(),
attempt = attempt.as_str(),
"Selected backend had no provider; treating backend as unavailable"
);
}
provider
}
pub(super) async fn try_backend_for_article(
&self,
router: &Arc<BackendSelector>,
request: &mut RequestContext,
client_writer: &crate::session::SharedClientWriter,
state: &mut ArticleAttemptState<'_>,
) -> Result<BackendAttemptResult, SessionError> {
let backend_id = match router.route_with_availability_suppressing(
self.client_id,
Some(state.availability),
*state.unavailable_backends,
) {
Ok(backend_id) => backend_id,
Err(err) => {
debug!(
client = %self.client_addr,
error = %err,
"No eligible backend available for article retry"
);
return Ok(BackendAttemptResult::NoRetryableBackend);
}
};
debug!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?request.verb(),
msg_id = ?request.message_id_value(),
missing_bits = format_args!("{:08b}", state.availability.missing_bits()),
"Article retry selected backend for direct attempt"
);
let guard = CommandGuard::new(router.clone(), backend_id);
let Some(provider) =
self.retry_backend_provider(router, backend_id, request, RetryAttemptKind::Direct)
else {
return Ok(BackendAttemptResult::BackendUnavailable);
};
let Some((mut conn, status_code, buffer)) = self
.prepare_backend_attempt(provider, backend_id, request, state)
.await?
else {
return Ok(BackendAttemptResult::BackendUnavailable);
};
if let Some(missing) =
AuthoritativeArticleMissing::from_status_code(backend_id, status_code)
{
debug!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?request.verb(),
msg_id = ?request.message_id_value(),
"Direct backend attempt returned 430 before writing response"
);
self.record_authoritative_article_missing(missing, state.availability);
if let Some(msg_id) = request.message_id_value() {
self.cache.record_backend_missing(msg_id, backend_id).await;
}
self.capture_suppressed_430_response(&mut conn, backend_id, request, buffer)
.await?;
self.release_or_reuse_connection(
conn,
backend_id,
request,
Some(state.backend_connection),
);
return Ok(BackendAttemptResult::ArticleNotFound { backend_id });
}
let msg_id = request.message_id_value();
let response = match self
.write_successful_retry_response(
conn,
client_writer,
backend_id,
buffer,
ResponseWriteParams {
request,
msg_id: msg_id.as_ref(),
status_code,
},
state.backend_connection,
)
.await
{
Ok(response) => response,
Err(e @ SessionError::ClientDisconnect(_)) => {
self.sync_availability_if_needed(msg_id.as_ref(), state.availability)
.await;
return Err(e);
}
Err(e) => return Err(e),
};
Self::record_successful_availability(backend_id, status_code, state.availability);
guard.complete();
Ok(BackendAttemptResult::success(request, backend_id, response))
}
async fn write_successful_retry_response(
&self,
conn: crate::pool::ConnectionGuard,
client_writer: &crate::session::SharedClientWriter,
backend_id: BackendId,
buffer: crate::pool::PooledBuffer,
params: ResponseWriteParams<'_>,
backend_connection: &mut Option<(BackendId, crate::pool::ConnectionGuard)>,
) -> Result<RequestResponseMetadata, SessionError> {
let mut client_write = client_writer.lock().await;
self.write_successful_backend_response(
conn,
&mut *client_write,
backend_id,
buffer,
params,
Some(backend_connection),
)
.await
}
pub(super) async fn prepare_backend_attempt(
&self,
provider: &crate::pool::DeadpoolConnectionProvider,
backend_id: BackendId,
request: &RequestContext,
state: &mut ArticleAttemptState<'_>,
) -> Result<PreparedBackendAttempt, SessionError> {
let request_wire_len = request.request_wire_len().get();
debug!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?request.verb(),
msg_id = ?request.message_id_value(),
request_wire_len,
"Preparing direct backend attempt"
);
let (conn, read_status, buffer, timings) = retry_once!(
self.execute_backend_attempt(provider, backend_id, request, state.backend_connection)
.await,
client = self.client_addr,
backend = backend_id.as_index()
)
.map_err(SessionError::Backend)?;
if let Some((ttfb, send, recv)) = timings {
self.record_timing_metrics(backend_id, ttfb, send, recv);
}
*state.client_to_backend_bytes = state.client_to_backend_bytes.add(request_wire_len);
let status_code = match read_status.status_code() {
Some(status_code) => status_code,
None => {
read_status.log_warnings(&buffer, self.client_addr, backend_id);
debug!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?request.verb(),
msg_id = ?request.message_id_value(),
"Backend attempt read bytes but could not parse a status code"
);
self.handle_invalid_backend_response(
backend_id,
InvalidBackendResponseContext {
provider,
request,
buffer: &buffer,
conn,
},
);
*state.unavailable_backends |= backend_id.availability_bit();
return Ok(None);
}
};
debug!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?request.verb(),
msg_id = ?request.message_id_value(),
status_code = status_code.as_u16(),
backend_read_bytes = buffer.initialized(),
availability_missing_bits = format_args!("{:08b}", state.availability.missing_bits()),
"Backend attempt received classifiable response bytes"
);
Ok(Some((conn, status_code, buffer)))
}
pub(super) async fn capture_suppressed_430_response(
&self,
conn: &mut crate::pool::ConnectionGuard,
backend_id: BackendId,
request: &RequestContext,
mut buffer: crate::pool::PooledBuffer,
) -> Result<(), SessionError> {
crate::session::backend::observe_response(
request,
&mut buffer,
conn,
&self.buffer_pool,
backend_id,
)
.await
.map_err(SessionError::from)
}
fn handle_invalid_backend_response(
&self,
backend_id: BackendId,
ctx: InvalidBackendResponseContext<'_>,
) {
let InvalidBackendResponseContext {
provider,
request,
buffer,
conn,
} = ctx;
tracing::warn!(
client = %self.client_addr,
backend = ?backend_id,
command_verb = ?request.verb(),
first_bytes_hex = %crate::session::backend::format_hex_preview(
buffer, 256
),
first_bytes_utf8 = %String::from_utf8_lossy(buffer),
"Backend returned invalid/unparseable response, attempting to salvage connection"
);
let request_kind = request.kind();
let provider_for_salvage = provider.clone();
let conn_for_salvage = conn.release();
tokio::spawn(async move {
tracing::debug!(
backend = ?backend_id,
request_kind = ?request_kind,
"Attempting to salvage connection after Invalid response"
);
crate::pool::salvage_with_health_check(conn_for_salvage, provider_for_salvage).await;
});
}
#[allow(clippy::too_many_arguments)]
pub(super) async fn write_successful_backend_response<W>(
&self,
mut conn: crate::pool::ConnectionGuard,
client_write: &mut W,
backend_id: BackendId,
backend_bytes: crate::pool::PooledBuffer,
params: ResponseWriteParams<'_>,
backend_connection: Option<&mut Option<(BackendId, crate::pool::ConnectionGuard)>>,
) -> Result<RequestResponseMetadata, SessionError>
where
W: AsyncWrite + Unpin,
{
let has_response_body = params.request.has_response_body(params.status_code);
let status_code = params.status_code;
debug!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?params.request.verb(),
status_code = status_code.as_u16(),
has_response_body,
"Writing backend response to client"
);
let bytes_written = match self
.write_response_to_client(&mut conn, client_write, backend_id, backend_bytes, params)
.await
{
Ok(bytes) => bytes,
Err(e) => {
return Err(self.handle_response_transfer_error(
conn,
backend_id,
params.request,
e,
));
}
};
if let Err(e) = client_write.flush().await {
return Err(self.handle_response_transfer_error(
conn,
backend_id,
params.request,
classify_response_write_err(e),
));
}
debug!(
client = %self.client_addr,
backend = backend_id.as_index(),
msg_id = ?params.msg_id,
bytes_written,
"Backend response write complete"
);
self.record_response_metrics(
backend_id,
params.request,
status_code,
params.request.request_wire_len().as_u64(),
bytes_written,
);
self.release_or_reuse_connection(conn, backend_id, params.request, backend_connection);
Ok(RequestResponseMetadata::new(
status_code,
usize::try_from(bytes_written).unwrap_or(usize::MAX).into(),
))
}
fn handle_response_transfer_error(
&self,
conn: crate::pool::ConnectionGuard,
backend_id: BackendId,
request: &RequestContext,
error: ResponseTransferError,
) -> SessionError {
if error.must_remove_connection() {
warn!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?request.verb(),
error = %error,
"Response write error, removing connection from pool"
);
self.metrics.record_error(backend_id);
self.metrics.user_error(self.username());
conn.retire_with_cooldown();
} else if let ResponseConnectionReuse::QueuedBytes { len } =
crate::session::response_transfer::connection_reuse_after_response(&conn)
{
warn!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?request.verb(),
queued_bytes = len,
"Response write left queued backend bytes; retiring connection"
);
conn.retire_without_cooldown();
} else {
debug!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?request.verb(),
"Response write error left backend connection reusable; releasing it back to pool"
);
let _ = conn.release();
}
SessionError::from(error)
}
pub(super) fn release_or_reuse_connection(
&self,
conn: crate::pool::ConnectionGuard,
backend_id: BackendId,
request: &RequestContext,
backend_connection: Option<&mut Option<(BackendId, crate::pool::ConnectionGuard)>>,
) {
if let ResponseConnectionReuse::QueuedBytes { len } =
crate::session::response_transfer::connection_reuse_after_response(&conn)
{
warn!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?request.verb(),
msg_id = ?request.message_id_value(),
connection_type = conn.connection_type(),
pending_bytes = conn.pending_bytes_len(),
queued_bytes = len,
"Direct per-command response left queued backend bytes; retiring connection"
);
conn.retire_without_cooldown();
} else if let Some(slot) = backend_connection
&& slot.is_none()
{
let status = conn.provider_status_counts();
debug!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?request.verb(),
msg_id = ?request.message_id_value(),
connection_type = conn.connection_type(),
pending_bytes = conn.pending_bytes_len(),
pool = %conn.provider_name(),
pool_available = status.available,
pool_size = status.size,
pool_max_size = status.max_size,
pool_waiting = status.waiting,
"Direct per-command response finished cleanly; keeping backend connection for this client batch"
);
*slot = Some((backend_id, conn));
} else {
debug!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?request.verb(),
msg_id = ?request.message_id_value(),
connection_type = conn.connection_type(),
pending_bytes = conn.pending_bytes_len(),
"Direct per-command response finished cleanly; releasing backend connection"
);
let _ = conn.release();
}
}
async fn checkout_direct_backend_connection(
&self,
provider: &crate::pool::DeadpoolConnectionProvider,
backend_id: crate::types::BackendId,
request: &RequestContext,
backend_connection: &mut Option<(BackendId, crate::pool::ConnectionGuard)>,
) -> Result<crate::pool::ConnectionGuard> {
let checkout_status = provider.status_counts();
debug!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?request.verb(),
msg_id = ?request.message_id_value(),
pool = %provider.name(),
pool_available = checkout_status.available,
pool_size = checkout_status.size,
pool_max_size = checkout_status.max_size,
pool_waiting = checkout_status.waiting,
"Starting pool checkout for direct backend attempt"
);
let guard = match backend_connection.take() {
Some((cached_backend_id, guard)) if cached_backend_id == backend_id => {
let checkout_status = provider.status_counts();
if checkout_status.available == 0
&& checkout_status.size >= checkout_status.max_size
{
debug!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?request.verb(),
msg_id = ?request.message_id_value(),
pool = %provider.name(),
pool_available = checkout_status.available,
pool_size = checkout_status.size,
pool_max_size = checkout_status.max_size,
pool_waiting = checkout_status.waiting,
connection_type = guard.connection_type(),
pending_bytes = guard.pending_bytes_len(),
"Reusing backend connection for direct backend attempt because pool capacity is fully checked out"
);
guard
} else if checkout_status.available == 0 {
debug!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?request.verb(),
msg_id = ?request.message_id_value(),
pool = %provider.name(),
pool_available = checkout_status.available,
pool_size = checkout_status.size,
pool_max_size = checkout_status.max_size,
pool_waiting = checkout_status.waiting,
connection_type = guard.connection_type(),
pending_bytes = guard.pending_bytes_len(),
"Checking out another backend connection before returning cached batch connection"
);
let released = guard.release();
match provider.get_pooled_connection().await {
Ok(conn) => {
drop(released);
let checkout_status = provider.status_counts();
debug!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?request.verb(),
msg_id = ?request.message_id_value(),
pool = %provider.name(),
pool_available = checkout_status.available,
pool_size = checkout_status.size,
pool_max_size = checkout_status.max_size,
pool_waiting = checkout_status.waiting,
connection_type = conn.connection_type(),
pending_bytes = conn.pending_bytes_len(),
"Pool checkout succeeded for direct backend attempt"
);
crate::pool::ConnectionGuard::new(conn, provider.clone())
}
Err(err) => {
debug!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?request.verb(),
msg_id = ?request.message_id_value(),
pool = %provider.name(),
error = %err,
connection_type = released.connection_type(),
pending_bytes = released.pending_bytes_len(),
"Pool checkout failed; reusing cached batch connection for direct backend attempt"
);
crate::pool::ConnectionGuard::new(released, provider.clone())
}
}
} else {
debug!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?request.verb(),
msg_id = ?request.message_id_value(),
pool = %provider.name(),
pool_available = checkout_status.available,
pool_size = checkout_status.size,
pool_max_size = checkout_status.max_size,
pool_waiting = checkout_status.waiting,
connection_type = guard.connection_type(),
pending_bytes = guard.pending_bytes_len(),
"Releasing cached batch connection because backend pool has idle capacity"
);
let _ = guard.release();
let conn = provider.get_pooled_connection().await?;
let checkout_status = provider.status_counts();
debug!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?request.verb(),
msg_id = ?request.message_id_value(),
pool = %provider.name(),
pool_available = checkout_status.available,
pool_size = checkout_status.size,
pool_max_size = checkout_status.max_size,
pool_waiting = checkout_status.waiting,
connection_type = conn.connection_type(),
pending_bytes = conn.pending_bytes_len(),
"Pool checkout succeeded for direct backend attempt"
);
crate::pool::ConnectionGuard::new(conn, provider.clone())
}
}
Some(cached) => {
let (cached_backend_id, cached_conn) = cached;
debug!(
client = %self.client_addr,
cached_backend = cached_backend_id.as_index(),
backend = backend_id.as_index(),
command_verb = ?request.verb(),
msg_id = ?request.message_id_value(),
connection_type = cached_conn.connection_type(),
pending_bytes = cached_conn.pending_bytes_len(),
"Releasing cached backend connection before switching backend"
);
let _ = cached_conn.release();
let checkout_status = provider.status_counts();
debug!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?request.verb(),
msg_id = ?request.message_id_value(),
pool = %provider.name(),
pool_available = checkout_status.available,
pool_size = checkout_status.size,
pool_max_size = checkout_status.max_size,
pool_waiting = checkout_status.waiting,
"Requesting pooled connection after backend switch"
);
let conn = provider.get_pooled_connection().await?;
let checkout_status = provider.status_counts();
debug!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?request.verb(),
msg_id = ?request.message_id_value(),
pool = %provider.name(),
pool_available = checkout_status.available,
pool_size = checkout_status.size,
pool_max_size = checkout_status.max_size,
pool_waiting = checkout_status.waiting,
connection_type = conn.connection_type(),
pending_bytes = conn.pending_bytes_len(),
"Pool checkout succeeded for direct backend attempt"
);
crate::pool::ConnectionGuard::new(conn, provider.clone())
}
None => {
let checkout_status = provider.status_counts();
debug!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?request.verb(),
msg_id = ?request.message_id_value(),
pool = %provider.name(),
pool_available = checkout_status.available,
pool_size = checkout_status.size,
pool_max_size = checkout_status.max_size,
pool_waiting = checkout_status.waiting,
"Requesting pooled connection"
);
let conn = provider.get_pooled_connection().await?;
let checkout_status = provider.status_counts();
debug!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?request.verb(),
msg_id = ?request.message_id_value(),
pool = %provider.name(),
pool_available = checkout_status.available,
pool_size = checkout_status.size,
pool_max_size = checkout_status.max_size,
pool_waiting = checkout_status.waiting,
connection_type = conn.connection_type(),
pending_bytes = conn.pending_bytes_len(),
"Pool checkout succeeded for direct backend attempt"
);
crate::pool::ConnectionGuard::new(conn, provider.clone())
}
};
Ok(guard)
}
async fn execute_backend_attempt(
&self,
provider: &crate::pool::DeadpoolConnectionProvider,
backend_id: crate::types::BackendId,
request: &RequestContext,
backend_connection: &mut Option<(BackendId, crate::pool::ConnectionGuard)>,
) -> Result<ExecutedBackendAttempt> {
let mut guard = self
.checkout_direct_backend_connection(provider, backend_id, request, backend_connection)
.await?;
debug!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?request.verb(),
msg_id = ?request.message_id_value(),
connection_type = guard.connection_type(),
pending_bytes = guard.pending_bytes_len(),
"Sending request to backend and waiting for classifiable response bytes"
);
let result = self
.execute_and_read_response(&mut guard, backend_id, request)
.await;
match result {
Ok((read_status, buffer, timings)) => Ok((guard, read_status, buffer, timings)),
Err(BackendReadAttemptError::Backend(e)) => {
debug!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?request.verb(),
error = %e,
"Backend attempt failed before response completed; dropping pooled connection"
);
guard.retire_with_cooldown();
Err(e)
}
}
}
async fn execute_and_read_response(
&self,
conn: &mut crate::stream::ConnectionStream,
backend_id: crate::types::BackendId,
request: &RequestContext,
) -> Result<BackendReadAttempt, BackendReadAttemptError> {
self.metrics.record_command(backend_id);
self.metrics.user_command(self.username());
let mut buffer = self.buffer_pool.acquire();
let (response, timings) = if should_sample_backend_timing() {
let (response, ttfb, send, recv) =
backend::send_request_timed(conn, request, &mut buffer)
.await
.map_err(BackendReadAttemptError::Backend)?;
(response, Some((ttfb, send, recv)))
} else {
let response = backend::send_request(conn, request, &mut buffer)
.await
.map_err(BackendReadAttemptError::Backend)?;
(response, None)
};
debug!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?request.verb(),
msg_id = ?request.message_id_value(),
"Backend response classification completed for direct attempt"
);
Ok((response, buffer, timings))
}
async fn write_response_to_client<W>(
&self,
pooled_conn: &mut deadpool::managed::Object<crate::pool::deadpool_connection::TcpManager>,
client_write: &mut W,
backend_id: BackendId,
backend_bytes: crate::pool::PooledBuffer,
params: ResponseWriteParams<'_>,
) -> Result<u64, ResponseTransferError>
where
W: AsyncWrite + Unpin,
{
let cache_action = determine_cache_action_for_request(
params.request,
params.status_code,
self.cache_articles,
params.msg_id.is_some(),
);
debug!(
"write_response_to_client: code={}, cache_articles={}, has_msg_id={}, action={:?}",
params.status_code.as_u16(),
self.cache_articles,
params.msg_id.is_some(),
cache_action
);
if matches!(cache_action, CacheAction::TrackAvailability) {
self.apply_cache_action(cache_action, params, backend_id, None);
}
let (bytes_written, captured) =
match ResponseRetention::for_request(params.request, cache_action) {
ResponseRetention::DiscardAfterWrite => {
let bytes = self
.write_response_without_retention(
pooled_conn,
client_write,
backend_id,
backend_bytes,
params,
)
.await?;
(bytes, None)
}
ResponseRetention::RetainAfterWrite => {
let (bytes, response) = self
.write_response_with_retention(
pooled_conn,
client_write,
backend_id,
backend_bytes,
params,
)
.await?;
(bytes, response)
}
};
if !matches!(cache_action, CacheAction::TrackAvailability) {
self.apply_cache_action(cache_action, params, backend_id, captured);
}
Ok(bytes_written)
}
fn apply_cache_action(
&self,
cache_action: CacheAction,
params: ResponseWriteParams<'_>,
backend_id: BackendId,
captured: Option<crate::pool::ChunkedResponse>,
) {
match (cache_action, params.msg_id, captured) {
(CacheAction::CaptureArticle, msg_id, Some(response)) => {
self.maybe_cache_upsert_buffer(msg_id, response.into(), backend_id);
}
(CacheAction::TrackAvailability, Some(msg_id), _)
| (CacheAction::CaptureArticle, Some(msg_id), None)
if !params.request.cache_records_backend_has_article(backend_id) =>
{
self.spawn_cache_upsert_availability(
msg_id,
params.status_code,
backend_id,
self.tier_for_backend(backend_id),
);
}
(CacheAction::TrackStat, msg_id, _)
if self.cache_articles && self.cache.stores_payload_responses() =>
{
self.maybe_cache_upsert_buffer(
msg_id,
crate::cache::CacheIngestResponse::from(b"223\r\n".as_slice()),
backend_id,
);
}
(CacheAction::None, _, _)
| (CacheAction::TrackAvailability, _, _)
| (CacheAction::TrackStat, _, _)
| (CacheAction::CaptureArticle, _, None) => {}
}
}
async fn write_response_without_retention<W>(
&self,
pooled_conn: &mut deadpool::managed::Object<crate::pool::deadpool_connection::TcpManager>,
client_write: &mut W,
backend_id: BackendId,
mut backend_bytes: crate::pool::PooledBuffer,
params: ResponseWriteParams<'_>,
) -> Result<u64, ResponseTransferError>
where
W: AsyncWrite + Unpin,
{
crate::session::multiline_framing::write_response(
params.request,
&mut backend_bytes,
pooled_conn,
client_write,
&self.buffer_pool,
backend_id,
)
.await
}
#[allow(clippy::too_many_arguments)]
async fn write_response_with_retention<W>(
&self,
pooled_conn: &mut deadpool::managed::Object<crate::pool::deadpool_connection::TcpManager>,
client_write: &mut W,
backend_id: BackendId,
mut backend_bytes: crate::pool::PooledBuffer,
params: ResponseWriteParams<'_>,
) -> Result<(u64, Option<crate::pool::ChunkedResponse>), ResponseTransferError>
where
W: AsyncWrite + Unpin,
{
let mut captured = crate::pool::ChunkedResponse::default();
let (bytes_written, retained) =
crate::session::backend::write_response_with_optional_capture(
params.request,
&mut backend_bytes,
pooled_conn,
client_write,
&mut captured,
&self.buffer_pool,
backend_id,
)
.await?;
drop(backend_bytes);
self.log_body_response_written(backend_id, params, bytes_written as usize);
if retained && let Some(msg_id_ref) = params.msg_id {
debug!(
"Client {} caching full article for {} ({} bytes captured)",
self.client_addr,
msg_id_ref,
captured.len()
);
}
if retained {
Ok((bytes_written, Some(captured)))
} else {
debug!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?params.request.verb(),
status_code = params.status_code.as_u16(),
bytes_written,
"Response exceeded retention limit; streamed without cache insertion"
);
Ok((bytes_written, None))
}
}
fn log_body_response_written(
&self,
backend_id: BackendId,
params: ResponseWriteParams<'_>,
bytes_written: usize,
) {
if matches!(params.request.kind(), RequestKind::Body) {
debug!(
client = %self.client_addr,
backend = backend_id.as_index(),
command_verb = ?params.request.verb(),
status_code = params.status_code.as_u16(),
bytes_written,
"Wrote captured BODY response to direct client"
);
}
}
pub(super) async fn send_430_to_client<W>(
&self,
client_write: &mut W,
backend_to_client_bytes: &mut BackendToClientBytes,
) -> Result<()>
where
W: AsyncWrite + Unpin,
{
client_write
.write_all(crate::protocol::NO_SUCH_ARTICLE)
.await?;
*backend_to_client_bytes =
backend_to_client_bytes.add(crate::protocol::NO_SUCH_ARTICLE.len());
Ok(())
}
fn record_timing_metrics(
&self,
backend_id: crate::types::BackendId,
ttfb: u64,
send: u64,
recv: u64,
) {
self.metrics.record_ttfb_micros(backend_id, ttfb);
self.metrics.record_send_recv_micros(backend_id, send, recv);
}
#[inline]
fn maybe_cache_upsert_buffer(
&self,
msg_id: Option<&crate::types::MessageId<'_>>,
data: crate::cache::CacheIngestResponse,
backend_id: BackendId,
) {
if let Some(msg_id_ref) = msg_id {
let tier = self.tier_for_backend(backend_id);
self.spawn_cache_upsert_buffer(msg_id_ref, data, backend_id, tier);
}
}
fn record_response_metrics(
&self,
backend_id: crate::types::BackendId,
request: &RequestContext,
status_code: StatusCode,
cmd_bytes: u64,
resp_bytes: u64,
) {
use crate::types::MetricsBytes;
match determine_metrics_action_for_request(request, status_code) {
MetricsAction::Error4xx => self.metrics.record_error_4xx(backend_id),
MetricsAction::Error5xx => self.metrics.record_error_5xx(backend_id),
MetricsAction::Article => self.metrics.record_article(backend_id, resp_bytes),
MetricsAction::None => {}
}
let cmd_bytes_metric = MetricsBytes::new(cmd_bytes);
let resp_bytes_metric = MetricsBytes::new(resp_bytes);
let _ =
self.metrics
.record_command_execution(backend_id, cmd_bytes_metric, resp_bytes_metric);
self.metrics.user_bytes_sent(self.username(), cmd_bytes);
self.metrics
.user_bytes_received(self.username(), resp_bytes);
}
}
#[cfg(test)]
mod tests {
use super::ArticleAttemptState;
use super::AuthoritativeArticleMissing;
use super::BackendAttemptResult;
use super::ResponseWriteParams;
use super::classify_response_write_err;
use crate::auth::AuthHandler;
use crate::cache::{ArticleAvailability, UnifiedCache};
use crate::metrics::MetricsCollector;
use crate::pool::{BufferPool, ConnectionGuard, DeadpoolConnectionProvider};
use crate::protocol::{RequestContext, RequestResponseMetadata, ResponseWireLen, StatusCode};
use crate::router::BackendSelector;
use crate::session::routing::CacheAction;
use crate::types::{
BackendId, BufferSize, ClientAddress, ClientToBackendBytes, MessageId, ServerName,
};
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::task::{Context, Poll};
use std::time::Duration;
use tokio::io::AsyncWrite;
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::net::{TcpListener, TcpStream};
fn request_context(line: &[u8]) -> RequestContext {
RequestContext::parse(line).expect("valid request line")
}
fn test_session() -> crate::session::ClientSession {
let addr: std::net::SocketAddr = "127.0.0.1:9999".parse().unwrap();
let buffer_pool = BufferPool::new(BufferSize::try_new(8192).unwrap(), 4);
let auth_handler = Arc::new(AuthHandler::new(None, None).unwrap());
let metrics = MetricsCollector::new(1);
crate::session::ClientSession::builder(
ClientAddress::from(addr),
buffer_pool,
auth_handler,
metrics,
)
.build()
}
fn test_session_with_cache(
cache: Arc<UnifiedCache>,
cache_articles: bool,
) -> crate::session::ClientSession {
let addr: std::net::SocketAddr = "127.0.0.1:9999".parse().unwrap();
let buffer_pool = BufferPool::new(BufferSize::try_new(8192).unwrap(), 4);
let auth_handler = Arc::new(AuthHandler::new(None, None).unwrap());
let metrics = MetricsCollector::new(1);
crate::session::ClientSession::builder(
ClientAddress::from(addr),
buffer_pool,
auth_handler,
metrics,
)
.with_cache(cache)
.with_cache_articles(cache_articles)
.build()
}
async fn spawn_greeting_server() -> (u16, Arc<AtomicUsize>) {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let accept_count = Arc::new(AtomicUsize::new(0));
let count = Arc::clone(&accept_count);
tokio::spawn(async move {
while let Ok((stream, _)) = listener.accept().await {
count.fetch_add(1, Ordering::SeqCst);
tokio::spawn(async move {
let (read_half, mut write_half) = stream.into_split();
let mut reader = BufReader::new(read_half);
if write_half.write_all(b"200 Ready\r\n").await.is_err() {
return;
}
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line).await {
Ok(0) | Err(_) => break,
Ok(_) => {
let cmd = line.trim().to_ascii_uppercase();
if cmd == "COMPRESS DEFLATE" {
let _ = write_half.write_all(b"500 Not supported\r\n").await;
} else if cmd.starts_with("MODE") {
let _ = write_half.write_all(b"200 Posting allowed\r\n").await;
} else if cmd.starts_with("QUIT") {
let _ = write_half.write_all(b"205 Goodbye\r\n").await;
break;
} else if cmd.starts_with("DATE") {
let _ = write_half.write_all(b"111 20240101000000\r\n").await;
} else {
let _ = write_half.write_all(b"200 OK\r\n").await;
}
}
}
}
});
}
});
(port, accept_count)
}
async fn spawn_article_response_server(response: &'static [u8]) -> (u16, Arc<AtomicUsize>) {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let article_commands = Arc::new(AtomicUsize::new(0));
let count = Arc::clone(&article_commands);
tokio::spawn(async move {
while let Ok((stream, _)) = listener.accept().await {
let count = Arc::clone(&count);
tokio::spawn(async move {
let (read_half, mut write_half) = stream.into_split();
let mut reader = BufReader::new(read_half);
if write_half.write_all(b"200 Ready\r\n").await.is_err() {
return;
}
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line).await {
Ok(0) | Err(_) => break,
Ok(_) => {
let cmd = line.trim().to_ascii_uppercase();
if cmd.starts_with("MODE") {
let _ = write_half.write_all(b"200 Posting allowed\r\n").await;
} else if cmd.starts_with("BODY") || cmd.starts_with("ARTICLE") {
count.fetch_add(1, Ordering::SeqCst);
let _ = write_half.write_all(response).await;
} else if cmd.starts_with("QUIT") {
let _ = write_half.write_all(b"205 Goodbye\r\n").await;
break;
} else {
let _ = write_half.write_all(b"200 OK\r\n").await;
}
}
}
}
});
}
});
(port, article_commands)
}
async fn shared_client_writer_pair() -> (
crate::session::SharedClientWriter,
tokio::net::tcp::OwnedReadHalf,
tokio::net::tcp::OwnedWriteHalf,
) {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let client = TcpStream::connect(("127.0.0.1", port)).await.unwrap();
let (proxy_side, _) = listener.accept().await.unwrap();
let (_proxy_read, proxy_write) = proxy_side.into_split();
let (client_read, client_write) = client.into_split();
(
crate::session::SharedClientWriter::new(proxy_write),
client_read,
client_write,
)
}
fn router_with_backend(provider: DeadpoolConnectionProvider) -> Arc<BackendSelector> {
let mut router = BackendSelector::new();
router.add_backend(
BackendId::from_index(0),
ServerName::try_new("backend-0".to_string()).unwrap(),
provider,
0,
);
Arc::new(router)
}
fn router_with_tiered_backends(
backends: impl IntoIterator<Item = (DeadpoolConnectionProvider, u8)>,
) -> Arc<BackendSelector> {
let mut router = BackendSelector::new();
for (index, (provider, tier)) in backends.into_iter().enumerate() {
router.add_backend(
BackendId::from_index(index),
ServerName::try_new(format!("backend-{index}")).unwrap(),
provider,
tier,
);
}
Arc::new(router)
}
struct FlushFailWriter {
writes: Vec<u8>,
}
impl FlushFailWriter {
fn new() -> Self {
Self { writes: Vec::new() }
}
}
impl AsyncWrite for FlushFailWriter {
fn poll_write(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
self.writes.extend_from_slice(buf);
Poll::Ready(Ok(buf.len()))
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Poll::Ready(Err(std::io::Error::from(std::io::ErrorKind::BrokenPipe)))
}
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Poll::Ready(Ok(()))
}
}
struct WriteFailWriter;
impl AsyncWrite for WriteFailWriter {
fn poll_write(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
_buf: &[u8],
) -> Poll<std::io::Result<usize>> {
Poll::Ready(Err(std::io::Error::from(std::io::ErrorKind::BrokenPipe)))
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Poll::Ready(Ok(()))
}
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Poll::Ready(Ok(()))
}
}
#[test]
fn backend_attempt_success_records_success() {
let backend_id = BackendId::from_index(1);
let response = RequestResponseMetadata::new(StatusCode::new(220), ResponseWireLen::new(42));
let mut request = request_context(b"ARTICLE <test@example.com>\r\n");
let result = BackendAttemptResult::success(&mut request, backend_id, response);
assert!(matches!(result, BackendAttemptResult::Success));
}
#[test]
fn backend_attempt_success_records_request_context() {
let backend_id = BackendId::from_index(1);
let response = RequestResponseMetadata::new(StatusCode::new(220), ResponseWireLen::new(42));
let mut request = request_context(b"ARTICLE <test@example.com>\r\n");
let result = BackendAttemptResult::success(&mut request, backend_id, response);
assert!(matches!(result, BackendAttemptResult::Success));
assert_eq!(request.backend_id(), Some(backend_id));
assert_eq!(request.response_metadata(), Some(response));
}
#[test]
fn owned_response_timeout_is_terminal_client_disconnect() {
let err = std::io::Error::new(std::io::ErrorKind::TimedOut, "timed out");
let classified = classify_response_write_err(err);
assert!(matches!(
classified,
crate::session::response_transfer::ResponseTransferError::ClientDisconnect(_)
));
}
#[test]
fn owned_response_abort_is_terminal_client_disconnect() {
let err = std::io::Error::new(std::io::ErrorKind::ConnectionAborted, "aborted");
let classified = classify_response_write_err(err);
assert!(matches!(
classified,
crate::session::response_transfer::ResponseTransferError::ClientDisconnect(_)
));
}
#[test]
fn authoritative_article_missing_only_classifies_430() {
let backend_id = BackendId::from_index(2);
let missing =
AuthoritativeArticleMissing::from_status_code(backend_id, StatusCode::new(430));
assert_eq!(
missing.map(AuthoritativeArticleMissing::backend_id),
Some(backend_id)
);
assert!(
AuthoritativeArticleMissing::from_status_code(backend_id, StatusCode::new(400))
.is_none()
);
assert!(
AuthoritativeArticleMissing::from_status_code(backend_id, StatusCode::new(500))
.is_none()
);
assert!(
AuthoritativeArticleMissing::from_status_code(backend_id, StatusCode::new(222))
.is_none()
);
}
#[test]
fn missing_availability_requires_authoritative_430_classification() {
let session = test_session();
let backend_id = BackendId::from_index(0);
let mut availability = ArticleAvailability::new();
assert!(
AuthoritativeArticleMissing::from_status_code(backend_id, StatusCode::new(503))
.is_none()
);
assert_eq!(availability.checked_bits(), 0);
assert_eq!(availability.missing_bits(), 0);
let missing =
AuthoritativeArticleMissing::from_status_code(backend_id, StatusCode::new(430))
.expect("430 is authoritative article-missing");
session.record_authoritative_article_missing(missing, &mut availability);
assert_eq!(availability.checked_bits(), 0b0000_0001);
assert_eq!(availability.missing_bits(), 0b0000_0001);
}
#[tokio::test]
async fn retry_430_attempt_does_not_wait_for_client_writer_lock() {
let session = test_session();
let (port, article_commands) = spawn_article_response_server(b"430 Missing\r\n").await;
let provider = DeadpoolConnectionProvider::builder("127.0.0.1", port)
.max_connections(1)
.build()
.unwrap();
let router = router_with_backend(provider);
let (client_writer, _client_read, _client_write) = shared_client_writer_pair().await;
let held_writer = client_writer.lock().await;
let mut request = request_context(b"BODY <missing@example.com>\r\n");
let mut availability = ArticleAvailability::new();
let mut client_to_backend_bytes = ClientToBackendBytes::zero();
let mut backend_connection = None;
let mut unavailable_backends = 0;
let mut state = ArticleAttemptState {
availability: &mut availability,
client_to_backend_bytes: &mut client_to_backend_bytes,
backend_connection: &mut backend_connection,
unavailable_backends: &mut unavailable_backends,
};
let result = tokio::time::timeout(
Duration::from_secs(1),
session.try_backend_for_article(&router, &mut request, &client_writer, &mut state),
)
.await
.expect("430 probe should not wait for the client writer lock")
.expect("430 probe should complete without transport error");
assert!(matches!(
result,
BackendAttemptResult::ArticleNotFound { backend_id }
if backend_id == BackendId::from_index(0)
));
assert!(availability.is_missing(BackendId::from_index(0)));
assert_eq!(article_commands.load(Ordering::SeqCst), 1);
drop(held_writer);
}
#[tokio::test]
async fn track_stat_does_not_store_payload_when_article_cache_disabled() {
let cache = Arc::new(UnifiedCache::memory(1024, Duration::from_secs(60)));
let session = test_session_with_cache(cache.clone(), false);
let request = request_context(b"STAT <stat-disabled@example.com>\r\n");
let msg_id =
MessageId::new("<stat-disabled@example.com>".to_string()).expect("valid message id");
session.apply_cache_action(
CacheAction::TrackStat,
ResponseWriteParams {
request: &request,
msg_id: Some(&msg_id),
status_code: StatusCode::new(223),
},
BackendId::from_index(0),
None,
);
tokio::task::yield_now().await;
assert!(
cache.get(&msg_id).await.is_none(),
"disabled article caching must not retain synthetic STAT payload entries"
);
}
#[tokio::test]
async fn invalid_backend_response_does_not_mark_article_missing() {
let session = test_session();
let (port, article_commands) =
spawn_article_response_server(b"not a status line\r\n").await;
let provider = DeadpoolConnectionProvider::builder("127.0.0.1", port)
.max_connections(1)
.build()
.unwrap();
let router = router_with_backend(provider);
let (client_writer, _client_read, _client_write) = shared_client_writer_pair().await;
let mut request = request_context(b"BODY <bad-response@example.com>\r\n");
let mut availability = ArticleAvailability::new();
let mut client_to_backend_bytes = ClientToBackendBytes::zero();
let mut backend_connection = None;
let mut unavailable_backends = 0;
let mut state = ArticleAttemptState {
availability: &mut availability,
client_to_backend_bytes: &mut client_to_backend_bytes,
backend_connection: &mut backend_connection,
unavailable_backends: &mut unavailable_backends,
};
let result = tokio::time::timeout(
Duration::from_secs(1),
session.try_backend_for_article(&router, &mut request, &client_writer, &mut state),
)
.await
.expect("invalid response attempt should not hang")
.expect("invalid response attempt should be handled as unavailable");
assert!(matches!(result, BackendAttemptResult::BackendUnavailable));
assert_eq!(article_commands.load(Ordering::SeqCst), 1);
assert_eq!(availability.checked_bits(), 0);
assert_eq!(availability.missing_bits(), 0);
assert_eq!(unavailable_backends, 0b0000_0001);
}
#[tokio::test]
async fn invalid_backend_response_retries_next_same_tier_without_poisoning_availability() {
let session = test_session();
let (bad_port, bad_article_commands) =
spawn_article_response_server(b"not a status line\r\n").await;
let good_response = b"222 0 <found@example.com> body follows\r\npayload\r\n.\r\n";
let (good_port, good_article_commands) = spawn_article_response_server(good_response).await;
let router = router_with_tiered_backends([
(
DeadpoolConnectionProvider::builder("127.0.0.1", bad_port)
.max_connections(1)
.build()
.unwrap(),
0,
),
(
DeadpoolConnectionProvider::builder("127.0.0.1", good_port)
.max_connections(1)
.build()
.unwrap(),
0,
),
]);
let (client_writer, mut client_read, _client_write) = shared_client_writer_pair().await;
let mut request = request_context(b"BODY <found@example.com>\r\n");
let mut availability = ArticleAvailability::new();
let mut client_to_backend_bytes = ClientToBackendBytes::zero();
let mut backend_connection = None;
let mut unavailable_backends = 0;
let mut state = ArticleAttemptState {
availability: &mut availability,
client_to_backend_bytes: &mut client_to_backend_bytes,
backend_connection: &mut backend_connection,
unavailable_backends: &mut unavailable_backends,
};
let first = session
.try_backend_for_article(&router, &mut request, &client_writer, &mut state)
.await
.expect("invalid response should be handled");
assert!(matches!(first, BackendAttemptResult::BackendUnavailable));
assert_eq!(state.availability.checked_bits(), 0);
assert_eq!(state.availability.missing_bits(), 0);
assert_eq!(*state.unavailable_backends, 0b0000_0001);
let second = session
.try_backend_for_article(&router, &mut request, &client_writer, &mut state)
.await
.expect("same-tier retry should succeed");
assert!(matches!(second, BackendAttemptResult::Success));
assert_eq!(bad_article_commands.load(Ordering::SeqCst), 1);
assert_eq!(good_article_commands.load(Ordering::SeqCst), 1);
assert_eq!(availability.checked_bits(), 0b0000_0010);
assert_eq!(availability.missing_bits(), 0);
assert_eq!(request.backend_id(), Some(BackendId::from_index(1)));
let mut written = vec![0; good_response.len()];
tokio::time::timeout(Duration::from_secs(1), client_read.read_exact(&mut written))
.await
.expect("client response should be readable")
.expect("client read should succeed");
assert_eq!(written, good_response);
}
#[tokio::test]
async fn uncaptured_article_cache_action_records_availability() {
let addr: std::net::SocketAddr = "127.0.0.1:9999".parse().unwrap();
let buffer_pool = BufferPool::new(BufferSize::try_new(8192).unwrap(), 4);
let auth_handler = Arc::new(AuthHandler::new(None, None).unwrap());
let metrics = MetricsCollector::new(1);
let cache = Arc::new(crate::cache::UnifiedCache::memory(
1000,
Duration::from_secs(60),
));
let session = crate::session::ClientSession::builder(
ClientAddress::from(addr),
buffer_pool,
auth_handler,
metrics,
)
.with_cache(cache)
.with_cache_articles(true)
.build();
let backend_id = BackendId::from_index(0);
let request = request_context(b"ARTICLE <large@example.com>\r\n");
let msg_id = crate::types::MessageId::new("<large@example.com>".to_string()).unwrap();
session.apply_cache_action(
crate::session::routing::CacheAction::CaptureArticle,
ResponseWriteParams {
request: &request,
msg_id: Some(&msg_id),
status_code: StatusCode::new(220),
},
backend_id,
None,
);
let cached = tokio::time::timeout(Duration::from_secs(1), async {
loop {
if let Some(cached) = session.cache.get(&msg_id).await {
break cached;
}
tokio::time::sleep(Duration::from_millis(5)).await;
}
})
.await
.expect("availability update should be recorded");
assert_eq!(cached.status_code(), StatusCode::new(220));
assert_eq!(cached.payload_len().get(), 0);
assert!(cached.has_availability_info());
assert!(cached.availability().any_backend_has_article());
}
#[tokio::test]
async fn transient_invalid_replies_do_not_escalate_to_backup_tier() {
let session = test_session();
let (bad0_port, bad0_article_commands) =
spawn_article_response_server(b"bad zero\r\n").await;
let (bad1_port, bad1_article_commands) =
spawn_article_response_server(b"bad one\r\n").await;
let (backup_port, backup_article_commands) =
spawn_article_response_server(b"222 0 <backup@example.com>\r\npayload\r\n.\r\n").await;
let router = router_with_tiered_backends([
(
DeadpoolConnectionProvider::builder("127.0.0.1", bad0_port)
.max_connections(1)
.build()
.unwrap(),
0,
),
(
DeadpoolConnectionProvider::builder("127.0.0.1", bad1_port)
.max_connections(1)
.build()
.unwrap(),
0,
),
(
DeadpoolConnectionProvider::builder("127.0.0.1", backup_port)
.max_connections(1)
.build()
.unwrap(),
1,
),
]);
let (client_writer, _client_read, _client_write) = shared_client_writer_pair().await;
let mut request = request_context(b"BODY <bad-response@example.com>\r\n");
let mut availability = ArticleAvailability::new();
let mut client_to_backend_bytes = ClientToBackendBytes::zero();
let mut backend_connection = None;
let mut unavailable_backends = 0;
let mut state = ArticleAttemptState {
availability: &mut availability,
client_to_backend_bytes: &mut client_to_backend_bytes,
backend_connection: &mut backend_connection,
unavailable_backends: &mut unavailable_backends,
};
for expected_mask in [0b0000_0001, 0b0000_0011] {
let result = session
.try_backend_for_article(&router, &mut request, &client_writer, &mut state)
.await
.expect("invalid response should be handled");
assert!(matches!(result, BackendAttemptResult::BackendUnavailable));
assert_eq!(*state.unavailable_backends, expected_mask);
}
let exhausted = session
.try_backend_for_article(&router, &mut request, &client_writer, &mut state)
.await
.expect("tier exhaustion should be reported without transport failure");
assert!(matches!(
exhausted,
BackendAttemptResult::NoRetryableBackend
));
assert_eq!(bad0_article_commands.load(Ordering::SeqCst), 1);
assert_eq!(bad1_article_commands.load(Ordering::SeqCst), 1);
assert_eq!(backup_article_commands.load(Ordering::SeqCst), 0);
assert_eq!(availability.checked_bits(), 0);
assert_eq!(availability.missing_bits(), 0);
}
#[tokio::test]
async fn successful_retry_attempt_waits_for_client_writer_only_to_emit_response() {
let session = test_session();
let response = b"222 0 <found@example.com> body follows\r\npayload\r\n.\r\n";
let (port, article_commands) = spawn_article_response_server(response).await;
let provider = DeadpoolConnectionProvider::builder("127.0.0.1", port)
.max_connections(1)
.build()
.unwrap();
let router = router_with_backend(provider);
let (client_writer, mut client_read, _client_write) = shared_client_writer_pair().await;
let held_writer = client_writer.lock().await;
let mut request = request_context(b"BODY <found@example.com>\r\n");
let mut availability = ArticleAvailability::new();
let mut client_to_backend_bytes = ClientToBackendBytes::zero();
let mut backend_connection = None;
let mut unavailable_backends = 0;
let mut state = ArticleAttemptState {
availability: &mut availability,
client_to_backend_bytes: &mut client_to_backend_bytes,
backend_connection: &mut backend_connection,
unavailable_backends: &mut unavailable_backends,
};
let mut attempt = Box::pin(session.try_backend_for_article(
&router,
&mut request,
&client_writer,
&mut state,
));
tokio::time::timeout(Duration::from_secs(1), async {
while article_commands.load(Ordering::SeqCst) == 0 {
tokio::select! {
_ = &mut attempt => {
panic!("successful attempt completed before client writer lock was released");
}
() = tokio::time::sleep(Duration::from_millis(5)) => {}
}
}
})
.await
.expect("backend should receive article command while client writer is locked");
assert!(
tokio::time::timeout(Duration::from_millis(50), &mut attempt)
.await
.is_err(),
"successful response should wait for the client writer lock before emitting"
);
drop(held_writer);
let result = tokio::time::timeout(Duration::from_secs(1), attempt)
.await
.expect("successful attempt should finish once the writer lock is released")
.expect("successful attempt should not fail");
assert!(matches!(result, BackendAttemptResult::Success));
assert_eq!(article_commands.load(Ordering::SeqCst), 1);
assert_eq!(availability.checked_bits(), 0b0000_0001);
assert_eq!(availability.missing_bits(), 0);
assert!(availability.any_backend_has_article());
let mut written = vec![0; response.len()];
tokio::time::timeout(Duration::from_secs(1), client_read.read_exact(&mut written))
.await
.expect("client response should be readable")
.expect("client read should succeed");
assert_eq!(written, response);
}
#[tokio::test]
async fn flush_disconnect_after_owned_response_reuses_backend_connection() {
let session = test_session();
let (port, accept_count) = spawn_greeting_server().await;
let provider = DeadpoolConnectionProvider::builder("127.0.0.1", port)
.max_connections(5)
.build()
.unwrap();
let conn = provider.get_pooled_connection().await.unwrap();
assert_eq!(accept_count.load(Ordering::SeqCst), 1);
let guard = ConnectionGuard::new(conn, provider.clone());
let request = request_context(b"STAT <test@example.com>\r\n");
let mut client_write = FlushFailWriter::new();
let mut backend_bytes = BufferPool::new(BufferSize::try_new(8192).unwrap(), 1).acquire();
backend_bytes.copy_from_slice(b"223 0 <test@example.com> status\r\n");
let err = session
.write_successful_backend_response(
guard,
&mut client_write,
BackendId::from_index(0),
backend_bytes,
ResponseWriteParams {
request: &request,
msg_id: None,
status_code: StatusCode::new(223),
},
None,
)
.await
.expect_err("flush should fail with client disconnect");
assert!(matches!(
err,
crate::session::SessionError::ClientDisconnect(_)
));
assert_eq!(client_write.writes, b"223 0 <test@example.com> status\r\n");
let _reused = provider.get_pooled_connection().await.unwrap();
assert_eq!(
accept_count.load(Ordering::SeqCst),
1,
"client-side flush failure after a clean owned response must not retire the backend connection"
);
}
#[tokio::test]
async fn write_disconnect_after_complete_response_reuses_backend_connection() {
let session = test_session();
let (port, accept_count) = spawn_greeting_server().await;
let provider = DeadpoolConnectionProvider::builder("127.0.0.1", port)
.max_connections(5)
.build()
.unwrap();
let conn = provider.get_pooled_connection().await.unwrap();
assert_eq!(accept_count.load(Ordering::SeqCst), 1);
let guard = ConnectionGuard::new(conn, provider.clone());
let request = request_context(b"ARTICLE <test@example.com>\r\n");
let mut client_write = WriteFailWriter;
let mut backend_bytes = BufferPool::new(BufferSize::try_new(8192).unwrap(), 1).acquire();
backend_bytes.copy_from_slice(b"220 Article follows\r\nbody\r\n.\r\n");
let err = session
.write_successful_backend_response(
guard,
&mut client_write,
BackendId::from_index(0),
backend_bytes,
ResponseWriteParams {
request: &request,
msg_id: None,
status_code: StatusCode::new(220),
},
None,
)
.await
.expect_err("client write should fail after the framer completed the response");
assert!(matches!(
err,
crate::session::SessionError::ClientDisconnect(_)
));
let _reused = provider.get_pooled_connection().await.unwrap();
assert_eq!(
accept_count.load(Ordering::SeqCst),
1,
"client write failure after a complete framed response must not retire the backend connection"
);
}
}