refactor: extract GitksServer / GitksConfig as library, thin main.rs

- Add GitksConfig struct with from_env() for explicit config
- Add GitksServer / GitksServerBuilder to lib.rs
- Move tracing init, disk cache, metrics, hooks setup into builder
- Expose serve() / serve_with_shutdown() for embedding
- main.rs now only handles etcd overlay and delegates to library
This commit is contained in:
zhenyi
2026-06-12 21:36:57 +08:00
parent 96b391ff2d
commit c6f99fff47
2 changed files with 426 additions and 291 deletions
+413
View File
@@ -29,3 +29,416 @@ pub mod server;
pub mod snapshot; pub mod snapshot;
pub mod tag; pub mod tag;
pub mod tree; pub mod tree;
use std::future::Future;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::time::Duration;
use server::GitksService;
/// Configuration for building a [`GitksServer`].
///
/// Use [`GitksConfig::from_env`] to read from environment variables,
/// or construct manually when embedding as a library.
#[derive(Debug, Clone)]
pub struct GitksConfig {
/// Repository storage prefix path (required).
pub repo_prefix: PathBuf,
/// gRPC listen host.
pub host: String,
/// gRPC listen port.
pub port: String,
/// Storage name used in repository headers.
pub storage_name: String,
/// Advertised gRPC address for other services.
pub grpc_addr: String,
/// Enable disk-based cache.
pub disk_cache_enabled: bool,
/// Max age for disk cache entries (seconds).
pub disk_cache_max_age: u64,
/// Enable pack-objects cache wrapper.
pub pack_cache_enabled: bool,
/// Enable pack cache backpressure.
pub pack_cache_backpressure: bool,
/// Enable hook support.
pub hooks_enabled: bool,
/// Server-side hooks directory.
pub server_hooks_dir: Option<PathBuf>,
/// Callback address for hooks.
pub hook_callback_addr: Option<String>,
/// Hook execution timeout (seconds).
pub hook_timeout: u64,
/// Allow custom hooks from repository config.
pub allow_custom_hooks: bool,
/// Prometheus metrics port.
pub metrics_port: u16,
/// Slow request detection threshold (ms).
pub slow_request_threshold: u64,
/// Log format: "pretty" or "json".
pub log_format: String,
/// Optional log directory for file output.
pub log_dir: Option<String>,
/// Log rotation strategy.
pub log_rotation: String,
/// Max log files to retain.
pub log_retention: usize,
}
/// A ready-to-run gitks gRPC server.
pub struct GitksServer {
service: GitksService,
addr: SocketAddr,
http_cancel: tokio_util::sync::CancellationToken,
_log_guard: Option<tracing_appender::non_blocking::WorkerGuard>,
_metrics_handle: tokio::task::JoinHandle<()>,
_semaphore_cleanup: tokio::task::JoinHandle<()>,
}
/// Builder for [`GitksServer`].
///
/// # Examples
///
/// ```no_run
/// use gitks::{GitksServer, GitksConfig};
///
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let config = GitksConfig::from_env()?;
/// let server = GitksServer::builder()
/// .config(config)
/// .build()?;
/// server.serve().await?;
/// # Ok(())
/// # }
/// ```
pub struct GitksServerBuilder {
config: Option<GitksConfig>,
}
impl GitksConfig {
/// Build config from environment variables.
pub fn from_env() -> Result<Self, Box<dyn std::error::Error>> {
dotenvy::dotenv().ok();
Ok(Self {
repo_prefix: PathBuf::from(
std::env::var("REPO_PREFIX_PATH")
.map_err(|_| "REPO_PREFIX_PATH is required (e.g. /data/repos)")?,
),
host: env_or("GITKS_HOST", "0.0.0.0"),
port: env_or("GITKS_PORT", "50051"),
storage_name: env_or("STORAGE_NAME", "default"),
grpc_addr: std::env::var("GITKS_ADVERTISE_ADDR")
.unwrap_or_else(|_| format!("http://{}:{}", env_or("GITKS_HOST", "0.0.0.0"), env_or("GITKS_PORT", "50051"))),
disk_cache_enabled: env_bool("GITKS_DISK_CACHE_ENABLED", false),
disk_cache_max_age: env_u64("GITKS_DISK_CACHE_MAX_AGE", 300),
pack_cache_enabled: env_bool("GITKS_PACK_CACHE_ENABLED", false),
pack_cache_backpressure: env_bool("GITKS_PACK_CACHE_BACKPRESSURE", true),
hooks_enabled: env_bool("GITKS_HOOKS_ENABLED", true),
server_hooks_dir: std::env::var("GITKS_SERVER_HOOKS_DIR").ok().map(PathBuf::from),
hook_callback_addr: std::env::var("GITKS_HOOK_CALLBACK_ADDR").ok(),
hook_timeout: env_u64("GITKS_HOOK_TIMEOUT", 30),
allow_custom_hooks: env_bool("GITKS_ALLOW_CUSTOM_HOOKS", true),
metrics_port: env_u64("GITKS_METRICS_PORT", 9100) as u16,
slow_request_threshold: env_u64("GITKS_SLOW_REQUEST_THRESHOLD_MS", 5000),
log_format: env_or("GITKS_LOG_FORMAT", "pretty"),
log_dir: std::env::var("GITKS_LOG_DIR").ok(),
log_rotation: env_or("GITKS_LOG_ROTATION", "daily"),
log_retention: env_u64("GITKS_LOG_RETENTION", 7) as usize,
})
}
}
impl GitksServer {
/// Create a new builder.
pub fn builder() -> GitksServerBuilder {
GitksServerBuilder::default()
}
/// Start the gRPC server and block until Ctrl+C (or SIGTERM on Unix).
pub async fn serve(self) -> Result<(), Box<dyn std::error::Error>> {
self.serve_with_shutdown(shutdown_signal()).await
}
/// Start the gRPC server and block until the provided `shutdown` future resolves.
pub async fn serve_with_shutdown(
self,
shutdown: impl Future<Output = ()>,
) -> Result<(), Box<dyn std::error::Error>> {
metrics::set_ready(true);
server::serve_with_shutdown(self.addr, self.service, shutdown).await?;
metrics::set_ready(false);
self.http_cancel.cancel();
tracing::info!("gitks shut down complete");
Ok(())
}
}
impl GitksServerBuilder {
/// Set the server configuration.
pub fn config(mut self, config: GitksConfig) -> Self {
self.config = Some(config);
self
}
/// Build the server.
pub fn build(self) -> Result<GitksServer, Box<dyn std::error::Error>> {
let config = self.config.unwrap_or_else(|| {
GitksConfig::from_env().expect("failed to load gitks config")
});
// Validate repo_prefix
if !config.repo_prefix.is_absolute() {
return Err("REPO_PREFIX_PATH must be an absolute path".into());
}
if !config.repo_prefix.exists() {
tracing::info!(path = %config.repo_prefix.display(), "creating repo prefix directory");
std::fs::create_dir_all(&config.repo_prefix)?;
}
// Init logging if log_dir is set
let log_guard = init_tracing_from_config(&config);
tracing::info!(
version = env!("CARGO_PKG_VERSION"),
log_format = %config.log_format,
"gitks starting up"
);
// Disk cache
let disk_cache = disk_cache::DiskCache::new(
config.repo_prefix.clone(),
env!("CARGO_PKG_VERSION").to_string(),
config.disk_cache_max_age,
config.disk_cache_enabled,
);
if config.disk_cache_enabled {
tracing::info!(max_age_secs = config.disk_cache_max_age, "disk cache enabled");
disk_cache.cleanup_on_startup()?;
disk_cache::start_cache_cleanup_task(disk_cache.clone(), Duration::from_secs(300));
} else {
tracing::info!("disk cache disabled");
}
// Pack cache
let pack_cache = if config.disk_cache_enabled {
tracing::info!(
pack_objects_cache = config.pack_cache_enabled,
backpressure = config.pack_cache_backpressure,
"pack cache wrapper enabled"
);
Some(pack_cache::PackCache::new(
disk_cache.clone(),
config.pack_cache_backpressure,
))
} else {
None
};
// Hook manager
let hook_manager = if config.hooks_enabled {
tracing::info!(
timeout_secs = config.hook_timeout,
custom_hooks = config.allow_custom_hooks,
"hooks enabled"
);
Some(hooks::HookManager::new(
config.repo_prefix.clone(),
config.server_hooks_dir,
config.hook_callback_addr,
Duration::from_secs(config.hook_timeout),
config.allow_custom_hooks,
))
} else {
tracing::info!("hooks disabled");
None
};
// Metrics
let http_cancel = tokio_util::sync::CancellationToken::new();
metrics::set_http_cancel_token(http_cancel.clone());
let metrics_handle = metrics::start_metrics_server(config.metrics_port);
tracing::info!(port = config.metrics_port, "metrics server started");
// Rate limiter cleanup
let semaphore_cleanup = rate_limit::start_semaphore_cleanup_task();
// Slow request detection
metrics::set_slow_request_threshold(config.slow_request_threshold);
tracing::info!(
threshold_ms = config.slow_request_threshold,
"slow request detection configured"
);
// Build service
let addr: SocketAddr = format!("{}:{}", config.host, config.port).parse()?;
let mut svc = GitksService::new(config.repo_prefix.clone());
if config.disk_cache_enabled {
svc = svc.with_disk_cache(disk_cache);
}
if let Some(pc) = pack_cache {
svc = svc.with_pack_cache(pc);
}
if let Some(hm) = hook_manager {
svc = svc.with_hook_manager(hm);
}
let svc = svc.with_grpc_addr(config.grpc_addr.clone());
tracing::info!(
addr = %addr,
repo_prefix = %config.repo_prefix.display(),
storage = %config.storage_name,
advertise = %config.grpc_addr,
"starting gitks gRPC server"
);
Ok(GitksServer {
service: svc,
addr,
http_cancel,
_log_guard: log_guard,
_metrics_handle: metrics_handle,
_semaphore_cleanup: semaphore_cleanup,
})
}
}
impl Default for GitksServerBuilder {
fn default() -> Self {
Self { config: None }
}
}
fn env_or(key: &str, default: &str) -> String {
std::env::var(key).unwrap_or_else(|_| default.into())
}
fn env_bool(key: &str, default: bool) -> bool {
match std::env::var(key).as_deref() {
Ok("true" | "1" | "yes") => true,
Ok("false" | "0" | "no") => false,
_ => default,
}
}
fn env_u64(key: &str, default: u64) -> u64 {
std::env::var(key)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}
fn init_tracing_from_config(
config: &GitksConfig,
) -> Option<tracing_appender::non_blocking::WorkerGuard> {
use tracing_subscriber::EnvFilter;
use tracing_subscriber::fmt;
use tracing_subscriber::prelude::*;
let env_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
let fmt_layer = match config.log_format.as_str() {
"json" => fmt::layer()
.json()
.with_target(true)
.with_file(true)
.with_line_number(true)
.with_thread_ids(true)
.with_span_events(fmt::format::FmtSpan::NEW | fmt::format::FmtSpan::CLOSE)
.boxed(),
_ => fmt::layer()
.pretty()
.with_target(true)
.with_file(true)
.with_line_number(true)
.boxed(),
};
if let Some(ref log_dir) = config.log_dir {
let rotation = match config.log_rotation.as_str() {
"hourly" => tracing_appender::rolling::Rotation::HOURLY,
"never" => tracing_appender::rolling::Rotation::NEVER,
_ => tracing_appender::rolling::Rotation::DAILY,
};
let retention = config.log_retention;
let mut builder = tracing_appender::rolling::Builder::new()
.rotation(rotation)
.filename_prefix("gitks")
.filename_suffix("log");
if retention > 0 {
builder = builder.max_log_files(retention);
}
let file_appender = match builder.build(log_dir) {
Ok(file_appender) => file_appender,
Err(err) => {
eprintln!("failed to create log directory '{log_dir}': {err}");
tracing_subscriber::registry()
.with(env_filter)
.with(fmt_layer)
.init();
return None;
}
};
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let file_layer = fmt::layer()
.json()
.with_target(true)
.with_file(true)
.with_line_number(true)
.with_writer(non_blocking)
.with_filter(EnvFilter::new("info"))
.boxed();
tracing_subscriber::registry()
.with(env_filter)
.with(fmt_layer)
.with(file_layer)
.init();
Some(guard)
} else {
tracing_subscriber::registry()
.with(env_filter)
.with(fmt_layer)
.init();
None
}
}
async fn shutdown_signal() {
let ctrl_c = async {
tokio::signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("failed to install SIGTERM handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {
tracing::info!("received Ctrl+C, starting graceful shutdown");
}
_ = terminate => {
tracing::info!("received SIGTERM, starting graceful shutdown");
}
}
}
+13 -291
View File
@@ -1,25 +1,10 @@
//! Copyright (c) 2022-2026 GitDataAi All rights reserved. //! Copyright (c) 2022-2026 GitDataAi All rights reserved.
use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use gitks::disk_cache::DiskCache;
use gitks::hooks::HookManager;
use gitks::metrics;
use gitks::server::{GitksService, serve_with_shutdown};
use etcd_client::{Client, PutOptions}; use etcd_client::{Client, PutOptions};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::fmt;
use tracing_subscriber::prelude::*;
const DEFAULT_HOST: &str = "0.0.0.0";
const DEFAULT_PORT: &str = "50051";
const DEFAULT_STORAGE_NAME: &str = "default";
/// etcd-backed config reader. Priority: etcd > env > default. /// etcd-backed config reader. Priority: etcd > env > default.
struct EtcdConfig { struct EtcdConfig {
client: Arc<Mutex<Client>>, client: Arc<Mutex<Client>>,
@@ -37,7 +22,6 @@ impl EtcdConfig {
}) })
} }
/// Get config: etcd first, env second, default last.
async fn get(&self, key: &str, default: &str) -> String { async fn get(&self, key: &str, default: &str) -> String {
let etcd_key = format!("{}config/{}", self.prefix, key); let etcd_key = format!("{}config/{}", self.prefix, key);
if let Ok(mut c) = self.client.try_lock() if let Ok(mut c) = self.client.try_lock()
@@ -51,7 +35,6 @@ impl EtcdConfig {
std::env::var(key).unwrap_or_else(|_| default.to_string()) std::env::var(key).unwrap_or_else(|_| default.to_string())
} }
/// Register this service under the common prefix for discovery by other services.
async fn register(&self, service_name: &str, addr: &str) -> Result<(), String> { async fn register(&self, service_name: &str, addr: &str) -> Result<(), String> {
let instance_id = uuid::Uuid::now_v7().to_string(); let instance_id = uuid::Uuid::now_v7().to_string();
let addr = addr.to_string(); let addr = addr.to_string();
@@ -102,117 +85,13 @@ impl EtcdConfig {
} }
} }
fn env_or(key: &str, default: &str) -> String {
std::env::var(key).unwrap_or_else(|_| default.into())
}
fn env_bool(key: &str, default: bool) -> bool {
match std::env::var(key).as_deref() {
Ok("true" | "1" | "yes") => true,
Ok("false" | "0" | "no") => false,
_ => default,
}
}
fn env_u64(key: &str, default: u64) -> u64 {
std::env::var(key)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}
fn init_tracing() -> Option<tracing_appender::non_blocking::WorkerGuard> {
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
let log_format = env_or("GITKS_LOG_FORMAT", "pretty");
let fmt_layer = match log_format.as_str() {
"json" => fmt::layer()
.json()
.with_target(true)
.with_file(true)
.with_line_number(true)
.with_thread_ids(true)
.with_span_events(fmt::format::FmtSpan::NEW | fmt::format::FmtSpan::CLOSE)
.boxed(),
_ => fmt::layer()
.pretty()
.with_target(true)
.with_file(true)
.with_line_number(true)
.boxed(),
};
if let Ok(log_dir) = std::env::var("GITKS_LOG_DIR") {
let rotation = match env_or("GITKS_LOG_ROTATION", "daily").as_str() {
"hourly" => tracing_appender::rolling::Rotation::HOURLY,
"never" => tracing_appender::rolling::Rotation::NEVER,
_ => tracing_appender::rolling::Rotation::DAILY,
};
let retention = env_u64("GITKS_LOG_RETENTION", 7) as usize;
let mut builder = tracing_appender::rolling::Builder::new()
.rotation(rotation)
.filename_prefix("gitks")
.filename_suffix("log");
if retention > 0 {
builder = builder.max_log_files(retention);
}
let file_appender = match builder.build(&log_dir) {
Ok(file_appender) => file_appender,
Err(err) => {
eprintln!("failed to create log directory '{log_dir}': {err}");
tracing_subscriber::registry()
.with(env_filter)
.with(fmt_layer)
.init();
return None;
}
};
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let file_layer = fmt::layer()
.json()
.with_target(true)
.with_file(true)
.with_line_number(true)
.with_writer(non_blocking)
.with_filter(EnvFilter::new("info"))
.boxed();
tracing_subscriber::registry()
.with(env_filter)
.with(fmt_layer)
.with(file_layer)
.init();
Some(guard)
} else {
tracing_subscriber::registry()
.with(env_filter)
.with(fmt_layer)
.init();
None
}
}
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
dotenvy::dotenv().ok(); dotenvy::dotenv().ok();
let _log_guard = init_tracing();
tracing::info!( let mut config = gitks::GitksConfig::from_env()?;
version = env!("CARGO_PKG_VERSION"),
log_format = %env_or("GITKS_LOG_FORMAT", "pretty"),
"gitks starting up"
);
let host = env_or("GITKS_HOST", DEFAULT_HOST);
let port = env_or("GITKS_PORT", DEFAULT_PORT);
let storage_name = env_or("STORAGE_NAME", DEFAULT_STORAGE_NAME);
// Overlay etcd config (etcd > env > default)
let etcd_endpoints: Vec<String> = std::env::var("GITKS_ETCD_ENDPOINTS") let etcd_endpoints: Vec<String> = std::env::var("GITKS_ETCD_ENDPOINTS")
.ok() .ok()
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
@@ -220,177 +99,20 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.unwrap_or_else(|| vec!["http://localhost:2379".to_string()]); .unwrap_or_else(|| vec!["http://localhost:2379".to_string()]);
let etcd_prefix = std::env::var("ETCD_KEY_PREFIX").unwrap_or_else(|_| "/appks/".to_string()); let etcd_prefix = std::env::var("ETCD_KEY_PREFIX").unwrap_or_else(|_| "/appks/".to_string());
let etcd = EtcdConfig::connect(etcd_endpoints, &etcd_prefix).await.ok(); if let Ok(etcd) = EtcdConfig::connect(etcd_endpoints, &etcd_prefix).await {
let host = if let Some(ref e) = etcd { config.host = etcd.get("GITKS_HOST", &config.host).await;
e.get("GITKS_HOST", &host).await config.port = etcd.get("GITKS_PORT", &config.port).await;
} else { config.storage_name = etcd.get("GITKS_STORAGE_NAME", &config.storage_name).await;
host config.grpc_addr = etcd
}; .get("GITKS_ADVERTISE_ADDR", &config.grpc_addr)
let port = if let Some(ref e) = etcd { .await;
e.get("GITKS_PORT", &port).await
} else {
port
};
let storage_name = if let Some(ref e) = etcd {
e.get("GITKS_STORAGE_NAME", &storage_name).await
} else {
storage_name
};
let grpc_addr =
std::env::var("GITKS_ADVERTISE_ADDR").unwrap_or_else(|_| format!("http://{host}:{port}"));
if let Some(ref e) = etcd { let addr_str = format!("{}:{}", config.host, config.port);
let addr_str = format!("{host}:{port}"); etcd.register("gitks", &addr_str).await.ok();
e.register("gitks", &addr_str).await.ok();
} }
let repo_prefix = std::env::var("REPO_PREFIX_PATH") let server = gitks::GitksServer::builder().config(config).build()?;
.map_err(|_| "REPO_PREFIX_PATH environment variable is required (e.g. /data/repos)")?; server.serve().await?;
let repo_prefix = PathBuf::from(&repo_prefix);
if !repo_prefix.is_absolute() {
return Err("REPO_PREFIX_PATH must be an absolute path".into());
}
if !repo_prefix.exists() {
tracing::info!(path = %repo_prefix.display(), "creating repo prefix directory");
std::fs::create_dir_all(&repo_prefix)?;
}
let disk_cache_enabled = env_bool("GITKS_DISK_CACHE_ENABLED", false);
let disk_cache_max_age = env_u64("GITKS_DISK_CACHE_MAX_AGE", 300);
let disk_cache = DiskCache::new(
repo_prefix.clone(),
env!("CARGO_PKG_VERSION").to_string(),
disk_cache_max_age,
disk_cache_enabled,
);
if disk_cache_enabled {
tracing::info!(max_age_secs = disk_cache_max_age, "disk cache enabled");
disk_cache.cleanup_on_startup()?;
gitks::disk_cache::start_cache_cleanup_task(disk_cache.clone(), Duration::from_secs(300));
} else {
tracing::info!("disk cache disabled");
}
let pack_cache_enabled = env_bool("GITKS_PACK_CACHE_ENABLED", false);
let pack_backpressure = env_bool("GITKS_PACK_CACHE_BACKPRESSURE", true);
let pack_cache = if disk_cache_enabled {
tracing::info!(
pack_objects_cache = pack_cache_enabled,
backpressure = pack_backpressure,
"pack cache wrapper enabled"
);
Some(gitks::pack_cache::PackCache::new(
disk_cache.clone(),
pack_backpressure,
))
} else {
None
};
let hooks_enabled = env_bool("GITKS_HOOKS_ENABLED", true);
let server_hooks_dir = std::env::var("GITKS_SERVER_HOOKS_DIR")
.ok()
.map(PathBuf::from);
let hook_callback_addr = std::env::var("GITKS_HOOK_CALLBACK_ADDR").ok();
let hook_timeout = env_u64("GITKS_HOOK_TIMEOUT", 30);
let allow_custom_hooks = env_bool("GITKS_ALLOW_CUSTOM_HOOKS", true);
let hook_manager = if hooks_enabled {
tracing::info!(
timeout_secs = hook_timeout,
custom_hooks = allow_custom_hooks,
"hooks enabled"
);
Some(HookManager::new(
repo_prefix.clone(),
server_hooks_dir,
hook_callback_addr,
Duration::from_secs(hook_timeout),
allow_custom_hooks,
))
} else {
tracing::info!("hooks disabled");
None
};
let metrics_port = env_u64("GITKS_METRICS_PORT", 9100) as u16;
let http_cancel = tokio_util::sync::CancellationToken::new();
metrics::set_http_cancel_token(http_cancel.clone());
let _metrics_handle = metrics::start_metrics_server(metrics_port);
tracing::info!(port = metrics_port, "metrics server started");
let _semaphore_cleanup = gitks::rate_limit::start_semaphore_cleanup_task();
let slow_request_threshold = env_u64("GITKS_SLOW_REQUEST_THRESHOLD_MS", 5000);
metrics::set_slow_request_threshold(slow_request_threshold);
tracing::info!(
threshold_ms = slow_request_threshold,
"slow request detection configured"
);
let addr: std::net::SocketAddr = format!("{host}:{port}").parse()?;
let mut svc = GitksService::new(repo_prefix.clone());
if disk_cache_enabled {
svc = svc.with_disk_cache(disk_cache);
}
if let Some(pc) = pack_cache {
svc = svc.with_pack_cache(pc);
}
if let Some(hm) = hook_manager {
svc = svc.with_hook_manager(hm);
}
let svc = svc.with_grpc_addr(grpc_addr.clone());
tracing::info!(
addr = %addr,
repo_prefix = %repo_prefix.display(),
storage = %storage_name,
advertise = %grpc_addr,
"starting gitks gRPC server"
);
metrics::set_ready(true);
serve_with_shutdown(addr, svc, shutdown_signal()).await?;
metrics::set_ready(false);
http_cancel.cancel();
tracing::info!("gitks shut down complete");
Ok(()) Ok(())
} }
/// Resolves when the process receives SIGTERM or SIGINT (Ctrl+C).
async fn shutdown_signal() {
let ctrl_c = async {
tokio::signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("failed to install SIGTERM handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {
tracing::info!("received Ctrl+C, starting graceful shutdown");
}
_ = terminate => {
tracing::info!("received SIGTERM, starting graceful shutdown");
}
}
}