#![allow(clippy::module_name_repetitions)]
use std::{io, sync::Arc};
use thiserror::Error;
use crate::observability::{ObservabilityError, ObservabilitySampleRate};
use crate::proactor::{BufferRingError, ProactorError};
use crate::tls::TlsError;
use crate::ws::{WsConfig, WsError};
pub const DEFAULT_BUF_RING_SLOT_SIZE: u32 = 4 * 1024;
pub const DEFAULT_BUF_RING_ENTRIES: u16 = 256;
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
pub enum RecvMode {
#[default]
Multishot,
MultishotBundle,
}
impl std::fmt::Display for RecvMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Multishot => f.write_str("multishot"),
Self::MultishotBundle => f.write_str("multishot-bundle"),
}
}
}
impl std::str::FromStr for RecvMode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"multishot" | "multi" | "recv-multi" => Ok(Self::Multishot),
"multishot-bundle" | "multi-bundle" | "bundle" | "recv-bundle" => {
Ok(Self::MultishotBundle)
}
_ => Err(format!(
"invalid recv mode {s:?}; expected multishot or multishot-bundle"
)),
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum State {
Init,
Connecting,
TlsHandshake,
WsHandshake,
Open,
Closing,
Closed,
}
#[derive(Debug, Error)]
pub enum ConnectionError {
#[error("io error: {0}")]
Io(#[from] io::Error),
#[error("dns resolution returned no addresses for {0}")]
DnsEmpty(String),
#[error("proactor: {0}")]
Proactor(#[from] ProactorError),
#[error("buf ring: {0}")]
BufRing(#[from] BufferRingError),
#[error("tls: {0}")]
Tls(#[from] TlsError),
#[error("ws: {0}")]
Ws(#[from] WsError),
#[error("observability: {0}")]
Observability(#[from] ObservabilityError),
#[error("operation not allowed in state {0:?}")]
InvalidState(State),
#[error("connect failed: {0}")]
ConnectFailed(#[source] io::Error),
#[error("recv failed: {0}")]
RecvFailed(#[source] io::Error),
#[error("send failed: {0}")]
SendFailed(#[source] io::Error),
#[error("peer closed connection")]
PeerClosed,
#[error("CQE returned unknown OpKind: raw user_data = 0x{0:016x}")]
UnknownOpKind(u64),
#[error("pool {0} id space exhausted; restart or implement id reuse")]
IdSpaceExhausted(&'static str),
}
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
pub struct IngressStats {
pub recv_data_cqes: u64,
pub recv_bytes: u64,
pub recv_multishot_rearms: u64,
pub recv_ring_exhaustions: u64,
pub plain_recv_batches: u64,
pub plain_recv_batch_cqes: u64,
pub plain_recv_copied_batches: u64,
pub plain_recv_copied_bytes: u64,
pub plaintext_source_chunks: u64,
pub plaintext_bytes: u64,
pub ws_data_drains: u64,
pub ws_data_drain_skips: u64,
pub ws_data_events: u64,
pub ws_text_events: u64,
pub ws_binary_events: u64,
}
pub(crate) const CONN_ID_MASK: u64 = 0x0FFF_FFFF;
pub(crate) const CONN_GENERATION_MASK: u64 = 0x0FFF_FFFF;
pub(crate) const CONN_GENERATION_SHIFT: u32 = 28;
#[inline]
#[must_use]
pub(crate) const fn encode_conn_token(conn_id: u32, generation: u32) -> u64 {
((generation as u64) << CONN_GENERATION_SHIFT) | (conn_id as u64)
}
#[inline]
#[must_use]
pub(crate) const fn token_conn_id(token: u64) -> u32 {
(token & CONN_ID_MASK) as u32
}
#[inline]
#[must_use]
pub(crate) const fn token_generation(token: u64) -> u32 {
((token >> CONN_GENERATION_SHIFT) & CONN_GENERATION_MASK) as u32
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub(crate) struct ConnectionRuntimeIdentity {
pub conn_id: u32,
pub generation: u32,
pub bgid: u16,
}
impl ConnectionRuntimeIdentity {
#[inline]
#[must_use]
pub(crate) const fn token(self) -> u64 {
encode_conn_token(self.conn_id, self.generation)
}
}
#[derive(Debug, Clone)]
pub(crate) struct AssignedConnectionConfig {
pub user: ConnectionConfig,
pub identity: ConnectionRuntimeIdentity,
}
#[derive(Debug, Clone)]
pub struct ConnectionConfig {
pub host: String,
pub port: u16,
pub path: String,
pub use_tls: bool,
pub tls_config: Option<Arc<rustls::ClientConfig>>,
pub ws_config: Option<WsConfig>,
pub buf_ring_slot_size: u32,
pub buf_ring_entries: u16,
pub recv_mode: RecvMode,
pub socket_busy_poll_usecs: Option<u32>,
pub send_buffer_initial_capacity: Option<usize>,
pub tls_pending_out_initial_capacity: Option<usize>,
pub plain_recv_batch_copy_max_bytes: usize,
pub track_ingress_stats: bool,
pub observability_sample_rate: ObservabilitySampleRate,
pub record_observability_histograms: bool,
}
impl ConnectionConfig {
#[must_use]
pub fn new(host: impl Into<String>, port: u16, path: impl Into<String>) -> Self {
Self {
host: host.into(),
port,
path: path.into(),
use_tls: true,
tls_config: None,
buf_ring_slot_size: DEFAULT_BUF_RING_SLOT_SIZE,
buf_ring_entries: DEFAULT_BUF_RING_ENTRIES,
ws_config: None,
recv_mode: RecvMode::Multishot,
socket_busy_poll_usecs: None,
send_buffer_initial_capacity: None,
tls_pending_out_initial_capacity: None,
plain_recv_batch_copy_max_bytes: 0,
track_ingress_stats: false,
observability_sample_rate: ObservabilitySampleRate::always(),
record_observability_histograms: false,
}
}
#[must_use]
pub const fn with_tls(mut self, on: bool) -> Self {
self.use_tls = on;
self
}
#[must_use]
pub fn with_tls_config(mut self, config: Arc<rustls::ClientConfig>) -> Self {
self.tls_config = Some(config);
self
}
#[must_use]
pub const fn with_buf_ring(mut self, slot_size: u32, entries: u16) -> Self {
debug_assert!(slot_size > 0, "slot_size must be > 0");
debug_assert!(
entries > 0 && entries.is_power_of_two(),
"entries must be non-zero power of 2"
);
self.buf_ring_slot_size = slot_size;
self.buf_ring_entries = entries;
self
}
#[must_use]
pub fn with_ws_config(mut self, config: WsConfig) -> Self {
self.ws_config = Some(config);
self
}
#[must_use]
pub const fn with_recv_mode(mut self, mode: RecvMode) -> Self {
self.recv_mode = mode;
self
}
#[must_use]
pub const fn with_socket_busy_poll_usecs(mut self, usecs: u32) -> Self {
self.socket_busy_poll_usecs = Some(usecs);
self
}
#[must_use]
pub fn with_ws_limits(mut self, max_message_size: usize, max_frame_payload: u64) -> Self {
let mut config = self
.ws_config
.take()
.unwrap_or_else(|| WsConfig::new(self.host.clone(), self.path.clone()));
config.max_message_size = max_message_size;
config.max_frame_payload = max_frame_payload;
self.ws_config = Some(config);
self
}
#[must_use]
pub fn with_ws_recv_buffer_capacity(mut self, bytes: usize) -> Self {
let mut config = self
.ws_config
.take()
.unwrap_or_else(|| WsConfig::new(self.host.clone(), self.path.clone()));
config.initial_recv_buffer_capacity = Some(bytes);
self.ws_config = Some(config);
self
}
#[must_use]
pub fn with_ws_message_buffer_capacity(mut self, bytes: usize) -> Self {
let mut config = self
.ws_config
.take()
.unwrap_or_else(|| WsConfig::new(self.host.clone(), self.path.clone()));
config.initial_message_buffer_capacity = Some(bytes);
self.ws_config = Some(config);
self
}
#[must_use]
pub fn with_ws_tx_buffer_capacity(mut self, bytes: usize) -> Self {
let mut config = self
.ws_config
.take()
.unwrap_or_else(|| WsConfig::new(self.host.clone(), self.path.clone()));
config.initial_tx_buffer_capacity = Some(bytes);
self.ws_config = Some(config);
self
}
#[must_use]
pub fn with_ws_buffer_capacities(
mut self,
recv_bytes: usize,
message_bytes: usize,
tx_bytes: usize,
) -> Self {
let mut config = self
.ws_config
.take()
.unwrap_or_else(|| WsConfig::new(self.host.clone(), self.path.clone()));
config.initial_recv_buffer_capacity = Some(recv_bytes);
config.initial_message_buffer_capacity = Some(message_bytes);
config.initial_tx_buffer_capacity = Some(tx_bytes);
self.ws_config = Some(config);
self
}
#[must_use]
pub fn with_auto_pong(mut self, on: bool) -> Self {
let mut config = self
.ws_config
.take()
.unwrap_or_else(|| WsConfig::new(self.host.clone(), self.path.clone()));
config.auto_pong = on;
self.ws_config = Some(config);
self
}
#[must_use]
pub const fn with_send_buffer_capacity(mut self, bytes: usize) -> Self {
self.send_buffer_initial_capacity = Some(bytes);
self
}
#[must_use]
pub const fn with_tls_pending_out_capacity(mut self, bytes: usize) -> Self {
self.tls_pending_out_initial_capacity = Some(bytes);
self
}
#[must_use]
pub const fn with_connection_buffer_capacities(
mut self,
send_bytes: usize,
tls_pending_out_bytes: usize,
) -> Self {
self.send_buffer_initial_capacity = Some(send_bytes);
self.tls_pending_out_initial_capacity = Some(tls_pending_out_bytes);
self
}
#[must_use]
pub const fn with_plain_recv_batch_copy_max_bytes(mut self, bytes: usize) -> Self {
self.plain_recv_batch_copy_max_bytes = bytes;
self
}
#[must_use]
pub const fn with_ingress_stats(mut self, on: bool) -> Self {
self.track_ingress_stats = on;
self
}
#[must_use]
pub const fn with_observability_sample_rate_bps(mut self, basis_points: u16) -> Self {
self.observability_sample_rate = ObservabilitySampleRate::from_basis_points(basis_points);
self
}
#[must_use]
pub const fn with_observability_histograms(mut self, on: bool) -> Self {
self.record_observability_histograms = on;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recv_mode_display_and_parse_are_stable() {
assert_eq!(RecvMode::Multishot.to_string(), "multishot");
assert_eq!(RecvMode::MultishotBundle.to_string(), "multishot-bundle");
assert!(matches!(
"multishot".parse::<RecvMode>(),
Ok(RecvMode::Multishot)
));
assert!(matches!(
"multi".parse::<RecvMode>(),
Ok(RecvMode::Multishot)
));
assert!(matches!(
"recv-bundle".parse::<RecvMode>(),
Ok(RecvMode::MultishotBundle)
));
assert!("edge-triggered".parse::<RecvMode>().is_err());
}
#[test]
fn connection_config_defaults_are_low_latency_safe() {
let cfg = ConnectionConfig::new("example.com", 443, "/ws");
assert_eq!(cfg.host, "example.com");
assert_eq!(cfg.port, 443);
assert_eq!(cfg.path, "/ws");
assert!(cfg.use_tls);
assert_eq!(cfg.buf_ring_slot_size, DEFAULT_BUF_RING_SLOT_SIZE);
assert_eq!(cfg.buf_ring_entries, DEFAULT_BUF_RING_ENTRIES);
assert_eq!(cfg.recv_mode, RecvMode::Multishot);
assert_eq!(cfg.socket_busy_poll_usecs, None);
assert_eq!(cfg.plain_recv_batch_copy_max_bytes, 0);
assert!(!cfg.track_ingress_stats);
assert_eq!(
cfg.observability_sample_rate.basis_points(),
ObservabilitySampleRate::MAX_BASIS_POINTS
);
assert!(!cfg.record_observability_histograms);
}
#[test]
fn connection_config_builder_preserves_all_tuning_knobs() -> Result<(), &'static str> {
let ws = WsConfig::new("wrong-host", "/wrong")
.with_max_message_size(1024)
.with_max_frame_payload(2048)
.with_initial_buffer_capacities(11, 22, 33);
let cfg = ConnectionConfig::new("venue.example", 9443, "/real")
.with_tls(false)
.with_ws_config(ws)
.with_ws_limits(4096, 8192)
.with_ws_recv_buffer_capacity(128)
.with_ws_message_buffer_capacity(256)
.with_ws_tx_buffer_capacity(512)
.with_auto_pong(false)
.with_buf_ring(2048, 512)
.with_recv_mode(RecvMode::MultishotBundle)
.with_socket_busy_poll_usecs(100)
.with_connection_buffer_capacities(4096, 8192)
.with_plain_recv_batch_copy_max_bytes(16 * 1024)
.with_ingress_stats(true)
.with_observability_sample_rate_bps(12_345)
.with_observability_histograms(true);
assert_eq!(cfg.host, "venue.example");
assert_eq!(cfg.port, 9443);
assert_eq!(cfg.path, "/real");
assert!(!cfg.use_tls);
assert_eq!(cfg.buf_ring_slot_size, 2048);
assert_eq!(cfg.buf_ring_entries, 512);
assert_eq!(cfg.recv_mode, RecvMode::MultishotBundle);
assert_eq!(cfg.socket_busy_poll_usecs, Some(100));
assert_eq!(cfg.send_buffer_initial_capacity, Some(4096));
assert_eq!(cfg.tls_pending_out_initial_capacity, Some(8192));
assert_eq!(cfg.plain_recv_batch_copy_max_bytes, 16 * 1024);
assert!(cfg.track_ingress_stats);
assert_eq!(
cfg.observability_sample_rate.basis_points(),
ObservabilitySampleRate::MAX_BASIS_POINTS
);
assert!(cfg.record_observability_histograms);
let ws = cfg.ws_config.ok_or("missing ws config")?;
assert_eq!(ws.host, "wrong-host");
assert_eq!(ws.path, "/wrong");
assert_eq!(ws.max_message_size, 4096);
assert_eq!(ws.max_frame_payload, 8192);
assert_eq!(ws.initial_recv_buffer_capacity, Some(128));
assert_eq!(ws.initial_message_buffer_capacity, Some(256));
assert_eq!(ws.initial_tx_buffer_capacity, Some(512));
assert!(!ws.auto_pong);
Ok(())
}
#[test]
fn connection_config_individual_buffer_builders_are_composable() -> Result<(), &'static str> {
let cfg = ConnectionConfig::new("venue.example", 443, "/ws")
.with_send_buffer_capacity(1)
.with_tls_pending_out_capacity(2)
.with_ws_buffer_capacities(3, 4, 5);
assert_eq!(cfg.send_buffer_initial_capacity, Some(1));
assert_eq!(cfg.tls_pending_out_initial_capacity, Some(2));
let ws = cfg.ws_config.ok_or("missing ws config")?;
assert_eq!(ws.initial_recv_buffer_capacity, Some(3));
assert_eq!(ws.initial_message_buffer_capacity, Some(4));
assert_eq!(ws.initial_tx_buffer_capacity, Some(5));
Ok(())
}
}