Skip to content

Commit 882d1cc

Browse files
committed
add tryx_collect and tryx_join_all adapters
1 parent 65266bb commit 882d1cc

16 files changed

Lines changed: 594 additions & 84 deletions

File tree

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,13 @@ futures-core = { version = "0.3.28", default-features = false }
1414
futures-sink = { version = "0.3.28", default-features = false }
1515
futures-util = { version = "0.3.28", default-features = false, features = ["sink"] }
1616
tokio = { version = "1.28.2", optional = true }
17+
futures = { version = "0.3.28", optional = true }
18+
pin-project-lite = "0.2.9"
1719

1820
[dev-dependencies]
1921
anyhow = "1.0.72"
22+
async-stream = "0.3.5"
23+
debug-ignore = "1.0.5"
2024
tempfile = "3.6.0"
2125
tokio = { version = "1.28.2", features = ["fs", "io-util", "macros", "rt", "rt-multi-thread", "sync", "time"] }
2226
tokio-test = "0.4.0"
@@ -31,4 +35,4 @@ alloc = ["futures-core/alloc", "futures-sink/alloc", "futures-util/alloc"]
3135
macros = []
3236

3337
# Internal-only feature, used for documentation and doctests. Not part of the public API.
34-
internal-docs = ["dep:tokio", "tokio?/io-util", "tokio?/macros", "tokio?/rt"]
38+
internal-docs = ["dep:futures", "dep:tokio", "tokio?/io-util", "tokio?/macros", "tokio?/rt"]

build.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// The rustc-cfg listed below are considered public API, but it is *unstable*
2+
// and outside of the normal semver guarantees:
3+
//
4+
// - `futures_no_atomic_cas`
5+
// Assume the target does *not* support atomic CAS operations.
6+
// This is usually detected automatically by the build script, but you may
7+
// need to enable it manually when building for custom targets or using
8+
// non-cargo build systems that don't run the build script.
9+
//
10+
// With the exceptions mentioned above, the rustc-cfg emitted by the build
11+
// script are *not* public API.
12+
13+
#![warn(rust_2018_idioms, single_use_lifetimes)]
14+
15+
use std::env;
16+
17+
include!("no_atomic_cas.rs");
18+
19+
fn main() {
20+
let target = match env::var("TARGET") {
21+
Ok(target) => target,
22+
Err(e) => {
23+
println!(
24+
"cargo:warning={}: unable to get TARGET environment variable: {}",
25+
env!("CARGO_PKG_NAME"),
26+
e
27+
);
28+
return;
29+
}
30+
};
31+
32+
// Note that this is `no_*`, not `has_*`. This allows treating
33+
// `cfg(target_has_atomic = "ptr")` as true when the build script doesn't
34+
// run. This is needed for compatibility with non-cargo build systems that
35+
// don't run the build script.
36+
if NO_ATOMIC_CAS.contains(&&*target) {
37+
println!("cargo:rustc-cfg=futures_no_atomic_cas");
38+
}
39+
40+
println!("cargo:rerun-if-changed=no_atomic_cas.rs");
41+
}

no_atomic_cas.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// This file is @generated by no-atomic-cas.sh.
2+
// It is not intended for manual editing.
3+
4+
const NO_ATOMIC_CAS: &[&str] = &[
5+
"armv4t-none-eabi",
6+
"armv5te-none-eabi",
7+
"avr-unknown-gnu-atmega328",
8+
"bpfeb-unknown-none",
9+
"bpfel-unknown-none",
10+
"msp430-none-elf",
11+
"riscv32i-unknown-none-elf",
12+
"riscv32im-unknown-none-elf",
13+
"riscv32imc-unknown-none-elf",
14+
"thumbv4t-none-eabi",
15+
"thumbv5te-none-eabi",
16+
"thumbv6m-none-eabi",
17+
];

scripts/no-atomic-cas.sh

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
IFS=$'\n\t'
4+
cd "$(dirname "$0")"/..
5+
6+
# Update the list of targets that do not support atomic CAS operations.
7+
#
8+
# Usage:
9+
# ./scripts/no-atomic-cas.sh
10+
11+
file="no_atomic_cas.rs"
12+
13+
while [[ "$#" -gt 0 ]]; do
14+
case $1 in
15+
-h|--help) echo "Usage: no-atomic-cas.sh"; exit 0 ;;
16+
*) echo "Unknown parameter passed: $1"; exit 1 ;;
17+
esac
18+
shift
19+
done
20+
21+
# For -Z unstable-options
22+
export RUSTC_BOOTSTRAP=1
23+
24+
no_atomic_cas=()
25+
for target in $(rustc --print target-list); do
26+
target_spec=$(rustc --print target-spec-json -Z unstable-options --target "${target}")
27+
res=$(jq <<<"${target_spec}" -r 'select(."atomic-cas" == false)')
28+
[[ -z "${res}" ]] || no_atomic_cas+=("${target}")
29+
done
30+
31+
cat >"${file}" <<EOF
32+
// This file is @generated by $(basename "$0").
33+
// It is not intended for manual editing.
34+
35+
const NO_ATOMIC_CAS: &[&str] = &[
36+
EOF
37+
for target in "${no_atomic_cas[@]}"; do
38+
echo " \"${target}\"," >>"${file}"
39+
done
40+
cat >>"${file}" <<EOF
41+
];
42+
EOF

src/future/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//! Alternative adapters for asynchronous values.
2+
//!
3+
//! This module contains adapters that are similar to the `try_` adapters in the [`futures::future`]
4+
//! module, but don't cancel other futures if one of them errors out.
5+
6+
#[cfg(feature = "alloc")]
7+
mod tryx_join_all;
8+
#[cfg(feature = "alloc")]
9+
pub use tryx_join_all::{tryx_join_all, TryxJoinAll};

src/future/tryx_join_all.rs

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
//! Definition of the `TryxJoinAll` adapter, waiting for all of a list of
2+
//! futures to finish with either success or error.
3+
4+
use crate::{
5+
stream::{TryStreamExt, TryxCollect},
6+
support::assert_future,
7+
};
8+
use alloc::{boxed::Box, vec::Vec};
9+
use core::{
10+
fmt,
11+
future::Future,
12+
iter::FromIterator,
13+
mem,
14+
pin::Pin,
15+
task::{Context, Poll},
16+
};
17+
use futures_core::future::TryFuture;
18+
use futures_util::{
19+
future::{IntoFuture, MaybeDone, TryFutureExt},
20+
stream::FuturesOrdered,
21+
};
22+
23+
#[cfg(not(futures_no_atomic_cas))]
24+
pub(crate) const SMALL: usize = 30;
25+
26+
/// Future for the [`tryx_join_all`] function.
27+
#[must_use = "futures do nothing unless you `.await` or poll them"]
28+
pub struct TryxJoinAll<F>
29+
where
30+
F: TryFuture,
31+
{
32+
kind: TryxJoinAllKind<F>,
33+
}
34+
35+
enum TryxJoinAllKind<F>
36+
where
37+
F: TryFuture,
38+
{
39+
Small {
40+
elems: Pin<Box<[MaybeDone<IntoFuture<F>>]>>,
41+
},
42+
#[cfg(not(futures_no_atomic_cas))]
43+
Big {
44+
// The use of FuturesOrdered here ensures that in case of errors, the first future listed in
45+
// the iterator that errors out will be returned.
46+
fut: TryxCollect<FuturesOrdered<IntoFuture<F>>, Vec<F::Ok>>,
47+
},
48+
}
49+
50+
impl<F> fmt::Debug for TryxJoinAll<F>
51+
where
52+
F: TryFuture + fmt::Debug,
53+
F::Ok: fmt::Debug,
54+
F::Error: fmt::Debug,
55+
F::Output: fmt::Debug,
56+
{
57+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58+
match self.kind {
59+
TryxJoinAllKind::Small { ref elems } => {
60+
f.debug_struct("TryxJoinAll").field("elems", elems).finish()
61+
}
62+
#[cfg(not(futures_no_atomic_cas))]
63+
TryxJoinAllKind::Big { ref fut, .. } => fmt::Debug::fmt(fut, f),
64+
}
65+
}
66+
}
67+
68+
/// Creates a future which represents either a collection of the results of the futures given or an
69+
/// error.
70+
///
71+
/// The returned future will drive execution for all of its underlying futures, collecting the
72+
/// results into a destination `Vec<T>` in the same order as they were provided.
73+
///
74+
/// Unlike [`futures::future::try_join_all`], if any future returns an error then all other futures
75+
/// will **not** be canceled. Instead, all other futures will be run to completion.
76+
///
77+
/// * If all futures complete successfully, then the returned future will succeed with a
78+
/// `Vec` of all the successful results.
79+
/// * If one or more futures fail, then the returned future will error out with the error
80+
/// for the first future listed that failed.
81+
///
82+
/// This function is only available when the `std` or `alloc` feature of this library is activated,
83+
/// and it is activated by default.
84+
///
85+
/// # Why use `tryx_join_all`?
86+
///
87+
/// See the documentation for [`tryx_join`](crate::tryx_join) for a discussion of why you might
88+
/// want to use a `tryx_` adapter.
89+
///
90+
/// # See Also
91+
///
92+
/// `tryx_join_all` will switch to the more powerful [`FuturesOrdered`] for performance reasons if
93+
/// the number of futures is large. You may want to look into using it or its counterpart
94+
/// [`FuturesUnordered`][futures::stream::FuturesUnordered] directly.
95+
///
96+
/// Some examples for additional functionality provided by these are:
97+
///
98+
/// * Adding new futures to the set even after it has been started.
99+
///
100+
/// * Only polling the specific futures that have been woken. In cases where you have a lot of
101+
/// futures this will result in much more efficient polling.
102+
///
103+
/// # Examples
104+
///
105+
/// ```
106+
/// # futures::executor::block_on(async {
107+
/// use futures::future;
108+
/// use cancel_safe_futures::future::tryx_join_all;
109+
///
110+
/// let futures = vec![
111+
/// future::ok::<u32, u32>(1),
112+
/// future::ok::<u32, u32>(2),
113+
/// future::ok::<u32, u32>(3),
114+
/// ];
115+
///
116+
/// assert_eq!(tryx_join_all(futures).await, Ok(vec![1, 2, 3]));
117+
///
118+
/// let futures = vec![
119+
/// future::ok::<u32, u32>(1),
120+
/// future::err::<u32, u32>(2),
121+
/// future::ok::<u32, u32>(3),
122+
/// ];
123+
///
124+
/// assert_eq!(tryx_join_all(futures).await, Err(2));
125+
/// # });
126+
/// ```
127+
pub fn tryx_join_all<I>(iter: I) -> TryxJoinAll<I::Item>
128+
where
129+
I: IntoIterator,
130+
I::Item: TryFuture,
131+
{
132+
let iter = iter.into_iter().map(TryFutureExt::into_future);
133+
134+
#[cfg(futures_no_atomic_cas)]
135+
{
136+
let kind = TryxJoinAllKind::Small {
137+
elems: iter.map(MaybeDone::Future).collect::<Box<[_]>>().into(),
138+
};
139+
140+
assert_future::<Result<Vec<<I::Item as TryFuture>::Ok>, <I::Item as TryFuture>::Error>, _>(
141+
TryxJoinAll { kind },
142+
)
143+
}
144+
145+
#[cfg(not(futures_no_atomic_cas))]
146+
{
147+
let kind = match iter.size_hint().1 {
148+
Some(max) if max <= SMALL => TryxJoinAllKind::Small {
149+
elems: iter.map(MaybeDone::Future).collect::<Box<[_]>>().into(),
150+
},
151+
_ => TryxJoinAllKind::Big {
152+
fut: iter.collect::<FuturesOrdered<_>>().tryx_collect(),
153+
},
154+
};
155+
156+
assert_future::<Result<Vec<<I::Item as TryFuture>::Ok>, <I::Item as TryFuture>::Error>, _>(
157+
TryxJoinAll { kind },
158+
)
159+
}
160+
}
161+
162+
impl<F> Future for TryxJoinAll<F>
163+
where
164+
F: TryFuture,
165+
{
166+
type Output = Result<Vec<F::Ok>, F::Error>;
167+
168+
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
169+
enum FinalState {
170+
Pending,
171+
AllDone,
172+
}
173+
174+
match &mut self.kind {
175+
TryxJoinAllKind::Small { elems } => {
176+
let mut state = FinalState::AllDone;
177+
178+
for elem in iter_pin_mut(elems.as_mut()) {
179+
match elem.poll(cx) {
180+
Poll::Pending => state = FinalState::Pending,
181+
Poll::Ready(()) => {}
182+
}
183+
}
184+
185+
match state {
186+
FinalState::Pending => Poll::Pending,
187+
FinalState::AllDone => {
188+
let mut elems = mem::replace(elems, Box::pin([]));
189+
let results: Result<Vec<_>, _> = iter_pin_mut(elems.as_mut())
190+
.map(|e| e.take_output().unwrap())
191+
.collect();
192+
Poll::Ready(results)
193+
}
194+
}
195+
}
196+
#[cfg(not(futures_no_atomic_cas))]
197+
TryxJoinAllKind::Big { fut } => Pin::new(fut).poll(cx),
198+
}
199+
}
200+
}
201+
202+
impl<F> FromIterator<F> for TryxJoinAll<F>
203+
where
204+
F: TryFuture,
205+
{
206+
fn from_iter<T: IntoIterator<Item = F>>(iter: T) -> Self {
207+
tryx_join_all(iter)
208+
}
209+
}
210+
211+
pub(crate) fn iter_pin_mut<T>(slice: Pin<&mut [T]>) -> impl Iterator<Item = Pin<&mut T>> {
212+
// Safety: `std` _could_ make this unsound if it were to decide Pin's
213+
// invariants aren't required to transmit through slices. Otherwise this has
214+
// the same safety as a normal field pin projection.
215+
unsafe { slice.get_unchecked_mut() }
216+
.iter_mut()
217+
.map(|t| unsafe { Pin::new_unchecked(t) })
218+
}

src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@
7272
//! The `tryx` library includes:
7373
//!
7474
//! * [`tryx_join`]: similar to [`tokio::try_join`].
75+
//! * [`future::tryx_join_all`]: similar to [`futures::future::try_join_all`].
76+
//! * [`TryStreamExt`]: contains extension methods similar to [`futures::stream::TryStreamExt`].
7577
//!
7678
//! ### Example
7779
//!
@@ -87,6 +89,9 @@
8789
#![warn(missing_docs)]
8890
#![cfg_attr(doc_cfg, feature(doc_cfg, doc_auto_cfg))]
8991

92+
#[cfg(feature = "alloc")]
93+
extern crate alloc;
94+
9095
// Includes re-exports used by macros.
9196
//
9297
// This module is not intended to be part of the public API. In general, any
@@ -95,7 +100,11 @@
95100
#[doc(hidden)]
96101
pub mod macros;
97102

103+
pub mod future;
98104
pub mod prelude;
99105
pub mod sink;
106+
pub mod stream;
107+
mod support;
100108

101109
pub use sink::SinkExt;
110+
pub use stream::TryStreamExt;

0 commit comments

Comments
 (0)