Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

future: provide try_join! macro #2169

Merged
merged 3 commits into from
Jan 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tokio/src/future/maybe_done.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ impl<Fut: Future> MaybeDone<Fut> {
/// The output of this method will be [`Some`] if and only if the inner
/// future has been completed and [`take_output`](MaybeDone::take_output)
/// has not yet been called.
pub(crate) fn output_mut(self: Pin<&mut Self>) -> Option<&mut Fut::Output> {
pub fn output_mut(self: Pin<&mut Self>) -> Option<&mut Fut::Output> {
unsafe {
let this = self.get_unchecked_mut();
match this {
Expand Down
6 changes: 6 additions & 0 deletions tokio/src/macros/join.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
/// concurrently on the same task. Each async expression evaluates to a future
/// and the futures from each expression are multiplexed on the current task.
///
/// When working with async expressions returning `Result`, `join!` will wait
/// for **all** branches complete regardless if any complete with `Err`. Use
/// [`try_join!`] to return early when `Err` is encountered.
///
/// [`try_join!`]: macro@try_join
///
/// # Notes
///
/// The supplied futures are stored inline and does not require allocating a
Expand Down
3 changes: 3 additions & 0 deletions tokio/src/macros/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ cfg_macros! {
#[macro_use]
mod thread_local;

#[macro_use]
mod try_join;

// Includes re-exports needed to implement macros
#[doc(hidden)]
pub mod support;
131 changes: 131 additions & 0 deletions tokio/src/macros/try_join.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/// Wait on multiple concurrent branches, returning when **all** branches
/// complete with `Ok(_)` or on the first `Err(_)`.
///
/// The `try_join!` macro must be used inside of async functions, closures, and
/// blocks.
///
/// Similar to [`join!`], the `try_join!` macro takes a list of async
/// expressions and evaluates them concurrently on the same task. Each async
/// expression evaluates to a future and the futures from each expression are
/// multiplexed on the current task. The `try_join!` macro returns when **all**
/// branches return with `Ok` or when the **first** branch returns with `Err`.
///
/// [`join!`]: macro@join
///
/// # Notes
///
/// The supplied futures are stored inline and does not require allocating a
/// `Vec`.
///
/// ### Runtime characteristics
///
/// By running all async expressions on the current task, the expressions are
/// able to run **concurrently** but not in **parallel**. This means all
/// expressions are run on the same thread and if one branch blocks the thread,
/// all other expressions will be unable to continue. If parallelism is
/// required, spawn each async expression using [`tokio::spawn`] and pass the
/// join handle to `try_join!`.
///
/// [`tokio::spawn`]: crate::spawn
///
/// # Examples
///
/// Basic try_join with two branches.
///
/// ```
/// async fn do_stuff_async() -> Result<(), &'static str> {
/// // async work
/// # Ok(())
/// }
///
/// async fn more_async_work() -> Result<(), &'static str> {
/// // more here
/// # Ok(())
/// }
///
/// #[tokio::main]
/// async fn main() {
/// let res = tokio::try_join!(
/// do_stuff_async(),
/// more_async_work());
///
/// match res {
/// Ok((first, second)) => {
/// // do something with the values
/// }
/// Err(err) => {
/// println!("processing failed; error = {}", err);
/// }
/// }
/// }
/// ```
#[macro_export]
macro_rules! try_join {
(@ {
// One `_` for each branch in the `try_join!` macro. This is not used once
// normalization is complete.
( $($count:tt)* )

// Normalized try_join! branches
$( ( $($skip:tt)* ) $e:expr, )*

}) => {{
use $crate::macros::support::{maybe_done, poll_fn, Future, Pin};
use $crate::macros::support::Poll::{Ready, Pending};

// Safety: nothing must be moved out of `futures`. This is to satisfy
// the requirement of `Pin::new_unchecked` called below.
let mut futures = ( $( maybe_done($e), )* );

poll_fn(move |cx| {
let mut is_pending = false;

$(
// Extract the future for this branch from the tuple.
let ( $($skip,)* fut, .. ) = &mut futures;

// Safety: future is stored on the stack above
// and never moved.
let mut fut = unsafe { Pin::new_unchecked(fut) };

// Try polling
if fut.as_mut().poll(cx).is_pending() {
is_pending = true;
} else if fut.as_mut().output_mut().expect("expected completed future").is_err() {
return Ready(Err(fut.take_output().expect("expected completed future").err().unwrap()))
}
)*

if is_pending {
Pending
} else {
Ready(Ok(($({
// Extract the future for this branch from the tuple.
let ( $($skip,)* fut, .. ) = &mut futures;

// Safety: future is stored on the stack above
// and never moved.
let mut fut = unsafe { Pin::new_unchecked(fut) };

fut
.take_output()
.expect("expected completed future")
.ok()
.expect("expected Ok(_)")
},)*)))
}
}).await
}};

// ===== Normalize =====

(@ { ( $($s:tt)* ) $($t:tt)* } $e:expr, $($r:tt)* ) => {
$crate::try_join!(@{ ($($s)* _) $($t)* ($($s)*) $e, } $($r)*)
};

// ===== Entry point =====

( $($e:expr),* $(,)?) => {
$crate::try_join!(@{ () } $($e,)*)
};
}
6 changes: 3 additions & 3 deletions tokio/tests/macros_join.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ async fn sync_two_lit_expr_no_comma() {
}

#[tokio::test]
async fn sync_two_await() {
let (tx1, rx1) = oneshot::channel();
let (tx2, rx2) = oneshot::channel();
async fn two_await() {
let (tx1, rx1) = oneshot::channel::<&str>();
let (tx2, rx2) = oneshot::channel::<u32>();

let mut join = task::spawn(async {
tokio::join!(async { rx1.await.unwrap() }, async { rx2.await.unwrap() })
Expand Down
100 changes: 100 additions & 0 deletions tokio/tests/macros_try_join.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use tokio::sync::oneshot;
use tokio_test::{assert_pending, assert_ready, task};

#[tokio::test]
async fn sync_one_lit_expr_comma() {
let foo = tokio::try_join!(async { ok(1) },);

assert_eq!(foo, Ok((1,)));
}

#[tokio::test]
async fn sync_one_lit_expr_no_comma() {
let foo = tokio::try_join!(async { ok(1) });

assert_eq!(foo, Ok((1,)));
}

#[tokio::test]
async fn sync_two_lit_expr_comma() {
let foo = tokio::try_join!(async { ok(1) }, async { ok(2) },);

assert_eq!(foo, Ok((1, 2)));
}

#[tokio::test]
async fn sync_two_lit_expr_no_comma() {
let foo = tokio::try_join!(async { ok(1) }, async { ok(2) });

assert_eq!(foo, Ok((1, 2)));
}

#[tokio::test]
async fn two_await() {
let (tx1, rx1) = oneshot::channel::<&str>();
let (tx2, rx2) = oneshot::channel::<u32>();

let mut join =
task::spawn(async { tokio::try_join!(async { rx1.await }, async { rx2.await }) });

assert_pending!(join.poll());

tx2.send(123).unwrap();
assert!(join.is_woken());
assert_pending!(join.poll());

tx1.send("hello").unwrap();
assert!(join.is_woken());
let res: Result<(&str, u32), _> = assert_ready!(join.poll());

assert_eq!(Ok(("hello", 123)), res);
}

#[tokio::test]
async fn err_abort_early() {
let (tx1, rx1) = oneshot::channel::<&str>();
let (tx2, rx2) = oneshot::channel::<u32>();
let (_tx3, rx3) = oneshot::channel::<u32>();

let mut join = task::spawn(async {
tokio::try_join!(async { rx1.await }, async { rx2.await }, async {
rx3.await
})
});

assert_pending!(join.poll());

tx2.send(123).unwrap();
assert!(join.is_woken());
assert_pending!(join.poll());

drop(tx1);
assert!(join.is_woken());

let res = assert_ready!(join.poll());

assert!(res.is_err());
}

#[test]
fn join_size() {
use futures::future;
use std::mem;

let fut = async {
let ready = future::ready(ok(0i32));
tokio::try_join!(ready)
};
assert_eq!(mem::size_of_val(&fut), 16);

let fut = async {
let ready1 = future::ready(ok(0i32));
let ready2 = future::ready(ok(0i32));
tokio::try_join!(ready1, ready2)
};
assert_eq!(mem::size_of_val(&fut), 28);
}

fn ok<T>(val: T) -> Result<T, ()> {
Ok(val)
}