Skip to content

Commit 69a7121

Browse files
authored
Introduce named global executor for choice in execution (#2)
It is a good for binary crate to active at most one global spawner compat. But it could be relatively hard if dependency graph is large. In this case, it would be good to offer choices in execution. This commit uses environment variable SPAWNS_GLOBAL_EXECUTOR to choose one in absent of thread context spawners.
1 parent 3f38b2d commit 69a7121

File tree

7 files changed

+133
-33
lines changed

7 files changed

+133
-33
lines changed

spawns-compat/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ async-global-executor = { version = "2", optional = true }
2424

2525
[dev-dependencies]
2626
async-std = "1.12.0"
27+
futures-lite = "2.3.0"
2728
tokio = { version = "1.37.0", features = ["full"] }
2829

2930
[package.metadata.docs.rs]

spawns-compat/src/async_global_executor.rs

+9-5
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ use spawns_core::{Compat, Task, COMPATS};
33
use std::boxed::Box;
44

55
#[distributed_slice(COMPATS)]
6-
pub static ASYNC_GLOBAL_EXECUTOR: Compat = Compat::Global(async_global);
6+
pub static ASYNC_GLOBAL_EXECUTOR: Compat = Compat::NamedGlobal {
7+
name: "async-global-executor",
8+
spawn: async_global,
9+
};
710

811
fn async_global(task: Task) {
912
let Task { future, .. } = task;
@@ -14,11 +17,12 @@ fn async_global(task: Task) {
1417
#[cfg(feature = "async-global-executor")]
1518
#[cfg(not(feature = "smol"))]
1619
mod tests {
20+
use futures_lite::future;
1721
use spawns_core::*;
1822

1923
#[test]
2024
fn spawn_one() {
21-
async_std::task::block_on(async {
25+
future::block_on(async {
2226
let handle = spawn(async { id() });
2327
let id = handle.id();
2428
assert_eq!(handle.await.unwrap(), id);
@@ -27,7 +31,7 @@ mod tests {
2731

2832
#[test]
2933
fn spawn_cascading() {
30-
async_std::task::block_on(async {
34+
future::block_on(async {
3135
let handle = spawn(async { spawn(async { id() }) });
3236
let handle = handle.await.unwrap();
3337
let id = handle.id();
@@ -37,7 +41,7 @@ mod tests {
3741

3842
#[test]
3943
fn spawn_interleaving() {
40-
async_std::task::block_on(async move {
44+
future::block_on(async move {
4145
let handle = spawn(async { async_std::task::spawn(async { spawn(async { id() }) }) });
4246
let handle = handle.await.unwrap().await;
4347
let id = handle.id();
@@ -47,7 +51,7 @@ mod tests {
4751

4852
#[test]
4953
fn spawn_into_smol() {
50-
async_std::task::block_on(async move {
54+
future::block_on(async move {
5155
let handle = spawn(async { async_std::task::spawn(async { try_id() }) });
5256
let handle = handle.await.unwrap();
5357
assert_eq!(handle.await, None);

spawns-compat/src/smol.rs

+9-5
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ use spawns_core::{Compat, Task, COMPATS};
33
use std::boxed::Box;
44

55
#[distributed_slice(COMPATS)]
6-
pub static SMOL: Compat = Compat::Global(smol_global);
6+
pub static SMOL: Compat = Compat::NamedGlobal {
7+
name: "smol",
8+
spawn: smol_global,
9+
};
710

811
fn smol_global(task: Task) {
912
let Task { future, .. } = task;
@@ -14,11 +17,12 @@ fn smol_global(task: Task) {
1417
#[cfg(feature = "smol")]
1518
#[cfg(not(feature = "async-global-executor"))]
1619
mod tests {
20+
use futures_lite::future;
1721
use spawns_core::*;
1822

1923
#[test]
2024
fn spawn_one() {
21-
smol::block_on(async {
25+
future::block_on(async {
2226
let handle = spawn(async { id() });
2327
let id = handle.id();
2428
assert_eq!(handle.await.unwrap(), id);
@@ -27,7 +31,7 @@ mod tests {
2731

2832
#[test]
2933
fn spawn_cascading() {
30-
smol::block_on(async {
34+
future::block_on(async {
3135
let handle = spawn(async { spawn(async { id() }) });
3236
let handle = handle.await.unwrap();
3337
let id = handle.id();
@@ -37,7 +41,7 @@ mod tests {
3741

3842
#[test]
3943
fn spawn_interleaving() {
40-
smol::block_on(async move {
44+
future::block_on(async move {
4145
let handle = spawn(async { smol::spawn(async { spawn(async { id() }) }) });
4246
let handle = handle.await.unwrap().await;
4347
let id = handle.id();
@@ -47,7 +51,7 @@ mod tests {
4751

4852
#[test]
4953
fn spawn_into_smol() {
50-
smol::block_on(async move {
54+
future::block_on(async move {
5155
let handle = spawn(async { smol::spawn(async { try_id() }) });
5256
let handle = handle.await.unwrap();
5357
assert_eq!(handle.await, None);

spawns-core/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ compat = ["linkme"]
1515
panic-multiple-global-spawners = []
1616
test-compat-global1 = ["compat"]
1717
test-compat-global2 = ["compat", "test-compat-global1"]
18+
test-named-global = []
1819

1920
[dependencies]
2021
linkme = { version = "0.3.25", optional = true }

spawns-core/src/compat.rs

+73-18
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
use crate::Task;
22
use linkme::distributed_slice;
3+
use std::sync::OnceLock;
34

45
/// Compat encapsulate functions to find async runtimes to spawn task.
56
pub enum Compat {
7+
/// Named global function to spawn task.
8+
NamedGlobal { name: &'static str, spawn: fn(Task) },
69
/// Global function to spawn task.
7-
///
8-
/// [spawn](`crate::spawn()`) will panic if there is no local spawners but multiple global spawners.
10+
#[doc(hidden)]
11+
#[deprecated(since = "1.0.3", note = "use NamedGlobal instead")]
912
Global(fn(Task)),
1013
#[allow(clippy::type_complexity)]
1114
/// Local function to detect async runtimes.
@@ -16,33 +19,85 @@ pub enum Compat {
1619
#[distributed_slice]
1720
pub static COMPATS: [Compat] = [..];
1821

19-
pub(crate) fn find_spawn() -> Option<fn(Task)> {
20-
match COMPATS.len() {
21-
0 => return None,
22-
1 => match COMPATS[0] {
23-
Compat::Global(inject) => return Some(inject),
24-
Compat::Local(detect) => return detect(),
25-
},
26-
_ => {}
27-
}
22+
#[derive(Clone, Copy)]
23+
pub(crate) enum Failure {
24+
NotFound,
25+
#[allow(dead_code)]
26+
MultipleGlobals,
27+
}
2828

29-
let mut last_global = None;
29+
fn pick_global(choose: Option<&str>) -> Result<fn(Task), Failure> {
3030
let mut globals = 0;
31-
match COMPATS.iter().find_map(|injection| match injection {
32-
Compat::Local(local) => local(),
31+
let mut last_named = None;
32+
let mut last_unnamed = None;
33+
match COMPATS.iter().find_map(|compat| match compat {
34+
Compat::Local(_) => None,
35+
#[allow(deprecated)]
3336
Compat::Global(global) => {
3437
globals += 1;
35-
last_global = Some(global);
38+
last_unnamed = Some(global);
3639
None
3740
}
41+
Compat::NamedGlobal { spawn, name } => {
42+
if choose == Some(name) {
43+
Some(spawn)
44+
} else {
45+
globals += 1;
46+
last_named = Some(spawn);
47+
None
48+
}
49+
}
3850
}) {
39-
Some(spawn) => Some(spawn),
51+
Some(spawn) => Ok(*spawn),
4052
None => {
4153
#[cfg(feature = "panic-multiple-global-spawners")]
4254
if globals > 1 {
43-
panic!("multiple global spawners")
55+
return Err(Failure::MultipleGlobals);
4456
}
45-
last_global.copied()
57+
last_named
58+
.or(last_unnamed)
59+
.ok_or(Failure::NotFound)
60+
.copied()
4661
}
4762
}
4863
}
64+
65+
fn find_global() -> Result<fn(Task), Failure> {
66+
static FOUND: OnceLock<Result<fn(Task), Failure>> = OnceLock::new();
67+
if let Some(found) = FOUND.get() {
68+
return *found;
69+
}
70+
let choose = std::env::var("SPAWNS_GLOBAL_SPAWNER").ok();
71+
let result = pick_global(choose.as_deref());
72+
*FOUND.get_or_init(|| result)
73+
}
74+
75+
fn find_local() -> Option<fn(Task)> {
76+
COMPATS.iter().find_map(|compat| match compat {
77+
Compat::Local(local) => local(),
78+
#[allow(deprecated)]
79+
Compat::Global(_) => None,
80+
Compat::NamedGlobal { .. } => None,
81+
})
82+
}
83+
84+
pub(crate) fn find_spawn() -> Option<fn(Task)> {
85+
match COMPATS.len() {
86+
0 => return None,
87+
1 => match COMPATS[0] {
88+
Compat::NamedGlobal { spawn, .. } => return Some(spawn),
89+
#[allow(deprecated)]
90+
Compat::Global(spawn) => return Some(spawn),
91+
Compat::Local(local) => return local(),
92+
},
93+
_ => {}
94+
}
95+
match find_local()
96+
.ok_or(Failure::NotFound)
97+
.or_else(|_| find_global())
98+
{
99+
Ok(spawn) => Some(spawn),
100+
Err(Failure::NotFound) => None,
101+
Err(Failure::MultipleGlobals) => panic!("multiple global spawners"),
102+
}
103+
}

spawns-core/src/spawn.rs

+22-2
Original file line numberDiff line numberDiff line change
@@ -195,11 +195,15 @@ mod tests {
195195

196196
#[cfg(feature = "test-compat-global1")]
197197
#[distributed_slice(COMPATS)]
198+
#[allow(deprecated)]
198199
pub static THREAD_GLOBAL: Compat = Compat::Global(thread_global);
199200

200201
#[cfg(feature = "test-compat-global2")]
201202
#[distributed_slice(COMPATS)]
202-
pub static DROP_GLOBAL: Compat = Compat::Global(drop_global);
203+
pub static DROP_GLOBAL: Compat = Compat::NamedGlobal {
204+
name: "drop",
205+
spawn: drop_global,
206+
};
203207

204208
#[cfg(feature = "test-compat-global2")]
205209
fn drop_global(task: Task) {
@@ -255,6 +259,7 @@ mod tests {
255259
}
256260

257261
#[cfg(feature = "test-compat-global2")]
262+
#[cfg(not(feature = "test-named-global"))]
258263
#[cfg(feature = "panic-multiple-global-spawners")]
259264
#[test]
260265
#[should_panic(expected = "multiple global spawners")]
@@ -263,10 +268,25 @@ mod tests {
263268
}
264269

265270
#[cfg(feature = "test-compat-global2")]
271+
#[cfg(not(feature = "test-named-global"))]
266272
#[cfg(not(feature = "panic-multiple-global-spawners"))]
267273
#[test]
268274
fn multiple_globals() {
269-
block_on(spawn(ready(()))).unwrap();
275+
// The one chosen is indeterminate.
276+
spawn(ready(()));
277+
}
278+
279+
// Rust runs all tests in one process for given features, so it is crucial to keep features
280+
// set unique for this test as it setup environment variable SPAWNS_GLOBAL_SPAWNER.
281+
#[cfg(feature = "test-compat-global2")]
282+
#[cfg(feature = "test-named-global")]
283+
#[cfg(feature = "panic-multiple-global-spawners")]
284+
#[test]
285+
fn multiple_globals_choose_named() {
286+
std::env::set_var("SPAWNS_GLOBAL_SPAWNER", "drop");
287+
let handle = spawn(ready(()));
288+
let err = block_on(handle).unwrap_err();
289+
assert!(err.is_cancelled());
270290
}
271291
}
272292
}

spawns/src/lib.rs

+18-3
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,28 @@
3636
//! }
3737
//! ```
3838
//!
39-
//! To cooperate with existing async runtimes, it provides features to inject spawners for them.
39+
//! ## Compatibility with existing async runtimes
40+
//!
41+
//! This is an open world, there might be tens async runtimes. `spawns` provides features to inject
42+
//! spawners for few.
43+
//!
4044
//! * `tokio`: uses `tokio::runtime::Handle::try_current()` to detect thread local `tokio` runtime handle.
4145
//! * `smol`: uses `smol::spawn` to spawn task in absent of thread local spawners.
4246
//! * `async-global-executor`: uses `async_global_executor::spawn` to spawn task in absent of thread local spawners.
4347
//!
44-
//! Since `smol` and `async-global-executor` both blindly spawn tasks, it is unknown which one is
45-
//! chosen. Feature "panic-multiple-global-spawners" is provided to panic on this situation.
48+
//! For other async runtimes, one could inject [Compat]s to [static@COMPATS] themselves.
49+
//!
50+
//! Noted that, all those compatibility features, injections should only active on tests and
51+
//! binaries. Otherwise, they will be propagated to dependents with unnecessary dependencies.
52+
//!
53+
//! ## Dealing with multiple global executors
54+
//! Global executor cloud spawn task with no help from thread context. But this exposes us an
55+
//! dilemma to us, which one to use if there are multiple global executors present ? By default,
56+
//! `spawns` randomly chooses one and stick to it to spawn tasks in absent of thread context
57+
//! spawners. Generally, this should be safe as global executors should be designed to spawn
58+
//! everywhere. If this is not the case, one could use environment variable `SPAWNS_GLOBAL_SPAWNER`
59+
//! to specify one. As a safety net, feature `panic-multiple-global-spawners` is provided to panic
60+
//! if there are multiple global candidates.
4661
4762
pub use spawns_core::*;
4863

0 commit comments

Comments
 (0)