diff --git a/Cargo.lock b/Cargo.lock index aa34d8f09..3a7569f11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -566,6 +566,18 @@ dependencies = [ "yaml-rust2", ] +[[package]] +name = "console" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "const-random" version = "0.1.18" @@ -820,6 +832,12 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1351,6 +1369,18 @@ dependencies = [ "hashbrown 0.15.2", ] +[[package]] +name = "insta" +version = "1.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6513e4067e16e69ed1db5ab56048ed65db32d10ba5fc1217f5393f8f17d8b5a5" +dependencies = [ + "console", + "linked-hash-map", + "once_cell", + "similar", +] + [[package]] name = "is-terminal" version = "0.4.13" @@ -1470,6 +1500,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -2486,9 +2522,13 @@ version = "0.0.2" dependencies = [ "anyhow", "futures", + "insta", "pin-project-lite", + "postcompile", "scuffle-bootstrap-derive", "scuffle-context", + "scuffle-future-ext", + "scuffle-signal", "scuffle-workspace-hack", "tokio", ] @@ -2498,6 +2538,8 @@ name = "scuffle-bootstrap-derive" version = "0.0.2" dependencies = [ "darling", + "insta", + "prettyplease", "proc-macro2", "quote", "scuffle-workspace-hack", @@ -2920,6 +2962,12 @@ dependencies = [ "libc", ] +[[package]] +name = "similar" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" + [[package]] name = "slab" version = "0.4.9" diff --git a/Cargo.toml b/Cargo.toml index 909b72d75..95f89568f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ scuffle-http = { path = "crates/http", version = "0.0.4" } scuffle-metrics = { path = "crates/metrics", version = "0.0.4" } scuffle-pprof = { path = "crates/pprof", version = "0.0.2" } scuffle-batching = { path = "crates/batching", version = "0.0.4" } -scuffle-postcompile = { path = "crates/postcompile", version = "0.0.5" } +postcompile = { path = "crates/postcompile", version = "0.0.5" } scuffle-ffmpeg = { path = "crates/ffmpeg", version = "0.0.2" } scuffle-h3-webtransport = { path = "crates/h3-webtransport", version = "0.0.2" } scuffle-metrics-derive = { path = "crates/metrics/derive", version = "0.0.2" } diff --git a/Justfile b/Justfile index e146ffadb..ccfab6508 100644 --- a/Justfile +++ b/Justfile @@ -29,6 +29,9 @@ test *args: cargo +{{RUST_TOOLCHAIN}} llvm-cov report --lcov --output-path ./lcov.info cargo +{{RUST_TOOLCHAIN}} llvm-cov report --html +doc *args: + cargo +{{RUST_TOOLCHAIN}} doc --no-deps --all-features {{args}} + deny *args: cargo +{{RUST_TOOLCHAIN}} deny {{args}} --all-features check diff --git a/crates/batching/src/lib.rs b/crates/batching/src/lib.rs index 56cd59178..c7c7922f8 100644 --- a/crates/batching/src/lib.rs +++ b/crates/batching/src/lib.rs @@ -1,12 +1,5 @@ //! # scuffle-batching //! -//! > WARNING -//! > This crate is under active development and may not be stable. -//! -//! [![crates.io](https://img.shields.io/crates/v/scuffle-batching.svg)](https://crates.io/crates/scuffle-batching) [![docs.rs](https://img.shields.io/docsrs/scuffle-batching)](https://docs.rs/scuffle-batching) -//! -//! --- -//! //! A crate designed to batch multiple requests into a single request. //! //! ## Why do we need this? diff --git a/crates/bootstrap/Cargo.toml b/crates/bootstrap/Cargo.toml index a84100b66..017520165 100644 --- a/crates/bootstrap/Cargo.toml +++ b/crates/bootstrap/Cargo.toml @@ -10,6 +10,9 @@ documentation = "https://docs.rs/scuffle-bootstrap" license = "MIT OR Apache-2.0" keywords = ["bootstrap", "binary", "cli", "config"] +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } + [dependencies] anyhow = "1.0" tokio = { version = "1", features = ["full"] } @@ -19,3 +22,9 @@ pin-project-lite = "0.2" scuffle-context.workspace = true scuffle-bootstrap-derive.workspace = true scuffle-workspace-hack.workspace = true + +[dev-dependencies] +insta = "1.42.0" +postcompile = { workspace = true, features = ["prettyplease"] } +scuffle-future-ext.workspace = true +scuffle-signal = { workspace = true, features = ["bootstrap"] } diff --git a/crates/bootstrap/README.md b/crates/bootstrap/README.md index 46742764e..d607b1cfb 100644 --- a/crates/bootstrap/README.md +++ b/crates/bootstrap/README.md @@ -3,15 +3,54 @@ > [!WARNING] > This crate is under active development and may not be stable. - [![crates.io](https://img.shields.io/crates/v/scuffle-bootstrap.svg)](https://crates.io/crates/scuffle-bootstrap) [![docs.rs](https://img.shields.io/docsrs/scuffle-bootstrap)](https://docs.rs/scuffle-bootstrap) +[![crates.io](https://img.shields.io/crates/v/scuffle-bootstrap.svg)](https://crates.io/crates/scuffle-bootstrap) [![docs.rs](https://img.shields.io/docsrs/scuffle-bootstrap)](https://docs.rs/scuffle-bootstrap) --- A utility crate for creating binaries. +Refer to `Global`, `Service`, and `main` for more information. + ## Usage -TODO(troy): Add usage examples to readme. +```rust +use std::sync::Arc; + +/// Our global state +struct Global; + +// Required by the signal service +impl scuffle_signal::SignalConfig for Global {} + +impl scuffle_bootstrap::global::GlobalWithoutConfig for Global { + async fn init() -> anyhow::Result> { + Ok(Arc::new(Self)) + } +} + +/// Our own custom service +struct MySvc; + +impl scuffle_bootstrap::service::Service for MySvc { + async fn run(self, global: Arc, ctx: scuffle_context::Context) -> anyhow::Result<()> { + println!("running"); + + // Do some work here + + // Wait for the context to be cacelled by the signal service + ctx.done().await; + Ok(()) + } +} + +// This generates the main function which runs all the services +scuffle_bootstrap::main! { + Global { + scuffle_signal::SignalSvc, + MySvc, + } +} +``` ## License diff --git a/crates/bootstrap/derive/Cargo.toml b/crates/bootstrap/derive/Cargo.toml index 52d1ef5a9..01294745e 100644 --- a/crates/bootstrap/derive/Cargo.toml +++ b/crates/bootstrap/derive/Cargo.toml @@ -10,6 +10,9 @@ license = "MIT OR Apache-2.0" description = "Derive macros for scuffle-bootstrap." keywords = ["bootstrap", "derive", "macros"] +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } + [lib] proc-macro = true @@ -19,3 +22,7 @@ proc-macro2 = "1" quote = "1" darling = "0.20" scuffle-workspace-hack.workspace = true + +[dev-dependencies] +insta = "1" +prettyplease = "0.2" diff --git a/crates/bootstrap/derive/src/lib.rs b/crates/bootstrap/derive/src/lib.rs index 10db18af5..2606c2dba 100644 --- a/crates/bootstrap/derive/src/lib.rs +++ b/crates/bootstrap/derive/src/lib.rs @@ -1,12 +1,8 @@ +#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))] + use proc_macro::TokenStream; mod main_impl; -// mod service_impl; - -// #[proc_macro_attribute] -// pub fn service(args: TokenStream, input: TokenStream) -> TokenStream { -// handle_error(service_impl::impl_service(args.into(), input.into())) -// } #[proc_macro] pub fn main(input: TokenStream) -> TokenStream { @@ -19,3 +15,132 @@ fn handle_error(input: Result) -> TokenStr Err(err) => err.to_compile_error().into(), } } + +#[cfg(test)] +#[cfg_attr(all(test, coverage_nightly), coverage(off))] +mod tests { + use super::*; + + #[test] + fn test_main() { + let input = quote::quote! { + MyGlobal { + MyService, + } + }; + + let output = match main_impl::impl_main(input) { + Ok(value) => value, + Err(err) => err.to_compile_error(), + }; + + let syntax_tree = prettyplease::unparse(&syn::parse_file(&output.to_string()).unwrap()); + + insta::assert_snapshot!(syntax_tree, @r##" + #[automatically_derived] + fn main() -> ::scuffle_bootstrap::prelude::anyhow::Result<()> { + #[doc(hidden)] + pub const fn impl_global() {} + const _: () = impl_global::(); + ::scuffle_bootstrap::prelude::anyhow::Context::context( + ::pre_init(), + "pre_init", + )?; + let runtime = ::tokio_runtime(); + let config = ::scuffle_bootstrap::prelude::anyhow::Context::context( + runtime + .block_on( + <::Config as ::scuffle_bootstrap::config::ConfigParser>::parse(), + ), + "config parse", + )?; + let ctx_handle = ::scuffle_bootstrap::prelude::scuffle_context::Handler::global(); + let mut shared_global = ::core::option::Option::None; + let mut services_vec = ::std::vec::Vec::< + ::scuffle_bootstrap::service::NamedFuture< + ::scuffle_bootstrap::prelude::tokio::task::JoinHandle>, + >, + >::new(); + let result = runtime + .block_on(async { + let global = ::init(config) + .await?; + shared_global = ::core::option::Option::Some(global.clone()); + { + #[doc(hidden)] + pub async fn spawn_service( + svc: impl ::scuffle_bootstrap::service::Service, + global: &::std::sync::Arc, + ctx_handle: &::scuffle_bootstrap::prelude::scuffle_context::Handler, + name: &'static str, + ) -> anyhow::Result< + Option< + ::scuffle_bootstrap::service::NamedFuture< + ::scuffle_bootstrap::prelude::tokio::task::JoinHandle< + anyhow::Result<()>, + >, + >, + >, + > { + let name = ::scuffle_bootstrap::service::Service::< + MyGlobal, + >::name(&svc) + .unwrap_or_else(|| name); + if ::scuffle_bootstrap::prelude::anyhow::Context::context( + ::scuffle_bootstrap::service::Service::< + MyGlobal, + >::enabled(&svc, &global) + .await, + name, + )? { + Ok( + Some( + ::scuffle_bootstrap::service::NamedFuture::new( + name, + ::scuffle_bootstrap::prelude::tokio::spawn( + ::scuffle_bootstrap::service::Service::< + MyGlobal, + >::run(svc, global.clone(), ctx_handle.context()), + ), + ), + ), + ) + } else { + Ok(None) + } + } + let res = spawn_service(MyService, &global, &ctx_handle, "MyService") + .await; + if let Some(spawned) = res? { + services_vec.push(spawned); + } + } + ::on_services_start(&global) + .await?; + macro_rules! handle_service_exit { + ($remaining:ident) => { + { let ((name, result), _, remaining) = + ::scuffle_bootstrap::prelude::futures::future::select_all($remaining) + . await; let result = + ::scuffle_bootstrap::prelude::anyhow::Context::context(::scuffle_bootstrap::prelude::anyhow::Context::context(result, + name) ?, name); < MyGlobal as ::scuffle_bootstrap::global::Global > + ::on_service_exit(& global, name, result). await ?; remaining } + }; + } + let mut remaining = handle_service_exit!(services_vec); + while !remaining.is_empty() { + remaining = handle_service_exit!(remaining); + } + ::scuffle_bootstrap::prelude::anyhow::Ok(()) + }); + let ::core::option::Option::Some(global) = shared_global else { + return result; + }; + runtime + .block_on( + ::on_exit(&global, result), + ) + } + "##); + } +} diff --git a/crates/bootstrap/derive/src/main_impl.rs b/crates/bootstrap/derive/src/main_impl.rs index 36f7a1470..a460d22c8 100644 --- a/crates/bootstrap/derive/src/main_impl.rs +++ b/crates/bootstrap/derive/src/main_impl.rs @@ -215,6 +215,8 @@ pub fn impl_main(input: TokenStream) -> Result { #(#services)* + #entry_as_global::on_services_start(&#global_ident).await?; + macro_rules! handle_service_exit { ($remaining:ident) => {{ let ((name, result), _, remaining) = #crate_path::prelude::futures::future::select_all($remaining).await; diff --git a/crates/bootstrap/derive/src/service_impl.rs b/crates/bootstrap/derive/src/service_impl.rs deleted file mode 100644 index c21a07aa1..000000000 --- a/crates/bootstrap/derive/src/service_impl.rs +++ /dev/null @@ -1,7 +0,0 @@ -#![allow(unused)] - -use proc_macro2::TokenStream; - -pub fn impl_service(input: TokenStream, args: TokenStream) -> Result { - todo!() -} diff --git a/crates/bootstrap/src/config.rs b/crates/bootstrap/src/config.rs index 720c788a1..b8edd57ed 100644 --- a/crates/bootstrap/src/config.rs +++ b/crates/bootstrap/src/config.rs @@ -1,3 +1,13 @@ +/// This trait is used to parse a configuration for the application. +/// +/// The avoid having to manually implement this trait, the `bootstrap!` macro in +/// the [`scuffle_settings`](../../scuffle_settings) crate can be used to +/// generate an implementation. +/// +/// # See Also +/// +/// - [`Global`](crate::Global) +/// - [`scuffle_settings`](../../scuffle_settings) pub trait ConfigParser: Sized { fn parse() -> impl std::future::Future>; } @@ -17,3 +27,19 @@ impl ConfigParser for EmptyConfig { std::future::ready(Ok(EmptyConfig)) } } + +#[cfg(test)] +#[cfg_attr(all(test, coverage_nightly), coverage(off))] +mod tests { + use super::{ConfigParser, EmptyConfig}; + + #[tokio::test] + async fn unit_config() { + assert!(matches!(<()>::parse().await, Ok(()))); + } + + #[tokio::test] + async fn empty_config() { + assert!(matches!(EmptyConfig::parse().await, Ok(EmptyConfig))); + } +} diff --git a/crates/bootstrap/src/global.rs b/crates/bootstrap/src/global.rs index 35cd09055..5d4118511 100644 --- a/crates/bootstrap/src/global.rs +++ b/crates/bootstrap/src/global.rs @@ -1,8 +1,18 @@ +//! Global state for the application. +//! +//! # [`Global`] vs. [`GlobalWithoutConfig`] +//! +//! [`Global`] has a set of functions that are called at different stages of the +//! application lifecycle. To use [`Global`], your application is expected to +//! have a config type implementing [`ConfigParser`]. If your application does +//! not have a config, consider using the [`GlobalWithoutConfig`] trait which is +//! a simplified version of [`Global`]. + use std::sync::Arc; use crate::config::{ConfigParser, EmptyConfig}; -fn default_runtime() -> tokio::runtime::Runtime { +fn default_runtime_builder() -> tokio::runtime::Builder { let worker_threads = std::env::var("TOKIO_WORKER_THREADS") .unwrap_or_default() .parse::() @@ -66,88 +76,202 @@ fn default_runtime() -> tokio::runtime::Runtime { builder.max_io_events_per_tick(max_io_events_per_tick); } - builder.build().expect("runtime build") + builder } +/// This trait is implemented for the global type of your application. +/// It is intended to be used to store any global state of your application. +/// When using the [`main!`](crate::main) macro, one instance of this type will +/// be made available to all services. +/// +/// Using this trait requires a config type implementing [`ConfigParser`]. +/// If your application does not have a config, consider using the +/// [`GlobalWithoutConfig`] trait. +/// +/// # See Also +/// +/// - [`GlobalWithoutConfig`] +/// - [`Service`](crate::Service) +/// - [`main`](crate::main) pub trait Global: Send + Sync + 'static { type Config: ConfigParser + Send + 'static; - /// Builds the tokio runtime for the application. + /// Pre-initialization. + /// + /// Called before initializing the tokio runtime and loading the config. + /// Returning an error from this function will cause the process to + /// immediately exit without calling [`on_exit`](Global::on_exit) first. #[inline(always)] - fn tokio_runtime() -> tokio::runtime::Runtime { - default_runtime() + fn pre_init() -> anyhow::Result<()> { + Ok(()) } - /// Called before loading the config. + /// Builds the tokio runtime for the process. + /// + /// If not overridden, a default runtime builder is used to build the + /// runtime. It uses the following environment variables: + /// - `TOKIO_WORKER_THREADS`: Number of worker threads to use. If 1, a + /// current thread runtime is used. + /// + /// See [`tokio::runtime::Builder::worker_threads`] for details. + /// - `TOKIO_MAX_BLOCKING_THREADS`: Maximum number of blocking threads. + /// + /// See [`tokio::runtime::Builder::max_blocking_threads`] for details. + /// - `TOKIO_DISABLE_TIME`: If `true` disables time. + /// + /// See [`tokio::runtime::Builder::enable_time`] for details. + /// - `TOKIO_DISABLE_IO`: If `true` disables IO. + /// + /// See [`tokio::runtime::Builder::enable_io`] for details. + /// - `TOKIO_THREAD_STACK_SIZE`: Thread stack size. + /// + /// See [`tokio::runtime::Builder::thread_stack_size`] for details. + /// - `TOKIO_GLOBAL_QUEUE_INTERVAL`: Global queue interval. + /// + /// See [`tokio::runtime::Builder::global_queue_interval`] for details. + /// - `TOKIO_EVENT_INTERVAL`: Event interval. + /// + /// See [`tokio::runtime::Builder::event_interval`] for details. + /// - `TOKIO_MAX_IO_EVENTS_PER_TICK`: Maximum IO events per tick. + /// + /// See [`tokio::runtime::Builder::max_io_events_per_tick`] for details. #[inline(always)] - fn pre_init() -> anyhow::Result<()> { - Ok(()) + fn tokio_runtime() -> tokio::runtime::Runtime { + default_runtime_builder().build().expect("runtime build") } /// Initialize the global. + /// + /// Called to initialize the global. + /// Returning an error from this function will cause the process to + /// immediately exit without calling [`on_exit`](Global::on_exit) first. fn init(config: Self::Config) -> impl std::future::Future>> + Send; - /// Called when all services have been started. + /// Called right before all services start. + /// + /// Returning an error from this function will prevent any service from + /// starting and [`on_exit`](Global::on_exit) will be called with the result + /// of this function. #[inline(always)] fn on_services_start(self: &Arc) -> impl std::future::Future> + Send { std::future::ready(Ok(())) } - /// Called when the shutdown process is complete, right before exiting the - /// process. + /// Called after a service exits. + /// + /// `name` is the name of the service that exited and `result` is the result + /// the service exited with. Returning an error from this function will + /// stop all currently running services and [`on_exit`](Global::on_exit) + /// will be called with the result of this function. #[inline(always)] - fn on_exit( + fn on_service_exit( self: &Arc, + name: &'static str, result: anyhow::Result<()>, ) -> impl std::future::Future> + Send { + let _ = name; std::future::ready(result) } - /// Called when a service exits. + /// Called after the shutdown is complete, right before exiting the + /// process. + /// + /// `result` is [`Err`](anyhow::Result) when the process exits due to an + /// error in one of the services or handler functions, otherwise `Ok(())`. #[inline(always)] - fn on_service_exit( + fn on_exit( self: &Arc, - name: &'static str, result: anyhow::Result<()>, ) -> impl std::future::Future> + Send { - let _ = name; std::future::ready(result) } } +/// Simplified version of [`Global`]. +/// +/// Implementing this trait will automatically implement [`Global`] for your +/// type. This trait is intended to be used when you don't need a config for +/// your global. +/// +/// Refer to [`Global`] for details. pub trait GlobalWithoutConfig: Send + Sync + 'static { + /// Builds the tokio runtime for the process. + /// + /// If not overridden, a default runtime builder is used to build the + /// runtime. It uses the following environment variables: + /// - `TOKIO_WORKER_THREADS`: Number of worker threads to use. If 1, a + /// current thread runtime is used. + /// + /// See [`tokio::runtime::Builder::worker_threads`] for details. + /// - `TOKIO_MAX_BLOCKING_THREADS`: Maximum number of blocking threads. + /// + /// See [`tokio::runtime::Builder::max_blocking_threads`] for details. + /// - `TOKIO_DISABLE_TIME`: If `true` disables time. + /// + /// See [`tokio::runtime::Builder::enable_time`] for details. + /// - `TOKIO_DISABLE_IO`: If `true` disables IO. + /// + /// See [`tokio::runtime::Builder::enable_io`] for details. + /// - `TOKIO_THREAD_STACK_SIZE`: Thread stack size. + /// + /// See [`tokio::runtime::Builder::thread_stack_size`] for details. + /// - `TOKIO_GLOBAL_QUEUE_INTERVAL`: Global queue interval. + /// + /// See [`tokio::runtime::Builder::global_queue_interval`] for details. + /// - `TOKIO_EVENT_INTERVAL`: Event interval. + /// + /// See [`tokio::runtime::Builder::event_interval`] for details. + /// - `TOKIO_MAX_IO_EVENTS_PER_TICK`: Maximum IO events per tick. + /// + /// See [`tokio::runtime::Builder::max_io_events_per_tick`] for details. #[inline(always)] fn tokio_runtime() -> tokio::runtime::Runtime { - default_runtime() + default_runtime_builder().build().expect("runtime build") } /// Initialize the global. + /// + /// Called to initialize the global. + /// Returning an error from this function will cause the process to + /// immediately exit without calling [`on_exit`](Global::on_exit) first. fn init() -> impl std::future::Future>> + Send; - /// Called when all services have been started. + /// Called right before all services start. + /// + /// Returning an error from this function will prevent any service from + /// starting and [`on_exit`](Global::on_exit) will be called with the result + /// of this function. #[inline(always)] fn on_services_start(self: &Arc) -> impl std::future::Future> + Send { std::future::ready(Ok(())) } - /// Called when the shutdown process is complete, right before exiting the - /// process. + /// Called after a service exits. + /// + /// `name` is the name of the service that exited and `result` is the result + /// the service exited with. Returning an error from this function will + /// stop all currently running services and [`on_exit`](Global::on_exit) + /// will be called with the result of this function. #[inline(always)] - fn on_exit( + fn on_service_exit( self: &Arc, + name: &'static str, result: anyhow::Result<()>, ) -> impl std::future::Future> + Send { + let _ = name; std::future::ready(result) } - /// Called when a service exits. + /// Called after the shutdown is complete, right before exiting the + /// process. + /// + /// `result` is [`Err`](anyhow::Result) when the process exits due to an + /// error in one of the services or handler functions, otherwise `Ok(())`. #[inline(always)] - fn on_service_exit( + fn on_exit( self: &Arc, - name: &'static str, result: anyhow::Result<()>, ) -> impl std::future::Future> + Send { - let _ = name; std::future::ready(result) } } @@ -157,10 +281,7 @@ impl Global for T { #[inline(always)] fn tokio_runtime() -> tokio::runtime::Runtime { - tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .expect("runtime build") + ::tokio_runtime() } #[inline(always)] @@ -168,6 +289,11 @@ impl Global for T { ::init() } + #[inline(always)] + fn on_services_start(self: &Arc) -> impl std::future::Future> + Send { + ::on_services_start(self) + } + #[inline(always)] fn on_service_exit( self: &Arc, @@ -177,11 +303,6 @@ impl Global for T { ::on_service_exit(self, name, result) } - #[inline(always)] - fn on_services_start(self: &Arc) -> impl std::future::Future> + Send { - ::on_services_start(self) - } - #[inline(always)] fn on_exit( self: &Arc, @@ -190,3 +311,70 @@ impl Global for T { ::on_exit(self, result) } } + +#[cfg(test)] +#[cfg_attr(all(test, coverage_nightly), coverage(off))] +mod tests { + use std::sync::Arc; + use std::thread; + + use super::{Global, GlobalWithoutConfig}; + use crate::EmptyConfig; + + struct TestGlobal; + + impl Global for TestGlobal { + type Config = (); + + async fn init(_config: Self::Config) -> anyhow::Result> { + Ok(Arc::new(Self)) + } + } + + #[tokio::test] + async fn default_global() { + thread::spawn(|| { + // To get the coverage + TestGlobal::tokio_runtime(); + }); + + assert!(matches!(TestGlobal::pre_init(), Ok(()))); + let global = TestGlobal::init(()).await.unwrap(); + assert!(matches!(global.on_services_start().await, Ok(()))); + + assert!(matches!(global.on_exit(Ok(())).await, Ok(()))); + assert!(global.on_exit(Err(anyhow::anyhow!("error"))).await.is_err()); + + assert!(matches!(global.on_service_exit("test", Ok(())).await, Ok(()))); + assert!(global.on_service_exit("test", Err(anyhow::anyhow!("error"))).await.is_err()); + } + + struct TestGlobalWithoutConfig; + + impl GlobalWithoutConfig for TestGlobalWithoutConfig { + async fn init() -> anyhow::Result> { + Ok(Arc::new(Self)) + } + } + + #[tokio::test] + async fn default_global_no_config() { + thread::spawn(|| { + // To get the coverage + ::tokio_runtime(); + }); + + assert!(matches!(TestGlobalWithoutConfig::pre_init(), Ok(()))); + ::init(EmptyConfig).await.unwrap(); + let global = ::init().await.unwrap(); + assert!(matches!(Global::on_services_start(&global).await, Ok(()))); + + assert!(matches!(Global::on_exit(&global, Ok(())).await, Ok(()))); + assert!(Global::on_exit(&global, Err(anyhow::anyhow!("error"))).await.is_err()); + + assert!(matches!(Global::on_service_exit(&global, "test", Ok(())).await, Ok(()))); + assert!(Global::on_service_exit(&global, "test", Err(anyhow::anyhow!("error"))) + .await + .is_err()); + } +} diff --git a/crates/bootstrap/src/lib.rs b/crates/bootstrap/src/lib.rs index fdf599262..30f20a6cf 100644 --- a/crates/bootstrap/src/lib.rs +++ b/crates/bootstrap/src/lib.rs @@ -1,13 +1,115 @@ +//! # scuffle-bootstrap +//! +//! A utility crate for creating binaries. +//! +//! Refer to [`Global`], [`Service`], and [`main`] for more information. +//! +//! ## Usage +//! +//! ```rust,no_run +//! use std::sync::Arc; +//! +//! /// Our global state +//! struct Global; +//! +//! // Required by the signal service +//! impl scuffle_signal::SignalConfig for Global {} +//! +//! impl scuffle_bootstrap::global::GlobalWithoutConfig for Global { +//! async fn init() -> anyhow::Result> { +//! Ok(Arc::new(Self)) +//! } +//! } +//! +//! /// Our own custom service +//! struct MySvc; +//! +//! impl scuffle_bootstrap::service::Service for MySvc { +//! async fn run(self, global: Arc, ctx: scuffle_context::Context) -> anyhow::Result<()> { +//! # let _ = global; +//! println!("running"); +//! +//! // Do some work here +//! +//! // Wait for the context to be cacelled by the signal service +//! ctx.done().await; +//! Ok(()) +//! } +//! } +//! +//! // This generates the main function which runs all the services +//! scuffle_bootstrap::main! { +//! Global { +//! scuffle_signal::SignalSvc, +//! MySvc, +//! } +//! } +//! ``` +//! +//! ## License +//! +//! This project is licensed under the [MIT](./LICENSE.MIT) or +//! [Apache-2.0](./LICENSE.Apache-2.0) license. You can choose between one of +//! them if you use this work. +//! +//! `SPDX-License-Identifier: MIT OR Apache-2.0` +#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))] + pub mod config; pub mod global; pub mod service; -pub use config::{ConfigParser, EmptyConfig}; -pub use global::Global; -pub use scuffle_bootstrap_derive::main; +pub use config::ConfigParser; +#[doc(hidden)] +pub use config::EmptyConfig; +pub use global::{Global, GlobalWithoutConfig}; pub use service::Service; #[doc(hidden)] pub mod prelude { - pub use {anyhow, futures, scuffle_context, tokio}; + pub use {anyhow, futures, scuffle_bootstrap_derive, scuffle_context, tokio}; +} + +/// This macro is used to generate the main function for a given global type +/// and service types. It will run all the services in parallel and wait for +/// them to finish before exiting. +/// +/// # Example +/// +/// ```rust +/// # use std::sync::Arc; +/// # struct MyGlobal; +/// # struct MyService; +/// # impl scuffle_bootstrap::global::GlobalWithoutConfig for MyGlobal { +/// # async fn init() -> anyhow::Result> { +/// # Ok(Arc::new(Self)) +/// # } +/// # } +/// # impl scuffle_bootstrap::service::Service for MyService { +/// # async fn run(self, global: Arc, ctx: scuffle_context::Context) -> anyhow::Result<()> { +/// # println!("running"); +/// # ctx.done().await; +/// # Ok(()) +/// # } +/// # } +/// # impl scuffle_signal::SignalConfig for MyGlobal { +/// # } +/// scuffle_bootstrap::main! { +/// MyGlobal { +/// scuffle_signal::SignalSvc, +/// MyService, +/// } +/// } +/// ``` +/// +/// # See Also +/// +/// - [`Service`](crate::Service) +/// - [`Global`](crate::Global) +// We wrap the macro here so that the doc tests can be run & that the docs resolve for `Service` & `Global` +#[macro_export] +macro_rules! main { + ($($body:tt)*) => { + $crate::prelude::scuffle_bootstrap_derive::main! { $($body)* } + }; } diff --git a/crates/bootstrap/src/service.rs b/crates/bootstrap/src/service.rs index 8b79d613a..30d18cf79 100644 --- a/crates/bootstrap/src/service.rs +++ b/crates/bootstrap/src/service.rs @@ -2,17 +2,50 @@ use std::pin::Pin; use std::sync::Arc; use std::task::{ready, Context, Poll}; +#[cfg(any(test, doctest))] +#[doc(hidden)] +pub use scuffle_signal::SignalSvc; + +/// A service that can be run. +/// +/// This trait is used to define a service that can be run in parallel to other +/// services. +/// +/// # See Also +/// +/// - [`Global`](crate::Global) +/// - [`GlobalWithoutConfig`](crate::GlobalWithoutConfig) +/// - [`main`](crate::main) pub trait Service: Send + Sync + 'static + Sized { + /// Returns the name of the service, if any. fn name(&self) -> Option<&'static str> { None } - /// Initialize the service + /// Initialize the service and return `Ok(true)` if the service should be + /// run. fn enabled(&self, global: &Arc) -> impl std::future::Future> + Send { let _ = global; std::future::ready(Ok(true)) } + /// Run the service. + /// This function should return a future that is pending as long as the + /// service is running. When the service finishes without any errors, + /// the future should resolve to `Ok(())`. As a best practice, the + /// service should stop as soon as the provided context is done. + /// + /// Note: Adding the + /// [`scuffle_signal::SignalSvc`](../../scuffle_signal/struct.SignalSvc. + /// html) service to the list of services when calling + /// [`main`](crate::main) will cancel the context as soon as a shutdown + /// signal is received. + /// + /// # See Also + /// + /// - [`Context`](scuffle_context::Context) + /// - [`scuffle_signal::SignalSvc`](../../scuffle_signal/struct.SignalSvc. + /// html) fn run( self, global: Arc, @@ -67,3 +100,59 @@ where Poll::Ready((this.name, res)) } } + +#[cfg(test)] +#[cfg_attr(all(test, coverage_nightly), coverage(off))] +mod tests { + use std::sync::Arc; + + use scuffle_future_ext::FutureExt; + + use super::{NamedFuture, Service}; + + struct DefaultService; + + impl Service<()> for DefaultService {} + + #[tokio::test] + async fn defaukt_service() { + let svc = DefaultService; + let global = Arc::new(()); + let (ctx, handler) = scuffle_context::Context::new(); + + assert_eq!(svc.name(), None); + assert!(svc.enabled(&global).await.unwrap()); + + handler.cancel(); + + assert!(matches!(svc.run(global, ctx).await, Ok(()))); + + assert!(handler + .shutdown() + .with_timeout(tokio::time::Duration::from_millis(200)) + .await + .is_ok()); + } + + #[tokio::test] + async fn future_service() { + let (ctx, handler) = scuffle_context::Context::new(); + let global = Arc::new(()); + + let fut_fn = |_global: Arc<()>, _ctx: scuffle_context::Context| async { anyhow::Result::<()>::Ok(()) }; + assert!(fut_fn.run(global, ctx).await.is_ok()); + + handler.cancel(); + assert!(handler + .shutdown() + .with_timeout(tokio::time::Duration::from_millis(200)) + .await + .is_ok()); + } + + #[tokio::test] + async fn named_future() { + let named_fut = NamedFuture::new("test", async { 42 }); + assert_eq!(named_fut.await, ("test", 42)); + } +} diff --git a/crates/context/src/ext.rs b/crates/context/src/ext.rs index 86cb36653..7c6a55f69 100644 --- a/crates/context/src/ext.rs +++ b/crates/context/src/ext.rs @@ -7,7 +7,10 @@ use tokio_util::sync::{WaitForCancellationFuture, WaitForCancellationFutureOwned use crate::{Context, ContextTracker}; -/// This type is used to make the inner enum [`ContextRefInner`] private. +/// A reference to a context which implements [`Future`] and can be polled. +/// Can either be owned or borrowed. +/// +/// Create by using the [`From`] implementations. pub struct ContextRef<'a> { inner: ContextRefInner<'a>, } @@ -34,10 +37,6 @@ impl<'a> From<&'a Context> for ContextRef<'a> { } pin_project_lite::pin_project! { - /// A reference to a context which implements [`Future`] and can be polled. - /// Can either be owned or borrowed. - /// - /// Create by using the [`From`] implementations. #[project = ContextRefInnerProj] enum ContextRefInner<'a> { Owned { diff --git a/crates/context/src/lib.rs b/crates/context/src/lib.rs index 5ced1ae4b..69034eb0c 100644 --- a/crates/context/src/lib.rs +++ b/crates/context/src/lib.rs @@ -1,12 +1,5 @@ //! # scuffle-context //! -//! > WARNING -//! > This crate is under active development and may not be stable. -//! -//! [![crates.io](https://img.shields.io/crates/v/scuffle-context.svg)](https://crates.io/crates/scuffle-context) [![docs.rs](https://img.shields.io/docsrs/scuffle-context)](https://docs.rs/scuffle-context) -//! -//! --- -//! //! A crate designed to provide the ability to cancel futures using a context //! go-like approach, allowing for graceful shutdowns and cancellations. //! diff --git a/crates/signal/Cargo.toml b/crates/signal/Cargo.toml index 03e3a1eef..786db400c 100644 --- a/crates/signal/Cargo.toml +++ b/crates/signal/Cargo.toml @@ -25,7 +25,7 @@ tokio = { version = "1.41.1", features = ["macros", "rt", "time"] } tokio-test = "0.4.4" libc = "0.2" futures = "0.3" -scuffle-future-ext = { path = "../future-ext" } +scuffle-future-ext.workspace = true [features] bootstrap = ["scuffle-bootstrap", "scuffle-context", "anyhow", "tokio/macros"] diff --git a/crates/signal/src/lib.rs b/crates/signal/src/lib.rs index 164aecac3..0c1c17c0a 100644 --- a/crates/signal/src/lib.rs +++ b/crates/signal/src/lib.rs @@ -1,12 +1,5 @@ //! # scuffle-signal //! -//! > WARNING -//! > This crate is under active development and may not be stable. -//! -//! [![crates.io](https://img.shields.io/crates/v/scuffle-signal.svg)](https://crates.io/crates/scuffle-signal) [![docs.rs](https://img.shields.io/docsrs/scuffle-signal)](https://docs.rs/scuffle-signal) -//! -//! --- -//! //! A crate designed to provide a more user friendly interface to //! `tokio::signal`. //!