1use std::sync::atomic::Ordering;
2use std::sync::Arc;
3use std::time::{Duration, Instant};
4
5use axum::{
6 extract::{Path, Query, State},
7 http::{HeaderMap, HeaderValue, StatusCode},
8 response::{IntoResponse, Response},
9 routing::get,
10 Json, Router,
11};
12use serde::Deserialize;
13use serde_json::{json, Value};
14use tower_http::trace::TraceLayer;
15
16use crate::cache::PrefixState;
17use crate::upstream::UpstreamClient;
18
19pub struct AgentState {
20 pub upstream: UpstreamClient,
21 pub prefixes: Vec<Arc<PrefixState>>,
22}
23
24impl AgentState {
25 pub fn find_prefix(&self, key: &str) -> Option<&Arc<PrefixState>> {
26 self.prefixes
30 .iter()
31 .filter(|p| key.starts_with(&p.prefix))
32 .max_by_key(|p| p.prefix.len())
33 }
34
35 pub fn find_prefix_for_listing<'a>(&'a self, prefix: &str) -> Option<&'a Arc<PrefixState>> {
36 self.prefixes
39 .iter()
40 .filter(|p| prefix.starts_with(&p.prefix))
41 .max_by_key(|p| p.prefix.len())
42 }
43
44 pub fn all_ready(&self) -> bool {
45 self.prefixes
46 .iter()
47 .all(|p| p.ready.load(Ordering::Relaxed))
48 }
49}
50
51pub fn router(state: Arc<AgentState>) -> Router {
52 Router::new()
53 .route("/healthz", get(healthz))
54 .route("/readyz", get(readyz))
55 .route("/cfg", get(list_handler))
56 .route("/cfg/*key", get(cfg_handler))
57 .with_state(state)
58 .layer(TraceLayer::new_for_http())
59}
60
61async fn healthz() -> impl IntoResponse {
62 (StatusCode::OK, Json(json!({"status": "ok"})))
63}
64
65async fn readyz(State(state): State<Arc<AgentState>>) -> impl IntoResponse {
66 if state.all_ready() {
67 (StatusCode::OK, Json(json!({"status": "ok"})))
68 } else {
69 (
70 StatusCode::SERVICE_UNAVAILABLE,
71 Json(json!({"status": "loading"})),
72 )
73 }
74}
75
76#[derive(Deserialize)]
77struct CfgQuery {
78 wait: Option<String>,
82 #[serde(default)]
84 since: i64,
85}
86
87async fn cfg_handler(
88 State(state): State<Arc<AgentState>>,
89 Path(key): Path<String>,
90 Query(q): Query<CfgQuery>,
91) -> Response {
92 let Some(prefix) = state.find_prefix(&key) else {
93 return error_response(
94 StatusCode::NOT_FOUND,
95 "key_not_in_watched_prefix",
96 &format!("no watched prefix matches key {:?}", key),
97 );
98 };
99
100 let wait = q.wait.as_deref().and_then(parse_wait);
101
102 match wait {
103 None => immediate_response(prefix, &key),
104 Some(wait) => long_poll(prefix.clone(), key, q.since, wait).await,
105 }
106}
107
108fn immediate_response(prefix: &PrefixState, key: &str) -> Response {
109 match prefix.get(key) {
110 Some(entry) => value_response(&entry.value, entry.event_id),
111 None => error_response(StatusCode::NOT_FOUND, "not_found", "key not in cache"),
112 }
113}
114
115async fn long_poll(prefix: Arc<PrefixState>, key: String, since: i64, wait: Duration) -> Response {
116 let deadline = Instant::now() + wait;
117
118 loop {
119 let cur = prefix.get(&key);
120 let cur_id = cur.as_ref().map(|e| e.event_id).unwrap_or(0);
121 if cur_id > since {
122 return match cur {
124 Some(e) => value_response(&e.value, e.event_id),
125 None => {
126 error_response_with_index(
129 StatusCode::NOT_FOUND,
130 "not_found",
131 "key not in cache",
132 prefix.latest_event_id.load(Ordering::Relaxed),
133 )
134 }
135 };
136 }
137
138 let now = Instant::now();
139 if now >= deadline {
140 return match cur {
142 Some(e) => value_response(&e.value, e.event_id),
143 None => error_response_with_index(
144 StatusCode::NOT_FOUND,
145 "not_found",
146 "key not in cache",
147 prefix.latest_event_id.load(Ordering::Relaxed),
148 ),
149 };
150 }
151 let remaining = deadline - now;
152
153 let mut rx = prefix.notify.subscribe();
154 match tokio::time::timeout(remaining, rx.recv()).await {
155 Ok(_) => {
156 continue;
159 }
160 Err(_) => {
161 continue;
163 }
164 }
165 }
166}
167
168fn parse_wait(s: &str) -> Option<Duration> {
169 let d = humantime::parse_duration(s).ok()?;
170 Some(d.min(Duration::from_secs(5 * 60)))
171}
172
173#[derive(Deserialize)]
174struct ListQuery {
175 prefix: String,
176}
177
178async fn list_handler(
179 State(state): State<Arc<AgentState>>,
180 Query(q): Query<ListQuery>,
181) -> Response {
182 let Some(prefix_state) = state.find_prefix_for_listing(&q.prefix) else {
183 return error_response(
184 StatusCode::NOT_FOUND,
185 "prefix_not_watched",
186 &format!("requested prefix {:?} is not under any watched prefix", q.prefix),
187 );
188 };
189
190 let items: Vec<_> = prefix_state
191 .list()
192 .into_iter()
193 .filter(|(k, _)| k.starts_with(&q.prefix))
194 .map(|(k, e)| {
195 json!({
196 "key": k,
197 "value": e.value,
198 "event_id": e.event_id,
199 })
200 })
201 .collect();
202
203 let body = json!({
204 "prefix": q.prefix,
205 "latest_event_id": prefix_state.latest_event_id.load(Ordering::Relaxed),
206 "items": items,
207 });
208 let mut headers = HeaderMap::new();
209 headers.insert(
210 "X-Ferra-Index",
211 HeaderValue::from_str(
212 &prefix_state
213 .latest_event_id
214 .load(Ordering::Relaxed)
215 .to_string(),
216 )
217 .unwrap(),
218 );
219 (StatusCode::OK, headers, Json(body)).into_response()
220}
221
222fn value_response(value: &Value, event_id: i64) -> Response {
223 let mut headers = HeaderMap::new();
224 headers.insert(
225 "X-Ferra-Index",
226 HeaderValue::from_str(&event_id.to_string()).unwrap(),
227 );
228 (StatusCode::OK, headers, Json(value.clone())).into_response()
229}
230
231fn error_response(status: StatusCode, code: &str, message: &str) -> Response {
232 (
233 status,
234 Json(json!({
235 "error": code,
236 "message": message,
237 })),
238 )
239 .into_response()
240}
241
242fn error_response_with_index(
243 status: StatusCode,
244 code: &str,
245 message: &str,
246 event_id: i64,
247) -> Response {
248 let mut headers = HeaderMap::new();
249 headers.insert(
250 "X-Ferra-Index",
251 HeaderValue::from_str(&event_id.to_string()).unwrap(),
252 );
253 (
254 status,
255 headers,
256 Json(json!({
257 "error": code,
258 "message": message,
259 })),
260 )
261 .into_response()
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn parse_wait_caps_at_5min() {
270 assert_eq!(parse_wait("30s"), Some(Duration::from_secs(30)));
271 assert_eq!(parse_wait("10m"), Some(Duration::from_secs(5 * 60)));
272 assert_eq!(parse_wait("garbage"), None);
273 }
274
275 #[test]
276 fn find_prefix_picks_longest_match() {
277 let upstream = UpstreamClient::new("http://x".into());
278 let s = AgentState {
279 upstream,
280 prefixes: vec![
281 Arc::new(PrefixState::new("services/".into())),
282 Arc::new(PrefixState::new("services/payment/".into())),
283 ],
284 };
285 let p = s.find_prefix("services/payment/timeout_ms").unwrap();
286 assert_eq!(p.prefix, "services/payment/");
287 let p = s.find_prefix("services/auth/jwt").unwrap();
288 assert_eq!(p.prefix, "services/");
289 assert!(s.find_prefix("flags/x").is_none());
290 }
291}