Skip to main content

osproxy_observe/
store.rs

1//! The fleet-wide diagnostics-directive store seam (`docs/05` §3-4).
2//!
3//! The signed `X-Debug-Directive` header is *surgical*, one request, one
4//! instance. The store is its *fleet-wide* counterpart: a controller publishes a
5//! [`DirectiveSet`] and every proxy instance reads it, so an operator can raise
6//! verbosity across the fleet (a tenant, an endpoint, a sampled slice) without a
7//! restart. Like the migration control plane (`osproxy-control`), the proxy ships
8//! the **seam plus an in-process reference**, not a distributed store: a real
9//! etcd/Consul/OpenSearch-index backend implements the same trait unchanged.
10//!
11//! Reads are **fresh per request** and on the hot path, so [`DirectiveStore::load`]
12//! is a cheap `Arc` clone of the current snapshot, a distributed backend keeps a
13//! watched local copy and returns it here rather than doing I/O per call. TTL
14//! safety is intrinsic: directives carry an absolute expiry, so even a published
15//! set that is never replaced self-expires at evaluation time.
16
17use std::sync::Arc;
18
19use arc_swap::ArcSwap;
20
21use crate::directive::DirectiveSet;
22
23/// The backend holding the fleet's active diagnostics directives. Proxy instances
24/// poll it fresh per request; a controller publishes new sets into it.
25pub trait DirectiveStore: Send + Sync {
26    /// The currently active directive set. Called on the request hot path, so it
27    /// must be cheap (an `Arc` clone of a cached snapshot), never blocking I/O.
28    fn load(&self) -> Arc<DirectiveSet>;
29}
30
31/// A fixed set: the directives never change for this process. The default store,
32/// and the wrapper for a statically configured [`DirectiveSet`].
33impl DirectiveStore for Arc<DirectiveSet> {
34    fn load(&self) -> Arc<DirectiveSet> {
35        Arc::clone(self)
36    }
37}
38
39/// The in-process reference store: a controller (or an admin endpoint) `publish`es
40/// a new set and proxy threads `load` it. Swappable for a distributed
41/// `DirectiveStore` without touching the pipeline (`docs/05` §3).
42#[derive(Debug, Default)]
43pub struct InMemoryDirectiveStore {
44    current: ArcSwap<DirectiveSet>,
45}
46
47impl InMemoryDirectiveStore {
48    /// An empty store, every request evaluates to `Off` until a set is published.
49    #[must_use]
50    pub fn new() -> Self {
51        Self {
52            current: ArcSwap::from_pointee(DirectiveSet::new()),
53        }
54    }
55
56    /// Seeds the store with an initial directive set (builder style).
57    #[must_use]
58    pub fn with_directives(self, set: DirectiveSet) -> Self {
59        self.publish(set);
60        self
61    }
62
63    /// Replaces the active set, the fleet-wide "flip" an operator performs. The
64    /// next `load` on every thread sees it (no restart). A lock-free atomic store.
65    pub fn publish(&self, set: DirectiveSet) {
66        self.current.store(Arc::new(set));
67    }
68}
69
70impl DirectiveStore for InMemoryDirectiveStore {
71    fn load(&self) -> Arc<DirectiveSet> {
72        // Lock-free: a relaxed atomic load of the current snapshot pointer, so the
73        // per-request read scales across cores instead of serializing on a mutex.
74        self.current.load_full()
75    }
76}
77
78#[cfg(test)]
79#[path = "store_tests.rs"]
80mod tests;