Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Async & Runtimes

The async feature

With the async feature, the attribute generates load_async(), init_async() and changes() — there is no argument to opt in with, and an unused changes() costs nothing. The feature pulls in no runtime at all. See Async.

Async

The async feature brings in no runtime. changes() is a Future, so tokio, async-std, smol and a hand-written executor all drive it identically:

#![allow(unused)]
fn main() {
#[dynamic_config]
#[derive(Debug, Deserialize)]
struct DbConfig { pool_size: u32 }

let builder = DbConfig::builder("db").file("config.toml");
builder.init_async().await?;
builder.watch(Duration::from_millis(250))?.detach();

let mut changes = DbConfig::changes();

spawn(async move {
    loop {
        let config = changes.changed().await;
        pool.resize(config.pool_size);
    }
});
}

The snapshot current when changes() is called counts as already seen, so the first changed().await waits for the next reload. A handle created before init() has seen nothing, so the initial install is its first change — changes() doubles as "wake me when configuration exists", and that is contract, not accident. Reloads that land while nothing is awaiting are not queued — waking up to the latest configuration is what a reader wants, and a queue would hand it stale ones first.

Where the blocking work goes

Reading configuration touches the filesystem, so load_async moves it off the executor. Where is the one genuinely runtime-specific part, so it is pluggable:

Setupload_async uses
tokio featuretokio::task::spawn_blocking
set_blocking_executor installedthat executor
neithera freshly spawned thread

A configuration load happens at startup and on reload, so a thread per call is a real answer rather than a placeholder. For async-std or smol, hand the crate its pool once:

#![allow(unused)]
fn main() {
struct AsyncStd;

impl BlockingExecutor for AsyncStd {
    fn execute(&self, work: Box<dyn FnOnce() + Send + 'static>) {
        async_std::task::spawn_blocking(work);
    }
}

dynamic_config::set_blocking_executor(AsyncStd)?;
}

The watcher itself stays on a plain thread whatever you choose: notify's channel is synchronous, and keeping it off the runtime means file watching works whether or not one is running.