use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use bevy_ecs::entity::Entity;
use tokio::runtime::Handle;
use tokio::sync::Notify;
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
use tokio::task::{JoinHandle, JoinSet};
use crate::inference_pool::expect_permit;
pub type ToolExecFuture = Pin<Box<dyn Future<Output = Vec<(String, String)>> + Send>>;
pub type BoxedToolExec = Box<dyn FnOnce() -> ToolExecFuture + Send>;
pub struct ToolJob {
pub entity: Entity,
pub exec: BoxedToolExec,
pub cancel: crate::cancel::CancelToken,
}
pub struct ToolOutcome {
pub entity: Entity,
pub results: Vec<(String, String)>,
pub elapsed: std::time::Duration,
}
#[derive(Debug)]
pub struct ToolLaneStats {
queued: AtomicUsize,
busy: AtomicUsize,
parked: AtomicUsize,
workers: AtomicUsize,
}
impl ToolLaneStats {
pub fn new(workers: usize) -> Self {
Self {
queued: AtomicUsize::new(0),
busy: AtomicUsize::new(0),
parked: AtomicUsize::new(0),
workers: AtomicUsize::new(workers.max(1)),
}
}
pub fn enqueued(&self) {
self.queued.fetch_add(1, Ordering::Relaxed);
}
fn abandoned(&self) {
self.queued.fetch_sub(1, Ordering::Relaxed);
}
fn started(&self) {
self.queued.fetch_sub(1, Ordering::Relaxed);
self.busy.fetch_add(1, Ordering::Relaxed);
}
fn finished(&self) {
self.busy.fetch_sub(1, Ordering::Relaxed);
}
fn began_park(&self) {
self.busy.fetch_sub(1, Ordering::Relaxed);
self.parked.fetch_add(1, Ordering::Relaxed);
}
fn resumed(&self) {
self.parked.fetch_sub(1, Ordering::Relaxed);
self.busy.fetch_add(1, Ordering::Relaxed);
}
fn ended_park(&self) {
self.parked.fetch_sub(1, Ordering::Relaxed);
}
pub fn queued(&self) -> usize {
self.queued.load(Ordering::Relaxed)
}
pub fn busy(&self) -> usize {
self.busy.load(Ordering::Relaxed)
}
pub fn parked(&self) -> usize {
self.parked.load(Ordering::Relaxed)
}
pub fn workers(&self) -> usize {
self.workers.load(Ordering::Relaxed)
}
fn widen(&self, extra: usize) {
self.workers.fetch_add(extra, Ordering::Relaxed);
}
#[must_use]
pub fn is_saturated(&self) -> bool {
self.busy() >= self.workers() && self.queued() > 0
}
}
pub struct ToolLane {
permits: Arc<Semaphore>,
stats: Arc<ToolLaneStats>,
results: UnboundedSender<ToolOutcome>,
wake: Arc<Notify>,
runtime: Handle,
}
impl ToolLane {
pub fn new(
runtime: Handle,
results: UnboundedSender<ToolOutcome>,
wake: Arc<Notify>,
concurrency: usize,
stats: Arc<ToolLaneStats>,
) -> Arc<Self> {
Arc::new(Self {
permits: Arc::new(Semaphore::new(concurrency.max(1))),
stats,
results,
wake,
runtime,
})
}
pub fn serve(self: &Arc<Self>, jobs: UnboundedReceiver<ToolJob>) -> JoinHandle<()> {
let lane = self.clone();
self.runtime.clone().spawn(serve_lane(lane, jobs))
}
pub fn relieve(&self, extra: usize) -> usize {
if extra == 0 {
return 0;
}
self.permits.add_permits(extra);
self.stats.widen(extra);
extra
}
}
async fn serve_lane(lane: Arc<ToolLane>, mut jobs: UnboundedReceiver<ToolJob>) {
let mut batches = JoinSet::new();
loop {
tokio::select! {
job = jobs.recv() => match job {
Some(job) => {
batches.spawn_on(run_batch(lane.clone(), job), &lane.runtime);
}
None => break, },
Some(_) = batches.join_next(), if !batches.is_empty() => {}
}
}
while batches.join_next().await.is_some() {}
}
async fn run_batch(lane: Arc<ToolLane>, job: ToolJob) {
let ToolJob {
entity,
exec,
cancel,
} = job;
let permit = tokio::select! {
biased;
_ = cancel.cancelled() => {
lane.stats.abandoned();
return;
}
permit = lane.permits.clone().acquire_owned() => expect_permit(permit),
};
lane.stats.started();
let ticket = Arc::new(LaneTicket::new(lane.clone(), permit));
let started = std::time::Instant::now();
let out = LANE_TICKET
.scope(ticket, async move {
tokio::select! {
biased;
_ = cancel.cancelled() => None,
out = exec() => Some(out),
}
})
.await;
let Some(out) = out else { return };
let _ = lane.results.send(ToolOutcome {
entity,
results: out,
elapsed: started.elapsed(),
});
lane.wake.notify_one();
}
tokio::task_local! {
static LANE_TICKET: Arc<LaneTicket>;
}
struct LaneTicket {
lane: Arc<ToolLane>,
permit: std::sync::Mutex<Option<OwnedSemaphorePermit>>,
parked: AtomicBool,
}
impl LaneTicket {
fn new(lane: Arc<ToolLane>, permit: OwnedSemaphorePermit) -> Self {
Self {
lane,
permit: std::sync::Mutex::new(Some(permit)),
parked: AtomicBool::new(false),
}
}
fn release(&self) {
let held = self.take_permit();
drop(held);
self.parked.store(true, Ordering::Relaxed);
self.lane.stats.began_park();
self.lane.wake.notify_one();
}
async fn reacquire(&self) {
let permit = expect_permit(self.lane.permits.clone().acquire_owned().await);
self.lane.stats.resumed();
self.parked.store(false, Ordering::Relaxed);
*self
.permit
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(permit);
}
fn take_permit(&self) -> Option<OwnedSemaphorePermit> {
self.permit
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take()
}
}
impl Drop for LaneTicket {
fn drop(&mut self) {
drop(self.take_permit());
match self.parked.load(Ordering::Relaxed) {
true => self.lane.stats.ended_park(),
false => self.lane.stats.finished(),
}
self.lane.wake.notify_one();
}
}
pub async fn off_lane<T>(fut: impl Future<Output = T>) -> T {
let Ok(ticket) = LANE_TICKET.try_with(Arc::clone) else {
return fut.await;
};
ticket.release();
let out = fut.await;
ticket.reacquire().await;
out
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
use tokio::sync::mpsc;
struct Harness {
lane: Arc<ToolLane>,
jobs: Option<UnboundedSender<ToolJob>>,
outcomes: mpsc::UnboundedReceiver<ToolOutcome>,
serving: Option<JoinHandle<()>>,
stats: Arc<ToolLaneStats>,
}
impl Harness {
fn new(concurrency: usize) -> Self {
let (jobs, job_rx) = mpsc::unbounded_channel();
let (result_tx, outcomes) = mpsc::unbounded_channel();
let stats = Arc::new(ToolLaneStats::new(concurrency));
let lane = ToolLane::new(
Handle::current(),
result_tx,
Arc::new(Notify::new()),
concurrency,
stats.clone(),
);
let serving = lane.serve(job_rx);
Self {
lane,
jobs: Some(jobs),
outcomes,
serving: Some(serving),
stats,
}
}
fn submit(&self, job: ToolJob) {
self.stats.enqueued();
self.sender().send(job).expect("the lane is serving");
}
fn sender(&self) -> &UnboundedSender<ToolJob> {
self.jobs.as_ref().expect("the lane is still open")
}
async fn drain(&mut self) {
drop(self.jobs.take());
let serving = self.serving.take().expect("the lane was serving");
timeout(serving).await.expect("the lane task ended");
}
async fn next_outcome(&mut self) -> ToolOutcome {
timeout(self.outcomes.recv())
.await
.expect("an outcome arrived")
}
async fn next_indices(&mut self, n: usize) -> Vec<u64> {
let mut seen = Vec::new();
for _ in 0..n {
seen.push(self.next_outcome().await.entity.to_bits());
}
seen.sort_unstable();
seen
}
}
async fn timeout<T>(fut: impl Future<Output = T>) -> T {
tokio::time::timeout(Duration::from_secs(30), fut)
.await
.expect("the lane made progress")
}
fn sorted_bits(entities: &[Entity]) -> Vec<u64> {
let mut bits: Vec<u64> = entities.iter().map(|e| e.to_bits()).collect();
bits.sort_unstable();
bits
}
fn entity(index: u32) -> Entity {
Entity::from_raw_u32(index).expect("a small literal index is a valid entity id")
}
fn job(index: u32, pairs: Vec<(&'static str, &'static str)>) -> ToolJob {
job_with(index, pairs, crate::cancel::CancelToken::new())
}
fn job_with(
index: u32,
pairs: Vec<(&'static str, &'static str)>,
cancel: crate::cancel::CancelToken,
) -> ToolJob {
ToolJob {
entity: entity(index),
exec: Box::new(move || {
Box::pin(async move {
pairs
.into_iter()
.map(|(a, b)| (a.to_string(), b.to_string()))
.collect()
})
}),
cancel,
}
}
fn held_job(
index: u32,
started: Arc<Notify>,
release: Arc<Notify>,
cancel: crate::cancel::CancelToken,
) -> ToolJob {
ToolJob {
entity: entity(index),
exec: Box::new(move || {
Box::pin(async move {
started.notify_one();
release.notified().await;
vec![("held".to_string(), "done".to_string())]
})
}),
cancel,
}
}
fn parking_job(
index: u32,
started: Arc<Notify>,
release: Arc<Notify>,
cancel: crate::cancel::CancelToken,
) -> ToolJob {
ToolJob {
entity: entity(index),
exec: Box::new(move || {
Box::pin(async move {
off_lane(async move {
started.notify_one();
release.notified().await;
})
.await;
vec![("parked".to_string(), "done".to_string())]
})
}),
cancel,
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_lane_runs_batches_and_reports_them() {
let mut h = Harness::new(1);
h.submit(job(1, vec![("c", "r")]));
h.submit(job(2, vec![("c", "r")]));
let first = h.next_outcome().await;
assert_eq!(
first.results,
vec![("c".to_string(), "r".to_string())],
"the batch reported its call"
);
let mut seen = vec![first.entity.to_bits()];
seen.extend(h.next_indices(1).await);
seen.sort_unstable();
assert_eq!(
seen,
sorted_bits(&[entity(1), entity(2)]),
"both batches were reported"
);
h.drain().await;
assert!(h.outcomes.try_recv().is_err(), "no more outcomes");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_parked_batch_lets_the_batch_it_waits_on_run() {
let mut h = Harness::new(1);
let started = Arc::new(Notify::new());
let release = Arc::new(Notify::new());
h.submit(parking_job(
1,
started.clone(),
release.clone(),
crate::cancel::CancelToken::new(),
));
timeout(started.notified()).await;
assert_eq!(
(h.stats.busy(), h.stats.parked()),
(0, 1),
"the waiter gave the lane back"
);
let releaser = release.clone();
h.submit(ToolJob {
entity: entity(2),
exec: Box::new(move || {
Box::pin(async move {
releaser.notify_one();
vec![("c2".to_string(), "r2".to_string())]
})
}),
cancel: crate::cancel::CancelToken::new(),
});
assert_eq!(
h.next_indices(2).await,
sorted_bits(&[entity(1), entity(2)]),
"both batches finished"
);
h.drain().await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
async fn a_resumed_batch_takes_a_permit_again() {
let mut h = Harness::new(1);
let started = Arc::new(Notify::new());
let release = Arc::new(Notify::new());
h.submit(parking_job(
1,
started.clone(),
release.clone(),
crate::cancel::CancelToken::new(),
));
timeout(started.notified()).await;
let held_started = Arc::new(Notify::new());
let held_release = Arc::new(Notify::new());
h.submit(held_job(
2,
held_started.clone(),
held_release.clone(),
crate::cancel::CancelToken::new(),
));
timeout(held_started.notified()).await;
assert_eq!(h.stats.busy(), 1, "the lane is full again");
release.notify_one();
assert!(
tokio::time::timeout(Duration::from_millis(250), h.outcomes.recv())
.await
.is_err(),
"the resumed batch waited for a permit instead of running"
);
held_release.notify_one();
let first = h.next_outcome().await;
assert_eq!(first.entity, entity(2), "the holder finished first");
let second = h.next_outcome().await;
assert_eq!(second.entity, entity(1), "then the resumed batch");
h.drain().await;
assert_eq!((h.stats.busy(), h.stats.parked()), (0, 0));
}
#[tokio::test]
async fn off_lane_outside_the_lane_just_awaits() {
assert_eq!(off_lane(async { 7 }).await, 7);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_cancelled_batch_is_abandoned_and_frees_the_lane() {
let mut h = Harness::new(1);
let cancel = crate::cancel::CancelToken::new();
let started = Arc::new(Notify::new());
let release = Arc::new(Notify::new());
h.submit(held_job(1, started.clone(), release, cancel.clone()));
timeout(started.notified()).await;
assert_eq!((h.stats.queued(), h.stats.busy()), (0, 1));
h.submit(job(2, vec![("c2", "r2")]));
cancel.cancel();
let next = h.next_outcome().await;
assert_eq!(next.entity, entity(2), "the queued batch ran");
h.drain().await;
assert!(
h.outcomes.try_recv().is_err(),
"the cancelled batch reported nothing"
);
assert_eq!(h.stats.busy(), 0, "and gave its permit back");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_cancelled_parked_batch_leaves_the_counters_straight() {
let mut h = Harness::new(1);
let cancel = crate::cancel::CancelToken::new();
let started = Arc::new(Notify::new());
let release = Arc::new(Notify::new());
h.submit(parking_job(1, started.clone(), release, cancel.clone()));
timeout(started.notified()).await;
assert_eq!((h.stats.busy(), h.stats.parked()), (0, 1));
cancel.cancel();
h.submit(job(2, vec![("c2", "r2")]));
let next = h.next_outcome().await;
assert_eq!(next.entity, entity(2));
h.drain().await;
assert_eq!((h.stats.busy(), h.stats.parked()), (0, 0));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_batch_cancelled_while_queued_never_runs() {
let mut h = Harness::new(1);
let blocker = crate::cancel::CancelToken::new();
let started = Arc::new(Notify::new());
let release = Arc::new(Notify::new());
h.submit(held_job(1, started.clone(), release.clone(), blocker));
timeout(started.notified()).await;
let cancel = crate::cancel::CancelToken::new();
h.submit(job_with(2, vec![("c2", "r2")], cancel.clone()));
cancel.cancel();
release.notify_one();
let first = h.next_outcome().await;
assert_eq!(first.entity, entity(1));
h.drain().await;
assert!(
h.outcomes.try_recv().is_err(),
"the cancelled batch never produced results"
);
assert_eq!(h.stats.queued(), 0, "and left the queue count clean");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn the_lane_runs_batches_concurrently_up_to_its_cap() {
let h = Harness::new(3);
let barrier = Arc::new(tokio::sync::Barrier::new(3));
for i in 1..=3u32 {
let barrier = barrier.clone();
h.submit(ToolJob {
entity: entity(i),
exec: Box::new(move || {
Box::pin(async move {
barrier.wait().await;
vec![("c".to_string(), "r".to_string())]
})
}),
cancel: crate::cancel::CancelToken::new(),
});
}
let mut h = h;
h.drain().await;
for _ in 0..3 {
timeout(h.outcomes.recv()).await.expect("outcome present");
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_lane_survives_a_dropped_outcome_receiver() {
let mut h = Harness::new(1);
h.submit(job(9, vec![("c", "r")]));
h.outcomes.close();
h.drain().await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
async fn relief_widens_the_lane() {
let mut h = Harness::new(1);
let started = Arc::new(Notify::new());
let release = Arc::new(Notify::new());
h.submit(held_job(
1,
started.clone(),
release.clone(),
crate::cancel::CancelToken::new(),
));
timeout(started.notified()).await;
h.submit(job(2, vec![("c2", "r2")]));
assert!(h.stats.is_saturated(), "full, with a batch behind it");
assert_eq!(h.lane.relieve(0), 0, "relieving nothing changes nothing");
assert_eq!(h.lane.relieve(1), 1);
assert_eq!(h.stats.workers(), 2, "the cap moved with the permits");
let freed = h.next_outcome().await;
assert_eq!(freed.entity, entity(2), "the queued batch got in");
release.notify_one();
let held = h.next_outcome().await;
assert_eq!(held.entity, entity(1));
h.drain().await;
}
#[tokio::test]
async fn a_zero_width_lane_is_clamped_to_one() {
assert_eq!(ToolLaneStats::new(0).workers(), 1);
let mut h = Harness::new(0);
h.submit(job(7, vec![("c", "r")]));
assert_eq!(h.next_outcome().await.entity, entity(7));
h.drain().await;
}
#[test]
fn lane_stats_track_queue_depth_and_saturation() {
let stats = ToolLaneStats::new(2);
assert_eq!((stats.queued(), stats.busy(), stats.parked()), (0, 0, 0));
assert!(!stats.is_saturated(), "an idle lane is not saturated");
stats.enqueued();
stats.enqueued();
stats.enqueued();
assert_eq!(stats.queued(), 3);
stats.started();
stats.started();
assert_eq!((stats.queued(), stats.busy()), (1, 2));
assert!(
stats.is_saturated(),
"the lane is full with a batch still queued"
);
stats.began_park();
assert_eq!((stats.busy(), stats.parked()), (1, 1));
assert!(!stats.is_saturated(), "parked capacity is capacity");
stats.resumed();
assert_eq!((stats.busy(), stats.parked()), (2, 0));
stats.began_park();
stats.ended_park();
assert_eq!((stats.busy(), stats.parked()), (1, 0));
stats.finished();
stats.abandoned();
assert_eq!((stats.queued(), stats.busy()), (0, 0));
}
}