Expand description
Read dynamic-config configuration from an etcd v3 key/value store.
etcd speaks gRPC, so its client is async — which is why this implements the
async AsyncRemoteSource trait rather than the blocking one.
use dynamic_config_etcd::Etcd;
DbConfig::set_remote_async(
Etcd::new(["http://etcd.internal:2379"], "myapp/db.json").await?,
);
// Fetching is explicit; the load that follows touches no network.
DbConfig::refresh_remote_async().await?;§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.
§Several keys as one document
A deployment that splits its configuration across a range — myapp/db.json,
myapp/server.json — can have one source read the lot, and Keys says
which:
// Named keys: a list of layers, merged in the order given, later wins.
let etcd = Etcd::new(endpoints, Keys::several(["myapp/base.json", "myapp/local.json"])).await?;
// A prefix: disjoint sections, and an overlap between two of them is an error.
let etcd = Etcd::new(endpoints, Keys::prefix("myapp/"))
.await?
.with_format(dynamic_config::Format::Json);Both are one round trip: a list is a transaction of range reads and a prefix is one range read, so either way the keys are read at a single etcd revision and a write landing mid-read cannot tear the document in half.
Three consequences, each of which belongs here rather than in an incident:
- A prefix that matches more than 512 keys is refused. A prefix is caller input and the answer to it is server input; an empty prefix matches a whole cluster.
- Provenance becomes store-grained. The merged document is one layer,
so
source_ofanswers “from etcd … keys a, b” and not which of them supplied a given value.describenames every key in the set, which is as close as one layer gets. - One unreadable key fails the whole fetch. A configuration quietly missing a section is worse than a refresh that failed and left the last document serving.
§The connection is made once, and lazily
Etcd::new builds the client and fetch
reuses it — a source that reconnected on every read would turn a refresh
loop into a connection storm.
The underlying client connects lazily, so new succeeding does not mean
the endpoints are reachable: an unreachable etcd surfaces on the first
fetch, not at construction. That is the client’s behaviour rather than a
choice made here, and papering over it with an eager round trip would make
every construction cost one.
§Timeouts
Etcd::with_timeout is the deadline for a single fetch attempt,
excluding retries the underlying client performs — the sentence every
store in this family answers to. Ten seconds by default.
etcd’s own ConnectOptions::with_timeout bounds connecting, which is a
different thing and does not help a connection established minutes ago, so
the deadline here wraps the request. Both can be set; they cover different
halves. Neither applies to Etcd::watch, which is long-lived on purpose.
§Watching
etcd’s watch is a real push stream, so Etcd::watch is a future the caller
spawns and cancels by dropping — no runtime is imposed and no flag is polled.
A prefix can be watched; a named list cannot. A watch on a set is only
honest if the store says the set changed and the set can then be re-read
as of one instant. A prefix answers both: one stream over the range says the
set moved and carries the revision it moved at, and one range read at that
revision is the whole subtree as one instant had it — so a delivered
document is a state the cluster really was in, never one key’s new value
merged with another’s old one. A named list answers neither: etcd
establishes a watch on a key or a range, so a list is N independent streams,
and none of them is about the set. That shape refuses at
watch, before the first event; poll
refresh_remote_async() on a timer instead — it is the same one round trip
the fetch always was.
let task = tokio::spawn(async move {
etcd.watch(move |document| sink(document)).await
});
// Dropping or aborting the task stops the watch.
task.abort();§A watch that is failing says so
A watch is the half of a store dynamic-config cannot see: a delivery keeps
RemoteStatus current, and a stream that broke delivers nothing and would
otherwise report nothing — so dynamic_config_remote_up would describe the
last delivery rather than the last attempt.
reporting_to closes that: the sink the loop already
holds is told about every attempt that came back with nothing, and a store
that stopped answering an hour ago reads as down without anything having to
call refresh_remote_async().
§Every failure branch of the watch loop, and what it reports
A watch is the half of a store dynamic-config cannot see, and
reporting_to is what lets it speak: the sink the
loop already holds is told about every attempt that came back with
nothing. Which attempts those are is a table rather than prose, because
the question an operator asks is which silence is deliberate.
Three rules decide the column, and they are the same three in all seven store crates:
- A failure the loop survives by retrying reports. That is the case the whole feature exists for: the stream is down, the last delivery is old, and nothing else would ever say so out loud.
- A recovery that worked stays silent. Only a delivery or a fetch
clears the streak, so reporting a five-minute token turning over on a
healthy cluster would drive
remote_upto zero and leave it there. - A refusal that never asked the store reports nowhere. No format, a
key shape that cannot be watched, material that will not build a
client:
RemoteStatus::reachable()is whether the store answered the last time it was asked, and these never ask. They are returned to the caller, who is the one holding the mistake — and a status cannot correct them, since it carries a kind and a path and no message.
| Branch | Reports |
|---|---|
| the format is missing, or the source names a list of keys | no — rule 3: nothing has been asked of the cluster |
| the stream cannot be established | yes — the first round trip |
| …because the token had expired, and the refresh worked | no — the store answered, the credential was replaced, and the resumed stream lost no event |
| …and the refresh, the re-establish, or the recovery cap fails | yes |
| the stream errors for any other reason | yes, and the watch ends |
| etcd cancels the watch — a compacted revision, usually | yes, and the watch ends |
| a prefix batch’s range read fails (one token refresh and retry first) | yes |
| two keys under a prefix supply one path | yes — the read failed, not the callback |
| the value is not UTF-8 | yes — the same failure a fetch of it would have recorded |
a progress notification, or an event that is not a Put | no — nothing changed |
| a single key was deleted, or the last key under a prefix went away | no — see the note below |
on_change refuses the document | no — the store answered; apply counted the delivery, and what the document did next is ConfigStatus’s half |
| the stream ends without an error | yes — a watch that stops quietly is a configuration that stops updating quietly |
The deletion row is a difference between stores, deliberately left
standing. Here and in dynamic-config-redis a key holding nothing leaves
the running snapshot alone and says nothing, because the store is answering
and only a delivery clears a streak — reporting it would park remote_up
at zero for as long as nobody recreated the key. dynamic-config-consul
records it as a failed attempt instead, on the argument that a fetch of
the same key fails. Both are defensible, both are written down at the
branch, and neither moves in a patch release.
Structs§
- Certificate
tls - etcd’s TLS types, behind this crate’s
tlsfeature. - Client
- etcd’s own connection options, re-exported so authenticating needs no direct
dependency on
etcd-client. Asynchronousetcdclient using v3 API. - Connect
Options - etcd’s own connection options, re-exported so authenticating needs no direct
dependency on
etcd-client. Options forConnectoperation. - Etcd
- A key in etcd, as a configuration source.
- Identity
tls - etcd’s TLS types, behind this crate’s
tlsfeature. - TlsConfig
- A private certificate authority and a client certificate, as data.
- TlsOptions
tls - etcd’s TLS types, behind this crate’s
tlsfeature.
Enums§
- Keys
- What a source reads: one key, several named keys, or a range.