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

etcd

dynamic-config-etcd reads configuration from an etcd v3 key/value store. etcd speaks gRPC, so its client is async and this crate implements AsyncRemoteSource — pretending otherwise would only hide a block_on inside it.

[dependencies]
dynamic-config = { version = "<version>", features = ["async"] }
dynamic-config-etcd = "<version>"
#![allow(unused)]
fn main() {
use dynamic_config_etcd::Etcd;

DbConfig::set_remote_async(
    Etcd::new(["http://etcd.internal:2379"], "myapp/db.json").await?,
);
DbConfig::refresh_remote_async().await?;   // the round trip, explicitly
}

What it reads: one key holding a whole configuration document — the same bytes a config file would hold; the format comes from the key's extension, or with_format.

Several keys as one document: Keys::several([..]) merges named keys in call order — later wins, the rule .file(..) already teaches — and Keys::prefix("myapp/") merges the sections under a prefix, where an overlap between two of them is an error naming both keys and the paths. Both are one round trip and both are read at a single etcd revision: a list goes as a transaction of range reads, a prefix as one range read, so a write landing mid-read cannot tear the document. A list is capped at etcd's own --max-txn-ops (128); a prefix is capped at 512 keys. See several keys as one document for what this costs in provenance and in watching.

Connecting: credentials and TLS go through etcd's own ConnectOptions, re-exported so there is no second vocabulary and no direct etcd-client dependency. TLS types sit behind the crate's tls feature (a private-network deployment has no use for the stack), and tls-roots adds the platform's root store. from_client shares a connection the program already has. One honest sharp edge: the client connects lazily, so an unreachable etcd surfaces on the first read, not at construction.

TLS as data: Etcd::with_tls(endpoints, keys, options, tls) takes a private certificate authority and a client certificate as paths or PEM bytes, with no tonic type in the calling code — the same TlsConfig every store in this family takes. Behind the tls feature, because that is what buys the stack. ConnectOptions still carries everything that is not TLS, and the TlsConfig owns the TLS slot. mTLS matters more here than elsewhere: an etcd started with --client-cert-auth is the ordinary hardened deployment. TLS, and the one vocabulary all eight speak is the whole story, including why there is no way to turn verification off.

Expiry: etcd's auth tokens carry a TTL (five minutes by default). On invalid auth token the crate logs in again and retries — once; a fresh token refused too means the policy is wrong, and retrying would turn a clear failure into a hang.

Watching: a real push stream — watch is a future the caller spawns and cancels by dropping, on any executor. The current value is not delivered at startup, a deletion is not a change, and the future never returns Ok: a watch either runs or has failed (compaction and a closed connection are the usual reasons) — loop around it to reconnect. A token refresh that works resumes from the last delivered revision.

A prefix can be watched; a named list cannot. One stream over one range says the range moved and carries the revision it moved at, and one range read at that revision is the whole set as of one instant — so the document delivered is a state the cluster really was in. A named list would be one stream per key, and nothing about N independent streams says the set moved together, so it is refused at watch(). Under a prefix a deletion is a change, because the set is what is left after it: spurious, never torn.

A failing watch says so: reporting_to(sink) hands the loop the same RemoteSink it already delivers through, and every attempt that comes back with nothing is reported to it — the stream erroring, etcd cancelling the watch, the range read at an event's revision failing, a document that will not merge, and the connection closing under it. Without it a watch is the half of a store dynamic-config cannot see: only deliveries are recorded, so dynamic_config_remote_up describes the last delivery rather than the last attempt, and a store that stopped answering an hour ago reads as healthy until something calls refresh_remote_async(). What a failure moves is narrow on purpose — the streak and the last failure, never the staleness clock — so up goes to zero while remote_last_fetch_seconds keeps ageing, which is the pair an alert wants. See telemetry.

A replaced auth token is not a failure. etcd's simple tokens expire, so a watch that outlives one logs in again and resumes from the last delivered revision; nothing is reported for that, because the store answered and no event was lost. Reporting it would drive up to zero every time a five-minute token turned over on a healthy cluster — and, since only a delivery or a fetch clears the streak, it would stay there until the next change. A re-authentication that fails, a stream that will not re-establish, and a recovery cap that runs out all report.

The README carries the full story, the builder-defaults table and the etcd_watching example; MSRV 1.85.