use std::collections::{BTreeSet, HashMap, HashSet};
use std::net::{IpAddr, SocketAddr};
use std::sync::{Arc, Mutex as StdMutex};
use std::time::{Duration, Instant};
use tokio::net::UdpSocket;
use tokio::sync::Mutex;
use tracing::{debug, error, info};
use super::validation::{PlatformSocketStrategy, PlatformType};
use crate::error::Error;
use crate::Result;
pub const DEFAULT_RTP_PORT_RANGE_START: u16 = 16384; pub const DEFAULT_RTP_PORT_RANGE_END: u16 = 32767;
pub const MIN_PORT: u16 = 1024;
const PORT_REUSE_DELAY_MS: u64 = 1000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AllocationStrategy {
Sequential,
Random,
Incremental,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PairingStrategy {
Adjacent,
Separate,
Muxed,
}
#[derive(Debug, Clone)]
pub struct PortAllocatorConfig {
pub port_range_start: u16,
pub port_range_end: u16,
pub allocation_strategy: AllocationStrategy,
pub pairing_strategy: PairingStrategy,
pub prefer_port_reuse: bool,
pub default_ip: IpAddr,
pub allocation_retries: u32,
pub validate_ports: bool,
pub capacity_hint: usize,
}
impl Default for PortAllocatorConfig {
fn default() -> Self {
Self {
port_range_start: DEFAULT_RTP_PORT_RANGE_START,
port_range_end: DEFAULT_RTP_PORT_RANGE_END,
allocation_strategy: AllocationStrategy::Random,
pairing_strategy: PairingStrategy::Muxed, prefer_port_reuse: true,
default_ip: IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED),
allocation_retries: 50,
validate_ports: true,
capacity_hint: 0,
}
}
}
struct ReleasedPort {
port: u16,
ip: IpAddr,
released_at: Instant,
}
struct IndexedAllocatorState {
available_ports: BTreeSet<u16>,
allocated_ports: HashSet<u16>,
session_ports: HashMap<String, Vec<(IpAddr, u16)>>,
last_port: u16,
}
pub struct PortAllocator {
config: PortAllocatorConfig,
indexed_state: Option<Arc<StdMutex<IndexedAllocatorState>>>,
allocated_ports: Arc<Mutex<HashSet<u16>>>,
released_ports: Arc<Mutex<Vec<ReleasedPort>>>,
last_released_cleanup: Arc<Mutex<Instant>>,
session_ports: Arc<Mutex<HashMap<String, Vec<(IpAddr, u16)>>>>,
last_port: Arc<Mutex<u16>>,
socket_strategy: PlatformSocketStrategy,
}
impl PortAllocator {
pub fn new() -> Self {
Self::with_config(PortAllocatorConfig::default())
}
pub fn with_config(config: PortAllocatorConfig) -> Self {
let socket_strategy = PlatformSocketStrategy::for_current_platform();
let indexed_state = Self::indexed_pool_enabled(&config).then(|| {
Arc::new(StdMutex::new(IndexedAllocatorState {
available_ports: (config.port_range_start..=config.port_range_end).collect(),
allocated_ports: HashSet::with_capacity(config.capacity_hint),
session_ports: HashMap::with_capacity(config.capacity_hint),
last_port: config.port_range_start,
}))
});
Self {
config: config.clone(),
indexed_state,
allocated_ports: Arc::new(Mutex::new(HashSet::with_capacity(config.capacity_hint))),
released_ports: Arc::new(Mutex::new(Vec::with_capacity(
config.capacity_hint.min(1024),
))),
last_released_cleanup: Arc::new(Mutex::new(Instant::now())),
session_ports: Arc::new(Mutex::new(HashMap::with_capacity(config.capacity_hint))),
last_port: Arc::new(Mutex::new(config.port_range_start)),
socket_strategy,
}
}
fn indexed_pool_enabled(config: &PortAllocatorConfig) -> bool {
matches!(
config.allocation_strategy,
AllocationStrategy::Sequential | AllocationStrategy::Incremental
) && matches!(
config.pairing_strategy,
PairingStrategy::Muxed | PairingStrategy::Separate
) && !config.prefer_port_reuse
&& !config.validate_ports
}
pub fn socket_strategy(&self) -> PlatformSocketStrategy {
self.socket_strategy.clone()
}
pub fn set_socket_strategy(&mut self, strategy: PlatformSocketStrategy) {
self.socket_strategy = strategy;
}
pub async fn allocate_port_pair(
&self,
session_id: &str,
ip: Option<IpAddr>,
) -> Result<(SocketAddr, Option<SocketAddr>)> {
let ip = ip.unwrap_or(self.config.default_ip);
if self.indexed_state.is_some() {
return self.allocate_indexed_port_pair(session_id, ip);
}
self.cleanup_released_ports().await;
match self.config.pairing_strategy {
PairingStrategy::Muxed => {
let port = self.allocate_port(ip).await?;
let socket_addr = SocketAddr::new(ip, port);
self.track_allocation(session_id, ip, port).await?;
Ok((socket_addr, None))
}
PairingStrategy::Adjacent => {
let mut retries = 0;
while retries < self.config.allocation_retries {
let mut port = match self.config.allocation_strategy {
AllocationStrategy::Sequential => self.get_next_sequential_port().await,
AllocationStrategy::Random => self.get_random_port().await,
AllocationStrategy::Incremental => self.get_next_incremental_port().await,
};
if port % 2 != 0 {
port -= 1;
if port < self.config.port_range_start {
port =
self.config.port_range_start + (self.config.port_range_start % 2);
}
}
let rtp_port = port;
let rtcp_port = port + 1;
if rtcp_port > self.config.port_range_end {
retries += 1;
continue;
}
let mut allocated = self.allocated_ports.lock().await;
if !allocated.contains(&rtp_port) && !allocated.contains(&rtcp_port) {
allocated.insert(rtp_port);
allocated.insert(rtcp_port);
drop(allocated);
self.track_allocation(session_id, ip, rtp_port).await?;
self.track_allocation(session_id, ip, rtcp_port).await?;
let rtp_addr = SocketAddr::new(ip, rtp_port);
let rtcp_addr = SocketAddr::new(ip, rtcp_port);
debug!(
"Allocated adjacent ports {} and {} for session {}",
rtp_port, rtcp_port, session_id
);
return Ok((rtp_addr, Some(rtcp_addr)));
}
retries += 1;
}
return Err(Error::Transport(
"Failed to allocate adjacent port pair after maximum retries".to_string(),
));
}
PairingStrategy::Separate => {
let rtp_port = self.allocate_port(ip).await?;
let rtcp_port = self.allocate_port(ip).await?;
let rtp_addr = SocketAddr::new(ip, rtp_port);
let rtcp_addr = SocketAddr::new(ip, rtcp_port);
self.track_allocation(session_id, ip, rtp_port).await?;
self.track_allocation(session_id, ip, rtcp_port).await?;
Ok((rtp_addr, Some(rtcp_addr)))
}
}
}
pub async fn allocate_port(&self, ip: IpAddr) -> Result<u16> {
if self.indexed_state.is_some() {
return self.allocate_indexed_unvalidated();
}
let mut retries = 0;
while retries < self.config.allocation_retries {
if self.config.prefer_port_reuse {
if let Some(port) = self.find_reusable_port(ip).await {
if self.claim_port(ip, port).await {
return Ok(port);
}
}
}
let port = match self.config.allocation_strategy {
AllocationStrategy::Sequential => self.get_next_sequential_port().await,
AllocationStrategy::Random => self.get_random_port().await,
AllocationStrategy::Incremental => self.get_next_incremental_port().await,
};
if self.claim_port(ip, port).await {
return Ok(port);
}
retries += 1;
}
Err(Error::Transport(
"Failed to allocate port after maximum retries".to_string(),
))
}
fn allocate_indexed_port_pair(
&self,
session_id: &str,
ip: IpAddr,
) -> Result<(SocketAddr, Option<SocketAddr>)> {
let state = self
.indexed_state
.as_ref()
.ok_or_else(|| Error::Transport("Indexed port pool is not configured".to_string()))?;
let mut state = state
.lock()
.map_err(|_| Error::Transport("Indexed port pool lock poisoned".to_string()))?;
match self.config.pairing_strategy {
PairingStrategy::Muxed => {
let port = self.take_indexed_port(&mut state)?;
self.track_indexed_allocation(&mut state, session_id, ip, port);
Ok((SocketAddr::new(ip, port), None))
}
PairingStrategy::Separate => {
let rtp_port = self.take_indexed_port(&mut state)?;
let rtcp_port = match self.take_indexed_port(&mut state) {
Ok(port) => port,
Err(e) => {
self.release_indexed_port(&mut state, rtp_port);
return Err(e);
}
};
self.track_indexed_allocation(&mut state, session_id, ip, rtp_port);
self.track_indexed_allocation(&mut state, session_id, ip, rtcp_port);
Ok((
SocketAddr::new(ip, rtp_port),
Some(SocketAddr::new(ip, rtcp_port)),
))
}
PairingStrategy::Adjacent => Err(Error::Transport(
"Indexed port pool does not support adjacent port pairs".to_string(),
)),
}
}
fn allocate_indexed_unvalidated(&self) -> Result<u16> {
let state = self
.indexed_state
.as_ref()
.ok_or_else(|| Error::Transport("Indexed port pool is not configured".to_string()))?;
let mut state = state
.lock()
.map_err(|_| Error::Transport("Indexed port pool lock poisoned".to_string()))?;
self.take_indexed_port(&mut state)
}
fn take_indexed_port(&self, state: &mut IndexedAllocatorState) -> Result<u16> {
loop {
let cursor = state.last_port;
let port = state
.available_ports
.range(cursor..)
.next()
.copied()
.or_else(|| state.available_ports.iter().next().copied());
let Some(port) = port else {
return Err(Error::Transport(
"Failed to allocate port after maximum retries".to_string(),
));
};
state.available_ports.remove(&port);
if state.allocated_ports.contains(&port) {
continue;
}
state.allocated_ports.insert(port);
state.last_port = if port >= self.config.port_range_end {
self.config.port_range_start
} else {
port + 1
};
return Ok(port);
}
}
fn track_indexed_allocation(
&self,
state: &mut IndexedAllocatorState,
session_id: &str,
ip: IpAddr,
port: u16,
) {
state
.session_ports
.entry(session_id.to_string())
.or_default()
.push((ip, port));
}
fn release_indexed_port(&self, state: &mut IndexedAllocatorState, port: u16) {
if state.allocated_ports.remove(&port)
&& port >= self.config.port_range_start
&& port <= self.config.port_range_end
{
state.available_ports.insert(port);
}
}
pub async fn release_session(&self, session_id: &str) -> Result<()> {
if let Some(state) = &self.indexed_state {
let mut state = state
.lock()
.map_err(|_| Error::Transport("Indexed port pool lock poisoned".to_string()))?;
if let Some(ports) = state.session_ports.remove(session_id) {
for (_ip, port) in ports {
self.release_indexed_port(&mut state, port);
}
return Ok(());
}
return Err(Error::Transport(format!(
"No session found with ID: {}",
session_id
)));
}
let mut sessions = self.session_ports.lock().await;
if let Some(ports) = sessions.remove(session_id) {
for (ip, port) in ports {
self.release_port(ip, port).await;
}
Ok(())
} else {
Err(Error::Transport(format!(
"No session found with ID: {}",
session_id
)))
}
}
pub async fn release_port(&self, ip: IpAddr, port: u16) {
if let Some(state) = &self.indexed_state {
match state.lock() {
Ok(mut state) => self.release_indexed_port(&mut state, port),
Err(_) => error!("Indexed port pool lock poisoned while releasing {}", port),
}
debug!("Released port {} on {}", port, ip);
return;
}
let removed = {
let mut allocated = self.allocated_ports.lock().await;
allocated.remove(&port)
};
if !removed {
debug!("Ignored release for unallocated port {} on {}", port, ip);
return;
}
if !self.config.prefer_port_reuse {
debug!("Released port {} on {}", port, ip);
return;
}
{
let mut released = self.released_ports.lock().await;
released.push(ReleasedPort {
port,
ip,
released_at: Instant::now(),
});
}
debug!("Released port {} on {}", port, ip);
}
pub async fn create_validated_socket(&self, addr: SocketAddr) -> Result<UdpSocket> {
let socket = UdpSocket::bind(addr)
.await
.map_err(|e| Error::Transport(format!("Failed to bind socket to {}: {}", addr, e)))?;
self.socket_strategy
.apply_to_socket(&socket)
.await
.map_err(|e| Error::Transport(format!("Failed to apply socket settings: {}", e)))?;
Ok(socket)
}
pub async fn allocated_count(&self) -> usize {
if let Some(state) = &self.indexed_state {
return match state.lock() {
Ok(state) => state.allocated_ports.len(),
Err(_) => 0,
};
}
let allocated = self.allocated_ports.lock().await;
allocated.len()
}
pub fn total_ports(&self) -> usize {
(self.config.port_range_end - self.config.port_range_start + 1) as usize
}
async fn find_reusable_port(&self, ip: IpAddr) -> Option<u16> {
let mut released = self.released_ports.lock().await;
let now = Instant::now();
let reuse_delay = Duration::from_millis(PORT_REUSE_DELAY_MS);
let index = released
.iter()
.position(|p| p.ip == ip && now.duration_since(p.released_at) > reuse_delay);
if let Some(idx) = index {
let port = released.swap_remove(idx).port;
Some(port)
} else {
None
}
}
async fn claim_port(&self, ip: IpAddr, port: u16) -> bool {
if port < self.config.port_range_start || port > self.config.port_range_end {
return false;
}
let mut allocated = self.allocated_ports.lock().await;
if allocated.contains(&port) {
return false;
}
if self.config.validate_ports {
let addr = SocketAddr::new(ip, port);
allocated.insert(port);
drop(allocated);
match UdpSocket::bind(addr).await {
Ok(socket) => {
let local_addr = socket.local_addr().ok();
drop(socket);
if let Some(local_addr) = local_addr {
debug!(
"Successfully validated port {} (bound to {})",
port, local_addr
);
}
true
}
Err(e) => {
debug!("Failed to bind to port {}: {}", port, e);
let mut allocated = self.allocated_ports.lock().await;
allocated.remove(&port);
false
}
}
} else {
allocated.insert(port);
true
}
}
async fn get_next_sequential_port(&self) -> u16 {
let mut last_port = self.last_port.lock().await;
let port = *last_port;
*last_port = if port + 1 > self.config.port_range_end {
self.config.port_range_start
} else {
port + 1
};
port
}
async fn get_next_incremental_port(&self) -> u16 {
let mut last_port = self.last_port.lock().await;
let port = *last_port;
*last_port = if port + 1 > self.config.port_range_end {
self.config.port_range_start
} else {
port + 1
};
port
}
async fn get_random_port(&self) -> u16 {
use rand::Rng;
let range_size = self.config.port_range_end - self.config.port_range_start + 1;
let offset = rand::thread_rng().gen_range(0..range_size);
self.config.port_range_start + offset
}
async fn track_allocation(&self, session_id: &str, ip: IpAddr, port: u16) -> Result<()> {
let mut sessions = self.session_ports.lock().await;
let session_ports = sessions
.entry(session_id.to_string())
.or_insert_with(Vec::new);
session_ports.push((ip, port));
Ok(())
}
async fn cleanup_released_ports(&self) {
if !self.config.prefer_port_reuse {
return;
}
let now = Instant::now();
{
let mut last_cleanup = self.last_released_cleanup.lock().await;
if now.duration_since(*last_cleanup) < Duration::from_secs(1) {
return;
}
*last_cleanup = now;
}
let mut released = self.released_ports.lock().await;
let reuse_delay = Duration::from_millis(PORT_REUSE_DELAY_MS * 10);
released.retain(|p| now.duration_since(p.released_at) <= reuse_delay);
}
}
pub struct GlobalPortAllocator;
impl GlobalPortAllocator {
pub async fn configure(start_port: u16, end_port: u16) -> Result<()> {
static INSTANCE: once_cell::sync::OnceCell<Mutex<Option<Arc<PortAllocator>>>> =
once_cell::sync::OnceCell::new();
let static_mutex = INSTANCE.get_or_init(|| Mutex::new(None));
let mut allocator = static_mutex.lock().await;
if allocator.is_some() {
return Err(Error::Transport(
"Cannot reconfigure GlobalPortAllocator after it has been initialized".to_string(),
));
}
let mut config = PortAllocatorConfig::default();
config.port_range_start = start_port;
config.port_range_end = end_port;
match PlatformType::current() {
PlatformType::Windows => {
config.allocation_retries = 15;
}
PlatformType::MacOS => {
config.allocation_retries = 12;
}
PlatformType::Linux => {
config.allocation_retries = 8;
}
_ => {}
}
let port_allocator = PortAllocator::with_config(config.clone());
*allocator = Some(Arc::new(port_allocator));
info!(
"Configured global port allocator with range {}-{}",
start_port, end_port
);
Ok(())
}
pub async fn instance() -> Arc<PortAllocator> {
static INSTANCE: once_cell::sync::OnceCell<Mutex<Option<Arc<PortAllocator>>>> =
once_cell::sync::OnceCell::new();
let static_mutex = INSTANCE.get_or_init(|| Mutex::new(None));
let mut allocator = static_mutex.lock().await;
if allocator.is_none() {
let mut config = PortAllocatorConfig::default();
match PlatformType::current() {
PlatformType::Windows => {
config.allocation_retries = 15;
config.port_range_start = 20000;
config.port_range_end = 30000;
}
PlatformType::MacOS => {
config.allocation_retries = 12;
}
PlatformType::Linux => {
config.allocation_retries = 8;
}
_ => {}
}
let port_allocator = PortAllocator::with_config(config.clone());
*allocator = Some(Arc::new(port_allocator));
if allocator.is_some() {
info!(
"Created global port allocator with range {}-{}",
config.port_range_start, config.port_range_end
);
}
}
allocator.as_ref().unwrap().clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::runtime::Runtime;
#[test]
fn test_port_allocator_creation() {
let allocator = PortAllocator::new();
assert_eq!(
allocator.config.port_range_start,
DEFAULT_RTP_PORT_RANGE_START
);
assert_eq!(allocator.config.port_range_end, DEFAULT_RTP_PORT_RANGE_END);
}
#[test]
fn test_port_allocator_with_custom_config() {
let config = PortAllocatorConfig {
port_range_start: 10000,
port_range_end: 20000,
allocation_strategy: AllocationStrategy::Sequential,
pairing_strategy: PairingStrategy::Adjacent,
prefer_port_reuse: false,
default_ip: IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
allocation_retries: 20,
validate_ports: false,
capacity_hint: 0,
};
let allocator = PortAllocator::with_config(config.clone());
assert_eq!(allocator.config.port_range_start, config.port_range_start);
assert_eq!(allocator.config.port_range_end, config.port_range_end);
assert_eq!(
allocator.config.allocation_strategy,
config.allocation_strategy
);
}
#[test]
fn test_release_skips_reuse_list_when_reuse_disabled() {
let rt = Runtime::new().unwrap();
rt.block_on(async {
let config = PortAllocatorConfig {
port_range_start: 10000,
port_range_end: 10100,
allocation_strategy: AllocationStrategy::Incremental,
pairing_strategy: PairingStrategy::Muxed,
prefer_port_reuse: false,
default_ip: IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
allocation_retries: 20,
validate_ports: false,
capacity_hint: 128,
};
let allocator = PortAllocator::with_config(config);
let ip = IpAddr::V4(std::net::Ipv4Addr::LOCALHOST);
for i in 0..32 {
let session_id = format!("session-{i}");
let _ = allocator
.allocate_port_pair(&session_id, Some(ip))
.await
.expect("allocate port pair");
allocator
.release_session(&session_id)
.await
.expect("release session");
}
let released = allocator.released_ports.lock().await;
assert!(
released.is_empty(),
"reuse-disabled allocators should not accumulate released-port scan state"
);
});
}
#[test]
fn test_indexed_unvalidated_allocation_skips_stale_entries() {
let rt = Runtime::new().unwrap();
rt.block_on(async {
let config = PortAllocatorConfig {
port_range_start: 10000,
port_range_end: 10060,
allocation_strategy: AllocationStrategy::Incremental,
pairing_strategy: PairingStrategy::Muxed,
prefer_port_reuse: false,
default_ip: IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
allocation_retries: 61,
validate_ports: false,
capacity_hint: 61,
};
let allocator = PortAllocator::with_config(config);
{
let indexed_state = allocator
.indexed_state
.as_ref()
.expect("indexed state should be enabled");
let mut state = indexed_state.lock().expect("indexed state lock");
for port in 10000..=10054 {
state.available_ports.remove(&port);
state.allocated_ports.insert(port);
}
state.last_port = 10000;
}
let port = allocator
.allocate_port(IpAddr::V4(std::net::Ipv4Addr::LOCALHOST))
.await
.expect("allocator should find the first indexed free port");
assert_eq!(port, 10055);
});
}
#[test]
fn test_indexed_unvalidated_allocation_reuses_released_port() {
let rt = Runtime::new().unwrap();
rt.block_on(async {
let config = PortAllocatorConfig {
port_range_start: 10000,
port_range_end: 10002,
allocation_strategy: AllocationStrategy::Incremental,
pairing_strategy: PairingStrategy::Muxed,
prefer_port_reuse: false,
default_ip: IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
allocation_retries: 3,
validate_ports: false,
capacity_hint: 3,
};
let allocator = PortAllocator::with_config(config);
let ip = IpAddr::V4(std::net::Ipv4Addr::LOCALHOST);
let first = allocator.allocate_port(ip).await.expect("first port");
let second = allocator.allocate_port(ip).await.expect("second port");
let third = allocator.allocate_port(ip).await.expect("third port");
assert_eq!([first, second, third], [10000, 10001, 10002]);
assert!(allocator.allocate_port(ip).await.is_err());
allocator.release_port(ip, second).await;
let reused = allocator
.allocate_port(ip)
.await
.expect("released port should be available through the index");
assert_eq!(reused, second);
});
}
#[test]
fn test_port_allocation() {
let rt = Runtime::new().unwrap();
rt.block_on(async {
let allocator = PortAllocator::new();
let port = allocator
.allocate_port(IpAddr::V4(std::net::Ipv4Addr::LOCALHOST))
.await;
assert!(port.is_ok());
let port = port.unwrap();
assert!(port >= DEFAULT_RTP_PORT_RANGE_START);
assert!(port <= DEFAULT_RTP_PORT_RANGE_END);
assert_eq!(allocator.allocated_count().await, 1);
allocator
.release_port(IpAddr::V4(std::net::Ipv4Addr::LOCALHOST), port)
.await;
assert_eq!(allocator.allocated_count().await, 0);
});
}
#[test]
fn test_port_pair_allocation() {
let rt = Runtime::new().unwrap();
rt.block_on(async {
let config = PortAllocatorConfig {
pairing_strategy: PairingStrategy::Adjacent,
validate_ports: false, default_ip: IpAddr::V4(std::net::Ipv4Addr::LOCALHOST), ..Default::default()
};
let allocator = PortAllocator::with_config(config);
let result = allocator.allocate_port_pair("test-session", None).await;
assert!(result.is_ok());
let (rtp_addr, rtcp_addr) = result.unwrap();
assert!(rtcp_addr.is_some());
let rtcp_addr = rtcp_addr.unwrap();
assert_eq!(rtp_addr.port() % 2, 0);
assert_eq!(rtcp_addr.port(), rtp_addr.port() + 1);
let sessions = allocator.session_ports.lock().await;
if let Some(session_ports) = sessions.get("test-session") {
assert_eq!(
session_ports.len(),
2,
"Expected 2 ports allocated to the session"
);
} else {
panic!("Session test-session not found");
}
drop(sessions);
let result = allocator.release_session("test-session").await;
assert!(result.is_ok());
let sessions = allocator.session_ports.lock().await;
assert!(
!sessions.contains_key("test-session"),
"Session should be removed after release"
);
});
}
#[test]
fn test_muxed_port_allocation() {
let rt = Runtime::new().unwrap();
rt.block_on(async {
let config = PortAllocatorConfig {
pairing_strategy: PairingStrategy::Muxed,
..Default::default()
};
let allocator = PortAllocator::with_config(config);
let result = allocator.allocate_port_pair("test-session", None).await;
assert!(result.is_ok());
let (_rtp_addr, rtcp_addr) = result.unwrap();
assert!(rtcp_addr.is_none());
assert_eq!(allocator.allocated_count().await, 1);
let result = allocator.release_session("test-session").await;
assert!(result.is_ok());
assert_eq!(allocator.allocated_count().await, 0);
});
}
#[test]
fn test_separate_port_allocation() {
let rt = Runtime::new().unwrap();
rt.block_on(async {
let config = PortAllocatorConfig {
pairing_strategy: PairingStrategy::Separate,
..Default::default()
};
let allocator = PortAllocator::with_config(config);
let result = allocator.allocate_port_pair("test-session", None).await;
assert!(result.is_ok());
let (rtp_addr, rtcp_addr) = result.unwrap();
assert!(rtcp_addr.is_some());
let rtcp_addr = rtcp_addr.unwrap();
assert_ne!(rtp_addr.port(), rtcp_addr.port());
assert_eq!(allocator.allocated_count().await, 2);
let result = allocator.release_session("test-session").await;
assert!(result.is_ok());
assert_eq!(allocator.allocated_count().await, 0);
});
}
#[test]
fn test_global_allocator() {
let rt = Runtime::new().unwrap();
rt.block_on(async {
let allocator1 = GlobalPortAllocator::instance().await;
let allocator2 = GlobalPortAllocator::instance().await;
let count1 = Arc::strong_count(&allocator1);
let count2 = Arc::strong_count(&allocator2);
assert_eq!(count1, count2);
let port = allocator1
.allocate_port(IpAddr::V4(std::net::Ipv4Addr::LOCALHOST))
.await;
assert!(port.is_ok());
let count1 = allocator1.allocated_count().await;
let count2 = allocator2.allocated_count().await;
assert_eq!(count1, count2);
assert!(count1 > 0);
});
}
}