Skip to main content

reifydb_sub_server_http/
subsystem.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright (c) 2026 ReifyDB
3
4use std::{
5	any::Any,
6	io,
7	net::SocketAddr,
8	sync::{
9		Arc,
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, sync::rwlock::RwLock};
20use reifydb_sub_api::subsystem::{HealthStatus, Subsystem};
21use reifydb_sub_server::state::AppState;
22use reifydb_value::Result;
23use tokio::{net::TcpListener, sync::oneshot};
24use tracing::{error, info, warn};
25
26use crate::{routes::router, state::HttpServerState};
27
28pub struct HttpSubsystem {
29	bind_addr: Option<String>,
30
31	admin_bind_addr: Option<String>,
32
33	actual_addr: RwLock<Option<SocketAddr>>,
34
35	admin_actual_addr: RwLock<Option<SocketAddr>>,
36
37	state: AppState,
38
39	running: Arc<AtomicBool>,
40
41	shutdown_tx: Option<oneshot::Sender<()>>,
42
43	shutdown_complete_rx: Option<oneshot::Receiver<()>>,
44
45	admin_shutdown_tx: Option<oneshot::Sender<()>>,
46
47	admin_shutdown_complete_rx: Option<oneshot::Receiver<()>>,
48
49	runtime: SharedRuntime,
50}
51
52impl HttpSubsystem {
53	pub fn new(
54		bind_addr: Option<String>,
55		admin_bind_addr: Option<String>,
56		state: AppState,
57		runtime: SharedRuntime,
58	) -> Self {
59		Self {
60			bind_addr,
61			admin_bind_addr,
62			actual_addr: RwLock::new(None),
63			admin_actual_addr: RwLock::new(None),
64			state,
65			running: Arc::new(AtomicBool::new(false)),
66			shutdown_tx: None,
67			shutdown_complete_rx: None,
68			admin_shutdown_tx: None,
69			admin_shutdown_complete_rx: None,
70			runtime,
71		}
72	}
73
74	pub fn bind_addr(&self) -> Option<&str> {
75		self.bind_addr.as_deref()
76	}
77
78	pub fn local_addr(&self) -> Option<SocketAddr> {
79		*self.actual_addr.read()
80	}
81
82	pub fn port(&self) -> Option<u16> {
83		self.local_addr().map(|a| a.port())
84	}
85
86	pub fn admin_local_addr(&self) -> Option<SocketAddr> {
87		*self.admin_actual_addr.read()
88	}
89
90	pub fn admin_port(&self) -> Option<u16> {
91		self.admin_local_addr().map(|a| a.port())
92	}
93
94	fn spawn_main_server(&mut self) -> Result<()> {
95		let Some(addr) = self.bind_addr.clone() else {
96			self.running.store(true, Ordering::SeqCst);
97			return Ok(());
98		};
99		let listener = self.bind_listener(&addr)?;
100		let actual_addr = local_addr_or_err(&listener)?;
101		*self.actual_addr.write() = Some(actual_addr);
102		info!("HTTP server bound to {}", actual_addr);
103
104		let (shutdown_tx, shutdown_rx) = oneshot::channel();
105		let (complete_tx, complete_rx) = oneshot::channel();
106		let server_state = HttpServerState::new(self.state.clone());
107		let running = self.running.clone();
108		self.runtime.spawn(async move {
109			running.store(true, Ordering::SeqCst);
110			let result = serve_http(listener, server_state, shutdown_rx, "HTTP server").await;
111			if let Err(e) = result {
112				error!("HTTP server error: {}", e);
113			}
114			running.store(false, Ordering::SeqCst);
115			let _ = complete_tx.send(());
116			info!("HTTP server stopped");
117		});
118		self.shutdown_tx = Some(shutdown_tx);
119		self.shutdown_complete_rx = Some(complete_rx);
120		Ok(())
121	}
122
123	fn spawn_admin_server(&mut self) -> Result<()> {
124		let Some(admin_addr) = self.admin_bind_addr.clone() else {
125			return Ok(());
126		};
127		let listener = self.bind_listener(&admin_addr)?;
128		let actual_addr = local_addr_or_err(&listener)?;
129		*self.admin_actual_addr.write() = Some(actual_addr);
130		info!("HTTP admin server bound to {}", actual_addr);
131
132		let (admin_shutdown_tx, admin_shutdown_rx) = oneshot::channel();
133		let (admin_complete_tx, admin_complete_rx) = oneshot::channel();
134		let admin_config = self.state.config().clone().admin_enabled(true);
135		let admin_app_state = self.state.clone_with_config(admin_config);
136		let admin_server_state = HttpServerState::new(admin_app_state);
137		self.runtime.spawn(async move {
138			let result =
139				serve_http(listener, admin_server_state, admin_shutdown_rx, "HTTP admin server").await;
140			if let Err(e) = result {
141				error!("HTTP admin server error: {}", e);
142			}
143			let _ = admin_complete_tx.send(());
144			info!("HTTP admin server stopped");
145		});
146		self.admin_shutdown_tx = Some(admin_shutdown_tx);
147		self.admin_shutdown_complete_rx = Some(admin_complete_rx);
148		Ok(())
149	}
150
151	#[inline]
152	fn bind_listener(&self, addr: &str) -> Result<TcpListener> {
153		self.runtime.block_on(TcpListener::bind(addr)).map_err(|e| {
154			CoreError::SubsystemBindFailed {
155				addr: addr.to_string(),
156				reason: e.to_string(),
157			}
158			.into()
159		})
160	}
161}
162
163impl HasVersion for HttpSubsystem {
164	fn version(&self) -> SystemVersion {
165		SystemVersion {
166			name: env!("CARGO_PKG_NAME")
167				.strip_prefix("reifydb-")
168				.unwrap_or(env!("CARGO_PKG_NAME"))
169				.to_string(),
170			version: env!("CARGO_PKG_VERSION").to_string(),
171			description: "HTTP server subsystem for query and command handling".to_string(),
172			r#type: ComponentType::Subsystem,
173		}
174	}
175}
176
177impl Subsystem for HttpSubsystem {
178	fn name(&self) -> &'static str {
179		"Http"
180	}
181
182	fn start(&mut self) -> Result<()> {
183		if self.running.load(Ordering::SeqCst) {
184			return Ok(());
185		}
186		self.spawn_main_server()?;
187		self.spawn_admin_server()?;
188		Ok(())
189	}
190
191	fn shutdown(&mut self) -> Result<()> {
192		if let Some(tx) = self.admin_shutdown_tx.take() {
193			let _ = tx.send(());
194		}
195		if let Some(rx) = self.admin_shutdown_complete_rx.take() {
196			let _ = self.runtime.block_on(rx);
197		}
198
199		if let Some(tx) = self.shutdown_tx.take() {
200			let _ = tx.send(());
201		}
202		if let Some(rx) = self.shutdown_complete_rx.take() {
203			let _ = self.runtime.block_on(rx);
204		}
205		self.running.store(false, Ordering::SeqCst);
206		Ok(())
207	}
208
209	fn is_running(&self) -> bool {
210		self.running.load(Ordering::SeqCst)
211	}
212
213	fn health_status(&self) -> HealthStatus {
214		if self.running.load(Ordering::SeqCst) {
215			HealthStatus::Healthy
216		} else if self.shutdown_tx.is_some() {
217			HealthStatus::Warning {
218				description: "Starting up".to_string(),
219			}
220		} else {
221			HealthStatus::Failed {
222				description: "Not running".to_string(),
223			}
224		}
225	}
226
227	fn as_any(&self) -> &dyn Any {
228		self
229	}
230
231	fn as_any_mut(&mut self) -> &mut dyn Any {
232		self
233	}
234}
235
236#[inline]
237fn local_addr_or_err(listener: &TcpListener) -> Result<SocketAddr> {
238	listener.local_addr().map_err(|e| {
239		CoreError::SubsystemAddressUnavailable {
240			reason: e.to_string(),
241		}
242		.into()
243	})
244}
245
246async fn serve_http(
247	listener: TcpListener,
248	server_state: HttpServerState,
249	shutdown_rx: oneshot::Receiver<()>,
250	name: &'static str,
251) -> io::Result<()> {
252	let app = router(server_state);
253	let listener = listener.tap_io(|tcp_stream| {
254		if let Err(e) = tcp_stream.set_nodelay(true) {
255			warn!("Failed to set TCP_NODELAY: {e}");
256		}
257	});
258	serve(listener, app)
259		.with_graceful_shutdown(async move {
260			shutdown_rx.await.ok();
261			info!("{} received shutdown signal", name);
262		})
263		.await
264}