feat: handle SIGTERM in +watch and +subscribe for clean shutdown (#550)
* feat: handle SIGTERM in +watch and +subscribe for clean shutdown Add shared shutdown_signal() helper that merges SIGINT and SIGTERM into a single future. Replace tokio::signal::ctrl_c() in both watch and subscribe pull loops so they exit cleanly under Kubernetes, Docker, and systemd. On non-Unix platforms, only SIGINT (Ctrl+C) is handled. * fix: register SIGTERM handler once via persistent background task Use OnceLock + tokio::sync::Notify so the signal handler stays active for the process lifetime. Eliminates the race window between loop iterations where a SIGTERM would bypass the handler. * fix: graceful fallback when SIGTERM registration fails Replace expect() with match: if signal(SIGTERM) fails, log a warning and fall back to SIGINT-only. Prevents silent task death that would hang all shutdown_signal() callers indefinitely. * fix: prevent spurious shutdown from ignored ctrl_c errors Use Ok(_) pattern matching in select! branches and expect() for standalone ctrl_c().await calls. Previously .ok() silently swallowed errors, causing notify_waiters() to fire immediately. * fix: handle ctrl_c error in select! to avoid losing SIGINT branch Bind the full Result from ctrl_c() and expect() on it instead of pattern matching Ok(_), which silently dropped the branch on Err. --------- Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"@googleworkspace/cli": patch
|
||||
---
|
||||
|
||||
Handle SIGTERM in `gws gmail +watch` and `gws events +subscribe` for clean container shutdown.
|
||||
|
||||
Long-running pull loops now exit gracefully on SIGTERM (in addition to Ctrl+C),
|
||||
enabling clean shutdown under Kubernetes, Docker, and systemd.
|
||||
@@ -345,8 +345,8 @@ async fn pull_loop(
|
||||
Err(e) => return Err(anyhow::anyhow!("Pub/Sub pull failed: {e}").into()),
|
||||
}
|
||||
}
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
eprintln!("\nReceived interrupt, stopping...");
|
||||
_ = super::super::shutdown_signal() => {
|
||||
eprintln!("\nReceived shutdown signal, stopping...");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
@@ -411,11 +411,11 @@ async fn pull_loop(
|
||||
break;
|
||||
}
|
||||
|
||||
// Check for SIGINT between polls
|
||||
// Check for SIGINT/SIGTERM between polls
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(std::time::Duration::from_secs(config.poll_interval)) => {},
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
eprintln!("\nReceived interrupt, stopping...");
|
||||
_ = super::super::shutdown_signal() => {
|
||||
eprintln!("\nReceived shutdown signal, stopping...");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,8 +288,8 @@ async fn watch_pull_loop(
|
||||
Err(e) => return Err(GwsError::Other(anyhow::anyhow!("Pub/Sub pull failed: {e}"))),
|
||||
}
|
||||
}
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
eprintln!("\nReceived interrupt, stopping...");
|
||||
_ = super::super::shutdown_signal() => {
|
||||
eprintln!("\nReceived shutdown signal, stopping...");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
@@ -348,8 +348,8 @@ async fn watch_pull_loop(
|
||||
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(std::time::Duration::from_secs(config.poll_interval)) => {},
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
eprintln!("\nReceived interrupt, stopping...");
|
||||
_ = super::super::shutdown_signal() => {
|
||||
eprintln!("\nReceived shutdown signal, stopping...");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,63 @@ pub mod workflows;
|
||||
/// is defined in a single place.
|
||||
pub(crate) const PUBSUB_API_BASE: &str = "https://pubsub.googleapis.com/v1";
|
||||
|
||||
/// Returns a future that completes when a shutdown signal is received.
|
||||
///
|
||||
/// On Unix this listens for both SIGINT (Ctrl+C) and SIGTERM; on other
|
||||
/// platforms only SIGINT is handled. Used by long-running pull loops
|
||||
/// (`gmail::watch`, `events::subscribe`) to exit cleanly under container
|
||||
/// orchestrators (Kubernetes, Docker, systemd) that send SIGTERM.
|
||||
///
|
||||
/// The signal handler is registered once in a background task on first call
|
||||
/// so it remains active for the lifetime of the process — no gap between
|
||||
/// loop iterations.
|
||||
pub(crate) async fn shutdown_signal() {
|
||||
use std::sync::OnceLock;
|
||||
use tokio::sync::Notify;
|
||||
|
||||
static NOTIFY: OnceLock<std::sync::Arc<Notify>> = OnceLock::new();
|
||||
|
||||
let notify = NOTIFY.get_or_init(|| {
|
||||
let n = std::sync::Arc::new(Notify::new());
|
||||
let n2 = n.clone();
|
||||
tokio::spawn(async move {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use tokio::signal::unix::{signal, SignalKind};
|
||||
match signal(SignalKind::terminate()) {
|
||||
Ok(mut sigterm) => {
|
||||
tokio::select! {
|
||||
res = tokio::signal::ctrl_c() => {
|
||||
res.expect("failed to listen for SIGINT");
|
||||
}
|
||||
Some(_) = sigterm.recv() => {}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"warning: could not register SIGTERM handler: {e}. \
|
||||
Listening for Ctrl+C only."
|
||||
);
|
||||
tokio::signal::ctrl_c()
|
||||
.await
|
||||
.expect("failed to listen for SIGINT");
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
tokio::signal::ctrl_c()
|
||||
.await
|
||||
.expect("failed to listen for SIGINT");
|
||||
}
|
||||
n2.notify_waiters();
|
||||
});
|
||||
n
|
||||
});
|
||||
|
||||
notify.notified().await;
|
||||
}
|
||||
|
||||
/// A trait for service-specific CLI helpers that inject custom commands.
|
||||
pub trait Helper: Send + Sync {
|
||||
/// Injects subcommands into the service command.
|
||||
|
||||
Reference in New Issue
Block a user