1use std::{net::SocketAddr, process::ExitCode};
12
13use tokio::net::TcpListener;
14use tonic::transport::Server as TonicServer;
15use tracing::{error, info};
16
17use crate::{
18 ServerConfig, ServerError, ServerState, api,
19 config::{CliOverrides, NamespaceMode, StoreBackend},
20 observability,
21 shutdown::{self, ShutdownOutcome},
22};
23
24pub async fn run(overrides: CliOverrides) -> ExitCode {
33 match run_server(overrides).await {
34 Ok(code) => code,
35 Err(error) => {
36 error!(%error, "aion-server failed");
37 if error.is_config() {
38 ExitCode::from(2)
39 } else {
40 ExitCode::FAILURE
41 }
42 }
43 }
44}
45
46async fn run_server(cli: CliOverrides) -> Result<ExitCode, ServerError> {
47 observability::tracing::init()?;
48
49 let config = ServerConfig::load(&cli)?;
50 reject_auth_without_feature(&config)?;
51 let store_backend = config.store.backend;
52 let state = ServerState::build(config).await?;
53 reject_tls_until_supported(&state)?;
54
55 let runtime = state.runtime_config();
56 let grpc_address = runtime.listen.grpc;
57 let http_address = runtime.listen.http;
58 let workflow_packages: Vec<String> = runtime
59 .workflow_packages
60 .iter()
61 .map(|path| path.display().to_string())
62 .collect();
63 info!(
64 version = env!("CARGO_PKG_VERSION"),
65 grpc_address = %grpc_address,
66 http_address = %http_address,
67 default_namespace = %runtime.default_namespace,
68 namespace_mode = namespace_mode_label(&runtime.namespace.mode),
69 store_backend = store_backend_label(store_backend),
70 auth_enabled = runtime.auth.enabled,
71 deploy_enabled = runtime.deploy.enabled,
72 metrics_enabled = runtime.metrics.enabled,
73 workflow_package_count = workflow_packages.len(),
74 workflow_packages = ?workflow_packages,
75 "aion-server startup banner"
76 );
77 let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
78 let mut grpc = tokio::spawn(serve_grpc(state.clone(), grpc_address, shutdown_rx.clone()));
79 let mut http = tokio::spawn(serve_http(state.clone(), http_address, shutdown_rx));
80
81 let outcome = tokio::select! {
82 result = &mut grpc => {
83 transport_result("gRPC", result)?;
84 state.shutdown()?;
85 ShutdownOutcome::Clean
86 },
87 result = &mut http => {
88 transport_result("HTTP", result)?;
89 state.shutdown()?;
90 ShutdownOutcome::Clean
91 },
92 result = shutdown_signal() => {
93 result?;
94 let _receiver_count = shutdown_tx.send(true);
95 let outcome = shutdown::drain_after_first_signal(state.clone(), async {
96 let _ = shutdown_signal().await;
97 }).await?;
98 if !matches!(outcome, ShutdownOutcome::Forced) {
99 transport_result("gRPC", grpc.await)?;
100 transport_result("HTTP", http.await)?;
101 }
102 outcome
103 },
104 };
105
106 Ok(outcome.exit_code())
107}
108
109fn transport_result(
110 transport: &'static str,
111 result: Result<Result<(), ServerError>, tokio::task::JoinError>,
112) -> Result<(), ServerError> {
113 match result {
114 Ok(transport_outcome) => transport_outcome,
115 Err(join_error) => Err(ServerError::Transport {
116 transport,
117 message: join_error.to_string(),
118 }),
119 }
120}
121
122async fn serve_grpc(
123 state: ServerState,
124 address: SocketAddr,
125 shutdown: tokio::sync::watch::Receiver<bool>,
126) -> Result<(), ServerError> {
127 let workflow = api::grpc::workflow_service(state.clone());
128 let worker = api::worker_grpc::worker_service(state.clone());
129 let mut router = TonicServer::builder()
130 .add_service(workflow)
131 .add_service(worker);
132 if state.runtime_config().deploy.enabled {
135 router = router.add_service(api::deploy_grpc::deploy_service(state)?);
136 }
137 router
138 .serve_with_shutdown(address, shutdown_requested(shutdown))
139 .await
140 .map_err(|source| transport_bind("grpc", address, source))?;
141 Ok(())
142}
143
144async fn serve_http(
145 state: ServerState,
146 address: SocketAddr,
147 shutdown: tokio::sync::watch::Receiver<bool>,
148) -> Result<(), ServerError> {
149 let listener = TcpListener::bind(address)
150 .await
151 .map_err(|source| transport_bind("http", address, source))?;
152 axum::serve(listener, api::http::http_router(state)?)
153 .with_graceful_shutdown(shutdown_requested(shutdown))
154 .await
155 .map_err(|source| transport_bind("http", address, source))?;
156 Ok(())
157}
158
159async fn shutdown_requested(mut shutdown: tokio::sync::watch::Receiver<bool>) {
160 while !*shutdown.borrow_and_update() {
161 if shutdown.changed().await.is_err() {
162 break;
163 }
164 }
165}
166
167async fn shutdown_signal() -> Result<(), ServerError> {
168 #[cfg(unix)]
169 {
170 use tokio::signal::unix::{SignalKind, signal};
171
172 let mut terminate = signal(SignalKind::terminate())
173 .map_err(|source| signal_listener("SIGTERM", &source))?;
174 let mut interrupt =
175 signal(SignalKind::interrupt()).map_err(|source| signal_listener("SIGINT", &source))?;
176 tokio::select! {
177 _ = terminate.recv() => Ok(()),
178 _ = interrupt.recv() => Ok(()),
179 }
180 }
181
182 #[cfg(not(unix))]
183 {
184 tokio::signal::ctrl_c()
185 .await
186 .map_err(|source| signal_listener("shutdown signal", &source))
187 }
188}
189
190fn signal_listener(listener: &'static str, source: &std::io::Error) -> ServerError {
191 ServerError::SignalListener {
192 listener,
193 message: source.to_string(),
194 }
195}
196
197fn reject_auth_without_feature(config: &ServerConfig) -> Result<(), ServerError> {
198 if cfg!(not(feature = "auth")) && config.auth.enabled {
199 return Err(ServerError::Config {
200 message: "auth.enabled=true but binary compiled without auth feature".to_owned(),
201 });
202 }
203 Ok(())
204}
205
206fn reject_tls_until_supported(state: &ServerState) -> Result<(), ServerError> {
207 if state.runtime_config().tls.is_some() {
208 return Err(ServerError::Config {
209 message: "configured TLS material cannot be served until transport TLS is wired"
210 .to_owned(),
211 });
212 }
213 Ok(())
214}
215
216fn store_backend_label(backend: StoreBackend) -> &'static str {
217 match backend {
218 StoreBackend::Memory => "memory",
219 StoreBackend::LibSql => "libsql",
220 }
221}
222
223fn namespace_mode_label(mode: &NamespaceMode) -> &'static str {
224 match mode {
225 NamespaceMode::SharedEngine => "SharedEngine",
226 NamespaceMode::SingleTenant { .. } => "SingleTenant",
227 }
228}
229
230fn transport_bind<E>(transport: &'static str, address: SocketAddr, source: E) -> ServerError
231where
232 E: std::error::Error,
233{
234 ServerError::TransportBind {
235 transport,
236 address,
237 message: source.to_string(),
238 }
239}