dynamic-config 0.0.1

Hot-reloadable, lock-free application configuration with a one-attribute API, built on figment.
Documentation
//! The async surface on Embassy — an executor written for microcontrollers.
//!
//! ```text
//! cargo run -p dynamic-config --example embassy_runtime --features async,json
//! ```
//!
//! Embassy is about as far from tokio as an executor gets: no thread pool, no
//! reactor, no allocation in the scheduler. It is here for exactly that reason.
//! `changes()` is a hand-written `Future` over a generation counter and a list
//! of wakers — `std`, and nothing else — so if Embassy drives it, the claim that
//! this crate is runtime-agnostic means something.
//!
//! Embassy's `platform-std` runs its executor on a host, which is what makes
//! this runnable; the shape on a device is the same, minus the filesystem.
//!
//! # What an embedded program would actually do
//!
//! Not this. `#[dynamic_config]` reads files and the environment, and a
//! microcontroller has neither. What it *does* have is a remote store or a
//! serial link, and both arrive through the same door: hand a document to
//! `apply_remote`, and every reader wakes up. That path is `std`-only today —
//! see the roadmap — but the async half is already runtime-free, and this
//! example is the part that can be shown now.

use dynamic_config::{dynamic_config, Fetched, Format};
use embassy_executor::{Executor, Spawner};
use serde::Deserialize;
use static_cell::StaticCell;

#[dynamic_config(files = [], key = "server", env = "APP_", async)]
#[derive(Debug, Deserialize)]
struct ServerConfig {
    greeting: String,
    workers: usize,
}

/// A task that waits for configuration to change and does something about it.
///
/// No channel type, no runtime handle, no `Send` bound imposed by an executor:
/// it awaits a `Future` this crate returns, and Embassy polls it.
///
/// It runs forever; the pusher decides when the example is over.
#[embassy_executor::task]
async fn reader() {
    let mut changes = ServerConfig::changes();

    loop {
        let config = changes.changed().await;

        println!(
            "  reader woke: {} ({} workers)",
            config.greeting, config.workers
        );
    }
}

/// Stands in for whatever pushes configuration on a device — a remote store, a
/// serial link, a message on a bus.
#[embassy_executor::task]
async fn pusher() {
    // The reader's handle observes changes made *after* it exists, and it does
    // not exist until the reader task is first polled. Which task an executor
    // polls first is its business, not ours, so this yields before pushing
    // anything rather than assuming an order — the same reason a real program
    // starts its watch after the rest of it is up.
    embassy_futures::yield_now().await;

    println!("two pushes with no turn for the reader in between:");

    push("first", 1);
    push("second", 2);

    // A yield, not a timer: no clock, nothing a microcontroller would have to
    // configure. Two, so the reader is certainly polled before the next push.
    embassy_futures::yield_now().await;
    embassy_futures::yield_now().await;

    println!("\n  ...one wakeup, carrying the *latest* of them.");
    println!("  Reloads that land while nothing is awaiting are not queued:");
    println!("  waking to the newest configuration is what a reader wants, and");
    println!("  a queue would hand it stale ones first.\n");

    println!("and one more, with the reader waiting:");
    push("third", 3);

    embassy_futures::yield_now().await;
    embassy_futures::yield_now().await;

    println!("\nDriven by an executor with no threads, no reactor and no");
    println!("allocation in its scheduler. `changes()` is a `Future` over a");
    println!("generation counter and a list of wakers — `std`, and nothing else.");

    std::process::exit(0);
}

/// Applies one document, the way a watch loop would.
fn push(greeting: &str, workers: usize) {
    let document = format!(r#"{{"server": {{"greeting": "{greeting}", "workers": {workers}}}}}"#);

    if let Err(error) = ServerConfig::apply_remote(Fetched::new(document, Format::Json)) {
        println!("  the document did not apply: {error}");
    }
}

#[embassy_executor::task]
async fn start(spawner: Spawner) {
    // A `#[task]` hands back a token or says the pool is full; the spawner
    // itself is infallible once it has one.
    spawner.spawn(reader().expect("the task pool has room"));
    spawner.spawn(pusher().expect("the task pool has room"));
}

static EXECUTOR: StaticCell<Executor> = StaticCell::new();

fn main() {
    let executor = EXECUTOR.init(Executor::new());

    executor.run(|spawner| spawner.spawn(start(spawner).expect("the task pool has room")));
}