use super::config::MeasureError;
use std::cell::Cell;
use std::future::Future;
use std::pin::pin;
use std::sync::Arc;
use std::task::{Context, Wake, Waker};
use tokio::runtime::{Handle, Runtime};
use tokio::sync::Notify;
thread_local! {
static POLLING_BODY: Cell<bool> = const { Cell::new(false) };
}
struct PollingBody(bool);
impl PollingBody {
fn set() -> Self {
Self(POLLING_BODY.with(|flag| flag.replace(true)))
}
}
impl Drop for PollingBody {
fn drop(&mut self) {
POLLING_BODY.with(|flag| flag.set(self.0));
}
}
struct BodyWaker(Notify);
impl Wake for BodyWaker {
fn wake(self: Arc<Self>) {
self.0.notify_one();
}
fn wake_by_ref(self: &Arc<Self>) {
self.0.notify_one();
}
}
pub(super) fn runtime_context_error(caller: &str) -> Option<MeasureError> {
let in_task = tokio::task::try_id().is_some();
let in_body = POLLING_BODY.with(Cell::get);
let has_handle = Handle::try_current().is_ok();
if in_task || (has_handle && !in_body) {
return Some(MeasureError::AsyncRuntime(format!(
"{caller} called inside a runtime context. \
Call it from a thread outside any runtime, or inside a drive_async_body body."
)));
}
None
}
pub fn drive_async_body<F>(rt: &Runtime, body: F) -> Result<(), MeasureError>
where
F: Future,
{
if let Some(error) = runtime_context_error("drive_async_body") {
return Err(error);
}
let mut body = pin!(body);
let signal = Arc::new(BodyWaker(Notify::new()));
let waker = Waker::from(Arc::clone(&signal));
let mut cx = Context::from_waker(&waker);
loop {
let poll = {
let _context = rt.enter();
let _polling = PollingBody::set();
body.as_mut().poll(&mut cx)
};
if poll.is_ready() {
return Ok(());
}
rt.block_on(signal.0.notified());
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn current_thread() -> Runtime {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime builds")
}
#[test]
fn body_awaits_timer_on_current_thread() {
let rt = current_thread();
let done = Cell::new(false);
drive_async_body(&rt, async {
tokio::time::sleep(Duration::from_micros(1)).await;
done.set(true);
})
.expect("body runs");
assert!(done.get());
}
#[test]
fn body_spawns_task_on_current_thread() {
let rt = current_thread();
let value = Cell::new(0_u64);
drive_async_body(&rt, async {
let task = tokio::spawn(async { 7_u64 });
value.set(task.await.expect("task joins"));
})
.expect("body runs");
assert_eq!(value.get(), 7);
}
#[test]
fn polling_flag_resets_after_body() {
let rt = current_thread();
drive_async_body(&rt, async {
assert!(POLLING_BODY.with(Cell::get));
})
.expect("body runs");
assert!(!POLLING_BODY.with(Cell::get));
}
#[test]
fn inside_block_on_returns_error() {
let outer = current_thread();
let inner = current_thread();
let result = outer.block_on(async { drive_async_body(&inner, async {}) });
assert!(matches!(result, Err(MeasureError::AsyncRuntime(_))));
}
}