Skip to main content

Crate dynamic_config_redis

Crate dynamic_config_redis 

Source
Expand description

Read dynamic-config configuration from a Redis key.

Redis speaks a plain request/response protocol, so this implements the blocking RemoteSource trait: nothing here needs an async runtime, and neither does using it.

use dynamic_config_redis::Redis;

DbConfig::set_remote(Redis::new("redis://redis.internal:6379", "myapp/db.json")?);

// Fetching is explicit; the load that follows touches no network.
DbConfig::refresh_remote()?;

§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.

A Redis hash would be the other obvious mapping — one field per setting — and is deliberately not what this does. A hash cannot hold a nested table without inventing a flattening convention, and a document already has one.

§Several keys as one document

A deployment that splits its configuration across several keys 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 redis = Redis::new(url, Keys::several(["myapp/base.json", "myapp/local.json"]))?;

// A prefix: disjoint sections, and an overlap between two of them is an error.
let redis = Redis::new(url, Keys::prefix("myapp/"))?
    .with_format(dynamic_config::Format::Json);

The two forms cost different things, and Redis is the store where the difference matters most:

  • A named list is one MGET — one command, one round trip, and Redis runs it as one operation, so the set is consistent. It is also the only multi-key shape here that can be watched; see Redis::watch.
  • A prefix is a SCAN and then an MGET, and the SCAN is deliberately not KEYS: KEYS walks the whole key space in one blocking operation and is the classic way to stall a production Redis. The price is that SCAN is not atomic — a key written while the cursor is moving may or may not be in the set — so a prefix read here can catch a deployment mid-write in a way a named list cannot, and cannot be watched at all. Prefer a named list where the keys are known.

The prefix is matched as a literal, not as a pattern. SCAN MATCH takes a glob, so *, ?, [ and \ in the prefix are escaped before the command goes out, and every key the server answers with is checked against the literal prefix before it is used — a prefix means the prefix, not whatever a glob would have made of it.

Three more consequences that belong here rather than in an incident:

  • A prefix that matches more than 512 keys is refused.
  • Provenance becomes store-grained. The merged document is one layer, so source_of answers “from redis … keys a, b” and not which key supplied a value. describe names the set.
  • One unreadable key fails the whole fetch.

§Credentials

In the URL, which is where Redis puts them and where every deployment already has them: redis://user:password@host:6379/0, or rediss:// for TLS — which needs this crate’s tls feature to supply the client’s rustls stack. from_client takes a client the program already built, for anything the URL cannot say.

A password Redis will not accept — NOAUTH, WRONGPASS, NOPERM — is reported as ErrorKind::Auth rather than Remote, because reconnecting does not change the server’s mind. The password itself never reaches the message: the URL is redacted before it is stored.

§Timeouts

Redis::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.

Redis has three separate knobs and this sets all of them from the one value: connect, write and read. A deadline covering only the connect would sail past a server that accepted the socket and then went quiet, which is what a wedged Redis actually looks like.

§Watching

Keyspace notifications, so Redis::watch is genuinely change-driven — no polling, no timer. It runs on a thread rather than a future, because nothing here needs a runtime, so stopping it has to come from outside — hence the Watching token.

A named list can be watched; a prefix cannot. A watch on a set is only honest if the store says the set changed and the set can then be read as of one instant. A subscription per key answers the first, and MGET answers the second: it is one command, and Redis runs one command as one operation, so the values it returns are the set as of one point in the command stream. The document delivered is therefore a state the server really held. A prefix has to find its keys again first, and SCAN is a cursor walked over many commands with writes free to land between them — so it refuses at watch, before the first notification; name the keys with Keys::several, or poll refresh_remote() on a timer instead.

let watch = RemoteWatch::new();
let watching = watch.watching();

std::thread::spawn(move || redis.watch(&watching, move |document| sink(document)));

// Dropping `watch` — or calling `watch.stop()` — ends the loop.

A failing watch says so, if it is asked to. reporting_to hands the loop the same sink it delivers through, and the failures inside it — a re-read that came back with nothing, and a subscription that died — are reported to the RemoteStatus as they happen. 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.

§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:

  1. 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.
  2. 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_up to zero and leave it there.
  3. 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.
BranchReports
the format is missing, or the keys cannot be watchedno — rule 3: nothing has been asked of the server
keyspace notifications are switched off on the serveryes — a CONFIG GET answered, and it answered that this watch cannot work
the subscriber connection, the database index, a SUBSCRIBE, or the read timeout failsyes — every one of those is a round trip, or a socket that has already made one
the read timeout expires with no messageno — that is how stop is noticed
the subscription failsyes, and the watch ends
a del or expired eventno — see the note below
the re-read after a notification failsyes, and the loop waits for the next notification
a coalesced duplicate: the set came back the sameno — the server answered
on_change refuses the documentno — the server answered; apply counted the delivery, and what the document did next is ConfigStatus’s half

The deletion row is a difference between stores, deliberately left standing. Here and in dynamic-config-etcd a key holding nothing leaves the running snapshot alone and says nothing, because the server 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 instead, on the argument that a fetch of the same key fails. Both are written down at the branch, and neither moves in a patch release.

Structs§

Client
Redis’ own client, re-exported so from_client needs no direct dependency. The client type.
Redis
A key in Redis, as a configuration source.
TlsConfig
A private certificate authority and a client certificate, as data.

Enums§

Keys
What a source reads: one key, several named keys, or a prefix.