1use std::sync::Arc;
4
5use async_trait::async_trait;
6use http::StatusCode;
7use saluki_api::{
8 extract::State,
9 response::IntoResponse,
10 routing::{get, Router},
11 APIHandler, DynamicRoute, EndpointType,
12};
13use saluki_common::sync::shutdown::ShutdownHandle;
14use saluki_core::{
15 diagnostic::DiagnosticsEmitter,
16 runtime::{state::DataspaceRegistry, InitializationError, Supervisable, SupervisorFuture},
17 support::SubsystemIdentifier,
18};
19use saluki_error::generic_error;
20use serde_json::Value;
21
22pub type ConfigSnapshotFn = Arc<dyn Fn() -> Value + Send + Sync>;
24
25#[derive(Clone)]
27pub struct ConfigState {
28 snapshot: ConfigSnapshotFn,
29}
30
31pub struct ConfigAPIHandler {
37 state: ConfigState,
38}
39
40impl ConfigAPIHandler {
41 fn new(snapshot: ConfigSnapshotFn) -> Self {
42 Self {
43 state: ConfigState { snapshot },
44 }
45 }
46
47 async fn config_handler(State(state): State<ConfigState>) -> impl IntoResponse {
48 let config = (state.snapshot)();
49 (StatusCode::OK, serde_json::to_string(&config).unwrap()).into_response()
50 }
51}
52
53impl APIHandler for ConfigAPIHandler {
54 type State = ConfigState;
55
56 fn generate_initial_state(&self) -> Self::State {
57 self.state.clone()
58 }
59
60 fn generate_routes(&self) -> Router<Self::State> {
61 Router::new().route("/config", get(Self::config_handler))
62 }
63}
64
65pub struct ConfigWorker {
71 handler: ConfigAPIHandler,
72}
73
74impl ConfigWorker {
75 pub fn new(snapshot: ConfigSnapshotFn) -> Self {
77 Self {
78 handler: ConfigAPIHandler::new(snapshot),
79 }
80 }
81}
82
83#[async_trait]
84impl Supervisable for ConfigWorker {
85 fn name(&self) -> &str {
86 "config-api"
87 }
88
89 async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
90 let config_route = DynamicRoute::http(EndpointType::Privileged, &self.handler);
91
92 let snapshot = self.handler.state.snapshot.clone();
93
94 Ok(Box::pin(async move {
95 let dataspace =
96 DataspaceRegistry::try_current().ok_or_else(|| generic_error!("Dataspace not available."))?;
97
98 dataspace.assert(config_route, "config-api");
99
100 let diagnostics =
101 DiagnosticsEmitter::from_dataspace(SubsystemIdentifier::from_segments(["config-api"]), dataspace);
102 diagnostics.register_collector("runtime_config_dump.yaml", move || {
103 serde_json::to_vec_pretty(&snapshot()).unwrap_or_default()
104 });
105
106 process_shutdown.await;
107 Ok(())
108 }))
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use std::sync::atomic::{AtomicUsize, Ordering};
115
116 use http_body_util::BodyExt as _;
117 use serde_json::json;
118
119 use super::*;
120
121 async fn response_parts(handler: &ConfigAPIHandler) -> (StatusCode, String) {
122 let response = ConfigAPIHandler::config_handler(State(handler.state.clone()))
123 .await
124 .into_response();
125 let status = response.status();
126 let body = response.into_body().collect().await.expect("body collects").to_bytes();
127
128 (status, String::from_utf8(body.to_vec()).expect("body is UTF-8"))
129 }
130
131 #[tokio::test]
132 async fn config_endpoint_serves_a_fresh_snapshot_per_request() {
133 let calls = Arc::new(AtomicUsize::new(0));
134 let snapshot_calls = Arc::clone(&calls);
135 let handler = ConfigAPIHandler::new(Arc::new(
136 move || json!({ "revision": snapshot_calls.fetch_add(1, Ordering::Relaxed) }),
137 ));
138
139 let (status, body) = response_parts(&handler).await;
140 assert_eq!(status, StatusCode::OK);
141 assert_eq!(body, r#"{"revision":0}"#);
142
143 let (status, body) = response_parts(&handler).await;
144 assert_eq!(status, StatusCode::OK);
145 assert_eq!(body, r#"{"revision":1}"#);
146 assert_eq!(calls.load(Ordering::Relaxed), 2);
147 }
148}