mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 23:18:24 +00:00
Motivation: four sync integration tests failed deterministically in some environments: proactive_sync_plus:: missing_relay_list_uses_bounded_fallback_coverage, maintainer_reprocessing:: unresolved_repositories_share_one_dependency_poll_per_relay, metrics::test_live_sync_event_count, and reconnect_backoff:: flapping_relay_handshakes_do_not_reset_exponential_backoff. Each waits for a plain `field=value` substring (e.g. `consecutive_failures=1`) in the relay subprocess log written by the TestRelay fixture. The tracing fmt layer colorizes by default, so the redirected log contained `consecutive_failures^[[0m^[[2m=^[[0m1`, which never matches. tracing-subscriber honours NO_COLOR, so the same tests passed or failed depending on the invoking environment: runs with NO_COLOR set produced clean logs, interactive-launched runs produced ANSI logs and deterministic failures. Approach: initialize the fmt layer with `.with_ansi(std::io::stdout().is_terminal())`. Colors are emitted only when a human is watching a terminal; redirected output (test fixtures, journald, pipelines) stays plain. This is a logging hygiene fix in the binary, not a test accommodation: the defect was writing terminal control sequences to a non-terminal stream. Correctness assumptions: the relay binary is the only process whose logs the fixtures scrape, and tests always redirect its stdout to a file, so is_terminal() is deterministically false there and the log format no longer varies with the parent environment. Excluded scope: several sync tests remain flaky under full-suite parallel load but pass repeatedly in isolation (req_concurrency::startup_historic_sync_stays_within_relay_req_concurrency_limit, live_sync::live_sync_regroups_after_filter_count_refusal, tag_variations::test_layer3_sync_with_lowercase_e_tag). They assert on metrics or proxy behaviour, not log text; their bounded deadlines are exceeded when the whole suite competes for CPU. Left undiagnosed rather than papered over with wider timeouts. Validation: the four log-scraping tests pass individually and in full runs after the fix; cargo fmt makes no changes; cargo clippy --workspace --all-targets -- -D warnings is clean; full cargo test is green except the pre-existing load flakes above, each of which passes 3/3 when run in isolation.
123 lines
4.5 KiB
Rust
123 lines
4.5 KiB
Rust
use std::io::IsTerminal;
|
|
|
|
use anyhow::Result;
|
|
use clap::Parser;
|
|
use tokio::signal;
|
|
use tracing::info;
|
|
use tracing_subscriber::{filter::FilterExt, layer::SubscriberExt, EnvFilter, Layer};
|
|
|
|
use ngit_grasp::{
|
|
cleanup_empty_repos, config::Config, logging::SuppressRoutineDisconnects, nostr,
|
|
server::RelayServer,
|
|
};
|
|
|
|
/// Top-level CLI dispatcher.
|
|
///
|
|
/// With no subcommand the binary runs the relay (all relay flags apply).
|
|
/// With a subcommand it runs the requested maintenance tool instead.
|
|
#[derive(Debug, Parser)]
|
|
#[command(author, version, about = "ngit-grasp GRASP relay", long_about = None)]
|
|
#[command(propagate_version = true)]
|
|
enum Cli {
|
|
/// Run the GRASP relay server (default when no subcommand is given).
|
|
#[command(name = "serve")]
|
|
Serve(Box<Config>),
|
|
|
|
/// Remove kind 30617/30618 events whose bare git repository is empty or missing.
|
|
///
|
|
/// Runs in dry-run mode by default. Pass --execute to make changes.
|
|
/// Stop the relay service before running with --execute.
|
|
CleanupEmptyRepos(cleanup_empty_repos::CleanupArgs),
|
|
|
|
/// Permanently eject deleted repository data from holding/archive stores.
|
|
///
|
|
/// This is an operator/admin maintenance command and is idempotent.
|
|
HoldingEject(nostr::lifecycle::HoldingEjectArgs),
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
// Load .env file before clap parses, so env vars are available.
|
|
dotenvy::dotenv().ok();
|
|
|
|
// Peek at argv[1] to decide whether a subcommand was explicitly provided.
|
|
// If not, prepend the implicit "serve" subcommand so that clap routes to Cli::Serve
|
|
// and all relay flags are parsed normally (preserving backward compatibility).
|
|
let mut args: Vec<String> = std::env::args().collect();
|
|
let known_subcommands = ["serve", "cleanup-empty-repos", "holding-eject", "help"];
|
|
let has_subcommand = args.get(1).is_some_and(|a| {
|
|
known_subcommands.contains(&a.as_str())
|
|
|| matches!(a.as_str(), "-h" | "--help" | "-V" | "--version")
|
|
});
|
|
if !has_subcommand {
|
|
args.insert(1, "serve".to_string());
|
|
}
|
|
|
|
match Cli::parse_from(args) {
|
|
Cli::CleanupEmptyRepos(cleanup_args) => cleanup_empty_repos::run(&cleanup_args).await,
|
|
Cli::HoldingEject(eject_args) => nostr::lifecycle::run_holding_eject(eject_args).await,
|
|
Cli::Serve(config) => {
|
|
let mut config = *config;
|
|
config.relay_owner_nsec = Some(Config::load_relay_owner_key()?);
|
|
run_relay(config).await
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Run the relay until an OS shutdown signal arrives.
|
|
///
|
|
/// All relay wiring lives in [`ngit_grasp::server::RelayServer`];
|
|
/// this function only owns concerns specific to the standalone binary:
|
|
/// the global tracing subscriber and signal handling.
|
|
async fn run_relay(config: Config) -> Result<()> {
|
|
// Initialize tracing with configured log level
|
|
let filter = EnvFilter::new(&config.log_level).and(SuppressRoutineDisconnects);
|
|
// Only colorize when stdout is a terminal: ANSI escape codes in redirected
|
|
// logs corrupt journald/file output and break log-scraping consumers.
|
|
let subscriber = tracing_subscriber::registry().with(
|
|
tracing_subscriber::fmt::layer()
|
|
.with_ansi(std::io::stdout().is_terminal())
|
|
.with_filter(filter),
|
|
);
|
|
tracing::subscriber::set_global_default(subscriber)?;
|
|
|
|
info!("Starting ngit-grasp with log level: {}", config.log_level);
|
|
|
|
let server = RelayServer::start(config).await?;
|
|
|
|
server.run_until(shutdown_signal()).await
|
|
}
|
|
|
|
/// Resolves when the process receives SIGINT (Ctrl+C) or, on unix, SIGTERM.
|
|
async fn shutdown_signal() {
|
|
#[cfg(unix)]
|
|
{
|
|
use tokio::signal::unix::{signal as unix_signal, SignalKind};
|
|
let mut sigterm = match unix_signal(SignalKind::terminate()) {
|
|
Ok(sigterm) => sigterm,
|
|
Err(e) => {
|
|
tracing::error!("Failed to install SIGTERM handler: {}", e);
|
|
// Fall back to Ctrl+C only.
|
|
let _ = signal::ctrl_c().await;
|
|
info!("Received SIGINT (Ctrl+C), cleaning up...");
|
|
return;
|
|
}
|
|
};
|
|
|
|
tokio::select! {
|
|
_ = signal::ctrl_c() => {
|
|
info!("Received SIGINT (Ctrl+C), cleaning up...");
|
|
}
|
|
_ = sigterm.recv() => {
|
|
info!("Received SIGTERM, cleaning up...");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(not(unix))]
|
|
{
|
|
let _ = signal::ctrl_c().await;
|
|
info!("Received SIGINT (Ctrl+C), cleaning up...");
|
|
}
|
|
}
|