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}