From 2a42823194df95e1074e1d4f09dfbb73e9e83ee2 Mon Sep 17 00:00:00 2001 From: jlizen Date: Thu, 26 Dec 2024 23:48:41 +0000 Subject: [PATCH 01/10] move to sync fn input in executor, ditch everything but spawn_blocking/custom/current_context, tweak defaults to enable concurrency limiting + use spawn_blocking for tokio feature, add more docs --- Cargo.toml | 18 +- README.md | 87 +++ src/block_in_place.rs | 45 -- src/concurrency_limit.rs | 18 +- src/custom_executor.rs | 55 -- src/error.rs | 17 +- src/{ => executor}/current_context.rs | 17 +- src/executor/custom_executor.rs | 62 ++ src/executor/mod.rs | 295 ++++++++ src/executor/spawn_blocking.rs | 37 + src/lib.rs | 712 +++++------------- src/secondary_tokio_runtime.rs | 221 ------ src/spawn_blocking.rs | 38 - tests/block_in_place_strategy.rs | 58 -- tests/block_in_place_wrong_runtime.rs | 9 - tests/current_context_default.rs | 17 +- tests/current_context_strategy.rs | 62 +- tests/custom_executor_simple.rs | 27 +- tests/custom_executor_strategy.rs | 68 +- tests/multiple_initialize_err.rs | 6 +- ...lize_err_with_secondary_runtime_builder.rs | 16 - .../secondary_tokio_builder_allowed_config.rs | 19 - ...condary_tokio_builder_disallowed_config.rs | 14 - tests/secondary_tokio_strategy.rs | 81 -- tests/spawn_blocking_default.rs | 15 +- tests/spawn_blocking_strategy.rs | 60 +- 26 files changed, 809 insertions(+), 1265 deletions(-) delete mode 100644 src/block_in_place.rs delete mode 100644 src/custom_executor.rs rename src/{ => executor}/current_context.rs (51%) create mode 100644 src/executor/custom_executor.rs create mode 100644 src/executor/mod.rs create mode 100644 src/executor/spawn_blocking.rs delete mode 100644 src/secondary_tokio_runtime.rs delete mode 100644 src/spawn_blocking.rs delete mode 100644 tests/block_in_place_strategy.rs delete mode 100644 tests/block_in_place_wrong_runtime.rs delete mode 100644 tests/multiple_initialize_err_with_secondary_runtime_builder.rs delete mode 100644 tests/secondary_tokio_builder_allowed_config.rs delete mode 100644 tests/secondary_tokio_builder_disallowed_config.rs delete mode 100644 tests/secondary_tokio_strategy.rs diff --git a/Cargo.toml b/Cargo.toml index ff2faf3..4390fcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,28 +6,28 @@ license = "MIT" repository = "https://github.com/jlizen/compute-heavy-future-executor" homepage = "https://github.com/jlizen/compute-heavy-future-executor" rust-version = "1.70" -exclude = ["/.github", "/examples", "/scripts"] +exclude = ["/.github", "/Exampless", "/scripts"] readme = "README.md" -description = "Additional executor patterns for handling compute-bounded, blocking futures." +description = "Executor patterns for handling compute-bounded calls inside async contexts." categories = ["asynchronous"] [features] -tokio = ["tokio/rt"] -tokio_block_in_place = ["tokio", "tokio/rt-multi-thread"] -secondary_tokio_runtime = ["tokio", "tokio/rt-multi-thread", "dep:libc", "dep:num_cpus"] +default = ["tokio"] +tokio = ["tokio/rt",] [dependencies] -libc = { version = "0.2.168", optional = true } log = "0.4.22" -num_cpus = { version = "1.0", optional = true } -tokio = { version = "1.0", features = ["macros", "sync"] } +num_cpus = "1.0" +tokio = { version = "1.0", features = ["sync"] } [dev-dependencies] -tokio = { version = "1.0", features = ["full"]} +tokio = { version = "1", features = ["full"]} futures-util = "0.3.31" +rayon = "1" [package.metadata.docs.rs] all-features = true +rustdoc-args = ["--cfg", "docsrs"] [lints.rust] diff --git a/README.md b/README.md index 484c00a..da63af4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,89 @@ # compute-heavy-future-executor Experimental crate that adds additional executor patterns to use with frequently blocking futures. + +Today, when library authors are write async APIs, they don't have a good way to handle long-running sync segments. + +An application author can use selective handling such as `tokio::task::spawn_blocking()` along with concurrency control to delegate sync segments to blocking threads. Or, they might send the work to a `rayon` threadpool. + +But, library authors generally don't have this flexibility. As, they generally want to be agnostic across runtime. Or, even if they are `tokio`-specific, they generally don't want to call `tokio::task::spawn_blocking()` as it is +suboptimal without extra configuration (concurrency control) as well as highly opinionated to send the work across threads. + +This library aims to solve this problem by providing libray authors a static, globally scoped strategy that they can delegate blocking sync work to without drawing any conclusions about handling. + +And then, the applications using the library can either rely on the default strategy that this package provides, or tune them with their preferred approach. + +## Usage - Library Authors +For library authors, it's as simple as adding a dependency enabling `compute-heavy-future-executor` (perhaps behind a feature flag). + +The below will default to 'current context' execution (ie non-op) unless the caller enables the tokio feature. +``` +[dependencies] +compute-heavy-future-executor = { version = "0.1", default-features = false } +``` + +Meanwhile to be slightly more opinionated, the below will enable usage of `spawn_blocking` with concurrency control +by default unless the caller opts out: +``` +[dependencies] +compute-heavy-future-executor = { version = "0.1" } +``` + +And then wrap any sync work by passing it as a closure to a global `execute_sync()` call: + +``` +use compute_heavy_future_executor::execute_sync; + +fn sync_work(input: String)-> u8 { + std::thread::sleep(std::time::Duration::from_secs(5)); + println!("{input}"); + 5 +} +pub async fn a_future_that_has_blocking_sync_work() -> u8 { + // relies on caller-specified strategy for translating execute_sync into a future that won't + // block the current worker thread + execute_sync(move || { sync_work("foo".to_string()) }).await.unwrap() +} + +``` + +## Usage - Application owners +Application authors can benefit from this crate with no application code changes, if you are using +a library that is itself using this crate. + +If you want to customize the strategy beyond defaults, they can add +`compute-heavy-future-executor` to their dependencies: + +``` +[dependencies] +// enables tokio and therefore spawn_blocking strategy by default +compute-heavy-future-executor = { version = "0.1" } +// used for example with custom executor +rayon = "1" +``` + +And then configure your global strategy as desired. For instance, see below for usage of rayon +instead of `spawn_blocking()`. + +``` +use std::sync::OnceLock; +use rayon::ThreadPool; + +use compute_heavy_future_executor::{ + global_sync_strategy_builder, CustomExecutorSyncClosure, +}; + +static THREADPOOL: OnceLock = OnceLock::new(); + +fn initialize_strategy() { + THREADPOOL.set(|| rayon::ThreadPoolBuilder::default().build().unwrap()); + + let custom_closure: CustomExecutorSyncClosure = + Box::new(|f| Box::new(async move { Ok(THREADPOOL.get().unwrap().spawn(f)) })); + + global_sync_strategy_builder() + // probably no need for max concurrency as rayon already is defaulting to a thread per core + // and using a task queue + .initialize_custom_executor(custom_closure).unwrap(); +} + +``` \ No newline at end of file diff --git a/src/block_in_place.rs b/src/block_in_place.rs deleted file mode 100644 index fd62710..0000000 --- a/src/block_in_place.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::{ - concurrency_limit::ConcurrencyLimit, - error::{Error, InvalidConfig}, - ComputeHeavyFutureExecutor, -}; - -use tokio::runtime::{Handle, RuntimeFlavor}; - -pub(crate) struct BlockInPlaceExecutor { - concurrency_limit: ConcurrencyLimit, -} - -impl BlockInPlaceExecutor { - pub(crate) fn new(max_concurrency: Option) -> Result { - match Handle::current().runtime_flavor() { - RuntimeFlavor::MultiThread => Ok(()), - #[cfg(tokio_unstable)] - RuntimeFlavor::MultiThreadAlt => Ok(()), - flavor => Err(Error::InvalidConfig(InvalidConfig { - field: "current tokio runtime flavor", - received: format!("{flavor:#?}"), - expected: "MultiThread", - }))?, - }?; - - Ok(Self { - concurrency_limit: ConcurrencyLimit::new(max_concurrency), - }) - } -} - -impl ComputeHeavyFutureExecutor for BlockInPlaceExecutor { - async fn execute(&self, fut: F) -> Result - where - F: std::future::Future + Send + 'static, - O: Send + 'static, - { - let _permit = self.concurrency_limit.acquire_permit().await; - - Ok(tokio::task::block_in_place(move || { - tokio::runtime::Handle::current().block_on(async { fut.await }) - })) - // permit implicitly drops - } -} diff --git a/src/concurrency_limit.rs b/src/concurrency_limit.rs index 1b9281e..971acb2 100644 --- a/src/concurrency_limit.rs +++ b/src/concurrency_limit.rs @@ -23,19 +23,13 @@ impl ConcurrencyLimit { /// Internally turns errors into a no-op (`None`) and outputs log lines. pub(crate) async fn acquire_permit(&self) -> Option { match self.semaphore.clone() { - Some(semaphore) => { - match semaphore - .acquire_owned() - .await - .map_err(|err| Error::Semaphore(err)) - { - Ok(permit) => Some(permit), - Err(err) => { - log::error!("failed to acquire permit: {err}"); - None - } + Some(semaphore) => match semaphore.acquire_owned().await.map_err(Error::Semaphore) { + Ok(permit) => Some(permit), + Err(err) => { + log::error!("failed to acquire permit: {err}"); + None } - } + }, None => None, } } diff --git a/src/custom_executor.rs b/src/custom_executor.rs deleted file mode 100644 index 300c432..0000000 --- a/src/custom_executor.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::{future::Future, pin::Pin}; - -use crate::{ - concurrency_limit::ConcurrencyLimit, error::Error, make_future_cancellable, - ComputeHeavyFutureExecutor, -}; - -/// A closure that accepts an arbitrary future and polls it to completion -/// via its preferred strategy. -pub type CustomExecutorClosure = Box< - dyn Fn( - Pin + Send + 'static>>, - ) -> Box< - dyn Future>> - + Send - + 'static, - > + Send - + Sync, ->; - -pub(crate) struct CustomExecutor { - closure: CustomExecutorClosure, - concurrency_limit: ConcurrencyLimit, -} - -impl CustomExecutor { - pub(crate) fn new(closure: CustomExecutorClosure, max_concurrency: Option) -> Self { - Self { - closure, - concurrency_limit: ConcurrencyLimit::new(max_concurrency), - } - } -} - -impl ComputeHeavyFutureExecutor for CustomExecutor { - async fn execute(&self, fut: F) -> Result - where - F: Future + Send + 'static, - O: Send + 'static, - { - let _permit = self.concurrency_limit.acquire_permit().await; - - let (wrapped_future, rx) = make_future_cancellable(fut); - - // if our custom executor future resolves to an error, we know it will never send - // the response so we immediately return - if let Err(err) = Box::into_pin((self.closure)(Box::pin(wrapped_future))).await { - return Err(Error::BoxError(err)); - } - - rx.await.map_err(|err| Error::RecvError(err)) - - // permit implicitly drops - } -} diff --git a/src/error.rs b/src/error.rs index af43323..f87db21 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,33 +2,30 @@ use core::fmt; use crate::ExecutorStrategy; +/// An error from the custom executor #[non_exhaustive] #[derive(Debug)] pub enum Error { + /// Executor has already had a global strategy configured. AlreadyInitialized(ExecutorStrategy), - InvalidConfig(InvalidConfig), + /// Issue listening on the custom executor response channel. RecvError(tokio::sync::oneshot::error::RecvError), + /// Error enforcing concurrency Semaphore(tokio::sync::AcquireError), + /// Dynamic error from the custom executor closure BoxError(Box), #[cfg(feature = "tokio")] + /// Background spawn blocking task panicked JoinError(tokio::task::JoinError), } -#[derive(Debug)] -pub struct InvalidConfig { - pub field: &'static str, - pub received: String, - pub expected: &'static str, -} - impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Error::AlreadyInitialized(strategy) => write!( f, - "global strategy is already initialzed with strategy: {strategy:#?}" + "global strategy is already initialized with strategy: {strategy:#?}" ), - Error::InvalidConfig(err) => write!(f, "invalid config: {err:#?}"), Error::BoxError(err) => write!(f, "custom executor error: {err}"), Error::RecvError(err) => write!(f, "error in custom executor response channel: {err}"), Error::Semaphore(err) => write!( diff --git a/src/current_context.rs b/src/executor/current_context.rs similarity index 51% rename from src/current_context.rs rename to src/executor/current_context.rs index 6badb65..d0f1faf 100644 --- a/src/current_context.rs +++ b/src/executor/current_context.rs @@ -1,4 +1,6 @@ -use crate::{concurrency_limit::ConcurrencyLimit, error::Error, ComputeHeavyFutureExecutor}; +use crate::{concurrency_limit::ConcurrencyLimit, error::Error}; + +use super::ExecuteSync; pub(crate) struct CurrentContextExecutor { concurrency_limit: ConcurrencyLimit, @@ -12,16 +14,15 @@ impl CurrentContextExecutor { } } -impl ComputeHeavyFutureExecutor for CurrentContextExecutor { - async fn execute(&self, fut: F) -> Result +impl ExecuteSync for CurrentContextExecutor { + async fn execute_sync(&self, f: F) -> Result where - F: std::future::Future + Send + 'static, - O: Send + 'static, + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, { let _permit = self.concurrency_limit.acquire_permit().await; - Ok(fut.await) - - // implicit permit drop + Ok(f()) + // permit implicitly drops } } diff --git a/src/executor/custom_executor.rs b/src/executor/custom_executor.rs new file mode 100644 index 0000000..f1d7bc2 --- /dev/null +++ b/src/executor/custom_executor.rs @@ -0,0 +1,62 @@ +use std::future::Future; + +use crate::{concurrency_limit::ConcurrencyLimit, error::Error}; + +use super::ExecuteSync; + +/// A closure that accepts an arbitrary sync function and returns a future that executes it. +/// The Custom Executor will implicitly wrap the input function in a oneshot +/// channel to erase its input/output type. +pub type CustomExecutorSyncClosure = Box< + dyn Fn( + Box, + ) -> Box< + dyn Future>> + + Send + + 'static, + > + Send + + Sync, +>; + +pub(crate) struct CustomExecutor { + closure: CustomExecutorSyncClosure, + concurrency_limit: ConcurrencyLimit, +} + +impl CustomExecutor { + pub(crate) fn new(closure: CustomExecutorSyncClosure, max_concurrency: Option) -> Self { + Self { + closure, + concurrency_limit: ConcurrencyLimit::new(max_concurrency), + } + } +} + +impl ExecuteSync for CustomExecutor { + // the compiler correctly is pointing out that the custom closure isn't guaranteed to call f. + // but, we leave that to the implementer to guarantee since we are limited by working with static signatures + #[allow(unused_variables)] + async fn execute_sync(&self, f: F) -> Result + where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, + { + let _permit = self.concurrency_limit.acquire_permit().await; + + let (tx, rx) = tokio::sync::oneshot::channel(); + + let wrapped_input_closure = Box::new(|| { + let res = f(); + if tx.send(res).is_err() { + log::trace!("custom sync executor foreground dropped before it could receive the result of the sync closure"); + } + }); + + Box::into_pin((self.closure)(wrapped_input_closure)) + .await + .map_err(Error::BoxError)?; + + rx.await.map_err(Error::RecvError) + // permit implicitly drops + } +} diff --git a/src/executor/mod.rs b/src/executor/mod.rs new file mode 100644 index 0000000..8ff5940 --- /dev/null +++ b/src/executor/mod.rs @@ -0,0 +1,295 @@ +pub(crate) mod current_context; +pub(crate) mod custom_executor; +#[cfg(feature = "tokio")] +pub(crate) mod spawn_blocking; + +use std::sync::OnceLock; + +use current_context::CurrentContextExecutor; +use custom_executor::{CustomExecutor, CustomExecutorSyncClosure}; + +use crate::{Error, ExecutorStrategy, GlobalStrategy}; + +pub(crate) trait ExecuteSync { + /// Accepts a sync function and processes it to completion. + async fn execute_sync(&self, f: F) -> Result + where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static; +} + +fn set_sync_strategy(strategy: SyncExecutor) -> Result<(), Error> { + COMPUTE_HEAVY_SYNC_EXECUTOR_STRATEGY + .set(strategy) + .map_err(|_| { + Error::AlreadyInitialized(COMPUTE_HEAVY_SYNC_EXECUTOR_STRATEGY.get().unwrap().into()) + })?; + + log::info!( + "initialized compute-heavy future executor strategy - {:#?}", + global_sync_strategy() + ); + + Ok(()) +} + +/// Get the currently initialized sync strategy, +/// or the default strategy for the current feature in case no strategy has been loaded. +/// +/// See [`SyncExecutorBuilder`] for details on strategies. +/// +/// # Examples +/// +/// ``` +/// use compute_heavy_future_executor::{ +/// global_sync_strategy, +/// global_sync_strategy_builder, +/// GlobalStrategy, +/// ExecutorStrategy +/// }; +/// +/// # fn run() { +/// +/// #[cfg(feature = "tokio")] +/// assert_eq!(global_sync_strategy(), GlobalStrategy::Default(ExecutorStrategy::SpawnBlocking)); +/// +/// #[cfg(not(feature = "tokio"))] +/// assert_eq!(global_sync_strategy(), GlobalStrategy::Default(ExecutorStrategy::CurrentContext)); +/// +/// global_sync_strategy_builder() +/// .initialize_current_context() +/// .unwrap(); +/// +/// assert_eq!(global_sync_strategy(), GlobalStrategy::Initialized(ExecutorStrategy::CurrentContext)); +/// +/// # } +/// ``` +pub fn global_sync_strategy() -> GlobalStrategy { + match COMPUTE_HEAVY_SYNC_EXECUTOR_STRATEGY.get() { + Some(strategy) => GlobalStrategy::Initialized(strategy.into()), + None => GlobalStrategy::Default(<&SyncExecutor>::default().into()), + } +} + +pub(crate) fn get_global_sync_executor() -> &'static SyncExecutor { + COMPUTE_HEAVY_SYNC_EXECUTOR_STRATEGY + .get() + .unwrap_or_else(|| <&SyncExecutor>::default()) +} + +/// The stored strategy used to spawn compute-heavy futures. +static COMPUTE_HEAVY_SYNC_EXECUTOR_STRATEGY: OnceLock = OnceLock::new(); + +/// The fallback strategy used in case no strategy is explicitly set +static DEFAULT_COMPUTE_HEAVY_SYNC_EXECUTOR_STRATEGY: OnceLock = OnceLock::new(); + +#[non_exhaustive] +pub(crate) enum SyncExecutor { + /// A non-op strategy that runs the function in the current context + CurrentContext(current_context::CurrentContextExecutor), + /// User-provided closure + CustomExecutor(custom_executor::CustomExecutor), + /// tokio task::spawn_blocking + #[cfg(feature = "tokio")] + SpawnBlocking(spawn_blocking::SpawnBlockingExecutor), +} + +impl Default for &SyncExecutor { + fn default() -> Self { + DEFAULT_COMPUTE_HEAVY_SYNC_EXECUTOR_STRATEGY.get_or_init(|| { + let core_count = num_cpus::get(); + + #[cfg(feature = "tokio")] + { + log::info!("Defaulting to SpawnBlocking strategy for compute-heavy future executor \ + with max concurrency of {core_count} until a strategy is initialized"); + + SyncExecutor::SpawnBlocking(spawn_blocking::SpawnBlockingExecutor::new(Some(core_count))) + } + + #[cfg(not(feature = "tokio"))] + { + log::warn!("Defaulting to CurrentContext (non-op) strategy for compute-heavy future executor \ + with max concurrency of {core_count} until a strategy is initialized."); + SyncExecutor::CurrentContext(CurrentContextExecutor::new(Some(core_count))) + } + }) + } +} + +impl ExecuteSync for SyncExecutor { + async fn execute_sync(&self, f: F) -> Result + where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, + { + match self { + SyncExecutor::CurrentContext(executor) => executor.execute_sync(f).await, + SyncExecutor::CustomExecutor(executor) => executor.execute_sync(f).await, + #[cfg(feature = "tokio")] + SyncExecutor::SpawnBlocking(executor) => executor.execute_sync(f).await, + } + } +} + +impl From<&SyncExecutor> for ExecutorStrategy { + fn from(value: &SyncExecutor) -> Self { + match value { + SyncExecutor::CurrentContext(_) => Self::CurrentContext, + SyncExecutor::CustomExecutor(_) => Self::CustomExecutor, + #[cfg(feature = "tokio")] + SyncExecutor::SpawnBlocking(_) => Self::SpawnBlocking, + } + } +} + +/// A builder to replace the default sync executor strategy +/// with a caller-provided strategy. +/// +/// # Examples +/// +/// ``` +/// use compute_heavy_future_executor::global_sync_strategy_builder; +/// +/// # fn run() { +/// global_sync_strategy_builder() +/// .max_concurrency(10) +/// .initialize_current_context() +/// .unwrap(); +/// # } +/// ``` +#[must_use = "doesn't do anything unless used"] +#[derive(Default, Debug)] +pub struct SyncExecutorBuilder { + max_concurrency: Option, +} + +impl SyncExecutorBuilder { + /// Set the max number of simultaneous futures processed by this executor. + /// + /// If this number is exceeded, the executor will wait to execute the + /// input closure until a permit can be acquired. + /// + /// ## Default + /// No maximum concurrency when strategies are manually built. + /// + /// For default strategies, the default concurrency limit will be the number of cpu cores. + /// + /// # Examples + /// + /// ``` + /// use compute_heavy_future_executor::global_sync_strategy_builder; + /// + /// # fn run() { + /// global_sync_strategy_builder() + /// .max_concurrency(10) + /// .initialize_current_context() + /// .unwrap(); + /// # } + pub fn max_concurrency(self, max_task_concurrency: usize) -> Self { + Self { + max_concurrency: Some(max_task_concurrency), + } + } + + /// Initializes a new (non-op) global strategy to wait in the current context. + /// + /// This is effectively a non-op wrapper that adds no special handling for the sync future + /// besides optional concurrency control. + /// + /// This is the default if the `tokio` feature is disabled (with concurrency equal to cpu core count). + /// + /// # Error + /// Returns an error if the global strategy is already initialized. + /// It can only be initialized once. + /// + /// # Examples + /// + /// ``` + /// use compute_heavy_future_executor::global_sync_strategy_builder; + /// + /// # async fn run() { + /// global_sync_strategy_builder().initialize_current_context().unwrap(); + /// # } + /// ``` + pub fn initialize_current_context(self) -> Result<(), Error> { + let strategy = + SyncExecutor::CurrentContext(CurrentContextExecutor::new(self.max_concurrency)); + set_sync_strategy(strategy) + } + + /// Initializes a new global strategy to execute input closures by blocking on them inside the + /// tokio blocking threadpool via Tokio's [`spawn_blocking`]. + /// + /// Requires `tokio` feature. + /// + /// This is the default strategy if `tokio` feature is enabled, with a concurrency limit equal to the number of cpu cores. + /// + /// # Error + /// Returns an error if the global strategy is already initialized. + /// It can only be initialized once. + /// + /// # Examples + /// + /// ``` + /// use compute_heavy_future_executor::global_sync_strategy_builder; + /// + /// # async fn run() { + /// // this will include no concurrency limit when explicitly initialized + /// // without a call to [`concurrency_limit()`] + /// global_sync_strategy_builder().initialize_spawn_blocking().unwrap(); + /// # } + /// ``` + /// [`spawn_blocking`]: tokio::task::spawn_blocking + /// + #[cfg(feature = "tokio")] + pub fn initialize_spawn_blocking(self) -> Result<(), Error> { + use spawn_blocking::SpawnBlockingExecutor; + + let strategy = + SyncExecutor::SpawnBlocking(SpawnBlockingExecutor::new(self.max_concurrency)); + set_sync_strategy(strategy) + } + + /// Accepts a closure that will accept an arbitrary closure and call it. The input + /// function will be implicitly wrapped in a oneshot channel to avoid input/output types. + /// + /// Intended for injecting arbitrary runtimes/strategies or customizing existing ones. + /// + /// For instance, you could delegate to a [`Rayon threadpool`] or use Tokio's [`block_in_place`]. + /// See `tests/custom_executor_strategy.rs` for a `Rayon` example. + /// + /// # Error + /// Returns an error if the global strategy is already initialized. + /// It can only be initialized once. + /// + /// # Examples + /// + /// ``` + /// use compute_heavy_future_executor::global_sync_strategy_builder; + /// use compute_heavy_future_executor::CustomExecutorSyncClosure; + /// + /// # async fn run() { + /// // caution: this will panic if used outside of tokio multithreaded runtime + /// // this is a kind of dangerous strategy, read up on `block in place's` limitations + /// // before using this approach + /// let closure: CustomExecutorSyncClosure = Box::new(|f| { + /// Box::new(async move { Ok(tokio::task::block_in_place(move || f())) }) + /// }); + /// + /// global_sync_strategy_builder().initialize_custom_executor(closure).unwrap(); + /// # } + /// + /// ``` + /// + /// [`Rayon threadpool`]: https://docs.rs/rayon/latest/rayon/struct.ThreadPool.html + /// [`block_in_place`]: https://docs.rs/tokio/latest/tokio/task/fn.block_in_place.html + pub fn initialize_custom_executor( + self, + closure: CustomExecutorSyncClosure, + ) -> Result<(), Error> { + let strategy = + SyncExecutor::CustomExecutor(CustomExecutor::new(closure, self.max_concurrency)); + set_sync_strategy(strategy) + } +} diff --git a/src/executor/spawn_blocking.rs b/src/executor/spawn_blocking.rs new file mode 100644 index 0000000..4fb3112 --- /dev/null +++ b/src/executor/spawn_blocking.rs @@ -0,0 +1,37 @@ +use tokio::runtime::Handle; + +use crate::{concurrency_limit::ConcurrencyLimit, error::Error}; + +use super::ExecuteSync; + +pub(crate) struct SpawnBlockingExecutor { + concurrency_limit: ConcurrencyLimit, + handle: Handle, +} + +impl SpawnBlockingExecutor { + pub(crate) fn new(max_concurrency: Option) -> Self { + let concurrency_limit = ConcurrencyLimit::new(max_concurrency); + + Self { + concurrency_limit, + handle: Handle::current(), + } + } +} + +impl ExecuteSync for SpawnBlockingExecutor { + async fn execute_sync(&self, f: F) -> Result + where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, + { + let _permit = self.concurrency_limit.acquire_permit().await; + + self.handle + .spawn_blocking(f) + .await + .map_err(Error::JoinError) + // permit implicitly drops + } +} diff --git a/src/lib.rs b/src/lib.rs index db6a430..dffb496 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,400 +1,166 @@ -#[cfg(feature = "tokio_block_in_place")] -mod block_in_place; +#![deny(missing_docs)] +#![deny(missing_debug_implementations)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(test, deny(warnings))] + +//! # compute-heavy-future-executor +//! +//! Experimental crate that adds additional executor patterns to use with frequently blocking futures. +//! +//! Today, when library authors are writing async APIs, they don't have a good way to handle long-running sync segments. +//! +//! For an application, they can use selective handling such as `tokio::task::spawn_blocking()` along with concurrency control to delegate sync segments to blocking threads. Or, they might send the work to a `rayon` threadpool. +//! +//! But, library authors generally don't have this flexibility. As, they generally want to be agnostic across runtime. Or, even if they are `tokio`-specific, they generally don't want to call `tokio::task::spawn_blocking()` as it is +//! suboptimal without extra configuration (concurrency control) as well as highly opinionated to send the work across threads. +//! +//! This library aims to solve this problem by providing libray authors a static, globally scoped strategy that they can delegate blocking sync work to without drawing any conclusions about handling. +//! +//! And then, the applications using the library can either rely on the default strategy that this package provides, or tune them with their preferred approach. +//! +//! The default strategy is to use `tokio::spawn_blocking` with a concurrency limit of the current cpu core count. +//! For non-tokio runtimes, or to use alternative threadpools such as `rayon`, alternative strategies are available. +//! +//! ## Usage - Library Authors +//! First add dependency enabling `compute-heavy-future-executor` (perhaps behind a feature flag). +//! +//! The below will default to 'current context' sync execution (ie non-op) unless the caller enables the tokio feature. +//! +//! ```ignore +//! [dependencies] +//! compute-heavy-future-executor = { version = "0.1", default-features = false } +//! ``` +//! +//! Meanwhile to be slightly more opinionated, the below will enable usage of `spawn_blocking` with concurrency control +//! by default unless the caller opts out: +//! +//! ```ignore +//! [dependencies] +//! compute-heavy-future-executor = { version = "0.1" } +//! ``` +//! +//! And then wrap any sync work by passing it as a closure to a global `execute_sync()` call: +//! +//! ``` +//! use compute_heavy_future_executor::execute_sync; +//! +//! fn sync_work(input: String)-> u8 { +//! std::thread::sleep(std::time::Duration::from_secs(5)); +//! println!("{input}"); +//! 5 +//! } +//! +//! pub async fn a_future_that_has_blocking_sync_work() -> u8 { +//! // relies on caller-specified strategy for translating execute_sync into a future that won't +//! // block the current worker thread +//! execute_sync(move || { sync_work("foo".to_string()) }).await.unwrap() +//! } +//! +//! ``` +//! +//! ## Usage - Application owners +//! Application authors can benefit from this crate with no application code changes, if you are using +//! a library that is itself using this crate. +//! +//! If you want to customize the strategy beyond defaults, they can add +//! `compute-heavy-future-executor` to their dependencies: +//! +//! ```ignore +//! [dependencies] +//! // enables tokio and therefore spawn_blocking strategy by default unless `default-features = false` is provided +//! compute-heavy-future-executor = { version = "0.1" } +//! // used for example with custom executor +//! rayon = "1" +//! ``` +//! +//! And then configure your global strategy as desired. For instance, see below for usage of rayon +//! instead of `spawn_blocking()`. +//! +//! ``` +//! use std::sync::OnceLock; +//! use rayon::ThreadPool; +//! +//! use compute_heavy_future_executor::{ +//! global_sync_strategy_builder, CustomExecutorSyncClosure, +//! }; +//! +//! static THREADPOOL: OnceLock = OnceLock::new(); +//! +//! fn initialize_strategy() { +//! THREADPOOL.set(rayon::ThreadPoolBuilder::default().build().unwrap()); +//! +//! let custom_closure: CustomExecutorSyncClosure = +//! Box::new(|f| Box::new(async move { Ok(THREADPOOL.get().unwrap().spawn(f)) })); +//! +//! global_sync_strategy_builder() +//! // probably no need for max concurrency as rayon already is defaulting to a thread per core +//! // and using a task queue +//! .initialize_custom_executor(custom_closure).unwrap(); +//! } +//! +//! ``` +//! +//! ## Feature flags +//! +//! Feature flags are used both to control default behaviors and to reduce the amount +//! of compiled code. +//! +//! The `tokio` strategy is enabled by default. Disable it via `default-features = false` in your `Cargo.toml`. +//! +//! - `tokio`: Enables the `SpawnBlocking` sync strategy and sets it to be the default. +//! + mod concurrency_limit; -mod current_context; -mod custom_executor; -pub mod error; -#[cfg(feature = "secondary_tokio_runtime")] -mod secondary_tokio_runtime; -#[cfg(feature = "tokio")] -mod spawn_blocking; +mod error; +mod executor; -pub use custom_executor::CustomExecutorClosure; pub use error::Error; -#[cfg(feature = "secondary_tokio_runtime")] -pub use secondary_tokio_runtime::SecondaryTokioRuntimeStrategyBuilder; - -#[cfg(feature = "tokio_block_in_place")] -use block_in_place::BlockInPlaceExecutor; -use current_context::CurrentContextExecutor; -use custom_executor::CustomExecutor; -#[cfg(feature = "secondary_tokio_runtime")] -use secondary_tokio_runtime::SecondaryTokioRuntimeExecutor; -#[cfg(feature = "tokio")] -use spawn_blocking::SpawnBlockingExecutor; - -use std::{fmt::Debug, future::Future, sync::OnceLock}; - -use tokio::{select, sync::oneshot::Receiver}; - -// TODO: module docs, explain the point of this library, give some samples - -/// Initialize a builder to set the global compute heavy future -/// executor strategy. -#[must_use = "doesn't do anything unless used"] -pub fn global_strategy_builder() -> GlobalStrategyBuilder { - GlobalStrategyBuilder::default() -} - -/// Get the currently initialized strategy, or the default strategy for the -/// current feature and runtime type in case no strategy has been loaded. -pub fn global_strategy() -> CurrentStrategy { - match COMPUTE_HEAVY_FUTURE_EXECUTOR_STRATEGY.get() { - Some(strategy) => CurrentStrategy::Initialized(strategy.into()), - None => CurrentStrategy::Default(<&ExecutorStrategyImpl>::default().into()), - } -} - -#[must_use = "doesn't do anything unless used"] -#[derive(Default)] -pub struct GlobalStrategyBuilder { - max_concurrency: Option, -} - -impl GlobalStrategyBuilder { - /// Set the max number of simultaneous futures processed by this executor. - /// - /// If this number is exceeded, the futures sent to - /// [`execute_compute_heavy_future()`] will sleep until a permit - /// can be acquired. - /// - /// ## Default - /// No maximum concurrency - /// - /// # Example - /// - /// ``` - /// use compute_heavy_future_executor::global_strategy_builder; - /// - /// # async fn run() { - /// global_strategy_builder() - /// .max_concurrency(10) - /// .initialize_current_context() - /// .unwrap(); - /// # } - pub fn max_concurrency(self, max_task_concurrency: usize) -> Self { - Self { - max_concurrency: Some(max_task_concurrency), - ..self - } - } - - /// Initializes a new global strategy to wait in the current context. - /// - /// This is effectively a non-op wrapper that adds no special handling for the future besides optional concurrency control. - /// This is the default if the `tokio` feature is disabled. - /// - /// # Cancellation - /// Yes, the future is dropped if the caller drops the returned future from - ///[`execute_compute_heavy_future()`]. - /// - /// Note that it will only be dropped across yield points in the case of long-blocking futures. - /// - /// ## Error - /// Returns an error if the global strategy is already initialized. - /// It can only be initialized once. - /// - /// # Example - /// - /// ``` - /// use compute_heavy_future_executor::global_strategy_builder; - /// use compute_heavy_future_executor::execute_compute_heavy_future; - /// - /// # async fn run() { - /// global_strategy_builder().initialize_current_context().unwrap(); - /// - /// let future = async { - /// std::thread::sleep(std::time::Duration::from_millis(50)); - /// 5 - /// }; - /// - /// let res = execute_compute_heavy_future(future).await.unwrap(); - /// assert_eq!(res, 5); - /// # } - /// ``` - pub fn initialize_current_context(self) -> Result<(), Error> { - let strategy = - ExecutorStrategyImpl::CurrentContext(CurrentContextExecutor::new(self.max_concurrency)); - set_strategy(strategy) - } - - /// Initializes a new global strategy to execute futures by blocking on them inside the - /// tokio blocking threadpool. This is the default strategy if none is explicitly initialized, - /// if the `tokio` feature is enabled. - /// - /// By default, tokio will spin up a blocking thread - /// per task, which may be more than your count of CPU cores, depending on runtime config. - /// - /// If you expect many concurrent cpu-heavy futures, consider limiting your blocking - /// tokio threadpool size. - /// Or, you can use a heavier weight strategy like [`initialize_secondary_tokio_runtime()`]. - /// - /// # Cancellation - /// Yes, the future is dropped if the caller drops the returned future - /// from [`execute_compute_heavy_future()`]. - /// - /// Note that it will only be dropped across yield points in the case of long-blocking futures. - /// - /// ## Error - /// Returns an error if the global strategy is already initialized. - /// It can only be initialized once. - /// - /// # Example - /// - /// ``` - /// use compute_heavy_future_executor::global_strategy_builder; - /// use compute_heavy_future_executor::execute_compute_heavy_future; - /// - /// # async fn run() { - /// global_strategy_builder().initialize_spawn_blocking().unwrap(); - /// - /// let future = async { - /// std::thread::sleep(std::time::Duration::from_millis(50)); - /// 5 - /// }; - /// - /// let res = execute_compute_heavy_future(future).await.unwrap(); - /// assert_eq!(res, 5); - /// # } - /// ``` - #[cfg(feature = "tokio")] - pub fn initialize_spawn_blocking(self) -> Result<(), Error> { - let strategy = - ExecutorStrategyImpl::SpawnBlocking(SpawnBlockingExecutor::new(self.max_concurrency)); - set_strategy(strategy) - } - - /// Initializes a new global strategy to execute futures by calling tokio::task::block_in_place - /// on the current tokio worker thread. This evicts other tasks on same worker thread to - /// avoid blocking them. - /// - /// This approach can starve your executor of worker threads if called with too many - /// concurrent cpu-heavy futures. - /// - /// If you expect many concurrent cpu-heavy futures, consider a - /// heavier weight strategy like [`initialize_secondary_tokio_runtime()`]. - /// - /// # Cancellation - /// No, this strategy does not allow futures to be cancelled. - /// - /// ## Error - /// Returns an error if called from a context besides a tokio multithreaded runtime. - /// - /// Returns an error if the global strategy is already initialized. - /// It can only be initialized once. - /// - /// # Example - /// - /// ``` - /// use compute_heavy_future_executor::global_strategy_builder; - /// use compute_heavy_future_executor::execute_compute_heavy_future; - /// - /// # async fn run() { - /// global_strategy_builder().initialize_block_in_place().unwrap(); - /// - /// let future = async { - /// std::thread::sleep(std::time::Duration::from_millis(50)); - /// 5 - /// }; - /// - /// let res = execute_compute_heavy_future(future).await.unwrap(); - /// assert_eq!(res, 5); - /// # } - /// ``` - #[cfg(feature = "tokio_block_in_place")] - pub fn initialize_block_in_place(self) -> Result<(), Error> { - let strategy = - ExecutorStrategyImpl::BlockInPlace(BlockInPlaceExecutor::new(self.max_concurrency)?); - set_strategy(strategy) - } - - /// Initializes a new global strategy that spins up a secondary background tokio runtime - /// that executes futures on lower priority worker threads. - /// - /// This uses certain defaults, listed below. To modify these defaults, - /// instead use [`secondary_tokio_runtime_builder()`] - /// - /// # Defaults - /// ## Thread niceness - /// The thread niceness for the secondary runtime's worker threads, - /// which on linux is used to increase or lower relative - /// OS scheduling priority. - /// - /// Default: 10 - /// - /// ## Thread count - /// The count of worker threads in the secondary tokio runtime. - /// - /// Default: CPU core count - /// - /// ## Channel size - /// The buffer size of the channel used to spawn tasks - /// in the background executor. - /// - /// Default: 10 - /// - /// ## Max task concurrency - /// The max number of simultaneous background tasks running - /// - /// Default: no limit - /// - /// # Cancellation - /// Yes, the future is dropped if the caller drops the returned future - /// from [`execute_compute_heavy_future()`]. - /// - /// Note that it will only be dropped across yield points in the case of long-blocking futures. - /// - /// ## Error - /// Returns an error if the global strategy is already initialized. - /// It can only be initialized once. - /// - /// # Example - /// - /// ``` - /// use compute_heavy_future_executor::global_strategy_builder; - /// use compute_heavy_future_executor::execute_compute_heavy_future; - /// - /// # async fn run() { - /// global_strategy_builder().initialize_secondary_tokio_runtime().unwrap(); - /// - /// let future = async { - /// std::thread::sleep(std::time::Duration::from_millis(50)); - /// 5 - /// }; - /// - /// let res = execute_compute_heavy_future(future).await.unwrap(); - /// assert_eq!(res, 5); - /// # } - /// ``` - #[cfg(feature = "secondary_tokio_runtime")] - pub fn initialize_secondary_tokio_runtime(self) -> Result<(), Error> { - self.secondary_tokio_runtime_builder().initialize() - } - - /// Creates a [`SecondaryTokioRuntimeStrategyBuilder`] for a customized secondary tokio runtime strategy. - /// - /// Subsequent calls on the returned builder allow modifying defaults. - /// - /// The returned builder will require calling [`SecondaryTokioRuntimeStrategyBuilder::initialize()`] to - /// ultimately load the strategy. - /// - /// # Cancellation - /// Yes, the future is dropped if the caller drops the returned future - /// from [`execute_compute_heavy_future()`]. - /// - /// Note that it will only be dropped across yield points in the case of long-blocking futures. - /// - /// # Example - /// - /// ``` - /// use compute_heavy_future_executor::global_strategy_builder; - /// use compute_heavy_future_executor::execute_compute_heavy_future; - /// - /// # async fn run() { - /// global_strategy_builder() - /// .secondary_tokio_runtime_builder() - /// .niceness(1) - /// .thread_count(2) - /// .channel_size(3) - /// .max_concurrency(4) - /// .initialize() - /// .unwrap(); - /// - /// let future = async { - /// std::thread::sleep(std::time::Duration::from_millis(50)); - /// 5 - /// }; - /// - /// let res = execute_compute_heavy_future(future).await.unwrap(); - /// assert_eq!(res, 5); - /// # } - /// ``` - #[cfg(feature = "secondary_tokio_runtime")] - #[must_use = "doesn't do anything unless used"] - pub fn secondary_tokio_runtime_builder(self) -> SecondaryTokioRuntimeStrategyBuilder { - SecondaryTokioRuntimeStrategyBuilder::new(self.max_concurrency) - } - - /// Accepts a closure that will poll an arbitrary feature to completion. - /// - /// Intended for injecting arbitrary runtimes/strategies or customizing existing ones. - /// - /// # Cancellation - /// Yes, the closure's returned future is dropped if the caller drops the returned future from [`execute_compute_heavy_future()`]. - /// Note that it will only be dropped across yield points in the case of long-blocking futures. - /// - /// ## Error - /// Returns an error if the global strategy is already initialized. - /// It can only be initialized once. - /// - /// # Example - /// - /// ``` - /// use compute_heavy_future_executor::global_strategy_builder; - /// use compute_heavy_future_executor::execute_compute_heavy_future; - /// use compute_heavy_future_executor::CustomExecutorClosure; - /// - /// // this isn't actually a good strategy, to be clear - /// # async fn run() { - /// let closure: CustomExecutorClosure = Box::new(|fut| { - /// Box::new( - /// async move { - /// tokio::task::spawn(async move { fut.await }) - /// .await - /// .map_err(|err| err.into()) - /// } - /// ) - /// }); - /// - /// global_strategy_builder().initialize_custom_executor(closure).unwrap(); - /// - /// let future = async { - /// std::thread::sleep(std::time::Duration::from_millis(50)); - /// 5 - /// }; - /// - /// let res = execute_compute_heavy_future(future).await.unwrap(); - /// assert_eq!(res, 5); - /// # } - /// - /// ``` - pub fn initialize_custom_executor(self, closure: CustomExecutorClosure) -> Result<(), Error> { - let strategy = ExecutorStrategyImpl::CustomExecutor(CustomExecutor::new( - closure, - self.max_concurrency, - )); - set_strategy(strategy) - } -} +pub use executor::{ + custom_executor::CustomExecutorSyncClosure, global_sync_strategy, SyncExecutorBuilder, +}; -pub(crate) fn set_strategy(strategy: ExecutorStrategyImpl) -> Result<(), Error> { - COMPUTE_HEAVY_FUTURE_EXECUTOR_STRATEGY - .set(strategy) - .map_err(|_| { - Error::AlreadyInitialized(COMPUTE_HEAVY_FUTURE_EXECUTOR_STRATEGY.get().unwrap().into()) - })?; +use executor::{get_global_sync_executor, ExecuteSync, SyncExecutor}; - log::info!( - "initialized compute-heavy future executor strategy - {:#?}", - global_strategy() - ); - - Ok(()) -} -trait ComputeHeavyFutureExecutor { - /// Accepts a future and returns its result - async fn execute(&self, fut: F) -> Result - where - F: Future + Send + 'static, - O: Send + 'static; -} +use std::fmt::Debug; +/// The currently loaded global strategy. #[derive(Debug, Clone, Copy, PartialEq)] -pub enum CurrentStrategy { +pub enum GlobalStrategy { + /// The strategy loaded by default. Default(ExecutorStrategy), + /// Caller-specified strategy, replacing default Initialized(ExecutorStrategy), } +/// The different types of executor strategies that can be loaded. +/// See [`SyncExecutorBuilder`] for more detail on each strategy. +/// +/// # Examples +/// +/// ``` +/// use compute_heavy_future_executor::{ +/// global_sync_strategy, +/// global_sync_strategy_builder, +/// GlobalStrategy, +/// ExecutorStrategy +/// }; +/// +/// # fn run() { +/// +/// #[cfg(feature = "tokio")] +/// assert_eq!(global_sync_strategy(), GlobalStrategy::Default(ExecutorStrategy::SpawnBlocking)); +/// +/// #[cfg(not(feature = "tokio"))] +/// assert_eq!(global_sync_strategy(), GlobalStrategy::Default(ExecutorStrategy::CurrentContext)); +/// +/// global_sync_strategy_builder() +/// .initialize_current_context() +/// .unwrap(); +/// +/// assert_eq!(global_sync_strategy(), GlobalStrategy::Initialized(ExecutorStrategy::CurrentContext)); +/// +/// # } +/// ``` #[non_exhaustive] #[derive(Debug, Clone, Copy, PartialEq)] pub enum ExecutorStrategy { @@ -405,173 +171,67 @@ pub enum ExecutorStrategy { /// tokio task::spawn_blocking #[cfg(feature = "tokio")] SpawnBlocking, - /// tokio task::block_in_place - #[cfg(feature = "tokio_block_in_place")] - BlockInPlace, - #[cfg(feature = "secondary_tokio_runtime")] - /// Spin up a second, lower-priority tokio runtime - /// that communicates via channels - SecondaryTokioRuntime, -} - -impl From<&ExecutorStrategyImpl> for ExecutorStrategy { - fn from(value: &ExecutorStrategyImpl) -> Self { - match value { - ExecutorStrategyImpl::CurrentContext(_) => Self::CurrentContext, - ExecutorStrategyImpl::CustomExecutor(_) => Self::CustomExecutor, - #[cfg(feature = "tokio")] - ExecutorStrategyImpl::SpawnBlocking(_) => Self::SpawnBlocking, - #[cfg(feature = "tokio_block_in_place")] - ExecutorStrategyImpl::BlockInPlace(_) => Self::BlockInPlace, - #[cfg(feature = "secondary_tokio_runtime")] - ExecutorStrategyImpl::SecondaryTokioRuntime(_) => Self::SecondaryTokioRuntime, - } - } -} - -/// The stored strategy used to spawn compute-heavy futures. -static COMPUTE_HEAVY_FUTURE_EXECUTOR_STRATEGY: OnceLock = OnceLock::new(); - -#[non_exhaustive] -enum ExecutorStrategyImpl { - /// A non-op strategy that awaits in the current context - CurrentContext(CurrentContextExecutor), - /// User-provided closure - CustomExecutor(CustomExecutor), - /// tokio task::spawn_blocking - #[cfg(feature = "tokio")] - SpawnBlocking(SpawnBlockingExecutor), - /// tokio task::block_in_place - #[cfg(feature = "tokio_block_in_place")] - BlockInPlace(BlockInPlaceExecutor), - #[cfg(feature = "secondary_tokio_runtime")] - /// Spin up a second, lower-priority tokio runtime - /// that communicates via channels - SecondaryTokioRuntime(SecondaryTokioRuntimeExecutor), } -impl ComputeHeavyFutureExecutor for ExecutorStrategyImpl { - async fn execute(&self, fut: F) -> Result - where - F: Future + Send + 'static, - O: Send + 'static, - { - match self { - ExecutorStrategyImpl::CurrentContext(executor) => executor.execute(fut).await, - ExecutorStrategyImpl::CustomExecutor(executor) => executor.execute(fut).await, - #[cfg(feature = "tokio")] - ExecutorStrategyImpl::SpawnBlocking(executor) => executor.execute(fut).await, - #[cfg(feature = "tokio_block_in_place")] - ExecutorStrategyImpl::BlockInPlace(executor) => executor.execute(fut).await, - #[cfg(feature = "secondary_tokio_runtime")] - ExecutorStrategyImpl::SecondaryTokioRuntime(executor) => executor.execute(fut).await, - } - } -} - -/// The fallback strategy used in case no strategy is explicitly set -static DEFAULT_COMPUTE_HEAVY_FUTURE_EXECUTOR_STRATEGY: OnceLock = - OnceLock::new(); - -impl Default for &ExecutorStrategyImpl { - fn default() -> Self { - &DEFAULT_COMPUTE_HEAVY_FUTURE_EXECUTOR_STRATEGY.get_or_init(|| { - #[cfg(feature = "tokio")] - { - log::info!("Defaulting to SpawnBlocking strategy for compute-heavy future executor \ - until a strategy is initialized"); - - ExecutorStrategyImpl::SpawnBlocking(SpawnBlockingExecutor::new(None)) - } - - #[cfg(not(feature = "tokio"))] - { - log::warn!("Defaulting to CurrentContext (non-op) strategy for compute-heavy future executor \ - until a strategy is initialized."); - ExecutorStrategyImpl::CurrentContext(CurrentContextExecutor::new(None)) - } - }) - } +/// Initialize a builder to set the global sync function +/// executor strategy. +/// +/// See [`SyncExecutorBuilder`] for details on strategies. +/// +/// # Examples +/// +/// ``` +/// use compute_heavy_future_executor::global_sync_strategy_builder; +/// # fn run() { +/// global_sync_strategy_builder().max_concurrency(3).initialize_spawn_blocking().unwrap(); +/// # } +/// ``` +#[must_use = "doesn't do anything unless used"] +pub fn global_sync_strategy_builder() -> SyncExecutorBuilder { + SyncExecutorBuilder::default() } -/// Spawn a future to the configured compute-heavy executor and wait on its output. +/// Send a sync closure to the configured or default global compute-heavy executor, and wait on its output. /// /// # Strategy selection /// /// If no strategy is configured, this library will fall back to the following defaults: -/// - no `tokio`` feature - current context -/// - all other cases - spawn blocking -/// -/// You can override these defaults by initializing a strategy via [`global_strategy_builder()`] -/// and [`GlobalStrategyBuilder`]. -/// -/// # Cancellation +/// - no `tokio`` feature - current context w/ max concurrency = cpu core count +/// - `tokio` feature - spawn blocking w/ max concurrency = cpu core count /// -/// Most strategies will cancel the input future, if the caller drops the returned future, -/// with the following exception: -/// - the block in place strategy never cancels the future (until the executor is shut down) +/// You can override these defaults by initializing a strategy via [`global_sync_strategy_builder()`] +/// and [`SyncExecutorBuilder`]. /// -/// # Example +/// # Examples /// /// ``` /// # async fn run() { -/// use compute_heavy_future_executor::execute_compute_heavy_future; +/// use compute_heavy_future_executor::execute_sync; /// -/// let future = async { -/// std::thread::sleep(std::time::Duration::from_millis(50)); +/// let closure = || { +/// std::thread::sleep(std::time::Duration::from_secs(1)); /// 5 -/// }; +/// }; /// -/// let res = execute_compute_heavy_future(future).await.unwrap(); +/// let res = execute_sync(closure).await.unwrap(); /// assert_eq!(res, 5); /// # } /// /// ``` /// -pub async fn execute_compute_heavy_future(fut: F) -> Result +pub async fn execute_sync(f: F) -> Result where - F: Future + Send + 'static, + F: FnOnce() -> R + Send + 'static, R: Send + 'static, { - let executor = COMPUTE_HEAVY_FUTURE_EXECUTOR_STRATEGY - .get() - .unwrap_or_else(|| <&ExecutorStrategyImpl>::default()); + let executor = get_global_sync_executor(); + match executor { - ExecutorStrategyImpl::CurrentContext(executor) => executor.execute(fut).await, - ExecutorStrategyImpl::CustomExecutor(executor) => executor.execute(fut).await, - #[cfg(feature = "tokio_block_in_place")] - ExecutorStrategyImpl::BlockInPlace(executor) => executor.execute(fut).await, + SyncExecutor::CurrentContext(executor) => executor.execute_sync(f).await, + SyncExecutor::CustomExecutor(executor) => executor.execute_sync(f).await, #[cfg(feature = "tokio")] - ExecutorStrategyImpl::SpawnBlocking(executor) => executor.execute(fut).await, - #[cfg(feature = "secondary_tokio_runtime")] - ExecutorStrategyImpl::SecondaryTokioRuntime(executor) => executor.execute(fut).await, + SyncExecutor::SpawnBlocking(executor) => executor.execute_sync(f).await, } } -pub fn make_future_cancellable(fut: F) -> (impl Future, Receiver) -where - F: std::future::Future + Send + 'static, - O: Send + 'static, -{ - let (mut tx, rx) = tokio::sync::oneshot::channel(); - let wrapped_future = async { - select! { - // if tx is closed, we always want to poll that future first, - // so we don't need to add rng - biased; - - _ = tx.closed() => { - // receiver already dropped, don't need to do anything - // cancel the background future - }, - result = fut => { - // if this fails, the receiver already dropped, so we don't need to do anything - let _ = tx.send(result); - } - } - }; - - (wrapped_future, rx) -} - // tests are in /tests/ to allow separate initialization of oncelock across processes when using default cargo test runner diff --git a/src/secondary_tokio_runtime.rs b/src/secondary_tokio_runtime.rs deleted file mode 100644 index 318418c..0000000 --- a/src/secondary_tokio_runtime.rs +++ /dev/null @@ -1,221 +0,0 @@ -use std::{future::Future, pin::Pin}; - -use tokio::sync::mpsc::Sender; - -use crate::{ - concurrency_limit::ConcurrencyLimit, - error::{Error, InvalidConfig}, - make_future_cancellable, set_strategy, ComputeHeavyFutureExecutor, ExecutorStrategyImpl, -}; - -const DEFAULT_NICENESS: i8 = 10; -const DEFAULT_CHANNEL_SIZE: usize = 10; - -fn default_thread_count() -> usize { - num_cpus::get() -} - -/// Extention of [`GlobalStrategyBuilder`] for a customized secondary tokio runtime strategy. -/// -/// Requires calling [`SecondaryTokioRuntimeStrategyBuilder::initialize()`] to -/// initialize the strategy. -/// -/// # Example -/// -/// ``` -/// use compute_heavy_future_executor::global_strategy_builder; -/// use compute_heavy_future_executor::execute_compute_heavy_future; -/// -/// # async fn run() { -/// global_strategy_builder() -/// .secondary_tokio_runtime_builder() -/// .niceness(1) -/// .thread_count(2) -/// .channel_size(3) -/// .max_concurrency(4) -/// .initialize() -/// .unwrap(); -/// # } -/// ``` -#[must_use = "doesn't do anything unless used"] -#[derive(Default)] -pub struct SecondaryTokioRuntimeStrategyBuilder { - niceness: Option, - thread_count: Option, - channel_size: Option, - // passed down from the parent `GlobalStrategy` builder, not modified internally - max_concurrency: Option, -} - -impl SecondaryTokioRuntimeStrategyBuilder { - pub(crate) fn new(max_concurrency: Option) -> Self { - Self { - max_concurrency, - ..Default::default() - } - } -} - -impl SecondaryTokioRuntimeStrategyBuilder { - /// Set the thread niceness for the secondary runtime's worker threads, - /// which on linux is used to increase or lower relative - /// OS scheduling priority. - /// - /// Allowed values are -20..=19 - /// - /// ## Default - /// - /// The default value is 10. - pub fn niceness(self, niceness: i8) -> Self { - Self { - niceness: Some(niceness), - ..self - } - } - - /// Set the count of worker threads in the secondary tokio runtime. - /// - /// ## Default - /// - /// The default value is the number of cpu cores - pub fn thread_count(self, thread_count: usize) -> Self { - Self { - thread_count: Some(thread_count), - ..self - } - } - - /// Set the buffer size of the channel used to spawn tasks - /// in the background executor. - /// - /// ## Default - /// - /// The default value is 10 - pub fn channel_size(self, channel_size: usize) -> Self { - Self { - channel_size: Some(channel_size), - ..self - } - } - - /// Set the max number of simultaneous futures processed by this executor. - /// - /// Yes, the future is dropped if the caller drops the returned future from - ///[`execute_compute_heavy_future()`]. - /// - /// ## Default - /// No maximum concurrency - pub fn max_concurrency(self, max_task_concurrency: usize) -> Self { - Self { - max_concurrency: Some(max_task_concurrency), - ..self - } - } - - pub fn initialize(self) -> Result<(), Error> { - let niceness = self.niceness.unwrap_or(DEFAULT_NICENESS); - - // please https://github.com/rust-lang/rfcs/issues/671 - if !(-20..=19).contains(&niceness) { - return Err(Error::InvalidConfig(InvalidConfig { - field: "niceness", - received: niceness.to_string(), - expected: "-20..=19", - })); - } - - let thread_count = self.thread_count.unwrap_or_else(|| default_thread_count()); - let channel_size = self.channel_size.unwrap_or(DEFAULT_CHANNEL_SIZE); - - let executor = SecondaryTokioRuntimeExecutor::new( - niceness, - thread_count, - channel_size, - self.max_concurrency, - ); - - set_strategy(ExecutorStrategyImpl::SecondaryTokioRuntime(executor)) - } -} - -type BackgroundFuture = Pin + Send + 'static>>; - -pub(crate) struct SecondaryTokioRuntimeExecutor { - tx: Sender, - concurrency_limit: ConcurrencyLimit, -} - -impl SecondaryTokioRuntimeExecutor { - pub(crate) fn new( - niceness: i8, - thread_count: usize, - channel_size: usize, - max_concurrency: Option, - ) -> Self { - // channel is only for routing work to new task::spawn so should be very quick - let (tx, mut rx) = tokio::sync::mpsc::channel(channel_size); - - std::thread::Builder::new() - .name("compute-heavy-executor".to_string()) - .spawn(move || { - let rt = tokio::runtime::Builder::new_multi_thread() - .thread_name("compute-heavy-executor-pool-thread") - .worker_threads(thread_count) - .on_thread_start(move || unsafe { - // Reduce thread pool thread niceness, so they are lower priority - // than the foreground executor and don't interfere with I/O tasks - #[cfg(target_os = "linux")] - { - *libc::__errno_location() = 0; - if libc::nice(niceness.into()) == -1 && *libc::__errno_location() != 0 { - let error = std::io::Error::last_os_error(); - log::error!("failed to set threadpool niceness of secondary compute-heavy tokio executor: {}", error); - } - } - }) - .enable_all() - .build() - .unwrap_or_else(|e| panic!("cpu heavy runtime failed_to_initialize: {}", e)); - - rt.block_on(async { - log::debug!("starting to process work on secondary compute-heavy tokio executor"); - - while let Some(work) = rx.recv().await { - tokio::task::spawn(async move { - work.await - }); - } - }); - log::warn!("exiting secondary compute heavy tokio runtime because foreground channel closed"); - }) - .unwrap_or_else(|e| panic!("secondary compute-heavy runtime thread failed_to_initialize: {}", e)); - - Self { - tx, - concurrency_limit: ConcurrencyLimit::new(max_concurrency), - } - } -} - -impl ComputeHeavyFutureExecutor for SecondaryTokioRuntimeExecutor { - async fn execute(&self, fut: F) -> Result - where - F: std::future::Future + Send + 'static, - O: Send + 'static, - { - let _permit = self.concurrency_limit.acquire_permit().await; - - let (wrapped_future, rx) = make_future_cancellable(fut); - - match self.tx.send(Box::pin(wrapped_future)).await { - Ok(_) => (), - Err(err) => { - panic!("secondary compute-heavy runtime channel cannot be reached: {err}") - } - } - - rx.await.map_err(|err| Error::RecvError(err)) - - // permit implicitly drops - } -} diff --git a/src/spawn_blocking.rs b/src/spawn_blocking.rs deleted file mode 100644 index fee12a6..0000000 --- a/src/spawn_blocking.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::{ - concurrency_limit::ConcurrencyLimit, error::Error, make_future_cancellable, - ComputeHeavyFutureExecutor, -}; - -pub(crate) struct SpawnBlockingExecutor { - concurrency_limit: ConcurrencyLimit, -} - -impl SpawnBlockingExecutor { - pub(crate) fn new(max_concurrency: Option) -> Self { - let concurrency_limit = ConcurrencyLimit::new(max_concurrency); - - Self { concurrency_limit } - } -} - -impl ComputeHeavyFutureExecutor for SpawnBlockingExecutor { - async fn execute(&self, fut: F) -> Result - where - F: std::future::Future + Send + 'static, - O: Send + 'static, - { - let _permit = self.concurrency_limit.acquire_permit().await; - - let (wrapped_future, rx) = make_future_cancellable(fut); - - if let Err(err) = tokio::task::spawn_blocking(move || { - tokio::runtime::Handle::current().block_on(wrapped_future) - }) - .await - { - return Err(Error::JoinError(err)); - } - - rx.await.map_err(|err| Error::RecvError(err)) - } -} diff --git a/tests/block_in_place_strategy.rs b/tests/block_in_place_strategy.rs deleted file mode 100644 index c425260..0000000 --- a/tests/block_in_place_strategy.rs +++ /dev/null @@ -1,58 +0,0 @@ -#[cfg(feature = "tokio_block_in_place")] -mod test { - use std::time::Duration; - - use compute_heavy_future_executor::{ - execute_compute_heavy_future, global_strategy, global_strategy_builder, CurrentStrategy, - ExecutorStrategy, - }; - use futures_util::future::join_all; - - fn initialize() { - // we are racing all tests against the single oncelock - let _ = global_strategy_builder() - .max_concurrency(3) - .initialize_block_in_place(); - } - - #[cfg(feature = "tokio_block_in_place")] - #[tokio::test(flavor = "multi_thread")] - async fn block_in_place_strategy() { - initialize(); - - let future = async { 5 }; - - let res = execute_compute_heavy_future(future).await.unwrap(); - assert_eq!(res, 5); - - assert_eq!( - global_strategy(), - CurrentStrategy::Initialized(ExecutorStrategy::BlockInPlace) - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 10)] - async fn block_in_place_concurrency() { - initialize(); - - let start = std::time::Instant::now(); - - let mut futures = Vec::new(); - - for _ in 0..5 { - let future = async move { std::thread::sleep(Duration::from_millis(15)) }; - // we need to spawn here since otherwise block in place will cancel other futures inside the same task, - // ref https://docs.rs/tokio/latest/tokio/task/fn.block_in_place.html - let future = - tokio::task::spawn(async move { execute_compute_heavy_future(future).await }); - futures.push(future); - } - - join_all(futures).await; - - let elapsed_millis = start.elapsed().as_millis(); - assert!(elapsed_millis < 50, "futures did not run concurrently"); - - assert!(elapsed_millis > 20, "futures exceeded max concurrency"); - } -} diff --git a/tests/block_in_place_wrong_runtime.rs b/tests/block_in_place_wrong_runtime.rs deleted file mode 100644 index dec7b72..0000000 --- a/tests/block_in_place_wrong_runtime.rs +++ /dev/null @@ -1,9 +0,0 @@ -#[cfg(feature = "tokio_block_in_place")] -#[tokio::test] -async fn block_in_place_wrong_runtime() { - use compute_heavy_future_executor::{global_strategy_builder, Error}; - - let res = global_strategy_builder().initialize_block_in_place(); - - assert!(matches!(res, Err(Error::InvalidConfig(_)))); -} diff --git a/tests/current_context_default.rs b/tests/current_context_default.rs index 3985b67..6e4f2ff 100644 --- a/tests/current_context_default.rs +++ b/tests/current_context_default.rs @@ -1,19 +1,24 @@ #[cfg(not(feature = "tokio"))] #[tokio::test] -async fn default_to_current_context_tokio_single_threaded() { +async fn default_to_current_context() { + use std::time::Duration; + use compute_heavy_future_executor::{ - execute_compute_heavy_future, global_strategy, CurrentStrategy, ExecutorStrategy, + execute_sync, global_sync_strategy, ExecutorStrategy, GlobalStrategy, }; // this is a tokio test but we haven't enabled the tokio config flag - let future = async { 5 }; + let closure = || { + std::thread::sleep(Duration::from_millis(5)); + 5 + }; - let res = execute_compute_heavy_future(future).await.unwrap(); + let res = execute_sync(closure).await.unwrap(); assert_eq!(res, 5); assert_eq!( - global_strategy(), - CurrentStrategy::Default(ExecutorStrategy::CurrentContext) + global_sync_strategy(), + GlobalStrategy::Default(ExecutorStrategy::CurrentContext) ); } diff --git a/tests/current_context_strategy.rs b/tests/current_context_strategy.rs index 2669d82..eb824d6 100644 --- a/tests/current_context_strategy.rs +++ b/tests/current_context_strategy.rs @@ -1,15 +1,14 @@ use std::time::Duration; use compute_heavy_future_executor::{ - execute_compute_heavy_future, global_strategy, global_strategy_builder, CurrentStrategy, - ExecutorStrategy, + execute_sync, global_sync_strategy, global_sync_strategy_builder, ExecutorStrategy, + GlobalStrategy, }; use futures_util::future::join_all; -use tokio::select; fn initialize() { // we are racing all tests against the single oncelock - let _ = global_strategy_builder() + let _ = global_sync_strategy_builder() .max_concurrency(3) .initialize_current_context(); } @@ -18,44 +17,21 @@ fn initialize() { async fn current_context_strategy() { initialize(); - let future = async { 5 }; - - let res = execute_compute_heavy_future(future).await.unwrap(); - assert_eq!(res, 5); - - assert_eq!( - global_strategy(), - CurrentStrategy::Initialized(ExecutorStrategy::CurrentContext) - ); -} - -#[tokio::test] -async fn current_context_cancellable() { - initialize(); - - let (tx, mut rx) = tokio::sync::oneshot::channel::<()>(); - let future = async move { - { - tokio::time::sleep(Duration::from_secs(60)).await; - let _ = tx.send(()); - } + let closure = || { + std::thread::sleep(Duration::from_millis(15)); + 5 }; - select! { - _ = tokio::time::sleep(Duration::from_millis(4)) => { }, - _ = execute_compute_heavy_future(future) => {} - } - - tokio::time::sleep(Duration::from_millis(8)).await; + let res = execute_sync(closure).await.unwrap(); + assert_eq!(res, 5); - // future should have been cancelled when spawn compute heavy future was dropped assert_eq!( - rx.try_recv(), - Err(tokio::sync::oneshot::error::TryRecvError::Closed) + global_sync_strategy(), + GlobalStrategy::Initialized(ExecutorStrategy::CurrentContext) ); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn current_context_concurrency() { initialize(); @@ -63,11 +39,19 @@ async fn current_context_concurrency() { let mut futures = Vec::new(); - for _ in 0..5 { - // can't use std::thread::sleep because this is all in the same thread - let future = async move { tokio::time::sleep(Duration::from_millis(15)).await }; - futures.push(execute_compute_heavy_future(future)); + let closure = || { + std::thread::sleep(Duration::from_millis(15)); + 5 + }; + + // note that we also are racing against concurrency from other tests in this module + for _ in 0..6 { + // we need to spawn tasks since otherwise we'll just block the current worker thread + let future = + async move { tokio::task::spawn(async move { execute_sync(closure).await }).await }; + futures.push(future); } + tokio::time::sleep(Duration::from_millis(5)).await; join_all(futures).await; diff --git a/tests/custom_executor_simple.rs b/tests/custom_executor_simple.rs index 5999adc..255f30f 100644 --- a/tests/custom_executor_simple.rs +++ b/tests/custom_executor_simple.rs @@ -1,28 +1,33 @@ +use std::time::Duration; + use compute_heavy_future_executor::{ - execute_compute_heavy_future, global_strategy, global_strategy_builder, CurrentStrategy, - CustomExecutorClosure, ExecutorStrategy, + execute_sync, global_sync_strategy, global_sync_strategy_builder, CustomExecutorSyncClosure, + ExecutorStrategy, GlobalStrategy, }; #[tokio::test] async fn custom_executor_simple() { - let closure: CustomExecutorClosure = Box::new(|fut| { + let custom_closure: CustomExecutorSyncClosure = Box::new(|f| { Box::new(async move { - let res = fut.await; - Ok(res) + f(); + Ok(()) }) }); - global_strategy_builder() - .initialize_custom_executor(closure) + global_sync_strategy_builder() + .initialize_custom_executor(custom_closure) .unwrap(); - let future = async { 5 }; + let closure = || { + std::thread::sleep(Duration::from_millis(15)); + 5 + }; - let res = execute_compute_heavy_future(future).await.unwrap(); + let res = execute_sync(closure).await.unwrap(); assert_eq!(res, 5); assert_eq!( - global_strategy(), - CurrentStrategy::Initialized(ExecutorStrategy::CustomExecutor) + global_sync_strategy(), + GlobalStrategy::Initialized(ExecutorStrategy::CustomExecutor) ); } diff --git a/tests/custom_executor_strategy.rs b/tests/custom_executor_strategy.rs index 210f5bf..f83102a 100644 --- a/tests/custom_executor_strategy.rs +++ b/tests/custom_executor_strategy.rs @@ -1,33 +1,38 @@ -use std::time::Duration; +use std::{sync::OnceLock, time::Duration}; use compute_heavy_future_executor::{ - execute_compute_heavy_future, global_strategy_builder, CustomExecutorClosure, + execute_sync, global_sync_strategy_builder, CustomExecutorSyncClosure, }; use futures_util::future::join_all; -use tokio::select; +use rayon::ThreadPool; + +static THREADPOOL: OnceLock = OnceLock::new(); fn initialize() { - let closure: CustomExecutorClosure = Box::new(|fut| { + THREADPOOL.get_or_init(|| rayon::ThreadPoolBuilder::default().build().unwrap()); + + let custom_closure: CustomExecutorSyncClosure = Box::new(|f| { Box::new(async move { - tokio::task::spawn(async move { fut.await }) - .await - .map_err(|err| err.into()) + THREADPOOL.get().unwrap().spawn(f); + Ok(()) }) }); - // we are racing all tests against the single oncelock - let _ = global_strategy_builder() + let _ = global_sync_strategy_builder() .max_concurrency(3) - .initialize_custom_executor(closure); + .initialize_custom_executor(custom_closure); } #[tokio::test] async fn custom_executor_strategy() { initialize(); - let future = async { 5 }; + let closure = || { + std::thread::sleep(Duration::from_millis(15)); + 5 + }; - let res = execute_compute_heavy_future(future).await.unwrap(); + let res = execute_sync(closure).await.unwrap(); assert_eq!(res, 5); } @@ -39,10 +44,15 @@ async fn custom_executor_concurrency() { let mut futures = Vec::new(); - for _ in 0..5 { - // can't use std::thread::sleep because this is all in the same thread - let future = async move { tokio::time::sleep(Duration::from_millis(15)).await }; - futures.push(execute_compute_heavy_future(future)); + let closure = || { + std::thread::sleep(Duration::from_millis(15)); + 5 + }; + tokio::time::sleep(Duration::from_millis(5)).await; + + // note that we also are racing against concurrency from other tests in this module + for _ in 0..6 { + futures.push(execute_sync(closure)); } join_all(futures).await; @@ -52,29 +62,3 @@ async fn custom_executor_concurrency() { assert!(elapsed_millis > 20, "futures exceeded max concurrency"); } - -#[tokio::test] -async fn custom_executor_cancellable() { - initialize(); - - let (tx, mut rx) = tokio::sync::oneshot::channel::<()>(); - let future = async move { - { - tokio::time::sleep(Duration::from_secs(60)).await; - let _ = tx.send(()); - } - }; - - select! { - _ = tokio::time::sleep(Duration::from_millis(4)) => { }, - _ = execute_compute_heavy_future(future) => {} - } - - tokio::time::sleep(Duration::from_millis(8)).await; - - // future should have been cancelled when spawn compute heavy future was dropped - assert_eq!( - rx.try_recv(), - Err(tokio::sync::oneshot::error::TryRecvError::Closed) - ); -} diff --git a/tests/multiple_initialize_err.rs b/tests/multiple_initialize_err.rs index 5ad3e2d..6e9606a 100644 --- a/tests/multiple_initialize_err.rs +++ b/tests/multiple_initialize_err.rs @@ -1,13 +1,13 @@ -use compute_heavy_future_executor::{error::Error, global_strategy_builder}; +use compute_heavy_future_executor::{global_sync_strategy_builder, Error}; #[test] fn multiple_initialize_err() { - global_strategy_builder() + global_sync_strategy_builder() .initialize_current_context() .unwrap(); assert!(matches!( - global_strategy_builder().initialize_current_context(), + global_sync_strategy_builder().initialize_current_context(), Err(Error::AlreadyInitialized(_)) )); } diff --git a/tests/multiple_initialize_err_with_secondary_runtime_builder.rs b/tests/multiple_initialize_err_with_secondary_runtime_builder.rs deleted file mode 100644 index 5ec6f1b..0000000 --- a/tests/multiple_initialize_err_with_secondary_runtime_builder.rs +++ /dev/null @@ -1,16 +0,0 @@ -#[cfg(feature = "secondary_tokio_runtime")] -#[test] -fn multiple_initialize_err_with_secondary_runtime_builder() { - use compute_heavy_future_executor::{error::Error, global_strategy_builder}; - - let builder = global_strategy_builder().secondary_tokio_runtime_builder(); // not yet initialized - - global_strategy_builder() - .initialize_current_context() - .unwrap(); - - assert!(matches!( - builder.initialize(), - Err(Error::AlreadyInitialized(_)) - )); -} diff --git a/tests/secondary_tokio_builder_allowed_config.rs b/tests/secondary_tokio_builder_allowed_config.rs deleted file mode 100644 index c04b7da..0000000 --- a/tests/secondary_tokio_builder_allowed_config.rs +++ /dev/null @@ -1,19 +0,0 @@ -#[cfg(feature = "secondary_tokio_runtime")] -#[tokio::test] -async fn secondary_tokio_runtime_builder_allowed_config() { - use compute_heavy_future_executor::{execute_compute_heavy_future, global_strategy_builder}; - - global_strategy_builder() - .max_concurrency(5) - .secondary_tokio_runtime_builder() - .channel_size(10) - .niceness(5) - .thread_count(2) - .initialize() - .unwrap(); - - let future = async { 5 }; - - let res = execute_compute_heavy_future(future).await.unwrap(); - assert_eq!(res, 5); -} diff --git a/tests/secondary_tokio_builder_disallowed_config.rs b/tests/secondary_tokio_builder_disallowed_config.rs deleted file mode 100644 index 51b512a..0000000 --- a/tests/secondary_tokio_builder_disallowed_config.rs +++ /dev/null @@ -1,14 +0,0 @@ -#[cfg(feature = "secondary_tokio_runtime")] -#[tokio::test] -#[should_panic] -async fn secondary_tokio_runtime_builder_disallowed_config() { - use compute_heavy_future_executor::{error::Error, global_strategy_builder}; - - let res = global_strategy_builder() - .secondary_tokio_runtime_builder() - .channel_size(10) - .niceness(5) - .initialize(); - - assert!(matches!(res, Err(Error::InvalidConfig(_)))); -} diff --git a/tests/secondary_tokio_strategy.rs b/tests/secondary_tokio_strategy.rs deleted file mode 100644 index c0379f5..0000000 --- a/tests/secondary_tokio_strategy.rs +++ /dev/null @@ -1,81 +0,0 @@ -#[cfg(feature = "secondary_tokio_runtime")] -mod test { - use std::time::Duration; - - use futures_util::future::join_all; - use tokio::select; - - use compute_heavy_future_executor::{ - execute_compute_heavy_future, global_strategy, global_strategy_builder, CurrentStrategy, - ExecutorStrategy, - }; - - fn initialize() { - // we are racing all tests against the single oncelock - let _ = global_strategy_builder() - .max_concurrency(3) - .initialize_secondary_tokio_runtime(); - } - - #[tokio::test] - async fn secondary_tokio_runtime_strategy() { - initialize(); - - let future = async { 5 }; - - let res = execute_compute_heavy_future(future).await.unwrap(); - assert_eq!(res, 5); - - assert_eq!( - global_strategy(), - CurrentStrategy::Initialized(ExecutorStrategy::SecondaryTokioRuntime) - ); - } - - #[tokio::test] - async fn secondary_tokio_runtime_concurrency() { - initialize(); - - let start = std::time::Instant::now(); - - let mut futures = Vec::new(); - - for _ in 0..5 { - let future = async move { std::thread::sleep(Duration::from_millis(15)) }; - futures.push(execute_compute_heavy_future(future)); - } - - join_all(futures).await; - - let elapsed_millis = start.elapsed().as_millis(); - assert!(elapsed_millis < 50, "futures did not run concurrently"); - - assert!(elapsed_millis > 20, "futures exceeded max concurrency"); - } - - #[tokio::test] - async fn secondary_tokio_runtime_strategy_cancel_safe() { - initialize(); - - let (tx, mut rx) = tokio::sync::oneshot::channel::<()>(); - let future = async move { - { - tokio::time::sleep(Duration::from_secs(60)).await; - let _ = tx.send(()); - } - }; - - select! { - _ = tokio::time::sleep(Duration::from_millis(4)) => { }, - _ = execute_compute_heavy_future(future) => {} - } - - tokio::time::sleep(Duration::from_millis(8)).await; - - // future should have been cancelled when spawn compute heavy future was dropped - assert_eq!( - rx.try_recv(), - Err(tokio::sync::oneshot::error::TryRecvError::Closed) - ); - } -} diff --git a/tests/spawn_blocking_default.rs b/tests/spawn_blocking_default.rs index 3214f99..74e9f39 100644 --- a/tests/spawn_blocking_default.rs +++ b/tests/spawn_blocking_default.rs @@ -1,17 +1,22 @@ #[cfg(feature = "tokio")] #[tokio::test] async fn spawn_blocking_strategy() { + use std::time::Duration; + use compute_heavy_future_executor::{ - execute_compute_heavy_future, global_strategy, CurrentStrategy, ExecutorStrategy, + execute_sync, global_sync_strategy, ExecutorStrategy, GlobalStrategy, }; - let future = async { 5 }; + let closure = || { + std::thread::sleep(Duration::from_millis(15)); + 5 + }; - let res = execute_compute_heavy_future(future).await.unwrap(); + let res = execute_sync(closure).await.unwrap(); assert_eq!(res, 5); assert_eq!( - global_strategy(), - CurrentStrategy::Default(ExecutorStrategy::SpawnBlocking) + global_sync_strategy(), + GlobalStrategy::Default(ExecutorStrategy::SpawnBlocking) ); } diff --git a/tests/spawn_blocking_strategy.rs b/tests/spawn_blocking_strategy.rs index b44d05f..f0a3f90 100644 --- a/tests/spawn_blocking_strategy.rs +++ b/tests/spawn_blocking_strategy.rs @@ -3,16 +3,15 @@ mod test { use std::time::Duration; use futures_util::future::join_all; - use tokio::select; use compute_heavy_future_executor::{ - execute_compute_heavy_future, global_strategy, global_strategy_builder, CurrentStrategy, - ExecutorStrategy, + execute_sync, global_sync_strategy, global_sync_strategy_builder, ExecutorStrategy, + GlobalStrategy, }; fn initialize() { // we are racing all tests against the single oncelock - let _ = global_strategy_builder() + let _ = global_sync_strategy_builder() .max_concurrency(3) .initialize_spawn_blocking(); } @@ -21,14 +20,17 @@ mod test { async fn spawn_blocking_strategy() { initialize(); - let future = async { 5 }; + let closure = || { + std::thread::sleep(Duration::from_millis(15)); + 5 + }; - let res = execute_compute_heavy_future(future).await.unwrap(); + let res = execute_sync(closure).await.unwrap(); assert_eq!(res, 5); assert_eq!( - global_strategy(), - CurrentStrategy::Initialized(ExecutorStrategy::SpawnBlocking) + global_sync_strategy(), + GlobalStrategy::Initialized(ExecutorStrategy::SpawnBlocking) ); } @@ -39,42 +41,24 @@ mod test { let mut futures = Vec::new(); - for _ in 0..5 { - let future = async move { std::thread::sleep(Duration::from_millis(15)) }; - futures.push(execute_compute_heavy_future(future)); + let closure = || { + std::thread::sleep(Duration::from_millis(15)); + 5 + }; + + // note that we also are racing against concurrency from other tests in this module + for _ in 0..6 { + let future = async move { execute_sync(closure).await }; + futures.push(future); } + tokio::time::sleep(Duration::from_millis(5)).await; - join_all(futures).await; + let res = join_all(futures).await; + println!("{res:#?}"); let elapsed_millis = start.elapsed().as_millis(); assert!(elapsed_millis < 60, "futures did not run concurrently"); assert!(elapsed_millis > 20, "futures exceeded max concurrency"); } - - #[tokio::test] - async fn spawn_blocking_strategy_cancellable() { - initialize(); - - let (tx, mut rx) = tokio::sync::oneshot::channel::<()>(); - let future = async move { - { - tokio::time::sleep(Duration::from_secs(60)).await; - let _ = tx.send(()); - } - }; - - select! { - _ = tokio::time::sleep(Duration::from_millis(4)) => { }, - _ = execute_compute_heavy_future(future) => {} - } - - tokio::time::sleep(Duration::from_millis(8)).await; - - // future should have been cancelled when spawn compute heavy future was dropped - assert_eq!( - rx.try_recv(), - Err(tokio::sync::oneshot::error::TryRecvError::Closed) - ); - } } From e859f3880e3070d94aee3e98f9b3275a9af386cf Mon Sep 17 00:00:00 2001 From: jlizen Date: Sat, 28 Dec 2024 23:35:29 +0000 Subject: [PATCH 02/10] add stdError impl to Error type, add Send + Sync + `static bounds --- src/error.rs | 9 +++++++++ src/lib.rs | 1 + 2 files changed, 10 insertions(+) diff --git a/src/error.rs b/src/error.rs index f87db21..46b5a16 100644 --- a/src/error.rs +++ b/src/error.rs @@ -40,3 +40,12 @@ impl fmt::Display for Error { } } } + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::BoxError(err) => Some(err.source()?), + _ => None + } + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index dffb496..dc6a7bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -223,6 +223,7 @@ pub async fn execute_sync(f: F) -> Result where F: FnOnce() -> R + Send + 'static, R: Send + 'static, + Error: Send + Sync + 'static { let executor = get_global_sync_executor(); From 22c7240a9668f6378d356af269a8714e6d24e274 Mon Sep 17 00:00:00 2001 From: jlizen Date: Mon, 30 Dec 2024 19:24:59 +0000 Subject: [PATCH 03/10] rename to vacation, default to no-op in all cases, add enum to `execute_sync()` to specify likeliness to block, add helper for constructing good default tokio strategy --- Cargo.toml | 10 ++-- README.md | 61 ++++++++++--------- src/error.rs | 4 +- src/executor/mod.rs | 68 +++++++++++---------- src/lib.rs | 99 +++++++++++++++++-------------- tests/current_context_default.rs | 7 +-- tests/current_context_strategy.rs | 16 ++--- tests/custom_executor_simple.rs | 8 +-- tests/custom_executor_strategy.rs | 10 ++-- tests/multiple_initialize_err.rs | 2 +- tests/spawn_blocking_default.rs | 22 ------- tests/spawn_blocking_strategy.rs | 10 ++-- 12 files changed, 161 insertions(+), 156 deletions(-) delete mode 100644 tests/spawn_blocking_default.rs diff --git a/Cargo.toml b/Cargo.toml index 4390fcb..3c48553 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,15 @@ [package] -name = "compute-heavy-future-executor" +name = "vacation" version = "0.1.0" edition = "2021" license = "MIT" -repository = "https://github.com/jlizen/compute-heavy-future-executor" -homepage = "https://github.com/jlizen/compute-heavy-future-executor" +repository = "https://github.com/jlizen/vacation" +homepage = "https://github.com/jlizen/vacation" rust-version = "1.70" exclude = ["/.github", "/Exampless", "/scripts"] readme = "README.md" -description = "Executor patterns for handling compute-bounded calls inside async contexts." -categories = ["asynchronous"] +description = "Give your (runtime) aworkers a break!" +categories = ["asynchronous", "executor"] [features] default = ["tokio"] diff --git a/README.md b/README.md index da63af4..aec5366 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ -# compute-heavy-future-executor -Experimental crate that adds additional executor patterns to use with frequently blocking futures. +# vacation + Vacation: give your (runtime) aworkers a break! + +## Overview Today, when library authors are write async APIs, they don't have a good way to handle long-running sync segments. @@ -8,30 +10,22 @@ An application author can use selective handling such as `tokio::task::spawn_blo But, library authors generally don't have this flexibility. As, they generally want to be agnostic across runtime. Or, even if they are `tokio`-specific, they generally don't want to call `tokio::task::spawn_blocking()` as it is suboptimal without extra configuration (concurrency control) as well as highly opinionated to send the work across threads. -This library aims to solve this problem by providing libray authors a static, globally scoped strategy that they can delegate blocking sync work to without drawing any conclusions about handling. +This library solves this problem by providing libray authors a static, globally scoped strategy that they can delegate blocking sync work to without drawing any conclusions about handling. -And then, the applications using the library can either rely on the default strategy that this package provides, or tune them with their preferred approach. +And then, the applications using the library can tune handling based on their preferred approach. ## Usage - Library Authors -For library authors, it's as simple as adding a dependency enabling `compute-heavy-future-executor` (perhaps behind a feature flag). +For library authors, it's as simple as adding a dependency enabling `vacation` (perhaps behind a feature flag). -The below will default to 'current context' execution (ie non-op) unless the caller enables the tokio feature. ``` [dependencies] -compute-heavy-future-executor = { version = "0.1", default-features = false } -``` - -Meanwhile to be slightly more opinionated, the below will enable usage of `spawn_blocking` with concurrency control -by default unless the caller opts out: -``` -[dependencies] -compute-heavy-future-executor = { version = "0.1" } +vacation = { version = "0.1", default-features = false } ``` And then wrap any sync work by passing it as a closure to a global `execute_sync()` call: ``` -use compute_heavy_future_executor::execute_sync; +use vacation::execute_sync; fn sync_work(input: String)-> u8 { std::thread::sleep(std::time::Duration::from_secs(5)); @@ -47,35 +41,49 @@ pub async fn a_future_that_has_blocking_sync_work() -> u8 { ``` ## Usage - Application owners -Application authors can benefit from this crate with no application code changes, if you are using -a library that is itself using this crate. +Application authors will need to add this library as a a direct dependency in order to customize the execution strategy. +By default, the strategy is just a non-op. + +### Simple example + +``` +[dependencies] +// enables `tokio` feature by default => spawn_blocking strategy +vacation = { version = "0.1" } +``` + +And then call the `initialize_tokio()` helper that uses some sensible defaults: +``` +use vacation::initialize_tokio; + +#[tokio::main] +async fn main() { + initialize_tokio().unwrap(); +} +``` -If you want to customize the strategy beyond defaults, they can add -`compute-heavy-future-executor` to their dependencies: +### Rayon example +Or, you can add an alternate strategy, for instance a custom closure using Rayon. ``` [dependencies] -// enables tokio and therefore spawn_blocking strategy by default -compute-heavy-future-executor = { version = "0.1" } +vacation = { version = "0.1", default-features = false } // used for example with custom executor rayon = "1" ``` -And then configure your global strategy as desired. For instance, see below for usage of rayon -instead of `spawn_blocking()`. - ``` use std::sync::OnceLock; use rayon::ThreadPool; -use compute_heavy_future_executor::{ +use vacation::{ global_sync_strategy_builder, CustomExecutorSyncClosure, }; static THREADPOOL: OnceLock = OnceLock::new(); fn initialize_strategy() { - THREADPOOL.set(|| rayon::ThreadPoolBuilder::default().build().unwrap()); + THREADPOOL.setrayon::ThreadPoolBuilder::default().build().unwrap()); let custom_closure: CustomExecutorSyncClosure = Box::new(|f| Box::new(async move { Ok(THREADPOOL.get().unwrap().spawn(f)) })); @@ -85,5 +93,4 @@ fn initialize_strategy() { // and using a task queue .initialize_custom_executor(custom_closure).unwrap(); } - ``` \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index 46b5a16..7ebf8ca 100644 --- a/src/error.rs +++ b/src/error.rs @@ -45,7 +45,7 @@ impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Self::BoxError(err) => Some(err.source()?), - _ => None + _ => None, } } -} \ No newline at end of file +} diff --git a/src/executor/mod.rs b/src/executor/mod.rs index 8ff5940..1517745 100644 --- a/src/executor/mod.rs +++ b/src/executor/mod.rs @@ -8,7 +8,7 @@ use std::sync::OnceLock; use current_context::CurrentContextExecutor; use custom_executor::{CustomExecutor, CustomExecutorSyncClosure}; -use crate::{Error, ExecutorStrategy, GlobalStrategy}; +use crate::{global_sync_strategy_builder, Error, ExecutorStrategy, GlobalStrategy}; pub(crate) trait ExecuteSync { /// Accepts a sync function and processes it to completion. @@ -41,7 +41,7 @@ fn set_sync_strategy(strategy: SyncExecutor) -> Result<(), Error> { /// # Examples /// /// ``` -/// use compute_heavy_future_executor::{ +/// use vacation::{ /// global_sync_strategy, /// global_sync_strategy_builder, /// GlobalStrategy, @@ -97,22 +97,10 @@ pub(crate) enum SyncExecutor { impl Default for &SyncExecutor { fn default() -> Self { DEFAULT_COMPUTE_HEAVY_SYNC_EXECUTOR_STRATEGY.get_or_init(|| { - let core_count = num_cpus::get(); - - #[cfg(feature = "tokio")] - { - log::info!("Defaulting to SpawnBlocking strategy for compute-heavy future executor \ - with max concurrency of {core_count} until a strategy is initialized"); - - SyncExecutor::SpawnBlocking(spawn_blocking::SpawnBlockingExecutor::new(Some(core_count))) - } - - #[cfg(not(feature = "tokio"))] - { - log::warn!("Defaulting to CurrentContext (non-op) strategy for compute-heavy future executor \ - with max concurrency of {core_count} until a strategy is initialized."); - SyncExecutor::CurrentContext(CurrentContextExecutor::new(Some(core_count))) - } + log::warn!( + "Defaulting to CurrentContext (non-op) strategy for compute-heavy future executor" + ); + SyncExecutor::CurrentContext(CurrentContextExecutor::new(None)) }) } } @@ -143,13 +131,35 @@ impl From<&SyncExecutor> for ExecutorStrategy { } } +/// Initialize a set of sensible defaults for a tokio runtime: +/// +/// - SpawnBlocking strategy +/// - Max concurrency equal to the cpu core count. +/// +/// # Error +/// Returns an error if the global strategy is already initialized. +/// It can only be initialized once. +/// # Examples +/// +/// ``` +/// use vacation::initialize_tokio; +/// +/// # fn run() { +/// initialize_tokio().unwrap(); +/// # } +pub fn initialize_tokio() -> Result<(), Error> { + global_sync_strategy_builder() + .max_concurrency(num_cpus::get()) + .initialize_spawn_blocking() +} + /// A builder to replace the default sync executor strategy /// with a caller-provided strategy. /// /// # Examples /// /// ``` -/// use compute_heavy_future_executor::global_sync_strategy_builder; +/// use vacation::global_sync_strategy_builder; /// /// # fn run() { /// global_sync_strategy_builder() @@ -170,15 +180,15 @@ impl SyncExecutorBuilder { /// If this number is exceeded, the executor will wait to execute the /// input closure until a permit can be acquired. /// - /// ## Default - /// No maximum concurrency when strategies are manually built. + /// A good value tends to be the number of cpu cores on your machine. /// - /// For default strategies, the default concurrency limit will be the number of cpu cores. + /// ## Default + /// No maximum concurrency. /// /// # Examples /// /// ``` - /// use compute_heavy_future_executor::global_sync_strategy_builder; + /// use vacation::global_sync_strategy_builder; /// /// # fn run() { /// global_sync_strategy_builder() @@ -197,7 +207,7 @@ impl SyncExecutorBuilder { /// This is effectively a non-op wrapper that adds no special handling for the sync future /// besides optional concurrency control. /// - /// This is the default if the `tokio` feature is disabled (with concurrency equal to cpu core count). + /// This is the default strategy if nothing is initialized, with no max concurrency. /// /// # Error /// Returns an error if the global strategy is already initialized. @@ -206,7 +216,7 @@ impl SyncExecutorBuilder { /// # Examples /// /// ``` - /// use compute_heavy_future_executor::global_sync_strategy_builder; + /// use vacation::global_sync_strategy_builder; /// /// # async fn run() { /// global_sync_strategy_builder().initialize_current_context().unwrap(); @@ -223,8 +233,6 @@ impl SyncExecutorBuilder { /// /// Requires `tokio` feature. /// - /// This is the default strategy if `tokio` feature is enabled, with a concurrency limit equal to the number of cpu cores. - /// /// # Error /// Returns an error if the global strategy is already initialized. /// It can only be initialized once. @@ -232,7 +240,7 @@ impl SyncExecutorBuilder { /// # Examples /// /// ``` - /// use compute_heavy_future_executor::global_sync_strategy_builder; + /// use vacation::global_sync_strategy_builder; /// /// # async fn run() { /// // this will include no concurrency limit when explicitly initialized @@ -266,8 +274,8 @@ impl SyncExecutorBuilder { /// # Examples /// /// ``` - /// use compute_heavy_future_executor::global_sync_strategy_builder; - /// use compute_heavy_future_executor::CustomExecutorSyncClosure; + /// use vacation::global_sync_strategy_builder; + /// use vacation::CustomExecutorSyncClosure; /// /// # async fn run() { /// // caution: this will panic if used outside of tokio multithreaded runtime diff --git a/src/lib.rs b/src/lib.rs index dc6a7bd..eabfb4f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,9 +3,11 @@ #![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(test, deny(warnings))] -//! # compute-heavy-future-executor +//! # vacation //! -//! Experimental crate that adds additional executor patterns to use with frequently blocking futures. +//! `vacation``: give your (runtime) aworkers a break! +//! +//! ## Overview //! //! Today, when library authors are writing async APIs, they don't have a good way to handle long-running sync segments. //! @@ -14,35 +16,24 @@ //! But, library authors generally don't have this flexibility. As, they generally want to be agnostic across runtime. Or, even if they are `tokio`-specific, they generally don't want to call `tokio::task::spawn_blocking()` as it is //! suboptimal without extra configuration (concurrency control) as well as highly opinionated to send the work across threads. //! -//! This library aims to solve this problem by providing libray authors a static, globally scoped strategy that they can delegate blocking sync work to without drawing any conclusions about handling. -//! -//! And then, the applications using the library can either rely on the default strategy that this package provides, or tune them with their preferred approach. +//! This library solves this problem by providing libray authors a static, globally scoped strategy that they can delegate blocking sync work to without drawing any conclusions about handling. //! -//! The default strategy is to use `tokio::spawn_blocking` with a concurrency limit of the current cpu core count. -//! For non-tokio runtimes, or to use alternative threadpools such as `rayon`, alternative strategies are available. +//! And then, the applications using the library can tune handling based on their preferred approach. //! //! ## Usage - Library Authors -//! First add dependency enabling `compute-heavy-future-executor` (perhaps behind a feature flag). +//! First add dependency enabling `vacation` (perhaps behind a feature flag). //! //! The below will default to 'current context' sync execution (ie non-op) unless the caller enables the tokio feature. //! //! ```ignore //! [dependencies] -//! compute-heavy-future-executor = { version = "0.1", default-features = false } -//! ``` -//! -//! Meanwhile to be slightly more opinionated, the below will enable usage of `spawn_blocking` with concurrency control -//! by default unless the caller opts out: -//! -//! ```ignore -//! [dependencies] -//! compute-heavy-future-executor = { version = "0.1" } +//! vacation = { version = "0.1", default-features = false } //! ``` //! //! And then wrap any sync work by passing it as a closure to a global `execute_sync()` call: //! //! ``` -//! use compute_heavy_future_executor::execute_sync; +//! use vacation::{execute_sync, ChanceOfBlocking}; //! //! fn sync_work(input: String)-> u8 { //! std::thread::sleep(std::time::Duration::from_secs(5)); @@ -53,34 +44,50 @@ //! pub async fn a_future_that_has_blocking_sync_work() -> u8 { //! // relies on caller-specified strategy for translating execute_sync into a future that won't //! // block the current worker thread -//! execute_sync(move || { sync_work("foo".to_string()) }).await.unwrap() +//! execute_sync(move || { sync_work("foo".to_string()) }, ChanceOfBlocking::High).await.unwrap() //! } //! //! ``` //! //! ## Usage - Application owners -//! Application authors can benefit from this crate with no application code changes, if you are using -//! a library that is itself using this crate. +//! Application authors will need to add this library as a a direct dependency in order to customize the execution strategy. +//! By default, the strategy is just a non-op. +//! +//! You can customize the strategy using the [`SyncExecutorBuilder`] or [`initialize_tokio()`]. //! -//! If you want to customize the strategy beyond defaults, they can add -//! `compute-heavy-future-executor` to their dependencies: +//! ### Simple example //! //! ```ignore //! [dependencies] -//! // enables tokio and therefore spawn_blocking strategy by default unless `default-features = false` is provided -//! compute-heavy-future-executor = { version = "0.1" } +//! // enables `tokio` feature by default => spawn_blocking strategy +//! vacation = { version = "0.1" } +//! ``` +//! +//! And then call the `initialize_tokio()` helper that uses some sensible defaults: +//! ```ignore +//! use vacation::initialize_tokio; +//! +//! #[tokio::main] +//! async fn main() { +//! initialize_tokio().unwrap(); +//! } +//! ``` +//! +//! ### Rayon example +//! Or, you can add an alternate strategy, for instance a custom closure using Rayon. +//! +//! ```ignore +//! [dependencies] +//! vacation = { version = "0.1", default-features = false } //! // used for example with custom executor //! rayon = "1" //! ``` //! -//! And then configure your global strategy as desired. For instance, see below for usage of rayon -//! instead of `spawn_blocking()`. -//! //! ``` //! use std::sync::OnceLock; //! use rayon::ThreadPool; //! -//! use compute_heavy_future_executor::{ +//! use vacation::{ //! global_sync_strategy_builder, CustomExecutorSyncClosure, //! }; //! @@ -97,17 +104,14 @@ //! // and using a task queue //! .initialize_custom_executor(custom_closure).unwrap(); //! } -//! //! ``` //! //! ## Feature flags //! -//! Feature flags are used both to control default behaviors and to reduce the amount +//! Feature flags are used to reduce the amount //! of compiled code. //! -//! The `tokio` strategy is enabled by default. Disable it via `default-features = false` in your `Cargo.toml`. -//! -//! - `tokio`: Enables the `SpawnBlocking` sync strategy and sets it to be the default. +//! - `tokio`: Enables the `SpawnBlocking` sync strategy. //! mod concurrency_limit; @@ -116,7 +120,8 @@ mod executor; pub use error::Error; pub use executor::{ - custom_executor::CustomExecutorSyncClosure, global_sync_strategy, SyncExecutorBuilder, + custom_executor::CustomExecutorSyncClosure, global_sync_strategy, initialize_tokio, + SyncExecutorBuilder, }; use executor::{get_global_sync_executor, ExecuteSync, SyncExecutor}; @@ -138,7 +143,7 @@ pub enum GlobalStrategy { /// # Examples /// /// ``` -/// use compute_heavy_future_executor::{ +/// use vacation::{ /// global_sync_strategy, /// global_sync_strategy_builder, /// GlobalStrategy, @@ -181,7 +186,7 @@ pub enum ExecutorStrategy { /// # Examples /// /// ``` -/// use compute_heavy_future_executor::global_sync_strategy_builder; +/// use vacation::global_sync_strategy_builder; /// # fn run() { /// global_sync_strategy_builder().max_concurrency(3).initialize_spawn_blocking().unwrap(); /// # } @@ -191,13 +196,19 @@ pub fn global_sync_strategy_builder() -> SyncExecutorBuilder { SyncExecutorBuilder::default() } +/// Likelihood of the provided closure blocking for a significant period of time. +/// Will eventually be used to customize strategies with more granularity. +#[derive(Debug)] +pub enum ChanceOfBlocking { + /// Very likely to block, use primary sync strategy + High, +} + /// Send a sync closure to the configured or default global compute-heavy executor, and wait on its output. /// /// # Strategy selection /// -/// If no strategy is configured, this library will fall back to the following defaults: -/// - no `tokio`` feature - current context w/ max concurrency = cpu core count -/// - `tokio` feature - spawn blocking w/ max concurrency = cpu core count +/// If no strategy is configured, this library will fall back to a non-op `CurrentContext` strategy. /// /// You can override these defaults by initializing a strategy via [`global_sync_strategy_builder()`] /// and [`SyncExecutorBuilder`]. @@ -206,24 +217,24 @@ pub fn global_sync_strategy_builder() -> SyncExecutorBuilder { /// /// ``` /// # async fn run() { -/// use compute_heavy_future_executor::execute_sync; +/// use vacation::{execute_sync, ChanceOfBlocking}; /// /// let closure = || { /// std::thread::sleep(std::time::Duration::from_secs(1)); /// 5 /// }; /// -/// let res = execute_sync(closure).await.unwrap(); +/// let res = execute_sync(closure, ChanceOfBlocking::High).await.unwrap(); /// assert_eq!(res, 5); /// # } /// /// ``` /// -pub async fn execute_sync(f: F) -> Result +pub async fn execute_sync(f: F, _chance_of_blocking: ChanceOfBlocking) -> Result where F: FnOnce() -> R + Send + 'static, R: Send + 'static, - Error: Send + Sync + 'static + Error: Send + Sync + 'static, { let executor = get_global_sync_executor(); diff --git a/tests/current_context_default.rs b/tests/current_context_default.rs index 6e4f2ff..001100e 100644 --- a/tests/current_context_default.rs +++ b/tests/current_context_default.rs @@ -1,10 +1,9 @@ -#[cfg(not(feature = "tokio"))] #[tokio::test] async fn default_to_current_context() { use std::time::Duration; - use compute_heavy_future_executor::{ - execute_sync, global_sync_strategy, ExecutorStrategy, GlobalStrategy, + use vacation::{ + execute_sync, global_sync_strategy, ChanceOfBlocking, ExecutorStrategy, GlobalStrategy, }; // this is a tokio test but we haven't enabled the tokio config flag @@ -14,7 +13,7 @@ async fn default_to_current_context() { 5 }; - let res = execute_sync(closure).await.unwrap(); + let res = execute_sync(closure, ChanceOfBlocking::High).await.unwrap(); assert_eq!(res, 5); assert_eq!( diff --git a/tests/current_context_strategy.rs b/tests/current_context_strategy.rs index eb824d6..09ae5b1 100644 --- a/tests/current_context_strategy.rs +++ b/tests/current_context_strategy.rs @@ -1,10 +1,10 @@ use std::time::Duration; -use compute_heavy_future_executor::{ - execute_sync, global_sync_strategy, global_sync_strategy_builder, ExecutorStrategy, - GlobalStrategy, -}; use futures_util::future::join_all; +use vacation::{ + execute_sync, global_sync_strategy, global_sync_strategy_builder, ChanceOfBlocking, + ExecutorStrategy, GlobalStrategy, +}; fn initialize() { // we are racing all tests against the single oncelock @@ -22,7 +22,7 @@ async fn current_context_strategy() { 5 }; - let res = execute_sync(closure).await.unwrap(); + let res = execute_sync(closure, ChanceOfBlocking::High).await.unwrap(); assert_eq!(res, 5); assert_eq!( @@ -47,8 +47,10 @@ async fn current_context_concurrency() { // note that we also are racing against concurrency from other tests in this module for _ in 0..6 { // we need to spawn tasks since otherwise we'll just block the current worker thread - let future = - async move { tokio::task::spawn(async move { execute_sync(closure).await }).await }; + let future = async move { + tokio::task::spawn(async move { execute_sync(closure, ChanceOfBlocking::High).await }) + .await + }; futures.push(future); } tokio::time::sleep(Duration::from_millis(5)).await; diff --git a/tests/custom_executor_simple.rs b/tests/custom_executor_simple.rs index 255f30f..4561849 100644 --- a/tests/custom_executor_simple.rs +++ b/tests/custom_executor_simple.rs @@ -1,8 +1,8 @@ use std::time::Duration; -use compute_heavy_future_executor::{ - execute_sync, global_sync_strategy, global_sync_strategy_builder, CustomExecutorSyncClosure, - ExecutorStrategy, GlobalStrategy, +use vacation::{ + execute_sync, global_sync_strategy, global_sync_strategy_builder, ChanceOfBlocking, + CustomExecutorSyncClosure, ExecutorStrategy, GlobalStrategy, }; #[tokio::test] @@ -23,7 +23,7 @@ async fn custom_executor_simple() { 5 }; - let res = execute_sync(closure).await.unwrap(); + let res = execute_sync(closure, ChanceOfBlocking::High).await.unwrap(); assert_eq!(res, 5); assert_eq!( diff --git a/tests/custom_executor_strategy.rs b/tests/custom_executor_strategy.rs index f83102a..19dd923 100644 --- a/tests/custom_executor_strategy.rs +++ b/tests/custom_executor_strategy.rs @@ -1,10 +1,10 @@ use std::{sync::OnceLock, time::Duration}; -use compute_heavy_future_executor::{ - execute_sync, global_sync_strategy_builder, CustomExecutorSyncClosure, -}; use futures_util::future::join_all; use rayon::ThreadPool; +use vacation::{ + execute_sync, global_sync_strategy_builder, ChanceOfBlocking, CustomExecutorSyncClosure, +}; static THREADPOOL: OnceLock = OnceLock::new(); @@ -32,7 +32,7 @@ async fn custom_executor_strategy() { 5 }; - let res = execute_sync(closure).await.unwrap(); + let res = execute_sync(closure, ChanceOfBlocking::High).await.unwrap(); assert_eq!(res, 5); } @@ -52,7 +52,7 @@ async fn custom_executor_concurrency() { // note that we also are racing against concurrency from other tests in this module for _ in 0..6 { - futures.push(execute_sync(closure)); + futures.push(execute_sync(closure, ChanceOfBlocking::High)); } join_all(futures).await; diff --git a/tests/multiple_initialize_err.rs b/tests/multiple_initialize_err.rs index 6e9606a..d6a5c84 100644 --- a/tests/multiple_initialize_err.rs +++ b/tests/multiple_initialize_err.rs @@ -1,4 +1,4 @@ -use compute_heavy_future_executor::{global_sync_strategy_builder, Error}; +use vacation::{global_sync_strategy_builder, Error}; #[test] fn multiple_initialize_err() { diff --git a/tests/spawn_blocking_default.rs b/tests/spawn_blocking_default.rs deleted file mode 100644 index 74e9f39..0000000 --- a/tests/spawn_blocking_default.rs +++ /dev/null @@ -1,22 +0,0 @@ -#[cfg(feature = "tokio")] -#[tokio::test] -async fn spawn_blocking_strategy() { - use std::time::Duration; - - use compute_heavy_future_executor::{ - execute_sync, global_sync_strategy, ExecutorStrategy, GlobalStrategy, - }; - - let closure = || { - std::thread::sleep(Duration::from_millis(15)); - 5 - }; - - let res = execute_sync(closure).await.unwrap(); - assert_eq!(res, 5); - - assert_eq!( - global_sync_strategy(), - GlobalStrategy::Default(ExecutorStrategy::SpawnBlocking) - ); -} diff --git a/tests/spawn_blocking_strategy.rs b/tests/spawn_blocking_strategy.rs index f0a3f90..42196b3 100644 --- a/tests/spawn_blocking_strategy.rs +++ b/tests/spawn_blocking_strategy.rs @@ -4,9 +4,9 @@ mod test { use futures_util::future::join_all; - use compute_heavy_future_executor::{ - execute_sync, global_sync_strategy, global_sync_strategy_builder, ExecutorStrategy, - GlobalStrategy, + use vacation::{ + execute_sync, global_sync_strategy, global_sync_strategy_builder, ChanceOfBlocking, + ExecutorStrategy, GlobalStrategy, }; fn initialize() { @@ -25,7 +25,7 @@ mod test { 5 }; - let res = execute_sync(closure).await.unwrap(); + let res = execute_sync(closure, ChanceOfBlocking::High).await.unwrap(); assert_eq!(res, 5); assert_eq!( @@ -48,7 +48,7 @@ mod test { // note that we also are racing against concurrency from other tests in this module for _ in 0..6 { - let future = async move { execute_sync(closure).await }; + let future = async move { execute_sync(closure, ChanceOfBlocking::High).await }; futures.push(future); } tokio::time::sleep(Duration::from_millis(5)).await; From bbb5f759deb199b3f51d243d90889dfe6be22f40 Mon Sep 17 00:00:00 2001 From: jlizen Date: Mon, 30 Dec 2024 20:36:54 +0000 Subject: [PATCH 04/10] clean up Cargo.toml --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 3c48553..1b00146 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ license = "MIT" repository = "https://github.com/jlizen/vacation" homepage = "https://github.com/jlizen/vacation" rust-version = "1.70" -exclude = ["/.github", "/Exampless", "/scripts"] +exclude = ["/.github", "/examples", "/scripts"] readme = "README.md" description = "Give your (runtime) aworkers a break!" categories = ["asynchronous", "executor"] From 4409d5328a4c239f3a3f852a604993605bac6808 Mon Sep 17 00:00:00 2001 From: jlizen Date: Mon, 30 Dec 2024 21:59:56 +0000 Subject: [PATCH 05/10] lots of renaming, switch to typestate builder pattern with final install() method, add optional handle to spawn_blocking constructor --- Cargo.toml | 2 +- README.md | 28 +- src/executor/custom_executor.rs | 16 +- ...current_context.rs => execute_directly.rs} | 10 +- src/executor/mod.rs | 283 +++++++++++------- src/executor/spawn_blocking.rs | 14 +- src/lib.rs | 87 +++--- tests/current_context_default.rs | 12 +- tests/current_context_strategy.rs | 20 +- tests/custom_executor_simple.rs | 16 +- tests/custom_executor_strategy.rs | 15 +- tests/multiple_initialize_err.rs | 8 +- tests/spawn_blocking_strategy.rs | 13 +- 13 files changed, 284 insertions(+), 240 deletions(-) rename src/executor/{current_context.rs => execute_directly.rs} (70%) diff --git a/Cargo.toml b/Cargo.toml index 1b00146..76fb1da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ homepage = "https://github.com/jlizen/vacation" rust-version = "1.70" exclude = ["/.github", "/examples", "/scripts"] readme = "README.md" -description = "Give your (runtime) aworkers a break!" +description = "Give your (runtime) workers a break!" categories = ["asynchronous", "executor"] [features] diff --git a/README.md b/README.md index aec5366..e719399 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # vacation - Vacation: give your (runtime) aworkers a break! + Vacation: Give your (runtime) workers a break! ## Overview -Today, when library authors are write async APIs, they don't have a good way to handle long-running sync segments. +Today, when library authors write async APIs, they don't have a good way to handle long-running sync segments. An application author can use selective handling such as `tokio::task::spawn_blocking()` along with concurrency control to delegate sync segments to blocking threads. Or, they might send the work to a `rayon` threadpool. @@ -22,20 +22,18 @@ For library authors, it's as simple as adding a dependency enabling `vacation` ( vacation = { version = "0.1", default-features = false } ``` -And then wrap any sync work by passing it as a closure to a global `execute_sync()` call: +And then wrap any sync work by passing it as a closure to a global `execute()` call: ``` -use vacation::execute_sync; - fn sync_work(input: String)-> u8 { std::thread::sleep(std::time::Duration::from_secs(5)); println!("{input}"); 5 } pub async fn a_future_that_has_blocking_sync_work() -> u8 { - // relies on caller-specified strategy for translating execute_sync into a future that won't + // relies on caller-specified strategy for translating execute into a future that won't // block the current worker thread - execute_sync(move || { sync_work("foo".to_string()) }).await.unwrap() + vacation::execute(move || { sync_work("foo".to_string()) }, vacation::ChanceOfBlocking::High).await.unwrap() } ``` @@ -52,13 +50,11 @@ By default, the strategy is just a non-op. vacation = { version = "0.1" } ``` -And then call the `initialize_tokio()` helper that uses some sensible defaults: +And then call the `install_tokio_strategy()` helper that uses some sensible defaults: ``` -use vacation::initialize_tokio; - #[tokio::main] async fn main() { - initialize_tokio().unwrap(); + vacation::install_tokio_strategy().unwrap(); } ``` @@ -76,21 +72,17 @@ rayon = "1" use std::sync::OnceLock; use rayon::ThreadPool; -use vacation::{ - global_sync_strategy_builder, CustomExecutorSyncClosure, -}; - static THREADPOOL: OnceLock = OnceLock::new(); fn initialize_strategy() { THREADPOOL.setrayon::ThreadPoolBuilder::default().build().unwrap()); - let custom_closure: CustomExecutorSyncClosure = + let custom_closure: CustomClosure = Box::new(|f| Box::new(async move { Ok(THREADPOOL.get().unwrap().spawn(f)) })); - global_sync_strategy_builder() + vacation::init() // probably no need for max concurrency as rayon already is defaulting to a thread per core // and using a task queue - .initialize_custom_executor(custom_closure).unwrap(); + .custom_executor(custom_closure).install().unwrap(); } ``` \ No newline at end of file diff --git a/src/executor/custom_executor.rs b/src/executor/custom_executor.rs index f1d7bc2..6512b7f 100644 --- a/src/executor/custom_executor.rs +++ b/src/executor/custom_executor.rs @@ -2,12 +2,12 @@ use std::future::Future; use crate::{concurrency_limit::ConcurrencyLimit, error::Error}; -use super::ExecuteSync; +use super::Execute; /// A closure that accepts an arbitrary sync function and returns a future that executes it. /// The Custom Executor will implicitly wrap the input function in a oneshot /// channel to erase its input/output type. -pub type CustomExecutorSyncClosure = Box< +pub type CustomClosure = Box< dyn Fn( Box, ) -> Box< @@ -18,13 +18,13 @@ pub type CustomExecutorSyncClosure = Box< + Sync, >; -pub(crate) struct CustomExecutor { - closure: CustomExecutorSyncClosure, +pub(crate) struct Custom { + closure: CustomClosure, concurrency_limit: ConcurrencyLimit, } -impl CustomExecutor { - pub(crate) fn new(closure: CustomExecutorSyncClosure, max_concurrency: Option) -> Self { +impl Custom { + pub(crate) fn new(closure: CustomClosure, max_concurrency: Option) -> Self { Self { closure, concurrency_limit: ConcurrencyLimit::new(max_concurrency), @@ -32,11 +32,11 @@ impl CustomExecutor { } } -impl ExecuteSync for CustomExecutor { +impl Execute for Custom { // the compiler correctly is pointing out that the custom closure isn't guaranteed to call f. // but, we leave that to the implementer to guarantee since we are limited by working with static signatures #[allow(unused_variables)] - async fn execute_sync(&self, f: F) -> Result + async fn execute(&self, f: F) -> Result where F: FnOnce() -> R + Send + 'static, R: Send + 'static, diff --git a/src/executor/current_context.rs b/src/executor/execute_directly.rs similarity index 70% rename from src/executor/current_context.rs rename to src/executor/execute_directly.rs index d0f1faf..96ce5a0 100644 --- a/src/executor/current_context.rs +++ b/src/executor/execute_directly.rs @@ -1,12 +1,12 @@ use crate::{concurrency_limit::ConcurrencyLimit, error::Error}; -use super::ExecuteSync; +use super::Execute; -pub(crate) struct CurrentContextExecutor { +pub(crate) struct ExecuteDirectly { concurrency_limit: ConcurrencyLimit, } -impl CurrentContextExecutor { +impl ExecuteDirectly { pub(crate) fn new(max_concurrency: Option) -> Self { Self { concurrency_limit: ConcurrencyLimit::new(max_concurrency), @@ -14,8 +14,8 @@ impl CurrentContextExecutor { } } -impl ExecuteSync for CurrentContextExecutor { - async fn execute_sync(&self, f: F) -> Result +impl Execute for ExecuteDirectly { + async fn execute(&self, f: F) -> Result where F: FnOnce() -> R + Send + 'static, R: Send + 'static, diff --git a/src/executor/mod.rs b/src/executor/mod.rs index 1517745..63fe447 100644 --- a/src/executor/mod.rs +++ b/src/executor/mod.rs @@ -1,33 +1,31 @@ -pub(crate) mod current_context; pub(crate) mod custom_executor; +pub(crate) mod execute_directly; #[cfg(feature = "tokio")] pub(crate) mod spawn_blocking; use std::sync::OnceLock; -use current_context::CurrentContextExecutor; -use custom_executor::{CustomExecutor, CustomExecutorSyncClosure}; +use custom_executor::{Custom, CustomClosure}; +use execute_directly::ExecuteDirectly; -use crate::{global_sync_strategy_builder, Error, ExecutorStrategy, GlobalStrategy}; +use crate::{init, Error, ExecutorStrategy, GlobalStrategy}; -pub(crate) trait ExecuteSync { +pub(crate) trait Execute { /// Accepts a sync function and processes it to completion. - async fn execute_sync(&self, f: F) -> Result + async fn execute(&self, f: F) -> Result where F: FnOnce() -> R + Send + 'static, R: Send + 'static; } -fn set_sync_strategy(strategy: SyncExecutor) -> Result<(), Error> { - COMPUTE_HEAVY_SYNC_EXECUTOR_STRATEGY +fn set_global_strategy(strategy: Executor) -> Result<(), Error> { + GLOBAL_EXECUTOR_STRATEGY .set(strategy) - .map_err(|_| { - Error::AlreadyInitialized(COMPUTE_HEAVY_SYNC_EXECUTOR_STRATEGY.get().unwrap().into()) - })?; + .map_err(|_| Error::AlreadyInitialized(GLOBAL_EXECUTOR_STRATEGY.get().unwrap().into()))?; log::info!( - "initialized compute-heavy future executor strategy - {:#?}", - global_sync_strategy() + "initialized vacation synchronous executor strategy - {:#?}", + global_strategy() ); Ok(()) @@ -36,14 +34,13 @@ fn set_sync_strategy(strategy: SyncExecutor) -> Result<(), Error> { /// Get the currently initialized sync strategy, /// or the default strategy for the current feature in case no strategy has been loaded. /// -/// See [`SyncExecutorBuilder`] for details on strategies. +/// See [`ExecutorBuilder`] for details on strategies. /// /// # Examples /// /// ``` /// use vacation::{ -/// global_sync_strategy, -/// global_sync_strategy_builder, +/// global_strategy, /// GlobalStrategy, /// ExecutorStrategy /// }; @@ -51,82 +48,83 @@ fn set_sync_strategy(strategy: SyncExecutor) -> Result<(), Error> { /// # fn run() { /// /// #[cfg(feature = "tokio")] -/// assert_eq!(global_sync_strategy(), GlobalStrategy::Default(ExecutorStrategy::SpawnBlocking)); +/// assert_eq!(global_strategy(), GlobalStrategy::Default(ExecutorStrategy::SpawnBlocking)); /// /// #[cfg(not(feature = "tokio"))] -/// assert_eq!(global_sync_strategy(), GlobalStrategy::Default(ExecutorStrategy::CurrentContext)); +/// assert_eq!(global_strategy(), GlobalStrategy::Default(ExecutorStrategy::ExecuteDirectly)); /// -/// global_sync_strategy_builder() -/// .initialize_current_context() +/// vacation::init() +/// .execute_directly() +/// .install() /// .unwrap(); /// -/// assert_eq!(global_sync_strategy(), GlobalStrategy::Initialized(ExecutorStrategy::CurrentContext)); +/// assert_eq!(global_strategy(), GlobalStrategy::Initialized(ExecutorStrategy::ExecuteDirectly)); /// /// # } /// ``` -pub fn global_sync_strategy() -> GlobalStrategy { - match COMPUTE_HEAVY_SYNC_EXECUTOR_STRATEGY.get() { +pub fn global_strategy() -> GlobalStrategy { + match GLOBAL_EXECUTOR_STRATEGY.get() { Some(strategy) => GlobalStrategy::Initialized(strategy.into()), - None => GlobalStrategy::Default(<&SyncExecutor>::default().into()), + None => GlobalStrategy::Default(<&Executor>::default().into()), } } -pub(crate) fn get_global_sync_executor() -> &'static SyncExecutor { - COMPUTE_HEAVY_SYNC_EXECUTOR_STRATEGY +pub(crate) fn get_global_executor() -> &'static Executor { + GLOBAL_EXECUTOR_STRATEGY .get() - .unwrap_or_else(|| <&SyncExecutor>::default()) + .unwrap_or_else(|| <&Executor>::default()) } /// The stored strategy used to spawn compute-heavy futures. -static COMPUTE_HEAVY_SYNC_EXECUTOR_STRATEGY: OnceLock = OnceLock::new(); +static GLOBAL_EXECUTOR_STRATEGY: OnceLock = OnceLock::new(); /// The fallback strategy used in case no strategy is explicitly set -static DEFAULT_COMPUTE_HEAVY_SYNC_EXECUTOR_STRATEGY: OnceLock = OnceLock::new(); +static DEFAULT_GLOBAL_EXECUTOR_STRATEGY: OnceLock = OnceLock::new(); #[non_exhaustive] -pub(crate) enum SyncExecutor { +pub(crate) enum Executor { /// A non-op strategy that runs the function in the current context - CurrentContext(current_context::CurrentContextExecutor), + ExecuteDirectly(execute_directly::ExecuteDirectly), /// User-provided closure - CustomExecutor(custom_executor::CustomExecutor), + Custom(custom_executor::Custom), /// tokio task::spawn_blocking #[cfg(feature = "tokio")] - SpawnBlocking(spawn_blocking::SpawnBlockingExecutor), + SpawnBlocking(spawn_blocking::SpawnBlocking), } -impl Default for &SyncExecutor { +impl Default for &Executor { fn default() -> Self { - DEFAULT_COMPUTE_HEAVY_SYNC_EXECUTOR_STRATEGY.get_or_init(|| { + DEFAULT_GLOBAL_EXECUTOR_STRATEGY.get_or_init(|| { log::warn!( - "Defaulting to CurrentContext (non-op) strategy for compute-heavy future executor" + "Defaulting to ExecuteDirectly (non-op) strategy for vacation compute-heavy future executor" ); - SyncExecutor::CurrentContext(CurrentContextExecutor::new(None)) + Executor::ExecuteDirectly(ExecuteDirectly::new(None)) }) } } -impl ExecuteSync for SyncExecutor { - async fn execute_sync(&self, f: F) -> Result +impl Execute for Executor { + async fn execute(&self, f: F) -> Result where F: FnOnce() -> R + Send + 'static, R: Send + 'static, { match self { - SyncExecutor::CurrentContext(executor) => executor.execute_sync(f).await, - SyncExecutor::CustomExecutor(executor) => executor.execute_sync(f).await, + Executor::ExecuteDirectly(executor) => executor.execute(f).await, + Executor::Custom(executor) => executor.execute(f).await, #[cfg(feature = "tokio")] - SyncExecutor::SpawnBlocking(executor) => executor.execute_sync(f).await, + Executor::SpawnBlocking(executor) => executor.execute(f).await, } } } -impl From<&SyncExecutor> for ExecutorStrategy { - fn from(value: &SyncExecutor) -> Self { +impl From<&Executor> for ExecutorStrategy { + fn from(value: &Executor) -> Self { match value { - SyncExecutor::CurrentContext(_) => Self::CurrentContext, - SyncExecutor::CustomExecutor(_) => Self::CustomExecutor, + Executor::ExecuteDirectly(_) => Self::ExecuteDirectly, + Executor::Custom(_) => Self::Custom, #[cfg(feature = "tokio")] - SyncExecutor::SpawnBlocking(_) => Self::SpawnBlocking, + Executor::SpawnBlocking(_) => Self::SpawnBlocking, } } } @@ -136,21 +134,23 @@ impl From<&SyncExecutor> for ExecutorStrategy { /// - SpawnBlocking strategy /// - Max concurrency equal to the cpu core count. /// +/// Only available with the `tokio` feature. +/// /// # Error /// Returns an error if the global strategy is already initialized. /// It can only be initialized once. /// # Examples /// /// ``` -/// use vacation::initialize_tokio; -/// /// # fn run() { -/// initialize_tokio().unwrap(); +/// vacation::install_tokio_strategy().unwrap(); /// # } -pub fn initialize_tokio() -> Result<(), Error> { - global_sync_strategy_builder() +#[cfg(feature = "tokio")] +pub fn install_tokio_strategy() -> Result<(), Error> { + init() .max_concurrency(num_cpus::get()) - .initialize_spawn_blocking() + .spawn_blocking() + .install() } /// A builder to replace the default sync executor strategy @@ -159,22 +159,51 @@ pub fn initialize_tokio() -> Result<(), Error> { /// # Examples /// /// ``` -/// use vacation::global_sync_strategy_builder; -/// /// # fn run() { -/// global_sync_strategy_builder() +/// vacation::init() /// .max_concurrency(10) -/// .initialize_current_context() +/// .execute_directly() +/// .install() /// .unwrap(); /// # } /// ``` #[must_use = "doesn't do anything unless used"] -#[derive(Default, Debug)] -pub struct SyncExecutorBuilder { - max_concurrency: Option, +#[derive(Default)] +pub struct ExecutorBuilder { + pub(crate) max_concurrency: Option, + pub(crate) strategy: Strategy, +} + +impl std::fmt::Debug for ExecutorBuilder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ExecutorBuilder") + .field("max_concurrency", &self.max_concurrency) + .field("strategy", &self.strategy) + .finish() + } +} + +#[derive(Debug)] +pub struct NoStrategy; +pub enum HasStrategy { + ExecuteDirectly, + #[cfg(feature = "tokio")] + SpawnBlocking(tokio::runtime::Handle), + Custom(CustomClosure), } -impl SyncExecutorBuilder { +impl std::fmt::Debug for HasStrategy { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::ExecuteDirectly => write!(f, "ExecuteDirectly"), + #[cfg(feature = "tokio")] + Self::SpawnBlocking(handle) => f.debug_tuple("SpawnBlocking").field(handle).finish(), + Self::Custom(_) => f.debug_tuple("Custom").finish(), + } + } +} + +impl ExecutorBuilder { /// Set the max number of simultaneous futures processed by this executor. /// /// If this number is exceeded, the executor will wait to execute the @@ -188,20 +217,20 @@ impl SyncExecutorBuilder { /// # Examples /// /// ``` - /// use vacation::global_sync_strategy_builder; - /// /// # fn run() { - /// global_sync_strategy_builder() + /// vacation::init() /// .max_concurrency(10) - /// .initialize_current_context() + /// .execute_directly() + /// .install() /// .unwrap(); /// # } + #[must_use = "doesn't do anything unless used with a strategy"] pub fn max_concurrency(self, max_task_concurrency: usize) -> Self { Self { max_concurrency: Some(max_task_concurrency), + ..self } } - /// Initializes a new (non-op) global strategy to wait in the current context. /// /// This is effectively a non-op wrapper that adds no special handling for the sync future @@ -209,23 +238,19 @@ impl SyncExecutorBuilder { /// /// This is the default strategy if nothing is initialized, with no max concurrency. /// - /// # Error - /// Returns an error if the global strategy is already initialized. - /// It can only be initialized once. - /// /// # Examples /// /// ``` - /// use vacation::global_sync_strategy_builder; - /// /// # async fn run() { - /// global_sync_strategy_builder().initialize_current_context().unwrap(); + /// vacation::init().execute_directly().install().unwrap(); /// # } /// ``` - pub fn initialize_current_context(self) -> Result<(), Error> { - let strategy = - SyncExecutor::CurrentContext(CurrentContextExecutor::new(self.max_concurrency)); - set_sync_strategy(strategy) + #[must_use = "doesn't do anything unless install()-ed"] + pub fn execute_directly(self) -> ExecutorBuilder { + ExecutorBuilder:: { + strategy: HasStrategy::ExecuteDirectly, + max_concurrency: self.max_concurrency, + } } /// Initializes a new global strategy to execute input closures by blocking on them inside the @@ -233,30 +258,55 @@ impl SyncExecutorBuilder { /// /// Requires `tokio` feature. /// - /// # Error - /// Returns an error if the global strategy is already initialized. - /// It can only be initialized once. - /// /// # Examples /// /// ``` - /// use vacation::global_sync_strategy_builder; - /// /// # async fn run() { /// // this will include no concurrency limit when explicitly initialized /// // without a call to [`concurrency_limit()`] - /// global_sync_strategy_builder().initialize_spawn_blocking().unwrap(); + /// vacation::init().spawn_blocking().install().unwrap(); /// # } /// ``` /// [`spawn_blocking`]: tokio::task::spawn_blocking /// + #[must_use = "doesn't do anything unless install()-ed"] #[cfg(feature = "tokio")] - pub fn initialize_spawn_blocking(self) -> Result<(), Error> { - use spawn_blocking::SpawnBlockingExecutor; + pub fn spawn_blocking(self) -> ExecutorBuilder { + ExecutorBuilder:: { + strategy: HasStrategy::SpawnBlocking(tokio::runtime::Handle::current()), + max_concurrency: self.max_concurrency, + } + } - let strategy = - SyncExecutor::SpawnBlocking(SpawnBlockingExecutor::new(self.max_concurrency)); - set_sync_strategy(strategy) + /// Initializes a new global strategy to execute input closures by blocking on them inside the + /// tokio blocking threadpool via Tokio's [`spawn_blocking`], on a specific runtime. + /// + /// Uses the provided tokio runtime handle to decide which runtime to `spawn_blocking` onto. + /// + /// Requires `tokio` feature. + /// + /// # Examples + /// + /// ``` + /// # async fn run() { + /// // this will include no concurrency limit when explicitly initialized + /// // without a call to [`concurrency_limit()`] + /// let handle = tokio::runtime::Handle::current(); + /// vacation::init().spawn_blocking_with_handle(handle).install().unwrap(); + /// # } + /// ``` + /// [`spawn_blocking`]: tokio::task::spawn_blocking + /// + #[must_use = "doesn't do anything unless install()-ed"] + #[cfg(feature = "tokio")] + pub fn spawn_blocking_with_handle( + self, + runtime_handle: tokio::runtime::Handle, + ) -> ExecutorBuilder { + ExecutorBuilder:: { + strategy: HasStrategy::SpawnBlocking(runtime_handle), + max_concurrency: self.max_concurrency, + } } /// Accepts a closure that will accept an arbitrary closure and call it. The input @@ -267,37 +317,62 @@ impl SyncExecutorBuilder { /// For instance, you could delegate to a [`Rayon threadpool`] or use Tokio's [`block_in_place`]. /// See `tests/custom_executor_strategy.rs` for a `Rayon` example. /// - /// # Error - /// Returns an error if the global strategy is already initialized. - /// It can only be initialized once. /// /// # Examples /// /// ``` - /// use vacation::global_sync_strategy_builder; - /// use vacation::CustomExecutorSyncClosure; - /// /// # async fn run() { /// // caution: this will panic if used outside of tokio multithreaded runtime /// // this is a kind of dangerous strategy, read up on `block in place's` limitations /// // before using this approach - /// let closure: CustomExecutorSyncClosure = Box::new(|f| { + /// let closure: vacation::CustomClosure = Box::new(|f| { /// Box::new(async move { Ok(tokio::task::block_in_place(move || f())) }) /// }); /// - /// global_sync_strategy_builder().initialize_custom_executor(closure).unwrap(); + /// vacation::init().custom_executor(closure).install().unwrap(); /// # } /// /// ``` /// /// [`Rayon threadpool`]: https://docs.rs/rayon/latest/rayon/struct.ThreadPool.html /// [`block_in_place`]: https://docs.rs/tokio/latest/tokio/task/fn.block_in_place.html - pub fn initialize_custom_executor( - self, - closure: CustomExecutorSyncClosure, - ) -> Result<(), Error> { - let strategy = - SyncExecutor::CustomExecutor(CustomExecutor::new(closure, self.max_concurrency)); - set_sync_strategy(strategy) + #[must_use = "doesn't do anything unless install()-ed"] + pub fn custom_executor(self, closure: CustomClosure) -> ExecutorBuilder { + ExecutorBuilder:: { + strategy: HasStrategy::Custom(closure), + max_concurrency: self.max_concurrency, + } + } +} + +impl ExecutorBuilder { + /// Initializes the loaded configuration and stores it as a global strategy. + /// + /// # Error + /// Returns an error if the global strategy is already initialized. + /// It can only be initialized once. + /// + /// /// # Examples + /// + /// ``` + /// # async fn run() { + /// vacation::init().execute_directly().install().unwrap(); + /// # } + /// ``` + pub fn install(self) -> Result<(), Error> { + let executor = match self.strategy { + HasStrategy::ExecuteDirectly => { + Executor::ExecuteDirectly(ExecuteDirectly::new(self.max_concurrency)) + } + #[cfg(feature = "tokio")] + HasStrategy::SpawnBlocking(handle) => Executor::SpawnBlocking( + spawn_blocking::SpawnBlocking::new(handle, self.max_concurrency), + ), + HasStrategy::Custom(closure) => { + Executor::Custom(Custom::new(closure, self.max_concurrency)) + } + }; + + set_global_strategy(executor) } } diff --git a/src/executor/spawn_blocking.rs b/src/executor/spawn_blocking.rs index 4fb3112..ca42e46 100644 --- a/src/executor/spawn_blocking.rs +++ b/src/executor/spawn_blocking.rs @@ -2,26 +2,26 @@ use tokio::runtime::Handle; use crate::{concurrency_limit::ConcurrencyLimit, error::Error}; -use super::ExecuteSync; +use super::Execute; -pub(crate) struct SpawnBlockingExecutor { +pub(crate) struct SpawnBlocking { concurrency_limit: ConcurrencyLimit, handle: Handle, } -impl SpawnBlockingExecutor { - pub(crate) fn new(max_concurrency: Option) -> Self { +impl SpawnBlocking { + pub(crate) fn new(handle: Handle, max_concurrency: Option) -> Self { let concurrency_limit = ConcurrencyLimit::new(max_concurrency); Self { concurrency_limit, - handle: Handle::current(), + handle, } } } -impl ExecuteSync for SpawnBlockingExecutor { - async fn execute_sync(&self, f: F) -> Result +impl Execute for SpawnBlocking { + async fn execute(&self, f: F) -> Result where F: FnOnce() -> R + Send + 'static, R: Send + 'static, diff --git a/src/lib.rs b/src/lib.rs index eabfb4f..0e85986 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,11 +5,11 @@ //! # vacation //! -//! `vacation``: give your (runtime) aworkers a break! +//! `vacation``: Give your (runtime) workers a break! //! //! ## Overview //! -//! Today, when library authors are writing async APIs, they don't have a good way to handle long-running sync segments. +//! Today, when library authors write async APIs, they don't have a good way to handle long-running sync segments. //! //! For an application, they can use selective handling such as `tokio::task::spawn_blocking()` along with concurrency control to delegate sync segments to blocking threads. Or, they might send the work to a `rayon` threadpool. //! @@ -30,11 +30,9 @@ //! vacation = { version = "0.1", default-features = false } //! ``` //! -//! And then wrap any sync work by passing it as a closure to a global `execute_sync()` call: +//! And then wrap any sync work by passing it as a closure to a global `execute()` call: //! //! ``` -//! use vacation::{execute_sync, ChanceOfBlocking}; -//! //! fn sync_work(input: String)-> u8 { //! std::thread::sleep(std::time::Duration::from_secs(5)); //! println!("{input}"); @@ -42,9 +40,9 @@ //! } //! //! pub async fn a_future_that_has_blocking_sync_work() -> u8 { -//! // relies on caller-specified strategy for translating execute_sync into a future that won't +//! // relies on caller-specified strategy for translating execute into a future that won't //! // block the current worker thread -//! execute_sync(move || { sync_work("foo".to_string()) }, ChanceOfBlocking::High).await.unwrap() +//! vacation::execute(move || { sync_work("foo".to_string()) }, vacation::ChanceOfBlocking::High).await.unwrap() //! } //! //! ``` @@ -53,7 +51,7 @@ //! Application authors will need to add this library as a a direct dependency in order to customize the execution strategy. //! By default, the strategy is just a non-op. //! -//! You can customize the strategy using the [`SyncExecutorBuilder`] or [`initialize_tokio()`]. +//! You can customize the strategy using the [`ExecutorBuilder`] or [`install_tokio_strategy()`]. //! //! ### Simple example //! @@ -63,13 +61,11 @@ //! vacation = { version = "0.1" } //! ``` //! -//! And then call the `initialize_tokio()` helper that uses some sensible defaults: +//! And then call the `install_tokio_strategy()` helper that uses some sensible defaults: //! ```ignore -//! use vacation::initialize_tokio; -//! //! #[tokio::main] //! async fn main() { -//! initialize_tokio().unwrap(); +//! vacation::install_tokio_strategy().unwrap(); //! } //! ``` //! @@ -87,22 +83,18 @@ //! use std::sync::OnceLock; //! use rayon::ThreadPool; //! -//! use vacation::{ -//! global_sync_strategy_builder, CustomExecutorSyncClosure, -//! }; -//! //! static THREADPOOL: OnceLock = OnceLock::new(); //! //! fn initialize_strategy() { //! THREADPOOL.set(rayon::ThreadPoolBuilder::default().build().unwrap()); //! -//! let custom_closure: CustomExecutorSyncClosure = +//! let custom_closure: vacation::CustomClosure = //! Box::new(|f| Box::new(async move { Ok(THREADPOOL.get().unwrap().spawn(f)) })); //! -//! global_sync_strategy_builder() +//! vacation::init() //! // probably no need for max concurrency as rayon already is defaulting to a thread per core //! // and using a task queue -//! .initialize_custom_executor(custom_closure).unwrap(); +//! .custom_executor(custom_closure).install().unwrap(); //! } //! ``` //! @@ -120,11 +112,10 @@ mod executor; pub use error::Error; pub use executor::{ - custom_executor::CustomExecutorSyncClosure, global_sync_strategy, initialize_tokio, - SyncExecutorBuilder, + custom_executor::CustomClosure, global_strategy, install_tokio_strategy, ExecutorBuilder, }; -use executor::{get_global_sync_executor, ExecuteSync, SyncExecutor}; +use executor::{get_global_executor, Execute, Executor, NoStrategy}; use std::fmt::Debug; @@ -138,14 +129,13 @@ pub enum GlobalStrategy { } /// The different types of executor strategies that can be loaded. -/// See [`SyncExecutorBuilder`] for more detail on each strategy. +/// See [`ExecutorBuilder`] for more detail on each strategy. /// /// # Examples /// /// ``` /// use vacation::{ -/// global_sync_strategy, -/// global_sync_strategy_builder, +/// global_strategy, /// GlobalStrategy, /// ExecutorStrategy /// }; @@ -153,16 +143,17 @@ pub enum GlobalStrategy { /// # fn run() { /// /// #[cfg(feature = "tokio")] -/// assert_eq!(global_sync_strategy(), GlobalStrategy::Default(ExecutorStrategy::SpawnBlocking)); +/// assert_eq!(global_strategy(), GlobalStrategy::Default(ExecutorStrategy::SpawnBlocking)); /// /// #[cfg(not(feature = "tokio"))] -/// assert_eq!(global_sync_strategy(), GlobalStrategy::Default(ExecutorStrategy::CurrentContext)); +/// assert_eq!(global_strategy(), GlobalStrategy::Default(ExecutorStrategy::ExecuteDirectly)); /// -/// global_sync_strategy_builder() -/// .initialize_current_context() +/// vacation::init() +/// .execute_directly() +/// .install() /// .unwrap(); /// -/// assert_eq!(global_sync_strategy(), GlobalStrategy::Initialized(ExecutorStrategy::CurrentContext)); +/// assert_eq!(global_strategy(), GlobalStrategy::Initialized(ExecutorStrategy::ExecuteDirectly)); /// /// # } /// ``` @@ -170,9 +161,9 @@ pub enum GlobalStrategy { #[derive(Debug, Clone, Copy, PartialEq)] pub enum ExecutorStrategy { /// A non-op strategy that awaits in the current context - CurrentContext, + ExecuteDirectly, /// User-provided closure - CustomExecutor, + Custom, /// tokio task::spawn_blocking #[cfg(feature = "tokio")] SpawnBlocking, @@ -181,19 +172,21 @@ pub enum ExecutorStrategy { /// Initialize a builder to set the global sync function /// executor strategy. /// -/// See [`SyncExecutorBuilder`] for details on strategies. +/// See [`ExecutorBuilder`] for details on strategies. /// /// # Examples /// /// ``` -/// use vacation::global_sync_strategy_builder; /// # fn run() { -/// global_sync_strategy_builder().max_concurrency(3).initialize_spawn_blocking().unwrap(); +/// vacation::init().max_concurrency(3).spawn_blocking().install().unwrap(); /// # } /// ``` #[must_use = "doesn't do anything unless used"] -pub fn global_sync_strategy_builder() -> SyncExecutorBuilder { - SyncExecutorBuilder::default() +pub fn init() -> ExecutorBuilder { + ExecutorBuilder { + strategy: NoStrategy, + max_concurrency: None, + } } /// Likelihood of the provided closure blocking for a significant period of time. @@ -208,41 +201,39 @@ pub enum ChanceOfBlocking { /// /// # Strategy selection /// -/// If no strategy is configured, this library will fall back to a non-op `CurrentContext` strategy. +/// If no strategy is configured, this library will fall back to a non-op `ExecuteDirectly` strategy. /// -/// You can override these defaults by initializing a strategy via [`global_sync_strategy_builder()`] -/// and [`SyncExecutorBuilder`]. +/// You can override these defaults by initializing a strategy via [`init()`] +/// and [`ExecutorBuilder`]. /// /// # Examples /// /// ``` /// # async fn run() { -/// use vacation::{execute_sync, ChanceOfBlocking}; -/// /// let closure = || { /// std::thread::sleep(std::time::Duration::from_secs(1)); /// 5 /// }; /// -/// let res = execute_sync(closure, ChanceOfBlocking::High).await.unwrap(); +/// let res = vacation::execute(closure, vacation::ChanceOfBlocking::High).await.unwrap(); /// assert_eq!(res, 5); /// # } /// /// ``` /// -pub async fn execute_sync(f: F, _chance_of_blocking: ChanceOfBlocking) -> Result +pub async fn execute(f: F, _chance_of_blocking: ChanceOfBlocking) -> Result where F: FnOnce() -> R + Send + 'static, R: Send + 'static, Error: Send + Sync + 'static, { - let executor = get_global_sync_executor(); + let executor = get_global_executor(); match executor { - SyncExecutor::CurrentContext(executor) => executor.execute_sync(f).await, - SyncExecutor::CustomExecutor(executor) => executor.execute_sync(f).await, + Executor::ExecuteDirectly(executor) => executor.execute(f).await, + Executor::Custom(executor) => executor.execute(f).await, #[cfg(feature = "tokio")] - SyncExecutor::SpawnBlocking(executor) => executor.execute_sync(f).await, + Executor::SpawnBlocking(executor) => executor.execute(f).await, } } diff --git a/tests/current_context_default.rs b/tests/current_context_default.rs index 001100e..5077279 100644 --- a/tests/current_context_default.rs +++ b/tests/current_context_default.rs @@ -1,10 +1,8 @@ #[tokio::test] -async fn default_to_current_context() { +async fn default_to_execute_directly() { use std::time::Duration; - use vacation::{ - execute_sync, global_sync_strategy, ChanceOfBlocking, ExecutorStrategy, GlobalStrategy, - }; + use vacation::{execute, global_strategy, ChanceOfBlocking, ExecutorStrategy, GlobalStrategy}; // this is a tokio test but we haven't enabled the tokio config flag @@ -13,11 +11,11 @@ async fn default_to_current_context() { 5 }; - let res = execute_sync(closure, ChanceOfBlocking::High).await.unwrap(); + let res = execute(closure, ChanceOfBlocking::High).await.unwrap(); assert_eq!(res, 5); assert_eq!( - global_sync_strategy(), - GlobalStrategy::Default(ExecutorStrategy::CurrentContext) + global_strategy(), + GlobalStrategy::Default(ExecutorStrategy::ExecuteDirectly) ); } diff --git a/tests/current_context_strategy.rs b/tests/current_context_strategy.rs index 09ae5b1..d599157 100644 --- a/tests/current_context_strategy.rs +++ b/tests/current_context_strategy.rs @@ -2,19 +2,16 @@ use std::time::Duration; use futures_util::future::join_all; use vacation::{ - execute_sync, global_sync_strategy, global_sync_strategy_builder, ChanceOfBlocking, - ExecutorStrategy, GlobalStrategy, + execute, global_strategy, init, ChanceOfBlocking, ExecutorStrategy, GlobalStrategy, }; fn initialize() { // we are racing all tests against the single oncelock - let _ = global_sync_strategy_builder() - .max_concurrency(3) - .initialize_current_context(); + let _ = init().max_concurrency(3).execute_directly().install(); } #[tokio::test] -async fn current_context_strategy() { +async fn execute_directly_strategy() { initialize(); let closure = || { @@ -22,17 +19,17 @@ async fn current_context_strategy() { 5 }; - let res = execute_sync(closure, ChanceOfBlocking::High).await.unwrap(); + let res = execute(closure, ChanceOfBlocking::High).await.unwrap(); assert_eq!(res, 5); assert_eq!( - global_sync_strategy(), - GlobalStrategy::Initialized(ExecutorStrategy::CurrentContext) + global_strategy(), + GlobalStrategy::Initialized(ExecutorStrategy::ExecuteDirectly) ); } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] -async fn current_context_concurrency() { +async fn execute_directly_concurrency() { initialize(); let start = std::time::Instant::now(); @@ -48,8 +45,7 @@ async fn current_context_concurrency() { for _ in 0..6 { // we need to spawn tasks since otherwise we'll just block the current worker thread let future = async move { - tokio::task::spawn(async move { execute_sync(closure, ChanceOfBlocking::High).await }) - .await + tokio::task::spawn(async move { execute(closure, ChanceOfBlocking::High).await }).await }; futures.push(future); } diff --git a/tests/custom_executor_simple.rs b/tests/custom_executor_simple.rs index 4561849..aae3428 100644 --- a/tests/custom_executor_simple.rs +++ b/tests/custom_executor_simple.rs @@ -1,33 +1,31 @@ use std::time::Duration; use vacation::{ - execute_sync, global_sync_strategy, global_sync_strategy_builder, ChanceOfBlocking, - CustomExecutorSyncClosure, ExecutorStrategy, GlobalStrategy, + execute, global_strategy, init, ChanceOfBlocking, CustomClosure, ExecutorStrategy, + GlobalStrategy, }; #[tokio::test] async fn custom_executor_simple() { - let custom_closure: CustomExecutorSyncClosure = Box::new(|f| { + let custom_closure: CustomClosure = Box::new(|f| { Box::new(async move { f(); Ok(()) }) }); - global_sync_strategy_builder() - .initialize_custom_executor(custom_closure) - .unwrap(); + init().custom_executor(custom_closure).install().unwrap(); let closure = || { std::thread::sleep(Duration::from_millis(15)); 5 }; - let res = execute_sync(closure, ChanceOfBlocking::High).await.unwrap(); + let res = execute(closure, ChanceOfBlocking::High).await.unwrap(); assert_eq!(res, 5); assert_eq!( - global_sync_strategy(), - GlobalStrategy::Initialized(ExecutorStrategy::CustomExecutor) + global_strategy(), + GlobalStrategy::Initialized(ExecutorStrategy::Custom) ); } diff --git a/tests/custom_executor_strategy.rs b/tests/custom_executor_strategy.rs index 19dd923..04a2391 100644 --- a/tests/custom_executor_strategy.rs +++ b/tests/custom_executor_strategy.rs @@ -2,25 +2,24 @@ use std::{sync::OnceLock, time::Duration}; use futures_util::future::join_all; use rayon::ThreadPool; -use vacation::{ - execute_sync, global_sync_strategy_builder, ChanceOfBlocking, CustomExecutorSyncClosure, -}; +use vacation::{execute, init, ChanceOfBlocking, CustomClosure}; static THREADPOOL: OnceLock = OnceLock::new(); fn initialize() { THREADPOOL.get_or_init(|| rayon::ThreadPoolBuilder::default().build().unwrap()); - let custom_closure: CustomExecutorSyncClosure = Box::new(|f| { + let custom_closure: CustomClosure = Box::new(|f| { Box::new(async move { THREADPOOL.get().unwrap().spawn(f); Ok(()) }) }); - let _ = global_sync_strategy_builder() + let _ = init() .max_concurrency(3) - .initialize_custom_executor(custom_closure); + .custom_executor(custom_closure) + .install(); } #[tokio::test] @@ -32,7 +31,7 @@ async fn custom_executor_strategy() { 5 }; - let res = execute_sync(closure, ChanceOfBlocking::High).await.unwrap(); + let res = execute(closure, ChanceOfBlocking::High).await.unwrap(); assert_eq!(res, 5); } @@ -52,7 +51,7 @@ async fn custom_executor_concurrency() { // note that we also are racing against concurrency from other tests in this module for _ in 0..6 { - futures.push(execute_sync(closure, ChanceOfBlocking::High)); + futures.push(execute(closure, ChanceOfBlocking::High)); } join_all(futures).await; diff --git a/tests/multiple_initialize_err.rs b/tests/multiple_initialize_err.rs index d6a5c84..55a2bf7 100644 --- a/tests/multiple_initialize_err.rs +++ b/tests/multiple_initialize_err.rs @@ -1,13 +1,11 @@ -use vacation::{global_sync_strategy_builder, Error}; +use vacation::{init, Error}; #[test] fn multiple_initialize_err() { - global_sync_strategy_builder() - .initialize_current_context() - .unwrap(); + init().execute_directly().install().unwrap(); assert!(matches!( - global_sync_strategy_builder().initialize_current_context(), + init().execute_directly().install(), Err(Error::AlreadyInitialized(_)) )); } diff --git a/tests/spawn_blocking_strategy.rs b/tests/spawn_blocking_strategy.rs index 42196b3..466fe11 100644 --- a/tests/spawn_blocking_strategy.rs +++ b/tests/spawn_blocking_strategy.rs @@ -5,15 +5,12 @@ mod test { use futures_util::future::join_all; use vacation::{ - execute_sync, global_sync_strategy, global_sync_strategy_builder, ChanceOfBlocking, - ExecutorStrategy, GlobalStrategy, + execute, global_strategy, init, ChanceOfBlocking, ExecutorStrategy, GlobalStrategy, }; fn initialize() { // we are racing all tests against the single oncelock - let _ = global_sync_strategy_builder() - .max_concurrency(3) - .initialize_spawn_blocking(); + let _ = init().max_concurrency(3).spawn_blocking().install(); } #[tokio::test] @@ -25,11 +22,11 @@ mod test { 5 }; - let res = execute_sync(closure, ChanceOfBlocking::High).await.unwrap(); + let res = execute(closure, ChanceOfBlocking::High).await.unwrap(); assert_eq!(res, 5); assert_eq!( - global_sync_strategy(), + global_strategy(), GlobalStrategy::Initialized(ExecutorStrategy::SpawnBlocking) ); } @@ -48,7 +45,7 @@ mod test { // note that we also are racing against concurrency from other tests in this module for _ in 0..6 { - let future = async move { execute_sync(closure, ChanceOfBlocking::High).await }; + let future = async move { execute(closure, ChanceOfBlocking::High).await }; futures.push(future); } tokio::time::sleep(Duration::from_millis(5)).await; From 2123784232947967d57269a826a8fdb3f16e1766 Mon Sep 17 00:00:00 2001 From: jlizen Date: Mon, 30 Dec 2024 22:17:45 +0000 Subject: [PATCH 06/10] include readme directly in module docs, move default strategy off Default trait impl --- README.md | 12 +- src/executor/mod.rs | 24 ++-- src/lib.rs | 104 +----------------- ...default.rs => execute_directly_default.rs} | 9 ++ ...rategy.rs => execute_directly_strategy.rs} | 0 5 files changed, 27 insertions(+), 122 deletions(-) rename tests/{current_context_default.rs => execute_directly_default.rs} (65%) rename tests/{current_context_strategy.rs => execute_directly_strategy.rs} (100%) diff --git a/README.md b/README.md index e719399..37af7c3 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ And then, the applications using the library can tune handling based on their pr ## Usage - Library Authors For library authors, it's as simple as adding a dependency enabling `vacation` (perhaps behind a feature flag). -``` +```ignore [dependencies] vacation = { version = "0.1", default-features = false } ``` @@ -31,7 +31,7 @@ fn sync_work(input: String)-> u8 { 5 } pub async fn a_future_that_has_blocking_sync_work() -> u8 { - // relies on caller-specified strategy for translating execute into a future that won't + // relies on application-specified strategy for translating execute into a future that won't // block the current worker thread vacation::execute(move || { sync_work("foo".to_string()) }, vacation::ChanceOfBlocking::High).await.unwrap() } @@ -44,7 +44,7 @@ By default, the strategy is just a non-op. ### Simple example -``` +```ignore [dependencies] // enables `tokio` feature by default => spawn_blocking strategy vacation = { version = "0.1" } @@ -61,7 +61,7 @@ async fn main() { ### Rayon example Or, you can add an alternate strategy, for instance a custom closure using Rayon. -``` +```ignore [dependencies] vacation = { version = "0.1", default-features = false } // used for example with custom executor @@ -75,9 +75,9 @@ use rayon::ThreadPool; static THREADPOOL: OnceLock = OnceLock::new(); fn initialize_strategy() { - THREADPOOL.setrayon::ThreadPoolBuilder::default().build().unwrap()); + THREADPOOL.set(rayon::ThreadPoolBuilder::default().build().unwrap()); - let custom_closure: CustomClosure = + let custom_closure: vacation::CustomClosure = Box::new(|f| Box::new(async move { Ok(THREADPOOL.get().unwrap().spawn(f)) })); vacation::init() diff --git a/src/executor/mod.rs b/src/executor/mod.rs index 63fe447..b9bc1da 100644 --- a/src/executor/mod.rs +++ b/src/executor/mod.rs @@ -65,14 +65,23 @@ fn set_global_strategy(strategy: Executor) -> Result<(), Error> { pub fn global_strategy() -> GlobalStrategy { match GLOBAL_EXECUTOR_STRATEGY.get() { Some(strategy) => GlobalStrategy::Initialized(strategy.into()), - None => GlobalStrategy::Default(<&Executor>::default().into()), + None => GlobalStrategy::Default(get_default_strategy().into()), } } pub(crate) fn get_global_executor() -> &'static Executor { GLOBAL_EXECUTOR_STRATEGY .get() - .unwrap_or_else(|| <&Executor>::default()) + .unwrap_or_else(|| get_default_strategy()) +} + +pub(crate) fn get_default_strategy() -> &'static Executor { + DEFAULT_GLOBAL_EXECUTOR_STRATEGY.get_or_init(|| { + log::warn!( + "Defaulting to ExecuteDirectly (non-op) strategy for vacation compute-heavy future executor" + ); + Executor::ExecuteDirectly(ExecuteDirectly::new(None)) + }) } /// The stored strategy used to spawn compute-heavy futures. @@ -92,17 +101,6 @@ pub(crate) enum Executor { SpawnBlocking(spawn_blocking::SpawnBlocking), } -impl Default for &Executor { - fn default() -> Self { - DEFAULT_GLOBAL_EXECUTOR_STRATEGY.get_or_init(|| { - log::warn!( - "Defaulting to ExecuteDirectly (non-op) strategy for vacation compute-heavy future executor" - ); - Executor::ExecuteDirectly(ExecuteDirectly::new(None)) - }) - } -} - impl Execute for Executor { async fn execute(&self, f: F) -> Result where diff --git a/src/lib.rs b/src/lib.rs index 0e85986..e7c74ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,109 +2,7 @@ #![deny(missing_debug_implementations)] #![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(test, deny(warnings))] - -//! # vacation -//! -//! `vacation``: Give your (runtime) workers a break! -//! -//! ## Overview -//! -//! Today, when library authors write async APIs, they don't have a good way to handle long-running sync segments. -//! -//! For an application, they can use selective handling such as `tokio::task::spawn_blocking()` along with concurrency control to delegate sync segments to blocking threads. Or, they might send the work to a `rayon` threadpool. -//! -//! But, library authors generally don't have this flexibility. As, they generally want to be agnostic across runtime. Or, even if they are `tokio`-specific, they generally don't want to call `tokio::task::spawn_blocking()` as it is -//! suboptimal without extra configuration (concurrency control) as well as highly opinionated to send the work across threads. -//! -//! This library solves this problem by providing libray authors a static, globally scoped strategy that they can delegate blocking sync work to without drawing any conclusions about handling. -//! -//! And then, the applications using the library can tune handling based on their preferred approach. -//! -//! ## Usage - Library Authors -//! First add dependency enabling `vacation` (perhaps behind a feature flag). -//! -//! The below will default to 'current context' sync execution (ie non-op) unless the caller enables the tokio feature. -//! -//! ```ignore -//! [dependencies] -//! vacation = { version = "0.1", default-features = false } -//! ``` -//! -//! And then wrap any sync work by passing it as a closure to a global `execute()` call: -//! -//! ``` -//! fn sync_work(input: String)-> u8 { -//! std::thread::sleep(std::time::Duration::from_secs(5)); -//! println!("{input}"); -//! 5 -//! } -//! -//! pub async fn a_future_that_has_blocking_sync_work() -> u8 { -//! // relies on caller-specified strategy for translating execute into a future that won't -//! // block the current worker thread -//! vacation::execute(move || { sync_work("foo".to_string()) }, vacation::ChanceOfBlocking::High).await.unwrap() -//! } -//! -//! ``` -//! -//! ## Usage - Application owners -//! Application authors will need to add this library as a a direct dependency in order to customize the execution strategy. -//! By default, the strategy is just a non-op. -//! -//! You can customize the strategy using the [`ExecutorBuilder`] or [`install_tokio_strategy()`]. -//! -//! ### Simple example -//! -//! ```ignore -//! [dependencies] -//! // enables `tokio` feature by default => spawn_blocking strategy -//! vacation = { version = "0.1" } -//! ``` -//! -//! And then call the `install_tokio_strategy()` helper that uses some sensible defaults: -//! ```ignore -//! #[tokio::main] -//! async fn main() { -//! vacation::install_tokio_strategy().unwrap(); -//! } -//! ``` -//! -//! ### Rayon example -//! Or, you can add an alternate strategy, for instance a custom closure using Rayon. -//! -//! ```ignore -//! [dependencies] -//! vacation = { version = "0.1", default-features = false } -//! // used for example with custom executor -//! rayon = "1" -//! ``` -//! -//! ``` -//! use std::sync::OnceLock; -//! use rayon::ThreadPool; -//! -//! static THREADPOOL: OnceLock = OnceLock::new(); -//! -//! fn initialize_strategy() { -//! THREADPOOL.set(rayon::ThreadPoolBuilder::default().build().unwrap()); -//! -//! let custom_closure: vacation::CustomClosure = -//! Box::new(|f| Box::new(async move { Ok(THREADPOOL.get().unwrap().spawn(f)) })); -//! -//! vacation::init() -//! // probably no need for max concurrency as rayon already is defaulting to a thread per core -//! // and using a task queue -//! .custom_executor(custom_closure).install().unwrap(); -//! } -//! ``` -//! -//! ## Feature flags -//! -//! Feature flags are used to reduce the amount -//! of compiled code. -//! -//! - `tokio`: Enables the `SpawnBlocking` sync strategy. -//! +#![doc = include_str!("../README.md")] mod concurrency_limit; mod error; diff --git a/tests/current_context_default.rs b/tests/execute_directly_default.rs similarity index 65% rename from tests/current_context_default.rs rename to tests/execute_directly_default.rs index 5077279..c2ab22a 100644 --- a/tests/current_context_default.rs +++ b/tests/execute_directly_default.rs @@ -18,4 +18,13 @@ async fn default_to_execute_directly() { global_strategy(), GlobalStrategy::Default(ExecutorStrategy::ExecuteDirectly) ); + + // make sure we can continue to call it without failures due to repeat initialization + let res = execute(closure, ChanceOfBlocking::High).await.unwrap(); + assert_eq!(res, 5); + + assert_eq!( + global_strategy(), + GlobalStrategy::Default(ExecutorStrategy::ExecuteDirectly) + ); } diff --git a/tests/current_context_strategy.rs b/tests/execute_directly_strategy.rs similarity index 100% rename from tests/current_context_strategy.rs rename to tests/execute_directly_strategy.rs From 0d7b96da9b990ebd325f2fd2c62132930bcbd465 Mon Sep 17 00:00:00 2001 From: jlizen Date: Mon, 30 Dec 2024 22:23:47 +0000 Subject: [PATCH 07/10] more file renaming --- src/executor/{custom_executor.rs => custom.rs} | 0 src/executor/mod.rs | 6 +++--- src/lib.rs | 2 +- tests/{custom_executor_simple.rs => custom_simple.rs} | 2 +- tests/{custom_executor_strategy.rs => custom_strategy.rs} | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) rename src/executor/{custom_executor.rs => custom.rs} (100%) rename tests/{custom_executor_simple.rs => custom_simple.rs} (94%) rename tests/{custom_executor_strategy.rs => custom_strategy.rs} (95%) diff --git a/src/executor/custom_executor.rs b/src/executor/custom.rs similarity index 100% rename from src/executor/custom_executor.rs rename to src/executor/custom.rs diff --git a/src/executor/mod.rs b/src/executor/mod.rs index b9bc1da..5c18075 100644 --- a/src/executor/mod.rs +++ b/src/executor/mod.rs @@ -1,11 +1,11 @@ -pub(crate) mod custom_executor; +pub(crate) mod custom; pub(crate) mod execute_directly; #[cfg(feature = "tokio")] pub(crate) mod spawn_blocking; use std::sync::OnceLock; -use custom_executor::{Custom, CustomClosure}; +use custom::{Custom, CustomClosure}; use execute_directly::ExecuteDirectly; use crate::{init, Error, ExecutorStrategy, GlobalStrategy}; @@ -95,7 +95,7 @@ pub(crate) enum Executor { /// A non-op strategy that runs the function in the current context ExecuteDirectly(execute_directly::ExecuteDirectly), /// User-provided closure - Custom(custom_executor::Custom), + Custom(custom::Custom), /// tokio task::spawn_blocking #[cfg(feature = "tokio")] SpawnBlocking(spawn_blocking::SpawnBlocking), diff --git a/src/lib.rs b/src/lib.rs index e7c74ac..8fc942e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,7 @@ mod executor; pub use error::Error; pub use executor::{ - custom_executor::CustomClosure, global_strategy, install_tokio_strategy, ExecutorBuilder, + custom::CustomClosure, global_strategy, install_tokio_strategy, ExecutorBuilder, }; use executor::{get_global_executor, Execute, Executor, NoStrategy}; diff --git a/tests/custom_executor_simple.rs b/tests/custom_simple.rs similarity index 94% rename from tests/custom_executor_simple.rs rename to tests/custom_simple.rs index aae3428..a844e4c 100644 --- a/tests/custom_executor_simple.rs +++ b/tests/custom_simple.rs @@ -6,7 +6,7 @@ use vacation::{ }; #[tokio::test] -async fn custom_executor_simple() { +async fn custom_simple() { let custom_closure: CustomClosure = Box::new(|f| { Box::new(async move { f(); diff --git a/tests/custom_executor_strategy.rs b/tests/custom_strategy.rs similarity index 95% rename from tests/custom_executor_strategy.rs rename to tests/custom_strategy.rs index 04a2391..45fc925 100644 --- a/tests/custom_executor_strategy.rs +++ b/tests/custom_strategy.rs @@ -23,7 +23,7 @@ fn initialize() { } #[tokio::test] -async fn custom_executor_strategy() { +async fn custom_strategy() { initialize(); let closure = || { @@ -36,7 +36,7 @@ async fn custom_executor_strategy() { } #[tokio::test] -async fn custom_executor_concurrency() { +async fn custom_concurrency() { initialize(); let start = std::time::Instant::now(); From 104adafc9434a8167573671162402c82e439818a Mon Sep 17 00:00:00 2001 From: jlizen Date: Thu, 2 Jan 2025 17:51:47 +0000 Subject: [PATCH 08/10] implicitly box custom closure, other minor tweaks --- README.md | 5 +++-- src/executor/custom.rs | 18 ++++++++---------- src/executor/mod.rs | 27 +++++++++++++++++++-------- src/lib.rs | 11 ++++++----- tests/custom_simple.rs | 10 +++++----- tests/custom_strategy.rs | 8 ++++---- 6 files changed, 45 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 37af7c3..f32ead5 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,9 @@ static THREADPOOL: OnceLock = OnceLock::new(); fn initialize_strategy() { THREADPOOL.set(rayon::ThreadPoolBuilder::default().build().unwrap()); - let custom_closure: vacation::CustomClosure = - Box::new(|f| Box::new(async move { Ok(THREADPOOL.get().unwrap().spawn(f)) })); + let custom_closure = |f: vacation::CustomClosureInput| { + Box::new(async move { Ok(THREADPOOL.get().unwrap().spawn(f)) }) as vacation::CustomClosureOutput + }; vacation::init() // probably no need for max concurrency as rayon already is defaulting to a thread per core diff --git a/src/executor/custom.rs b/src/executor/custom.rs index 6512b7f..722df11 100644 --- a/src/executor/custom.rs +++ b/src/executor/custom.rs @@ -4,19 +4,17 @@ use crate::{concurrency_limit::ConcurrencyLimit, error::Error}; use super::Execute; +/// The input for the custom closure +pub type CustomClosureInput = Box; +/// The output type for the custom closure +pub type CustomClosureOutput = + Box>> + Send + 'static>; + /// A closure that accepts an arbitrary sync function and returns a future that executes it. /// The Custom Executor will implicitly wrap the input function in a oneshot /// channel to erase its input/output type. -pub type CustomClosure = Box< - dyn Fn( - Box, - ) -> Box< - dyn Future>> - + Send - + 'static, - > + Send - + Sync, ->; +pub(crate) type CustomClosure = + Box CustomClosureOutput + Send + Sync>; pub(crate) struct Custom { closure: CustomClosure, diff --git a/src/executor/mod.rs b/src/executor/mod.rs index 5c18075..b7acd5e 100644 --- a/src/executor/mod.rs +++ b/src/executor/mod.rs @@ -3,7 +3,7 @@ pub(crate) mod execute_directly; #[cfg(feature = "tokio")] pub(crate) mod spawn_blocking; -use std::sync::OnceLock; +use std::{future::Future, sync::OnceLock}; use custom::{Custom, CustomClosure}; use execute_directly::ExecuteDirectly; @@ -129,7 +129,7 @@ impl From<&Executor> for ExecutorStrategy { /// Initialize a set of sensible defaults for a tokio runtime: /// -/// - SpawnBlocking strategy +/// - [`SpawnBlocking`]` strategy /// - Max concurrency equal to the cpu core count. /// /// Only available with the `tokio` feature. @@ -182,7 +182,7 @@ impl std::fmt::Debug for ExecutorBuilder { } #[derive(Debug)] -pub struct NoStrategy; +pub struct NeedsStrategy; pub enum HasStrategy { ExecuteDirectly, #[cfg(feature = "tokio")] @@ -323,9 +323,9 @@ impl ExecutorBuilder { /// // caution: this will panic if used outside of tokio multithreaded runtime /// // this is a kind of dangerous strategy, read up on `block in place's` limitations /// // before using this approach - /// let closure: vacation::CustomClosure = Box::new(|f| { - /// Box::new(async move { Ok(tokio::task::block_in_place(move || f())) }) - /// }); + /// let closure = |f: vacation::CustomClosureInput| { + /// Box::new(async move { Ok(tokio::task::block_in_place(move || f())) }) as vacation::CustomClosureOutput + /// }; /// /// vacation::init().custom_executor(closure).install().unwrap(); /// # } @@ -335,9 +335,20 @@ impl ExecutorBuilder { /// [`Rayon threadpool`]: https://docs.rs/rayon/latest/rayon/struct.ThreadPool.html /// [`block_in_place`]: https://docs.rs/tokio/latest/tokio/task/fn.block_in_place.html #[must_use = "doesn't do anything unless install()-ed"] - pub fn custom_executor(self, closure: CustomClosure) -> ExecutorBuilder { + pub fn custom_executor(self, closure: Closure) -> ExecutorBuilder + where + Closure: Fn( + Box, + ) -> Box< + dyn Future>> + + Send + + 'static, + > + Send + + Sync + + 'static, + { ExecutorBuilder:: { - strategy: HasStrategy::Custom(closure), + strategy: HasStrategy::Custom(Box::new(closure)), max_concurrency: self.max_concurrency, } } diff --git a/src/lib.rs b/src/lib.rs index 8fc942e..b72a2df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,10 +10,11 @@ mod executor; pub use error::Error; pub use executor::{ - custom::CustomClosure, global_strategy, install_tokio_strategy, ExecutorBuilder, + custom::{CustomClosureInput, CustomClosureOutput}, + global_strategy, install_tokio_strategy, ExecutorBuilder, }; -use executor::{get_global_executor, Execute, Executor, NoStrategy}; +use executor::{get_global_executor, Execute, Executor, NeedsStrategy}; use std::fmt::Debug; @@ -80,16 +81,16 @@ pub enum ExecutorStrategy { /// # } /// ``` #[must_use = "doesn't do anything unless used"] -pub fn init() -> ExecutorBuilder { +pub fn init() -> ExecutorBuilder { ExecutorBuilder { - strategy: NoStrategy, + strategy: NeedsStrategy, max_concurrency: None, } } /// Likelihood of the provided closure blocking for a significant period of time. /// Will eventually be used to customize strategies with more granularity. -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub enum ChanceOfBlocking { /// Very likely to block, use primary sync strategy High, diff --git a/tests/custom_simple.rs b/tests/custom_simple.rs index a844e4c..7c38faa 100644 --- a/tests/custom_simple.rs +++ b/tests/custom_simple.rs @@ -1,18 +1,18 @@ use std::time::Duration; use vacation::{ - execute, global_strategy, init, ChanceOfBlocking, CustomClosure, ExecutorStrategy, - GlobalStrategy, + execute, global_strategy, init, ChanceOfBlocking, CustomClosureInput, CustomClosureOutput, + ExecutorStrategy, GlobalStrategy, }; #[tokio::test] async fn custom_simple() { - let custom_closure: CustomClosure = Box::new(|f| { + let custom_closure = |f: CustomClosureInput| { Box::new(async move { f(); Ok(()) - }) - }); + }) as CustomClosureOutput + }; init().custom_executor(custom_closure).install().unwrap(); diff --git a/tests/custom_strategy.rs b/tests/custom_strategy.rs index 45fc925..7f94bdc 100644 --- a/tests/custom_strategy.rs +++ b/tests/custom_strategy.rs @@ -2,19 +2,19 @@ use std::{sync::OnceLock, time::Duration}; use futures_util::future::join_all; use rayon::ThreadPool; -use vacation::{execute, init, ChanceOfBlocking, CustomClosure}; +use vacation::{execute, init, ChanceOfBlocking, CustomClosureInput, CustomClosureOutput}; static THREADPOOL: OnceLock = OnceLock::new(); fn initialize() { THREADPOOL.get_or_init(|| rayon::ThreadPoolBuilder::default().build().unwrap()); - let custom_closure: CustomClosure = Box::new(|f| { + let custom_closure = |f: CustomClosureInput| { Box::new(async move { THREADPOOL.get().unwrap().spawn(f); Ok(()) - }) - }); + }) as CustomClosureOutput + }; let _ = init() .max_concurrency(3) From 321b0040600989782372b164ca2e84860d6b68fa Mon Sep 17 00:00:00 2001 From: jlizen Date: Thu, 2 Jan 2025 18:11:43 +0000 Subject: [PATCH 09/10] add panic warnings to tokio executors --- src/executor/mod.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/executor/mod.rs b/src/executor/mod.rs index b7acd5e..3771714 100644 --- a/src/executor/mod.rs +++ b/src/executor/mod.rs @@ -132,17 +132,25 @@ impl From<&Executor> for ExecutorStrategy { /// - [`SpawnBlocking`]` strategy /// - Max concurrency equal to the cpu core count. /// +/// Stores the current tokio runtime to spawn tasks with. To use an alternate +/// runtime, use [`spawn_blocking_with_handle()`]. +/// /// Only available with the `tokio` feature. /// -/// # Error +/// # Panic +/// Calling this from outside a tokio runtime will panic. +/// +/// # Errors /// Returns an error if the global strategy is already initialized. /// It can only be initialized once. +/// /// # Examples /// /// ``` /// # fn run() { /// vacation::install_tokio_strategy().unwrap(); /// # } +/// ``` #[cfg(feature = "tokio")] pub fn install_tokio_strategy() -> Result<(), Error> { init() @@ -254,8 +262,14 @@ impl ExecutorBuilder { /// Initializes a new global strategy to execute input closures by blocking on them inside the /// tokio blocking threadpool via Tokio's [`spawn_blocking`]. /// + /// Stores the current tokio runtime to spawn tasks with. To use an alternate + /// runtime, use [`spawn_blocking_with_handle()`]. + /// /// Requires `tokio` feature. /// + /// # Panic + /// Calling this from outside a tokio runtime will panic. + /// /// # Examples /// /// ``` From 4bda9bca157c7e0cb20a43cea2593b134fcf0e9e Mon Sep 17 00:00:00 2001 From: jlizen Date: Thu, 2 Jan 2025 18:23:26 +0000 Subject: [PATCH 10/10] fix links in docs --- src/executor/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/executor/mod.rs b/src/executor/mod.rs index 3771714..4d27350 100644 --- a/src/executor/mod.rs +++ b/src/executor/mod.rs @@ -129,11 +129,11 @@ impl From<&Executor> for ExecutorStrategy { /// Initialize a set of sensible defaults for a tokio runtime: /// -/// - [`SpawnBlocking`]` strategy +/// - [`ExecutorBuilder::spawn_blocking`] strategy /// - Max concurrency equal to the cpu core count. /// /// Stores the current tokio runtime to spawn tasks with. To use an alternate -/// runtime, use [`spawn_blocking_with_handle()`]. +/// runtime, use [`ExecutorBuilder::spawn_blocking_with_handle`]. /// /// Only available with the `tokio` feature. /// @@ -263,7 +263,7 @@ impl ExecutorBuilder { /// tokio blocking threadpool via Tokio's [`spawn_blocking`]. /// /// Stores the current tokio runtime to spawn tasks with. To use an alternate - /// runtime, use [`spawn_blocking_with_handle()`]. + /// runtime, use [`ExecutorBuilder::spawn_blocking_with_handle`]. /// /// Requires `tokio` feature. ///