use crate::rtmp::poller::{Interest, Poller, RawHandle, Waker, WAKER_TOKEN};
use crate::rtmp::rtmp_scheduler::{RtmpScheduler, ServerResult};
use crate::rtmp::write_queue::{BackpressureLevel, FlushResult, WriteQueue};
use bytes::Bytes;
use log::{debug, error, info, warn};
use rml_rtmp::chunk_io::ChunkSerializer;
use rml_rtmp::handshake::{Handshake, HandshakeProcessResult, PeerType};
use rml_rtmp::messages::RtmpMessage;
use rml_rtmp::rml_amf0::Amf0Value;
use rml_rtmp::time::RtmpTimestamp;
use std::collections::{HashMap, HashSet, VecDeque};
use std::io::{self, Read};
use std::net::{Shutdown, TcpStream};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Condvar, Mutex, MutexGuard};
use std::time::{Duration, Instant};
const READ_BUFFER_SIZE: usize = 8192;
const POLL_TIMEOUT_MS: u64 = 100;
const CONNECTION_TIMEOUT_SECS: u64 = 60; const WATCHER_PING_IDLE_SECS: u64 = CONNECTION_TIMEOUT_SECS / 2;
const TIMEOUT_CHECK_INTERVAL: Duration = Duration::from_secs(1);
const GRACEFUL_SHUTDOWN_TIMEOUT_SECS: u64 = 5; const CLOSE_DRAIN_TIMEOUT: Duration = Duration::from_secs(GRACEFUL_SHUTDOWN_TIMEOUT_SECS);
const MAX_READ_PER_POLL: usize = 512 * 1024; pub(crate) const PUBLISHER_CHANNEL_CAPACITY: usize = 1024;
const MAX_PUBLISH_BYTES_PER_POLL: usize = MAX_READ_PER_POLL;
const MAX_PUBLISH_ITEMS_PER_POLL: usize = PUBLISHER_CHANNEL_CAPACITY;
pub(crate) const PUBLISHER_INGRESS_HIGH_WATER_BYTES: usize = 8 * 1024 * 1024;
const REGISTRATION_QUEUE_CAPACITY: usize = 1024;
const MAX_REGISTRATIONS_PER_POLL: usize = 128;
const DEFAULT_MAX_CONNECTIONS: usize = 10000; #[cfg(windows)]
const DEFAULT_MAX_CONNECTIONS_WINDOWS: usize = 8000; pub const CHANNEL_HEADROOM: usize = 256;
const DIRTY_IDS_SHRINK_MIN_CAPACITY: usize = 64;
const SPARSE_DRAINS_BEFORE_SHRINK: u32 = 64;
fn get_fd_limit() -> Option<usize> {
#[cfg(unix)]
{
use std::mem::MaybeUninit;
let mut rlim = MaybeUninit::<libc::rlimit>::uninit();
if unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, rlim.as_mut_ptr()) } == 0 {
let rlim = unsafe { rlim.assume_init() };
return Some(rlim.rlim_cur as usize);
}
None
}
#[cfg(windows)]
{
Some(DEFAULT_MAX_CONNECTIONS_WINDOWS)
}
#[cfg(not(any(unix, windows)))]
{
None
}
}
pub fn effective_max_connections(config_max: Option<usize>) -> usize {
let config_value = config_max.unwrap_or(DEFAULT_MAX_CONNECTIONS);
let result = if let Some(fd_limit) = get_fd_limit() {
let fd_based_limit = (fd_limit as f64 * 0.8) as usize;
config_value.min(fd_based_limit)
} else {
config_value
};
result.clamp(1, TOKEN_ID_MASK)
}
#[cfg(target_pointer_width = "64")]
type Generation = u32;
#[cfg(target_pointer_width = "32")]
type Generation = u16;
#[cfg(target_pointer_width = "64")]
const TOKEN_ID_BITS: u32 = 32;
#[cfg(target_pointer_width = "32")]
const TOKEN_ID_BITS: u32 = 16;
const TOKEN_ID_MASK: usize = (1usize << TOKEN_ID_BITS) - 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ConnectionToken {
pub id: usize,
pub generation: Generation,
}
impl ConnectionToken {
fn new(id: usize, generation: Generation) -> Self {
Self { id, generation }
}
fn to_poller_token(&self) -> usize {
((self.generation as usize) << TOKEN_ID_BITS) | (self.id & TOKEN_ID_MASK)
}
fn from_poller_token(token: usize) -> Self {
Self {
id: token & TOKEN_ID_MASK,
generation: (token >> TOKEN_ID_BITS) as Generation,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConnectionState {
Handshaking,
Active,
SlowClient,
Closing,
Closed,
}
impl ConnectionState {
#[cfg(test)]
pub fn is_active(&self) -> bool {
matches!(self, ConnectionState::Active | ConnectionState::SlowClient)
}
pub fn can_read(&self) -> bool {
matches!(
self,
ConnectionState::Handshaking | ConnectionState::Active | ConnectionState::SlowClient
)
}
pub fn can_write(&self) -> bool {
matches!(
self,
ConnectionState::Handshaking
| ConnectionState::Active
| ConnectionState::SlowClient
| ConnectionState::Closing
)
}
}
pub struct ReactorConnection {
token: ConnectionToken,
socket: TcpStream,
raw_handle: RawHandle,
state: ConnectionState,
write_queue: WriteQueue,
read_buffer: Vec<u8>,
handshake: Option<Handshake>,
last_read_activity: Instant,
last_write_activity: Instant,
current_interest: Interest,
close_deadline: Option<Instant>,
last_ping_at: Option<Instant>,
in_pending_flush: bool,
in_interest_dirty: bool,
}
impl ReactorConnection {
pub fn new(token: ConnectionToken, socket: TcpStream) -> io::Result<Self> {
socket.set_nonblocking(true)?;
if let Err(e) = socket.set_nodelay(true) {
log::warn!(
"Failed to set TCP_NODELAY on connection {}: {:?}",
token.id,
e
);
}
#[cfg(unix)]
let raw_handle = {
use std::os::unix::io::AsRawFd;
socket.as_raw_fd()
};
#[cfg(windows)]
let raw_handle = {
use std::os::windows::io::AsRawSocket;
socket.as_raw_socket()
};
let now = Instant::now();
Ok(Self {
token,
socket,
raw_handle,
state: ConnectionState::Handshaking,
write_queue: WriteQueue::new(),
read_buffer: vec![0u8; READ_BUFFER_SIZE],
handshake: Some(Handshake::new(PeerType::Server)),
last_read_activity: now,
last_write_activity: now,
current_interest: Interest::READABLE,
close_deadline: None,
last_ping_at: None,
in_pending_flush: false,
in_interest_dirty: false,
})
}
pub fn raw_handle(&self) -> RawHandle {
self.raw_handle
}
pub fn last_activity(&self) -> Instant {
self.last_read_activity.max(self.last_write_activity)
}
#[cfg_attr(not(test), allow(dead_code))] pub fn is_timed_out(&self, timeout: Duration) -> bool {
self.is_timed_out_at(Instant::now(), timeout)
}
pub fn is_timed_out_at(&self, now: Instant, timeout: Duration) -> bool {
now.saturating_duration_since(self.last_activity()) > timeout
}
pub fn is_ping_due_at(&self, now: Instant, idle: Duration) -> bool {
if self.state != ConnectionState::Active || self.close_deadline.is_some() {
return false;
}
if self.has_pending_writes() {
return false;
}
if now.saturating_duration_since(self.last_activity()) < idle {
return false;
}
match self.last_ping_at {
Some(pinged_at) => now.saturating_duration_since(pinged_at) >= idle,
None => true,
}
}
pub fn note_ping_queued(&mut self, now: Instant) {
self.last_ping_at = Some(now);
}
pub fn enqueue_data(
&mut self,
data: Bytes,
is_keyframe: bool,
is_sequence_header: bool,
is_video: bool,
droppable: bool,
now: Instant,
) -> bool {
let result =
self.write_queue
.enqueue(data, is_keyframe, is_sequence_header, is_video, droppable, now);
match self.write_queue.backpressure_level() {
BackpressureLevel::Critical => {
self.state = ConnectionState::Closing;
return false;
}
BackpressureLevel::High | BackpressureLevel::Warning => {
if self.state == ConnectionState::Active {
self.state = ConnectionState::SlowClient;
}
}
BackpressureLevel::Normal => {
if self.state == ConnectionState::SlowClient {
self.state = ConnectionState::Active;
}
}
}
result
}
pub fn enqueue_raw(&mut self, data: Vec<u8>) -> bool {
if !self.write_queue.enqueue(
Bytes::from(data),
false,
false,
false,
false,
Instant::now(),
) {
self.state = ConnectionState::Closing;
return false;
}
true
}
pub fn enqueue_ping(&mut self, data: Vec<u8>) -> bool {
if !self.write_queue.enqueue_ping(Bytes::from(data)) {
self.state = ConnectionState::Closing;
return false;
}
true
}
fn recover_from_slow_client(&mut self) {
if self.state == ConnectionState::SlowClient
&& self.write_queue.backpressure_level() == BackpressureLevel::Normal
{
self.state = ConnectionState::Active;
}
}
pub fn try_flush(&mut self) -> io::Result<bool> {
if self.write_queue.is_empty() {
self.recover_from_slow_client();
return Ok(false);
}
match self.write_queue.try_flush(&mut self.socket) {
Ok(FlushResult::Complete {
bytes_written,
ping_bytes_written,
}) => {
if bytes_written > ping_bytes_written {
self.last_write_activity = Instant::now();
}
self.recover_from_slow_client();
Ok(false)
}
Ok(FlushResult::WouldBlock {
bytes_written,
ping_bytes_written,
}) => {
if bytes_written > ping_bytes_written {
self.last_write_activity = Instant::now();
}
self.recover_from_slow_client();
Ok(false)
}
Ok(FlushResult::Closed) => Ok(true),
Err(e) => {
debug!("Connection {} write error: {:?}", self.token.id, e);
Err(e)
}
}
}
pub fn try_read(&mut self) -> io::Result<(Vec<u8>, bool)> {
let mut all_data = Vec::new();
loop {
if all_data.len() >= MAX_READ_PER_POLL {
return Ok((all_data, false)); }
match self.socket.read(&mut self.read_buffer) {
Ok(0) => {
return Ok((all_data, true));
}
Ok(n) => {
self.last_read_activity = Instant::now();
all_data.extend_from_slice(&self.read_buffer[..n]);
}
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
return Ok((all_data, false));
}
Err(e) => {
debug!("Connection {} read error: {:?}", self.token.id, e);
return Err(e);
}
}
}
}
pub fn process_handshake(
&mut self,
data: &[u8],
) -> (Option<Vec<u8>>, Option<Vec<u8>>, bool, bool) {
let handshake = match self.handshake.as_mut() {
Some(h) => h,
None => return (Some(data.to_vec()), None, true, false), };
match handshake.process_bytes(data) {
Ok(HandshakeProcessResult::InProgress { response_bytes }) => {
let response = if response_bytes.is_empty() {
None
} else {
Some(response_bytes)
};
(None, response, false, false)
}
Ok(HandshakeProcessResult::Completed {
response_bytes,
remaining_bytes,
}) => {
let response = if response_bytes.is_empty() {
None
} else {
Some(response_bytes)
};
let remaining = if remaining_bytes.is_empty() {
None
} else {
Some(remaining_bytes)
};
self.handshake = None;
self.state = ConnectionState::Active;
(remaining, response, true, false)
}
Err(e) => {
debug!("Connection {} handshake error: {:?}", self.token.id, e);
(None, None, false, true)
}
}
}
pub fn has_pending_writes(&self) -> bool {
!self.write_queue.is_empty()
}
pub fn desired_interest(&self) -> Interest {
let mut interest = if self.state.can_read() {
Interest::READABLE
} else {
Interest {
readable: false,
writable: false,
}
};
if self.has_pending_writes() {
interest = interest.add_writable();
}
interest
}
fn mark_pending_flush(&mut self) -> bool {
!std::mem::replace(&mut self.in_pending_flush, true)
}
fn take_pending_flush_mark(&mut self) -> bool {
std::mem::replace(&mut self.in_pending_flush, false)
}
fn mark_interest_dirty(&mut self) -> bool {
!std::mem::replace(&mut self.in_interest_dirty, true)
}
fn take_interest_dirty_mark(&mut self) -> bool {
std::mem::replace(&mut self.in_interest_dirty, false)
}
pub fn mark_closing(&mut self) {
self.state = ConnectionState::Closing;
}
pub fn condemn(&mut self, deadline: Instant) {
self.state = ConnectionState::Closing;
self.close_deadline = Some(deadline);
}
pub fn is_condemned(&self) -> bool {
self.close_deadline.is_some()
}
pub fn condemn_expired(&self, now: Instant) -> bool {
self.close_deadline.is_some_and(|deadline| now >= deadline)
}
pub fn mark_closed(&mut self) {
self.state = ConnectionState::Closed;
}
pub fn shutdown(&mut self) {
if let Err(e) = self.socket.shutdown(Shutdown::Both) {
debug!(
"Socket shutdown error (expected if already closed): {:?}",
e
);
}
self.mark_closed();
}
#[cfg(test)]
fn nodelay(&self) -> io::Result<bool> {
self.socket.nodelay()
}
fn pending_bytes(&self) -> usize {
self.write_queue.pending_bytes()
}
#[cfg(test)]
fn queued_bytes(&self) -> usize {
self.write_queue.pending_bytes()
}
}
pub enum PublisherFeed {
Raw(Vec<u8>),
Media {
tag_type: u8,
timestamp: RtmpTimestamp,
data: Bytes,
},
}
impl PublisherFeed {
pub(crate) fn ingress_len(&self) -> usize {
match self {
PublisherFeed::Raw(bytes) => bytes.len(),
PublisherFeed::Media { data, .. } => data.len(),
}
}
}
#[derive(Clone)]
pub enum PublisherSource {
Raw(crossbeam_channel::Receiver<Vec<u8>>),
Feed(crossbeam_channel::Receiver<PublisherFeed>),
}
pub(crate) struct StreamKeyClaim {
stream_keys: Arc<dashmap::DashSet<String>>,
stream_key: Option<String>,
}
impl StreamKeyClaim {
pub(crate) fn claim(
stream_keys: Arc<dashmap::DashSet<String>>,
stream_key: String,
) -> Result<Self, String> {
if stream_keys.insert(stream_key.clone()) {
Ok(Self {
stream_keys,
stream_key: Some(stream_key),
})
} else {
Err(stream_key)
}
}
pub(crate) fn key(&self) -> &str {
self.stream_key
.as_deref()
.expect("an armed claim always holds its key")
}
}
impl Drop for StreamKeyClaim {
fn drop(&mut self) {
if let Some(stream_key) = self.stream_key.take() {
self.stream_keys.remove(&stream_key);
}
}
}
#[derive(Debug)]
pub(crate) struct IngressClosed;
struct BudgetInner {
queued: usize,
closed: bool,
next_ticket: u64,
serving: u64,
}
pub(crate) struct IngressBudget {
inner: Mutex<BudgetInner>,
cv: Condvar,
high_water: usize,
}
impl IngressBudget {
pub(crate) fn new(high_water: usize) -> (IngressBudgetGuard, Arc<IngressBudget>) {
let budget = Arc::new(IngressBudget {
inner: Mutex::new(BudgetInner {
queued: 0,
closed: false,
next_ticket: 0,
serving: 0,
}),
cv: Condvar::new(),
high_water,
});
(
IngressBudgetGuard {
budget: budget.clone(),
},
budget,
)
}
fn lock(&self) -> MutexGuard<'_, BudgetInner> {
self.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub(crate) fn acquire(&self, len: usize) -> Result<(), IngressClosed> {
let mut inner = self.lock();
let ticket = inner.next_ticket;
inner.next_ticket += 1;
while !inner.closed
&& !(inner.serving == ticket
&& (inner.queued == 0 || inner.queued.saturating_add(len) <= self.high_water))
{
inner = self
.cv
.wait(inner)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
if inner.closed {
return Err(IngressClosed);
}
inner.serving += 1;
inner.queued = inner.queued.saturating_add(len);
let has_waiters = inner.next_ticket != inner.serving;
drop(inner);
if has_waiters {
self.cv.notify_all();
}
Ok(())
}
pub(crate) fn release(&self, len: usize) {
let mut inner = self.lock();
debug_assert!(
inner.queued >= len,
"ingress budget released more than was acquired ({} < {len})",
inner.queued
);
inner.queued = inner.queued.saturating_sub(len);
let has_waiters = inner.next_ticket != inner.serving;
drop(inner);
if has_waiters {
self.cv.notify_all();
}
}
fn close(&self) {
let mut inner = self.lock();
inner.closed = true;
drop(inner);
self.cv.notify_all();
}
#[cfg(test)]
pub(crate) fn queued_bytes(&self) -> usize {
self.lock().queued
}
#[cfg(test)]
pub(crate) fn waiting_acquires(&self) -> u64 {
let inner = self.lock();
inner.next_ticket - inner.serving
}
}
pub(crate) struct IngressBudgetGuard {
budget: Arc<IngressBudget>,
}
impl IngressBudgetGuard {
pub(crate) fn budget(&self) -> &Arc<IngressBudget> {
&self.budget
}
}
impl Drop for IngressBudgetGuard {
fn drop(&mut self) {
self.budget.close();
}
}
pub(crate) struct PublisherRegistration {
pub(crate) claim: StreamKeyClaim,
pub(crate) source: PublisherSource,
pub(crate) budget: IngressBudgetGuard,
}
pub(crate) struct RegistrationQueue {
alive: bool,
queue: VecDeque<PublisherRegistration>,
}
pub(crate) struct RegistrationHandoff {
queue: Mutex<RegistrationQueue>,
}
pub(crate) enum EnqueueRefused {
Closed(PublisherRegistration),
Full(PublisherRegistration),
}
impl RegistrationHandoff {
pub(crate) fn new() -> Self {
Self {
queue: Mutex::new(RegistrationQueue {
alive: true,
queue: VecDeque::new(),
}),
}
}
fn lock(&self) -> MutexGuard<'_, RegistrationQueue> {
self.queue
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub(crate) fn enqueue(
&self,
registration: PublisherRegistration,
) -> Result<(), EnqueueRefused> {
let mut queue = self.lock();
if !queue.alive {
Err(EnqueueRefused::Closed(registration))
} else if queue.queue.len() >= REGISTRATION_QUEUE_CAPACITY {
Err(EnqueueRefused::Full(registration))
} else {
queue.queue.push_back(registration);
Ok(())
}
}
pub(crate) fn close(&self) {
self.lock().alive = false;
}
fn drain_into(&self, batch: &mut Vec<PublisherRegistration>) -> bool {
let mut queue = self.lock();
let take = queue.queue.len().min(MAX_REGISTRATIONS_PER_POLL);
batch.extend(queue.queue.drain(..take));
!queue.queue.is_empty()
}
fn kill(&self) -> VecDeque<PublisherRegistration> {
let mut queue = self.lock();
queue.alive = false;
std::mem::take(&mut queue.queue)
}
}
pub(crate) struct RegistrationKillSwitch {
handoff: Arc<RegistrationHandoff>,
}
impl RegistrationKillSwitch {
pub(crate) fn arm(handoff: Arc<RegistrationHandoff>) -> Self {
Self { handoff }
}
pub(crate) fn handoff(&self) -> &RegistrationHandoff {
&self.handoff
}
}
impl Drop for RegistrationKillSwitch {
fn drop(&mut self) {
let leftovers = self.handoff.kill();
drop(leftovers);
}
}
pub struct PublisherState {
pub(crate) claim: StreamKeyClaim,
pub source: PublisherSource,
pub(crate) budget: IngressBudgetGuard,
}
type OutboundWrite = (usize, Bytes, bool, bool, bool, bool);
fn collect_server_results(
server_results: &mut Vec<ServerResult>,
packets_to_write: &mut Vec<OutboundWrite>,
ids_to_close: &mut Vec<usize>,
) {
for result in server_results.drain(..) {
match result {
ServerResult::OutboundPacket {
target_connection_id,
bytes,
can_be_dropped,
is_keyframe,
is_sequence_header,
is_video,
} => {
packets_to_write.push((
target_connection_id,
bytes,
is_keyframe,
is_sequence_header,
is_video,
can_be_dropped,
));
}
ServerResult::DisconnectConnection {
connection_id: close_id,
} => {
ids_to_close.push(close_id);
}
}
}
}
pub enum HandleResult {
Disconnect(usize),
}
pub struct Reactor {
poller: Poller,
connections: slab::Slab<ReactorConnection>,
generations: HashMap<usize, Generation>,
scheduler: RtmpScheduler,
publishers: slab::Slab<PublisherState>,
status: Arc<AtomicUsize>,
max_connections: usize,
pending_flush_ids: Vec<usize>,
read_pending: HashSet<usize>,
interest_dirty_ids: Vec<usize>,
dirty_drain_scratch: Vec<usize>,
sparse_drain_streak: u32,
packets_buffer: Vec<OutboundWrite>,
ids_to_close_buffer: Vec<usize>,
server_results_buffer: Vec<ServerResult>,
results_buffer: Vec<HandleResult>,
last_timeout_check: Instant,
}
#[cfg_attr(not(test), allow(dead_code))] const STATUS_RUN: usize = 1;
const STATUS_END: usize = 2;
impl Reactor {
pub fn new(
gop_limit: usize,
max_connections: Option<usize>,
status: Arc<AtomicUsize>,
) -> io::Result<Self> {
let poller = Poller::new()?;
let effective_max = effective_max_connections(max_connections);
Ok(Self {
poller,
connections: slab::Slab::with_capacity(1024),
generations: HashMap::new(),
scheduler: RtmpScheduler::new(gop_limit),
publishers: slab::Slab::with_capacity(64),
status,
max_connections: effective_max,
pending_flush_ids: Vec::with_capacity(256),
read_pending: HashSet::new(),
interest_dirty_ids: Vec::with_capacity(256),
dirty_drain_scratch: Vec::with_capacity(256),
sparse_drain_streak: 0,
packets_buffer: Vec::with_capacity(64),
ids_to_close_buffer: Vec::with_capacity(16),
server_results_buffer: Vec::with_capacity(64),
results_buffer: Vec::with_capacity(16),
last_timeout_check: Instant::now(),
})
}
pub fn add_connection(&mut self, socket: TcpStream) -> io::Result<ConnectionToken> {
if self.connections.len() >= self.max_connections {
return Err(io::Error::new(
io::ErrorKind::ConnectionRefused,
format!(
"max connections limit reached ({}/{})",
self.connections.len(),
self.max_connections
),
));
}
let entry = self.connections.vacant_entry();
let id = entry.key();
let generation = self.generations.entry(id).or_insert(0);
*generation = generation.wrapping_add(1);
let token = ConnectionToken::new(id, *generation);
let conn = ReactorConnection::new(token, socket)?;
let poller_token = token.to_poller_token();
self.poller
.register(conn.raw_handle(), poller_token, Interest::READABLE)?;
entry.insert(conn);
debug!("Connection {} added (generation {})", id, token.generation);
Ok(token)
}
fn close_connection_after_flush(&mut self, id: usize) {
let now = Instant::now();
let remove = match self.connections.get_mut(id) {
None => true, Some(conn) => {
if conn.condemn_expired(now) {
true
} else {
match conn.try_flush() {
Err(_) | Ok(true) => true,
Ok(false) if !conn.has_pending_writes() => true,
Ok(false) => {
if !conn.is_condemned() {
conn.condemn(now + CLOSE_DRAIN_TIMEOUT);
}
false
}
}
}
}
};
if remove {
self.remove_connection(id);
} else {
if let Some(conn) = self.connections.get_mut(id) {
if conn.mark_interest_dirty() {
self.interest_dirty_ids.push(id);
}
}
}
}
pub fn remove_connection(&mut self, id: usize) {
self.read_pending.remove(&id);
if let Some(conn) = self.connections.try_remove(id) {
if let Err(e) = self.poller.deregister(conn.raw_handle()) {
debug!(
"Failed to deregister connection {} from poller: {:?}",
id, e
);
}
self.scheduler.notify_connection_closed(id);
debug!(
"Connection {} removed (generation {})",
id, conn.token.generation
);
}
}
pub fn add_publisher(&mut self, registration: PublisherRegistration) -> Option<usize> {
let PublisherRegistration {
claim,
source,
budget,
} = registration;
let entry = self.publishers.vacant_entry();
let id = entry.key();
if self.scheduler.new_channel(claim.key().to_string(), id) {
let state = entry.insert(PublisherState {
claim,
source,
budget,
});
debug!("Publisher {} added for stream: {}", id, state.claim.key());
Some(id)
} else {
None
}
}
pub fn remove_publisher(&mut self, id: usize) {
if let Some(pub_state) = self.publishers.try_remove(id) {
self.scheduler.notify_publisher_closed(id);
debug!("Publisher {} removed", id);
drop(pub_state);
}
}
fn update_interest(&mut self, id: usize) -> io::Result<()> {
if let Some(conn) = self.connections.get_mut(id) {
let desired = conn.desired_interest();
if desired != conn.current_interest {
self.poller
.modify(conn.raw_handle(), conn.token.to_poller_token(), desired)?;
conn.current_interest = desired;
}
}
Ok(())
}
fn validate_connection(&self, poller_token: usize) -> Option<usize> {
let token = ConnectionToken::from_poller_token(poller_token);
if let Some(conn) = self.connections.get(token.id) {
if conn.token.generation == token.generation {
return Some(token.id);
}
debug!(
"Stale event for connection {}: expected gen {}, got {}",
token.id, conn.token.generation, token.generation
);
}
None
}
fn handle_readable(&mut self, id: usize) -> Vec<HandleResult> {
self.results_buffer.clear();
self.packets_buffer.clear();
self.ids_to_close_buffer.clear();
let (data, should_close) = match self.read_connection_data(id) {
Some(result) => result,
None => return std::mem::take(&mut self.results_buffer),
};
self.process_connection_data(id, &data);
self.write_pending_packets();
for close_id in self.ids_to_close_buffer.drain(..) {
self.results_buffer.push(HandleResult::Disconnect(close_id));
}
if should_close {
self.results_buffer.push(HandleResult::Disconnect(id));
}
let self_disconnect = self.results_buffer.iter().any(|r| {
let HandleResult::Disconnect(close_id) = r;
*close_id == id
});
if !should_close && !self_disconnect && data.len() >= MAX_READ_PER_POLL {
self.read_pending.insert(id);
}
std::mem::take(&mut self.results_buffer)
}
fn resume_capped_reads(
&mut self,
resume_ids: Vec<usize>,
read_this_pass: &[usize],
ids_to_close: &mut Vec<usize>,
) {
for id in resume_ids {
if read_this_pass.contains(&id) || ids_to_close.contains(&id) {
continue;
}
for result in self.handle_readable(id) {
let HandleResult::Disconnect(close_id) = result;
ids_to_close.push(close_id);
}
}
}
fn read_connection_data(&mut self, id: usize) -> Option<(Vec<u8>, bool)> {
let conn = match self.connections.get_mut(id) {
Some(c) if c.state.can_read() => c,
_ => return None,
};
match conn.try_read() {
Ok((data, close)) => {
if data.is_empty() {
if close {
self.results_buffer.push(HandleResult::Disconnect(id));
}
return None;
}
Some((data, close))
}
Err(_) => {
self.results_buffer.push(HandleResult::Disconnect(id));
None
}
}
}
fn process_connection_data(&mut self, id: usize, data: &[u8]) {
let conn = match self.connections.get_mut(id) {
Some(c) => c,
None => return,
};
let state = conn.state;
if state == ConnectionState::Handshaking {
self.process_handshake_data(id, data);
} else {
self.process_normal_data(id, data);
}
}
fn process_handshake_data(&mut self, id: usize, data: &[u8]) {
let conn = match self.connections.get_mut(id) {
Some(c) => c,
None => return,
};
let (remaining, response, completed, error) = conn.process_handshake(data);
if error {
self.results_buffer.push(HandleResult::Disconnect(id));
return;
}
if let Some(resp) = response {
if !conn.enqueue_raw(resp) {
self.results_buffer.push(HandleResult::Disconnect(id));
return;
}
if conn.mark_pending_flush() {
self.pending_flush_ids.push(id);
}
if conn.mark_interest_dirty() {
self.interest_dirty_ids.push(id);
}
}
if completed {
debug!("Connection {} handshake completed", id);
}
if let Some(remaining_data) = remaining {
if !remaining_data.is_empty() {
self.process_scheduler_results(id, &remaining_data);
}
}
}
fn process_normal_data(&mut self, id: usize, data: &[u8]) {
self.process_scheduler_results(id, data);
}
fn process_scheduler_results(&mut self, id: usize, data: &[u8]) {
let backlog = self
.connections
.get(id)
.map(|conn| conn.pending_bytes())
.unwrap_or(0);
self.server_results_buffer.clear();
match self.scheduler.bytes_received_with_backlog(
id,
data,
backlog,
&mut self.server_results_buffer,
) {
Ok(()) => {
for result in self.server_results_buffer.drain(..) {
match result {
ServerResult::OutboundPacket {
target_connection_id,
bytes,
can_be_dropped,
is_keyframe,
is_sequence_header,
is_video,
} => {
self.packets_buffer.push((
target_connection_id,
bytes,
is_keyframe,
is_sequence_header,
is_video,
can_be_dropped,
));
}
ServerResult::DisconnectConnection {
connection_id: close_id,
} => {
self.ids_to_close_buffer.push(close_id);
}
}
}
}
Err(e) => {
debug!("Connection {} scheduler error: {}", id, e);
self.results_buffer.push(HandleResult::Disconnect(id));
}
}
}
fn write_pending_packets(&mut self) {
let now = Instant::now();
for (target_id, data, is_keyframe, is_sequence_header, is_video, droppable) in
self.packets_buffer.drain(..)
{
if let Some(target_conn) = self.connections.get_mut(target_id) {
if target_conn.is_condemned() {
continue;
}
let enqueued = target_conn.enqueue_data(
data,
is_keyframe,
is_sequence_header,
is_video,
droppable,
now,
);
if enqueued {
if target_conn.mark_pending_flush() {
self.pending_flush_ids.push(target_id);
}
if target_conn.mark_interest_dirty() {
self.interest_dirty_ids.push(target_id);
}
} else {
self.ids_to_close_buffer.push(target_id);
}
}
}
}
fn handle_writable(&mut self, id: usize) -> Option<HandleResult> {
let conn = match self.connections.get_mut(id) {
Some(c) if c.state.can_write() => c,
_ => return None,
};
match conn.try_flush() {
Ok(true) => Some(HandleResult::Disconnect(id)),
Ok(false) => {
if !conn.has_pending_writes() {
if conn.is_condemned() {
return Some(HandleResult::Disconnect(id));
}
if conn.mark_interest_dirty() {
self.interest_dirty_ids.push(id);
}
}
None
}
Err(_) => Some(HandleResult::Disconnect(id)),
}
}
fn dispatch_publish_bytes(
&mut self,
pub_id: usize,
bytes: Vec<u8>,
packets_to_write: &mut Vec<OutboundWrite>,
ids_to_close: &mut Vec<usize>,
) -> bool {
self.server_results_buffer.clear();
match self
.scheduler
.publish_bytes_received(pub_id, bytes, &mut self.server_results_buffer)
{
Ok(()) => {
collect_server_results(
&mut self.server_results_buffer,
packets_to_write,
ids_to_close,
);
true
}
Err(e) => {
warn!(
"Publisher {} rejected with a fatal session error and will be removed: {}",
pub_id, e
);
false
}
}
}
fn process_publishers(&mut self) -> (Vec<usize>, bool) {
let mut publisher_ids_to_remove = Vec::new();
let mut packets_to_write = Vec::new();
let mut ids_to_close = Vec::new();
let mut budget_exhausted = false;
let publisher_ids: Vec<usize> = self.publishers.iter().map(|(id, _)| id).collect();
for pub_id in publisher_ids {
let (source, ingress_budget) = {
let pub_state = match self.publishers.get(pub_id) {
Some(p) => p,
None => continue,
};
(
pub_state.source.clone(),
pub_state.budget.budget().clone(),
)
};
let mut items = 0usize;
let mut bytes_drained = 0usize;
match source {
PublisherSource::Raw(receiver) => loop {
match receiver.try_recv() {
Ok(bytes) => {
items += 1;
bytes_drained += bytes.len();
if !self.dispatch_publish_bytes(
pub_id,
bytes,
&mut packets_to_write,
&mut ids_to_close,
) {
let mut results = self.scheduler.abort_publisher_watchers(pub_id);
collect_server_results(
&mut results,
&mut packets_to_write,
&mut ids_to_close,
);
publisher_ids_to_remove.push(pub_id);
break;
}
if items >= MAX_PUBLISH_ITEMS_PER_POLL
|| bytes_drained >= MAX_PUBLISH_BYTES_PER_POLL
{
budget_exhausted = true;
break;
}
}
Err(crossbeam_channel::TryRecvError::Empty) => break,
Err(crossbeam_channel::TryRecvError::Disconnected) => {
debug!("Publisher {} disconnected", pub_id);
self.send_delete_stream(
pub_id,
&mut packets_to_write,
&mut ids_to_close,
);
publisher_ids_to_remove.push(pub_id);
break;
}
}
},
PublisherSource::Feed(receiver) => loop {
match receiver.try_recv() {
Ok(PublisherFeed::Raw(bytes)) => {
items += 1;
bytes_drained += bytes.len();
if !self.dispatch_publish_bytes(
pub_id,
bytes,
&mut packets_to_write,
&mut ids_to_close,
) {
let mut results = self.scheduler.abort_publisher_watchers(pub_id);
collect_server_results(
&mut results,
&mut packets_to_write,
&mut ids_to_close,
);
publisher_ids_to_remove.push(pub_id);
break;
}
if items >= MAX_PUBLISH_ITEMS_PER_POLL
|| bytes_drained >= MAX_PUBLISH_BYTES_PER_POLL
{
budget_exhausted = true;
break;
}
}
Ok(PublisherFeed::Media {
tag_type,
timestamp,
data,
}) => {
items += 1;
bytes_drained += data.len();
self.server_results_buffer.clear();
self.scheduler.publish_media_received(
pub_id,
tag_type,
timestamp,
data,
&mut self.server_results_buffer,
);
collect_server_results(
&mut self.server_results_buffer,
&mut packets_to_write,
&mut ids_to_close,
);
if items >= MAX_PUBLISH_ITEMS_PER_POLL
|| bytes_drained >= MAX_PUBLISH_BYTES_PER_POLL
{
budget_exhausted = true;
break;
}
}
Err(crossbeam_channel::TryRecvError::Empty) => break,
Err(crossbeam_channel::TryRecvError::Disconnected) => {
debug!("Publisher {} disconnected", pub_id);
self.send_delete_stream(
pub_id,
&mut packets_to_write,
&mut ids_to_close,
);
publisher_ids_to_remove.push(pub_id);
break;
}
}
},
}
if bytes_drained > 0 {
ingress_budget.release(bytes_drained);
}
}
self.packets_buffer.append(&mut packets_to_write);
self.write_pending_packets();
ids_to_close.append(&mut self.ids_to_close_buffer);
for close_id in ids_to_close {
self.close_connection_after_flush(close_id);
}
(publisher_ids_to_remove, budget_exhausted)
}
fn send_delete_stream(
&mut self,
pub_id: usize,
packets: &mut Vec<OutboundWrite>,
ids_to_close: &mut Vec<usize>,
) {
let mut arguments = Vec::new();
arguments.push(Amf0Value::Number(1.0));
let delete_stream_cmd = RtmpMessage::Amf0Command {
command_name: "deleteStream".to_string(),
transaction_id: 4.0,
command_object: Amf0Value::Null,
additional_arguments: arguments,
}
.into_message_payload(RtmpTimestamp { value: 0 }, 1);
if let Ok(payload) = delete_stream_cmd {
let mut serializer = ChunkSerializer::new();
if let Ok(packet) = serializer.serialize(&payload, false, true) {
self.server_results_buffer.clear();
match self.scheduler.publish_bytes_received(
pub_id,
packet.bytes,
&mut self.server_results_buffer,
) {
Ok(()) => {
collect_server_results(
&mut self.server_results_buffer,
packets,
ids_to_close,
);
}
Err(e) => {
log::warn!(
"Failed to process deleteStream command for publisher {}: {:?}",
pub_id,
e
);
}
}
}
}
}
fn flush_pending(&mut self) -> Vec<usize> {
let mut ids_to_close = Vec::new();
debug_assert!(self.dirty_drain_scratch.is_empty());
std::mem::swap(&mut self.dirty_drain_scratch, &mut self.pending_flush_ids);
for &id in &self.dirty_drain_scratch {
if let Some(conn) = self.connections.get_mut(id) {
if !conn.take_pending_flush_mark() {
continue;
}
if conn.has_pending_writes() {
match conn.try_flush() {
Ok(true) | Err(_) => {
ids_to_close.push(id);
}
Ok(false) if !conn.has_pending_writes() && conn.is_condemned() => {
ids_to_close.push(id);
}
Ok(false) => {
if conn.mark_interest_dirty() {
self.interest_dirty_ids.push(id);
}
}
}
} else if conn.is_condemned() {
ids_to_close.push(id);
} else {
if conn.mark_interest_dirty() {
self.interest_dirty_ids.push(id);
}
}
}
}
let drained_len = self.dirty_drain_scratch.len();
self.dirty_drain_scratch.clear();
self.shrink_drain_scratch_if_sparse(drained_len);
ids_to_close
}
fn shrink_drain_scratch_if_sparse(&mut self, drained_len: usize) {
if drained_len >= self.dirty_drain_scratch.capacity() / 4 {
self.sparse_drain_streak = 0;
return;
}
self.sparse_drain_streak = self.sparse_drain_streak.saturating_add(1);
if self.sparse_drain_streak >= SPARSE_DRAINS_BEFORE_SHRINK
&& self.dirty_drain_scratch.capacity() > DIRTY_IDS_SHRINK_MIN_CAPACITY
{
self.dirty_drain_scratch
.shrink_to((drained_len * 2).max(DIRTY_IDS_SHRINK_MIN_CAPACITY));
}
}
fn check_timeouts(&mut self) -> Vec<usize> {
let now = Instant::now();
if now.saturating_duration_since(self.last_timeout_check) < TIMEOUT_CHECK_INTERVAL {
return Vec::new();
}
self.last_timeout_check = now;
let timeout = Duration::from_secs(CONNECTION_TIMEOUT_SECS);
let ping_idle = Duration::from_secs(WATCHER_PING_IDLE_SECS);
let mut timed_out = Vec::new();
let mut ping_due = Vec::new();
for (id, conn) in self.connections.iter() {
if conn.is_timed_out_at(now, timeout) {
debug!("Connection {} timed out", id);
timed_out.push(id);
} else if conn.condemn_expired(now) {
debug!("Connection {} close-drain deadline expired", id);
timed_out.push(id);
} else if conn.is_ping_due_at(now, ping_idle) {
ping_due.push(id);
}
}
for id in ping_due {
let Some(packet) = self.scheduler.ping_watcher(id) else {
continue;
};
let Some(conn) = self.connections.get_mut(id) else {
continue;
};
if conn.enqueue_ping(packet.bytes) {
debug!("Connection {} idle for {WATCHER_PING_IDLE_SECS}s; ping queued", id);
conn.note_ping_queued(now);
if conn.mark_pending_flush() {
self.pending_flush_ids.push(id);
}
if conn.mark_interest_dirty() {
self.interest_dirty_ids.push(id);
}
} else {
timed_out.push(id);
}
}
timed_out
}
fn update_dirty_interests(&mut self) -> Vec<usize> {
debug_assert!(self.dirty_drain_scratch.is_empty());
std::mem::swap(&mut self.dirty_drain_scratch, &mut self.interest_dirty_ids);
let mut ids_to_close = Vec::new();
for i in 0..self.dirty_drain_scratch.len() {
let id = self.dirty_drain_scratch[i];
let marked = match self.connections.get_mut(id) {
Some(conn) => conn.take_interest_dirty_mark(),
None => false,
};
if !marked {
continue;
}
if let Err(e) = self.update_interest(id) {
log::warn!(
"Failed to update interest for connection {}: {:?}; closing (queued writes would otherwise stall)",
id, e
);
ids_to_close.push(id);
}
}
let drained_len = self.dirty_drain_scratch.len();
self.dirty_drain_scratch.clear();
self.shrink_drain_scratch_if_sparse(drained_len);
ids_to_close
}
pub fn run(
&mut self,
connection_receiver: crossbeam_channel::Receiver<TcpStream>,
registrations: &RegistrationHandoff,
waker: Option<Waker>,
) {
info!("Reactor started");
if let Some(waker) = &waker {
if let Err(e) =
self.poller
.register(waker.raw_handle(), WAKER_TOKEN, Interest::READABLE)
{
error!(
"Failed to register reactor waker (falling back to poll timeout): {:?}",
e
);
}
}
let poll_timeout = Duration::from_millis(POLL_TIMEOUT_MS);
let mut publishers_pending = false;
let mut events = Vec::new();
let mut registration_batch: Vec<PublisherRegistration> = Vec::new();
loop {
if self.status.load(Ordering::Acquire) == STATUS_END {
info!("Reactor received stop signal");
break;
}
while let Ok(socket) = connection_receiver.try_recv() {
match self.add_connection(socket) {
Ok(token) => {
debug!("New connection added: {:?}", token);
}
Err(e) => {
error!("Failed to add connection: {:?}", e);
}
}
}
let mut new_publisher_added = false;
let registrations_pending = registrations.drain_into(&mut registration_batch);
for registration in registration_batch.drain(..) {
if self.add_publisher(registration).is_some() {
new_publisher_added = true;
}
}
let poll_wait = if new_publisher_added
|| publishers_pending
|| registrations_pending
|| !self.read_pending.is_empty()
{
Duration::ZERO
} else {
poll_timeout
};
if let Err(e) = self.poller.poll(Some(poll_wait), &mut events) {
error!("Poller error: {:?}", e);
continue;
}
let resume_ids: Vec<usize> = if self.read_pending.is_empty() {
Vec::new()
} else {
self.read_pending.drain().collect()
};
let mut ids_to_close = Vec::new();
let mut read_ids: Vec<usize> = Vec::new();
for event in &events {
let poller_token = event.token;
if poller_token == WAKER_TOKEN {
if let Some(waker) = &waker {
waker.drain();
}
continue;
}
let Some(id) = self.validate_connection(poller_token) else {
continue;
};
if event.is_error() || event.is_hangup() {
ids_to_close.push(id);
continue;
}
if event.is_readable() {
if !resume_ids.is_empty() {
read_ids.push(id);
}
let results = self.handle_readable(id);
for result in results {
let HandleResult::Disconnect(close_id) = result;
ids_to_close.push(close_id);
}
}
if event.is_writable() {
if let Some(HandleResult::Disconnect(close_id)) = self.handle_writable(id) {
ids_to_close.push(close_id);
}
}
}
self.resume_capped_reads(resume_ids, &read_ids, &mut ids_to_close);
let (publisher_ids_to_remove, budget_exhausted) = self.process_publishers();
publishers_pending = budget_exhausted;
for pub_id in publisher_ids_to_remove {
self.remove_publisher(pub_id);
}
let flush_closes = self.flush_pending();
ids_to_close.extend(flush_closes);
let interest_closes = self.update_dirty_interests();
ids_to_close.extend(interest_closes);
let timed_out = self.check_timeouts();
ids_to_close.extend(timed_out);
ids_to_close.sort_unstable();
ids_to_close.dedup();
for id in ids_to_close {
self.close_connection_after_flush(id);
}
}
self.graceful_shutdown();
info!("Reactor stopped");
}
fn graceful_shutdown(&mut self) {
info!("Starting graceful shutdown...");
let deadline = Instant::now() + Duration::from_secs(GRACEFUL_SHUTDOWN_TIMEOUT_SECS);
for (_, conn) in self.connections.iter_mut() {
conn.mark_closing();
}
while Instant::now() < deadline {
let mut all_flushed = true;
for (_, conn) in self.connections.iter_mut() {
if conn.has_pending_writes() {
all_flushed = false;
if let Err(e) = conn.try_flush() {
debug!("Failed to flush connection during shutdown: {:?}", e);
}
}
}
if all_flushed {
break;
}
std::thread::sleep(Duration::from_millis(10));
}
for (_, conn) in self.connections.iter_mut() {
conn.shutdown();
}
info!("Graceful shutdown complete");
}
#[cfg(test)]
pub fn is_interest_dirty(&self, id: usize) -> bool {
let flagged = self
.connections
.get(id)
.is_some_and(|conn| conn.in_interest_dirty);
flagged && self.interest_dirty_ids.contains(&id)
}
#[cfg(test)]
pub fn is_pending_flush(&self, id: usize) -> bool {
let flagged = self
.connections
.get(id)
.is_some_and(|conn| conn.in_pending_flush);
flagged && self.pending_flush_ids.contains(&id)
}
#[cfg(test)]
pub fn drain_interest_dirty(&mut self) -> Vec<usize> {
let ids = std::mem::take(&mut self.interest_dirty_ids);
ids.into_iter()
.filter(|&id| {
self.connections
.get_mut(id)
.is_some_and(|conn| conn.take_interest_dirty_mark())
})
.collect()
}
#[cfg(test)]
pub fn mark_pending_flush_for_test(&mut self, id: usize) {
if let Some(conn) = self.connections.get_mut(id) {
if conn.mark_pending_flush() {
self.pending_flush_ids.push(id);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_budget() -> IngressBudgetGuard {
IngressBudget::new(PUBLISHER_INGRESS_HIGH_WATER_BYTES).0
}
fn send_acquired(
budget: &IngressBudget,
feed_tx: &crossbeam_channel::Sender<PublisherFeed>,
feed: PublisherFeed,
) {
budget
.acquire(feed.ingress_len())
.expect("the test budget must be open");
feed_tx.send(feed).expect("the test channel must accept");
}
#[test]
fn test_connection_state_transitions() {
assert!(ConnectionState::Handshaking.can_read());
assert!(ConnectionState::Handshaking.can_write());
assert!(!ConnectionState::Handshaking.is_active());
assert!(ConnectionState::Active.can_read());
assert!(ConnectionState::Active.can_write());
assert!(ConnectionState::Active.is_active());
assert!(ConnectionState::SlowClient.is_active());
assert!(!ConnectionState::Closing.can_read());
assert!(ConnectionState::Closing.can_write());
assert!(!ConnectionState::Closed.can_read());
assert!(!ConnectionState::Closed.can_write());
}
#[test]
fn test_interest_desired() {
use std::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let client = TcpStream::connect(addr).expect("Failed to connect");
let token = ConnectionToken::new(0, 1);
let conn = ReactorConnection::new(token, client).expect("Failed to create connection");
assert_eq!(conn.desired_interest(), Interest::READABLE);
}
#[test]
fn test_graceful_shutdown_flushes_data() {
use std::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let client = TcpStream::connect(addr).expect("Failed to connect");
let (server_socket, _) = listener.accept().expect("Failed to accept");
let token = ConnectionToken::new(0, 1);
let mut conn =
ReactorConnection::new(token, server_socket).expect("Failed to create connection");
conn.state = ConnectionState::Active;
let test_data = b"Hello, World!";
conn.enqueue_data(Bytes::from_static(test_data), false, false, false, true, Instant::now());
assert!(conn.has_pending_writes());
let _ = conn.try_flush();
client
.set_nonblocking(false)
.expect("Failed to set blocking");
let mut buf = vec![0u8; 100];
use std::time::Duration;
client
.set_read_timeout(Some(Duration::from_millis(100)))
.expect("Failed to set timeout");
match client.peek(&mut buf) {
Ok(n) if n > 0 => {
assert!(n >= test_data.len());
}
_ => {
}
}
}
#[test]
fn test_accepted_socket_has_tcp_nodelay() {
use std::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let _client = TcpStream::connect(addr).expect("Failed to connect");
let (server, _) = listener.accept().expect("Failed to accept");
let token = ConnectionToken::new(0, 1);
let conn = ReactorConnection::new(token, server).expect("Failed to create connection");
assert!(
conn.nodelay().expect("nodelay query failed"),
"accepted subscriber socket must have TCP_NODELAY enabled"
);
}
#[test]
fn test_connection_timeout_detection() {
use std::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let client = TcpStream::connect(addr).expect("Failed to connect");
let token = ConnectionToken::new(0, 1);
let conn = ReactorConnection::new(token, client).expect("Failed to create connection");
assert!(!conn.is_timed_out(Duration::from_secs(60)));
assert!(conn.is_timed_out(Duration::from_nanos(1)));
}
#[test]
fn test_is_timed_out_at_uses_hoisted_now() {
use std::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let client = TcpStream::connect(addr).expect("Failed to connect");
let token = ConnectionToken::new(0, 1);
let conn = ReactorConnection::new(token, client).expect("Failed to create connection");
let timeout = Duration::from_secs(CONNECTION_TIMEOUT_SECS);
let base = conn.last_activity();
assert!(!conn.is_timed_out_at(base, timeout));
assert!(!conn.is_timed_out_at(base + timeout, timeout));
assert!(conn.is_timed_out_at(base + timeout + Duration::from_millis(1), timeout));
assert!(!conn.is_timed_out_at(
base.checked_sub(Duration::from_secs(1)).unwrap_or(base),
timeout
));
}
#[test]
fn test_check_timeouts_throttle_and_detection() {
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("Failed to create reactor");
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let _client = TcpStream::connect(addr).expect("Failed to connect");
let (server, _) = listener.accept().expect("Failed to accept");
let token = reactor
.add_connection(server)
.expect("Failed to add connection");
assert!(
reactor.check_timeouts().is_empty(),
"sweep within the throttle interval must be skipped"
);
let stale = Instant::now()
.checked_sub(Duration::from_secs(CONNECTION_TIMEOUT_SECS * 2))
.expect("monotonic clock should be well past 120s after a full build");
if let Some(conn) = reactor.connections.get_mut(token.id) {
conn.last_read_activity = stale;
conn.last_write_activity = stale;
}
reactor.last_timeout_check = stale;
assert_eq!(
reactor.check_timeouts(),
vec![token.id],
"after the interval elapses the stale connection must be detected"
);
assert!(
reactor.check_timeouts().is_empty(),
"the sweep must restamp the throttle and skip an immediate re-run"
);
reactor.remove_connection(token.id);
}
#[test]
fn ping_due_predicate_gates_on_state_idle_and_prior_ping() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let _client = TcpStream::connect(addr).expect("Failed to connect");
let (server, _) = listener.accept().expect("Failed to accept");
let mut conn = ReactorConnection::new(ConnectionToken::new(0, 1), server)
.expect("Failed to create connection");
let idle = Duration::from_secs(WATCHER_PING_IDLE_SECS);
let base = conn.last_activity();
assert!(!conn.is_ping_due_at(base + idle, idle));
conn.state = ConnectionState::Active;
assert!(!conn.is_ping_due_at(base, idle));
assert!(!conn.is_ping_due_at(base + idle - Duration::from_millis(1), idle));
assert!(conn.is_ping_due_at(base + idle, idle));
conn.note_ping_queued(base + idle);
assert!(!conn.is_ping_due_at(base + idle, idle));
assert!(!conn.is_ping_due_at(base + idle * 2 - Duration::from_millis(1), idle));
assert!(conn.is_ping_due_at(base + idle * 2, idle));
assert!(conn.is_timed_out_at(
base + Duration::from_secs(CONNECTION_TIMEOUT_SECS) + Duration::from_millis(1),
Duration::from_secs(CONNECTION_TIMEOUT_SECS)
));
conn.condemn(base + idle * 3);
assert!(!conn.is_ping_due_at(base + idle * 2, idle));
}
#[test]
fn drained_slow_client_recovers_active_state_on_flush() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let _client = TcpStream::connect(addr).expect("Failed to connect");
let (server, _) = listener.accept().expect("Failed to accept");
let mut conn = ReactorConnection::new(ConnectionToken::new(0, 1), server)
.expect("Failed to create connection");
conn.state = ConnectionState::SlowClient;
assert!(!conn.has_pending_writes());
conn.try_flush().expect("flush on an empty queue");
assert_eq!(
conn.state,
ConnectionState::Active,
"an empty queue means no backpressure; the state must recover"
);
conn.state = ConnectionState::SlowClient;
assert!(conn.enqueue_raw(vec![0u8; 64]));
assert_eq!(
conn.state,
ConnectionState::SlowClient,
"sanity: enqueue_raw must not repair the state by itself"
);
conn.try_flush().expect("flush the queued tail");
assert!(!conn.has_pending_writes(), "64 bytes must flush in one go");
assert_eq!(
conn.state,
ConnectionState::Active,
"draining back to the Normal band must restore Active"
);
}
#[test]
fn flushed_ping_bytes_do_not_stamp_write_activity() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let _client = TcpStream::connect(addr).expect("Failed to connect");
let (server, _) = listener.accept().expect("Failed to accept");
let mut conn = ReactorConnection::new(ConnectionToken::new(0, 1), server)
.expect("Failed to create connection");
conn.state = ConnectionState::Active;
let stale = Instant::now()
.checked_sub(Duration::from_secs(CONNECTION_TIMEOUT_SECS + 1))
.expect("monotonic clock should be well past 61s after a full build");
conn.last_read_activity = stale;
conn.last_write_activity = stale;
let before = conn.last_activity();
assert!(conn.enqueue_ping(vec![2u8; 18]));
conn.note_ping_queued(Instant::now());
conn.try_flush().expect("flush the ping");
assert!(!conn.has_pending_writes(), "the ping must flush in one go");
assert_eq!(
conn.last_activity(),
before,
"a delivered ping must not stamp write activity"
);
assert!(conn.enqueue_raw(vec![3u8; 32]));
conn.try_flush().expect("flush the non-ping tail");
assert!(
conn.last_activity() > before,
"non-ping writes must stamp write activity"
);
let stale = Instant::now()
.checked_sub(Duration::from_secs(CONNECTION_TIMEOUT_SECS + 1))
.expect("monotonic clock should be well past 61s after a full build");
conn.last_read_activity = stale;
conn.last_write_activity = stale;
let before = conn.last_activity();
assert!(conn.enqueue_ping(vec![2u8; 18]));
conn.note_ping_queued(Instant::now());
assert!(conn.enqueue_data(Bytes::from(vec![3u8; 32]), false, false, false, true, Instant::now()));
conn.try_flush().expect("flush the ping plus the tail behind it");
assert!(!conn.has_pending_writes(), "both entries must flush");
assert!(
conn.last_activity() > before,
"ordinary bytes flushed behind the ping must stamp activity"
);
}
#[test]
fn ping_is_not_due_while_writes_are_pending() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let _client = TcpStream::connect(addr).expect("Failed to connect");
let (server, _) = listener.accept().expect("Failed to accept");
let mut conn = ReactorConnection::new(ConnectionToken::new(0, 1), server)
.expect("Failed to create connection");
conn.state = ConnectionState::Active;
let idle = Duration::from_secs(WATCHER_PING_IDLE_SECS);
assert!(conn.enqueue_raw(vec![0u8; 8]));
assert!(
!conn.is_ping_due_at(conn.last_activity() + idle * 3, idle),
"a queued tail must suppress the probe however idle the clock looks"
);
conn.try_flush().expect("drain the tail");
assert!(!conn.has_pending_writes());
let base = conn.last_activity();
assert!(conn.is_ping_due_at(base + idle, idle));
}
#[test]
fn high_band_control_chain_survives_and_decodes() {
use rml_rtmp::chunk_io::ChunkDeserializer;
use rml_rtmp::messages::UserControlEventType;
let ts0 = RtmpTimestamp { value: 0 };
let ack_payload = |n: u32| {
RtmpMessage::Acknowledgement {
sequence_number: n,
}
.into_message_payload(ts0, 0)
.expect("build acknowledgement")
};
let a0 = ack_payload(1);
let ping = RtmpMessage::UserControl {
event_type: UserControlEventType::PingRequest,
stream_id: None,
buffer_length: None,
timestamp: Some(RtmpTimestamp { value: 7 }),
}
.into_message_payload(ts0, 0)
.expect("build ping");
let v1 = RtmpMessage::VideoData {
data: Bytes::from(vec![0x17u8; 2 * 1024 * 1024 + 64 * 1024]),
}
.into_message_payload(ts0, 1)
.expect("build large video");
let a1 = ack_payload(2);
let v2 = RtmpMessage::VideoData {
data: Bytes::from(vec![0x27u8; 512]),
}
.into_message_payload(ts0, 1)
.expect("build sheddable video");
let a2 = ack_payload(3);
let v3 = RtmpMessage::VideoData {
data: Bytes::from(vec![0x17u8; 256]),
}
.into_message_payload(ts0, 1)
.expect("build trailing video");
let expected: Vec<(u8, Bytes)> = vec![
(3, a0.data.clone()),
(4, ping.data.clone()),
(9, v1.data.clone()),
(3, a1.data.clone()),
(3, a2.data.clone()),
(9, v3.data.clone()),
];
let mut serializer = ChunkSerializer::new();
let p_a0 = serializer.serialize(&a0, false, false).expect("ser a0");
let p_ping = serializer.serialize(&ping, false, false).expect("ser ping");
let p_v1 = serializer.serialize(&v1, false, true).expect("ser v1");
let p_a1 = serializer.serialize(&a1, false, false).expect("ser a1");
let p_v2 = serializer.serialize(&v2, false, true).expect("ser v2");
let p_a2 = serializer.serialize(&a2, false, false).expect("ser a2");
let p_v3 = serializer.serialize(&v3, false, true).expect("ser v3");
assert!(!p_a1.can_be_dropped, "rml must mark control non-droppable");
assert!(p_v1.can_be_dropped, "rml must mark tolerant media droppable");
let mut queue = WriteQueue::new();
let mut wire: Vec<u8> = Vec::new();
assert!(queue.enqueue(
Bytes::from(p_a0.bytes),
false,
false,
false,
p_a0.can_be_dropped,
Instant::now()
));
assert!(queue.enqueue_ping(Bytes::from(p_ping.bytes)));
queue.try_flush(&mut wire).expect("flush the opening pair");
assert!(queue.enqueue(
Bytes::from(p_v1.bytes),
true,
false,
true,
p_v1.can_be_dropped,
Instant::now()
));
assert_eq!(queue.backpressure_level(), BackpressureLevel::High);
assert!(queue.enqueue(
Bytes::from(p_a1.bytes),
false,
false,
false,
p_a1.can_be_dropped,
Instant::now()
));
let before_shed = queue.pending_bytes();
assert!(queue.enqueue(
Bytes::from(p_v2.bytes),
false,
false,
true,
p_v2.can_be_dropped,
Instant::now()
));
assert_eq!(
queue.pending_bytes(),
before_shed,
"the droppable non-keyframe must be shed in the High band"
);
queue.try_flush(&mut wire).expect("flush the pressured batch");
assert!(queue.is_empty(), "the pressured batch must drain fully");
assert!(queue.enqueue(
Bytes::from(p_a2.bytes),
false,
false,
false,
p_a2.can_be_dropped,
Instant::now()
));
assert!(queue.enqueue(
Bytes::from(p_v3.bytes),
true,
false,
true,
p_v3.can_be_dropped,
Instant::now()
));
queue.try_flush(&mut wire).expect("flush the tail");
let mut deserializer = ChunkDeserializer::new();
let mut decoded: Vec<(u8, Bytes)> = Vec::new();
let mut next = deserializer
.get_next_message(&wire)
.expect("the delivered wire must stay decodable");
while let Some(payload) = next {
decoded.push((payload.type_id, payload.data));
next = deserializer
.get_next_message(&[])
.expect("valid buffered continuation");
}
assert_eq!(
decoded.len(),
expected.len(),
"exactly the delivered messages must decode"
);
for (i, ((got_type, got_data), (want_type, want_data))) in
decoded.iter().zip(expected.iter()).enumerate()
{
assert_eq!(got_type, want_type, "message {i} type");
assert_eq!(got_data, want_data, "message {i} payload");
}
}
#[test]
fn sweep_pings_an_idle_watcher_instead_of_only_reaping_it() {
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(1, None, status).expect("Failed to create reactor");
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let _client = TcpStream::connect(addr).expect("Failed to connect");
let (server, _) = listener.accept().expect("Failed to accept");
let token = reactor
.add_connection(server)
.expect("Failed to add connection");
reactor
.scheduler
.register_watcher_for_test(token.id, "quiet-stream");
reactor
.connections
.get_mut(token.id)
.expect("connection exists")
.state = ConnectionState::Active;
let idle = Instant::now()
.checked_sub(Duration::from_secs(WATCHER_PING_IDLE_SECS + 1))
.expect("monotonic clock should be well past 31s after a full build");
{
let conn = reactor
.connections
.get_mut(token.id)
.expect("connection exists");
conn.last_read_activity = idle;
conn.last_write_activity = idle;
}
reactor.last_timeout_check = idle;
let reaped = reactor.check_timeouts();
assert!(
reaped.is_empty(),
"a watcher idle for half the timeout must not be reaped"
);
let (queued_bytes, activity_after_sweep) = {
let conn = reactor
.connections
.get(token.id)
.expect("connection exists");
assert!(
conn.has_pending_writes(),
"the sweep must queue a liveness ping for the idle watcher"
);
(conn.pending_bytes(), conn.last_activity())
};
assert!(
reactor.is_pending_flush(token.id),
"the queued ping must be scheduled for the next flush pass"
);
assert_eq!(
activity_after_sweep, idle,
"queueing a ping must not count as connection activity"
);
reactor.last_timeout_check = idle;
assert!(
reactor.check_timeouts().is_empty(),
"the watcher is still short of the timeout on the second sweep"
);
assert_eq!(
reactor
.connections
.get(token.id)
.expect("connection exists")
.pending_bytes(),
queued_bytes,
"a sweep within the ping window must not re-queue the ping"
);
let dead = Instant::now()
.checked_sub(Duration::from_secs(CONNECTION_TIMEOUT_SECS + 1))
.expect("monotonic clock should be well past 61s after a full build");
{
let conn = reactor
.connections
.get_mut(token.id)
.expect("connection exists");
conn.last_read_activity = dead;
conn.last_write_activity = dead;
}
reactor.last_timeout_check = dead;
assert_eq!(
reactor.check_timeouts(),
vec![token.id],
"an unflushed ping must not save a wedged watcher from the reaper"
);
reactor.remove_connection(token.id);
}
#[test]
fn test_reactor_creation() {
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let reactor = Reactor::new(3, None, status);
assert!(reactor.is_ok());
}
#[test]
fn test_connection_generation_increments() {
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("Failed to create reactor");
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let client1 = TcpStream::connect(addr).expect("Failed to connect");
let (server1, _) = listener.accept().expect("Failed to accept");
let token1 = reactor
.add_connection(server1)
.expect("Failed to add connection");
reactor.remove_connection(token1.id);
let client2 = TcpStream::connect(addr).expect("Failed to connect");
let (server2, _) = listener.accept().expect("Failed to accept");
let token2 = reactor
.add_connection(server2)
.expect("Failed to add connection");
assert_eq!(token1.id, token2.id);
assert_eq!(token2.generation, token1.generation + 1);
drop(client1);
drop(client2);
}
#[test]
fn test_token_validation() {
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("Failed to create reactor");
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let client = TcpStream::connect(addr).expect("Failed to connect");
let (server, _) = listener.accept().expect("Failed to accept");
let token = reactor
.add_connection(server)
.expect("Failed to add connection");
assert!(reactor
.validate_connection(token.to_poller_token())
.is_some());
reactor.remove_connection(token.id);
assert!(reactor
.validate_connection(token.to_poller_token())
.is_none());
drop(client);
}
#[test]
fn test_generation_prevents_aba_problem() {
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("Failed to create reactor");
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let client_a = TcpStream::connect(addr).expect("Failed to connect A");
let (server_a, _) = listener.accept().expect("Failed to accept A");
let token_a = reactor
.add_connection(server_a)
.expect("Failed to add connection A");
let stale_poller_token = token_a.to_poller_token();
reactor.remove_connection(token_a.id);
drop(client_a);
let client_b = TcpStream::connect(addr).expect("Failed to connect B");
let (server_b, _) = listener.accept().expect("Failed to accept B");
let token_b = reactor
.add_connection(server_b)
.expect("Failed to add connection B");
assert!(reactor
.validate_connection(token_b.to_poller_token())
.is_some());
assert!(reactor.validate_connection(stale_poller_token).is_none());
assert_eq!(token_a.id, token_b.id); assert_ne!(token_a.generation, token_b.generation);
reactor.remove_connection(token_b.id);
drop(client_b);
}
#[test]
fn poller_token_roundtrip_at_width_extremes() {
let cases: [(usize, Generation); 3] = [
(0, 1),
(TOKEN_ID_MASK - 1, Generation::MAX),
(1234, 567),
];
for (id, generation) in cases {
let token = ConnectionToken::new(id, generation);
let decoded = ConnectionToken::from_poller_token(token.to_poller_token());
assert_eq!(decoded, token);
}
}
#[test]
fn connection_token_never_collides_with_waker_token() {
let extreme = ConnectionToken::new(TOKEN_ID_MASK - 1, Generation::MAX);
assert_ne!(extreme.to_poller_token(), WAKER_TOKEN);
assert!(effective_max_connections(Some(usize::MAX)) <= TOKEN_ID_MASK);
}
#[test]
fn simulated_32bit_packing_is_lossless() {
const SIM_ID_BITS: u32 = 16;
const SIM_ID_MASK: u32 = (1u32 << SIM_ID_BITS) - 1;
let cases: [(u32, u16); 3] = [(0, 1), (SIM_ID_MASK - 1, u16::MAX), (1234, 567)];
for (id, generation) in cases {
let word = ((generation as u32) << SIM_ID_BITS) | (id & SIM_ID_MASK);
let decoded_id = word & SIM_ID_MASK;
let decoded_generation = (word >> SIM_ID_BITS) as u16;
assert_eq!(decoded_id, id);
assert_eq!(decoded_generation, generation);
}
}
#[test]
fn test_many_connections_creation() {
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("Failed to create reactor");
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let num_connections = 100;
let mut clients = Vec::new();
let mut tokens = Vec::new();
for i in 0..num_connections {
let client = TcpStream::connect(addr).expect(&format!("Failed to connect {}", i));
let (server, _) = listener.accept().expect(&format!("Failed to accept {}", i));
let token = reactor
.add_connection(server)
.expect(&format!("Failed to add connection {}", i));
clients.push(client);
tokens.push(token);
}
assert_eq!(reactor.connections.len(), num_connections);
for token in &tokens {
reactor.remove_connection(token.id);
}
assert_eq!(reactor.connections.len(), 0);
}
#[test]
#[ignore] fn perf_connection_scaling() {
use std::time::Instant;
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("Failed to create reactor");
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let max_fd = effective_max_connections(None);
let num_connections = (max_fd / 3).min(1000);
let mut clients = Vec::with_capacity(num_connections);
let mut tokens = Vec::with_capacity(num_connections);
let start = Instant::now();
for i in 0..num_connections {
let client =
TcpStream::connect(addr).unwrap_or_else(|_| panic!("Failed to connect {}", i));
let (server, _) = listener
.accept()
.unwrap_or_else(|_| panic!("Failed to accept {}", i));
let token = reactor
.add_connection(server)
.unwrap_or_else(|_| panic!("Failed to add {}", i));
clients.push(client);
tokens.push(token);
}
let connect_time = start.elapsed();
assert_eq!(reactor.connections.len(), num_connections);
let cleanup_start = Instant::now();
for token in &tokens {
reactor.remove_connection(token.id);
}
let cleanup_time = cleanup_start.elapsed();
println!();
println!("╔══════════════════════════════════════════════════════════╗");
println!("║ RTMP Performance Test: Connection Scaling ║");
println!("╠══════════════════════════════════════════════════════════╣");
println!("║ Platform: {:>40} ║", std::env::consts::OS);
println!("║ Arch: {:>40} ║", std::env::consts::ARCH);
println!("║ Connections: {:>40} ║", num_connections);
println!("╠══════════════════════════════════════════════════════════╣");
println!("║ Connect time: {:>37?} ║", connect_time);
println!(
"║ Per connection: {:>37?} ║",
connect_time / num_connections as u32
);
println!("║ Cleanup time: {:>37?} ║", cleanup_time);
println!(
"║ Per cleanup: {:>37?} ║",
cleanup_time / num_connections as u32
);
println!("╚══════════════════════════════════════════════════════════╝");
println!();
}
#[test]
#[ignore] fn perf_read_throughput() {
use std::io::Write;
use std::time::Instant;
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("Failed to create reactor");
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let mut client = TcpStream::connect(addr).expect("Failed to connect");
let (server, _) = listener.accept().expect("Failed to accept");
client.set_nodelay(true).ok();
let token = reactor
.add_connection(server)
.expect("Failed to add connection");
let test_sizes = [128, 1024, 4096, 8192, 16384, 65536];
let iterations = 100;
println!();
println!("╔══════════════════════════════════════════════════════════╗");
println!("║ RTMP Performance Test: Read Throughput ║");
println!("╠══════════════════════════════════════════════════════════╣");
println!("║ Platform: {:>40} ║", std::env::consts::OS);
println!("║ Arch: {:>40} ║", std::env::consts::ARCH);
println!("║ Iterations: {:>40} ║", iterations);
println!("╠══════════════════════════════════════════════════════════╣");
for &size in &test_sizes {
let data = vec![0xABu8; size];
let mut total_bytes = 0usize;
let start = Instant::now();
for _ in 0..iterations {
client.write_all(&data).expect("Failed to write");
client.flush().expect("Failed to flush");
total_bytes += size;
std::thread::sleep(std::time::Duration::from_micros(100));
if let Some(conn) = reactor.connections.get_mut(token.id) {
let _ = conn.try_read();
}
}
let elapsed = start.elapsed();
let throughput_mbps = (total_bytes as f64 / 1_000_000.0) / elapsed.as_secs_f64();
println!(
"║ Chunk {:>6} B: {:>8.2} MB/s ({:>6} B x {:>3}) ║",
size, throughput_mbps, size, iterations
);
}
println!("╚══════════════════════════════════════════════════════════╝");
println!();
reactor.remove_connection(token.id);
}
#[test]
fn test_handle_writable_marks_interest_dirty_on_queue_drain() {
use std::io::Read;
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("Failed to create reactor");
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let mut client = TcpStream::connect(addr).expect("Failed to connect");
let (server, _) = listener.accept().expect("Failed to accept");
client.set_nonblocking(true).ok();
let token = reactor
.add_connection(server)
.expect("Failed to add connection");
if let Some(conn) = reactor.connections.get_mut(token.id) {
conn.state = ConnectionState::Active;
}
let test_data = b"Hello";
if let Some(conn) = reactor.connections.get_mut(token.id) {
conn.enqueue_data(Bytes::from_static(test_data), false, false, false, true, Instant::now());
assert!(conn.has_pending_writes());
}
reactor.drain_interest_dirty();
let result = reactor.handle_writable(token.id);
assert!(result.is_none(), "Connection should not be closed");
if let Some(conn) = reactor.connections.get(token.id) {
assert!(!conn.has_pending_writes(), "Queue should be drained");
}
assert!(
reactor.is_interest_dirty(token.id),
"interest_dirty should contain connection ID after queue drain"
);
let mut buf = vec![0u8; 100];
client.set_nonblocking(false).ok();
client
.set_read_timeout(Some(std::time::Duration::from_millis(100)))
.ok();
let _ = client.read(&mut buf);
reactor.remove_connection(token.id);
}
#[test]
fn test_flush_pending_marks_interest_dirty_on_queue_drain() {
use std::io::Read;
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("Failed to create reactor");
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let mut client = TcpStream::connect(addr).expect("Failed to connect");
let (server, _) = listener.accept().expect("Failed to accept");
client.set_nonblocking(true).ok();
let token = reactor
.add_connection(server)
.expect("Failed to add connection");
if let Some(conn) = reactor.connections.get_mut(token.id) {
conn.state = ConnectionState::Active;
}
let test_data = b"World";
if let Some(conn) = reactor.connections.get_mut(token.id) {
conn.enqueue_data(Bytes::from_static(test_data), false, false, false, true, Instant::now());
}
reactor.mark_pending_flush_for_test(token.id);
reactor.drain_interest_dirty();
let ids_to_close = reactor.flush_pending();
assert!(
ids_to_close.is_empty(),
"No connections should need closing"
);
assert!(
reactor.is_interest_dirty(token.id),
"interest_dirty should contain connection ID after flush_pending drains queue"
);
let mut buf = vec![0u8; 100];
client.set_nonblocking(false).ok();
client
.set_read_timeout(Some(std::time::Duration::from_millis(100)))
.ok();
let _ = client.read(&mut buf);
reactor.remove_connection(token.id);
}
#[cfg(unix)]
fn set_small_socket_buffer(fd: std::os::unix::io::RawFd, opt: libc::c_int) {
let size: libc::c_int = 2048;
unsafe {
libc::setsockopt(
fd,
libc::SOL_SOCKET,
opt,
&size as *const libc::c_int as *const libc::c_void,
std::mem::size_of::<libc::c_int>() as libc::socklen_t,
);
}
}
#[cfg(unix)]
#[test]
fn test_flush_pending_wouldblock_registers_writable_not_pending_flush() {
use std::os::unix::io::AsRawFd;
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("Failed to create reactor");
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let client = TcpStream::connect(addr).expect("Failed to connect");
let (server, _) = listener.accept().expect("Failed to accept");
set_small_socket_buffer(server.as_raw_fd(), libc::SO_SNDBUF);
set_small_socket_buffer(client.as_raw_fd(), libc::SO_RCVBUF);
let token = reactor
.add_connection(server)
.expect("Failed to add connection");
if let Some(conn) = reactor.connections.get_mut(token.id) {
conn.state = ConnectionState::Active;
let big = Bytes::from(vec![0u8; 1024 * 1024]);
assert!(conn.enqueue_data(big, false, true, true, true, Instant::now()));
assert!(conn.has_pending_writes());
}
reactor.mark_pending_flush_for_test(token.id);
reactor.drain_interest_dirty();
let closes = reactor.flush_pending();
assert!(
closes.is_empty(),
"a full-buffer slow client must not be closed"
);
let conn = reactor
.connections
.get(token.id)
.expect("connection present");
assert!(
conn.has_pending_writes(),
"WouldBlock must leave the remaining data queued"
);
assert!(
!reactor.is_pending_flush(token.id),
"WouldBlock must NOT reinsert into pending_flush (no EAGAIN spin)"
);
assert!(
reactor.is_interest_dirty(token.id),
"WouldBlock must mark interest_dirty so writable interest is registered"
);
reactor.remove_connection(token.id);
drop(client);
}
#[test]
fn test_flush_pending_marks_interest_dirty_when_no_pending_writes() {
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("Failed to create reactor");
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let _client = TcpStream::connect(addr).expect("Failed to connect");
let (server, _) = listener.accept().expect("Failed to accept");
let token = reactor
.add_connection(server)
.expect("Failed to add connection");
if let Some(conn) = reactor.connections.get_mut(token.id) {
conn.state = ConnectionState::Active;
assert!(!conn.has_pending_writes());
}
reactor.mark_pending_flush_for_test(token.id);
reactor.drain_interest_dirty();
let ids_to_close = reactor.flush_pending();
assert!(
ids_to_close.is_empty(),
"No connections should need closing"
);
assert!(reactor.is_interest_dirty(token.id),
"interest_dirty should be marked even when no pending writes (to clear WRITABLE interest)");
reactor.remove_connection(token.id);
}
#[test]
fn dirty_marks_dedup_within_a_round() {
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("Failed to create reactor");
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let _client = TcpStream::connect(addr).expect("Failed to connect");
let (server, _) = listener.accept().expect("Failed to accept");
let token = reactor
.add_connection(server)
.expect("Failed to add connection");
if let Some(conn) = reactor.connections.get_mut(token.id) {
conn.state = ConnectionState::Active;
}
for _ in 0..2 {
reactor.packets_buffer.push((
token.id,
Bytes::from_static(b"tag"),
false,
false,
false,
true,
));
}
reactor.write_pending_packets();
assert!(reactor.is_pending_flush(token.id));
assert!(reactor.is_interest_dirty(token.id));
assert_eq!(
reactor.pending_flush_ids,
vec![token.id],
"two packets to one target must list it for flush exactly once"
);
assert_eq!(
reactor.interest_dirty_ids,
vec![token.id],
"two packets to one target must list it for interest exactly once"
);
let ids_to_close = reactor.flush_pending();
assert!(ids_to_close.is_empty());
assert!(!reactor.is_pending_flush(token.id));
assert_eq!(
reactor.drain_interest_dirty(),
vec![token.id],
"the interest drain must visit the id exactly once"
);
reactor.remove_connection(token.id);
}
#[test]
fn dirty_marks_rearm_across_drain_epochs() {
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("Failed to create reactor");
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let _client = TcpStream::connect(addr).expect("Failed to connect");
let (server, _) = listener.accept().expect("Failed to accept");
let token = reactor
.add_connection(server)
.expect("Failed to add connection");
if let Some(conn) = reactor.connections.get_mut(token.id) {
conn.state = ConnectionState::Active;
}
reactor.packets_buffer.push((
token.id,
Bytes::from_static(b"first"),
false,
false,
false,
true,
));
reactor.write_pending_packets();
assert!(reactor.is_pending_flush(token.id));
assert!(reactor.is_interest_dirty(token.id));
assert!(reactor.flush_pending().is_empty());
assert!(reactor.update_dirty_interests().is_empty());
assert!(
!reactor.is_pending_flush(token.id),
"the flush drain must clear the mark"
);
assert!(
!reactor.is_interest_dirty(token.id),
"the interest drain must clear the mark"
);
reactor.packets_buffer.push((
token.id,
Bytes::from_static(b"second"),
false,
false,
false,
true,
));
reactor.write_pending_packets();
assert!(
reactor.is_pending_flush(token.id) && reactor.is_interest_dirty(token.id),
"a mark after a drain must re-arm"
);
assert!(reactor.flush_pending().is_empty());
assert!(reactor.update_dirty_interests().is_empty());
assert!(!reactor.is_pending_flush(token.id));
assert!(!reactor.is_interest_dirty(token.id));
reactor.remove_connection(token.id);
}
#[test]
fn stale_dirty_ids_for_removed_connections_drain_harmlessly() {
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("Failed to create reactor");
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let _client1 = TcpStream::connect(addr).expect("Failed to connect");
let (server1, _) = listener.accept().expect("Failed to accept");
let token1 = reactor
.add_connection(server1)
.expect("Failed to add connection");
if let Some(conn) = reactor.connections.get_mut(token1.id) {
conn.state = ConnectionState::Active;
}
reactor.packets_buffer.push((
token1.id,
Bytes::from_static(b"doomed"),
false,
false,
false,
true,
));
reactor.write_pending_packets();
reactor.remove_connection(token1.id);
let _client2 = TcpStream::connect(addr).expect("Failed to connect");
let (server2, _) = listener.accept().expect("Failed to accept");
let token2 = reactor
.add_connection(server2)
.expect("Failed to add connection");
assert_eq!(
token2.id, token1.id,
"the slab must reuse the freed slot for this test to bite"
);
assert!(
!reactor.is_pending_flush(token2.id) && !reactor.is_interest_dirty(token2.id),
"a fresh connection must be born unmarked"
);
if let Some(conn) = reactor.connections.get_mut(token2.id) {
conn.state = ConnectionState::Active;
assert!(conn.enqueue_data(
Bytes::from_static(b"unscheduled"),
false,
false,
false,
true,
Instant::now()
));
}
assert!(
reactor.flush_pending().is_empty(),
"a stale id must not close anything"
);
assert!(
reactor
.connections
.get(token2.id)
.expect("connection present")
.has_pending_writes(),
"a stale id must not flush a fresh unmarked connection out of turn"
);
assert!(
reactor.update_dirty_interests().is_empty(),
"a stale id must not fail an interest update"
);
assert!(
!reactor
.connections
.get(token2.id)
.expect("connection present")
.current_interest
.writable,
"a stale id must not register writable interest for a fresh connection"
);
reactor.packets_buffer.push((
token2.id,
Bytes::from_static(b"scheduled"),
false,
false,
false,
true,
));
reactor.write_pending_packets();
assert!(reactor.flush_pending().is_empty());
assert!(
!reactor
.connections
.get(token2.id)
.expect("connection present")
.has_pending_writes(),
"a real mark must still flush the fresh connection"
);
reactor.packets_buffer.push((
token2.id,
Bytes::from_static(b"doomed again"),
false,
false,
false,
true,
));
reactor.write_pending_packets();
reactor.remove_connection(token2.id);
assert!(reactor.flush_pending().is_empty());
assert!(reactor.update_dirty_interests().is_empty());
}
#[test]
fn dirty_id_list_capacity_shrinks_after_flash_crowd() {
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("Failed to create reactor");
let flash_crowd: usize = 512;
reactor.pending_flush_ids.extend(0..flash_crowd);
let peak_capacity = reactor.pending_flush_ids.capacity();
assert!(
peak_capacity > DIRTY_IDS_SHRINK_MIN_CAPACITY,
"512 marked ids must grow the list past the shrink floor, got {peak_capacity}"
);
assert!(reactor.flush_pending().is_empty());
assert!(
reactor
.pending_flush_ids
.capacity()
.max(reactor.dirty_drain_scratch.capacity())
>= peak_capacity,
"a full drain must not shrink the buffer it just used at peak occupancy"
);
for _ in 0..SPARSE_DRAINS_BEFORE_SHRINK + 2 {
reactor.pending_flush_ids.push(0);
assert!(reactor.flush_pending().is_empty());
}
let live_capacity = reactor.pending_flush_ids.capacity();
let scratch_capacity = reactor.dirty_drain_scratch.capacity();
assert!(
live_capacity <= DIRTY_IDS_SHRINK_MIN_CAPACITY,
"the decayed live list must shrink to the floor, got {live_capacity}"
);
assert!(
scratch_capacity <= DIRTY_IDS_SHRINK_MIN_CAPACITY,
"the decayed drain scratch must shrink to the floor, got {scratch_capacity}"
);
reactor.interest_dirty_ids.extend(0..flash_crowd);
let interest_peak = reactor.interest_dirty_ids.capacity();
assert!(interest_peak > DIRTY_IDS_SHRINK_MIN_CAPACITY);
assert!(reactor.update_dirty_interests().is_empty());
for _ in 0..SPARSE_DRAINS_BEFORE_SHRINK + 2 {
reactor.interest_dirty_ids.push(0);
assert!(reactor.update_dirty_interests().is_empty());
}
assert!(
reactor.interest_dirty_ids.capacity() <= DIRTY_IDS_SHRINK_MIN_CAPACITY,
"the decayed interest list must shrink to the floor"
);
assert!(
reactor.dirty_drain_scratch.capacity() <= DIRTY_IDS_SHRINK_MIN_CAPACITY,
"the drain scratch must stay shrunk after the interest epochs"
);
}
#[test]
fn feed_publisher_drains_mixed_raw_and_media() {
use crate::rtmp::embed_rtmp_server::build_publish_control;
let stream_keys = Arc::new(dashmap::DashSet::new());
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("reactor");
let (feed_tx, feed_rx) = crossbeam_channel::bounded(64);
let (budget_guard, budget) = IngressBudget::new(PUBLISHER_INGRESS_HIGH_WATER_BYTES);
let claim = StreamKeyClaim::claim(stream_keys, "live".to_string()).expect("claim");
let pub_id = reactor
.add_publisher(PublisherRegistration {
claim,
source: PublisherSource::Feed(feed_rx),
budget: budget_guard,
})
.expect("publisher registered");
for control in
build_publish_control("app".to_string(), "live".to_string()).expect("control")
{
send_acquired(&budget, &feed_tx, PublisherFeed::Raw(control));
}
let video_seq: &[u8] = &[0x17, 0x00, 0x00, 0x00, 0x00, 0x01, 0x64];
let audio_seq: &[u8] = &[0xaf, 0x00, 0x12, 0x10];
send_acquired(
&budget,
&feed_tx,
PublisherFeed::Media {
tag_type: 0x09,
timestamp: RtmpTimestamp { value: 0 },
data: Bytes::from_static(video_seq),
},
);
send_acquired(
&budget,
&feed_tx,
PublisherFeed::Media {
tag_type: 0x08,
timestamp: RtmpTimestamp { value: 0 },
data: Bytes::from_static(audio_seq),
},
);
send_acquired(
&budget,
&feed_tx,
PublisherFeed::Media {
tag_type: 0x09,
timestamp: RtmpTimestamp { value: 33 },
data: Bytes::from_static(&[0x17, 0x01, 0xAA, 0xBB]),
},
);
send_acquired(
&budget,
&feed_tx,
PublisherFeed::Media {
tag_type: 0x09,
timestamp: RtmpTimestamp { value: 66 },
data: Bytes::from_static(&[0x17, 0x01, 0xCC, 0xDD]),
},
);
let (removed, _) = reactor.process_publishers();
assert!(removed.is_empty(), "healthy publisher must not be removed");
assert_eq!(
reactor
.scheduler
.channel_video_sequence_header("live")
.as_deref(),
Some(video_seq),
"bypassed video sequence header must be cached"
);
assert_eq!(
reactor
.scheduler
.channel_audio_sequence_header("live")
.as_deref(),
Some(audio_seq),
"bypassed audio sequence header must be cached"
);
assert!(
reactor.scheduler.channel_frozen_gop_count("live") >= 1,
"the completed GOP must be frozen from bypassed media"
);
drop(feed_tx);
let (removed, _) = reactor.process_publishers();
assert!(
removed.contains(&pub_id),
"a disconnected publisher must be scheduled for removal"
);
}
#[test]
fn stream_key_claim_releases_on_drop_but_not_on_move() {
let stream_keys: Arc<dashmap::DashSet<String>> = Arc::new(dashmap::DashSet::new());
let claim = StreamKeyClaim::claim(stream_keys.clone(), "k".to_string()).expect("claim");
assert!(
StreamKeyClaim::claim(stream_keys.clone(), "k".to_string()).is_err(),
"a second claim for a held key must lose"
);
let moved = claim;
assert!(
stream_keys.contains("k"),
"moving the guard must keep the key claimed"
);
drop(moved);
assert!(!stream_keys.contains("k"), "drop must release the claim");
}
#[test]
fn add_publisher_acceptance_defers_release_refusal_frees_the_key() {
let stream_keys = Arc::new(dashmap::DashSet::new());
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("reactor");
let (_feed_tx, feed_rx) = crossbeam_channel::bounded::<PublisherFeed>(1);
let claim =
StreamKeyClaim::claim(stream_keys.clone(), "live".to_string()).expect("claim");
let id = reactor
.add_publisher(PublisherRegistration {
claim,
source: PublisherSource::Feed(feed_rx),
budget: test_budget(),
})
.expect("accepted");
assert!(
stream_keys.contains("live"),
"acceptance must keep the key claimed"
);
reactor.remove_publisher(id);
assert!(
!stream_keys.contains("live"),
"removing the publisher must release its key"
);
assert!(reactor.scheduler.new_channel("net".to_string(), 777));
let (_feed_tx2, feed_rx2) = crossbeam_channel::bounded::<PublisherFeed>(1);
let claim = StreamKeyClaim::claim(stream_keys.clone(), "net".to_string()).expect("claim");
assert!(
reactor
.add_publisher(PublisherRegistration {
claim,
source: PublisherSource::Feed(feed_rx2),
budget: test_budget(),
})
.is_none(),
"a key already being published must be refused"
);
assert!(
!stream_keys.contains("net"),
"a refused registration must release its key claim"
);
}
#[test]
fn reactor_stop_releases_enqueued_but_unconsumed_key_claims() {
let stream_keys = Arc::new(dashmap::DashSet::new());
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status.clone()).expect("reactor");
let (_connection_sender, connection_receiver) =
crossbeam_channel::bounded::<TcpStream>(1);
let registrations = Arc::new(RegistrationHandoff::new());
let kill_switch = RegistrationKillSwitch::arm(registrations.clone());
let (_feed_tx, feed_rx) = crossbeam_channel::bounded::<PublisherFeed>(1);
let claim = StreamKeyClaim::claim(stream_keys.clone(), "orphan".to_string())
.expect("first claim must win");
registrations
.enqueue(PublisherRegistration {
claim,
source: PublisherSource::Feed(feed_rx),
budget: test_budget(),
})
.unwrap_or_else(|_| panic!("enqueue while the worker lives"));
status.store(STATUS_END, Ordering::Release);
reactor.run(connection_receiver, kill_switch.handoff(), None);
drop(reactor);
drop(kill_switch);
assert!(
!stream_keys.contains("orphan"),
"a stopped reactor must release queued, never-consumed key claims"
);
assert!(
StreamKeyClaim::claim(stream_keys.clone(), "orphan".to_string()).is_ok(),
"the key must be claimable again after the reactor stopped"
);
drop(registrations);
}
#[test]
fn reactor_teardown_releases_accepted_publisher_key_claims() {
let stream_keys = Arc::new(dashmap::DashSet::new());
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status.clone()).expect("reactor");
let (_feed_tx, feed_rx) = crossbeam_channel::bounded::<PublisherFeed>(1);
let claim =
StreamKeyClaim::claim(stream_keys.clone(), "live".to_string()).expect("claim");
reactor
.add_publisher(PublisherRegistration {
claim,
source: PublisherSource::Feed(feed_rx),
budget: test_budget(),
})
.expect("accepted");
assert!(
stream_keys.contains("live"),
"an accepted publisher's key stays claimed while the reactor lives"
);
let (_connection_sender, connection_receiver) =
crossbeam_channel::bounded::<TcpStream>(1);
let registrations = Arc::new(RegistrationHandoff::new());
let kill_switch = RegistrationKillSwitch::arm(registrations);
status.store(STATUS_END, Ordering::Release);
reactor.run(connection_receiver, kill_switch.handoff(), None);
drop(reactor);
assert!(
!stream_keys.contains("live"),
"tearing the reactor down must release accepted publishers' keys"
);
assert!(
StreamKeyClaim::claim(stream_keys.clone(), "live".to_string()).is_ok(),
"the key must be claimable again after teardown"
);
}
#[test]
fn worker_death_before_reactor_construction_releases_queued_key_claims() {
let stream_keys: Arc<dashmap::DashSet<String>> = Arc::new(dashmap::DashSet::new());
let registrations = Arc::new(RegistrationHandoff::new());
let kill_switch = RegistrationKillSwitch::arm(registrations.clone());
let (_feed_tx, feed_rx) = crossbeam_channel::bounded::<PublisherFeed>(1);
let claim = StreamKeyClaim::claim(stream_keys.clone(), "orphan".to_string())
.expect("first claim must win");
registrations
.enqueue(PublisherRegistration {
claim,
source: PublisherSource::Feed(feed_rx),
budget: test_budget(),
})
.unwrap_or_else(|_| panic!("enqueue while the worker lives"));
drop(kill_switch);
assert!(
!stream_keys.contains("orphan"),
"a worker that died before constructing the reactor must release queued key claims"
);
let (_late_tx, late_rx) = crossbeam_channel::bounded::<PublisherFeed>(1);
let late_claim = StreamKeyClaim::claim(stream_keys.clone(), "late".to_string())
.expect("claim after worker death");
let refused = registrations.enqueue(PublisherRegistration {
claim: late_claim,
source: PublisherSource::Feed(late_rx),
budget: test_budget(),
});
assert!(
refused.is_err(),
"the registration intake must be closed once the worker died"
);
drop(refused);
assert!(
!stream_keys.contains("late"),
"a refused registration must release its key claim immediately"
);
}
#[test]
fn drain_into_hands_queued_registrations_to_the_consumer_exactly_once() {
let stream_keys: Arc<dashmap::DashSet<String>> = Arc::new(dashmap::DashSet::new());
let registrations = RegistrationHandoff::new();
for key in ["a", "b"] {
let (_feed_tx, feed_rx) = crossbeam_channel::bounded::<PublisherFeed>(1);
let claim = StreamKeyClaim::claim(stream_keys.clone(), key.to_string())
.expect("first claim must win");
registrations
.enqueue(PublisherRegistration {
claim,
source: PublisherSource::Feed(feed_rx),
budget: test_budget(),
})
.unwrap_or_else(|_| panic!("enqueue while alive"));
}
let mut batch = Vec::new();
registrations.drain_into(&mut batch);
assert_eq!(batch.len(), 2, "one drain takes everything queued");
assert!(
stream_keys.contains("a") && stream_keys.contains("b"),
"drained registrations still hold their claims"
);
registrations.drain_into(&mut batch);
assert_eq!(batch.len(), 2, "the queue must be empty after a drain");
drop(batch);
assert!(
!stream_keys.contains("a") && !stream_keys.contains("b"),
"dropping the batch must release the claims exactly once"
);
}
#[test]
fn full_registration_queue_refuses_enqueue_and_reopens_the_key() {
let stream_keys: Arc<dashmap::DashSet<String>> = Arc::new(dashmap::DashSet::new());
let registrations = RegistrationHandoff::new();
for i in 0..REGISTRATION_QUEUE_CAPACITY {
let (_feed_tx, feed_rx) = crossbeam_channel::bounded::<PublisherFeed>(1);
let claim = StreamKeyClaim::claim(stream_keys.clone(), format!("k-{i}"))
.expect("first claim must win");
registrations
.enqueue(PublisherRegistration {
claim,
source: PublisherSource::Feed(feed_rx),
budget: test_budget(),
})
.unwrap_or_else(|_| panic!("enqueue {i} within the bound must be accepted"));
}
let (_feed_tx, feed_rx) = crossbeam_channel::bounded::<PublisherFeed>(1);
let claim = StreamKeyClaim::claim(stream_keys.clone(), "overflow".to_string())
.expect("first claim must win");
let refused = registrations.enqueue(PublisherRegistration {
claim,
source: PublisherSource::Feed(feed_rx),
budget: test_budget(),
});
assert!(
matches!(refused, Err(EnqueueRefused::Full(_))),
"the enqueue past the bound must be refused as Full"
);
drop(refused);
assert!(
!stream_keys.contains("overflow"),
"a refused registration must release its key claim"
);
assert!(
StreamKeyClaim::claim(stream_keys.clone(), "overflow".to_string()).is_ok(),
"the key must be claimable again right after the refusal"
);
let mut batch = Vec::new();
while registrations.drain_into(&mut batch) {}
assert_eq!(
batch.len(),
REGISTRATION_QUEUE_CAPACITY,
"the refusal must leave the queued entries intact"
);
let (_feed_tx, feed_rx) = crossbeam_channel::bounded::<PublisherFeed>(1);
let claim = StreamKeyClaim::claim(stream_keys.clone(), "after-drain".to_string())
.expect("claim after the drain");
assert!(
registrations
.enqueue(PublisherRegistration {
claim,
source: PublisherSource::Feed(feed_rx),
budget: test_budget(),
})
.is_ok(),
"a drained intake must accept registrations again"
);
}
#[test]
fn drain_into_budgets_each_round_without_losing_or_reordering_entries() {
let stream_keys: Arc<dashmap::DashSet<String>> = Arc::new(dashmap::DashSet::new());
let registrations = RegistrationHandoff::new();
let total = MAX_REGISTRATIONS_PER_POLL * 2 + 3;
for i in 0..total {
let (_feed_tx, feed_rx) = crossbeam_channel::bounded::<PublisherFeed>(1);
let claim = StreamKeyClaim::claim(stream_keys.clone(), format!("k-{i:04}"))
.expect("first claim must win");
registrations
.enqueue(PublisherRegistration {
claim,
source: PublisherSource::Feed(feed_rx),
budget: test_budget(),
})
.unwrap_or_else(|_| panic!("enqueue {i} within the bound"));
}
let mut drained_keys = Vec::new();
let mut rounds = 0;
loop {
let mut batch = Vec::new();
let more = registrations.drain_into(&mut batch);
assert!(
batch.len() <= MAX_REGISTRATIONS_PER_POLL,
"one round must not exceed the drain budget"
);
drained_keys.extend(batch.iter().map(|r| r.claim.key().to_string()));
rounds += 1;
if !more {
break;
}
assert_eq!(
batch.len(),
MAX_REGISTRATIONS_PER_POLL,
"a round that reports a remainder must have used its whole budget"
);
}
assert_eq!(
rounds, 3,
"the backlog must spread across ceil(total / budget) rounds"
);
let expected: Vec<String> = (0..total).map(|i| format!("k-{i:04}")).collect();
assert_eq!(
drained_keys, expected,
"every entry must arrive exactly once, in enqueue order"
);
}
#[test]
fn worker_exit_without_consuming_registration_reopens_the_key() {
let stream_keys: Arc<dashmap::DashSet<String>> = Arc::new(dashmap::DashSet::new());
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let reactor = Reactor::new(3, None, status).expect("reactor");
let registrations = Arc::new(RegistrationHandoff::new());
let kill_switch = RegistrationKillSwitch::arm(registrations.clone());
let (_feed_tx, feed_rx) = crossbeam_channel::bounded::<PublisherFeed>(1);
let claim = StreamKeyClaim::claim(stream_keys.clone(), "held".to_string())
.expect("first claim must win");
registrations
.enqueue(PublisherRegistration {
claim,
source: PublisherSource::Feed(feed_rx),
budget: test_budget(),
})
.unwrap_or_else(|_| panic!("enqueue while the worker lives"));
assert!(
stream_keys.contains("held"),
"a queued registration keeps its key claimed"
);
drop(reactor);
drop(kill_switch);
assert!(
!stream_keys.contains("held"),
"worker exit must release a never-consumed registration's key claim"
);
assert!(
StreamKeyClaim::claim(stream_keys.clone(), "held".to_string()).is_ok(),
"a create for the same key must win again after the worker died"
);
}
#[test]
fn reactor_drop_with_live_consumed_publisher_reopens_the_key() {
let stream_keys: Arc<dashmap::DashSet<String>> = Arc::new(dashmap::DashSet::new());
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("reactor");
let registrations = Arc::new(RegistrationHandoff::new());
let kill_switch = RegistrationKillSwitch::arm(registrations.clone());
let (_feed_tx, feed_rx) = crossbeam_channel::bounded::<PublisherFeed>(1);
let claim = StreamKeyClaim::claim(stream_keys.clone(), "live".to_string())
.expect("first claim must win");
registrations
.enqueue(PublisherRegistration {
claim,
source: PublisherSource::Feed(feed_rx),
budget: test_budget(),
})
.unwrap_or_else(|_| panic!("enqueue while the worker lives"));
let mut batch = Vec::new();
kill_switch.handoff().drain_into(&mut batch);
assert_eq!(batch.len(), 1, "the queued registration must be drained");
for registration in batch.drain(..) {
reactor.add_publisher(registration).expect("accepted");
}
assert!(
stream_keys.contains("live"),
"an accepted publisher keeps its key claimed while the reactor lives"
);
drop(reactor);
assert!(
!stream_keys.contains("live"),
"dropping the reactor must release live publishers' key claims"
);
assert!(
StreamKeyClaim::claim(stream_keys.clone(), "live".to_string()).is_ok(),
"a create for the same key must win again after the reactor died"
);
drop(kill_switch);
}
#[test]
fn close_connection_after_flush_delivers_the_queued_tail() {
use std::io::Read;
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("reactor");
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let mut client = TcpStream::connect(addr).expect("Failed to connect");
let (server, _) = listener.accept().expect("Failed to accept");
let token = reactor
.add_connection(server)
.expect("Failed to add connection");
if let Some(conn) = reactor.connections.get_mut(token.id) {
conn.state = ConnectionState::Active;
assert!(conn.enqueue_data(Bytes::from_static(b"final status"), false, false, false, true, Instant::now()));
}
reactor.close_connection_after_flush(token.id);
assert!(
reactor.connections.get(token.id).is_none(),
"the connection must still be removed"
);
client
.set_read_timeout(Some(Duration::from_secs(2)))
.expect("set timeout");
let mut received = Vec::new();
let mut buf = [0u8; 64];
loop {
match client.read(&mut buf) {
Ok(0) => break,
Ok(n) => received.extend_from_slice(&buf[..n]),
Err(e) => panic!("read failed before EOF: {e:?}"),
}
}
assert_eq!(
received, b"final status",
"the tail queued in the closing round must be delivered"
);
}
#[cfg(unix)]
#[test]
fn close_lingers_then_drains_an_undeliverable_tail() {
use std::io::Read;
use std::os::unix::io::AsRawFd;
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("reactor");
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let mut client = TcpStream::connect(addr).expect("Failed to connect");
let (server, _) = listener.accept().expect("Failed to accept");
set_small_socket_buffer(server.as_raw_fd(), libc::SO_SNDBUF);
set_small_socket_buffer(client.as_raw_fd(), libc::SO_RCVBUF);
let token = reactor
.add_connection(server)
.expect("Failed to add connection");
let payload = vec![0xABu8; 64 * 1024];
if let Some(conn) = reactor.connections.get_mut(token.id) {
conn.state = ConnectionState::Active;
assert!(conn.enqueue_data(
Bytes::from(payload.clone()),
false,
true,
true,
true,
Instant::now(),
));
}
reactor.close_connection_after_flush(token.id);
assert!(
reactor.connections.get(token.id).is_some(),
"a half-written tail must not be dropped (that truncates the RTMP message)"
);
assert!(
reactor.connections.get(token.id).unwrap().is_condemned(),
"the connection must be condemned for a bounded drain"
);
client
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("set timeout");
let mut received = Vec::new();
let mut buf = vec![0u8; 64 * 1024];
let deadline = Instant::now() + Duration::from_secs(20);
loop {
assert!(Instant::now() < deadline, "drain watchdog expired");
if reactor.connections.get(token.id).is_some() {
if let Some(HandleResult::Disconnect(cid)) = reactor.handle_writable(token.id) {
reactor.close_connection_after_flush(cid);
}
}
match client.read(&mut buf) {
Ok(0) => break,
Ok(n) => received.extend_from_slice(&buf[..n]),
Err(ref e)
if e.kind() == std::io::ErrorKind::WouldBlock
|| e.kind() == std::io::ErrorKind::TimedOut => {}
Err(e) => panic!("read failed before EOF: {e:?}"),
}
}
assert_eq!(
received.len(),
payload.len(),
"the whole tail must be delivered before the close"
);
assert!(
received.iter().all(|&b| b == 0xAB),
"the delivered stream must be a byte-exact prefix, no corruption"
);
assert!(
reactor.connections.get(token.id).is_none(),
"the connection is removed once its tail has drained"
);
}
#[cfg(unix)]
#[test]
fn condemn_deadline_backstops_a_peer_that_never_reads() {
use std::os::unix::io::AsRawFd;
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("reactor");
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let client = TcpStream::connect(addr).expect("Failed to connect");
let (server, _) = listener.accept().expect("Failed to accept");
set_small_socket_buffer(server.as_raw_fd(), libc::SO_SNDBUF);
set_small_socket_buffer(client.as_raw_fd(), libc::SO_RCVBUF);
let token = reactor
.add_connection(server)
.expect("Failed to add connection");
let payload = vec![0u8; 1024 * 1024];
if let Some(conn) = reactor.connections.get_mut(token.id) {
conn.state = ConnectionState::Active;
assert!(conn.enqueue_data(Bytes::from(payload), false, true, true, true, Instant::now()));
}
reactor.close_connection_after_flush(token.id);
assert!(
reactor.connections.get(token.id).unwrap().is_condemned(),
"an undrainable tail must condemn the connection"
);
let past = Instant::now();
if let Some(conn) = reactor.connections.get_mut(token.id) {
conn.condemn(past);
}
reactor.last_timeout_check = past
.checked_sub(TIMEOUT_CHECK_INTERVAL + Duration::from_secs(1))
.expect("monotonic clock has headroom");
let expired = reactor.check_timeouts();
assert!(
expired.contains(&token.id),
"check_timeouts must collect a condemnation whose deadline passed"
);
reactor.close_connection_after_flush(token.id);
assert!(
reactor.connections.get(token.id).is_none(),
"an expired condemnation must be force-removed (bounded lingering)"
);
drop(client);
}
#[test]
fn condemned_connection_is_skipped_by_live_fanout_and_closes_on_drain() {
use std::io::Read;
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("reactor");
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let mut client = TcpStream::connect(addr).expect("Failed to connect");
let (server, _) = listener.accept().expect("Failed to accept");
let token = reactor
.add_connection(server)
.expect("Failed to add connection");
let tail = 4096usize;
if let Some(conn) = reactor.connections.get_mut(token.id) {
conn.state = ConnectionState::Active;
assert!(conn.enqueue_data(Bytes::from(vec![0xABu8; tail]), false, true, true, true, Instant::now()));
conn.condemn(Instant::now() + Duration::from_secs(30));
assert!(conn.is_condemned());
}
reactor.packets_buffer.push((
token.id,
Bytes::from(vec![0xCDu8; 1024 * 1024]),
true,
false,
true,
true,
));
reactor.write_pending_packets();
let conn = reactor.connections.get(token.id).expect("still present");
assert_eq!(
conn.queued_bytes(),
tail,
"post-condemn media must not grow a condemned connection's queue"
);
assert!(
!reactor.is_pending_flush(token.id),
"a skipped condemned target must not be re-queued for flush"
);
client
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("set timeout");
let mut received = Vec::new();
let mut buf = vec![0u8; 8192];
let deadline = Instant::now() + Duration::from_secs(20);
loop {
assert!(Instant::now() < deadline, "drain watchdog expired");
if reactor.connections.get(token.id).is_some() {
if let Some(HandleResult::Disconnect(cid)) = reactor.handle_writable(token.id) {
reactor.close_connection_after_flush(cid);
}
}
match client.read(&mut buf) {
Ok(0) => break,
Ok(n) => received.extend_from_slice(&buf[..n]),
Err(ref e)
if e.kind() == std::io::ErrorKind::WouldBlock
|| e.kind() == std::io::ErrorKind::TimedOut =>
{
if reactor.connections.get(token.id).is_none() {
break;
}
}
Err(e) => panic!("read failed before EOF: {e:?}"),
}
}
assert_eq!(
received.len(),
tail,
"only the pre-condemn tail must be delivered"
);
assert!(
received.iter().all(|&b| b == 0xAB),
"the skipped media must never appear in the delivered stream"
);
assert!(
reactor.connections.get(token.id).is_none(),
"the connection closes once its tail drains, not at the deadline"
);
}
#[test]
fn publisher_drain_item_budget_bounds_one_round() {
let stream_keys = Arc::new(dashmap::DashSet::new());
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("reactor");
let (feed_tx, feed_rx) = crossbeam_channel::bounded(MAX_PUBLISH_ITEMS_PER_POLL + 64);
let (budget_guard, budget) = IngressBudget::new(PUBLISHER_INGRESS_HIGH_WATER_BYTES);
let claim = StreamKeyClaim::claim(stream_keys, "live".to_string()).expect("claim");
reactor
.add_publisher(PublisherRegistration {
claim,
source: PublisherSource::Feed(feed_rx),
budget: budget_guard,
})
.expect("publisher registered");
for i in 0..(MAX_PUBLISH_ITEMS_PER_POLL + 6) {
send_acquired(
&budget,
&feed_tx,
PublisherFeed::Media {
tag_type: 0x08,
timestamp: RtmpTimestamp { value: i as u32 },
data: Bytes::from_static(&[0xaf, 0x01, 0x00]),
},
);
}
let (removed, pending) = reactor.process_publishers();
assert!(
removed.is_empty(),
"a budget stop must not remove the publisher"
);
assert!(pending, "hitting the item budget must report pending work");
assert_eq!(
feed_tx.len(),
6,
"exactly the item budget must be consumed in one round"
);
let (removed, pending) = reactor.process_publishers();
assert!(removed.is_empty());
assert!(!pending, "a drained channel must clear the pending flag");
assert_eq!(feed_tx.len(), 0, "the second round must clear the backlog");
}
#[test]
fn publisher_drain_byte_budget_bounds_one_round() {
let stream_keys = Arc::new(dashmap::DashSet::new());
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("reactor");
let (feed_tx, feed_rx) = crossbeam_channel::bounded(16);
let (budget_guard, budget) = IngressBudget::new(PUBLISHER_INGRESS_HIGH_WATER_BYTES);
let claim = StreamKeyClaim::claim(stream_keys, "live".to_string()).expect("claim");
reactor
.add_publisher(PublisherRegistration {
claim,
source: PublisherSource::Feed(feed_rx),
budget: budget_guard,
})
.expect("publisher registered");
let big = Bytes::from(vec![0u8; 200 * 1024]);
for i in 0..3u32 {
send_acquired(
&budget,
&feed_tx,
PublisherFeed::Media {
tag_type: 0x08,
timestamp: RtmpTimestamp { value: i },
data: big.clone(),
},
);
}
send_acquired(
&budget,
&feed_tx,
PublisherFeed::Media {
tag_type: 0x08,
timestamp: RtmpTimestamp { value: 3 },
data: Bytes::from_static(&[0xaf, 0x01, 0x00]),
},
);
let (removed, pending) = reactor.process_publishers();
assert!(removed.is_empty());
assert!(pending, "hitting the byte budget must report pending work");
assert_eq!(
feed_tx.len(),
1,
"the item that crossed the byte budget is still consumed; only the next one waits"
);
let (removed, pending) = reactor.process_publishers();
assert!(removed.is_empty());
assert!(!pending);
assert_eq!(feed_tx.len(), 0);
}
#[test]
fn publisher_drain_oversized_item_is_consumed_not_dropped() {
let stream_keys = Arc::new(dashmap::DashSet::new());
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("reactor");
let (feed_tx, feed_rx) = crossbeam_channel::bounded(4);
let (budget_guard, budget) = IngressBudget::new(PUBLISHER_INGRESS_HIGH_WATER_BYTES);
let claim = StreamKeyClaim::claim(stream_keys, "live".to_string()).expect("claim");
reactor
.add_publisher(PublisherRegistration {
claim,
source: PublisherSource::Feed(feed_rx),
budget: budget_guard,
})
.expect("publisher registered");
let mut oversized = vec![0u8; 600 * 1024];
oversized[0] = 0xaf;
oversized[1] = 0x00;
let oversized = Bytes::from(oversized);
send_acquired(
&budget,
&feed_tx,
PublisherFeed::Media {
tag_type: 0x08,
timestamp: RtmpTimestamp { value: 0 },
data: oversized.clone(),
},
);
let video_seq: &[u8] = &[0x17, 0x00, 0x00, 0x00, 0x00, 0x01, 0x64];
send_acquired(
&budget,
&feed_tx,
PublisherFeed::Media {
tag_type: 0x09,
timestamp: RtmpTimestamp { value: 1 },
data: Bytes::from_static(video_seq),
},
);
let (removed, pending) = reactor.process_publishers();
assert!(removed.is_empty());
assert!(pending, "an oversized item exhausts the byte budget");
assert_eq!(
reactor.scheduler.channel_audio_sequence_header("live"),
Some(oversized),
"the oversized item must reach the scheduler in the round that consumed it"
);
assert_eq!(
feed_tx.len(),
1,
"the follow-up item waits for the next round"
);
let (_, pending) = reactor.process_publishers();
assert!(!pending);
assert_eq!(
reactor
.scheduler
.channel_video_sequence_header("live")
.as_deref(),
Some(video_seq),
"the next round must deliver the remaining item"
);
}
fn spawn_parked_acquire(
budget: &Arc<IngressBudget>,
len: usize,
) -> (
std::thread::JoinHandle<()>,
std::sync::mpsc::Receiver<Result<(), IngressClosed>>,
) {
let (done_tx, done_rx) = std::sync::mpsc::channel();
let producer_budget = budget.clone();
let producer = std::thread::spawn(move || {
done_tx
.send(producer_budget.acquire(len))
.expect("report the acquire's result");
});
assert!(
done_rx.recv_timeout(Duration::from_millis(100)).is_err(),
"an acquire that must park completed immediately"
);
(producer, done_rx)
}
#[test]
fn ingress_budget_blocks_at_high_water_and_release_resumes() {
let (_guard, budget) = IngressBudget::new(8);
budget.acquire(6).expect("an empty account admits");
let (producer, done_rx) = spawn_parked_acquire(&budget, 6);
budget.release(6);
done_rx
.recv_timeout(Duration::from_secs(10))
.expect("the release must wake the parked producer")
.expect("the woken acquire must succeed");
producer.join().expect("producer thread exits");
assert_eq!(
budget.queued_bytes(),
6,
"only the resumed acquire may remain on the account"
);
}
#[test]
fn ingress_budget_admits_oversized_item_when_empty() {
let (_guard, budget) = IngressBudget::new(8);
budget
.acquire(100)
.expect("an oversized item must enter an empty account");
assert_eq!(
budget.queued_bytes(),
100,
"the account must reflect the full oversize"
);
let (producer, done_rx) = spawn_parked_acquire(&budget, 100);
budget.release(100);
done_rx
.recv_timeout(Duration::from_secs(10))
.expect("the full release must wake the second producer")
.expect("the woken oversized acquire must succeed");
producer.join().expect("producer thread exits");
assert_eq!(budget.queued_bytes(), 100);
}
#[test]
fn ingress_budget_admission_is_fifo_under_contention() {
let (_guard, budget) = IngressBudget::new(8);
budget.acquire(6).expect("an empty account admits");
let (large, large_rx) = spawn_parked_acquire(&budget, 7);
let deadline = Instant::now() + Duration::from_secs(5);
while budget.waiting_acquires() < 1 {
assert!(
Instant::now() < deadline,
"the large acquire never took its ticket"
);
std::thread::yield_now();
}
let (small, small_rx) = spawn_parked_acquire(&budget, 1);
while budget.waiting_acquires() < 2 {
assert!(
Instant::now() < deadline,
"the small acquire never took its ticket"
);
std::thread::yield_now();
}
budget.release(6);
large_rx
.recv_timeout(Duration::from_secs(10))
.expect("the release must admit the head acquire")
.expect("the head acquire succeeds");
small_rx
.recv_timeout(Duration::from_secs(10))
.expect("the head's admission must chain to the next ticket")
.expect("the chained acquire succeeds");
large.join().expect("large producer exits");
small.join().expect("small producer exits");
assert_eq!(
budget.queued_bytes(),
8,
"exactly the two post-drain acquires remain on the account"
);
}
#[test]
fn ingress_budget_close_and_guard_drop_wake_blocked_producers() {
let (guard, budget) = IngressBudget::new(8);
budget.acquire(6).expect("an empty account admits");
let (producer, done_rx) = spawn_parked_acquire(&budget, 6);
budget.close();
assert!(
done_rx
.recv_timeout(Duration::from_secs(10))
.expect("the close must wake the parked producer")
.is_err(),
"a woken producer must observe the closed account"
);
producer.join().expect("producer thread exits");
assert!(
budget.acquire(1).is_err(),
"later acquires must be refused outright"
);
drop(guard);
let (guard, budget) = IngressBudget::new(8);
budget.acquire(6).expect("an empty account admits");
let (producer, done_rx) = spawn_parked_acquire(&budget, 6);
drop(guard);
assert!(
done_rx
.recv_timeout(Duration::from_secs(10))
.expect("the guard drop must wake the parked producer")
.is_err(),
"a woken producer must observe the closed account"
);
producer.join().expect("producer thread exits");
}
#[test]
fn process_publishers_releases_drained_bytes() {
let stream_keys = Arc::new(dashmap::DashSet::new());
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("reactor");
let (feed_tx, feed_rx) = crossbeam_channel::bounded(8);
let (budget_guard, budget) = IngressBudget::new(8);
let claim = StreamKeyClaim::claim(stream_keys, "live".to_string()).expect("claim");
reactor
.add_publisher(PublisherRegistration {
claim,
source: PublisherSource::Feed(feed_rx),
budget: budget_guard,
})
.expect("publisher registered");
let data = Bytes::from(vec![0xafu8; 32]);
budget
.acquire(data.len())
.expect("an empty account admits the first item");
feed_tx
.send(PublisherFeed::Media {
tag_type: 0x08,
timestamp: RtmpTimestamp { value: 0 },
data,
})
.unwrap();
let (producer, done_rx) = spawn_parked_acquire(&budget, 4);
let (removed, _) = reactor.process_publishers();
assert!(removed.is_empty(), "a healthy publisher must not be removed");
done_rx
.recv_timeout(Duration::from_secs(10))
.expect("the drain round's release must wake the producer")
.expect("the woken acquire must succeed");
producer.join().expect("producer thread exits");
assert_eq!(
budget.queued_bytes(),
4,
"the round must release exactly what it drained; only the resumed acquire remains"
);
}
#[test]
fn remove_publisher_closes_ingress_budget() {
let stream_keys = Arc::new(dashmap::DashSet::new());
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("reactor");
let (_feed_tx, feed_rx) = crossbeam_channel::bounded::<PublisherFeed>(8);
let (budget_guard, budget) = IngressBudget::new(8);
let claim = StreamKeyClaim::claim(stream_keys, "live".to_string()).expect("claim");
let id = reactor
.add_publisher(PublisherRegistration {
claim,
source: PublisherSource::Feed(feed_rx),
budget: budget_guard,
})
.expect("publisher registered");
budget.acquire(6).expect("an empty account admits");
let (producer, done_rx) = spawn_parked_acquire(&budget, 6);
reactor.remove_publisher(id);
assert!(
done_rx
.recv_timeout(Duration::from_secs(10))
.expect("the removal must wake the parked producer")
.is_err(),
"a producer woken by removal must observe the closed account"
);
producer.join().expect("producer thread exits");
}
#[test]
fn refused_registration_closes_ingress_budget() {
let registrations = RegistrationHandoff::new();
registrations.close();
let stream_keys: Arc<dashmap::DashSet<String>> = Arc::new(dashmap::DashSet::new());
let (_feed_tx, feed_rx) = crossbeam_channel::bounded::<PublisherFeed>(1);
let (budget_guard, budget) = IngressBudget::new(8);
let claim =
StreamKeyClaim::claim(stream_keys.clone(), "refused".to_string()).expect("claim");
budget.acquire(6).expect("an empty account admits");
let (producer, done_rx) = spawn_parked_acquire(&budget, 6);
let refused = registrations.enqueue(PublisherRegistration {
claim,
source: PublisherSource::Feed(feed_rx),
budget: budget_guard,
});
assert!(
matches!(refused, Err(EnqueueRefused::Closed(_))),
"the closed intake must refuse the registration"
);
drop(refused);
assert!(
done_rx
.recv_timeout(Duration::from_secs(10))
.expect("dropping the refusal must wake the parked producer")
.is_err(),
"a producer woken by the refusal must observe the closed account"
);
producer.join().expect("producer thread exits");
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("reactor");
assert!(reactor.scheduler.new_channel("net".to_string(), 777));
let (_feed_tx2, feed_rx2) = crossbeam_channel::bounded::<PublisherFeed>(1);
let (budget_guard, budget) = IngressBudget::new(8);
let claim = StreamKeyClaim::claim(stream_keys, "net".to_string()).expect("claim");
budget.acquire(6).expect("an empty account admits");
let (producer, done_rx) = spawn_parked_acquire(&budget, 6);
assert!(
reactor
.add_publisher(PublisherRegistration {
claim,
source: PublisherSource::Feed(feed_rx2),
budget: budget_guard,
})
.is_none(),
"a key already being published must be refused"
);
assert!(
done_rx
.recv_timeout(Duration::from_secs(10))
.expect("the scheduler refusal must wake the parked producer")
.is_err(),
"a producer woken by the refusal must observe the closed account"
);
producer.join().expect("producer thread exits");
}
fn reactor_with_handshake_bytes_pending() -> (Reactor, ConnectionToken, TcpStream) {
use std::io::Write;
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("Failed to create reactor");
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let mut client = TcpStream::connect(addr).expect("Failed to connect");
let (server, _) = listener.accept().expect("Failed to accept");
let token = reactor
.add_connection(server)
.expect("Failed to add connection");
let mut c0c1 = vec![0u8; 1537];
c0c1[0] = 3;
for (i, b) in c0c1[9..].iter_mut().enumerate() {
*b = (i % 251) as u8;
}
client.write_all(&c0c1).expect("write C0+C1");
client.flush().ok();
std::thread::sleep(Duration::from_millis(100));
(reactor, token, client)
}
#[test]
fn resume_capped_reads_skips_ids_slated_for_close() {
let (mut reactor, token, _client) = reactor_with_handshake_bytes_pending();
let mut ids_to_close = vec![token.id];
reactor.resume_capped_reads(vec![token.id], &[], &mut ids_to_close);
assert!(
!reactor.is_pending_flush(token.id),
"a close-slated id must not be re-read by the resume pass"
);
reactor.resume_capped_reads(vec![token.id], &[], &mut Vec::new());
assert!(
reactor.is_pending_flush(token.id),
"an unblocked resume must read the pending handshake bytes"
);
reactor.remove_connection(token.id);
}
#[test]
fn resume_capped_reads_skips_ids_already_read_this_pass() {
let (mut reactor, token, _client) = reactor_with_handshake_bytes_pending();
reactor.resume_capped_reads(vec![token.id], &[token.id], &mut Vec::new());
assert!(
!reactor.is_pending_flush(token.id),
"an id already read by the event pass must not be read again"
);
reactor.resume_capped_reads(vec![token.id], &[], &mut Vec::new());
assert!(
reactor.is_pending_flush(token.id),
"an unblocked resume must read the pending handshake bytes"
);
reactor.remove_connection(token.id);
}
#[test]
fn remove_connection_scrubs_read_pending() {
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let mut reactor = Reactor::new(3, None, status).expect("Failed to create reactor");
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind");
let addr = listener.local_addr().expect("Failed to get address");
let _client = TcpStream::connect(addr).expect("Failed to connect");
let (server, _) = listener.accept().expect("Failed to accept");
let token = reactor
.add_connection(server)
.expect("Failed to add connection");
reactor.read_pending.insert(token.id);
reactor.remove_connection(token.id);
assert!(
!reactor.read_pending.contains(&token.id),
"removal must scrub the id or a slab-reusing new connection would be read out of turn"
);
}
}