use std::pin::Pin;
use std::task::{Context as PollContext, Poll};
use futures::future::{FutureExt, LocalBoxFuture};
use performables::sync::channel;
use pin_project::pin_project;
use crate::core::ThreadAware;
use crate::runtime::Error;
use crate::runtime::dispatch::DispatcherClient;
use crate::task::{Context, JoinHandle};
type BoxedFutureFactory<'a, R> = Box<dyn (FnOnce(Context) -> LocalBoxFuture<'a, R>) + 'a + Send>;
struct ScopedJoin(channel::Receiver<()>);
impl Drop for ScopedJoin {
fn drop(&mut self) {
self.0.recv().expect_err("scoped storage signals destruction by disconnecting");
}
}
#[pin_project]
struct ScopedStorage<T> {
#[pin]
inner: T,
completion: channel::Sender<()>,
}
impl<FF> ScopedStorage<FF> {
fn into_future<F, R>(self, cx: Context) -> ScopedStorage<impl Future<Output = R>>
where
FF: FnOnce(Context) -> F,
F: Future<Output = R>,
{
let Self { inner, completion } = self;
ScopedStorage {
inner: async move { inner(cx).await },
completion,
}
}
}
impl<F: Future> Future for ScopedStorage<F> {
type Output = F::Output;
fn poll(self: Pin<&mut Self>, cx: &mut PollContext<'_>) -> Poll<Self::Output> {
self.project().inner.poll(cx)
}
}
#[derive(Debug)]
pub struct RuntimeScheduler {
pub(crate) dispatcher: DispatcherClient,
}
impl RuntimeScheduler {
pub(crate) const fn new(dispatcher: DispatcherClient) -> Self {
Self { dispatcher }
}
pub fn block_on<'a, FF, F, R>(&self, future_factory: FF) -> Result<R, Error>
where
FF: FnOnce(Context) -> F + Send + 'a,
F: Future<Output = R> + 'a,
R: Send + 'static,
{
if crate::runtime::worker::WorkerMarker::is_current() {
return Err(Error::block_on_from_worker());
}
drop(futures::executor::enter().map_err(Error::new)?);
let (completion, destroyed) = channel::unbounded();
let _join = ScopedJoin(destroyed);
let storage = ScopedStorage {
inner: future_factory,
completion,
};
let factory: BoxedFutureFactory<'a, R> = Box::new(move |cx| storage.into_future(cx).boxed_local());
let factory = unsafe { std::mem::transmute::<BoxedFutureFactory<'a, R>, BoxedFutureFactory<'static, R>>(factory) };
futures::executor::block_on(self.dispatcher.spawn(factory)).map_err(Error::new)
}
pub fn spawn_anywhere<D, F, R>(&self, data: D, future_factory: fn(Context, D) -> F) -> JoinHandle<R>
where
D: ThreadAware + 'static,
F: Future<Output = R> + 'static,
R: Send + 'static,
{
self.dispatcher.spawn(async move |cx| {
let mut data = data;
data.relocate(None, cx.thread());
future_factory(cx, data).await
})
}
pub fn spawn_blocking<B, R>(&self, body: B) -> JoinHandle<R>
where
B: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
self.dispatcher.next_blocking_worker().spawn_blocking(body)
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use std::cell::RefCell;
use std::future::ready;
use std::panic::{AssertUnwindSafe, catch_unwind, panic_any};
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::task::Waker;
use std::thread::{self, ThreadId};
use testing_aids::TEST_TIMEOUT;
use super::*;
use crate::runtime::Runtime;
use crate::task::JoinError;
struct DropAction<F: FnMut()>(F);
impl<F: FnMut()> Drop for DropAction<F> {
fn drop(&mut self) {
(self.0)();
}
}
#[test]
#[should_panic(expected = "scoped storage signals destruction by disconnecting")]
fn scoped_join_rejects_a_completion_message() {
let (completion, destroyed) = channel::unbounded();
completion.send(()).unwrap();
drop(ScopedJoin(destroyed));
}
fn check_storage_drop_order(panic_in_drop: bool) {
let (completion, destroyed) = channel::unbounded();
let observations = RefCell::new(Vec::new());
let record = || observations.borrow_mut().push(destroyed.try_recv().unwrap_err().is_empty());
let storage = ScopedStorage {
inner: (
DropAction(|| {
record();
if panic_in_drop {
panic_any(17u32);
}
}),
DropAction(record),
),
completion,
};
let outcome = catch_unwind(AssertUnwindSafe(|| drop(storage)));
assert_eq!(
(
outcome.is_err(),
observations.into_inner(),
destroyed.try_recv().unwrap_err().is_closed()
),
(panic_in_drop, vec![true, true], true,),
);
}
#[test]
fn scoped_storage_signals_after_all_borrowed_fields_drop() {
check_storage_drop_order(false);
}
#[test]
fn scoped_storage_signals_after_panicking_destructor_fields_drop() {
check_storage_drop_order(true);
}
#[test]
fn queued_scoped_factory_disconnects_after_its_borrowed_capture_drops() {
let (completion, destroyed) = channel::unbounded();
let mut observed = None;
let capture = DropAction(|| observed = Some(destroyed.try_recv().unwrap_err().is_empty()));
let storage = ScopedStorage {
inner: move |_: Context| {
drop(capture);
ready(())
},
completion,
};
let queued = move |cx| storage.into_future(cx);
drop(queued);
assert_eq!((observed, destroyed.try_recv().unwrap_err().is_closed()), (Some(true), true),);
}
#[derive(Debug, Default, PartialEq, Eq)]
struct DropRecord {
text: String,
thread: Option<ThreadId>,
}
#[derive(Debug, PartialEq, Eq)]
struct SendOnlyResult;
struct BorrowingFuture<'a> {
record: &'a mut DropRecord,
panic: Option<Arc<()>>,
_local: Rc<()>,
}
impl Future for BorrowingFuture<'_> {
type Output = SendOnlyResult;
fn poll(mut self: Pin<&mut Self>, _: &mut PollContext<'_>) -> Poll<Self::Output> {
if let Some(payload) = self.panic.take() {
panic_any(payload);
}
Poll::Ready(SendOnlyResult)
}
}
impl Drop for BorrowingFuture<'_> {
fn drop(&mut self) {
self.record.text.push_str("dropped");
self.record.thread = Some(thread::current().id());
}
}
#[test]
fn ready_scoped_future_keeps_completion_connected_until_destruction() {
let (completion, destroyed) = channel::unbounded();
let mut record = DropRecord::default();
let mut storage = Box::pin(ScopedStorage {
inner: BorrowingFuture {
record: &mut record,
panic: None,
_local: Rc::new(()),
},
completion,
});
let outcome = storage.as_mut().poll(&mut PollContext::from_waker(Waker::noop()));
assert_eq!(
(outcome, destroyed.try_recv().unwrap_err().is_empty()),
(Poll::Ready(SendOnlyResult), true)
);
drop(storage);
assert_eq!(
(record, destroyed.try_recv().unwrap_err().is_closed()),
(
DropRecord {
text: "dropped".to_owned(),
thread: Some(thread::current().id())
},
true,
),
);
}
#[test]
fn scoped_join_waits_for_storage_destruction_during_caller_unwind() {
let (completion, destroyed) = channel::unbounded();
let record = Mutex::new(None);
let payload = Arc::new(());
let expected = Arc::clone(&payload);
thread::scope(|scope| {
let (unwinding, unwind_started) = channel::unbounded();
let storage = ScopedStorage {
inner: DropAction(|| *record.lock().unwrap() = Some(thread::current().id())),
completion,
};
let worker = scope.spawn(move || {
unwind_started.recv_timeout(TEST_TIMEOUT).unwrap();
drop(storage);
});
let worker_id = worker.thread().id();
let outcome = catch_unwind(AssertUnwindSafe(|| {
let _join = ScopedJoin(destroyed);
let _unwind = DropAction(|| unwinding.send(()).unwrap());
panic_any(payload);
}));
let actual = outcome.unwrap_err().downcast::<Arc<()>>().unwrap();
assert_eq!((Arc::ptr_eq(&actual, &expected), *record.lock().unwrap()), (true, Some(worker_id)));
worker.join().unwrap();
});
}
#[cfg(not(miri))]
fn check_borrowed_future_completion(panics: bool) {
let runtime = Runtime::builder()
.workers(crate::runtime::WorkersPolicy::exactly(1))
.panic(crate::runtime::PanicPolicy::ignore())
.build()
.unwrap();
let worker = runtime.scheduler().block_on(async |_| thread::current().id()).unwrap();
let expected = Arc::new(());
let payload = panics.then(|| Arc::clone(&expected));
let mut record = DropRecord::default();
let outcome = catch_unwind(AssertUnwindSafe(|| {
runtime.scheduler().block_on(async |_| {
BorrowingFuture {
record: &mut record,
panic: payload,
_local: Rc::new(()),
}
.await
})
}))
.unwrap();
match outcome {
Ok(value) => {
assert!(!panics);
assert_eq!(value, SendOnlyResult);
}
Err(error) => {
assert!(panics);
let error = error.into_source().downcast::<JoinError>().unwrap();
assert!(error.is_panic());
let payload = error.into_panic().expect("the join error was checked as a panic");
assert!(Arc::ptr_eq(&payload.downcast::<Arc<()>>().unwrap(), &expected));
}
}
assert_eq!(
(record, runtime.scheduler().block_on(async |_| 23).unwrap()),
(
DropRecord {
text: "dropped".to_owned(),
thread: Some(worker)
},
23
),
);
}
#[cfg(not(miri))]
#[test]
fn scoped_return_waits_for_borrowed_non_send_future_destructor() {
check_borrowed_future_completion(false);
}
#[cfg(not(miri))]
#[test]
fn scoped_poll_panic_is_joined_after_borrowed_future_destruction() {
check_borrowed_future_completion(true);
}
}