use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(not(target_family = "wasm"))]
use tokio::sync::Semaphore;
#[cfg(all(test, not(target_family = "wasm")))]
use tokio::sync::{SemaphorePermit, TryAcquireError};
pub(super) struct InlineGuard {
#[cfg(not(target_family = "wasm"))]
permits: Semaphore,
#[cfg(not(target_family = "wasm"))]
max_permits: usize,
granted: AtomicU64,
diverted: AtomicU64,
}
impl InlineGuard {
pub(super) fn new(worker_threads: usize, reserve: usize) -> Self {
#[cfg(not(target_family = "wasm"))]
let cap = worker_threads.saturating_sub(reserve);
#[cfg(target_family = "wasm")]
let _ = (worker_threads, reserve);
Self {
#[cfg(not(target_family = "wasm"))]
permits: Semaphore::new(cap),
#[cfg(not(target_family = "wasm"))]
max_permits: cap,
granted: AtomicU64::new(0),
diverted: AtomicU64::new(0),
}
}
#[cfg(all(test, not(target_family = "wasm")))]
fn try_inline_permit(&self) -> Result<SemaphorePermit<'_>, TryAcquireError> {
self.permits.try_acquire()
}
#[cfg(not(target_family = "wasm"))]
pub(super) async fn try_inline_or_offload<'a, F, R>(&self, op: F) -> R
where
F: FnOnce() -> R + Send + 'a,
R: Send + 'a,
{
match self.permits.try_acquire() {
Ok(_permit) => {
self.granted.fetch_add(1, Ordering::Relaxed);
op()
}
Err(_) => {
self.diverted.fetch_add(1, Ordering::Relaxed);
affinitypool::spawn_local(op).await
}
}
}
pub(super) fn granted(&self) -> u64 {
self.granted.load(Ordering::Relaxed)
}
pub(super) fn diverted(&self) -> u64 {
self.diverted.load(Ordering::Relaxed)
}
}
#[cfg(not(target_family = "wasm"))]
impl Drop for InlineGuard {
fn drop(&mut self) {
let granted = self.granted.load(Ordering::Relaxed);
let diverted = self.diverted.load(Ordering::Relaxed);
let total = granted.saturating_add(diverted);
if total == 0 {
return;
}
let divert_pct = (diverted as f64 * 100.0) / (total as f64);
info!(
target: "surrealdb::core::kvs::rocksdb::inline_guard",
granted,
diverted,
divert_pct = format!("{divert_pct:.1}"),
cap = self.max_permits,
"inline-blocking summary at datastore shutdown"
);
}
}
#[cfg(all(test, not(target_family = "wasm")))]
mod tests {
use super::*;
#[test]
fn new_saturates_at_zero() {
assert_eq!(InlineGuard::new(0, 2).permits.available_permits(), 0);
assert_eq!(InlineGuard::new(1, 2).permits.available_permits(), 0);
assert_eq!(InlineGuard::new(2, 2).permits.available_permits(), 0);
assert_eq!(InlineGuard::new(3, 2).permits.available_permits(), 1);
assert_eq!(InlineGuard::new(16, 2).permits.available_permits(), 14);
assert_eq!(InlineGuard::new(12, 0).permits.available_permits(), 12);
assert_eq!(InlineGuard::new(4, 8).permits.available_permits(), 0);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn grants_when_permits_available() {
let g = InlineGuard::new(4, 2); let out = g.try_inline_or_offload(|| 7_u32).await;
assert_eq!(out, 7);
assert_eq!(g.granted(), 1);
assert_eq!(g.diverted(), 0);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn diverts_when_permits_exhausted() {
let g = InlineGuard::new(4, 2); let _h1 = g.try_inline_permit().expect("permit 1");
let _h2 = g.try_inline_permit().expect("permit 2");
let out = g.try_inline_or_offload(|| 42_u32).await;
assert_eq!(out, 42);
assert_eq!(g.granted(), 0);
assert_eq!(g.diverted(), 1);
}
#[tokio::test(flavor = "multi_thread")]
async fn always_diverts_when_runtime_below_reserve() {
let g = InlineGuard::new(1, 2); let out = g.try_inline_or_offload(|| "ok").await;
assert_eq!(out, "ok");
assert_eq!(g.granted(), 0);
assert_eq!(g.diverted(), 1);
}
}