Skip to main content

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}