1use 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 #[cfg(feature = "inbound")]
57 if let Some(ref db_pool) = self.db_pool {
58 if !self.config.webhooks.is_empty() {
59 crate::inbound::WebhookInboundState::init_spine(db_pool).await.map_err(|e| {
60 ServerError::ConfigError(format!(
61 "Failed to initialize inbound spine schema: {e}"
62 ))
63 })?;
64 fraiseql_webhooks::PostgresIdempotencyStore::new(db_pool.clone())
65 .init()
66 .await
67 .map_err(|e| {
68 ServerError::ConfigError(format!(
69 "Failed to initialize webhook idempotency schema: {e}"
70 ))
71 })?;
72 info!(
73 routes = self.config.webhooks.len(),
74 "Inbound ingestion schema ready (spine + idempotency ledger)"
75 );
76 }
77 }
78
79 if let Some(ref usage_cfg) = self.config.usage.clone() {
83 use std::time::Duration;
84
85 use sqlx::postgres::PgPoolOptions;
86 use tokio::time::MissedTickBehavior;
87
88 use crate::usage::aggregator::{PostgresBackend, global_aggregator};
89
90 match PgPoolOptions::new()
91 .max_connections(2) .connect(&self.config.database_url)
93 .await
94 {
95 Ok(pool) => {
96 match PostgresBackend::new(pool).await {
97 Ok(backend) => {
98 let backend = std::sync::Arc::new(backend);
99 global_aggregator().set_backend(backend.clone());
101 if let Err(e) = global_aggregator().load_from_backend().await {
103 warn!(error = %e, "Usage persistence: startup load failed — continuing with in-memory counters");
104 } else {
105 info!("Usage persistence: loaded counters from PostgreSQL");
106 }
107 let flush_interval = Duration::from_secs(usage_cfg.flush_interval_secs);
110 let agg = std::sync::Arc::clone(global_aggregator());
111 self.tasks.spawn(async move {
112 let mut ticker = tokio::time::interval(flush_interval);
113 ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
114 ticker.tick().await; loop {
116 ticker.tick().await;
117 if let Err(e) = agg.flush_to_backend().await {
118 warn!(error = %e, "Usage persistence: background flush failed");
119 }
120 }
121 });
122 info!(
123 flush_interval_secs = usage_cfg.flush_interval_secs,
124 "Usage persistence: PostgreSQL backend active"
125 );
126 },
127 Err(e) => {
128 warn!(
129 error = %e,
130 "Usage persistence: PostgresBackend initialization failed — \
131 continuing with in-memory (NoopBackend)"
132 );
133 },
134 }
135 },
136 Err(e) => {
137 warn!(
138 error = %e,
139 "Usage persistence: failed to connect to PostgreSQL — \
140 continuing with in-memory (NoopBackend)"
141 );
142 },
143 }
144 }
145
146 #[cfg(feature = "functions-runtime")]
152 self.prepare_functions_runtime().await?;
153
154 let (app, app_state) = self.build_router();
155
156 #[cfg(feature = "inbound-email")]
164 if let Some(ref db_pool) = self.db_pool {
165 if self.config.mailbox.values().any(|mailbox| mailbox.imap.is_some()) {
166 use crate::inbound::{email, spine::PostgresInboundSpine};
167
168 PostgresInboundSpine::new(db_pool.clone()).init().await.map_err(|e| {
172 ServerError::ConfigError(format!(
173 "Failed to initialize inbound spine schema: {e}"
174 ))
175 })?;
176 email::init_cursor_store(db_pool).await.map_err(|e| {
177 ServerError::ConfigError(format!(
178 "Failed to initialize inbound email cursor schema: {e}"
179 ))
180 })?;
181
182 let tracker = std::sync::Arc::new(email::PgSendTracker::new(db_pool.clone()));
186 tracker.init().await.map_err(|e| {
187 ServerError::ConfigError(format!(
188 "Failed to initialize send-tracking schema: {e}"
189 ))
190 })?;
191 let correlator =
192 std::sync::Arc::clone(&tracker) as std::sync::Arc<dyn email::SendCorrelator>;
193 let address_hash_key = self.build_address_hash_key();
194
195 let sink = self.storage_backend.as_ref().map(|backend| {
196 std::sync::Arc::new(email::LegacyStorageSink::new(backend.clone()))
197 as std::sync::Arc<
198 dyn fraiseql_functions::host::live::storage::StorageBackend,
199 >
200 });
201 if self.config.send.verp_probe_on_start {
205 email::run_startup_probes(&self.config.mailbox, |name| {
206 std::env::var(name).ok()
207 })
208 .await;
209 }
210
211 let hooks = app_state.before_mutation_hooks.clone();
212 let workers = email::build_workers(
213 &self.config.mailbox,
214 db_pool,
215 hooks.as_ref(),
216 sink.as_ref(),
217 Some(&correlator),
218 address_hash_key.as_ref(),
219 self.config.send.challenge_suppress_after,
220 |name| std::env::var(name).ok(),
221 );
222 let started = workers.len();
223 for (worker, interval) in workers {
224 self.tasks.spawn(async move { worker.poll_forever(interval).await });
225 }
226 info!(mailboxes = started, "poll-IMAP email workers started");
227 }
228 }
229
230 #[cfg(unix)]
235 if let Some(ref schema_path) = app_state.schema_path {
236 let reload_state = app_state.clone();
237 let reload_path = schema_path.clone();
238 self.tasks.spawn(async move {
239 let mut sigusr1 = match tokio::signal::unix::signal(
240 tokio::signal::unix::SignalKind::user_defined1(),
241 ) {
242 Ok(s) => s,
243 Err(e) => {
244 warn!(error = %e, "Failed to install SIGUSR1 handler — schema hot-reload disabled");
245 return;
246 },
247 };
248 loop {
249 sigusr1.recv().await;
250 info!(
251 path = %reload_path.display(),
252 "Received SIGUSR1 — reloading schema"
253 );
254 match reload_state.reload_schema(&reload_path).await {
255 Ok(()) => {
256 let hash = reload_state.executor().schema().content_hash();
257 reload_state
258 .metrics
259 .schema_reloads_total
260 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
261 info!(schema_hash = %hash, "Schema reloaded successfully via SIGUSR1");
262 },
263 Err(e) => {
264 reload_state
265 .metrics
266 .schema_reload_errors_total
267 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
268 error!(
269 error = %e,
270 path = %reload_path.display(),
271 "Schema reload failed via SIGUSR1 — keeping previous schema"
272 );
273 },
274 }
275 }
276 });
277 info!(
278 path = %schema_path.display(),
279 "SIGUSR1 schema reload handler installed"
280 );
281 }
282
283 let tls_setup = TlsSetup::new(self.config.tls.clone(), self.config.database_tls.clone());
285
286 if tls_setup.is_tls_enabled() {
292 return Err(ServerError::ConfigError(
293 "[tls] (server-side TLS termination) is enabled but not supported: FraiseQL \
294 serves plaintext HTTP and expects TLS to be terminated by a reverse proxy, \
295 load balancer, or service mesh. Remove the [tls] section (or set its \
296 `enabled = false`) and terminate TLS in front of the server. Database \
297 connection TLS ([database_tls]) is unaffected."
298 .to_string(),
299 ));
300 }
301
302 info!(
303 bind_addr = %self.config.bind_addr,
304 graphql_path = %self.config.graphql_path,
305 tls_enabled = tls_setup.is_tls_enabled(),
306 "Starting FraiseQL server"
307 );
308
309 #[cfg(feature = "observers")]
311 #[allow(unused_variables)]
312 let _bridge_handle = {
314 let mut handle: Option<tokio::task::JoinHandle<()>> = None;
315 if let Some(ref runtime) = self.observer_runtime {
316 info!("Starting observer runtime...");
317
318 let bridge =
320 EventBridge::new(self.subscription_manager.clone(), EventBridgeConfig::new());
321 let sender = bridge.sender();
322
323 let mut guard = runtime.write().await;
324 guard.set_event_bridge_sender(sender);
325
326 match guard.start().await {
327 Ok(()) => {
328 info!("Observer runtime started");
329 handle = Some(bridge.spawn());
331 info!(
332 "EventBridge started — CDC events will be forwarded to subscriptions"
333 );
334 },
335 Err(e) => {
336 if guard.transport_requires_broker()
343 && crate::ServerConfig::is_production_mode()
344 {
345 error!(
346 error = %e,
347 "Observer runtime failed to start on its configured \
348 transport; refusing to boot (set FRAISEQL_ENV=development \
349 to downgrade to a warning)"
350 );
351 return Err(e);
352 }
353 error!("Failed to start observer runtime: {}", e);
354 warn!("Server will continue without observers");
355 },
356 }
357 drop(guard);
358 }
359 handle
360 };
361
362 let listener = TcpListener::bind(self.config.bind_addr)
365 .await
366 .map_err(|e| ServerError::BindError(e.to_string()))?
367 .tap_io(|tcp_stream| {
368 if let Err(err) = tcp_stream.set_nodelay(true) {
369 warn!("failed to set TCP_NODELAY: {err:#}");
370 }
371 });
372
373 #[cfg(target_os = "linux")]
376 {
377 if let Ok(limits) = std::fs::read_to_string("/proc/self/limits") {
378 for line in limits.lines() {
379 if line.starts_with("Max open files") {
380 let parts: Vec<&str> = line.split_whitespace().collect();
381 if let Some(soft) = parts.get(3) {
382 if let Ok(n) = soft.parse::<u64>() {
383 if n < 65_536 {
384 warn!(
385 current_fd_limit = n,
386 recommended = 65_536,
387 "File descriptor limit is low; consider raising ulimit -n"
388 );
389 }
390 }
391 }
392 break;
393 }
394 }
395 }
396 }
397
398 info!(
400 postgres_ssl_mode = tls_setup.postgres_ssl_mode(),
401 redis_ssl = tls_setup.redis_ssl_enabled(),
402 clickhouse_https = tls_setup.clickhouse_https_enabled(),
403 elasticsearch_https = tls_setup.elasticsearch_https_enabled(),
404 "Database connection TLS configuration applied"
405 );
406
407 info!("Server listening on http://{}", self.config.bind_addr);
408
409 #[cfg(feature = "arrow")]
411 if let Some(flight_service) = self.flight_service.take() {
412 let flight_addr = self.config.flight_bind_addr;
413 info!("Arrow Flight server listening on grpc://{}", flight_addr);
414
415 self.tasks.spawn(async move {
419 if let Err(e) = tonic::transport::Server::builder()
420 .add_service(flight_service.into_server())
421 .serve(flight_addr)
422 .await
423 {
424 error!(error = %e, "Arrow Flight server terminated with error");
425 }
426 });
427
428 #[cfg(feature = "observers")]
430 let observer_runtime = self.observer_runtime.clone();
431
432 let shutdown_with_cleanup = async move {
433 shutdown.await;
434 #[cfg(feature = "observers")]
435 if let Some(ref runtime) = observer_runtime {
436 info!("Shutting down observer runtime");
437 let mut guard = runtime.write().await;
438 if let Err(e) = guard.stop().await {
439 #[cfg(feature = "observers")]
440 error!("Error stopping runtime: {}", e);
441 } else {
442 info!("Runtime stopped cleanly");
443 }
444 }
445 };
446
447 axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>())
449 .with_graceful_shutdown(shutdown_with_cleanup)
450 .await
451 .map_err(|e| ServerError::IoError(std::io::Error::other(e)))?;
452
453 drain_lifecycle_tasks(self.tasks, self.config.shutdown_timeout_secs).await;
456 }
457
458 #[cfg(not(feature = "arrow"))]
460 {
461 axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>())
462 .with_graceful_shutdown(shutdown)
463 .await
464 .map_err(|e| ServerError::IoError(std::io::Error::other(e)))?;
465
466 let shutdown_timeout =
467 std::time::Duration::from_secs(self.config.shutdown_timeout_secs);
468 info!(
469 timeout_secs = self.config.shutdown_timeout_secs,
470 "HTTP server stopped, draining remaining work"
471 );
472
473 let drain = tokio::time::timeout(shutdown_timeout, async {
474 #[cfg(feature = "observers")]
475 if let Some(ref runtime) = self.observer_runtime {
476 let mut guard = runtime.write().await;
477 match guard.stop().await {
478 Ok(()) => info!("Observer runtime stopped cleanly"),
479 Err(e) => warn!("Observer runtime shutdown error: {e}"),
480 }
481 }
482 })
483 .await;
484
485 if drain.is_err() {
486 warn!(
487 timeout_secs = self.config.shutdown_timeout_secs,
488 "Shutdown drain timed out; forcing exit"
489 );
490 } else {
491 info!("Graceful shutdown complete");
492 }
493
494 drain_lifecycle_tasks(self.tasks, self.config.shutdown_timeout_secs).await;
497 }
498
499 Ok(())
500 }
501
502 pub async fn serve_on_listener<F>(self, listener: TcpListener, shutdown: F) -> Result<()>
511 where
512 F: std::future::Future<Output = ()> + Send + 'static,
513 {
514 let (app, _app_state) = self.build_router();
515 axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>())
516 .with_graceful_shutdown(shutdown)
517 .await
518 .map_err(|e| ServerError::IoError(std::io::Error::other(e)))?;
519 drain_lifecycle_tasks(self.tasks, self.config.shutdown_timeout_secs).await;
523 Ok(())
524 }
525
526 pub async fn shutdown_signal() {
528 use tokio::signal;
529
530 let ctrl_c = async {
531 match signal::ctrl_c().await {
532 Ok(()) => {},
533 Err(e) => {
534 warn!(error = %e, "Failed to install Ctrl+C handler");
535 std::future::pending::<()>().await;
536 },
537 }
538 };
539
540 #[cfg(unix)]
541 let terminate = async {
542 match signal::unix::signal(signal::unix::SignalKind::terminate()) {
543 Ok(mut s) => {
544 s.recv().await;
545 },
546 Err(e) => {
547 warn!(error = %e, "Failed to install SIGTERM handler");
548 std::future::pending::<()>().await;
549 },
550 }
551 };
552
553 #[cfg(not(unix))]
554 let terminate = std::future::pending::<()>();
555
556 tokio::select! {
557 () = ctrl_c => info!("Received Ctrl+C"),
558 () = terminate => info!("Received SIGTERM"),
559 }
560 }
561}
562
563pub(super) async fn drain_lifecycle_tasks(
571 mut tasks: tokio::task::JoinSet<()>,
572 shutdown_timeout_secs: u64,
573) {
574 if tasks.is_empty() {
575 return;
576 }
577
578 tasks.abort_all();
579 let timeout = std::time::Duration::from_secs(shutdown_timeout_secs);
580 let drained = tokio::time::timeout(timeout, async {
581 while let Some(res) = tasks.join_next().await {
582 if let Err(e) = res {
583 if !e.is_cancelled() {
584 warn!(error = %e, "Lifecycle task terminated with a non-cancellation error");
585 }
586 }
587 }
588 })
589 .await;
590 if drained.is_err() {
591 warn!(
592 timeout_secs = shutdown_timeout_secs,
593 "Lifecycle task drain timed out; some background tasks did not stop in time"
594 );
595 } else {
596 info!("All lifecycle background tasks drained");
597 }
598}