use std::sync::{Arc, OnceLock};
use tokio::sync::Semaphore;
pub(crate) const ISLAND_CAPACITY: usize = 2;
static ISLAND: OnceLock<Arc<Semaphore>> = OnceLock::new();
fn island() -> Arc<Semaphore> {
ISLAND
.get_or_init(|| Arc::new(Semaphore::new(ISLAND_CAPACITY)))
.clone()
}
#[derive(Debug)]
pub(crate) struct IslandFailure(String);
impl std::fmt::Display for IslandFailure {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.0)
}
}
impl From<IslandFailure> for std::io::Error {
fn from(failure: IslandFailure) -> Self {
std::io::Error::other(failure.0)
}
}
pub(crate) async fn dispatch<T, F>(operation: F) -> Result<T, IslandFailure>
where
T: Send + 'static,
F: FnOnce() -> T + Send + 'static,
{
let permit = island()
.acquire_owned()
.await
.map_err(|_| IslandFailure("blocking island is closed".into()))?;
tokio::task::spawn_blocking(move || {
let _permit = permit;
operation()
})
.await
.map_err(|error| IslandFailure(format!("blocking operation failed: {error}")))
}
pub async fn dispatch_blocking<T, F>(operation: F) -> std::io::Result<T>
where
T: Send + 'static,
F: FnOnce() -> T + Send + 'static,
{
dispatch(operation).await.map_err(std::io::Error::from)
}
#[cfg(test)]
mod tests {
use super::{dispatch, island, ISLAND_CAPACITY};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn concurrent_dispatch_never_exceeds_the_island_capacity() {
let live = Arc::new(AtomicUsize::new(0));
let peak = Arc::new(AtomicUsize::new(0));
let mut handles = Vec::new();
for _ in 0..(ISLAND_CAPACITY * 8) {
let live = Arc::clone(&live);
let peak = Arc::clone(&peak);
handles.push(tokio::spawn(async move {
dispatch(move || {
let now = live.fetch_add(1, Ordering::SeqCst) + 1;
peak.fetch_max(now, Ordering::SeqCst);
std::thread::sleep(std::time::Duration::from_millis(5));
live.fetch_sub(1, Ordering::SeqCst);
})
.await
.expect("island dispatch")
}));
}
for handle in handles {
handle.await.expect("dispatch task");
}
assert!(
peak.load(Ordering::SeqCst) <= ISLAND_CAPACITY,
"island allowed {} concurrent blocking calls, ceiling is {ISLAND_CAPACITY}",
peak.load(Ordering::SeqCst)
);
}
#[tokio::test]
async fn dispatch_returns_the_operation_result() {
assert_eq!(dispatch(|| 7_u32).await.expect("island dispatch"), 7);
}
#[tokio::test]
async fn every_permit_is_released_after_the_operation_completes() {
for _ in 0..(ISLAND_CAPACITY * 4) {
dispatch(|| ()).await.expect("island dispatch");
}
assert_eq!(island().available_permits(), ISLAND_CAPACITY);
}
}