featherbit/state.rs
1//! Shared, lock-protected gateway state used by the data plane, the Admin
2//! API, and the hot-reload watcher. Owns the current gateway config and the
3//! routes compiled from it, and provides the recompile/swap operations.
4
5use std::sync::Arc;
6use tokio::sync::RwLock;
7
8use crate::config::{GatewayConfig, RouteConfig, SystemConfig};
9use crate::config_store::ConfigStore;
10use crate::debug::DebugState;
11use crate::graph::{compile_policy, validate_policy, CompiledGraph};
12use crate::metrics::GatewayMetrics;
13use crate::plugins::resources::PluginResources;
14
15/// Shared gateway state, accessible from both the data-plane server and the Admin API.
16///
17/// Wrapped in an [`Arc`] and cloned into every server task. The data plane
18/// only ever takes short **read** locks on `routes` while matching a request;
19/// **write** locks are taken by the Admin API and hot-reload paths when
20/// swapping in a freshly compiled route table. Route recompilation happens on
21/// [`SharedState::reload`] / [`SharedState::reload_from_disk`] — never on the
22/// request path.
23pub struct SharedState {
24 /// Immutable system-level configuration (`system.yaml`); fixed for the process lifetime.
25 #[allow(dead_code)] // callers take `&SystemConfig` directly; kept on state for reference
26 pub system: SystemConfig,
27 /// Current gateway configuration (`gateway.yaml`), mutated by the Admin API CRUD endpoints.
28 pub gateway: RwLock<GatewayConfig>,
29 /// Route table: each route paired with the compiled graph of the policy it references.
30 /// Kept in declaration order; the first matching route wins.
31 pub routes: RwLock<Vec<(RouteConfig, Arc<CompiledGraph>)>>,
32 /// Path to `gateway.yaml`, if known; required for [`SharedState::reload_from_disk`].
33 pub config_path: Option<std::path::PathBuf>,
34 /// Process-wide Prometheus registry. Compiled graphs record per-node
35 /// metrics into it, the data plane records per-request metrics, and the
36 /// Admin API's `/metrics` endpoint renders it.
37 pub metrics: Arc<GatewayMetrics>,
38 /// Shared plugin services (metrics handle, shared clients), threaded into
39 /// every plugin at policy compile time.
40 pub resources: Arc<PluginResources>,
41 /// Backend the config is loaded from and Admin API mutations are persisted
42 /// to (file by default; etcd for HA clusters).
43 pub config_store: Arc<dyn ConfigStore>,
44 /// Debug-mode settings and the bounded trace buffer. Written by the data
45 /// plane when a request opts into tracing, read by the Admin API. Fixed at
46 /// startup — `system.yaml` is not hot-reloaded.
47 pub debug: Arc<DebugState>,
48}
49
50impl SharedState {
51 /// Creates the shared state, validating and compiling every policy up front.
52 ///
53 /// Fails if any policy is invalid or a route references an unknown policy,
54 /// so a successfully constructed `SharedState` always has a usable route table.
55 pub fn new(
56 system: SystemConfig,
57 gateway: GatewayConfig,
58 config_path: Option<std::path::PathBuf>,
59 config_store: Arc<dyn ConfigStore>,
60 ) -> Result<Self, String> {
61 let metrics = Arc::new(GatewayMetrics::new());
62 let resources = PluginResources::new(Some(metrics.clone()));
63 resources
64 .consumers
65 .store(Arc::new(crate::consumers::ConsumerStore::from_config(
66 &gateway.consumers,
67 )?));
68 let routes = Self::compile_routes(&gateway, &resources)?;
69 let debug_state = Arc::new(DebugState::new(&system.debug));
70 if debug_state.enabled {
71 let bodies = if debug_state.capture_bodies {
72 "captured"
73 } else {
74 "excluded"
75 };
76 tracing::warn!(
77 "debug mode is ENABLED: policy traces capture request headers and \
78 context state into memory (bodies: {}). Do not enable in production.",
79 bodies
80 );
81 if debug_state.trace_all {
82 let header = debug_state.trigger_header.clone();
83 tracing::warn!(
84 "debug.trace_all is on: EVERY request is traced, not just those \
85 carrying '{}'. This snapshots the context once per node for all traffic.",
86 header
87 );
88 }
89 }
90 Ok(Self {
91 system,
92 gateway: RwLock::new(gateway),
93 routes: RwLock::new(routes),
94 config_path,
95 metrics,
96 resources,
97 config_store,
98 debug: debug_state,
99 })
100 }
101
102 /// Validates and compiles `new_gw`, then atomically swaps the consumer
103 /// store, route table, and in-memory gateway config.
104 ///
105 /// This is the single swap path used by every config driver (file watcher,
106 /// etcd watch, Admin API commits). All fallible work — consumer-store build
107 /// and policy compilation — happens **before** any swap, so a failure
108 /// leaves the running config untouched (the last-good guarantee).
109 pub async fn apply_gateway(&self, new_gw: GatewayConfig) -> Result<(), String> {
110 let consumers = crate::consumers::ConsumerStore::from_config(&new_gw.consumers)?;
111 let routes = Self::compile_routes(&new_gw, &self.resources)?;
112 tracing::info!(
113 "Applied config: {} routes from {} policies",
114 routes.len(),
115 new_gw.policies.len()
116 );
117 self.resources.consumers.store(Arc::new(consumers));
118 let mut gw = self.gateway.write().await;
119 *gw = new_gw;
120 let mut r = self.routes.write().await;
121 *r = routes;
122 Ok(())
123 }
124
125 /// Validates and compiles `gw` **without** swapping anything.
126 ///
127 /// Config stores call this to reject a candidate config before persisting
128 /// it, so the Admin API can return an error synchronously even when the
129 /// change will be applied asynchronously by a watch.
130 pub fn validate_gateway(&self, gw: &GatewayConfig) -> Result<(), String> {
131 crate::consumers::ConsumerStore::from_config(&gw.consumers)?;
132 Self::compile_routes(gw, &self.resources)?;
133 Ok(())
134 }
135
136 /// Reloads from disk (re-reads `gateway.yaml` with env interpolation),
137 /// recompiles, and swaps in the new config.
138 ///
139 /// Invoked by the hot-reload file watcher. Fails without side effects if
140 /// `config_path` is unset, the file cannot be parsed, or compilation fails.
141 pub async fn reload_from_disk(&self) -> Result<(), String> {
142 let path = self
143 .config_path
144 .as_ref()
145 .ok_or("No config path set for hot-reload")?;
146 let new_gw: GatewayConfig =
147 crate::config::load_yaml_with_env(path).map_err(|e| e.to_string())?;
148 self.apply_gateway(new_gw).await
149 }
150
151 /// Validates and compiles every policy, then binds each route to its
152 /// compiled graph. Policies shared by multiple routes are compiled once
153 /// and shared via `Arc`.
154 fn compile_routes(
155 gateway: &GatewayConfig,
156 resources: &Arc<PluginResources>,
157 ) -> Result<Vec<(RouteConfig, Arc<CompiledGraph>)>, String> {
158 let mut policy_map = std::collections::HashMap::new();
159 for policy in &gateway.policies {
160 if let Err(errors) = validate_policy(policy) {
161 return Err(format!("Invalid policy '{}': {:?}", policy.name, errors));
162 }
163 let compiled = compile_policy(policy, resources.clone())?;
164 policy_map.insert(policy.name.clone(), Arc::new(compiled));
165 }
166
167 let mut routes = Vec::new();
168 for route in &gateway.routes {
169 let graph = policy_map
170 .get(&route.policy)
171 .ok_or(format!(
172 "Route '{}' references unknown policy '{}'",
173 route.name, route.policy
174 ))?
175 .clone();
176 routes.push((route.clone(), graph));
177 }
178 Ok(routes)
179 }
180}