diff --git a/spawns-compat/Cargo.toml b/spawns-compat/Cargo.toml index 411251e..3aba76e 100644 --- a/spawns-compat/Cargo.toml +++ b/spawns-compat/Cargo.toml @@ -24,6 +24,7 @@ async-global-executor = { version = "2", optional = true } [dev-dependencies] async-std = "1.12.0" +futures-lite = "2.3.0" tokio = { version = "1.37.0", features = ["full"] } [package.metadata.docs.rs] diff --git a/spawns-compat/src/async_global_executor.rs b/spawns-compat/src/async_global_executor.rs index 3d89b47..0dd25fd 100644 --- a/spawns-compat/src/async_global_executor.rs +++ b/spawns-compat/src/async_global_executor.rs @@ -3,7 +3,10 @@ use spawns_core::{Compat, Task, COMPATS}; use std::boxed::Box; #[distributed_slice(COMPATS)] -pub static ASYNC_GLOBAL_EXECUTOR: Compat = Compat::Global(async_global); +pub static ASYNC_GLOBAL_EXECUTOR: Compat = Compat::NamedGlobal { + name: "async-global-executor", + spawn: async_global, +}; fn async_global(task: Task) { let Task { future, .. } = task; @@ -15,10 +18,11 @@ fn async_global(task: Task) { #[cfg(not(feature = "smol"))] mod tests { use spawns_core::*; + use futures_lite::future; #[test] fn spawn_one() { - async_std::task::block_on(async { + future::block_on(async { let handle = spawn(async { id() }); let id = handle.id(); assert_eq!(handle.await.unwrap(), id); @@ -27,7 +31,7 @@ mod tests { #[test] fn spawn_cascading() { - async_std::task::block_on(async { + future::block_on(async { let handle = spawn(async { spawn(async { id() }) }); let handle = handle.await.unwrap(); let id = handle.id(); @@ -37,7 +41,7 @@ mod tests { #[test] fn spawn_interleaving() { - async_std::task::block_on(async move { + future::block_on(async move { let handle = spawn(async { async_std::task::spawn(async { spawn(async { id() }) }) }); let handle = handle.await.unwrap().await; let id = handle.id(); @@ -47,7 +51,7 @@ mod tests { #[test] fn spawn_into_smol() { - async_std::task::block_on(async move { + future::block_on(async move { let handle = spawn(async { async_std::task::spawn(async { try_id() }) }); let handle = handle.await.unwrap(); assert_eq!(handle.await, None); diff --git a/spawns-compat/src/smol.rs b/spawns-compat/src/smol.rs index 6077a18..fc002ce 100644 --- a/spawns-compat/src/smol.rs +++ b/spawns-compat/src/smol.rs @@ -3,7 +3,10 @@ use spawns_core::{Compat, Task, COMPATS}; use std::boxed::Box; #[distributed_slice(COMPATS)] -pub static SMOL: Compat = Compat::Global(smol_global); +pub static SMOL: Compat = Compat::NamedGlobal { + name: "smol", + spawn: smol_global, +}; fn smol_global(task: Task) { let Task { future, .. } = task; @@ -15,10 +18,11 @@ fn smol_global(task: Task) { #[cfg(not(feature = "async-global-executor"))] mod tests { use spawns_core::*; + use futures_lite::future; #[test] fn spawn_one() { - smol::block_on(async { + future::block_on(async { let handle = spawn(async { id() }); let id = handle.id(); assert_eq!(handle.await.unwrap(), id); @@ -27,7 +31,7 @@ mod tests { #[test] fn spawn_cascading() { - smol::block_on(async { + future::block_on(async { let handle = spawn(async { spawn(async { id() }) }); let handle = handle.await.unwrap(); let id = handle.id(); @@ -37,7 +41,7 @@ mod tests { #[test] fn spawn_interleaving() { - smol::block_on(async move { + future::block_on(async move { let handle = spawn(async { smol::spawn(async { spawn(async { id() }) }) }); let handle = handle.await.unwrap().await; let id = handle.id(); @@ -47,7 +51,7 @@ mod tests { #[test] fn spawn_into_smol() { - smol::block_on(async move { + future::block_on(async move { let handle = spawn(async { smol::spawn(async { try_id() }) }); let handle = handle.await.unwrap(); assert_eq!(handle.await, None); diff --git a/spawns-core/Cargo.toml b/spawns-core/Cargo.toml index 2a0ea75..083a4a7 100644 --- a/spawns-core/Cargo.toml +++ b/spawns-core/Cargo.toml @@ -15,6 +15,7 @@ compat = ["linkme"] panic-multiple-global-spawners = [] test-compat-global1 = ["compat"] test-compat-global2 = ["compat", "test-compat-global1"] +test-named-global = [] [dependencies] linkme = { version = "0.3.25", optional = true } diff --git a/spawns-core/src/compat.rs b/spawns-core/src/compat.rs index 758a94d..c8a6105 100644 --- a/spawns-core/src/compat.rs +++ b/spawns-core/src/compat.rs @@ -1,11 +1,14 @@ use crate::Task; use linkme::distributed_slice; +use std::sync::OnceLock; /// Compat encapsulate functions to find async runtimes to spawn task. pub enum Compat { + /// Named global function to spawn task. + NamedGlobal { name: &'static str, spawn: fn(Task) }, /// Global function to spawn task. - /// - /// [spawn](`crate::spawn()`) will panic if there is no local spawners but multiple global spawners. + #[doc(hidden)] + #[deprecated(since = "1.0.3", note = "use NamedGlobal instead")] Global(fn(Task)), #[allow(clippy::type_complexity)] /// Local function to detect async runtimes. @@ -16,33 +19,85 @@ pub enum Compat { #[distributed_slice] pub static COMPATS: [Compat] = [..]; -pub(crate) fn find_spawn() -> Option { - match COMPATS.len() { - 0 => return None, - 1 => match COMPATS[0] { - Compat::Global(inject) => return Some(inject), - Compat::Local(detect) => return detect(), - }, - _ => {} - } +#[derive(Clone, Copy)] +pub(crate) enum Failure { + NotFound, + #[allow(dead_code)] + MultipleGlobals, +} - let mut last_global = None; +fn pick_global(choose: Option<&str>) -> Result { let mut globals = 0; - match COMPATS.iter().find_map(|injection| match injection { - Compat::Local(local) => local(), + let mut last_named = None; + let mut last_unnamed = None; + match COMPATS.iter().find_map(|compat| match compat { + Compat::Local(_) => None, + #[allow(deprecated)] Compat::Global(global) => { globals += 1; - last_global = Some(global); + last_unnamed = Some(global); None } + Compat::NamedGlobal { spawn, name } => { + if choose == Some(name) { + Some(spawn) + } else { + globals += 1; + last_named = Some(spawn); + None + } + } }) { - Some(spawn) => Some(spawn), + Some(spawn) => Ok(*spawn), None => { #[cfg(feature = "panic-multiple-global-spawners")] if globals > 1 { - panic!("multiple global spawners") + return Err(Failure::MultipleGlobals); } - last_global.copied() + last_named + .or(last_unnamed) + .ok_or(Failure::NotFound) + .copied() } } } + +fn find_global() -> Result { + static FOUND: OnceLock> = OnceLock::new(); + if let Some(found) = FOUND.get() { + return *found; + } + let choose = std::env::var("SPAWNS_GLOBAL_SPAWNER").ok(); + let result = pick_global(choose.as_deref()); + *FOUND.get_or_init(|| result) +} + +fn find_local() -> Option { + COMPATS.iter().find_map(|compat| match compat { + Compat::Local(local) => local(), + #[allow(deprecated)] + Compat::Global(_) => None, + Compat::NamedGlobal { .. } => None, + }) +} + +pub(crate) fn find_spawn() -> Option { + match COMPATS.len() { + 0 => return None, + 1 => match COMPATS[0] { + Compat::NamedGlobal { spawn, .. } => return Some(spawn), + #[allow(deprecated)] + Compat::Global(spawn) => return Some(spawn), + Compat::Local(local) => return local(), + }, + _ => {} + } + match find_local() + .ok_or(Failure::NotFound) + .or_else(|_| find_global()) + { + Ok(spawn) => Some(spawn), + Err(Failure::NotFound) => None, + Err(Failure::MultipleGlobals) => panic!("multiple global spawners"), + } +} diff --git a/spawns-core/src/spawn.rs b/spawns-core/src/spawn.rs index db474e8..94883c1 100644 --- a/spawns-core/src/spawn.rs +++ b/spawns-core/src/spawn.rs @@ -195,11 +195,15 @@ mod tests { #[cfg(feature = "test-compat-global1")] #[distributed_slice(COMPATS)] + #[allow(deprecated)] pub static THREAD_GLOBAL: Compat = Compat::Global(thread_global); #[cfg(feature = "test-compat-global2")] #[distributed_slice(COMPATS)] - pub static DROP_GLOBAL: Compat = Compat::Global(drop_global); + pub static DROP_GLOBAL: Compat = Compat::NamedGlobal { + name: "drop", + spawn: drop_global, + }; #[cfg(feature = "test-compat-global2")] fn drop_global(task: Task) { @@ -255,6 +259,7 @@ mod tests { } #[cfg(feature = "test-compat-global2")] + #[cfg(not(feature = "test-named-global"))] #[cfg(feature = "panic-multiple-global-spawners")] #[test] #[should_panic(expected = "multiple global spawners")] @@ -263,10 +268,25 @@ mod tests { } #[cfg(feature = "test-compat-global2")] + #[cfg(not(feature = "test-named-global"))] #[cfg(not(feature = "panic-multiple-global-spawners"))] #[test] fn multiple_globals() { - block_on(spawn(ready(()))).unwrap(); + // The one chosen is indeterminate. + spawn(ready(())); + } + + // Rust runs all tests in one process for given features, so it is crucial to keep features + // set unique for this test as it setup environment variable SPAWNS_GLOBAL_SPAWNER. + #[cfg(feature = "test-compat-global2")] + #[cfg(feature = "test-named-global")] + #[cfg(feature = "panic-multiple-global-spawners")] + #[test] + fn multiple_globals_choose_named() { + std::env::set_var("SPAWNS_GLOBAL_SPAWNER", "drop"); + let handle = spawn(ready(())); + let err = block_on(handle).unwrap_err(); + assert!(err.is_cancelled()); } } } diff --git a/spawns/src/lib.rs b/spawns/src/lib.rs index 8a0e4e0..e866b71 100644 --- a/spawns/src/lib.rs +++ b/spawns/src/lib.rs @@ -36,13 +36,28 @@ //! } //! ``` //! -//! To cooperate with existing async runtimes, it provides features to inject spawners for them. +//! ## Compatibility with existing async runtimes +//! +//! This is an open world, there might be tens async runtimes. `spawns` provides features to inject +//! spawners for few. +//! //! * `tokio`: uses `tokio::runtime::Handle::try_current()` to detect thread local `tokio` runtime handle. //! * `smol`: uses `smol::spawn` to spawn task in absent of thread local spawners. //! * `async-global-executor`: uses `async_global_executor::spawn` to spawn task in absent of thread local spawners. //! -//! Since `smol` and `async-global-executor` both blindly spawn tasks, it is unknown which one is -//! chosen. Feature "panic-multiple-global-spawners" is provided to panic on this situation. +//! For other async runtimes, one could inject [Compat]s to [static@COMPATS] themselves. +//! +//! Noted that, all those compatibility features, injections should only active on tests and +//! binaries. Otherwise, they will be propagated to dependents with unnecessary dependencies. +//! +//! ## Dealing with multiple global executors +//! Global executor cloud spawn task with no help from thread context. But this exposes us an +//! dilemma to us, which one to use if there are multiple global executors present ? By default, +//! `spawns` randomly chooses one and stick to it to spawn tasks in absent of thread context +//! spawners. Generally, this should be safe as global executors should be designed to spawn +//! everywhere. If this is not the case, one could use environment variable `SPAWNS_GLOBAL_SPAWNER` +//! to specify one. As a safety net, feature `panic-multiple-global-spawners` is provided to panic +//! if there are multiple global candidates. pub use spawns_core::*;