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, 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;
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;
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>,
}
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,
})
}
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
}
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,
},
}
#[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);
}
}
}
pub(crate) struct PublisherRegistration {
pub(crate) claim: StreamKeyClaim,
pub(crate) source: PublisherSource,
}
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,
}
type OutboundWrite = (usize, Bytes, bool, bool, bool, bool);
fn collect_server_results(
server_results: Vec<ServerResult>,
packets_to_write: &mut Vec<OutboundWrite>,
ids_to_close: &mut Vec<usize>,
) {
for result in server_results {
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: HashSet<usize>,
read_pending: HashSet<usize>,
interest_dirty: HashSet<usize>,
conn_ids_buffer: Vec<usize>,
packets_buffer: Vec<OutboundWrite>,
ids_to_close_buffer: Vec<usize>,
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: HashSet::with_capacity(256),
read_pending: HashSet::new(),
interest_dirty: HashSet::with_capacity(256),
conn_ids_buffer: Vec::with_capacity(1024),
packets_buffer: Vec::with_capacity(64),
ids_to_close_buffer: Vec::with_capacity(16),
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 {
self.interest_dirty.insert(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 } = 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 });
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;
}
self.pending_flush.insert(id);
self.interest_dirty.insert(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);
match self
.scheduler
.bytes_received_with_backlog(id, data, backlog)
{
Ok(server_results) => {
for result in server_results {
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) {
self.conn_ids_buffer.clear();
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 {
self.conn_ids_buffer.push(target_id);
} else {
self.ids_to_close_buffer.push(target_id);
}
}
}
for &id in &self.conn_ids_buffer {
self.pending_flush.insert(id);
self.interest_dirty.insert(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));
}
self.interest_dirty.insert(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 {
match self.scheduler.publish_bytes_received(pub_id, bytes) {
Ok(server_results) => {
collect_server_results(server_results, 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 = {
let pub_state = match self.publishers.get(pub_id) {
Some(p) => p,
None => continue,
};
pub_state.source.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 results = self.scheduler.abort_publisher_watchers(pub_id);
collect_server_results(
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 results = self.scheduler.abort_publisher_watchers(pub_id);
collect_server_results(
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();
let results = self
.scheduler
.publish_media_received(pub_id, tag_type, timestamp, data);
collect_server_results(
results,
&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;
}
}
},
}
}
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) {
match self.scheduler.publish_bytes_received(pub_id, packet.bytes) {
Ok(server_results) => {
collect_server_results(server_results, 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();
let pending_ids: Vec<usize> = self.pending_flush.drain().collect();
for id in pending_ids {
if let Some(conn) = self.connections.get_mut(id) {
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) => {
self.interest_dirty.insert(id);
}
}
} else if conn.is_condemned() {
ids_to_close.push(id);
} else {
self.interest_dirty.insert(id);
}
}
}
ids_to_close
}
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);
self.pending_flush.insert(id);
self.interest_dirty.insert(id);
} else {
timed_out.push(id);
}
}
timed_out
}
fn update_dirty_interests(&mut self) -> Vec<usize> {
let dirty_ids: Vec<usize> = self.interest_dirty.drain().collect();
let mut ids_to_close = Vec::new();
for id in dirty_ids {
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);
}
}
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 {
self.interest_dirty.contains(&id)
}
#[cfg(test)]
pub fn is_pending_flush(&self, id: usize) -> bool {
self.pending_flush.contains(&id)
}
#[cfg(test)]
pub fn drain_interest_dirty(&mut self) -> Vec<usize> {
self.interest_dirty.drain().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[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.pending_flush.insert(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.pending_flush.insert(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.pending_flush.insert(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 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 claim = StreamKeyClaim::claim(stream_keys, "live".to_string()).expect("claim");
let pub_id = reactor
.add_publisher(PublisherRegistration {
claim,
source: PublisherSource::Feed(feed_rx),
})
.expect("publisher registered");
for control in
build_publish_control("app".to_string(), "live".to_string()).expect("control")
{
feed_tx.send(PublisherFeed::Raw(control)).unwrap();
}
let video_seq: &[u8] = &[0x17, 0x00, 0x00, 0x00, 0x00, 0x01, 0x64];
let audio_seq: &[u8] = &[0xaf, 0x00, 0x12, 0x10];
feed_tx
.send(PublisherFeed::Media {
tag_type: 0x09,
timestamp: RtmpTimestamp { value: 0 },
data: Bytes::from_static(video_seq),
})
.unwrap();
feed_tx
.send(PublisherFeed::Media {
tag_type: 0x08,
timestamp: RtmpTimestamp { value: 0 },
data: Bytes::from_static(audio_seq),
})
.unwrap();
feed_tx
.send(PublisherFeed::Media {
tag_type: 0x09,
timestamp: RtmpTimestamp { value: 33 },
data: Bytes::from_static(&[0x17, 0x01, 0xAA, 0xBB]),
})
.unwrap();
feed_tx
.send(PublisherFeed::Media {
tag_type: 0x09,
timestamp: RtmpTimestamp { value: 66 },
data: Bytes::from_static(&[0x17, 0x01, 0xCC, 0xDD]),
})
.unwrap();
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),
})
.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),
})
.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),
})
.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),
})
.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),
})
.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),
});
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),
})
.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),
})
.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),
});
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),
})
.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),
})
.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),
})
.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),
})
.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 claim = StreamKeyClaim::claim(stream_keys, "live".to_string()).expect("claim");
reactor
.add_publisher(PublisherRegistration {
claim,
source: PublisherSource::Feed(feed_rx),
})
.expect("publisher registered");
for i in 0..(MAX_PUBLISH_ITEMS_PER_POLL + 6) {
feed_tx
.send(PublisherFeed::Media {
tag_type: 0x08,
timestamp: RtmpTimestamp { value: i as u32 },
data: Bytes::from_static(&[0xaf, 0x01, 0x00]),
})
.unwrap();
}
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 claim = StreamKeyClaim::claim(stream_keys, "live".to_string()).expect("claim");
reactor
.add_publisher(PublisherRegistration {
claim,
source: PublisherSource::Feed(feed_rx),
})
.expect("publisher registered");
let big = Bytes::from(vec![0u8; 200 * 1024]);
for i in 0..3u32 {
feed_tx
.send(PublisherFeed::Media {
tag_type: 0x08,
timestamp: RtmpTimestamp { value: i },
data: big.clone(),
})
.unwrap();
}
feed_tx
.send(PublisherFeed::Media {
tag_type: 0x08,
timestamp: RtmpTimestamp { value: 3 },
data: Bytes::from_static(&[0xaf, 0x01, 0x00]),
})
.unwrap();
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 claim = StreamKeyClaim::claim(stream_keys, "live".to_string()).expect("claim");
reactor
.add_publisher(PublisherRegistration {
claim,
source: PublisherSource::Feed(feed_rx),
})
.expect("publisher registered");
let mut oversized = vec![0u8; 600 * 1024];
oversized[0] = 0xaf;
oversized[1] = 0x00;
let oversized = Bytes::from(oversized);
feed_tx
.send(PublisherFeed::Media {
tag_type: 0x08,
timestamp: RtmpTimestamp { value: 0 },
data: oversized.clone(),
})
.unwrap();
let video_seq: &[u8] = &[0x17, 0x00, 0x00, 0x00, 0x00, 0x01, 0x64];
feed_tx
.send(PublisherFeed::Media {
tag_type: 0x09,
timestamp: RtmpTimestamp { value: 1 },
data: Bytes::from_static(video_seq),
})
.unwrap();
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 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"
);
}
}