Files
steep/crates/herald-server/src/main.rs
T
qpismont 624bc1e028
ci/woodpecker/push/tests Pipeline was successful
replace ContainerRuntime CLI to Bollard crate (default: docker socket)
limit tool result to open router tool response (with truncated info for
ai)

Hard kill if graceful shutdown is too long
2026-09-17 19:51:59 +00:00

158 lines
4.1 KiB
Rust

use std::time::Duration;
use crate::{
bot::Bot,
gitea::{GiteaAPI, WebhookType},
open_router::OpenRouterClient,
sandbox::SandboxConfig,
state::AppState,
};
use dotenvy::dotenv;
use tokio::signal::unix::{SignalKind, signal};
use tokio_util::sync::CancellationToken;
use tracing::{info, warn};
use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
mod api;
mod bot;
mod bot_actions;
mod consts;
mod env;
mod errors;
mod gitea;
mod metrics;
mod open_router;
mod sandbox;
mod state;
mod text;
/// Délai laissé aux reviews en cours et aux serveurs pour s'arrêter proprement.
///
/// Sans lui, une review prise dans une sandbox récalcitrante garderait le processus
/// en vie jusqu'à ce que le superviseur le tue.
const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(60);
fn main() -> anyhow::Result<()> {
dotenv().ok();
tracing_subscriber::registry()
.with(fmt::layer())
.with(
EnvFilter::try_from_default_env() // lit RUST_LOG depuis l'env
.unwrap_or_else(|_| EnvFilter::new("info")),
)
.init();
let _sentry_guard = if let Ok(sentry_dsn) = env::try_get_env("SENTRY_DSN") {
info!("Initialize sentry");
Some(sentry::init(
sentry::ClientOptions::new()
.dsn(&sentry_dsn)
.maybe_release(sentry::release_name!())
.send_default_pii(true),
))
} else {
warn!("SENTRY_DSN not set, sentry will not be initialized");
None
};
tokio::runtime::Runtime::new()?.block_on(run())
}
async fn run() -> anyhow::Result<()> {
let config = env::load_config()?;
if let Some(metric_bind_addr) = &config.metrics_bind_addr {
metrics::install(metric_bind_addr)?;
}
let gitea_api = GiteaAPI::new(&config.gitea_url, &config.gitea_token, config.gitea_timeout)?;
let gitea_user = gitea_api.get_authorized_user().await?;
info!(
port = config.http_port,
model = %config.open_router_model,
gitea_url = %config.gitea_url,
bot_name = %gitea_user.login,
"Starting Herald"
);
let open_router_client = OpenRouterClient::new(
&config.open_router_api_key,
&config.open_router_model,
config.open_router_timeout,
)?;
let shutdown = CancellationToken::new();
let sandbox = SandboxConfig {
runtime: devcontainer_rs::ContainerRuntime::connect()?,
max_iterations: config.sandbox_max_iterations,
};
if !sandbox.runtime.available().await {
warn!(
endpoint = sandbox.runtime.endpoint(),
"Container daemon is not reachable, every review will fail"
);
}
let bot = Bot::new(
gitea_user.login,
gitea_api,
open_router_client,
config.bot_max_concurrent,
config.open_router_model.clone(),
sandbox,
);
let (tx, rx) = tokio::sync::mpsc::channel::<WebhookType>(config.bot_max_concurrent * 2);
let app_state = AppState {
bot_tx: tx,
bot: bot.clone(),
config,
};
let signal = async {
let mut sigterm = signal(SignalKind::terminate())?;
let mut sigint = signal(SignalKind::interrupt())?;
tokio::select! {
_ = sigterm.recv() => info!("Received SIGTERM"),
_ = sigint.recv() => info!("Received SIGINT"),
}
info!("Shutting down...");
shutdown.cancel();
anyhow::Ok(())
};
let shutdown_deadline = async {
shutdown.cancelled().await;
tokio::time::sleep(SHUTDOWN_TIMEOUT).await;
};
tokio::select! {
result = async {
tokio::try_join!(
bot.start(rx, shutdown.clone()),
api::start(app_state, shutdown.clone()),
signal
)
} => {
result?;
}
() = shutdown_deadline => {
warn!(
timeout = ?SHUTDOWN_TIMEOUT,
"Shutdown did not finish in time, exiting anyway"
);
}
}
info!("Shutdown complete");
Ok(())
}