Skip to main content

reifydb_sub_server_http/
subsystem.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4use std::{
5	any::Any,
6	io,
7	net::SocketAddr,
8	sync::{
9		Arc, RwLock,
10		atomic::{AtomicBool, Ordering},
11	},
12};
13
14use axum::{serve, serve::ListenerExt};
15use reifydb_core::{
16	error::CoreError,
17	interface::version::{ComponentType, HasVersion, SystemVersion},
18};
19use reifydb_runtime::SharedRuntime;
20use reifydb_sub_api::subsystem::{HealthStatus, Subsystem};
21use reifydb_sub_server::state::AppState;
22use reifydb_type::Result;
23use tokio::{net::TcpListener, sync::oneshot};
24use tracing::{error, info, warn};
25
26use crate::{routes::router, state::HttpServerState};
27
28/// HTTP server subsystem.
29///
30/// Manages an Axum-based HTTP server with support for:
31/// - Graceful startup and shutdown
32/// - Health monitoring
33///
34/// # Example
35///
36/// ```ignore
37/// let state = AppState::new(pool, engine, QueryConfig::default(), RequestInterceptorChain::empty());
38///
39/// let mut http = HttpSubsystem::new(
40///     "0.0.0.0:8090".to_string(),
41///     state,
42/// );
43///
44/// http.start()?;
45/// // Server is now accepting connections
46///
47/// http.shutdown()?;
48/// // Server has gracefully stopped
49/// ```
50pub struct HttpSubsystem {
51	/// Address to bind the server to.
52	bind_addr: Option<String>,
53	/// Address to bind the admin server to.
54	admin_bind_addr: Option<String>,
55	/// Actual bound address (available after start).
56	actual_addr: RwLock<Option<SocketAddr>>,
57	/// Actual bound address for admin server (available after start).
58	admin_actual_addr: RwLock<Option<SocketAddr>>,
59	/// Shared application state.
60	state: AppState,
61	/// Flag indicating if the server is running.
62	running: Arc<AtomicBool>,
63	/// Channel to send shutdown signal.
64	shutdown_tx: Option<oneshot::Sender<()>>,
65	/// Channel to receive shutdown completion.
66	shutdown_complete_rx: Option<oneshot::Receiver<()>>,
67	/// Channel to send admin shutdown signal.
68	admin_shutdown_tx: Option<oneshot::Sender<()>>,
69	/// Channel to receive admin shutdown completion.
70	admin_shutdown_complete_rx: Option<oneshot::Receiver<()>>,
71	/// Shared tokio runtime.
72	runtime: SharedRuntime,
73}
74
75impl HttpSubsystem {
76	/// Create a new HTTP subsystem.
77	///
78	/// # Arguments
79	///
80	/// * `bind_addr` - Address and port to bind to (e.g., "0.0.0.0:8090")
81	/// * `state` - Shared application state with engine and config
82	/// * `runtime` - Shared runtime
83	pub fn new(
84		bind_addr: Option<String>,
85		admin_bind_addr: Option<String>,
86		state: AppState,
87		runtime: SharedRuntime,
88	) -> Self {
89		Self {
90			bind_addr,
91			admin_bind_addr,
92			actual_addr: RwLock::new(None),
93			admin_actual_addr: RwLock::new(None),
94			state,
95			running: Arc::new(AtomicBool::new(false)),
96			shutdown_tx: None,
97			shutdown_complete_rx: None,
98			admin_shutdown_tx: None,
99			admin_shutdown_complete_rx: None,
100			runtime,
101		}
102	}
103
104	/// Get the bind address.
105	pub fn bind_addr(&self) -> Option<&str> {
106		self.bind_addr.as_deref()
107	}
108
109	/// Get the actual bound address (available after start).
110	pub fn local_addr(&self) -> Option<SocketAddr> {
111		*self.actual_addr.read().unwrap()
112	}
113
114	/// Get the actual bound port (available after start).
115	pub fn port(&self) -> Option<u16> {
116		self.local_addr().map(|a| a.port())
117	}
118
119	/// Get the actual bound address for the admin server (available after start).
120	pub fn admin_local_addr(&self) -> Option<SocketAddr> {
121		*self.admin_actual_addr.read().unwrap()
122	}
123
124	/// Get the actual bound port for the admin server (available after start).
125	pub fn admin_port(&self) -> Option<u16> {
126		self.admin_local_addr().map(|a| a.port())
127	}
128
129	fn spawn_main_server(&mut self) -> Result<()> {
130		let Some(addr) = self.bind_addr.clone() else {
131			self.running.store(true, Ordering::SeqCst);
132			return Ok(());
133		};
134		let listener = self.bind_listener(&addr)?;
135		let actual_addr = local_addr_or_err(&listener)?;
136		*self.actual_addr.write().unwrap() = Some(actual_addr);
137		info!("HTTP server bound to {}", actual_addr);
138
139		let (shutdown_tx, shutdown_rx) = oneshot::channel();
140		let (complete_tx, complete_rx) = oneshot::channel();
141		let server_state = HttpServerState::new(self.state.clone());
142		let running = self.running.clone();
143		self.runtime.spawn(async move {
144			running.store(true, Ordering::SeqCst);
145			let result = serve_http(listener, server_state, shutdown_rx, "HTTP server").await;
146			if let Err(e) = result {
147				error!("HTTP server error: {}", e);
148			}
149			running.store(false, Ordering::SeqCst);
150			let _ = complete_tx.send(());
151			info!("HTTP server stopped");
152		});
153		self.shutdown_tx = Some(shutdown_tx);
154		self.shutdown_complete_rx = Some(complete_rx);
155		Ok(())
156	}
157
158	fn spawn_admin_server(&mut self) -> Result<()> {
159		let Some(admin_addr) = self.admin_bind_addr.clone() else {
160			return Ok(());
161		};
162		let listener = self.bind_listener(&admin_addr)?;
163		let actual_addr = local_addr_or_err(&listener)?;
164		*self.admin_actual_addr.write().unwrap() = Some(actual_addr);
165		info!("HTTP admin server bound to {}", actual_addr);
166
167		let (admin_shutdown_tx, admin_shutdown_rx) = oneshot::channel();
168		let (admin_complete_tx, admin_complete_rx) = oneshot::channel();
169		let admin_config = self.state.config().clone().admin_enabled(true);
170		let admin_app_state = self.state.clone_with_config(admin_config);
171		let admin_server_state = HttpServerState::new(admin_app_state);
172		self.runtime.spawn(async move {
173			let result =
174				serve_http(listener, admin_server_state, admin_shutdown_rx, "HTTP admin server").await;
175			if let Err(e) = result {
176				error!("HTTP admin server error: {}", e);
177			}
178			let _ = admin_complete_tx.send(());
179			info!("HTTP admin server stopped");
180		});
181		self.admin_shutdown_tx = Some(admin_shutdown_tx);
182		self.admin_shutdown_complete_rx = Some(admin_complete_rx);
183		Ok(())
184	}
185
186	#[inline]
187	fn bind_listener(&self, addr: &str) -> Result<TcpListener> {
188		self.runtime.block_on(TcpListener::bind(addr)).map_err(|e| {
189			CoreError::SubsystemBindFailed {
190				addr: addr.to_string(),
191				reason: e.to_string(),
192			}
193			.into()
194		})
195	}
196}
197
198impl HasVersion for HttpSubsystem {
199	fn version(&self) -> SystemVersion {
200		SystemVersion {
201			name: env!("CARGO_PKG_NAME")
202				.strip_prefix("reifydb-")
203				.unwrap_or(env!("CARGO_PKG_NAME"))
204				.to_string(),
205			version: env!("CARGO_PKG_VERSION").to_string(),
206			description: "HTTP server subsystem for query and command handling".to_string(),
207			r#type: ComponentType::Subsystem,
208		}
209	}
210}
211
212impl Subsystem for HttpSubsystem {
213	fn name(&self) -> &'static str {
214		"Http"
215	}
216
217	fn start(&mut self) -> Result<()> {
218		if self.running.load(Ordering::SeqCst) {
219			return Ok(());
220		}
221		self.spawn_main_server()?;
222		self.spawn_admin_server()?;
223		Ok(())
224	}
225
226	fn shutdown(&mut self) -> Result<()> {
227		// Shutdown admin server first
228		if let Some(tx) = self.admin_shutdown_tx.take() {
229			let _ = tx.send(());
230		}
231		if let Some(rx) = self.admin_shutdown_complete_rx.take() {
232			let _ = self.runtime.block_on(rx);
233		}
234		// Then shutdown main server
235		if let Some(tx) = self.shutdown_tx.take() {
236			let _ = tx.send(());
237		}
238		if let Some(rx) = self.shutdown_complete_rx.take() {
239			let _ = self.runtime.block_on(rx);
240		}
241		self.running.store(false, Ordering::SeqCst);
242		Ok(())
243	}
244
245	fn is_running(&self) -> bool {
246		self.running.load(Ordering::SeqCst)
247	}
248
249	fn health_status(&self) -> HealthStatus {
250		if self.running.load(Ordering::SeqCst) {
251			HealthStatus::Healthy
252		} else if self.shutdown_tx.is_some() {
253			// Started but not yet running (startup in progress)
254			HealthStatus::Warning {
255				description: "Starting up".to_string(),
256			}
257		} else {
258			HealthStatus::Failed {
259				description: "Not running".to_string(),
260			}
261		}
262	}
263
264	fn as_any(&self) -> &dyn Any {
265		self
266	}
267
268	fn as_any_mut(&mut self) -> &mut dyn Any {
269		self
270	}
271}
272
273#[inline]
274fn local_addr_or_err(listener: &TcpListener) -> Result<SocketAddr> {
275	listener.local_addr().map_err(|e| {
276		CoreError::SubsystemAddressUnavailable {
277			reason: e.to_string(),
278		}
279		.into()
280	})
281}
282
283async fn serve_http(
284	listener: TcpListener,
285	server_state: HttpServerState,
286	shutdown_rx: oneshot::Receiver<()>,
287	name: &'static str,
288) -> io::Result<()> {
289	let app = router(server_state);
290	let listener = listener.tap_io(|tcp_stream| {
291		if let Err(e) = tcp_stream.set_nodelay(true) {
292			warn!("Failed to set TCP_NODELAY: {e}");
293		}
294	});
295	serve(listener, app)
296		.with_graceful_shutdown(async move {
297			shutdown_rx.await.ok();
298			info!("{} received shutdown signal", name);
299		})
300		.await
301}