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

Generic configuration types

Config<Postgres> and Config<Mysql> are different types, so they get different snapshots:

#![allow(unused)]
fn main() {
#[dynamic_config(files = ["config.toml"], key = "db")]
#[derive(Debug, Deserialize)]
struct Db<D: Driver> {
    url: String,
    #[serde(skip)]
    driver: PhantomData<fn() -> D>,   // `fn() -> D`, so the marker stays Send + Sync
}

Db::<Postgres>::init()?;
Db::<Mysql>::init()?;                 // its own snapshot, its own layers
}

Type and const parameters both work. A lifetime parameter does not, and is rejected at compile time: the snapshot outlives every borrow that could name one.

It is not free, so you only pay for it if you use it

Rust has no generic statics, so a generic type's snapshot cannot live in one. It goes through a TypeId-keyed registry instead. Measured on this machine with cargo bench -p dynamic-config --features json, 5M reads each:

Storagecurrent()
static ConfigCell (non-generic)17 ns
TypeId registry (generic)27 ns

The macro knows which shape it is emitting, so a non-generic config type keeps its static and its 17 ns — adding generic support cost existing code nothing. The registry read is lock-free (an ArcSwap of the table, and TypeId passed through rather than hashed); the first naive version, with an RwLock and SipHash, measured 64 ns.

Either figure is noise next to a request. Both are the cost of taking a snapshot, not of reading fields from one — take it once per unit of work and the question stops mattering.