use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BackpressureStrategy {
#[default]
Pause,
DropVolatile,
CoalesceState,
Downgrade,
Disconnect,
SnapshotLater,
}
pub struct BackpressureController {
max_bytes: usize,
current_bytes: AtomicUsize,
strategy: parking_lot::Mutex<BackpressureStrategy>,
applied: AtomicUsize,
dropped: AtomicUsize,
slow_consumer: AtomicUsize,
flow_pause: AtomicUsize,
flow_resume: AtomicUsize,
volatile_drop: AtomicUsize,
state_coalesce: AtomicUsize,
}
impl BackpressureController {
pub fn new(max_bytes: usize) -> Self {
Self {
max_bytes,
current_bytes: AtomicUsize::new(0),
strategy: parking_lot::Mutex::new(BackpressureStrategy::default()),
applied: AtomicUsize::new(0),
dropped: AtomicUsize::new(0),
slow_consumer: AtomicUsize::new(0),
flow_pause: AtomicUsize::new(0),
flow_resume: AtomicUsize::new(0),
volatile_drop: AtomicUsize::new(0),
state_coalesce: AtomicUsize::new(0),
}
}
pub fn max_bytes(&self) -> usize {
self.max_bytes
}
pub fn current_bytes(&self) -> usize {
self.current_bytes.load(Ordering::Acquire)
}
pub fn strategy(&self) -> BackpressureStrategy {
*self.strategy.lock()
}
pub fn set_strategy(&self, s: BackpressureStrategy) {
*self.strategy.lock() = s;
}
pub fn available(&self) -> usize {
self.max_bytes.saturating_sub(self.current_bytes())
}
pub fn is_overloaded(&self) -> bool {
if self.max_bytes == 0 {
return false;
}
self.current_bytes() >= self.max_bytes - self.max_bytes / 10
}
pub fn try_enqueue(&self, payload_bytes: usize) -> BackpressureAction {
let mut prev = self.current_bytes.load(Ordering::Acquire);
loop {
if prev + payload_bytes <= self.max_bytes {
match self.current_bytes.compare_exchange_weak(
prev,
prev + payload_bytes,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => return BackpressureAction::Accept,
Err(current) => prev = current,
}
} else {
break;
}
}
self.record_applied();
match self.strategy() {
BackpressureStrategy::Pause => {
self.flow_pause.fetch_add(1, Ordering::Relaxed);
BackpressureAction::Pause
}
BackpressureStrategy::DropVolatile => {
self.record_dropped();
self.volatile_drop.fetch_add(1, Ordering::Relaxed);
BackpressureAction::DropVolatile
}
BackpressureStrategy::CoalesceState => {
self.record_dropped();
self.state_coalesce.fetch_add(1, Ordering::Relaxed);
BackpressureAction::CoalesceState
}
BackpressureStrategy::Downgrade => BackpressureAction::Downgrade,
BackpressureStrategy::Disconnect => {
self.record_dropped();
self.slow_consumer.fetch_add(1, Ordering::Relaxed);
BackpressureAction::Disconnect
}
BackpressureStrategy::SnapshotLater => BackpressureAction::SnapshotLater,
}
}
pub fn release(&self, bytes: usize) {
let hwm = self.high_water();
let mut prev = self.current_bytes.load(Ordering::Acquire);
loop {
let next = prev.saturating_sub(bytes);
match self.current_bytes.compare_exchange_weak(
prev,
next,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => {
if prev >= hwm && next < hwm {
self.flow_resume.fetch_add(1, Ordering::Relaxed);
}
return;
}
Err(current) => prev = current,
}
}
}
fn high_water(&self) -> usize {
self.max_bytes - self.max_bytes / 10
}
pub fn record_applied(&self) {
self.applied.fetch_add(1, Ordering::Relaxed);
}
pub fn record_dropped(&self) {
self.dropped.fetch_add(1, Ordering::Relaxed);
}
pub fn applied(&self) -> usize {
self.applied.load(Ordering::Relaxed)
}
pub fn dropped(&self) -> usize {
self.dropped.load(Ordering::Relaxed)
}
pub fn slow_consumer_count(&self) -> usize {
self.slow_consumer.load(Ordering::Relaxed)
}
pub fn flow_pause_count(&self) -> usize {
self.flow_pause.load(Ordering::Relaxed)
}
pub fn flow_resume_count(&self) -> usize {
self.flow_resume.load(Ordering::Relaxed)
}
pub fn volatile_drop_count(&self) -> usize {
self.volatile_drop.load(Ordering::Relaxed)
}
pub fn state_coalesce_count(&self) -> usize {
self.state_coalesce.load(Ordering::Relaxed)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackpressureAction {
Accept,
Pause,
DropVolatile,
CoalesceState,
Downgrade,
Disconnect,
SnapshotLater,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accept_under_limit() {
let bp = BackpressureController::new(100);
assert_eq!(bp.try_enqueue(50), BackpressureAction::Accept);
assert_eq!(bp.try_enqueue(40), BackpressureAction::Accept);
assert_eq!(bp.current_bytes(), 90);
}
#[test]
fn pause_when_over_limit() {
let bp = BackpressureController::new(100);
bp.set_strategy(BackpressureStrategy::Pause);
bp.try_enqueue(80);
assert_eq!(bp.try_enqueue(50), BackpressureAction::Pause);
}
#[test]
fn disconnect_strategy() {
let bp = BackpressureController::new(100);
bp.set_strategy(BackpressureStrategy::Disconnect);
bp.try_enqueue(80);
assert_eq!(bp.try_enqueue(50), BackpressureAction::Disconnect);
}
#[test]
fn release_decrements() {
let bp = BackpressureController::new(100);
bp.try_enqueue(50);
bp.release(30);
assert_eq!(bp.current_bytes(), 20);
}
#[test]
fn overloaded_detection() {
let bp = BackpressureController::new(100);
bp.try_enqueue(95);
assert!(bp.is_overloaded());
bp.release(10);
assert!(!bp.is_overloaded());
}
}