use bytes::Bytes;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tokio::sync::Notify;
use crate::error::{Error, Result};
const SHARDS: usize = 64;
pub struct UdpCore {
shards: Box<[Shard; SHARDS]>,
stats: CoreStats,
closed: AtomicBool,
}
struct CoreStats {
delivered: AtomicU64,
expired: AtomicU64,
unmatched: AtomicU64,
malformed: AtomicU64,
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct TransportStats {
pub delivered: u64,
pub expired: u64,
pub unmatched: u64,
pub malformed: u64,
}
struct Shard {
pending: Mutex<HashMap<i32, ResponseSlot>>,
}
struct ResponseSlot {
response: Option<(Bytes, SocketAddr)>,
registered_at: Instant,
deadline: Instant,
notify: Arc<Notify>,
expected_source: Option<SocketAddr>,
}
impl UdpCore {
pub fn new() -> Self {
let shards: Vec<Shard> = (0..SHARDS)
.map(|_| Shard {
pending: Mutex::new(HashMap::new()),
})
.collect();
Self {
shards: shards
.try_into()
.unwrap_or_else(|_| unreachable!("Vec has exactly SHARDS elements")),
stats: CoreStats {
delivered: AtomicU64::new(0),
expired: AtomicU64::new(0),
unmatched: AtomicU64::new(0),
malformed: AtomicU64::new(0),
},
closed: AtomicBool::new(false),
}
}
pub fn close(&self) {
self.closed.store(true, Ordering::Release);
for shard in self.shards.iter() {
let notifies: Vec<_> = shard
.pending
.lock()
.unwrap()
.values()
.map(|slot| slot.notify.clone())
.collect();
for notify in notifies {
notify.notify_one();
}
}
}
fn shard(&self, request_id: i32) -> &Shard {
&self.shards[request_id as usize % SHARDS]
}
pub fn register(
&self,
request_id: i32,
timeout: Duration,
expected_source: Option<SocketAddr>,
) {
let shard = self.shard(request_id);
let now = Instant::now();
let slot = ResponseSlot {
response: None,
registered_at: now,
deadline: now + timeout,
notify: Arc::new(Notify::new()),
expected_source,
};
shard.pending.lock().unwrap().insert(request_id, slot);
}
pub fn deliver(&self, request_id: i32, data: Bytes, source: SocketAddr) -> bool {
let shard = self.shard(request_id);
let mut pending = shard.pending.lock().unwrap();
if let Some(slot) = pending.get_mut(&request_id) {
if let Some(expected) = slot.expected_source
&& expected != source
{
drop(pending);
self.stats.unmatched.fetch_add(1, Ordering::Relaxed);
return false;
}
if slot.response.is_some() {
drop(pending);
self.stats.unmatched.fetch_add(1, Ordering::Relaxed);
return false;
}
slot.response = Some((data, source));
let notify = slot.notify.clone();
drop(pending);
notify.notify_one();
self.stats.delivered.fetch_add(1, Ordering::Relaxed);
return true;
}
self.stats.unmatched.fetch_add(1, Ordering::Relaxed);
false
}
pub(crate) fn note_malformed(&self) {
self.stats.malformed.fetch_add(1, Ordering::Relaxed);
}
pub async fn wait_for_response(
&self,
request_id: i32,
target: SocketAddr,
) -> Result<(Bytes, SocketAddr)> {
let shard = self.shard(request_id);
loop {
let (notify, deadline, registered_at) = {
let mut pending = shard.pending.lock().unwrap();
if let Some(slot) = pending.get_mut(&request_id) {
if let Some(response) = slot.response.take() {
pending.remove(&request_id);
return Ok(response);
}
(slot.notify.clone(), slot.deadline, slot.registered_at)
} else if self.closed.load(Ordering::Acquire) {
tracing::debug!(target: "async_snmp::transport::udp", { request_id, %target }, "transport shut down (slot missing)");
return Err(Error::Closed { target }.boxed());
} else {
tracing::debug!(target: "async_snmp::transport::udp", { request_id, %target, elapsed = ?Duration::ZERO }, "transport timeout (slot missing)");
return Err(Error::Timeout {
target,
elapsed: Duration::ZERO,
retries: 0,
}
.boxed());
}
};
if self.closed.load(Ordering::Acquire) {
self.unregister(request_id);
tracing::debug!(target: "async_snmp::transport::udp", { request_id, %target }, "transport shut down");
return Err(Error::Closed { target }.boxed());
}
let now = Instant::now();
if now >= deadline {
self.unregister(request_id);
self.stats.expired.fetch_add(1, Ordering::Relaxed);
let elapsed = now.saturating_duration_since(registered_at);
tracing::debug!(target: "async_snmp::transport::udp", { request_id, %target, ?elapsed }, "transport timeout");
return Err(Error::Timeout {
target,
elapsed,
retries: 0,
}
.boxed());
}
tokio::select! {
() = notify.notified() => {
}
() = tokio::time::sleep_until(tokio::time::Instant::from_std(deadline)) => {
}
}
}
}
pub fn stats(&self) -> TransportStats {
TransportStats {
delivered: self.stats.delivered.load(Ordering::Relaxed),
expired: self.stats.expired.load(Ordering::Relaxed),
unmatched: self.stats.unmatched.load(Ordering::Relaxed),
malformed: self.stats.malformed.load(Ordering::Relaxed),
}
}
pub fn unregister(&self, request_id: i32) {
let shard = self.shard(request_id);
shard.pending.lock().unwrap().remove(&request_id);
}
pub fn cleanup_expired(&self) {
let now = Instant::now();
let mut removed = 0u64;
for shard in self.shards.iter() {
let mut pending = shard.pending.lock().unwrap();
pending.retain(|_, slot| {
let keep = slot.deadline > now;
if !keep {
removed += 1;
}
keep
});
}
if removed > 0 {
self.stats.expired.fetch_add(removed, Ordering::Relaxed);
}
}
}
impl Default for UdpCore {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_addr() -> SocketAddr {
"127.0.0.1:161".parse().unwrap()
}
#[tokio::test]
async fn timeout_reports_elapsed_from_registration() {
let core = UdpCore::new();
let addr = test_addr();
let timeout = Duration::from_millis(50);
core.register(1, timeout, None);
let err = core.wait_for_response(1, addr).await.unwrap_err();
match *err {
Error::Timeout {
elapsed, retries, ..
} => {
assert!(
elapsed >= timeout,
"elapsed {elapsed:?} should be at least the timeout {timeout:?}"
);
assert!(
elapsed < Duration::from_secs(1),
"elapsed {elapsed:?} should not be derived from a 1s offset"
);
assert_eq!(retries, 0);
}
other => panic!("expected Timeout, got {other:?}"),
}
}
#[tokio::test]
async fn duplicate_delivery_is_ignored() {
let core = UdpCore::new();
let addr = test_addr();
core.register(7, Duration::from_secs(30), None);
let first = Bytes::from_static(b"first");
let dup = Bytes::from_static(b"second");
assert!(core.deliver(7, first.clone(), addr));
assert!(!core.deliver(7, dup, addr));
let stats = core.stats();
assert_eq!(stats.delivered, 1, "duplicate must not be counted");
assert_eq!(stats.unmatched, 1, "duplicate counts as unmatched");
let (data, _) = core.wait_for_response(7, addr).await.unwrap();
assert_eq!(data, first, "original response must not be overwritten");
}
}