Skip to main content

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}