use std::error::Error;
use std::sync::atomic::AtomicBool;
use std::sync::mpsc;
use rocketmq_error::CanonicalCondition;
use tokio::sync::oneshot;
use super::*;
fn executor() -> BlockingExecutor {
BlockingExecutor::new_with_budget(
BlockingPoolPolicy {
max_concurrency: 1,
..BlockingPoolPolicy::default()
},
BlockingLane::StorageIo,
GlobalBlockingBudget::isolated(1),
RuntimeHandle::new(tokio::runtime::Handle::current()),
)
}
async fn gated_task(executor: &BlockingExecutor) -> (BlockingTask<usize>, mpsc::Sender<()>) {
let (started_tx, started_rx) = oneshot::channel();
let (release_tx, release_rx) = mpsc::channel();
let task = executor
.submit_io("gated", move || {
let _ = started_tx.send(());
let _ = release_rx.recv();
42
})
.await
.unwrap();
started_rx.await.unwrap();
(task, release_tx)
}
fn assert_released(executor: &BlockingExecutor) {
let snapshot = executor.snapshot();
assert!(snapshot.tasks.is_empty());
assert_eq!(snapshot.global_running, 0);
assert_eq!(snapshot.global_available, 1);
}
#[tokio::test]
async fn diagnostics_aggregate_tracks_real_blocking_timeout_and_completion() {
let executor = executor();
let (mut task, release) = gated_task(&executor).await;
let aggregate = executor.aggregate();
let detailed = executor.snapshot();
assert_eq!(aggregate.lane, detailed.lane);
assert_eq!(aggregate.max_concurrency, detailed.max_concurrency);
assert_eq!(aggregate.max_queue_depth, detailed.max_queue_depth);
assert_eq!(aggregate.queued, detailed.queued);
assert_eq!(aggregate.running, detailed.running);
assert_eq!(aggregate.blocking_still_running, detailed.blocking_still_running);
assert_eq!(aggregate.task_kinds[0].1, 1);
assert_eq!(aggregate.task_kinds[1].1, 0);
assert_eq!(aggregate.task_kinds[2].1, 0);
assert!(aggregate.task_kinds[0].2 <= detailed.tasks[0].elapsed);
assert!(task.wait_until(Instant::now()).await.is_err());
let aggregate = executor.aggregate();
assert_eq!(aggregate.running, 0);
assert_eq!(aggregate.timed_out_still_running, 1);
assert_eq!(aggregate.blocking_still_running, 1);
assert_eq!(aggregate.task_kinds[0].1, 1);
release.send(()).unwrap();
assert_eq!(task.wait().await.unwrap(), 42);
let aggregate = executor.aggregate();
assert_eq!(aggregate.queued, 0);
assert_eq!(aggregate.blocking_still_running, 0);
assert!(aggregate.task_kinds.iter().all(|(_, count, _)| *count == 0));
}
#[tokio::test]
async fn expired_observation_retains_capacity_and_the_late_result() {
let executor = executor();
let (mut task, release) = gated_task(&executor).await;
let error = task.wait_until(Instant::now()).await.unwrap_err();
assert_eq!(error.condition(), CanonicalCondition::DeadlineExceeded);
let snapshot = executor.snapshot();
assert_eq!(snapshot.timed_out_still_running, 1);
assert_eq!(snapshot.global_running, 1);
assert_eq!(snapshot.global_available, 0);
release.send(()).unwrap();
assert_eq!(task.wait().await.unwrap(), 42);
assert_released(&executor);
assert_eq!(
task.wait().await.unwrap_err().condition(),
CanonicalCondition::Unavailable
);
}
#[tokio::test]
async fn dropping_one_wait_does_not_consume_execution_completion() {
let executor = executor();
let (mut task, release) = gated_task(&executor).await;
{
let wait = task.wait();
tokio::pin!(wait);
assert!(futures::poll!(&mut wait).is_pending());
}
assert_eq!(executor.snapshot().global_running, 1);
release.send(()).unwrap();
assert_eq!(task.wait().await.unwrap(), 42);
assert_released(&executor);
}
#[tokio::test]
async fn abandoning_the_ticket_keeps_real_work_tracked_until_exit() {
let executor = executor();
let (task, release) = gated_task(&executor).await;
let completion = task.join_handle.as_ref().unwrap().abort_handle();
drop(task);
assert_eq!(executor.snapshot().timed_out_still_running, 1);
assert_eq!(executor.snapshot().global_available, 0);
release.send(()).unwrap();
tokio::time::timeout(Duration::from_secs(5), async {
while !completion.is_finished() {
tokio::task::yield_now().await;
}
})
.await
.unwrap();
assert_released(&executor);
}
struct CaptureProbe {
executor: BlockingExecutor,
dropped: Arc<AtomicBool>,
}
impl Drop for CaptureProbe {
fn drop(&mut self) {
let snapshot = self.executor.snapshot();
self.dropped.store(
snapshot.global_running == 1 && snapshot.tasks.len() == 1,
Ordering::Release,
);
}
}
#[test]
fn cancellation_before_execution_releases_captures_capacity_and_registry() {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.max_blocking_threads(1)
.build()
.unwrap();
runtime.block_on(async {
let (started_tx, started_rx) = oneshot::channel();
let (release_tx, release_rx) = mpsc::channel();
let occupied = tokio::task::spawn_blocking(move || {
let _ = started_tx.send(());
let _ = release_rx.recv();
});
started_rx.await.unwrap();
let executor = executor();
let dropped = Arc::new(AtomicBool::new(false));
let capture = CaptureProbe {
executor: executor.clone(),
dropped: dropped.clone(),
};
let mut task = executor
.submit_io("never-started", move || {
drop(capture);
panic!("cancelled operation must not execute");
})
.await
.unwrap();
task.join_handle.as_ref().unwrap().abort();
release_tx.send(()).unwrap();
occupied.await.unwrap();
let error = task.wait().await.unwrap_err();
assert!(error
.source()
.unwrap()
.downcast_ref::<tokio::task::JoinError>()
.unwrap()
.is_cancelled());
assert!(dropped.load(Ordering::Acquire));
assert_released(&executor);
});
}
#[tokio::test]
async fn panic_settles_execution_and_destroys_user_resources() {
let executor = executor();
let dropped = Arc::new(AtomicBool::new(false));
let capture = CaptureProbe {
executor: executor.clone(),
dropped: dropped.clone(),
};
let mut task = executor
.submit_io("panicking", move || {
let _capture = capture;
panic!("injected blocking failure");
})
.await
.unwrap();
let error = task.wait().await.unwrap_err();
assert_eq!(error.operation(), crate::RuntimeOperation::RunBlockingTask);
assert!(error
.source()
.unwrap()
.downcast_ref::<tokio::task::JoinError>()
.unwrap()
.is_panic());
assert!(!error.to_string().contains("injected blocking failure"));
assert!(dropped.load(Ordering::Acquire));
assert_released(&executor);
assert_eq!(executor.spawn_io("after-panic", || 7).await.unwrap(), 7);
assert_released(&executor);
}
#[tokio::test]
async fn expired_drain_deadline_prevents_user_closure_from_starting() {
let executor = executor();
let task_id = BlockingTaskId(1);
executor.tasks.insert(
task_id,
BlockingTaskMeta {
id: task_id,
name: Arc::from("expired-drain-work"),
kind: BlockingKind::ShortIo,
state: BlockingTaskState::Running,
queued_at: Instant::now(),
started_at: Some(Instant::now()),
},
);
let permit = executor
.budget
.acquire(BlockingLane::StorageIo, Instant::now() + Duration::from_secs(1))
.await
.unwrap();
let called = Arc::new(AtomicBool::new(false));
let operation_called = Arc::clone(&called);
let work = BlockingWork {
operation: move || operation_called.store(true, Ordering::Release),
permit,
completion: BlockingCompletionGuard {
tasks: Arc::clone(&executor.tasks),
task_id,
_isolated_registration: None,
},
execution_deadline: Some(ShutdownDeadline::after(Duration::ZERO)),
};
let error = work.run().unwrap_err();
assert_eq!(error.condition(), CanonicalCondition::DeadlineExceeded);
assert!(!called.load(Ordering::Acquire));
assert_released(&executor);
}