dynamic-config-etcd
Read dynamic-config configuration from an etcd v3 key/value store.
[]
= { = "0.0.1", = ["async"] }
= "0.0.1"
use Etcd;
set_remote_async;
// Fetching is explicit; the load that follows touches no network.
refresh_remote_async.await?;
etcd speaks gRPC, so its client is async — which is why this implements the
async AsyncRemoteSource trait rather than the blocking one. Pretending
otherwise would only hide a block_on inside the crate.
What it reads
One key, whose value is a whole configuration document — the same bytes that
would be in a config file. The format comes from the key's extension, or from
with_format.
Connecting
Credentials and TLS go through etcd's own ConnectOptions, re-exported here so
using them needs no direct dependency on etcd-client. There is no second
vocabulary to learn, and options this crate has never heard of keep working.
use ;
let etcd = with_options
.await?;
TLS types — TlsOptions, Identity, Certificate — are behind this crate's
tls feature, because TLS pulls a whole stack in and a program talking to etcd
over a private network inside a cluster has no use for it. tls-roots also
trusts the platform's root store, which is what a public CA needs.
new does not prove the server is reachable. The client connects lazily, so
an unreachable etcd surfaces on the first read rather than at construction. That
is the client's behaviour, not a choice made here, and papering over it with an
eager round trip would make every construction cost one.
Sharing a client
let etcd = from_client;
For a caller that already talks to etcd and would rather not open a second
connection. The client is Clone — cheaply, it is a handle — so sharing one
costs nothing, and a shared client recovers from an expired token like any other.
Expiry
etcd issues simple auth tokens with a TTL, five minutes by default, and a
long-lived configuration reader outlives one. On invalid auth token this asks
for a new one and retries, once.
Not a reconnect: the gRPC channel looks after itself, and the client kept the credentials, so the thing that actually expired is the only thing replaced. That is also what lets a shared client recover — replacing a client the caller owns would not be this crate's to do.
Once, not in a loop: if a fresh token is refused too, the credentials are wrong, and retrying would turn a clear failure into a hang.
Watching
etcd's watch is a real push stream, so watch is a future the caller spawns and
cancels by dropping — no runtime is imposed and no flag is polled.
let task = spawn;
// Dropping or aborting the task stops the watch.
task.abort;
- The current value is not delivered at startup: a watch reports changes, and announcing the value the caller already has would make every restart look like an edit. Fetch first if the starting value matters.
- A deletion is not a change. No configuration is not a configuration, and neither replaying the last one nor pushing emptiness beats leaving the running snapshot alone.
- It never returns
Ok. A watch either runs or has failed; a silent success would leave a spawned task finished and a configuration frozen with nothing said about either. A cancelled watch — compaction is the usual reason — and a closed connection are both reported. Loop around it to reconnect.
Builders
| Method | Default |
|---|---|
with_format(..) |
from the key's extension |
with_options(..) |
ConnectOptions::new() — no credentials, no TLS |
from_client(..) |
opens its own connection |
Example
| Example | Shows |
|---|---|
etcd_watching |
Connecting with ConnectOptions, watching a push stream, and a task awaiting reloads alongside it. |
It needs a server, and its own doc comment says how to start one in a container and put a document in it.
Testing
The test suite drives a real etcd in a container — no mocks, including one started with authentication enabled and a one-second token TTL. That is how the lazy-connect behaviour above was found: a mock would have confirmed what we already believed instead.
The image comes from quay.io, which has no anonymous pull limits.
MSRV
1.85 — higher than dynamic-config's own 1.71, because a gRPC stack moves
faster than that crate wants to. A companion pays for what it pulls in; the core
stays where it is.
License
MIT