use std::io::{self, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::thread::{self, JoinHandle};
use std::time::Duration;
use syncbat::{
unknown_subscription_error, RuntimeCursor, SessionControl, SessionDelivery, SessionEnd,
SessionError, SessionPoll, SubscriptionRuntimeError, SubscriptionSession,
SubscriptionSessionFactory,
};
use super::error::NetbatError;
use super::limiter::{stats_lane, Admission, ConnectionLimit, ConnectionPermit, Limiter};
use super::limits::IoTimeouts;
use super::limits::Limits;
use super::stream_frame::{
decode_stream_line, encode_stream_frame, CursorBytes, DeliveryIndex, MaybeCursor,
PayloadSchemaRef, StreamFrame, StreamReasonCode, SubEndFrame, SubErrFrame, SubEventFrame,
SubWatermarkFrame, SubscribeFrame, SubscriptionToken,
};
use super::tcp::{apply_timeouts, read_line, ShutdownHandle, TransportSecurity};
const CURSOR_INVALID_CODE: &str = "cursor_invalid";
const CURSOR_MISMATCH_CODE: &str = "cursor_mismatch";
pub(super) const SUBSCRIPTION_POLL_INTERVAL: Duration = Duration::from_millis(50);
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub struct TcpSubscriptionServeStats {
pub accepted_connections: usize,
pub served_subscriptions: usize,
pub failed_subscriptions: usize,
pub malformed_pre_subscribe: usize,
pub runtime_failures: usize,
pub connection_io_failures: usize,
pub worker_panics: usize,
#[cfg(feature = "tls")]
pub tls_handshake_failures: usize,
pub shutdown_requested: bool,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum SubscriptionDispatch {
#[default]
Concurrent,
Sequential,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct TcpSubscriptionServerConfig {
pub limits: Limits,
pub timeouts: IoTimeouts,
pub connection_limit: ConnectionLimit,
pub dispatch: SubscriptionDispatch,
pub idle_sleep: Duration,
}
impl Default for TcpSubscriptionServerConfig {
fn default() -> Self {
Self {
limits: Limits::default(),
timeouts: IoTimeouts::default()
.with_read(Some(Duration::from_millis(250)))
.with_write(Some(Duration::from_secs(5))),
connection_limit: ConnectionLimit::default(),
dispatch: SubscriptionDispatch::default(),
idle_sleep: Duration::from_millis(10),
}
}
}
pub fn serve_subscription_stream(
reader: impl Read + Send + 'static,
mut writer: impl Write + Send + 'static,
runtime: &(impl SubscriptionSessionFactory + ?Sized),
limits: &Limits,
) -> Result<TcpSubscriptionServeStats, NetbatError> {
let mut stats = TcpSubscriptionServeStats::default();
let mut reader = reader;
let first_line = match read_line(&mut reader, limits.max_line_bytes) {
Ok(line) => line,
Err(NetbatError::EmptyStream) => return Ok(stats),
Err(error) => return Err(error),
};
let subscribe = match decode_subscribe_request(&first_line, limits) {
Ok(frame) => frame,
Err(_) => {
stats.failed_subscriptions += 1;
stats.malformed_pre_subscribe += 1;
return Ok(stats);
}
};
let (control_tx, control_rx) = flume::bounded(16);
let stop_control_reader = Arc::new(AtomicBool::new(false));
let mut session = match open_session_for_subscribe(
runtime,
&subscribe,
&mut writer,
limits,
control_rx,
&mut stats,
)? {
Some(session) => session,
None => return Ok(stats),
};
spawn_control_reader(
reader,
control_tx,
*limits,
subscribe.subscription_id.clone(),
Arc::clone(&stop_control_reader),
)?;
let _stop_reader_on_exit = StopReaderOnExit(stop_control_reader);
stats.served_subscriptions += 1;
run_subscription_loop(&mut writer, session.as_mut(), limits)?;
Ok(stats)
}
pub fn serve_tcp_subscription_listener<R>(
listener: TcpListener,
runtime: R,
config: &TcpSubscriptionServerConfig,
shutdown: &ShutdownHandle,
) -> Result<TcpSubscriptionServeStats, NetbatError>
where
R: SubscriptionSessionFactory + Send + Sync + 'static,
{
serve_tcp_subscription_listener_secured(
listener,
runtime,
config,
&TransportSecurity::Plaintext,
shutdown,
)
}
pub fn serve_tcp_subscription_listener_secured<R>(
listener: TcpListener,
runtime: R,
config: &TcpSubscriptionServerConfig,
security: &TransportSecurity,
shutdown: &ShutdownHandle,
) -> Result<TcpSubscriptionServeStats, NetbatError>
where
R: SubscriptionSessionFactory + Send + Sync + 'static,
{
listener.set_nonblocking(true)?;
let runtime = Arc::new(runtime);
let mut stats = TcpSubscriptionServeStats::default();
let limiter = Limiter::from_limit(config.connection_limit);
let (stats_tx, stats_rx) = stats_lane(config.connection_limit);
let mut workers: Vec<JoinHandle<()>> = Vec::new();
while !shutdown.is_shutdown() && limiter.accepting(stats.accepted_connections) {
drain_subscription_stats(&mut stats, &stats_rx);
workers.retain(|worker| !worker.is_finished());
match listener.accept() {
Ok((stream, _addr)) => {
let permit = match limiter.admit(shutdown, config.idle_sleep) {
Admission::Permit(permit) => permit,
Admission::Shutdown => break,
};
stats.accepted_connections += 1;
stream.set_nonblocking(false)?;
apply_timeouts(&stream, config.timeouts)?;
if let Some(worker) = dispatch_subscription(
stream, &runtime, config, security, &stats_tx, &mut stats, permit,
)? {
workers.push(worker);
}
}
Err(error) => match classify_accept_error(error.kind()) {
AcceptError::WouldBlock => thread::sleep(config.idle_sleep),
AcceptError::Interrupted => {}
AcceptError::Fatal => return Err(error.into()),
},
}
}
for worker in workers {
worker.join().map_err(|_| NetbatError::Io {
kind: io::ErrorKind::Other,
})?;
}
drain_subscription_stats(&mut stats, &stats_rx);
stats.shutdown_requested = shutdown.is_shutdown();
drop(listener);
Ok(stats)
}
fn dispatch_subscription<R>(
stream: TcpStream,
runtime: &Arc<R>,
config: &TcpSubscriptionServerConfig,
security: &TransportSecurity,
stats_tx: &flume::Sender<TcpSubscriptionServeStats>,
stats: &mut TcpSubscriptionServeStats,
permit: ConnectionPermit,
) -> Result<Option<JoinHandle<()>>, NetbatError>
where
R: SubscriptionSessionFactory + Send + Sync + 'static,
{
match config.dispatch {
SubscriptionDispatch::Sequential => {
serve_subscription_inline(stream, runtime.as_ref(), config, security, stats, permit)?;
Ok(None)
}
SubscriptionDispatch::Concurrent => Ok(Some(spawn_subscription_worker(
stream,
Arc::clone(runtime),
*config,
security.clone(),
stats_tx.clone(),
permit,
)?)),
}
}
fn serve_subscription_inline<R>(
stream: TcpStream,
runtime: &R,
config: &TcpSubscriptionServerConfig,
security: &TransportSecurity,
stats: &mut TcpSubscriptionServeStats,
permit: ConnectionPermit,
) -> Result<(), NetbatError>
where
R: SubscriptionSessionFactory,
{
let _permit = permit;
match serve_tcp_subscription_connection(stream, runtime, config, security) {
Ok(connection_stats) => merge_stats(stats, connection_stats),
Err(NetbatError::Io { .. }) => stats.connection_io_failures += 1,
Err(error) => return Err(error),
}
Ok(())
}
fn spawn_subscription_worker<R>(
stream: TcpStream,
runtime: Arc<R>,
config: TcpSubscriptionServerConfig,
security: TransportSecurity,
stats_tx: flume::Sender<TcpSubscriptionServeStats>,
permit: ConnectionPermit,
) -> Result<JoinHandle<()>, NetbatError>
where
R: SubscriptionSessionFactory + Send + Sync + 'static,
{
thread::Builder::new()
.name("netbat.sub-conn".to_owned())
.spawn(move || {
let _permit = permit;
let mut conn_stats = TcpSubscriptionServeStats::default();
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
serve_tcp_subscription_connection(stream, runtime.as_ref(), &config, &security)
}));
match outcome {
Ok(Ok(connection_stats)) => conn_stats = connection_stats,
Ok(Err(NetbatError::Io { .. })) => conn_stats.connection_io_failures += 1,
Ok(Err(_)) => conn_stats.failed_subscriptions += 1,
Err(_panic) => conn_stats.worker_panics += 1,
}
let _ = stats_tx.send(conn_stats);
})
.map_err(|error| NetbatError::Io { kind: error.kind() })
}
fn drain_subscription_stats(
total: &mut TcpSubscriptionServeStats,
stats_rx: &flume::Receiver<TcpSubscriptionServeStats>,
) {
while let Ok(partial) = stats_rx.try_recv() {
merge_stats(total, partial);
}
}
fn serve_tcp_subscription_connection(
stream: TcpStream,
runtime: &(impl SubscriptionSessionFactory + ?Sized),
config: &TcpSubscriptionServerConfig,
security: &TransportSecurity,
) -> Result<TcpSubscriptionServeStats, NetbatError> {
match security {
TransportSecurity::Plaintext => {
let reader = stream.try_clone().map_err(NetbatError::from)?;
serve_subscription_stream(reader, stream, runtime, &config.limits)
}
#[cfg(feature = "tls")]
TransportSecurity::Tls(tls) => {
stream_tcp_tls::serve_tls_subscription_connection(stream, tls, runtime, config)
}
}
}
fn open_session_for_subscribe<W: Write>(
runtime: &(impl SubscriptionSessionFactory + ?Sized),
subscribe: &SubscribeFrame,
writer: &mut W,
limits: &Limits,
control_rx: flume::Receiver<SessionControl>,
stats: &mut TcpSubscriptionServeStats,
) -> Result<Option<Box<dyn SubscriptionSession>>, NetbatError> {
let resume_bytes = maybe_cursor_bytes(subscribe.resume_cursor.clone());
match runtime.open_session(
subscribe.subscription_id.as_str(),
resume_bytes.as_deref(),
subscribe.client_window.get(),
control_rx,
) {
Ok(session) => Ok(Some(session)),
Err(error @ SubscriptionRuntimeError::InvalidConfig { .. }) => {
Err(map_runtime_error(&error))
}
Err(error) => {
stats.failed_subscriptions += 1;
let delivery = map_open_error(subscribe.subscription_id.as_str(), &error);
write_delivery(writer, &delivery, limits)?;
Ok(None)
}
}
}
fn run_subscription_loop(
writer: &mut impl Write,
session: &mut dyn SubscriptionSession,
limits: &Limits,
) -> Result<(), NetbatError> {
loop {
match session.poll(SUBSCRIPTION_POLL_INTERVAL) {
Ok(SessionPoll::Delivery(delivery)) => {
write_delivery(writer, &delivery, limits)?;
if terminal_delivery(&delivery) {
return Ok(());
}
}
Ok(SessionPoll::Blocked) => {}
Ok(SessionPoll::Ended) => return Ok(()),
Err(error) => {
return Err(map_runtime_error(&error));
}
}
}
}
fn write_delivery(
writer: &mut impl Write,
delivery: &SessionDelivery,
limits: &Limits,
) -> Result<(), NetbatError> {
let frame = delivery_to_frame(delivery, limits)?;
writer.write_all(&encode_stream_frame(&frame))?;
Ok(())
}
fn delivery_to_frame(
delivery: &SessionDelivery,
limits: &Limits,
) -> Result<StreamFrame, NetbatError> {
match delivery {
SessionDelivery::Event(event) => {
let subscription_id = subscription_token(&event.subscription_id, limits)?;
let schema =
PayloadSchemaRef::new(event.wire_payload_schema_ref.clone()).map_err(|_| {
NetbatError::MalformedStreamFrame {
reason: "payload schema ref invalid",
}
})?;
Ok(StreamFrame::SubEvent(SubEventFrame {
subscription_id,
delivery_index: delivery_index(event.delivery_index)?,
cursor_before: encode_maybe_cursor(&event.cursor_before),
cursor_after: encode_maybe_cursor(&event.cursor_after),
payload_schema_ref: schema,
payload: event.envelope_bytes.clone(),
}))
}
SessionDelivery::Watermark(watermark) => Ok(StreamFrame::SubWatermark(SubWatermarkFrame {
subscription_id: subscription_token(&watermark.subscription_id, limits)?,
delivery_index: delivery_index(watermark.delivery_index)?,
cursor_after: encode_required_cursor(&watermark.cursor_after),
})),
SessionDelivery::Error(error) => Ok(StreamFrame::SubErr(map_error_frame(error, limits)?)),
SessionDelivery::End(end) => Ok(StreamFrame::SubEnd(map_end_frame(end, limits)?)),
}
}
fn decode_subscribe_request(line: &[u8], limits: &Limits) -> Result<SubscribeFrame, NetbatError> {
match decode_stream_line(line, limits)? {
StreamFrame::Subscribe(frame) => Ok(frame),
StreamFrame::SubEvent(_)
| StreamFrame::SubWatermark(_)
| StreamFrame::SubAck(_)
| StreamFrame::SubCancel(_)
| StreamFrame::SubErr(_)
| StreamFrame::SubEnd(_) => Err(NetbatError::MalformedStreamFrame {
reason: "first stream frame must be SUBSCRIBE",
}),
}
}
fn maybe_cursor_bytes(cursor: MaybeCursor) -> Option<Vec<u8>> {
match cursor {
MaybeCursor::Absent => None,
MaybeCursor::Present(bytes) => Some(bytes.into_bytes()),
}
}
fn encode_maybe_cursor(cursor: &RuntimeCursor) -> MaybeCursor {
MaybeCursor::Present(CursorBytes::new(cursor.as_bytes().to_vec()))
}
fn encode_required_cursor(cursor: &RuntimeCursor) -> CursorBytes {
CursorBytes::new(cursor.as_bytes().to_vec())
}
struct StopReaderOnExit(Arc<AtomicBool>);
impl Drop for StopReaderOnExit {
fn drop(&mut self) {
self.0.store(true, Ordering::Release);
}
}
fn spawn_control_reader(
mut reader: impl Read + Send + 'static,
control_tx: flume::Sender<SessionControl>,
limits: Limits,
subscription_id: SubscriptionToken,
stop_reader: Arc<AtomicBool>,
) -> Result<(), NetbatError> {
thread::Builder::new()
.name("netbat.sub-control".to_owned())
.spawn(move || {
let _ = read_control_loop(
&mut reader,
&control_tx,
&limits,
&subscription_id,
&stop_reader,
);
})
.map_err(|error| NetbatError::Io { kind: error.kind() })?;
Ok(())
}
fn read_control_loop(
reader: &mut impl Read,
control_tx: &flume::Sender<SessionControl>,
limits: &Limits,
subscription_id: &SubscriptionToken,
stop_reader: &AtomicBool,
) -> Result<(), NetbatError> {
loop {
if stop_reader.load(Ordering::Acquire) {
break;
}
let line = match read_line(reader, limits.max_line_bytes) {
Ok(line) => line,
Err(NetbatError::Io { kind }) if timeout_kind(kind) => {
if stop_reader.load(Ordering::Acquire) {
break;
}
continue;
}
Err(NetbatError::EmptyStream) | Err(NetbatError::Io { .. }) => {
let _ = control_tx.send(SessionControl::Disconnected);
break;
}
Err(_) => {
let _ = control_tx.send(SessionControl::Malformed);
break;
}
};
let classified = classify_control_line(&line, limits, subscription_id);
let terminal = classified.terminal;
let _ = control_tx.send(classified.control);
if terminal {
break;
}
}
Ok(())
}
pub(super) struct ClassifiedControl {
pub(super) control: SessionControl,
pub(super) terminal: bool,
}
pub(super) fn classify_control_line(
line: &[u8],
limits: &Limits,
subscription_id: &SubscriptionToken,
) -> ClassifiedControl {
let frame = match decode_stream_line(line, limits) {
Ok(frame) => frame,
Err(_) => return malformed_control(),
};
match frame {
StreamFrame::SubAck(frame) => {
if frame.subscription_id.as_str() != subscription_id.as_str() {
return malformed_control();
}
let cursor = RuntimeCursor::from_bytes(frame.cursor_after.into_bytes());
ClassifiedControl {
control: SessionControl::Ack {
delivery_index: frame.delivery_index.get(),
cursor,
},
terminal: false,
}
}
StreamFrame::SubCancel(frame) => {
if frame.subscription_id.as_str() != subscription_id.as_str() {
return malformed_control();
}
ClassifiedControl {
control: SessionControl::Cancel,
terminal: true,
}
}
StreamFrame::Subscribe(_)
| StreamFrame::SubEvent(_)
| StreamFrame::SubWatermark(_)
| StreamFrame::SubErr(_)
| StreamFrame::SubEnd(_) => malformed_control(),
}
}
fn malformed_control() -> ClassifiedControl {
ClassifiedControl {
control: SessionControl::Malformed,
terminal: true,
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum AcceptError {
WouldBlock,
Interrupted,
Fatal,
}
fn classify_accept_error(kind: io::ErrorKind) -> AcceptError {
if kind == io::ErrorKind::WouldBlock {
AcceptError::WouldBlock
} else if kind == io::ErrorKind::Interrupted {
AcceptError::Interrupted
} else {
AcceptError::Fatal
}
}
fn timeout_kind(kind: io::ErrorKind) -> bool {
matches!(kind, io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut)
}
fn map_open_error(subscription_id: &str, error: &SubscriptionRuntimeError) -> SessionDelivery {
match error {
SubscriptionRuntimeError::UnknownSubscription { .. } => {
unknown_subscription_error(subscription_id)
}
SubscriptionRuntimeError::CursorInvalid { reason } => {
open_error_for_subscription(subscription_id, CURSOR_INVALID_CODE, reason)
}
SubscriptionRuntimeError::CursorMismatch { reason } => {
open_error_for_subscription(subscription_id, CURSOR_MISMATCH_CODE, reason)
}
SubscriptionRuntimeError::InvalidSubscriptionId { reason } => {
open_error_for_subscription(subscription_id, CURSOR_INVALID_CODE, reason)
}
SubscriptionRuntimeError::DuplicateSubscription { .. } => open_error_for_subscription(
subscription_id,
CURSOR_INVALID_CODE,
"duplicate subscription route",
),
SubscriptionRuntimeError::InvalidRoute { reason }
| SubscriptionRuntimeError::InvalidConfig { reason } => {
open_error_for_subscription(subscription_id, CURSOR_INVALID_CODE, reason)
}
SubscriptionRuntimeError::Store(_) => open_error_for_subscription(
subscription_id,
CURSOR_INVALID_CODE,
"store error during subscribe",
),
SubscriptionRuntimeError::EnvelopeEncoding(_) => open_error_for_subscription(
subscription_id,
CURSOR_INVALID_CODE,
"envelope encoding failed",
),
SubscriptionRuntimeError::Worker(_) => open_error_for_subscription(
subscription_id,
CURSOR_INVALID_CODE,
"subscription worker failed",
),
SubscriptionRuntimeError::AckInvalid { reason } => {
open_error_for_subscription(subscription_id, CURSOR_INVALID_CODE, reason)
}
}
}
fn open_error_for_subscription(
subscription_id: &str,
code: &'static str,
reason: &'static str,
) -> SessionDelivery {
SessionDelivery::Error(SessionError {
subscription_id: Some(subscription_id.to_owned()),
code,
last_delivered_cursor: None,
last_acked_cursor: None,
message: reason.as_bytes().to_vec(),
})
}
fn map_error_frame(error: &SessionError, limits: &Limits) -> Result<SubErrFrame, NetbatError> {
let subscription_id = match error.subscription_id.as_deref() {
Some(id) => subscription_token(id, limits)?,
None => subscription_token("unknown.invalid.v1", limits).map_err(|_| {
NetbatError::MalformedStreamFrame {
reason: "missing subscription id on stream error",
}
})?,
};
Ok(SubErrFrame {
subscription_id,
code: StreamReasonCode::new(error.code).map_err(|_| NetbatError::MalformedStreamFrame {
reason: "stream error code invalid",
})?,
last_delivered_cursor: optional_cursor(error.last_delivered_cursor.as_ref()),
last_acked_cursor: optional_cursor(error.last_acked_cursor.as_ref()),
message: error.message.clone(),
})
}
fn map_end_frame(end: &SessionEnd, limits: &Limits) -> Result<SubEndFrame, NetbatError> {
Ok(SubEndFrame {
subscription_id: subscription_token(&end.subscription_id, limits)?,
cursor_after: end
.cursor_after
.as_ref()
.map(encode_maybe_cursor)
.unwrap_or(MaybeCursor::Absent),
reason_code: StreamReasonCode::new(end.reason_code).map_err(|_| {
NetbatError::MalformedStreamFrame {
reason: "stream end code invalid",
}
})?,
})
}
fn subscription_token(id: &str, limits: &Limits) -> Result<SubscriptionToken, NetbatError> {
SubscriptionToken::new(id, limits)
}
fn delivery_index(value: u64) -> Result<DeliveryIndex, NetbatError> {
DeliveryIndex::new(value)
}
fn terminal_delivery(delivery: &SessionDelivery) -> bool {
matches!(
delivery,
SessionDelivery::Error(_) | SessionDelivery::End(_)
)
}
fn optional_cursor(cursor: Option<&RuntimeCursor>) -> MaybeCursor {
match cursor {
Some(cursor) => encode_maybe_cursor(cursor),
None => MaybeCursor::Absent,
}
}
fn merge_stats(total: &mut TcpSubscriptionServeStats, connection: TcpSubscriptionServeStats) {
total.served_subscriptions += connection.served_subscriptions;
total.failed_subscriptions += connection.failed_subscriptions;
total.malformed_pre_subscribe += connection.malformed_pre_subscribe;
total.runtime_failures += connection.runtime_failures;
total.connection_io_failures += connection.connection_io_failures;
total.worker_panics += connection.worker_panics;
#[cfg(feature = "tls")]
{
total.tls_handshake_failures += connection.tls_handshake_failures;
}
}
fn map_runtime_error(error: &SubscriptionRuntimeError) -> NetbatError {
NetbatError::MalformedStreamFrame {
reason: match error {
SubscriptionRuntimeError::Store(_) => "store error during stream poll",
SubscriptionRuntimeError::InvalidSubscriptionId { reason } => reason,
SubscriptionRuntimeError::DuplicateSubscription { .. } => {
"duplicate subscription route"
}
SubscriptionRuntimeError::InvalidRoute { reason }
| SubscriptionRuntimeError::InvalidConfig { reason } => reason,
SubscriptionRuntimeError::UnknownSubscription { .. } => "unknown subscription",
SubscriptionRuntimeError::CursorInvalid { reason } => reason,
SubscriptionRuntimeError::CursorMismatch { reason } => reason,
SubscriptionRuntimeError::EnvelopeEncoding(_) => "envelope encoding failed",
SubscriptionRuntimeError::Worker(_) => "subscription worker failed",
SubscriptionRuntimeError::AckInvalid { reason } => reason,
},
}
}
#[cfg(feature = "tls")]
#[path = "stream_tcp_tls.rs"]
mod stream_tcp_tls;
#[cfg(test)]
#[path = "stream_tcp_tests.rs"]
mod tests;