use std::sync::atomic::{AtomicU64, Ordering};
use super::{CONNECT_COST, PENDING_COST};
pub(super) trait FdAtomic {
fn load(&self, order: Ordering) -> u64;
fn store(&self, value: u64, order: Ordering);
fn fetch_add(&self, value: u64, order: Ordering) -> u64;
fn fetch_sub(&self, value: u64, order: Ordering) -> u64;
fn fetch_update<F>(&self, set_order: Ordering, fetch_order: Ordering, update: F) -> Result<u64, u64>
where
F: FnMut(u64) -> Option<u64>;
}
impl FdAtomic for AtomicU64 {
fn load(&self, order: Ordering) -> u64 {
AtomicU64::load(self, order)
}
fn store(&self, value: u64, order: Ordering) {
AtomicU64::store(self, value, order);
}
fn fetch_add(&self, value: u64, order: Ordering) -> u64 {
AtomicU64::fetch_add(self, value, order)
}
fn fetch_sub(&self, value: u64, order: Ordering) -> u64 {
AtomicU64::fetch_sub(self, value, order)
}
fn fetch_update<F>(&self, set_order: Ordering, fetch_order: Ordering, update: F) -> Result<u64, u64>
where
F: FnMut(u64) -> Option<u64>,
{
AtomicU64::fetch_update(self, set_order, fetch_order, update)
}
}
pub(super) struct Accounting<'counter, A: FdAtomic> {
cached_open: &'counter A,
connected: &'counter A,
pending: &'counter A,
}
impl<'counter, A: FdAtomic> Accounting<'counter, A> {
pub(super) fn new(cached_open: &'counter A, connected: &'counter A, pending: &'counter A) -> Self {
Self {
cached_open,
connected,
pending,
}
}
#[inline]
pub(super) fn try_admit(&self, threshold: u64) -> bool {
let claimed = self.pending.fetch_add(1, Ordering::AcqRel).saturating_add(1);
if self.predicted_with(claimed) > threshold {
self.pending.fetch_sub(1, Ordering::Release);
return false;
}
true
}
#[inline]
pub(super) fn predicted_with(&self, pending: u64) -> u64 {
let (connected, cached_open) = self.usage_components();
pending
.saturating_mul(PENDING_COST)
.saturating_add(connected)
.saturating_add(cached_open)
}
fn usage_components(&self) -> (u64, u64) {
let connected = self.connected.load(Ordering::Acquire);
let cached_open = self.cached_open.load(Ordering::Relaxed);
(connected, cached_open)
}
#[inline]
pub(super) fn settle(&self, new_socket: bool) {
if new_socket {
self.connected.fetch_add(CONNECT_COST, Ordering::Release);
}
self.pending.fetch_sub(1, Ordering::Release);
}
pub(super) fn sample_with<C: FnOnce() -> Option<u64>>(&self, count: C) {
let absorbed = self.connected.load(Ordering::Relaxed);
if let Some(open) = count() {
self.cached_open.store(open, Ordering::Relaxed);
_ = self
.connected
.fetch_update(Ordering::Release, Ordering::Relaxed, |charged| {
Some(charged.saturating_sub(absorbed))
});
}
}
}
#[cfg(test)]
impl FdAtomic for loom::sync::atomic::AtomicU64 {
fn load(&self, order: Ordering) -> u64 {
loom::sync::atomic::AtomicU64::load(self, order)
}
fn store(&self, value: u64, order: Ordering) {
loom::sync::atomic::AtomicU64::store(self, value, order);
}
fn fetch_add(&self, value: u64, order: Ordering) -> u64 {
loom::sync::atomic::AtomicU64::fetch_add(self, value, order)
}
fn fetch_sub(&self, value: u64, order: Ordering) -> u64 {
loom::sync::atomic::AtomicU64::fetch_sub(self, value, order)
}
fn fetch_update<F>(&self, set_order: Ordering, fetch_order: Ordering, update: F) -> Result<u64, u64>
where
F: FnMut(u64) -> Option<u64>,
{
loom::sync::atomic::AtomicU64::fetch_update(self, set_order, fetch_order, update)
}
}
#[cfg(test)]
#[expect(clippy::allow_attributes, reason = "test assertion policy")]
#[allow(clippy::expect_used, reason = "a failed model thread must fail the test")]
mod loom_tests {
use loom::{
sync::{Arc, atomic::AtomicU64},
thread,
};
use super::*;
#[derive(Clone)]
struct Counts {
cached_open: Arc<AtomicU64>,
connected: Arc<AtomicU64>,
pending: Arc<AtomicU64>,
}
impl Counts {
fn new(cached_open: u64, connected: u64, pending: u64) -> Self {
Self {
cached_open: Arc::new(AtomicU64::new(cached_open)),
connected: Arc::new(AtomicU64::new(connected)),
pending: Arc::new(AtomicU64::new(pending)),
}
}
fn accounting(&self) -> Accounting<'_, AtomicU64> {
Accounting::new(&self.cached_open, &self.connected, &self.pending)
}
}
#[test]
fn concurrent_admissions_fit_one_slot() {
loom::model(|| {
let counts = Counts::new(190, 0, 0);
let other = counts.clone();
let second = thread::spawn(move || other.accounting().try_admit(192));
let first = counts.accounting().try_admit(192);
let second = second.join().expect("second admission thread completes");
assert_eq!(u8::from(first) + u8::from(second), 1, "only one claim fits");
});
}
#[test]
fn settling_a_new_socket_never_frees_a_slot() {
loom::model(|| {
let counts = Counts::new(190, 0, 1);
let other = counts.clone();
let settlement = thread::spawn(move || other.accounting().settle(true));
assert!(
!counts.accounting().try_admit(192),
"a pending claim becoming a connection charge does not free a slot"
);
settlement.join().expect("settlement thread completes");
});
}
#[test]
fn sample_publication_never_combines_old_open_with_dropped_charges() {
loom::model(|| {
let counts = Counts::new(100, 2, 0);
let other = counts.clone();
let sampler = thread::spawn(move || other.accounting().sample_with(|| Some(102)));
let predicted = counts.accounting().predicted_with(0);
assert!(predicted >= 102, "sample publication undercounted: {predicted}");
sampler.join().expect("sampler thread completes");
});
}
#[test]
fn connection_update_preserves_sample_release_sequence() {
loom::model(|| {
let counts = Counts::new(100, 1, 1);
let sampler_counts = counts.clone();
let settler_counts = counts.clone();
let sampler = thread::spawn(move || sampler_counts.accounting().sample_with(|| Some(120)));
let settler = thread::spawn(move || settler_counts.accounting().settle(true));
let (connected, cached_open) = counts.accounting().usage_components();
if connected == 2 {
assert_eq!(
cached_open, 120,
"a charge extending the sampler's release sequence publishes the new sample"
);
}
sampler.join().expect("sample thread completes");
settler.join().expect("settlement thread completes");
});
}
}