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;