use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::{OwnedSemaphorePermit, Semaphore, SemaphorePermit};
use crate::abi::AbiError;
pub const DEFAULT_MAX_CONCURRENT_GPU_OPS: usize = 256;
pub const DEADLINE_NEAR_WINDOW: Duration = Duration::from_millis(50);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackPressureError {
Saturated,
DeadlineNear,
DeadlineElapsed,
}
impl BackPressureError {
pub fn name(self) -> &'static str {
match self {
BackPressureError::Saturated => "saturated",
BackPressureError::DeadlineNear => "deadline_near",
BackPressureError::DeadlineElapsed => "deadline_elapsed",
}
}
}
impl From<BackPressureError> for AbiError {
fn from(e: BackPressureError) -> Self {
match e {
BackPressureError::Saturated
| BackPressureError::DeadlineNear
| BackPressureError::DeadlineElapsed => AbiError::QuotaExceeded,
}
}
}
#[derive(Clone)]
pub struct BackPressure {
inner: Arc<BackPressureInner>,
deadline: Option<Instant>,
}
struct BackPressureInner {
semaphore: Arc<Semaphore>,
active: AtomicUsize,
max_concurrent: usize,
}
impl BackPressure {
pub fn new() -> Self {
Self::with_cap(DEFAULT_MAX_CONCURRENT_GPU_OPS)
}
pub fn with_cap(max_concurrent: usize) -> Self {
Self {
inner: Arc::new(BackPressureInner {
semaphore: Arc::new(Semaphore::new(max_concurrent)),
active: AtomicUsize::new(0),
max_concurrent,
}),
deadline: None,
}
}
pub fn with_deadline_hint(mut self, deadline: Option<Instant>) -> Self {
self.deadline = deadline;
self
}
pub fn deadline_hint(&self) -> Option<Instant> {
self.deadline
}
fn check_deadline(&self) -> Result<(), BackPressureError> {
let Some(d) = self.deadline else {
return Ok(());
};
let now = Instant::now();
if now >= d {
return Err(BackPressureError::DeadlineElapsed);
}
let remaining = d.saturating_duration_since(now);
if remaining <= DEADLINE_NEAR_WINDOW {
return Err(BackPressureError::DeadlineNear);
}
Ok(())
}
pub async fn acquire(&self) -> DispatchPermit {
let permit = self
.inner
.semaphore
.clone()
.acquire_owned()
.await
.expect("semaphore closed unexpectedly");
self.inner.active.fetch_add(1, Ordering::Relaxed);
DispatchPermit {
permit: Some(permit),
counter: self.inner.clone(),
}
}
pub async fn acquire_with_deadline(&self) -> Result<DispatchPermit, BackPressureError> {
self.check_deadline()?;
let permit_fut = self.inner.semaphore.clone().acquire_owned();
let permit = if let Some(d) = self.deadline {
tokio::select! {
p = permit_fut => p.expect("semaphore closed unexpectedly"),
_ = tokio::time::sleep_until(d.into()) => {
return Err(BackPressureError::DeadlineElapsed);
}
}
} else {
permit_fut.await.expect("semaphore closed unexpectedly")
};
if let Err(e) = self.check_deadline() {
drop(permit);
return Err(e);
}
self.inner.active.fetch_add(1, Ordering::Relaxed);
Ok(DispatchPermit {
permit: Some(permit),
counter: self.inner.clone(),
})
}
pub fn try_acquire_with_deadline(&self) -> Result<DispatchPermit, BackPressureError> {
self.check_deadline()?;
let permit = self
.inner
.semaphore
.clone()
.try_acquire_owned()
.map_err(|_| BackPressureError::Saturated)?;
self.inner.active.fetch_add(1, Ordering::Relaxed);
Ok(DispatchPermit {
permit: Some(permit),
counter: self.inner.clone(),
})
}
pub async fn acquire_borrowed(&self) -> Result<BorrowedDispatchPermit<'_>, AbiError> {
if let Err(e) = self.check_deadline() {
return Err(e.into());
}
if let Ok(permit) = self.inner.semaphore.try_acquire() {
self.inner.active.fetch_add(1, Ordering::Relaxed);
return Ok(BorrowedDispatchPermit {
permit: Some(permit),
counter: &self.inner,
});
}
if self.inner.max_concurrent == 0 {
return Err(AbiError::QuotaExceeded);
}
let permit = if let Some(d) = self.deadline {
tokio::select! {
p = self.inner.semaphore.acquire() => p.expect("semaphore closed unexpectedly"),
_ = tokio::time::sleep_until(d.into()) => {
return Err(BackPressureError::DeadlineElapsed.into());
}
}
} else {
self.inner
.semaphore
.acquire()
.await
.expect("semaphore closed unexpectedly")
};
if let Err(e) = self.check_deadline() {
drop(permit);
return Err(e.into());
}
self.inner.active.fetch_add(1, Ordering::Relaxed);
Ok(BorrowedDispatchPermit {
permit: Some(permit),
counter: &self.inner,
})
}
pub fn try_acquire(&self) -> Option<DispatchPermit> {
let permit = self.inner.semaphore.clone().try_acquire_owned().ok()?;
self.inner.active.fetch_add(1, Ordering::Relaxed);
Some(DispatchPermit {
permit: Some(permit),
counter: self.inner.clone(),
})
}
pub fn active(&self) -> usize {
self.inner.active.load(Ordering::Relaxed)
}
pub fn max_concurrent(&self) -> usize {
self.inner.max_concurrent
}
}
impl Default for BackPressure {
fn default() -> Self {
Self::new()
}
}
pub struct DispatchPermit {
permit: Option<OwnedSemaphorePermit>,
counter: Arc<BackPressureInner>,
}
impl std::fmt::Debug for DispatchPermit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DispatchPermit")
.field("permit_held", &self.permit.is_some())
.finish()
}
}
impl Drop for DispatchPermit {
fn drop(&mut self) {
self.permit = None;
self.counter.active.fetch_sub(1, Ordering::Relaxed);
}
}
pub struct BorrowedDispatchPermit<'a> {
permit: Option<SemaphorePermit<'a>>,
counter: &'a BackPressureInner,
}
impl std::fmt::Debug for BorrowedDispatchPermit<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BorrowedDispatchPermit")
.field("issued", &self.permit.is_some())
.finish()
}
}
impl Drop for BorrowedDispatchPermit<'_> {
fn drop(&mut self) {
self.permit = None;
self.counter.active.fetch_sub(1, Ordering::Relaxed);
}
}
pub struct DispatchFuture {
_permit: DispatchPermit,
dispatch_span: tracing::Span,
#[cfg(feature = "cuda")]
event: Option<cust::event::Event>,
#[cfg(feature = "cuda")]
sleep: Option<std::pin::Pin<Box<tokio::time::Sleep>>>,
}
impl DispatchFuture {
pub fn ready(permit: DispatchPermit) -> Self {
Self {
_permit: permit,
dispatch_span: tracing::info_span!("wasi_cuda.dispatch"),
#[cfg(feature = "cuda")]
event: None,
#[cfg(feature = "cuda")]
sleep: None,
}
}
#[cfg(feature = "cuda")]
pub fn with_event(permit: DispatchPermit, event: cust::event::Event) -> Self {
Self {
_permit: permit,
dispatch_span: tracing::info_span!("wasi_cuda.dispatch"),
event: Some(event),
sleep: None,
}
}
}
impl std::future::Future for DispatchFuture {
type Output = ();
fn poll(
self: std::pin::Pin<&mut Self>,
#[cfg_attr(not(feature = "cuda"), allow(unused_variables))] cx: &mut std::task::Context<'_>,
) -> std::task::Poll<()> {
let _entered = self.dispatch_span.enter();
#[cfg(feature = "cuda")]
{
if let Some(ev) = self.event.as_ref() {
return match ev.query() {
Ok(cust::event::EventStatus::Ready) => std::task::Poll::Ready(()),
Ok(cust::event::EventStatus::NotReady) | Err(_) => {
let waker = cx.waker().clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_micros(50)).await;
waker.wake();
});
std::task::Poll::Pending
}
};
}
}
std::task::Poll::Ready(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn acquire_and_release() {
let bp = BackPressure::with_cap(2);
assert_eq!(bp.active(), 0);
let a = bp.acquire().await;
assert_eq!(bp.active(), 1);
let b = bp.acquire().await;
assert_eq!(bp.active(), 2);
drop(a);
drop(b);
assert_eq!(bp.active(), 0);
}
#[tokio::test]
async fn try_acquire_under_pressure() {
let bp = BackPressure::with_cap(1);
let a = bp.acquire().await;
assert!(
bp.try_acquire().is_none(),
"second permit should be unavailable"
);
drop(a);
assert!(
bp.try_acquire().is_some(),
"permit should be available again"
);
}
#[tokio::test]
async fn dispatch_future_resolves_immediately() {
let bp = BackPressure::with_cap(4);
let permit = bp.acquire().await;
let fut = DispatchFuture::ready(permit);
fut.await;
assert_eq!(bp.active(), 0);
}
#[tokio::test]
async fn concurrent_acquire_progresses() {
let bp = BackPressure::with_cap(64);
let mut handles = Vec::new();
for _ in 0..1000 {
let bp = bp.clone();
handles.push(tokio::spawn(async move {
let permit = bp.acquire().await;
DispatchFuture::ready(permit).await;
}));
}
for h in handles {
h.await.unwrap();
}
assert_eq!(bp.active(), 0);
}
#[tokio::test]
async fn acquire_borrowed_and_release() {
let bp = BackPressure::with_cap(2);
assert_eq!(bp.active(), 0);
let a = bp.acquire_borrowed().await.expect("permit");
assert_eq!(bp.active(), 1);
let b = bp.acquire_borrowed().await.expect("permit");
assert_eq!(bp.active(), 2);
drop(a);
drop(b);
assert_eq!(bp.active(), 0);
}
#[tokio::test]
async fn acquire_borrowed_cap_zero_returns_quota_exceeded() {
let bp = BackPressure::with_cap(0);
let err = bp.acquire_borrowed().await.expect_err("cap=0 must error");
assert_eq!(err, AbiError::QuotaExceeded);
let err2 = bp.acquire_borrowed().await.expect_err("cap=0 must error");
assert_eq!(err2, AbiError::QuotaExceeded);
assert_eq!(bp.active(), 0);
}
#[test]
fn defaults_are_consistent() {
let bp = BackPressure::new();
assert_eq!(bp.max_concurrent(), DEFAULT_MAX_CONCURRENT_GPU_OPS);
assert_eq!(bp.active(), 0);
}
#[test]
fn deadline_hint_round_trips() {
let bp = BackPressure::with_cap(4);
assert!(bp.deadline_hint().is_none());
let d = Instant::now() + Duration::from_secs(1);
let bp = bp.with_deadline_hint(Some(d));
assert_eq!(bp.deadline_hint(), Some(d));
}
#[tokio::test]
async fn acquire_borrowed_rejects_new_under_deadline_near() {
let bp = BackPressure::with_cap(4)
.with_deadline_hint(Some(Instant::now() + Duration::from_millis(30)));
let err = bp
.acquire_borrowed()
.await
.expect_err("near-deadline acquire must be refused");
assert_eq!(err, AbiError::QuotaExceeded);
assert_eq!(bp.active(), 0);
}
#[tokio::test]
async fn acquire_borrowed_rejects_past_deadline() {
let bp = BackPressure::with_cap(4)
.with_deadline_hint(Some(Instant::now() - Duration::from_millis(5)));
let err = bp
.acquire_borrowed()
.await
.expect_err("elapsed deadline must refuse");
assert_eq!(err, AbiError::QuotaExceeded);
}
#[tokio::test]
async fn acquire_borrowed_passes_outside_near_window() {
let bp = BackPressure::with_cap(4)
.with_deadline_hint(Some(Instant::now() + Duration::from_secs(10)));
let permit = bp.acquire_borrowed().await.expect("permit");
assert_eq!(bp.active(), 1);
drop(permit);
assert_eq!(bp.active(), 0);
}
#[tokio::test]
async fn in_flight_completes_under_near_deadline() {
let bp = BackPressure::with_cap(4)
.with_deadline_hint(Some(Instant::now() + Duration::from_millis(80)));
let in_flight = bp.acquire_borrowed().await.expect("first permit");
assert_eq!(bp.active(), 1);
tokio::time::sleep(Duration::from_millis(40)).await;
let err = bp
.acquire_borrowed()
.await
.expect_err("second acquire must be refused");
assert_eq!(err, AbiError::QuotaExceeded);
assert_eq!(bp.active(), 1);
drop(in_flight);
assert_eq!(bp.active(), 0);
}
#[tokio::test]
async fn typed_acquire_with_deadline_surfaces_variants() {
let bp = BackPressure::with_cap(2)
.with_deadline_hint(Some(Instant::now() + Duration::from_millis(20)));
let err = bp
.acquire_with_deadline()
.await
.expect_err("near-deadline must refuse");
assert_eq!(err, BackPressureError::DeadlineNear);
let bp = BackPressure::with_cap(2)
.with_deadline_hint(Some(Instant::now() - Duration::from_millis(5)));
let err = bp
.acquire_with_deadline()
.await
.expect_err("elapsed-deadline must refuse");
assert_eq!(err, BackPressureError::DeadlineElapsed);
}
#[test]
fn backpressure_error_converts_to_abi_error() {
assert_eq!(
AbiError::from(BackPressureError::Saturated),
AbiError::QuotaExceeded
);
assert_eq!(
AbiError::from(BackPressureError::DeadlineNear),
AbiError::QuotaExceeded
);
assert_eq!(
AbiError::from(BackPressureError::DeadlineElapsed),
AbiError::QuotaExceeded
);
}
}