dynamic_config_redis/lib.rs
1//! Read [`dynamic-config`] configuration from a Redis key.
2//!
3//! Redis speaks a plain request/response protocol, so this implements the
4//! **blocking** [`RemoteSource`] trait: nothing here needs an async runtime,
5//! and neither does using it.
6//!
7//! ```no_run
8//! use dynamic_config_redis::Redis;
9//!
10//! # struct DbConfig;
11//! # impl DbConfig {
12//! # fn set_remote(_: Redis) {}
13//! # fn refresh_remote() -> Result<(), dynamic_config::Error> { Ok(()) }
14//! # }
15//! DbConfig::set_remote(Redis::new("redis://redis.internal:6379", "myapp/db.json")?);
16//!
17//! // Fetching is explicit; the load that follows touches no network.
18//! DbConfig::refresh_remote()?;
19//! # Ok::<(), Box<dyn std::error::Error>>(())
20//! ```
21//!
22//! # What it reads
23//!
24//! One key, whose value is **a whole configuration document** — the same bytes
25//! that would be in a config file. The format comes from the key's extension,
26//! or from [`with_format`](Redis::with_format).
27//!
28//! A Redis hash would be the other obvious mapping — one field per setting —
29//! and is deliberately not what this does. A hash cannot hold a nested table
30//! without inventing a flattening convention, and a document already has one.
31//!
32//! # Several keys as one document
33//!
34//! A deployment that splits its configuration across several keys can have one
35//! source read the lot, and [`Keys`] says which:
36//!
37//! ```no_run
38//! # use dynamic_config_redis::{Keys, Redis};
39//! # fn example() -> Result<(), dynamic_config::Error> {
40//! # let url = "redis://redis.internal:6379";
41//! // Named keys: a list of layers, merged in the order given, later wins.
42//! let redis = Redis::new(url, Keys::several(["myapp/base.json", "myapp/local.json"]))?;
43//!
44//! // A prefix: disjoint sections, and an overlap between two of them is an error.
45//! let redis = Redis::new(url, Keys::prefix("myapp/"))?
46//! .with_format(dynamic_config::Format::Json);
47//! # Ok(())
48//! # }
49//! ```
50//!
51//! The two forms cost different things, and Redis is the store where the
52//! difference matters most:
53//!
54//! - **A named list is one `MGET`** — one command, one round trip, and Redis
55//! runs it as one operation, so the set is consistent. It is also the only
56//! multi-key shape here that can be **watched**; see [`Redis::watch`].
57//! - **A prefix is a `SCAN` and then an `MGET`**, and the `SCAN` is
58//! deliberately not `KEYS`: `KEYS` walks the whole key space in one blocking
59//! operation and is the classic way to stall a production Redis. The price is
60//! that `SCAN` is **not atomic** — a key written while the cursor is moving
61//! may or may not be in the set — so a prefix read here can catch a
62//! deployment mid-write in a way a named list cannot, and cannot be watched
63//! at all. Prefer a named list where the keys are known.
64//!
65//! The prefix is matched as a **literal**, not as a pattern. `SCAN MATCH` takes
66//! a glob, so `*`, `?`, `[` and `\` in the prefix are escaped before the
67//! command goes out, and every key the server answers with is checked against
68//! the literal prefix before it is used — a prefix means the prefix, not
69//! whatever a glob would have made of it.
70//!
71//! Three more consequences that belong here rather than in an incident:
72//!
73//! - **A prefix that matches more than 512 keys is refused.**
74//! - **Provenance becomes store-grained.** The merged document is one layer, so
75//! `source_of` answers "from redis … keys a, b" and not which key supplied a
76//! value. [`describe`](RemoteSource::describe) names the set.
77//! - **One unreadable key fails the whole fetch.**
78//!
79//! # Credentials
80//!
81//! In the URL, which is where Redis puts them and where every deployment
82//! already has them: `redis://user:password@host:6379/0`, or `rediss://` for
83//! TLS — which needs this crate's `tls` feature to supply the client's rustls
84//! stack. [`from_client`](Redis::from_client) takes a client the program
85//! already built, for anything the URL cannot say.
86//!
87//! A password Redis will not accept — `NOAUTH`, `WRONGPASS`, `NOPERM` — is
88//! reported as `ErrorKind::Auth` rather than `Remote`, because reconnecting
89//! does not change the server's mind. The password itself never reaches the
90//! message: the URL is redacted before it is stored.
91//!
92//! # Timeouts
93//!
94//! [`Redis::with_timeout`] is the deadline for a single fetch attempt,
95//! excluding retries the underlying client performs — the sentence every store
96//! in this family answers to. Ten seconds by default.
97//!
98//! Redis has three separate knobs and this sets all of them from the one
99//! value: connect, write and read. A deadline covering only the connect would
100//! sail past a server that accepted the socket and then went quiet, which is
101//! what a wedged Redis actually looks like.
102//!
103//! # Watching
104//!
105//! Keyspace notifications, so [`Redis::watch`] is genuinely change-driven —
106//! no polling, no timer. It runs on a thread rather than a future, because
107//! nothing here needs a runtime, so stopping it has to come from outside —
108//! hence the [`Watching`] token.
109//!
110//! **A named list can be watched; a prefix cannot.** A watch on a set is only
111//! honest if the store says *the set* changed and the set can then be read as
112//! of one instant. A subscription per key answers the first, and `MGET`
113//! answers the second: it is one command, and Redis runs one command as one
114//! operation, so the values it returns are the set as of one point in the
115//! command stream. The document delivered is therefore a state the server
116//! really held. A prefix has to *find* its keys again first, and `SCAN` is a
117//! cursor walked over many commands with writes free to land between them —
118//! so it refuses at [`watch`](Redis::watch), before the first notification;
119//! name the keys with [`Keys::several`], or poll `refresh_remote()` on a
120//! timer instead.
121//!
122//! ```no_run
123//! # use dynamic_config::RemoteWatch;
124//! # use dynamic_config_redis::Redis;
125//! # fn example(redis: Redis) {
126//! # let sink = |_: dynamic_config::Fetched| -> Result<(), dynamic_config::Error> { Ok(()) };
127//! let watch = RemoteWatch::new();
128//! let watching = watch.watching();
129//!
130//! std::thread::spawn(move || redis.watch(&watching, move |document| sink(document)));
131//!
132//! // Dropping `watch` — or calling `watch.stop()` — ends the loop.
133//! # }
134//! ```
135//!
136//! **A failing watch says so, if it is asked to.**
137//! [`reporting_to`](Redis::reporting_to) hands the loop the same sink it
138//! delivers through, and the failures inside it — a re-read that came back
139//! with nothing, and a subscription that died — are reported to the
140//! `RemoteStatus` as they happen. Without it a watch is the half of a store
141//! `dynamic-config` cannot see: only deliveries are recorded, so
142//! `dynamic_config_remote_up` describes the last *delivery* rather than the
143//! last *attempt*.
144//!
145//!
146//! # Every failure branch of the watch loop, and what it reports
147//!
148//! A watch is the half of a store `dynamic-config` cannot see, and
149//! [`reporting_to`](Redis::reporting_to) is what lets it speak: the sink the
150//! loop already holds is told about every attempt that came back with
151//! nothing. Which attempts those are is a table rather than prose, because
152//! the question an operator asks is *which* silence is deliberate.
153//!
154//! Three rules decide the column, and they are the same three in all seven
155//! store crates:
156//!
157//! 1. **A failure the loop survives by retrying reports.** That is the case
158//! the whole feature exists for: the stream is down, the last delivery is
159//! old, and nothing else would ever say so out loud.
160//! 2. **A recovery that worked stays silent.** Only a delivery or a fetch
161//! clears the streak, so reporting a five-minute token turning over on a
162//! healthy cluster would drive `remote_up` to zero and leave it there.
163//! 3. **A refusal that never asked the store reports nowhere.** No format, a
164//! key shape that cannot be watched, material that will not build a
165//! client: `RemoteStatus::reachable()` is *whether the store answered the
166//! last time it was asked*, and these never ask. They are returned to the
167//! caller, who is the one holding the mistake — and a status cannot
168//! correct them, since it carries a kind and a path and no message.
169//!
170//! | Branch | Reports |
171//! |---|---|
172//! | the format is missing, or the keys cannot be watched | no — rule 3: nothing has been asked of the server |
173//! | keyspace notifications are switched off on the server | **yes** — a `CONFIG GET` answered, and it answered that this watch cannot work |
174//! | the subscriber connection, the database index, a `SUBSCRIBE`, or the read timeout fails | **yes** — every one of those is a round trip, or a socket that has already made one |
175//! | the read timeout expires with no message | no — that is how `stop` is noticed |
176//! | the subscription fails | **yes**, and the watch ends |
177//! | a `del` or `expired` event | no — see the note below |
178//! | the re-read after a notification fails | **yes**, and the loop waits for the next notification |
179//! | a coalesced duplicate: the set came back the same | no — the server answered |
180//! | `on_change` refuses the document | no — the server answered; `apply` counted the delivery, and what the document did next is `ConfigStatus`'s half |
181//!
182//! **The deletion row is a difference between stores, deliberately left
183//! standing.** Here and in `dynamic-config-etcd` a key holding nothing leaves
184//! the running snapshot alone and says nothing, because the server is
185//! answering and only a delivery clears a streak — reporting it would park
186//! `remote_up` at zero for as long as nobody recreated the key.
187//! `dynamic-config-consul` records it instead, on the argument that a `fetch`
188//! of the same key fails. Both are written down at the branch, and neither
189//! moves in a patch release.
190//!
191//! [`dynamic-config`]: https://docs.rs/dynamic-config
192
193#![forbid(unsafe_code)]
194#![deny(missing_docs)]
195#![cfg_attr(docsrs, feature(doc_cfg))]
196
197use std::sync::Mutex;
198use std::time::Duration;
199
200use dynamic_config::{Error, Fetched, Format, RemoteSink, RemoteSource, Watching};
201use dynamic_config_store_core::attempts::Attempts;
202use dynamic_config_store_core::documents::{self, Overlap};
203use dynamic_config_store_core::{guarded, LoneAuthority};
204use redis::Commands;
205
206/// Redis' own client, re-exported so [`from_client`](Redis::from_client) needs
207/// no direct dependency.
208pub use redis::Client;
209
210/// A private certificate authority and a client certificate, as data.
211///
212/// The shared vocabulary all seven store crates take, so that reaching TLS
213/// never means naming a `redis` type — see [`Redis::with_tls`]. Visible without
214/// the `tls` feature so that the *type* is nameable everywhere; the constructor
215/// that consumes it is not, because a TLS stack is what the feature buys.
216pub use dynamic_config_store_core::tls::TlsConfig;
217
218/// How long to wait for a change before looking again.
219///
220/// A subscription blocks until a message arrives; this is how long that block
221/// lasts before the loop checks whether it has been told to stop.
222const POLL_SLICE: Duration = Duration::from_millis(250);
223
224/// How long one fetch may take before it is given up on.
225///
226/// Ten seconds, matching the rest of the family. A configuration fetch that
227/// hangs is worse than one that fails: the caller can retry a failure.
228const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
229
230/// How many keys one `SCAN` round asks the server for.
231///
232/// `COUNT` is a hint, not a limit, and a low one turns a scan into many round
233/// trips while a high one asks the server to do more work per reply. A hundred
234/// is the shape of a configuration key space rather than of a cache.
235const SCAN_BATCH: usize = 100;
236
237/// The most `SCAN` rounds one prefix read will make.
238///
239/// A cursor is server state and a server that never advances it — a broken
240/// proxy, a cluster answering for the wrong slot — would otherwise be an
241/// infinite loop inside a fetch. The budget on the *keys* cannot catch that on
242/// its own: a scan can return nothing at all, round after round.
243const MOST_SCAN_ROUNDS: usize = 1_000;
244
245/// What a source reads: one key, several named keys, or a prefix.
246///
247/// Every constructor takes one, and a bare `&str` or `String` is
248/// [`Keys::one`] — so the single-key spelling every caller already wrote keeps
249/// working unchanged.
250#[derive(Clone, Debug, PartialEq, Eq)]
251pub enum Keys {
252 /// One key, whose value is the whole document.
253 One(String),
254 /// Several named keys, merged **in the order given — later wins**.
255 ///
256 /// The rule a list of `.file(..)` calls already teaches: the caller wrote
257 /// the list, so the list is the precedence. One `MGET`, so Redis reads the
258 /// set as one operation — which is also what makes this the one multi-key
259 /// shape [`Redis::watch`] accepts.
260 Several(Vec<String>),
261 /// Every key under a literal prefix, merged as **disjoint sections**.
262 ///
263 /// A caller naming a prefix is not expressing an order — `SCAN` returns
264 /// keys in whatever order the hash table is walked in — so two keys under
265 /// it supplying the same path is a deployment bug, and reported as one
266 /// rather than resolved.
267 ///
268 /// `SCAN`, never `KEYS`: `KEYS` blocks the server for the length of the
269 /// whole key space. The price is that the scan is not atomic, and that
270 /// price is also why a prefix **cannot be watched**: re-finding the keys
271 /// after a notification is a cursor walked over many commands, so the set
272 /// it collects can be half from before a write and half from after — a
273 /// document the server never held. `MGET` on a named list has no such
274 /// window.
275 Prefix(String),
276}
277
278impl Keys {
279 /// One key, whose value is the whole document.
280 #[must_use]
281 pub fn one(key: impl Into<String>) -> Self {
282 Self::One(key.into())
283 }
284
285 /// Several named keys, merged in the order given — later wins.
286 #[must_use]
287 pub fn several<I, S>(keys: I) -> Self
288 where
289 I: IntoIterator<Item = S>,
290 S: Into<String>,
291 {
292 Self::Several(keys.into_iter().map(Into::into).collect())
293 }
294
295 /// Every key under `prefix`, merged as disjoint sections.
296 #[must_use]
297 pub fn prefix(prefix: impl Into<String>) -> Self {
298 Self::Prefix(prefix.into())
299 }
300
301 /// The keys as a slice, for the diagnostics and the format inference.
302 ///
303 /// A prefix has none to list — the set is not known until the scan ends.
304 fn named(&self) -> &[String] {
305 match self {
306 Self::One(key) => std::slice::from_ref(key),
307 Self::Several(keys) => keys,
308 Self::Prefix(_) => &[],
309 }
310 }
311
312 /// How a diagnostic names what this source reads.
313 fn describe(&self) -> String {
314 match self {
315 Self::One(key) => format!("key {key}"),
316 Self::Several(keys) => format!("keys {}", keys.join(", ")),
317 Self::Prefix(prefix) => format!("prefix {prefix}"),
318 }
319 }
320}
321
322impl From<&str> for Keys {
323 fn from(key: &str) -> Self {
324 Self::one(key)
325 }
326}
327
328impl From<String> for Keys {
329 fn from(key: String) -> Self {
330 Self::One(key)
331 }
332}
333
334impl From<&String> for Keys {
335 fn from(key: &String) -> Self {
336 Self::one(key)
337 }
338}
339
340/// A key in Redis, as a configuration source.
341///
342/// Not `Clone`: it holds a connection, and two clones sharing a key while each
343/// opening their own would double the connections for no gain. Wrap it in an
344/// `Arc` if two places need one.
345pub struct Redis {
346 client: Client,
347 /// One connection, reused. A source that reconnected on every read would
348 /// turn a refresh loop into a connection storm.
349 connection: Mutex<Option<redis::Connection>>,
350 keys: Keys,
351 format: Option<Format>,
352 /// Why the keys' own extensions could not settle the format between them.
353 ///
354 /// Kept rather than reported at construction because `with_format` is
355 /// allowed to settle it afterwards.
356 disagreement: Option<String>,
357 described: String,
358 timeout: Duration,
359 /// Where the watch loop reports an attempt that came back with nothing.
360 ///
361 /// Nobody, unless [`reporting_to`](Redis::reporting_to) said otherwise:
362 /// a fetch records itself through `refresh_remote`, and a loop is the
363 /// half of this store `dynamic-config` cannot see on its own.
364 attempts: Attempts,
365}
366
367impl Redis {
368 /// The key `key`, on the Redis at `url`.
369 ///
370 /// The format is taken from the key's extension — `myapp/db.json` is JSON.
371 /// A key without one needs [`with_format`](Self::with_format).
372 ///
373 /// # Errors
374 ///
375 /// If the URL cannot be parsed. **Not** if the server is unreachable: the
376 /// connection is opened on the first read, so that construction stays free
377 /// of I/O like every other source in this family.
378 pub fn new(url: &str, keys: impl Into<Keys>) -> Result<Self, Error> {
379 // `redacted`, even here: a malformed URL still carries its password,
380 // and a parse error is the error most likely to be pasted somewhere.
381 let client = Client::open(url)
382 .map_err(|error| Error::remote(format!("redis {}: {error}", redacted(url))))?;
383
384 Ok(Self::build(client, keys, redacted(url)))
385 }
386
387 /// The key `key` on the Redis at `url`, with a private certificate
388 /// authority or a client certificate.
389 ///
390 /// The same three settings, spelled the same way, in all seven store
391 /// crates — and spelled as *data*, so nothing here names a `redis` type:
392 ///
393 /// ```no_run
394 /// # use dynamic_config_redis::{Redis, TlsConfig};
395 /// # fn example() -> Result<(), dynamic_config::Error> {
396 /// let redis = Redis::with_tls(
397 /// "rediss://cache.internal:6379",
398 /// "myapp/db.json",
399 /// &TlsConfig::new().with_ca_certificate_file("/etc/ssl/private-ca.pem"),
400 /// )?;
401 /// # Ok(())
402 /// # }
403 /// ```
404 ///
405 /// Redis expresses all of it: a CA from a file or from bytes, and a client
406 /// certificate from either. The credentials still travel in the URL, as
407 /// they do for [`new`](Self::new).
408 ///
409 /// The URL must be `rediss://`. A `redis://` URL with TLS material is a
410 /// deployment that believes it is encrypted and is not, so it is refused
411 /// here rather than by the client three layers down.
412 ///
413 /// There is no way to turn verification off; [`TlsConfig`]'s own
414 /// documentation argues that one. Redis' client has its own spelling —
415 /// the `#insecure` URL fragment, behind a further feature — and it stays
416 /// where it is, under its own frightening name.
417 ///
418 /// # Errors
419 ///
420 /// If the URL cannot be parsed, if it is not `rediss://`, if a PEM file
421 /// cannot be read, or if the material is not PEM. **Not** if the server is
422 /// unreachable: the connection is opened on the first read.
423 #[cfg(feature = "tls")]
424 #[cfg_attr(docsrs, doc(cfg(feature = "tls")))]
425 pub fn with_tls(url: &str, keys: impl Into<Keys>, tls: &TlsConfig) -> Result<Self, Error> {
426 use redis::IntoConnectionInfo;
427
428 let described = format!("redis {}", redacted(url));
429
430 let info = url
431 .into_connection_info()
432 .map_err(|error| Error::remote(format!("{described}: {error}")))?;
433
434 // Refused here rather than by the client, because the client's own
435 // refusal arrives as "Constructing a TLS client requires a URL with the
436 // `rediss://` scheme" with no address in it — and because a caller who
437 // supplied a CA for a plaintext URL has a deployment that believes it
438 // is encrypted.
439 if !matches!(info.addr(), redis::ConnectionAddr::TcpTls { .. }) {
440 return Err(Error::remote(format!(
441 "{described}: TLS material was supplied for a URL that is not \
442 `rediss://`; the material is refused rather than ignored"
443 )));
444 }
445
446 let certificates = redis::TlsCertificates {
447 root_cert: tls.ca_certificate_pem(&described)?,
448 client_tls: tls
449 .client_certificate_pem(&described)?
450 .map(|(client_cert, client_key)| redis::ClientTlsConfig {
451 client_cert,
452 client_key,
453 }),
454 };
455
456 // The client's own error text is deliberately dropped. It embeds
457 // `rustls-pki-types`' parse failure, which renders the line it choked
458 // on — and the line it choked on in a private key file is private key
459 // material. Which of the three is malformed is as far as it is safe to
460 // go, and the scheme case above is already ruled out.
461 let client = Client::build_with_tls(info, certificates).map_err(|_| {
462 Error::remote(format!(
463 "{described}: the TLS material was refused; check that the CA \
464 certificate, the client certificate and the private key are \
465 PEM-encoded material of the kind expected"
466 ))
467 })?;
468
469 Ok(Self::build(client, keys, redacted(url)))
470 }
471
472 /// Uses a client the program already has.
473 ///
474 /// For a caller that already talks to Redis, or one that built its client
475 /// with options a URL cannot express — including a TLS configuration
476 /// [`with_tls`](Self::with_tls) has no spelling for.
477 #[must_use]
478 pub fn from_client(client: Client, keys: impl Into<Keys>) -> Self {
479 Self::build(client, keys, "<an existing client>".to_owned())
480 }
481
482 fn build(client: Client, keys: impl Into<Keys>, described: String) -> Self {
483 let keys = keys.into();
484
485 let (format, disagreement) = match documents::agreed_format(keys.named()) {
486 Ok(format) => (format, None),
487 Err(complaint) => (None, Some(complaint)),
488 };
489
490 Self {
491 client,
492 connection: Mutex::new(None),
493 keys,
494 format,
495 disagreement,
496 described,
497 timeout: DEFAULT_TIMEOUT,
498 attempts: Attempts::default(),
499 }
500 }
501
502 /// States the format, for a key whose name does not.
503 ///
504 /// Required for [`Keys::Prefix`] — a prefix has no extension — and it also
505 /// settles a list whose keys name two different formats.
506 #[must_use]
507 pub fn with_format(mut self, format: Format) -> Self {
508 self.format = Some(format);
509 // The caller has now said which format wins, so the keys no longer
510 // have to agree between themselves.
511 self.disagreement = None;
512 self
513 }
514
515 /// How long a single fetch may take before it is given up on. Ten seconds
516 /// by default.
517 ///
518 /// The deadline for **one fetch attempt**, excluding retries the
519 /// underlying client performs — the same sentence every store in this
520 /// family answers to. Redis splits it into three, and this sets all of
521 /// them from the one value: opening the connection, writing the command,
522 /// and waiting for the reply.
523 ///
524 /// All three, because any one of them alone is the mistake: a deadline
525 /// that only covers connecting sails straight past a server that accepted
526 /// the socket and then stopped answering, which is what a wedged Redis
527 /// actually looks like.
528 ///
529 /// It bounds each read [`watch`](Self::watch) performs, not the watch
530 /// itself — a subscription waiting for the next notification is supposed
531 /// to wait.
532 #[must_use]
533 pub fn with_timeout(mut self, timeout: Duration) -> Self {
534 self.timeout = timeout;
535 // A connection opened under the old deadline carries it in its socket
536 // options, so the cached one has to go.
537 *self
538 .connection
539 .lock()
540 .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
541 self
542 }
543
544 /// Reports this source's **watch** failures to `sink`.
545 ///
546 /// A watch loop is the half of a store `dynamic-config` cannot see. A
547 /// delivery keeps `RemoteStatus` current because
548 /// [`RemoteSink::apply`] records one — but a loop whose subscription
549 /// died, or whose re-read keeps failing, delivers nothing and would
550 /// otherwise say nothing: `dynamic_config_remote_up` would report the
551 /// last *delivery* rather than the last *attempt*, and a Redis that
552 /// stopped answering an hour ago would look healthy until something
553 /// called `refresh_remote()`.
554 ///
555 /// ```no_run
556 /// # use dynamic_config::RemoteWatch;
557 /// # use dynamic_config_redis::Redis;
558 /// # struct DbConfig;
559 /// # impl DbConfig {
560 /// # fn remote_sink() -> dynamic_config::RemoteSink { unimplemented!() }
561 /// # }
562 /// # fn example(url: &str) -> Result<(), dynamic_config::Error> {
563 /// // Taken once, where the loop is wired: a sink captures the generation
564 /// // of the source installed at that moment, which is what stops a loop
565 /// // winding down from charging its failures to its replacement.
566 /// let sink = DbConfig::remote_sink();
567 ///
568 /// let watcher = Redis::new(url, "myapp/db.json")?.reporting_to(sink);
569 /// # Ok(())
570 /// # }
571 /// ```
572 ///
573 /// **A failure moves the failure streak and nothing else.** The fetch
574 /// count and the clock are left alone, so
575 /// `dynamic_config_remote_last_fetch_seconds` keeps ageing while
576 /// `dynamic_config_remote_up` goes to zero — the pair an alert wants.
577 /// Only the failure's kind and key path are recorded; a Redis URL never
578 /// reaches a `RemoteStatus`.
579 ///
580 /// It changes nothing about what [`watch`](Self::watch) *returns*, and
581 /// nothing about [`fetch`](RemoteSource::fetch), which already records
582 /// itself through `refresh_remote()`.
583 ///
584 /// [`RemoteSink::apply`]: dynamic_config::RemoteSink::apply
585 #[must_use]
586 pub fn reporting_to(mut self, sink: RemoteSink) -> Self {
587 self.attempts = Attempts::to(sink);
588 self
589 }
590
591 /// Reports `error` to whatever asked to hear about failed attempts, and
592 /// hands it straight back.
593 ///
594 /// Every failure that *ends* a watch goes through here, so reporting is
595 /// one word at each site rather than a branch that can be left out of the
596 /// next one. It cannot fail and it does not touch the error: a loop must
597 /// never have to handle a failure to report a failure, and the caller sees
598 /// exactly what it always saw.
599 fn failing(&self, error: Error) -> Error {
600 self.attempts.failed(&error);
601
602 error
603 }
604
605 /// Calls `on_change` whenever what this source reads changes.
606 ///
607 /// Uses **keyspace notifications**: Redis publishes to
608 /// `__keyspace@{db}__:{key}` when a key is written, and this subscribes to
609 /// exactly that channel — one per key of the set. Genuinely change-driven —
610 /// no polling, no timer.
611 ///
612 /// **One key or a named list.** A named list is the multi-key case Redis
613 /// can answer honestly, and the whole reason is `MGET`: it is *one*
614 /// command, and Redis executes commands one at a time, so the values it
615 /// answers with are the set as of one point in the command stream. The
616 /// document delivered here is therefore a state the server really held —
617 /// never one key's new value beside another's old one, which is the tear
618 /// that made every other network store refuse. A **prefix** is refused;
619 /// the reason is on [`Keys::Prefix`].
620 ///
621 /// What the read is *not* is simultaneous with the notification. It
622 /// follows the event, so the document may be **newer** than the write that
623 /// woke the loop, and two writes landing together can deliver the later
624 /// state rather than each state in turn. **Spurious, never torn** — the
625 /// same bargain `dynamic-config-git`'s watch makes, and the one that
626 /// matters: a delivery is always a state the store was in, and never an
627 /// older one than the delivery before it.
628 ///
629 /// A named list therefore also **coalesces**: writing three keys together
630 /// publishes three notifications and this delivers once, because the
631 /// document the second and third would carry is the one already delivered.
632 /// Every reload hook running three times for one deployment is a cost with
633 /// nothing to buy it.
634 ///
635 /// Keyspace notifications are **off by default** in Redis. A server that
636 /// has not enabled them publishes nothing, and this loop would wait
637 /// forever, so it checks at start-up and reports rather than hanging:
638 ///
639 /// ```text
640 /// CONFIG SET notify-keyspace-events KEA
641 /// ```
642 ///
643 /// The current value is not delivered at startup, for the same reason a
644 /// file watcher does not report an edit when it starts. Fetch first if the
645 /// starting value matters.
646 ///
647 /// A key that holds nothing is not a change this reports. For one key that
648 /// is a deletion; for a named list it is one member of the set going away,
649 /// which fails the read the same way [`fetch`](RemoteSource::fetch) does —
650 /// and a failed read here is treated as transient, because the next write
651 /// notifies again. No configuration is not a configuration, so the running
652 /// snapshot stays either way.
653 ///
654 /// # What a failing loop reports
655 ///
656 /// Nothing, unless [`reporting_to`](Self::reporting_to) was given a sink.
657 /// With one, the two failures **inside** the loop are reported to the
658 /// `RemoteStatus` as they happen: a re-read that came back with nothing,
659 /// and a subscription that died. The refusals **at the door** — a prefix,
660 /// no format, no keys, notifications off, a server that will not accept
661 /// the subscription — are not, because they are returned to the caller by
662 /// this very call, before there is a loop to be silent in; and half of
663 /// them are deployment mistakes rather than a store that stopped
664 /// answering, which is not what `dynamic_config_remote_up` means.
665 ///
666 /// # Errors
667 ///
668 /// If the subscription cannot be established, if keyspace notifications
669 /// are off, if the source reads a prefix, if the subscription itself
670 /// breaks — a dead connection ends the watch with an error rather than
671 /// spinning; restart it to resubscribe — or if `on_change` returns an
672 /// error, so a caller that wants to survive a bad document should log it
673 /// and return `Ok`.
674 pub fn watch<F>(&self, watching: &Watching, mut on_change: F) -> Result<(), Error>
675 where
676 F: FnMut(Fetched) -> Result<(), Error>,
677 {
678 // Validated up front so a key with no format fails at `watch` rather
679 // than on the first notification, hours later. The reads themselves go
680 // through `fetch`, which resolves the format again.
681 // These two are returned and recorded nowhere: nothing has been asked
682 // of the server yet, and `reachable()` is *whether the store answered
683 // the last time it was asked*. Everything below this line is a round
684 // trip, and every one of those reports.
685 self.format()?;
686 let keys: Vec<String> = self.watched()?.to_vec();
687
688 self.require_keyspace_notifications()
689 .map_err(|error| self.failing(error))?;
690
691 // A subscription needs a connection of its own: Redis puts the
692 // connection into a mode where ordinary commands are refused.
693 let mut subscriber = self
694 .client
695 .get_connection_with_timeout(self.timeout)
696 .map_err(|error| {
697 self.failing(Error::remote(format!(
698 "{}: cannot subscribe: {error}",
699 self.describe()
700 )))
701 })?;
702
703 // The database index is not readable from the client in this version,
704 // so it is asked for: `CLIENT INFO` reports the connection's own, which
705 // is the one the notifications will be published on. An index that
706 // cannot be determined is a hard error, not a guess of `0` — a watch
707 // subscribed to the wrong database is a watch that never fires, which
708 // reads as "configuration stopped changing" rather than as a failure.
709 let database = self.database().ok_or_else(|| {
710 self.failing(Error::remote(format!(
711 "{}: cannot determine the database index the connection lands on, so the keyspace channel cannot be named",
712 self.describe()
713 )))
714 })?;
715
716 let mut pubsub = subscriber.as_pubsub();
717
718 // One `SUBSCRIBE` per key rather than one carrying the lot: Redis
719 // confirms a subscription per channel, and the client reads one
720 // confirmation per call — so a single batched command would leave the
721 // rest of the confirmations in the buffer to be read later as if they
722 // were notifications.
723 for key in &keys {
724 let channel = format!("__keyspace@{database}__:{key}");
725
726 pubsub.subscribe(&channel).map_err(|error| {
727 self.failing(Error::remote(format!(
728 "{}: cannot subscribe: {error}",
729 self.describe()
730 )))
731 })?;
732 }
733
734 // Bounded, so `stop` is noticed without a message having to arrive.
735 pubsub.set_read_timeout(Some(POLL_SLICE)).map_err(|error| {
736 self.failing(Error::remote(format!("{}: {error}", self.describe())))
737 })?;
738
739 // What the last delivery carried, so a set written together is not
740 // delivered once per key. `None` for a single key: one write is one
741 // notification there, so there is nothing to coalesce, and suppressing
742 // a re-write of an identical value would be a change to behaviour that
743 // has already shipped.
744 let mut last: Option<String> = None;
745 let coalescing = keys.len() > 1;
746
747 while watching.keep_going() {
748 let message = match pubsub.get_message() {
749 Ok(message) => message,
750 // A timeout is the design: it is how this loop gets a chance
751 // to notice `stop`. Anything else is a broken subscription —
752 // and a broken socket returns *immediately*, so treating it
753 // as a timeout used to spin this loop at full CPU forever
754 // while the handle still looked alive. etcd and NATS end
755 // their watch with an error for the same condition; so does
756 // this now.
757 Err(error) if error.is_timeout() => continue,
758 Err(error) => {
759 let error = Error::remote(format!(
760 "{}: the subscription failed: {error}",
761 self.describe()
762 ));
763
764 // Reported *as well as* returned, and this is the failure
765 // that most needs it: the loop is usually on a thread
766 // nobody joins, so returning here is a watch that stops
767 // and a program that never hears about it. Configuration
768 // simply stops updating, which looks like a store that
769 // has nothing new to say.
770 self.attempts.failed(&error);
771
772 return Err(error);
773 }
774 };
775
776 let event: String = message.get_payload().unwrap_or_default();
777
778 // `del` and `expired` mean the key holds nothing. No configuration
779 // is not a configuration, so the running snapshot stays.
780 if event == "del" || event == "expired" {
781 continue;
782 }
783
784 // Through `fetch`, not `read`: fetch drops the cached connection
785 // on failure, so a read that died with its socket does not leave a
786 // dead connection for every later notification to trip over.
787 //
788 // For a named list that one call is one `MGET`, which is what makes
789 // this loop honest: the whole set comes back from a single command,
790 // so the document is a state the server was actually in rather than
791 // a merge of one key read here and another read a moment later.
792 let document = match self.fetch() {
793 Ok(document) => document,
794 // The notification arrived and the read did not: a transient
795 // failure, and the next write will notify again. A member of
796 // the set holding nothing lands here too.
797 //
798 // Reported even so, and deliberately: this call is the whole
799 // re-read — one `MGET` for a named list — so a credential the
800 // server has started refusing, or a member of the set that
801 // has gone missing, is a store answering nothing at every
802 // notification while the loop stays alive and silent. The
803 // streak is what tells the two apart from a blip: one failure
804 // between deliveries clears on the next one, and a store that
805 // has really stopped answering climbs.
806 Err(error) => {
807 self.attempts.failed(&error);
808
809 continue;
810 }
811 };
812
813 if coalescing {
814 if last.as_deref() == Some(document.text.as_str()) {
815 continue;
816 }
817
818 last = Some(document.text.clone());
819 }
820
821 guarded(&mut on_change, document, &self.describe())?;
822 }
823
824 Ok(())
825 }
826
827 /// The database index this client's connections land on.
828 ///
829 /// Notifications are published per database, so subscribing to the wrong
830 /// one is a watch that never fires.
831 fn database(&self) -> Option<i64> {
832 let mut connection = self.client.get_connection_with_timeout(self.timeout).ok()?;
833 let info: String = redis::cmd("CLIENT")
834 .arg("INFO")
835 .query(&mut connection)
836 .ok()?;
837
838 info.split_whitespace()
839 .find_map(|field| field.strip_prefix("db="))
840 .and_then(|value| value.parse().ok())
841 }
842
843 /// The format, or an error naming the call that supplies one.
844 fn format(&self) -> Result<Format, Error> {
845 if let Some(complaint) = &self.disagreement {
846 return Err(Error::remote(format!("{}: {complaint}", self.describe())));
847 }
848
849 self.format.ok_or_else(|| {
850 Error::remote(format!(
851 "{}: the key names no format; call `with_format`",
852 self.describe()
853 ))
854 })
855 }
856
857 /// The keys a watch subscribes to, or an error saying this source cannot
858 /// be watched at all.
859 ///
860 /// One key and a named list are both watchable, and the property that
861 /// decides it is the same one in both cases: the re-read after a
862 /// notification is a single `MGET`, and Redis runs one command as one
863 /// operation — so there is no window between "something changed" and "read
864 /// the set" for a second write to land in. A prefix has to *find* its keys
865 /// again first, with a `SCAN`, and a cursor is many commands with writes
866 /// free to land between them.
867 fn watched(&self) -> Result<&[String], Error> {
868 if let Keys::Prefix(_) = &self.keys {
869 return Err(Error::remote(format!(
870 "{}: a source that reads a prefix cannot be watched; finding \
871 the keys again means a `SCAN`, which is a cursor over many \
872 commands rather than one operation, so the set could be \
873 collected half from before a write and half from after — \
874 name the keys with `Keys::several`, which is watched as one \
875 `MGET`, or poll `refresh_remote()` on a timer instead",
876 self.describe()
877 )));
878 }
879
880 match self.keys.named() {
881 // `Keys::several([])` reads nothing, so a watch on it would
882 // subscribe to nothing and wait forever — which reads as
883 // "configuration stopped changing" rather than as a failure.
884 [] => Err(Error::remote(format!(
885 "{}: there are no keys to watch",
886 self.describe()
887 ))),
888 keys => Ok(keys),
889 }
890 }
891
892 /// What two of this source's keys supplying one path means.
893 ///
894 /// The distinction the feature turns on: a caller who wrote the list wrote
895 /// the precedence with it, and a caller who wrote a prefix wrote no order
896 /// at all — so the first merges and the second refuses.
897 fn overlap(&self) -> Overlap {
898 match self.keys {
899 Keys::One(_) | Keys::Several(_) => Overlap::LaterWins,
900 Keys::Prefix(_) => Overlap::Refused,
901 }
902 }
903
904 /// Opens a connection with the deadline applied to all three of its
905 /// halves — connecting, writing, and waiting for the reply.
906 fn open(&self) -> Result<redis::Connection, Error> {
907 let connection = self
908 .client
909 .get_connection_with_timeout(self.timeout)
910 .map_err(|error| self.classified(&error))?;
911
912 // Set after the connection exists, because these are socket options
913 // and there is no socket before it. A failure here is not fatal to
914 // the read — it means the transport has no timeouts to set, which is
915 // true of a unix socket on some platforms — so it is not propagated;
916 // the connect deadline above already applied.
917 let _ = connection.set_read_timeout(Some(self.timeout));
918 let _ = connection.set_write_timeout(Some(self.timeout));
919
920 Ok(connection)
921 }
922
923 /// Sorts one of Redis' failures into a kind.
924 ///
925 /// `NOAUTH`, `WRONGPASS` and `NOPERM` are the server's own words for a
926 /// credential it will not accept, and no amount of reconnecting changes
927 /// its mind — which is exactly what separates them from the socket
928 /// failures that share this path.
929 fn classified(&self, error: &redis::RedisError) -> Error {
930 let described = format!("{}: {error}", self.describe());
931
932 if error.kind() == redis::ErrorKind::AuthenticationFailed
933 || matches!(error.code(), Some("NOAUTH" | "WRONGPASS" | "NOPERM"))
934 {
935 return Error::auth(described);
936 }
937
938 Error::remote(described)
939 }
940
941 /// Reads whatever this source reads, opening the connection if this is the
942 /// first time.
943 ///
944 /// Every shape ends in one `MGET`: Redis reads a set of keys as a single
945 /// operation, so a document assembled from several keys was never half of
946 /// one set and half of another. Only *finding* the keys differs, and only
947 /// the prefix has to.
948 fn read(&self) -> Result<Vec<(String, String)>, Error> {
949 let mut slot = self
950 .connection
951 .lock()
952 .unwrap_or_else(std::sync::PoisonError::into_inner);
953
954 let connection = match slot.as_mut() {
955 Some(connection) => connection,
956 None => slot.insert(self.open()?),
957 };
958
959 let keys = match &self.keys {
960 Keys::One(key) => vec![key.clone()],
961 Keys::Several(keys) => keys.clone(),
962 Keys::Prefix(prefix) => self.scan(connection, prefix)?,
963 };
964
965 // An `MGET` with no keys is a protocol error, and a prefix that matched
966 // nothing is a missing configuration rather than an empty one.
967 if keys.is_empty() {
968 return Err(Error::remote(format!(
969 "{}: nothing matched, so there is nothing to load",
970 self.describe()
971 )));
972 }
973
974 let values: Vec<Option<String>> = connection.mget(&keys).map_err(|error| {
975 // The connection may be the thing that broke; `fetch` drops it so
976 // the next read opens a fresh one rather than reusing a dead
977 // socket.
978 self.classified(&error)
979 })?;
980
981 keys.into_iter()
982 .zip(values)
983 .map(|(key, value)| {
984 // Fail-whole, not merge-what-came-back: a configuration
985 // quietly missing a section is worse than a refresh that
986 // failed and left the last document serving.
987 let text = value.ok_or_else(|| {
988 Error::remote(format!("{}: `{key}` holds no value", self.describe()))
989 })?;
990
991 Ok((key, text))
992 })
993 .collect()
994 }
995
996 /// Every key under `prefix`, found with `SCAN` and never with `KEYS`.
997 ///
998 /// `KEYS` is one blocking operation over the whole key space, which is the
999 /// classic way to stall a production Redis; `SCAN` is a cursor, and the
1000 /// cost of the cursor is that the set is not read atomically.
1001 ///
1002 /// Sorted before it is returned, so the same keys produce the same
1003 /// document and the same diagnostic — `SCAN` returns them in hash-table
1004 /// order, which is neither stable nor anybody's precedence. Sorting is not
1005 /// a precedence rule either, which is exactly why an overlap under a prefix
1006 /// is refused rather than resolved.
1007 fn scan(&self, connection: &mut redis::Connection, prefix: &str) -> Result<Vec<String>, Error> {
1008 let pattern = format!("{}*", globbed(prefix));
1009
1010 let mut cursor = 0_u64;
1011 let mut found: Vec<String> = Vec::new();
1012
1013 for _ in 0..MOST_SCAN_ROUNDS {
1014 let (next, batch): (u64, Vec<String>) = redis::cmd("SCAN")
1015 .arg(cursor)
1016 .arg("MATCH")
1017 .arg(&pattern)
1018 .arg("COUNT")
1019 .arg(SCAN_BATCH)
1020 .query(connection)
1021 .map_err(|error| self.classified(&error))?;
1022
1023 for key in batch {
1024 // The escaping above should make this unreachable, and it is
1025 // one comparison: a prefix has to mean the prefix whatever a
1026 // proxy, a cluster or a future glob syntax makes of the
1027 // pattern.
1028 documents::under_prefix(&key, prefix, &self.describe())?;
1029
1030 found.push(key);
1031 }
1032
1033 // A cursor can hand the same key back more than once — it is a
1034 // guarantee about *coverage*, not about uniqueness — and the same
1035 // key twice would collide with itself under the prefix rule.
1036 found.sort();
1037 found.dedup();
1038
1039 documents::within_key_budget(found.len(), &self.describe())?;
1040
1041 cursor = next;
1042
1043 if cursor == 0 {
1044 return Ok(found);
1045 }
1046 }
1047
1048 Err(Error::remote(format!(
1049 "{}: the scan did not finish in {MOST_SCAN_ROUNDS} rounds; \
1050 the server is not advancing the cursor",
1051 self.describe()
1052 )))
1053 }
1054
1055 /// Reports if the server will never publish what the watch waits for.
1056 fn require_keyspace_notifications(&self) -> Result<(), Error> {
1057 let mut connection = self.open()?;
1058
1059 let settings: Vec<String> = redis::cmd("CONFIG")
1060 .arg("GET")
1061 .arg("notify-keyspace-events")
1062 .query(&mut connection)
1063 .map_err(|error| self.classified(&error))?;
1064
1065 let value = settings.get(1).map(String::as_str).unwrap_or_default();
1066
1067 // `K` is the keyspace class; without it nothing is published on the
1068 // channel this subscribes to, whatever else is enabled.
1069 if value.contains('K') {
1070 return Ok(());
1071 }
1072
1073 Err(Error::remote(format!(
1074 "{}: keyspace notifications are off, so nothing would ever arrive; \
1075 `CONFIG SET notify-keyspace-events KEA` on the server",
1076 self.describe()
1077 )))
1078 }
1079}
1080
1081impl RemoteSource for Redis {
1082 fn fetch(&self) -> Result<Fetched, Error> {
1083 let format = self.format()?;
1084
1085 // A named list is read in call order and a prefix scan is sorted, which
1086 // is what each rule wants — so nothing is reordered here.
1087 match self.read().and_then(|documents| {
1088 documents::merged(&documents, format, self.overlap(), &self.describe())
1089 }) {
1090 Ok(document) => Ok(document),
1091 Err(error) => {
1092 // A failed read may have been a dead connection; drop it so the
1093 // next attempt opens a fresh one.
1094 *self
1095 .connection
1096 .lock()
1097 .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
1098
1099 Err(error)
1100 }
1101 }
1102 }
1103
1104 fn describe(&self) -> String {
1105 format!("redis {} {}", self.described, self.keys.describe())
1106 }
1107}
1108
1109impl std::fmt::Debug for Redis {
1110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1111 f.debug_struct("Redis")
1112 .field("server", &self.described)
1113 .field("keys", &self.keys)
1114 .field("format", &self.format)
1115 .finish_non_exhaustive()
1116 }
1117}
1118
1119/// A literal string, escaped so `SCAN MATCH` reads it as itself.
1120///
1121/// The one place a prefix could quietly stop meaning what it says: `MATCH`
1122/// takes a glob, so a prefix containing `[` or `*` — a tenant id, a key
1123/// namespace with brackets in it — would match keys the caller never named.
1124/// Redis' own glob escape is a backslash, and the backslash itself has to go
1125/// first or it would escape the escapes.
1126fn globbed(literal: &str) -> String {
1127 let mut escaped = String::with_capacity(literal.len());
1128
1129 for character in literal.chars() {
1130 if matches!(character, '\\' | '*' | '?' | '[' | ']') {
1131 escaped.push('\\');
1132 }
1133
1134 escaped.push(character);
1135 }
1136
1137 escaped
1138}
1139
1140/// A URL with its password removed, for error messages.
1141///
1142/// `redis://user:hunter2@host` in a log is a credential in a log.
1143///
1144/// [`LoneAuthority::Username`] is the Redis-specific half: `redis://app@host`
1145/// names a user with the password elsewhere, so blanking the authority would
1146/// hide the half worth seeing. The NATS crate reads the same shape as a
1147/// token, which is why the two pass different arguments to one
1148/// implementation rather than keeping two.
1149fn redacted(url: &str) -> String {
1150 dynamic_config_store_core::redacted(url, LoneAuthority::Username)
1151}
1152
1153#[cfg(test)]
1154mod tests {
1155 use std::sync::Arc;
1156
1157 use super::*;
1158
1159 #[test]
1160 fn a_password_never_reaches_an_error_message() {
1161 assert_eq!(
1162 redacted("redis://app:hunter2@redis.internal:6379"),
1163 "redis://app:***@redis.internal:6379"
1164 );
1165 }
1166
1167 #[test]
1168 fn a_password_containing_at_signs_is_fully_redacted() {
1169 assert_eq!(
1170 redacted("redis://app:p@ss@w@rd@redis.internal:6379"),
1171 "redis://app:***@redis.internal:6379"
1172 );
1173 }
1174
1175 #[test]
1176 fn a_url_with_no_credentials_is_left_alone() {
1177 assert_eq!(
1178 redacted("redis://redis.internal:6379"),
1179 "redis://redis.internal:6379"
1180 );
1181 assert_eq!(redacted("not a url"), "not a url");
1182 }
1183
1184 #[test]
1185 fn the_format_comes_from_the_keys_extension() {
1186 let client = Client::open("redis://127.0.0.1:6379").unwrap();
1187 let source = Redis::from_client(client, "myapp/db.json");
1188
1189 assert_eq!(source.format, Some(Format::Json));
1190 }
1191
1192 /// A bare string still means one key, which is what keeps every caller who
1193 /// wrote the single-key spelling compiling.
1194 #[test]
1195 fn a_bare_key_is_still_one_key() {
1196 assert_eq!(Keys::from("myapp/db.json"), Keys::one("myapp/db.json"));
1197 assert_eq!(
1198 Keys::from("myapp/db.json".to_owned()),
1199 Keys::one("myapp/db.json")
1200 );
1201 }
1202
1203 /// The one place a prefix could quietly stop meaning what it says: `MATCH`
1204 /// takes a glob, so a tenant id with a bracket in it would otherwise
1205 /// select keys nobody asked for.
1206 #[test]
1207 fn a_prefix_is_matched_as_a_literal_and_not_as_a_glob() {
1208 assert_eq!(globbed("myapp/"), "myapp/");
1209 assert_eq!(globbed("my[a]pp/"), r"my\[a\]pp/");
1210 assert_eq!(globbed("my*pp/"), r"my\*pp/");
1211 assert_eq!(globbed("my?pp/"), r"my\?pp/");
1212 // The backslash goes first, or it would escape the escapes.
1213 assert_eq!(globbed(r"my\pp/"), r"my\\pp/");
1214 }
1215
1216 /// Provenance is store-grained once several keys become one document, so
1217 /// the one thing `describe()` can still do is name the whole set.
1218 #[test]
1219 fn describe_names_every_key_in_the_set() {
1220 let client = Client::open("redis://127.0.0.1:6379").unwrap();
1221
1222 let several = Redis::from_client(client.clone(), Keys::several(["a.json", "b.json"]));
1223 assert!(
1224 several.describe().contains("a.json") && several.describe().contains("b.json"),
1225 "{}",
1226 several.describe()
1227 );
1228
1229 let prefix = Redis::from_client(client, Keys::prefix("myapp/"));
1230 assert!(
1231 prefix.describe().contains("prefix myapp/"),
1232 "{}",
1233 prefix.describe()
1234 );
1235 }
1236
1237 /// Both refuse before any round trip, and both name the call that settles
1238 /// it — a misconfiguration should not need a server to be reported.
1239 #[test]
1240 fn a_format_that_cannot_be_inferred_is_refused_before_any_request() {
1241 let client = Client::open("redis://127.0.0.1:9").unwrap();
1242
1243 let error = Redis::from_client(client.clone(), Keys::prefix("myapp/"))
1244 .fetch()
1245 .expect_err("a prefix names no format");
1246
1247 assert!(error.to_string().contains("with_format"), "{error}");
1248
1249 let error = Redis::from_client(client, Keys::several(["db.json", "server.toml"]))
1250 .fetch()
1251 .expect_err("json and toml cannot both be it");
1252
1253 assert!(error.to_string().contains("db.json"), "{error}");
1254 assert!(error.to_string().contains("server.toml"), "{error}");
1255 }
1256
1257 /// The line between the two multi-key shapes, drawn where the protocol
1258 /// draws it: a named list is re-read with one `MGET` and is watched, and a
1259 /// prefix has to find its keys again with a `SCAN` — many commands, with
1260 /// writes free to land between them — and is refused before any round
1261 /// trip, naming the call that does work.
1262 #[test]
1263 fn a_prefix_refuses_to_be_watched_and_a_named_list_is_not_refused_with_it() {
1264 let client = Client::open("redis://127.0.0.1:9").unwrap();
1265 let source =
1266 Redis::from_client(client.clone(), Keys::prefix("myapp/")).with_format(Format::Json);
1267
1268 let watch = dynamic_config::RemoteWatch::new();
1269 let error = source
1270 .watch(&watch.watching(), |_| Ok(()))
1271 .expect_err("a prefix cannot be watched");
1272
1273 assert!(error.to_string().contains("cannot be watched"), "{error}");
1274 assert!(error.to_string().contains("SCAN"), "{error}");
1275 assert!(error.to_string().contains("Keys::several"), "{error}");
1276
1277 // Nothing is listening on port 9, so a named list gets as far as the
1278 // network and fails there — which is the point: it is not turned away
1279 // at the door the way the prefix is.
1280 let source = Redis::from_client(client, Keys::several(["a.json", "b.json"]));
1281 let error = source
1282 .watch(&watch.watching(), |_| Ok(()))
1283 .expect_err("nothing is listening");
1284
1285 assert!(
1286 !error.to_string().contains("cannot be watched"),
1287 "a named list is watchable: {error}"
1288 );
1289 }
1290
1291 /// Every error path a watch can take names the source, and naming the
1292 /// source means quoting the URL — so each of them gets the redaction test
1293 /// the fetch paths have. The password may contain `@`, which is the shape
1294 /// that has caught this crate out before.
1295 #[test]
1296 fn no_watch_error_path_prints_a_credential() {
1297 const URL: &str = "redis://app:p@ss@w@rd@127.0.0.1:9";
1298
1299 let watch = dynamic_config::RemoteWatch::new();
1300
1301 let refusals = [
1302 // Refused at the door: the prefix shape.
1303 Redis::new(URL, Keys::prefix("myapp/"))
1304 .unwrap()
1305 .with_format(Format::Json),
1306 // Refused at the door: nothing to subscribe to.
1307 Redis::new(URL, Keys::several(Vec::<String>::new()))
1308 .unwrap()
1309 .with_format(Format::Json),
1310 // Refused by the network: nothing is listening on port 9, so this
1311 // fails while checking for keyspace notifications.
1312 Redis::new(URL, Keys::several(["myapp/db.json", "myapp/server.json"])).unwrap(),
1313 ];
1314
1315 for source in refusals {
1316 let error = source
1317 .watch(&watch.watching(), |_| Ok(()))
1318 .expect_err("every one of these refuses");
1319
1320 let printed = format!("{error} {error:?} {source:?}");
1321
1322 assert!(!printed.contains("p@ss"), "{printed}");
1323 assert!(!printed.contains("w@rd"), "{printed}");
1324 assert!(printed.contains("app:***@"), "{printed}");
1325 }
1326 }
1327
1328 /// `Keys::several([])` reads nothing, so a watch on it would subscribe to
1329 /// nothing and wait forever — which reads as "configuration stopped
1330 /// changing" rather than as a failure.
1331 #[test]
1332 fn a_watch_on_an_empty_named_list_is_refused_rather_than_parked_forever() {
1333 let client = Client::open("redis://127.0.0.1:9").unwrap();
1334 let source = Redis::from_client(client, Keys::several(Vec::<String>::new()))
1335 .with_format(Format::Json);
1336
1337 let watch = dynamic_config::RemoteWatch::new();
1338 let error = source
1339 .watch(&watch.watching(), |_| Ok(()))
1340 .expect_err("there is nothing to subscribe to");
1341
1342 assert!(error.to_string().contains("no keys to watch"), "{error}");
1343 }
1344
1345 /// A listener that answers every command with `reply`, until the client
1346 /// hangs up.
1347 ///
1348 /// No Docker: a RESP error is one line, which is all a refusal needs. One
1349 /// reply *per command* rather than per read, because `redis-rs` pipelines
1350 /// its two `CLIENT SETINFO` handshake commands into a single write and
1351 /// then waits for both — answer once and the connection times out instead
1352 /// of failing the way the test means to test.
1353 fn scripted(reply: &'static str) -> (String, std::thread::JoinHandle<()>) {
1354 use std::io::{Read, Write};
1355
1356 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1357 let url = format!("redis://{}", listener.local_addr().unwrap());
1358
1359 let server = std::thread::spawn(move || {
1360 let Ok((mut stream, _)) = listener.accept() else {
1361 return;
1362 };
1363
1364 let mut buffer = [0u8; 4096];
1365
1366 while let Ok(read) = stream.read(&mut buffer) {
1367 if read == 0 {
1368 return;
1369 }
1370
1371 // Every RESP command is an array, and every array opens with
1372 // `*`, so the count of those lines is the count of replies
1373 // owed.
1374 let commands = String::from_utf8_lossy(&buffer[..read])
1375 .lines()
1376 .filter(|line| line.starts_with('*'))
1377 .count();
1378
1379 for _ in 0..commands.max(1) {
1380 if stream.write_all(reply.as_bytes()).is_err() {
1381 return;
1382 }
1383 }
1384 }
1385 });
1386
1387 (url, server)
1388 }
1389
1390 /// A password the server will not take is `Auth`: reconnecting with the
1391 /// same one is not a recovery, and a watch loop should stop rather than
1392 /// spin.
1393 #[test]
1394 fn a_refused_password_is_an_auth_failure() {
1395 let (url, server) = scripted("-WRONGPASS invalid username-password pair\r\n");
1396
1397 let source = Redis::new(&url, "myapp/db.json").unwrap();
1398 let error = source.fetch().expect_err("the server refused the password");
1399
1400 drop(source);
1401 let _ = server.join();
1402
1403 assert_eq!(error.kind(), dynamic_config::ErrorKind::Auth, "{error}");
1404 }
1405
1406 /// The same, for the server that wants a password and was given none.
1407 #[test]
1408 fn a_missing_password_is_an_auth_failure() {
1409 let (url, server) = scripted("-NOAUTH Authentication required.\r\n");
1410
1411 let source = Redis::new(&url, "myapp/db.json").unwrap();
1412 let error = source.fetch().expect_err("the server wants a password");
1413
1414 drop(source);
1415 let _ = server.join();
1416
1417 assert_eq!(error.kind(), dynamic_config::ErrorKind::Auth, "{error}");
1418 }
1419
1420 /// The over-classification that would cost the most: a store that is
1421 /// simply not there stays `Remote`, so a watch loop backs off and
1422 /// recovers when it comes back.
1423 #[test]
1424 fn an_unreachable_server_is_remote_rather_than_auth() {
1425 // Port 9 is discard; nothing listens there.
1426 let source = Redis::new("redis://app:hunter2@127.0.0.1:9", "myapp/db.json").unwrap();
1427
1428 let error = source.fetch().expect_err("nothing is listening");
1429
1430 assert_eq!(error.kind(), dynamic_config::ErrorKind::Remote);
1431 assert!(
1432 !error.to_string().contains("hunter2"),
1433 "the password must not reach the message: {error}"
1434 );
1435 }
1436
1437 // -----------------------------------------------------------------------
1438 // Several keys as one document, against a scripted server
1439 // -----------------------------------------------------------------------
1440
1441 /// A listener that answers each command by its verb, and records the
1442 /// commands it was sent.
1443 ///
1444 /// Verb-keyed rather than positional: `redis-rs` opens a connection with a
1445 /// handshake whose length is its business, and a test that counted replies
1446 /// would break the day it sends one more. Anything not in `replies` gets
1447 /// `+OK`, which is what the handshake wants anyway.
1448 /// Every command a scripted server was sent, as its arguments.
1449 type Asked = Arc<std::sync::Mutex<Vec<Vec<String>>>>;
1450
1451 fn by_verb(
1452 replies: Vec<(&'static str, Vec<String>)>,
1453 ) -> (String, Asked, std::thread::JoinHandle<()>) {
1454 use std::io::{Read, Write};
1455
1456 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1457 let url = format!("redis://{}", listener.local_addr().unwrap());
1458 let asked: Asked = Arc::default();
1459
1460 let seen = Arc::clone(&asked);
1461 let server = std::thread::spawn(move || {
1462 let mut queued: std::collections::HashMap<&str, std::collections::VecDeque<String>> =
1463 replies
1464 .into_iter()
1465 .map(|(verb, answers)| (verb, answers.into_iter().collect()))
1466 .collect();
1467
1468 let Ok((mut stream, _)) = listener.accept() else {
1469 return;
1470 };
1471
1472 let mut buffer = [0u8; 8192];
1473
1474 while let Ok(read) = stream.read(&mut buffer) {
1475 if read == 0 {
1476 return;
1477 }
1478
1479 for command in parsed(&String::from_utf8_lossy(&buffer[..read])) {
1480 let verb = command[0].to_ascii_uppercase();
1481
1482 seen.lock().unwrap().push(command.clone());
1483
1484 let reply = queued
1485 .get_mut(verb.as_str())
1486 .and_then(std::collections::VecDeque::pop_front)
1487 .unwrap_or_else(|| "+OK\r\n".to_owned());
1488
1489 if stream.write_all(reply.as_bytes()).is_err() {
1490 return;
1491 }
1492 }
1493 }
1494 });
1495
1496 (url, asked, server)
1497 }
1498
1499 /// Every command in a (possibly pipelined) RESP write, as its arguments.
1500 fn parsed(text: &str) -> Vec<Vec<String>> {
1501 let lines: Vec<&str> = text.split("\r\n").collect();
1502 let mut commands = Vec::new();
1503 let mut at = 0;
1504
1505 while at < lines.len() {
1506 let Some(count) = lines[at]
1507 .strip_prefix('*')
1508 .and_then(|n| n.parse::<usize>().ok())
1509 else {
1510 at += 1;
1511 continue;
1512 };
1513
1514 // Each argument is a `$len` line and a value line, so the command
1515 // occupies exactly `2 * count` lines after its header.
1516 let arguments: Vec<String> = (0..count)
1517 .filter_map(|n| lines.get(at + 2 + n * 2).map(|value| (*value).to_owned()))
1518 .collect();
1519
1520 if !arguments.is_empty() {
1521 commands.push(arguments);
1522 }
1523
1524 at += 1 + count * 2;
1525 }
1526
1527 commands
1528 }
1529
1530 /// A RESP array of bulk strings, `None` becoming a nil.
1531 fn resp_array(values: &[Option<&str>]) -> String {
1532 let mut encoded = format!("*{}\r\n", values.len());
1533
1534 for value in values {
1535 match value {
1536 Some(text) => encoded.push_str(&format!("${}\r\n{text}\r\n", text.len())),
1537 None => encoded.push_str("$-1\r\n"),
1538 }
1539 }
1540
1541 encoded
1542 }
1543
1544 /// One `SCAN` reply: the next cursor, and the keys in this round.
1545 fn resp_scan(cursor: &str, keys: &[&str]) -> String {
1546 let keys: Vec<Option<&str>> = keys.iter().map(|key| Some(*key)).collect();
1547
1548 format!(
1549 "*2\r\n${}\r\n{cursor}\r\n{}",
1550 cursor.len(),
1551 resp_array(&keys)
1552 )
1553 }
1554
1555 /// A named list is one `MGET` — one command, one round trip, one operation
1556 /// on the server — and the later key wins where two of them meet.
1557 #[test]
1558 fn a_named_list_is_one_mget_in_call_order() {
1559 let (url, asked, server) = by_verb(vec![(
1560 "MGET",
1561 vec![resp_array(&[
1562 Some(r#"{"db": {"host": "base", "port": 5432}}"#),
1563 Some(r#"{"db": {"port": 6432}}"#),
1564 ])],
1565 )]);
1566
1567 let source =
1568 Redis::new(&url, Keys::several(["myapp/base.json", "myapp/local.json"])).unwrap();
1569 let fetched = source.fetch().expect("both keys answered");
1570
1571 drop(source);
1572 let _ = server.join();
1573
1574 let asked = asked.lock().unwrap();
1575 let mget = asked
1576 .iter()
1577 .find(|command| command[0].eq_ignore_ascii_case("MGET"))
1578 .expect("one MGET");
1579
1580 assert_eq!(
1581 mget[1..],
1582 ["myapp/base.json".to_owned(), "myapp/local.json".to_owned()],
1583 "the caller's order is the merge order"
1584 );
1585 assert!(
1586 !asked
1587 .iter()
1588 .any(|command| command[0].eq_ignore_ascii_case("GET")),
1589 "a named list must not become one GET per key: {asked:?}"
1590 );
1591
1592 let merged = dynamic_config::Value::parse(&fetched.text, Format::Json).unwrap();
1593
1594 assert_eq!(
1595 merged.get("db.host"),
1596 Some(&dynamic_config::Value::String("base".to_owned())),
1597 "a key the later document never mentions survives"
1598 );
1599 assert_eq!(
1600 merged.get("db.port"),
1601 Some(&dynamic_config::Value::Integer(6432)),
1602 "and the later document wins where they meet"
1603 );
1604 }
1605
1606 /// `SCAN`, never `KEYS`: `KEYS` walks the whole key space in one blocking
1607 /// operation, which is the classic way to stall a production server. The
1608 /// cursor is followed to the end, and a key handed back twice is folded.
1609 #[test]
1610 fn a_prefix_scans_in_rounds_and_never_asks_for_keys() {
1611 let (url, asked, server) = by_verb(vec![
1612 (
1613 "SCAN",
1614 vec![
1615 resp_scan("17", &["myapp/db.json"]),
1616 // The same key again: a cursor guarantees coverage, not
1617 // uniqueness, and the duplicate would collide with itself
1618 // under the disjoint rule.
1619 resp_scan("0", &["myapp/server.json", "myapp/db.json"]),
1620 ],
1621 ),
1622 (
1623 "MGET",
1624 vec![resp_array(&[
1625 Some(r#"{"db": {"host": "db.internal"}}"#),
1626 Some(r#"{"server": {"port": 8080}}"#),
1627 ])],
1628 ),
1629 ]);
1630
1631 let source = Redis::new(&url, Keys::prefix("myapp/"))
1632 .unwrap()
1633 .with_format(Format::Json);
1634 let fetched = source.fetch().expect("the scan finished");
1635
1636 drop(source);
1637 let _ = server.join();
1638
1639 let asked = asked.lock().unwrap();
1640 let scans: Vec<&Vec<String>> = asked
1641 .iter()
1642 .filter(|command| command[0].eq_ignore_ascii_case("SCAN"))
1643 .collect();
1644
1645 assert_eq!(scans.len(), 2, "the cursor is followed: {asked:?}");
1646 assert_eq!(scans[0][1], "0", "the first round starts at zero");
1647 assert_eq!(scans[1][1], "17", "and the next carries the cursor back");
1648 assert!(
1649 scans[0].iter().any(|argument| argument == "myapp/*"),
1650 "the prefix goes out as a literal with one trailing star: {scans:?}"
1651 );
1652 assert!(
1653 !asked
1654 .iter()
1655 .any(|command| command[0].eq_ignore_ascii_case("KEYS")),
1656 "KEYS blocks a production server: {asked:?}"
1657 );
1658
1659 let mget = asked
1660 .iter()
1661 .find(|command| command[0].eq_ignore_ascii_case("MGET"))
1662 .expect("one MGET for the whole set");
1663
1664 assert_eq!(
1665 mget[1..],
1666 ["myapp/db.json".to_owned(), "myapp/server.json".to_owned()],
1667 "sorted and deduplicated, so the same keys give the same document"
1668 );
1669
1670 let merged = dynamic_config::Value::parse(&fetched.text, Format::Json).unwrap();
1671
1672 assert_eq!(
1673 merged.get("db.host"),
1674 Some(&dynamic_config::Value::String("db.internal".to_owned()))
1675 );
1676 assert_eq!(
1677 merged.get("server.port"),
1678 Some(&dynamic_config::Value::Integer(8080))
1679 );
1680 }
1681
1682 /// A prefix says "these are disjoint sections". Two of them supplying one
1683 /// path is a deployment bug, named rather than resolved — by path, never
1684 /// by value.
1685 #[test]
1686 fn two_keys_under_a_prefix_supplying_one_path_are_refused_by_name() {
1687 let (url, _asked, server) = by_verb(vec![
1688 (
1689 "SCAN",
1690 vec![resp_scan("0", &["myapp/a.json", "myapp/b.json"])],
1691 ),
1692 (
1693 "MGET",
1694 vec![resp_array(&[
1695 Some(r#"{"db": {"password": "hunter2-first"}}"#),
1696 Some(r#"{"db": {"password": "hunter2-second"}}"#),
1697 ])],
1698 ),
1699 ]);
1700
1701 let source = Redis::new(&url, Keys::prefix("myapp/"))
1702 .unwrap()
1703 .with_format(Format::Json);
1704 let error = source.fetch().expect_err("both keys supply db.password");
1705
1706 drop(source);
1707 let _ = server.join();
1708
1709 let printed = format!("{error} {error:?}");
1710
1711 assert!(printed.contains("myapp/a.json"), "{printed}");
1712 assert!(printed.contains("myapp/b.json"), "{printed}");
1713 assert!(printed.contains("db.password"), "{printed}");
1714 assert!(
1715 !printed.contains("hunter2"),
1716 "a collision report names paths and never values: {printed}"
1717 );
1718 }
1719
1720 /// Fail-whole, not merge-what-came-back: `MGET` answers a missing key with
1721 /// a nil, and four sections out of five is a configuration with a hole in
1722 /// it that nobody would notice.
1723 #[test]
1724 fn one_key_holding_nothing_fails_the_whole_fetch_and_names_it() {
1725 let (url, _asked, server) = by_verb(vec![(
1726 "MGET",
1727 vec![resp_array(&[Some(r#"{"db": {"host": "here"}}"#), None])],
1728 )]);
1729
1730 let source =
1731 Redis::new(&url, Keys::several(["myapp/db.json", "myapp/absent.json"])).unwrap();
1732 let error = source.fetch().expect_err("the second key is not there");
1733
1734 drop(source);
1735 let _ = server.join();
1736
1737 assert_eq!(error.kind(), dynamic_config::ErrorKind::Remote);
1738 assert!(error.to_string().contains("myapp/absent.json"), "{error}");
1739 assert!(error.to_string().contains("holds no value"), "{error}");
1740 }
1741
1742 /// A key the server answers with that is not under the prefix asked for is
1743 /// refused rather than merged — the check the glob escaping should make
1744 /// unreachable, kept because a prefix has to mean the prefix.
1745 #[test]
1746 fn a_key_outside_the_prefix_is_refused() {
1747 let (url, _asked, server) = by_verb(vec![(
1748 "SCAN",
1749 vec![resp_scan("0", &["myapp/db.json", "other/db.json"])],
1750 )]);
1751
1752 let source = Redis::new(&url, Keys::prefix("myapp/"))
1753 .unwrap()
1754 .with_format(Format::Json);
1755 let error = source.fetch().expect_err("one key escaped the prefix");
1756
1757 drop(source);
1758 let _ = server.join();
1759
1760 assert!(error.to_string().contains("other/db.json"), "{error}");
1761 }
1762
1763 /// A cursor is server state, and a server that never advances it would
1764 /// otherwise be an infinite loop inside a fetch — one the key budget
1765 /// cannot catch, because a scan can return nothing at all round after
1766 /// round.
1767 #[test]
1768 fn a_cursor_that_never_advances_ends_the_scan_rather_than_the_process() {
1769 let (url, _asked, server) = by_verb(vec![(
1770 "SCAN",
1771 // Far more than the loop will take, all of them going nowhere.
1772 std::iter::repeat_with(|| resp_scan("17", &[]))
1773 .take(MOST_SCAN_ROUNDS + 8)
1774 .collect(),
1775 )]);
1776
1777 let source = Redis::new(&url, Keys::prefix("myapp/"))
1778 .unwrap()
1779 .with_format(Format::Json);
1780 let error = source
1781 .fetch()
1782 .expect_err("the cursor never comes back to 0");
1783
1784 drop(source);
1785 let _ = server.join();
1786
1787 assert!(
1788 error.to_string().contains("advancing the cursor"),
1789 "{error}"
1790 );
1791 }
1792
1793 /// A prefix that matched nothing is a missing configuration, not an empty
1794 /// one — and an `MGET` with no keys is a protocol error, so it must not be
1795 /// sent either.
1796 #[test]
1797 fn a_prefix_that_matches_nothing_is_a_failure_rather_than_an_empty_document() {
1798 let (url, asked, server) = by_verb(vec![("SCAN", vec![resp_scan("0", &[])])]);
1799
1800 let source = Redis::new(&url, Keys::prefix("myapp/"))
1801 .unwrap()
1802 .with_format(Format::Json);
1803 let error = source.fetch().expect_err("nothing matched");
1804
1805 drop(source);
1806 let _ = server.join();
1807
1808 assert!(error.to_string().contains("nothing matched"), "{error}");
1809 assert!(
1810 !asked
1811 .lock()
1812 .unwrap()
1813 .iter()
1814 .any(|command| command[0].eq_ignore_ascii_case("MGET")),
1815 "an MGET with no keys is a protocol error"
1816 );
1817 }
1818
1819 /// The credential rule at the new error paths: a password must not be
1820 /// quoted back in a complaint about a set of keys.
1821 #[test]
1822 fn no_multi_key_error_path_prints_a_credential() {
1823 let source = Redis::new(
1824 "redis://app:hunter2@127.0.0.1:9",
1825 Keys::several(["myapp/db.json", "myapp/second.json"]),
1826 )
1827 .unwrap();
1828
1829 let error = source.fetch().expect_err("nothing is listening");
1830 let printed = format!("{error} {error:?} {source:?}");
1831
1832 assert!(!printed.contains("hunter2"), "{printed}");
1833 assert!(printed.contains("myapp/second.json"), "{printed}");
1834 }
1835
1836 /// The mistake the deadline exists to prevent: a server that accepts the
1837 /// socket and then never answers. A connect-only timeout sails past this.
1838 #[test]
1839 fn a_read_from_a_server_that_never_answers_ends_at_the_deadline() {
1840 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1841 let url = format!("redis://{}", listener.local_addr().unwrap());
1842
1843 let silent = std::thread::spawn(move || {
1844 let held = listener.accept();
1845 std::thread::sleep(Duration::from_secs(2));
1846 drop(held);
1847 });
1848
1849 let source = Redis::new(&url, "myapp/db.json")
1850 .unwrap()
1851 .with_timeout(Duration::from_millis(200));
1852
1853 let started = std::time::Instant::now();
1854 let error = source.fetch().expect_err("nothing ever answers");
1855 let elapsed = started.elapsed();
1856
1857 assert!(
1858 elapsed < Duration::from_secs(1),
1859 "the deadline must bound the read, not merely the connect: {elapsed:?}"
1860 );
1861 assert_eq!(
1862 error.kind(),
1863 dynamic_config::ErrorKind::Remote,
1864 "a store that went quiet may yet come back: {error}"
1865 );
1866
1867 let _ = silent.join();
1868 }
1869 // -----------------------------------------------------------------------
1870 // Watching a named list, against a scripted server
1871 // -----------------------------------------------------------------------
1872
1873 /// A bulk string.
1874 fn resp_bulk(text: &str) -> String {
1875 format!("${}\r\n{text}\r\n", text.len())
1876 }
1877
1878 /// A scripted server that accepts several connections at once and answers
1879 /// by verb and subcommand.
1880 ///
1881 /// Several, because one watch opens four: the notification check, the
1882 /// database probe, the subscriber, and the connection the reads reuse. The
1883 /// single-connection server the fetch tests use would deadlock on the
1884 /// second.
1885 ///
1886 /// Once every expected channel has been subscribed to it does what `then`
1887 /// says.
1888 fn subscribable(
1889 channels: usize,
1890 mget: Vec<String>,
1891 then: Then,
1892 ) -> (String, Asked, Arc<std::sync::atomic::AtomicBool>) {
1893 use std::sync::atomic::{AtomicBool, Ordering};
1894
1895 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1896 let url = format!("redis://{}", listener.local_addr().unwrap());
1897 let asked: Asked = Arc::default();
1898 let stop = Arc::new(AtomicBool::new(false));
1899
1900 listener.set_nonblocking(true).unwrap();
1901
1902 let seen = Arc::clone(&asked);
1903 let stopping = Arc::clone(&stop);
1904 let queued: Arc<Mutex<std::collections::VecDeque<String>>> =
1905 Arc::new(Mutex::new(mget.into_iter().collect()));
1906
1907 std::thread::spawn(move || {
1908 while !stopping.load(Ordering::Acquire) {
1909 match listener.accept() {
1910 Ok((stream, _)) => {
1911 // An accepted socket does not reliably inherit the
1912 // listener's blocking mode, and a scripted server that
1913 // read `WouldBlock` as end-of-connection would answer
1914 // nothing at all.
1915 stream.set_nonblocking(false).unwrap();
1916
1917 let seen = Arc::clone(&seen);
1918 let queued = Arc::clone(&queued);
1919
1920 std::thread::spawn(move || serve(stream, &seen, &queued, channels, then));
1921 }
1922 Err(_) => std::thread::sleep(Duration::from_millis(10)),
1923 }
1924 }
1925 });
1926
1927 (url, asked, stop)
1928 }
1929
1930 /// What [`subscribable`] does once every expected channel has been
1931 /// subscribed to.
1932 #[derive(Clone, Copy, PartialEq, Eq)]
1933 enum Then {
1934 /// Publishes one notification **per channel**, which is what one
1935 /// `MSET` over the set really does — so this is also the shape that
1936 /// would catch a set being delivered once per key.
1937 Publishes,
1938 /// Closes the subscriber's connection without a word, which is what a
1939 /// server restart, a `CLIENT KILL` or a proxy dropping the connection
1940 /// looks like from inside the loop.
1941 HangsUp,
1942 }
1943
1944 /// One connection of [`subscribable`].
1945 fn serve(
1946 mut stream: std::net::TcpStream,
1947 seen: &Asked,
1948 queued: &Mutex<std::collections::VecDeque<String>>,
1949 channels: usize,
1950 then: Then,
1951 ) {
1952 use std::io::{Read, Write};
1953
1954 let mut buffer = [0u8; 8192];
1955 let mut subscribed: Vec<String> = Vec::new();
1956
1957 while let Ok(read) = stream.read(&mut buffer) {
1958 if read == 0 {
1959 return;
1960 }
1961
1962 for command in parsed(&String::from_utf8_lossy(&buffer[..read])) {
1963 seen.lock().unwrap().push(command.clone());
1964
1965 let verb = command[0].to_ascii_uppercase();
1966 let sub = command
1967 .get(1)
1968 .map(|argument| argument.to_ascii_uppercase())
1969 .unwrap_or_default();
1970
1971 let reply = match (verb.as_str(), sub.as_str()) {
1972 ("CLIENT", "INFO") => {
1973 resp_bulk("id=4 addr=127.0.0.1:1 laddr=127.0.0.1:2 fd=8 name= db=0 age=0")
1974 }
1975 ("CONFIG", "GET") => resp_array(&[Some("notify-keyspace-events"), Some("KEA")]),
1976 ("SUBSCRIBE", _) => {
1977 subscribed.push(command[1].clone());
1978
1979 format!(
1980 "*3\r\n{}{}:{}\r\n",
1981 resp_bulk("subscribe"),
1982 resp_bulk(&command[1]),
1983 subscribed.len()
1984 )
1985 }
1986 ("MGET", _) => queued
1987 .lock()
1988 .unwrap()
1989 .pop_front()
1990 .unwrap_or_else(|| "*-1\r\n".to_owned()),
1991 _ => "+OK\r\n".to_owned(),
1992 };
1993
1994 if stream.write_all(reply.as_bytes()).is_err() {
1995 return;
1996 }
1997
1998 if verb == "SUBSCRIBE" && subscribed.len() == channels {
1999 if then == Then::HangsUp {
2000 return;
2001 }
2002
2003 for channel in &subscribed {
2004 let published = format!(
2005 "*3\r\n{}{}{}",
2006 resp_bulk("message"),
2007 resp_bulk(channel),
2008 resp_bulk("set")
2009 );
2010
2011 if stream.write_all(published.as_bytes()).is_err() {
2012 return;
2013 }
2014 }
2015 }
2016 }
2017 }
2018 }
2019
2020 /// The two halves of the claim that a named list can be watched honestly:
2021 /// the loop hears about **every** key of the set, and the re-read that
2022 /// follows is **one `MGET`** — one command, so one operation on the server.
2023 ///
2024 /// A watch that subscribed to the first key only would go quiet the moment
2025 /// a deployment changed the second; one that re-read key by key would
2026 /// deliver a document the server never held.
2027 #[test]
2028 fn a_named_list_subscribes_to_every_key_and_re_reads_the_set_with_one_mget() {
2029 let answer = resp_array(&[
2030 Some(r#"{"db": {"host": "base", "port": 5432}}"#),
2031 Some(r#"{"db": {"port": 6432}}"#),
2032 ]);
2033 let (url, asked, stop) = subscribable(2, vec![answer.clone(), answer], Then::Publishes);
2034
2035 let source =
2036 Redis::new(&url, Keys::several(["myapp/base.json", "myapp/local.json"])).unwrap();
2037
2038 let watch = dynamic_config::RemoteWatch::new();
2039 let watching = watch.watching();
2040 let (sender, receiver) = std::sync::mpsc::channel();
2041
2042 let loops = std::thread::spawn(move || {
2043 source.watch(&watching, move |document| {
2044 let _ = sender.send(document.text);
2045 Ok(())
2046 })
2047 });
2048
2049 let text = receiver
2050 .recv_timeout(Duration::from_secs(10))
2051 .expect("the notification should reach the callback");
2052
2053 let merged = dynamic_config::Value::parse(&text, Format::Json).unwrap();
2054
2055 assert_eq!(
2056 merged.get("db.host"),
2057 Some(&dynamic_config::Value::String("base".to_owned())),
2058 "the whole set is delivered, not the key that changed"
2059 );
2060 assert_eq!(
2061 merged.get("db.port"),
2062 Some(&dynamic_config::Value::Integer(6432)),
2063 "and the caller's order is still the merge order"
2064 );
2065
2066 // Both notifications have to have been *processed* before the
2067 // coalescing claim below means anything, and the second `MGET` is
2068 // what says so.
2069 let deadline = std::time::Instant::now() + Duration::from_secs(10);
2070
2071 while std::time::Instant::now() < deadline {
2072 let mgets = asked
2073 .lock()
2074 .unwrap()
2075 .iter()
2076 .filter(|command| command[0].eq_ignore_ascii_case("MGET"))
2077 .count();
2078
2079 if mgets >= 2 {
2080 break;
2081 }
2082
2083 std::thread::sleep(Duration::from_millis(20));
2084 }
2085
2086 assert!(
2087 receiver.recv_timeout(Duration::from_millis(500)).is_err(),
2088 "a set written together publishes once per key and is one document; \
2089 delivering it per key would run every reload hook per key"
2090 );
2091
2092 watch.stop();
2093 stop.store(true, std::sync::atomic::Ordering::Release);
2094
2095 let outcome = loops.join().expect("the loop should end");
2096
2097 assert!(outcome.is_ok(), "{outcome:?}");
2098
2099 let asked = asked.lock().unwrap();
2100 let subscribed: Vec<&String> = asked
2101 .iter()
2102 .filter(|command| command[0].eq_ignore_ascii_case("SUBSCRIBE"))
2103 .map(|command| &command[1])
2104 .collect();
2105
2106 assert_eq!(
2107 subscribed,
2108 [
2109 "__keyspace@0__:myapp/base.json",
2110 "__keyspace@0__:myapp/local.json"
2111 ],
2112 "every key of the set is subscribed to, on the database the \
2113 connection lands on: {asked:?}"
2114 );
2115
2116 let mget = asked
2117 .iter()
2118 .find(|command| command[0].eq_ignore_ascii_case("MGET"))
2119 .expect("the set is re-read with one MGET");
2120
2121 assert_eq!(
2122 mget[1..],
2123 ["myapp/base.json".to_owned(), "myapp/local.json".to_owned()],
2124 "one command carrying the whole set is what makes the delivery a \
2125 state the server really had"
2126 );
2127 assert!(
2128 !asked
2129 .iter()
2130 .any(|command| command[0].eq_ignore_ascii_case("GET")),
2131 "a re-read key by key is the torn document this watch exists to \
2132 avoid: {asked:?}"
2133 );
2134 assert!(
2135 !asked
2136 .iter()
2137 .any(|command| command[0].eq_ignore_ascii_case("SCAN")),
2138 "a named list knows its keys: {asked:?}"
2139 );
2140 }
2141
2142 // -----------------------------------------------------------------------
2143 // Reporting a failing watch
2144 //
2145 // One `#[dynamic_config]` type per test: the snapshot, the remote slot and
2146 // the sink's generation all live in statics keyed by the type, so two
2147 // tests sharing one would race and — worse — pass alone.
2148 // -----------------------------------------------------------------------
2149
2150 /// The failure nobody notices: the loop was working, the subscription
2151 /// died, and the watch ends on a thread whose result is usually dropped.
2152 /// Configuration simply stops updating.
2153 ///
2154 /// What the status must say afterwards is a *pair*: `reachable()` goes to
2155 /// `Some(false)` while `last_fetch` keeps the instant the last document
2156 /// really arrived — so an alert can ask "down, and stale for how long".
2157 /// A failure that reset the clock would hide the second half.
2158 #[test]
2159 fn a_dead_subscription_reports_the_store_as_down_and_leaves_the_clock_running() {
2160 use dynamic_config::dynamic_config;
2161
2162 #[dynamic_config]
2163 #[derive(Debug, serde::Deserialize)]
2164 struct Subscribed {
2165 // Never read: this test is about the status the store records,
2166 // not about the document, which never gets as far as a snapshot.
2167 #[allow(dead_code)]
2168 host: String,
2169 }
2170
2171 let (url, _asked, stop) = subscribable(
2172 1,
2173 vec![resp_array(&[Some(r#"{"db": {"host": "base"}}"#)])],
2174 Then::HangsUp,
2175 );
2176
2177 Subscribed::set_remote(Redis::new(&url, "myapp/db.json").unwrap());
2178 Subscribed::refresh_remote().expect("the store answers the first read");
2179
2180 // Taken after the source is installed, which is what fences it.
2181 let sink = Subscribed::remote_sink();
2182 let before = sink.status();
2183
2184 assert_eq!(before.reachable(), Some(true), "one fetch, and it answered");
2185 assert!(before.last_fetch.is_some());
2186
2187 let watcher = Redis::new(&url, "myapp/db.json")
2188 .unwrap()
2189 .reporting_to(sink);
2190 let watch = dynamic_config::RemoteWatch::new();
2191 let watching = watch.watching();
2192 let (ended, ending) = std::sync::mpsc::channel();
2193
2194 let loops = std::thread::spawn(move || {
2195 let outcome = watcher.watch(&watching, |_| Ok(()));
2196 let _ = ended.send(());
2197 outcome
2198 });
2199
2200 ending
2201 .recv_timeout(Duration::from_secs(10))
2202 .expect("a subscription that died ends the watch rather than spinning");
2203
2204 let outcome = loops.join().expect("the thread should end");
2205 let error = outcome.expect_err("the subscription died");
2206
2207 watch.stop();
2208 stop.store(true, std::sync::atomic::Ordering::Release);
2209
2210 assert!(
2211 error.to_string().contains("the subscription failed"),
2212 "{error}"
2213 );
2214
2215 let after = sink.status();
2216
2217 assert_eq!(
2218 after.reachable(),
2219 Some(false),
2220 "a loop that stopped reaching its store is a store that is down"
2221 );
2222 assert_eq!(after.consecutive_failures, 1);
2223 assert_eq!(
2224 after.last_fetch, before.last_fetch,
2225 "the staleness clock keeps running: `last_fetch` is when a document \
2226 last arrived, and a failed attempt is not one"
2227 );
2228 assert_eq!(
2229 after.fetches, before.fetches,
2230 "a failure is not a fetch, however it is counted elsewhere"
2231 );
2232 assert_eq!(
2233 after
2234 .last_failure
2235 .as_ref()
2236 .expect("a failure was recorded")
2237 .kind,
2238 dynamic_config::ErrorKind::Remote,
2239 "a subscription that dropped may yet come back"
2240 );
2241
2242 // The recorded failure is a kind and a path, and the URL that carries
2243 // the password is in neither.
2244 assert!(!format!("{:?}", after.last_failure).contains("myapp/db.json"));
2245 }
2246
2247 /// The other shape, and the one that argues for itself least obviously: a
2248 /// re-read that failed does not end the watch, so nothing else will ever
2249 /// mention it. One failure between deliveries is a blip the streak clears;
2250 /// a credential the server has started refusing is every notification from
2251 /// now on, and the streak is what says which this is.
2252 #[test]
2253 fn a_failed_re_read_is_reported_and_the_watch_carries_on() {
2254 use dynamic_config::dynamic_config;
2255
2256 #[dynamic_config]
2257 #[derive(Debug, serde::Deserialize)]
2258 struct Rereading {
2259 // Never read: this test is about the status the store records,
2260 // not about the document, which never gets as far as a snapshot.
2261 #[allow(dead_code)]
2262 host: String,
2263 }
2264
2265 let (url, _asked, stop) = subscribable(
2266 1,
2267 vec![
2268 // The first read is the caller's own `refresh_remote()`.
2269 resp_array(&[Some(r#"{"db": {"host": "base"}}"#)]),
2270 // The re-read the notification triggers: the key holds
2271 // nothing, which is what a deleted member of a set looks like.
2272 resp_array(&[None]),
2273 ],
2274 Then::Publishes,
2275 );
2276
2277 Rereading::set_remote(Redis::new(&url, "myapp/db.json").unwrap());
2278 Rereading::refresh_remote().expect("the store answers the first read");
2279
2280 let sink = Rereading::remote_sink();
2281 let before = sink.status();
2282
2283 let watcher = Redis::new(&url, "myapp/db.json")
2284 .unwrap()
2285 .reporting_to(sink);
2286 let watch = dynamic_config::RemoteWatch::new();
2287 let watching = watch.watching();
2288 let (ended, ending) = std::sync::mpsc::channel();
2289
2290 let loops = std::thread::spawn(move || {
2291 let outcome = watcher.watch(&watching, |_| Ok(()));
2292 let _ = ended.send(());
2293 outcome
2294 });
2295
2296 let deadline = std::time::Instant::now() + Duration::from_secs(10);
2297
2298 while sink.status().consecutive_failures == 0 && std::time::Instant::now() < deadline {
2299 std::thread::sleep(Duration::from_millis(20));
2300 }
2301
2302 let after = sink.status();
2303
2304 assert_eq!(
2305 after.reachable(),
2306 Some(false),
2307 "the notification arrived and the document did not"
2308 );
2309 assert_eq!(
2310 after.last_fetch, before.last_fetch,
2311 "a failed re-read leaves the clock where the last document left it"
2312 );
2313 assert_eq!(after.fetches, before.fetches);
2314
2315 assert!(
2316 ending.recv_timeout(Duration::from_millis(500)).is_err(),
2317 "a failed re-read is transient: the next write notifies again, so \
2318 the loop must still be running"
2319 );
2320
2321 watch.stop();
2322 stop.store(true, std::sync::atomic::Ordering::Release);
2323
2324 let outcome = loops.join().expect("the thread should end");
2325
2326 assert!(outcome.is_ok(), "stopping is not a failure: {outcome:?}");
2327 }
2328
2329 /// A watch refused at the door is *not* a store that stopped answering.
2330 ///
2331 /// The line, and 0.6.1's audit of all seven watch loops is what put it
2332 /// here rather than in one crate's habit: etcd and NATS used to report a
2333 /// refusal that never reached the store, Redis and S3 did not, and both
2334 /// had a test saying so. `RemoteStatus::reachable()` settles it — it is
2335 /// *whether the store answered the last time it was asked*, and a source
2336 /// with an unwatchable key shape never asks. Charging it to
2337 /// `dynamic_config_remote_up` would page somebody about Redis for a typo,
2338 /// and the status carries no message to correct them with.
2339 #[test]
2340 fn a_watch_refused_at_the_door_is_not_a_store_that_stopped_answering() {
2341 use dynamic_config::dynamic_config;
2342
2343 #[dynamic_config]
2344 #[derive(Debug, serde::Deserialize)]
2345 struct Refused {
2346 // Never read: this test is about the status the store records,
2347 // not about the document, which never gets as far as a snapshot.
2348 #[allow(dead_code)]
2349 host: String,
2350 }
2351
2352 // No source is installed: a sink does not need one, and this is the
2353 // state a program that only ever watches is in.
2354 let sink = Refused::remote_sink();
2355
2356 let client = Client::open("redis://127.0.0.1:9").unwrap();
2357 let source = Redis::from_client(client, Keys::prefix("myapp/"))
2358 .with_format(Format::Json)
2359 .reporting_to(sink);
2360
2361 let watch = dynamic_config::RemoteWatch::new();
2362 let error = source
2363 .watch(&watch.watching(), |_| Ok(()))
2364 .expect_err("a prefix cannot be watched");
2365
2366 assert!(error.to_string().contains("cannot be watched"), "{error}");
2367 assert_eq!(
2368 sink.status().reachable(),
2369 None,
2370 "nothing has been asked of this store, so it is neither up nor down"
2371 );
2372 }
2373
2374 // -----------------------------------------------------------------------
2375 // TLS: the shared vocabulary, translated into the client's own
2376 // `TlsCertificates`.
2377 // -----------------------------------------------------------------------
2378
2379 /// A certificate authority and a client certificate signed by it,
2380 /// generated here. A committed fixture expires, and a suite that fails on
2381 /// a date nobody chose is worse than one that costs a millisecond.
2382 #[cfg(feature = "tls")]
2383 fn material() -> (String, String, String) {
2384 use rcgen::{BasicConstraints, CertificateParams, IsCa, KeyPair};
2385
2386 let ca_key = KeyPair::generate().unwrap();
2387 let mut ca_params = CertificateParams::new(Vec::new()).unwrap();
2388 ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
2389 let ca = ca_params.self_signed(&ca_key).unwrap();
2390
2391 let issuer = rcgen::Issuer::from_params(&ca_params, &ca_key);
2392 let client_key = KeyPair::generate().unwrap();
2393 let client = CertificateParams::new(vec!["myapp".to_owned()])
2394 .unwrap()
2395 .signed_by(&client_key, &issuer)
2396 .unwrap();
2397
2398 (ca.pem(), client.pem(), client_key.serialize_pem())
2399 }
2400
2401 /// The happy path, with material nothing on this machine trusts: the
2402 /// client is built, and construction still reaches no network.
2403 #[cfg(feature = "tls")]
2404 #[test]
2405 fn a_private_authority_and_a_client_certificate_build_a_client() {
2406 let (ca, certificate, key) = material();
2407
2408 Redis::with_tls(
2409 "rediss://127.0.0.1:6379",
2410 "myapp/db.json",
2411 &TlsConfig::new()
2412 .with_ca_certificate_pem(ca)
2413 .with_client_certificate_pem(certificate, key),
2414 )
2415 .expect("the generated material is valid PEM");
2416 }
2417
2418 /// TLS material on a `redis://` URL is a deployment that believes it is
2419 /// encrypted and is not. Refused here, naming the scheme, rather than
2420 /// three layers down where the message has no address in it.
2421 #[cfg(feature = "tls")]
2422 #[test]
2423 fn tls_material_on_a_plaintext_url_is_refused_rather_than_ignored() {
2424 let (ca, _, _) = material();
2425
2426 let error = Redis::with_tls(
2427 "redis://127.0.0.1:6379",
2428 "myapp/db.json",
2429 &TlsConfig::new().with_ca_certificate_pem(ca),
2430 )
2431 .expect_err("that URL negotiates no TLS at all");
2432
2433 assert!(error.to_string().contains("rediss://"), "{error}");
2434 assert!(
2435 error.to_string().contains("refused rather than ignored"),
2436 "{error}"
2437 );
2438 }
2439
2440 /// The sharpest rule in this feature: the client's own message for a key
2441 /// it cannot parse embeds the parser's, and the parser's renders the line
2442 /// it choked on. So the upstream text is dropped, and this is the test
2443 /// that says so.
2444 #[cfg(feature = "tls")]
2445 #[test]
2446 fn a_malformed_private_key_never_quotes_itself_into_the_error() {
2447 const PLANTED: &str = "PLANTED-PRIVATE-KEY-MATERIAL";
2448
2449 let (ca, certificate, _) = material();
2450
2451 let error = Redis::with_tls(
2452 "rediss://127.0.0.1:6379",
2453 "myapp/db.json",
2454 &TlsConfig::new()
2455 .with_ca_certificate_pem(ca)
2456 .with_client_certificate_pem(
2457 certificate,
2458 format!("-----BEGIN PRIVATE KEY-----\n{PLANTED}\n-----END PRIVATE KEY-----\n"),
2459 ),
2460 )
2461 .expect_err("the key is not a key");
2462
2463 assert!(!error.to_string().contains(PLANTED), "{error}");
2464 assert!(!format!("{error:?}").contains(PLANTED), "{error:?}");
2465 }
2466
2467 /// A Redis URL carries its password in the authority, and the password
2468 /// may contain `@`. Every new error path gets the same redaction test the
2469 /// old ones have.
2470 #[cfg(feature = "tls")]
2471 #[test]
2472 fn a_tls_failure_never_carries_the_password_out_of_the_url() {
2473 let error = Redis::with_tls(
2474 "rediss://app:p@ss@w@rd@127.0.0.1:6379",
2475 "myapp/db.json",
2476 &TlsConfig::new().with_ca_certificate_file("/nonexistent/private-ca.pem"),
2477 )
2478 .expect_err("the CA file is not there");
2479
2480 let printed = format!("{error} {error:?}");
2481
2482 assert!(!printed.contains("p@ss"), "{printed}");
2483 assert!(printed.contains("app:***@"), "{printed}");
2484 assert!(printed.contains("/nonexistent/private-ca.pem"), "{printed}");
2485 }
2486}