use std::future::Future;
use std::net::SocketAddr;
use std::sync::Arc;
use arrow_flight::flight_service_server::FlightServiceServer;
use async_trait::async_trait;
use axum::routing::get;
use axum::Router;
use datafusion::execution::context::SessionContext;
use datafusion_flight_sql_server::service::FlightSqlService;
use jammi_ai::session::InferenceSession;
use jammi_db::config::JammiConfig;
use tokio::net::TcpListener;
use tokio::signal;
use tokio::sync::broadcast;
use tonic::transport::Server;
use tonic_web::GrpcWebLayer;
use crate::error::fallback_handler;
use crate::flight::TenantBoundProvider;
use crate::grpc::proto::session::session_service_server::SessionServiceServer;
use crate::grpc::proto::trigger::trigger_service_server::TriggerServiceServer;
use crate::grpc::session::{SessionServer, SessionStore, TenantInterceptor};
use crate::grpc::trigger::TriggerServer;
use crate::routes::health::{self, MetricsRegistry};
#[derive(Debug, thiserror::Error)]
pub enum ServerError {
#[error("config error: {0}")]
Config(String),
#[error("engine init: {0}")]
Engine(#[from] jammi_db::error::JammiError),
#[error("metrics registry: {0}")]
Metrics(#[from] prometheus::Error),
#[error("transport: {0}")]
Transport(#[from] tonic::transport::Error),
#[error("io: {0}")]
Io(#[from] std::io::Error),
#[error("addr parse: {0}")]
AddrParse(#[from] std::net::AddrParseError),
}
#[async_trait]
pub trait ReadinessCheck: Send + Sync {
async fn check(&self) -> Result<(), String>;
}
pub struct ReadinessProbe {
inner: Arc<dyn ReadinessCheck>,
}
impl ReadinessProbe {
pub fn new(inner: Arc<dyn ReadinessCheck>) -> Self {
Self { inner }
}
pub async fn check(&self) -> Result<(), String> {
self.inner.check().await
}
}
pub struct CatalogPingProbe {
session: Arc<InferenceSession>,
}
impl CatalogPingProbe {
pub fn new(session: Arc<InferenceSession>) -> Self {
Self { session }
}
}
#[async_trait]
impl ReadinessCheck for CatalogPingProbe {
async fn check(&self) -> Result<(), String> {
self.session
.catalog()
.ping()
.await
.map_err(|e| e.to_string())
}
}
pub struct OssServer {
flight_addr: SocketAddr,
health_addr: SocketAddr,
session: Arc<InferenceSession>,
session_store: SessionStore,
metrics: Arc<MetricsRegistry>,
readiness: Arc<ReadinessProbe>,
}
impl OssServer {
pub async fn new(config: JammiConfig) -> Result<Self, ServerError> {
config
.server
.validate()
.map_err(|e| ServerError::Config(e.to_string()))?;
let flight_addr: SocketAddr = config.server.flight_listen.parse()?;
let health_addr: SocketAddr = config.server.health_listen.parse()?;
let session = Arc::new(InferenceSession::new(config).await?);
let session_store = SessionStore::new();
let metrics = Arc::new(MetricsRegistry::new()?);
let readiness = Arc::new(ReadinessProbe::new(Arc::new(CatalogPingProbe::new(
Arc::clone(&session),
))));
Ok(Self {
flight_addr,
health_addr,
session,
session_store,
metrics,
readiness,
})
}
pub fn flight_addr(&self) -> SocketAddr {
self.flight_addr
}
pub fn health_addr(&self) -> SocketAddr {
self.health_addr
}
pub fn metrics(&self) -> Arc<MetricsRegistry> {
Arc::clone(&self.metrics)
}
pub fn session(&self) -> Arc<InferenceSession> {
Arc::clone(&self.session)
}
pub fn with_readiness(mut self, readiness: Arc<ReadinessProbe>) -> Self {
self.readiness = readiness;
self
}
pub async fn run(self) -> Result<(), ServerError> {
self.run_with_shutdown(shutdown_signal()).await
}
pub async fn run_with_shutdown(
self,
shutdown: impl Future<Output = ()> + Send + 'static,
) -> Result<(), ServerError> {
let (shutdown_tx, _) = broadcast::channel::<()>(1);
let mut shutdown_health_rx = shutdown_tx.subscribe();
let mut shutdown_grpc_rx = shutdown_tx.subscribe();
let shutdown_tx_for_signal = shutdown_tx.clone();
tokio::spawn(async move {
shutdown.await;
let _ = shutdown_tx_for_signal.send(());
});
let health_router = self.build_health_router();
let health_listener = TcpListener::bind(self.health_addr).await?;
tracing::info!(
address = %self.health_addr,
"HTTP side-channel listening (/healthz, /readyz, /metrics)"
);
let health_task = tokio::spawn(async move {
axum::serve(health_listener, health_router)
.with_graceful_shutdown(async move {
let _ = shutdown_health_rx.recv().await;
})
.await
.map_err(ServerError::from)
});
let grpc_future = self.build_and_serve_grpc(async move {
let _ = shutdown_grpc_rx.recv().await;
});
let grpc_result = grpc_future.await;
if grpc_result.is_err() {
let _ = shutdown_tx.send(());
}
let health_result = match health_task.await {
Ok(r) => r,
Err(join_err) => Err(ServerError::Io(std::io::Error::other(join_err.to_string()))),
};
grpc_result.and(health_result)
}
fn build_health_router(&self) -> Router {
let readyz = Router::new()
.route("/readyz", get(health::readyz))
.with_state(Arc::clone(&self.readiness));
let metrics = Router::new()
.route("/metrics", get(health::metrics))
.with_state(Arc::clone(&self.metrics));
Router::new()
.route("/healthz", get(health::healthz))
.merge(readyz)
.merge(metrics)
.fallback(fallback_handler)
}
async fn build_and_serve_grpc(
&self,
shutdown: impl Future<Output = ()> + Send + 'static,
) -> Result<(), ServerError> {
let trigger = crate::TriggerHandles {
topic_repo: self.session.topic_repo(),
publisher: self.session.publisher(),
subscriber: self.session.subscriber(),
};
serve_grpc_chain(
self.flight_addr,
self.session.context().clone(),
self.session.tenant_binding_arc(),
self.session_store.clone(),
Some(trigger),
shutdown,
)
.await
.map_err(ServerError::from)
}
}
pub async fn serve_grpc_chain(
addr: SocketAddr,
flight_ctx: SessionContext,
flight_binding: jammi_db::tenant_scope::TenantBinding,
store: SessionStore,
trigger: Option<crate::TriggerHandles>,
shutdown: impl Future<Output = ()> + Send + 'static,
) -> Result<(), tonic::transport::Error> {
let interceptor = TenantInterceptor::new(store.clone());
let provider = TenantBoundProvider::new(flight_ctx.state(), flight_binding, store.clone());
let flight = FlightSqlService::new_with_provider(Box::new(provider));
let flight_svc = FlightServiceServer::new(flight);
let session_svc =
SessionServiceServer::with_interceptor(SessionServer::new(store), interceptor.clone());
let mut builder = Server::builder()
.accept_http1(true)
.layer(GrpcWebLayer::new())
.add_service(flight_svc)
.add_service(session_svc);
if let Some(handles) = trigger {
let trigger_svc = TriggerServiceServer::with_interceptor(
TriggerServer::new(handles.topic_repo, handles.publisher, handles.subscriber),
interceptor,
);
builder = builder.add_service(trigger_svc);
tracing::info!(
"gRPC chain (Flight SQL + SessionService + TriggerService) listening on {addr}"
);
} else {
tracing::info!("gRPC chain (Flight SQL + SessionService) listening on {addr}");
}
builder.serve_with_shutdown(addr, shutdown).await
}
async fn shutdown_signal() {
let ctrl_c = async {
match signal::ctrl_c().await {
Ok(()) => {}
Err(e) => tracing::error!("Failed to install Ctrl+C handler: {e}"),
}
};
#[cfg(unix)]
let terminate = async {
match signal::unix::signal(signal::unix::SignalKind::terminate()) {
Ok(mut sig) => {
sig.recv().await;
}
Err(e) => {
tracing::error!("Failed to install SIGTERM handler: {e}");
std::future::pending::<()>().await;
}
}
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
() = ctrl_c => {},
() = terminate => {},
}
tracing::info!("Shutdown signal received, draining connections...");
}