fraiseql_server/server/
lifecycle.rs1use std::net::SocketAddr;
4
5use axum::serve::ListenerExt;
6use tokio::net::TcpListener;
7use tracing::{error, info, warn};
8
9use super::{DatabaseAdapter, Result, Server, ServerError, TlsSetup};
10#[cfg(feature = "observers")]
11use crate::subscriptions::event_bridge::{EventBridge, EventBridgeConfig};
12
13impl<A: DatabaseAdapter + Clone + Send + Sync + 'static> Server<A> {
14 pub async fn serve(self) -> Result<()> {
23 self.serve_with_shutdown(Self::shutdown_signal()).await
24 }
25
26 #[allow(clippy::cognitive_complexity)] pub async fn serve_with_shutdown<F>(mut self, shutdown: F) -> Result<()>
36 where
37 F: std::future::Future<Output = ()> + Send + 'static,
38 {
39 #[cfg(feature = "observers")]
42 if let Some(ref db_pool) = self.db_pool {
43 if self.config.admin_token.is_some() {
44 let rbac_backend =
45 crate::api::rbac_management::db_backend::RbacDbBackend::new(db_pool.clone());
46 rbac_backend.ensure_schema().await.map_err(|e| {
47 ServerError::ConfigError(format!("Failed to initialize RBAC schema: {e}"))
48 })?;
49 }
50 }
51
52 if let Some(ref usage_cfg) = self.config.usage.clone() {
56 use std::time::Duration;
57
58 use sqlx::postgres::PgPoolOptions;
59 use tokio::time::MissedTickBehavior;
60
61 use crate::usage::aggregator::{PostgresBackend, global_aggregator};
62
63 match PgPoolOptions::new()
64 .max_connections(2) .connect(&self.config.database_url)
66 .await
67 {
68 Ok(pool) => {
69 match PostgresBackend::new(pool).await {
70 Ok(backend) => {
71 let backend = std::sync::Arc::new(backend);
72 global_aggregator().set_backend(backend.clone());
74 if let Err(e) = global_aggregator().load_from_backend().await {
76 warn!(error = %e, "Usage persistence: startup load failed — continuing with in-memory counters");
77 } else {
78 info!("Usage persistence: loaded counters from PostgreSQL");
79 }
80 let flush_interval = Duration::from_secs(usage_cfg.flush_interval_secs);
83 let agg = std::sync::Arc::clone(global_aggregator());
84 self.tasks.spawn(async move {
85 let mut ticker = tokio::time::interval(flush_interval);
86 ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
87 ticker.tick().await; loop {
89 ticker.tick().await;
90 if let Err(e) = agg.flush_to_backend().await {
91 warn!(error = %e, "Usage persistence: background flush failed");
92 }
93 }
94 });
95 info!(
96 flush_interval_secs = usage_cfg.flush_interval_secs,
97 "Usage persistence: PostgreSQL backend active"
98 );
99 },
100 Err(e) => {
101 warn!(
102 error = %e,
103 "Usage persistence: PostgresBackend initialization failed — \
104 continuing with in-memory (NoopBackend)"
105 );
106 },
107 }
108 },
109 Err(e) => {
110 warn!(
111 error = %e,
112 "Usage persistence: failed to connect to PostgreSQL — \
113 continuing with in-memory (NoopBackend)"
114 );
115 },
116 }
117 }
118
119 let (app, app_state) = self.build_router();
120
121 #[cfg(unix)]
126 if let Some(ref schema_path) = app_state.schema_path {
127 let reload_state = app_state.clone();
128 let reload_path = schema_path.clone();
129 self.tasks.spawn(async move {
130 let mut sigusr1 = match tokio::signal::unix::signal(
131 tokio::signal::unix::SignalKind::user_defined1(),
132 ) {
133 Ok(s) => s,
134 Err(e) => {
135 warn!(error = %e, "Failed to install SIGUSR1 handler — schema hot-reload disabled");
136 return;
137 },
138 };
139 loop {
140 sigusr1.recv().await;
141 info!(
142 path = %reload_path.display(),
143 "Received SIGUSR1 — reloading schema"
144 );
145 match reload_state.reload_schema(&reload_path).await {
146 Ok(()) => {
147 let hash = reload_state.executor().schema().content_hash();
148 reload_state
149 .metrics
150 .schema_reloads_total
151 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
152 info!(schema_hash = %hash, "Schema reloaded successfully via SIGUSR1");
153 },
154 Err(e) => {
155 reload_state
156 .metrics
157 .schema_reload_errors_total
158 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
159 error!(
160 error = %e,
161 path = %reload_path.display(),
162 "Schema reload failed via SIGUSR1 — keeping previous schema"
163 );
164 },
165 }
166 }
167 });
168 info!(
169 path = %schema_path.display(),
170 "SIGUSR1 schema reload handler installed"
171 );
172 }
173
174 let tls_setup = TlsSetup::new(self.config.tls.clone(), self.config.database_tls.clone());
176
177 if tls_setup.is_tls_enabled() {
183 return Err(ServerError::ConfigError(
184 "[tls] (server-side TLS termination) is enabled but not supported: FraiseQL \
185 serves plaintext HTTP and expects TLS to be terminated by a reverse proxy, \
186 load balancer, or service mesh. Remove the [tls] section (or set its \
187 `enabled = false`) and terminate TLS in front of the server. Database \
188 connection TLS ([database_tls]) is unaffected."
189 .to_string(),
190 ));
191 }
192
193 info!(
194 bind_addr = %self.config.bind_addr,
195 graphql_path = %self.config.graphql_path,
196 tls_enabled = tls_setup.is_tls_enabled(),
197 "Starting FraiseQL server"
198 );
199
200 #[cfg(feature = "observers")]
202 #[allow(unused_variables)]
203 let _bridge_handle = {
205 let mut handle: Option<tokio::task::JoinHandle<()>> = None;
206 if let Some(ref runtime) = self.observer_runtime {
207 info!("Starting observer runtime...");
208
209 let bridge =
211 EventBridge::new(self.subscription_manager.clone(), EventBridgeConfig::new());
212 let sender = bridge.sender();
213
214 let mut guard = runtime.write().await;
215 guard.set_event_bridge_sender(sender);
216
217 match guard.start().await {
218 Ok(()) => {
219 info!("Observer runtime started");
220 handle = Some(bridge.spawn());
222 info!(
223 "EventBridge started — CDC events will be forwarded to subscriptions"
224 );
225 },
226 Err(e) => {
227 if guard.transport_requires_broker()
234 && crate::ServerConfig::is_production_mode()
235 {
236 error!(
237 error = %e,
238 "Observer runtime failed to start on its configured \
239 transport; refusing to boot (set FRAISEQL_ENV=development \
240 to downgrade to a warning)"
241 );
242 return Err(e);
243 }
244 error!("Failed to start observer runtime: {}", e);
245 warn!("Server will continue without observers");
246 },
247 }
248 drop(guard);
249 }
250 handle
251 };
252
253 let listener = TcpListener::bind(self.config.bind_addr)
256 .await
257 .map_err(|e| ServerError::BindError(e.to_string()))?
258 .tap_io(|tcp_stream| {
259 if let Err(err) = tcp_stream.set_nodelay(true) {
260 warn!("failed to set TCP_NODELAY: {err:#}");
261 }
262 });
263
264 #[cfg(target_os = "linux")]
267 {
268 if let Ok(limits) = std::fs::read_to_string("/proc/self/limits") {
269 for line in limits.lines() {
270 if line.starts_with("Max open files") {
271 let parts: Vec<&str> = line.split_whitespace().collect();
272 if let Some(soft) = parts.get(3) {
273 if let Ok(n) = soft.parse::<u64>() {
274 if n < 65_536 {
275 warn!(
276 current_fd_limit = n,
277 recommended = 65_536,
278 "File descriptor limit is low; consider raising ulimit -n"
279 );
280 }
281 }
282 }
283 break;
284 }
285 }
286 }
287 }
288
289 info!(
291 postgres_ssl_mode = tls_setup.postgres_ssl_mode(),
292 redis_ssl = tls_setup.redis_ssl_enabled(),
293 clickhouse_https = tls_setup.clickhouse_https_enabled(),
294 elasticsearch_https = tls_setup.elasticsearch_https_enabled(),
295 "Database connection TLS configuration applied"
296 );
297
298 info!("Server listening on http://{}", self.config.bind_addr);
299
300 #[cfg(feature = "arrow")]
302 if let Some(flight_service) = self.flight_service.take() {
303 let flight_addr = self.config.flight_bind_addr;
304 info!("Arrow Flight server listening on grpc://{}", flight_addr);
305
306 self.tasks.spawn(async move {
310 if let Err(e) = tonic::transport::Server::builder()
311 .add_service(flight_service.into_server())
312 .serve(flight_addr)
313 .await
314 {
315 error!(error = %e, "Arrow Flight server terminated with error");
316 }
317 });
318
319 #[cfg(feature = "observers")]
321 let observer_runtime = self.observer_runtime.clone();
322
323 let shutdown_with_cleanup = async move {
324 shutdown.await;
325 #[cfg(feature = "observers")]
326 if let Some(ref runtime) = observer_runtime {
327 info!("Shutting down observer runtime");
328 let mut guard = runtime.write().await;
329 if let Err(e) = guard.stop().await {
330 #[cfg(feature = "observers")]
331 error!("Error stopping runtime: {}", e);
332 } else {
333 info!("Runtime stopped cleanly");
334 }
335 }
336 };
337
338 axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>())
340 .with_graceful_shutdown(shutdown_with_cleanup)
341 .await
342 .map_err(|e| ServerError::IoError(std::io::Error::other(e)))?;
343
344 drain_lifecycle_tasks(self.tasks, self.config.shutdown_timeout_secs).await;
347 }
348
349 #[cfg(not(feature = "arrow"))]
351 {
352 axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>())
353 .with_graceful_shutdown(shutdown)
354 .await
355 .map_err(|e| ServerError::IoError(std::io::Error::other(e)))?;
356
357 let shutdown_timeout =
358 std::time::Duration::from_secs(self.config.shutdown_timeout_secs);
359 info!(
360 timeout_secs = self.config.shutdown_timeout_secs,
361 "HTTP server stopped, draining remaining work"
362 );
363
364 let drain = tokio::time::timeout(shutdown_timeout, async {
365 #[cfg(feature = "observers")]
366 if let Some(ref runtime) = self.observer_runtime {
367 let mut guard = runtime.write().await;
368 match guard.stop().await {
369 Ok(()) => info!("Observer runtime stopped cleanly"),
370 Err(e) => warn!("Observer runtime shutdown error: {e}"),
371 }
372 }
373 })
374 .await;
375
376 if drain.is_err() {
377 warn!(
378 timeout_secs = self.config.shutdown_timeout_secs,
379 "Shutdown drain timed out; forcing exit"
380 );
381 } else {
382 info!("Graceful shutdown complete");
383 }
384
385 drain_lifecycle_tasks(self.tasks, self.config.shutdown_timeout_secs).await;
388 }
389
390 Ok(())
391 }
392
393 pub async fn serve_on_listener<F>(self, listener: TcpListener, shutdown: F) -> Result<()>
402 where
403 F: std::future::Future<Output = ()> + Send + 'static,
404 {
405 let (app, _app_state) = self.build_router();
406 axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>())
407 .with_graceful_shutdown(shutdown)
408 .await
409 .map_err(|e| ServerError::IoError(std::io::Error::other(e)))?;
410 drain_lifecycle_tasks(self.tasks, self.config.shutdown_timeout_secs).await;
414 Ok(())
415 }
416
417 pub async fn shutdown_signal() {
419 use tokio::signal;
420
421 let ctrl_c = async {
422 match signal::ctrl_c().await {
423 Ok(()) => {},
424 Err(e) => {
425 warn!(error = %e, "Failed to install Ctrl+C handler");
426 std::future::pending::<()>().await;
427 },
428 }
429 };
430
431 #[cfg(unix)]
432 let terminate = async {
433 match signal::unix::signal(signal::unix::SignalKind::terminate()) {
434 Ok(mut s) => {
435 s.recv().await;
436 },
437 Err(e) => {
438 warn!(error = %e, "Failed to install SIGTERM handler");
439 std::future::pending::<()>().await;
440 },
441 }
442 };
443
444 #[cfg(not(unix))]
445 let terminate = std::future::pending::<()>();
446
447 tokio::select! {
448 () = ctrl_c => info!("Received Ctrl+C"),
449 () = terminate => info!("Received SIGTERM"),
450 }
451 }
452}
453
454pub(super) async fn drain_lifecycle_tasks(
462 mut tasks: tokio::task::JoinSet<()>,
463 shutdown_timeout_secs: u64,
464) {
465 if tasks.is_empty() {
466 return;
467 }
468
469 tasks.abort_all();
470 let timeout = std::time::Duration::from_secs(shutdown_timeout_secs);
471 let drained = tokio::time::timeout(timeout, async {
472 while let Some(res) = tasks.join_next().await {
473 if let Err(e) = res {
474 if !e.is_cancelled() {
475 warn!(error = %e, "Lifecycle task terminated with a non-cancellation error");
476 }
477 }
478 }
479 })
480 .await;
481 if drained.is_err() {
482 warn!(
483 timeout_secs = shutdown_timeout_secs,
484 "Lifecycle task drain timed out; some background tasks did not stop in time"
485 );
486 } else {
487 info!("All lifecycle background tasks drained");
488 }
489}