Skip to main content

dynamic_config_server/
server.rs

1//! The served sections, and the server that owns them.
2//!
3//! Each served section is a [`Dynamic<Document>`] — this crate is a *user*
4//! of the library, not a reimplementation of it. That is the constraint the
5//! design is built around: the same loader resolves the section, the same
6//! watcher notices the file change, the same last-known-good behaviour keeps
7//! a bad edit from taking the section down, and the same
8//! [`ConfigStatus`](dynamic_config::ConfigStatus) answers for it. If this
9//! server ever needs something the library cannot do, that is a library
10//! change, not a server one.
11//!
12//! Nothing here polls. A section reloads because the file watcher said so,
13//! and `/status` is a handful of atomic loads — so an idle server with a
14//! thousand sections costs a thousand idle inotify registrations and no CPU.
15
16use std::collections::BTreeMap;
17use std::fmt;
18use std::net::SocketAddr;
19use std::sync::atomic::{AtomicUsize, Ordering};
20use std::sync::Arc;
21use std::time::Duration;
22
23use dynamic_config::{Builder, Changes, ConfigStatus, Dynamic};
24
25use crate::audit::{AuditEntry, AuditSink, StderrAudit};
26use crate::auth::{Authenticator, Principal, Token};
27use crate::config::{Refusal, SectionConfig, ServerConfig};
28use crate::document::Document;
29
30/// One served application-and-profile pair.
31pub struct Section {
32    application: String,
33    profile: String,
34    config: Dynamic<Document>,
35    /// Dropping this stops the watch, so the handle lives as long as the
36    /// section does and is never read.
37    _watch: Option<dynamic_config::watch::WatchHandle>,
38}
39
40impl Section {
41    /// The application this serves.
42    #[must_use]
43    pub fn application(&self) -> &str {
44        &self.application
45    }
46
47    /// The profile this serves.
48    #[must_use]
49    pub fn profile(&self) -> &str {
50        &self.profile
51    }
52
53    /// The document currently serving, or `None` before the first install.
54    ///
55    /// One atomic load. A handler takes it once and reuses the `Arc`, so a
56    /// reload landing mid-request cannot show one response two generations.
57    #[must_use]
58    pub fn current(&self) -> Option<Arc<Document>> {
59        self.config.current()
60    }
61
62    /// Installs since this section was created.
63    #[must_use]
64    pub fn generation(&self) -> u64 {
65        self.config.generation()
66    }
67
68    /// The serving document together with a generation that is never ahead
69    /// of it.
70    ///
71    /// Two atomic loads, and their order is the whole point.
72    /// [`ConfigCell`](dynamic_config::ConfigCell) publishes a snapshot's
73    /// metadata *after* the snapshot itself — deliberately, so that reading
74    /// configuration stays one load with nothing to project out of it — so
75    /// a generation read first may lag the document and can never lead it.
76    ///
77    /// Reading the document first inverts that, and the inversion is the
78    /// harmful direction: a reload landing between the two loads would send
79    /// the *previous* document under the *new* number, and a client that
80    /// records it — or resumes its change stream with it — has been told it
81    /// consumed an update whose contents it never received. This way round,
82    /// the worst case is a response labelled one install behind its own
83    /// contents: the client is told about that install again and fetches
84    /// once more, which costs a round trip and loses nothing.
85    #[must_use]
86    pub fn installed(&self) -> Option<(u64, Arc<Document>)> {
87        let generation = self.generation();
88
89        self.current().map(|document| (generation, document))
90    }
91
92    /// What is true of this section right now. No I/O.
93    #[must_use]
94    pub fn status(&self) -> ConfigStatus {
95        self.config.status()
96    }
97
98    /// Whether this section can be served.
99    ///
100    /// A section is ready when it has a document *and* the last reload
101    /// installed one. The second half is the point of fronting a store: a
102    /// bad edit upstream leaves the previous document serving — callers see
103    /// no outage — and says so here, so a deployment pipeline notices
104    /// before the next restart turns "stale but working" into "will not
105    /// start".
106    #[must_use]
107    pub fn is_ready(&self) -> bool {
108        self.current().is_some() && self.status().is_healthy()
109    }
110
111    /// A handle woken by every later install of this section.
112    ///
113    /// What the change stream awaits. It costs one `Arc` clone and a `u64`
114    /// — no document, no diff, no queue — which is what lets a connection
115    /// per pod be an ordinary number rather than a memory bound.
116    #[must_use]
117    pub fn changes(&self) -> Changes<Document> {
118        self.config.changes()
119    }
120
121    /// One reload: read the sources, install if they are good.
122    ///
123    /// # Errors
124    ///
125    /// Whatever the load reports. A failure installs nothing and is counted
126    /// in [`status`](Self::status).
127    pub fn reload(&self) -> Result<(), dynamic_config::Error> {
128        self.config.reload()
129    }
130
131    /// This section's sources, for the diagnostics that re-read them.
132    ///
133    /// Cloned rather than borrowed because `check` and `explain` run on the
134    /// blocking pool, which needs to own what it reads.
135    #[must_use]
136    pub fn sources(&self) -> Builder<Document> {
137        self.config.builder().clone()
138    }
139}
140
141/// Keys and shape, never the document: a section holds resolved
142/// configuration and this type is the obvious thing to `{:?}` in a handler.
143impl fmt::Debug for Section {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        f.debug_struct("Section")
146            .field("application", &self.application)
147            .field("profile", &self.profile)
148            .field("generation", &self.generation())
149            .finish_non_exhaustive()
150    }
151}
152
153/// Everything the HTTP layer serves from.
154///
155/// Built by [`Server::start`], shared behind an `Arc` by the router, and
156/// immutable afterwards: sections are established at startup and the set
157/// does not change while the process runs. Adding one is a restart, which is
158/// the same answer the rest of this workspace gives to "where do the sources
159/// live".
160pub struct Server {
161    sections: BTreeMap<(String, String), Arc<Section>>,
162    authenticator: Authenticator,
163    audit: Arc<dyn AuditSink>,
164    address: SocketAddr,
165    /// The loaded certificate, key and client verifier, when this server
166    /// terminates TLS itself. Loaded at startup so that a key that cannot be
167    /// read is a refusal rather than the first connection's problem.
168    #[cfg(feature = "tls")]
169    tls: Option<crate::tls::Tls>,
170    /// The ceiling on open change streams, and how many are open. Zero as a
171    /// ceiling means the endpoint is not served at all.
172    max_streams: usize,
173    streams: Arc<AtomicUsize>,
174}
175
176/// One open change stream's place in the ceiling.
177///
178/// Held by the stream and released when it is dropped — which is when the
179/// connection ends, however it ends: a client that goes away, a proxy that
180/// times out, or a process that shuts down all drop the response body, and
181/// the body owns this.
182#[derive(Debug)]
183pub struct StreamPermit {
184    open: Arc<AtomicUsize>,
185}
186
187impl Drop for StreamPermit {
188    fn drop(&mut self) {
189        self.open.fetch_sub(1, Ordering::Release);
190    }
191}
192
193impl Server {
194    /// Loads every section and refuses to start if anything is wrong.
195    ///
196    /// The order matters: [`ServerConfig::validate`] runs first and nothing
197    /// is opened if it refuses, so a server that would have been
198    /// world-readable never gets as far as reading a secret off disk.
199    ///
200    /// A section that will not load at startup is fatal. A config server
201    /// that comes up serving nothing for one application is a silent outage
202    /// for whoever needed it — better to fail the deployment.
203    ///
204    /// # Errors
205    ///
206    /// A [`Refusal`], a section that will not load, or a watch that will not
207    /// start.
208    pub fn start(config: &ServerConfig) -> Result<Self, StartupError> {
209        Self::start_with(config, StderrAudit)
210    }
211
212    /// [`start`](Self::start), with somewhere else for the audit log to go.
213    ///
214    /// # Errors
215    ///
216    /// As [`start`](Self::start).
217    pub fn start_with(config: &ServerConfig, audit: impl AuditSink) -> Result<Self, StartupError> {
218        config.validate()?;
219
220        let address = config.address()?;
221        // Before any section is opened: a private key that cannot be read,
222        // or that anybody on the host can read, is the same class of problem
223        // as a roster that would serve `billing` to everyone, and it is
224        // answered in the same place — at startup, with nothing yet open.
225        #[cfg(feature = "tls")]
226        let tls = config
227            .tls
228            .as_ref()
229            .map(crate::tls::Tls::load)
230            .transpose()
231            .map_err(StartupError::Tls)?;
232        let debounce = Duration::from_millis(config.watch_debounce_ms);
233        let mut sections = BTreeMap::new();
234
235        for described in &config.sections {
236            let section = load_section(described, debounce)?;
237
238            sections.insert(
239                (described.application.clone(), described.profile.clone()),
240                Arc::new(section),
241            );
242        }
243
244        let mut clients: Vec<(Token, Principal)> = Vec::new();
245        let mut anonymous = None;
246
247        for client in &config.clients {
248            let principal = Principal::new(&client.name, client.applications.clone());
249
250            match &client.token {
251                Some(token) => clients.push((token.clone(), principal)),
252                None => anonymous = Some(principal),
253            }
254        }
255
256        Ok(Self {
257            sections,
258            authenticator: Authenticator::new(clients, anonymous),
259            audit: Arc::new(audit),
260            address,
261            #[cfg(feature = "tls")]
262            tls,
263            max_streams: config.max_stream_connections,
264            streams: Arc::new(AtomicUsize::new(0)),
265        })
266    }
267
268    /// The address the binary listens on.
269    #[must_use]
270    pub fn address(&self) -> SocketAddr {
271        self.address
272    }
273
274    /// The loaded TLS configuration, or `None` for a server that expects a
275    /// terminator in front of it.
276    ///
277    /// What [`serve_tls`](crate::serve_tls) needs, and what tells an
278    /// embedder which of the two serving paths to take.
279    #[cfg(feature = "tls")]
280    #[cfg_attr(docsrs, doc(cfg(feature = "tls")))]
281    #[must_use]
282    pub fn tls(&self) -> Option<&crate::tls::Tls> {
283        self.tls.as_ref()
284    }
285
286    /// Where this server's audit lines go, for the parts of the serving path
287    /// that are outside a request — a TLS handshake that was refused has no
288    /// request to be recorded against, and is still something an operator
289    /// looks for in the audit trail.
290    #[cfg(feature = "tls")]
291    pub(crate) fn audit_sink(&self) -> Arc<dyn AuditSink> {
292        Arc::clone(&self.audit)
293    }
294
295    /// Who is calling, given the raw `Authorization` header.
296    #[must_use]
297    pub fn authenticate(&self, authorization: Option<&str>) -> Option<Principal> {
298        self.authenticator.authenticate(authorization)
299    }
300
301    /// The section serving `application` at `profile`, if one does.
302    ///
303    /// **Call this only after authorising.** It is the lookup that would
304    /// otherwise tell a caller whether a section exists, and the whole
305    /// not-an-oracle property rests on nothing reaching it that has not
306    /// already been granted the application.
307    #[must_use]
308    pub fn section(&self, application: &str, profile: &str) -> Option<&Arc<Section>> {
309        // Owned keys because `BTreeMap<(String, String), _>` cannot be
310        // probed with a pair of `&str` without a `Borrow` impl that does not
311        // exist. The map is small — one entry per served pair — and this is
312        // not the read path; the document itself comes from an atomic load.
313        self.sections
314            .get(&(application.to_owned(), profile.to_owned()))
315    }
316
317    /// Every served section.
318    pub fn sections(&self) -> impl Iterator<Item = &Arc<Section>> {
319        self.sections.values()
320    }
321
322    /// Whether every section is ready. What `/readyz` answers.
323    #[must_use]
324    pub fn is_ready(&self) -> bool {
325        self.sections().all(|section| section.is_ready())
326    }
327
328    /// Records one request.
329    pub fn record(&self, entry: &AuditEntry) {
330        self.audit.record(entry);
331    }
332
333    /// Whether this server serves change streams at all.
334    #[must_use]
335    pub fn streams_enabled(&self) -> bool {
336        self.max_streams > 0
337    }
338
339    /// A place in the change-stream ceiling, if there is one free.
340    ///
341    /// A compare-and-swap loop rather than a fetch-add and a refund: an
342    /// increment that overshoots is briefly visible to another request,
343    /// which would let two callers each see the ceiling exceeded and both
344    /// back off.
345    #[must_use]
346    pub fn open_stream(&self) -> Option<StreamPermit> {
347        let mut open = self.streams.load(Ordering::Acquire);
348
349        loop {
350            if open >= self.max_streams {
351                return None;
352            }
353
354            match self.streams.compare_exchange_weak(
355                open,
356                open + 1,
357                Ordering::AcqRel,
358                Ordering::Acquire,
359            ) {
360                Ok(_) => {
361                    return Some(StreamPermit {
362                        open: Arc::clone(&self.streams),
363                    })
364                }
365                Err(current) => open = current,
366            }
367        }
368    }
369
370    /// How many change streams are open. For the tests and for a deployment
371    /// that wants the number in its own metrics.
372    #[must_use]
373    pub fn open_streams(&self) -> usize {
374        self.streams.load(Ordering::Acquire)
375    }
376}
377
378impl fmt::Debug for Server {
379    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
380        f.debug_struct("Server")
381            .field("address", &self.address)
382            .field("sections", &self.sections.len())
383            .field("anonymous", &self.authenticator.allows_anonymous())
384            // The posture, never the material: `Tls`'s own `Debug` is
385            // hand-written for the same reason this one is.
386            .field("tls", &self.posture())
387            .finish_non_exhaustive()
388    }
389}
390
391impl Server {
392    /// How this server's own socket is protected, in a few words — for a
393    /// startup line, a `Debug` and an operator's first question.
394    ///
395    /// One of `none`, `tls` or `tls, client certificate required`. Never a
396    /// path, never a subject, never a key: it says which of the three shapes
397    /// this process is in and nothing about the material it is in it with.
398    #[cfg(feature = "tls")]
399    #[must_use]
400    pub fn posture(&self) -> &'static str {
401        match self.tls.as_ref().map(crate::tls::Tls::is_mutual) {
402            Some(true) => "tls, client certificate required",
403            Some(false) => "tls",
404            None => "none",
405        }
406    }
407
408    /// The same, for a build with no TLS in it: there is one answer.
409    #[cfg(not(feature = "tls"))]
410    #[must_use]
411    pub fn posture(&self) -> &'static str {
412        "none"
413    }
414}
415
416fn load_section(described: &SectionConfig, debounce: Duration) -> Result<Section, StartupError> {
417    let mut builder = Builder::<Document>::new(described.application.as_str());
418
419    if described.whole_document {
420        builder = builder.whole_document();
421    }
422
423    for file in &described.files {
424        builder = builder.file(file.as_str());
425    }
426    if let Some(prefix) = &described.env_prefix {
427        builder = builder.env(prefix.as_str());
428    }
429
430    let config = Dynamic::new(builder);
431
432    config.init().map_err(|source| StartupError::Section {
433        application: described.application.clone(),
434        profile: described.profile.clone(),
435        source,
436    })?;
437
438    // Zero disables watching, for a deployment that reloads by restarting or
439    // by a means of its own. Anything else is the library's watcher, which
440    // is event-driven — this server has no polling loop of its own and does
441    // not want one.
442    let watch = if debounce.is_zero() {
443        None
444    } else {
445        Some(
446            config
447                .watch(debounce)
448                .map_err(|source| StartupError::Watch {
449                    application: described.application.clone(),
450                    profile: described.profile.clone(),
451                    source,
452                })?,
453        )
454    };
455
456    Ok(Section {
457        application: described.application.clone(),
458        profile: described.profile.clone(),
459        config,
460        _watch: watch,
461    })
462}
463
464/// Why a server did not start.
465#[derive(Debug)]
466#[non_exhaustive]
467pub enum StartupError {
468    /// The configuration was refused before anything was opened.
469    Refused(Refusal),
470    /// A section's sources would not load.
471    Section {
472        /// The application.
473        application: String,
474        /// The profile.
475        profile: String,
476        /// What the load reported. Value-free, like every error in the
477        /// library.
478        source: dynamic_config::Error,
479    },
480    /// A section's watch would not start.
481    Watch {
482        /// The application.
483        application: String,
484        /// The profile.
485        profile: String,
486        /// What the watcher reported.
487        source: std::io::Error,
488    },
489    /// The TLS material would not load. Carries no key material: see
490    /// [`TlsError`](crate::tls::TlsError).
491    #[cfg(feature = "tls")]
492    #[cfg_attr(docsrs, doc(cfg(feature = "tls")))]
493    Tls(crate::tls::TlsError),
494}
495
496impl From<Refusal> for StartupError {
497    fn from(refusal: Refusal) -> Self {
498        Self::Refused(refusal)
499    }
500}
501
502impl fmt::Display for StartupError {
503    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
504        match self {
505            Self::Refused(refusal) => write!(f, "refusing to start: {refusal}"),
506            Self::Section {
507                application,
508                profile,
509                source,
510            } => write!(
511                f,
512                "refusing to start: the section `{application}`/`{profile}` will not load: \
513                 {source}"
514            ),
515            Self::Watch {
516                application,
517                profile,
518                source,
519            } => write!(
520                f,
521                "refusing to start: the section `{application}`/`{profile}` cannot be \
522                 watched: {source}"
523            ),
524            #[cfg(feature = "tls")]
525            Self::Tls(error) => write!(f, "refusing to start: {error}"),
526        }
527    }
528}
529
530impl std::error::Error for StartupError {
531    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
532        match self {
533            Self::Refused(refusal) => Some(refusal),
534            Self::Section { source, .. } => Some(source),
535            Self::Watch { source, .. } => Some(source),
536            #[cfg(feature = "tls")]
537            Self::Tls(error) => Some(error),
538        }
539    }
540}