#![cfg(any(unix, windows))]
use std::marker::PhantomData;
use std::time::Duration;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GracefulShutdown {
#[cfg(unix)]
pub unix: UnixGracefulShutdown,
#[cfg(windows)]
pub windows: WindowsGracefulShutdown,
}
impl GracefulShutdown {
#[must_use]
pub fn builder() -> GracefulShutdownBuilder<UnixUnset> {
GracefulShutdownBuilder {
#[cfg(unix)]
unix: None,
#[cfg(windows)]
windows: WindowsGracefulShutdown {
timeout: Duration::ZERO,
},
_state: PhantomData,
}
}
}
#[doc(hidden)]
#[derive(Debug, Clone, Copy)]
pub struct UnixUnset;
#[doc(hidden)]
#[derive(Debug, Clone, Copy)]
pub struct UnixSet;
#[doc(hidden)]
#[derive(Debug, Clone, Copy)]
pub struct BothSet;
#[derive(Debug, Clone)]
pub struct GracefulShutdownBuilder<State> {
#[cfg(unix)]
unix: Option<UnixGracefulShutdown>,
#[cfg(windows)]
windows: WindowsGracefulShutdown,
_state: PhantomData<fn() -> State>,
}
impl GracefulShutdownBuilder<UnixUnset> {
#[must_use]
#[cfg_attr(not(unix), allow(clippy::needless_pass_by_value))]
pub fn unix(self, sequence: UnixGracefulShutdown) -> GracefulShutdownBuilder<UnixSet> {
#[cfg(not(unix))]
let _ = sequence;
GracefulShutdownBuilder {
#[cfg(unix)]
unix: Some(sequence),
#[cfg(windows)]
windows: self.windows,
_state: PhantomData,
}
}
#[must_use]
pub fn unix_sigterm(self, timeout: Duration) -> GracefulShutdownBuilder<UnixSet> {
#[cfg(not(unix))]
let _ = timeout;
GracefulShutdownBuilder {
#[cfg(unix)]
unix: Some(UnixGracefulShutdown::terminate_only(timeout)),
#[cfg(windows)]
windows: self.windows,
_state: PhantomData,
}
}
#[must_use]
pub fn unix_sigint(self, timeout: Duration) -> GracefulShutdownBuilder<UnixSet> {
#[cfg(not(unix))]
let _ = timeout;
GracefulShutdownBuilder {
#[cfg(unix)]
unix: Some(UnixGracefulShutdown::interrupt_only(timeout)),
#[cfg(windows)]
windows: self.windows,
_state: PhantomData,
}
}
}
impl GracefulShutdownBuilder<UnixSet> {
#[must_use]
#[cfg_attr(not(windows), allow(clippy::needless_pass_by_value))]
pub fn windows(self, sequence: WindowsGracefulShutdown) -> GracefulShutdownBuilder<BothSet> {
#[cfg(not(windows))]
let _ = sequence;
GracefulShutdownBuilder {
#[cfg(unix)]
unix: self.unix,
#[cfg(windows)]
windows: sequence,
_state: PhantomData,
}
}
#[must_use]
pub fn windows_ctrl_break(self, timeout: Duration) -> GracefulShutdownBuilder<BothSet> {
#[cfg(not(windows))]
let _ = timeout;
GracefulShutdownBuilder {
#[cfg(unix)]
unix: self.unix,
#[cfg(windows)]
windows: WindowsGracefulShutdown::new(timeout),
_state: PhantomData,
}
}
}
impl GracefulShutdownBuilder<BothSet> {
#[must_use]
pub fn build(self) -> GracefulShutdown {
GracefulShutdown {
#[cfg(unix)]
unix: self
.unix
.expect("UnixGracefulShutdown must be set before build()"),
#[cfg(windows)]
windows: self.windows,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum UnixGracefulSignal {
Interrupt,
Terminate,
}
#[cfg(unix)]
impl UnixGracefulSignal {
pub(crate) fn label(self) -> &'static str {
match self {
Self::Interrupt => "SIGINT",
Self::Terminate => "SIGTERM",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UnixGracefulPhase {
pub signal: UnixGracefulSignal,
pub timeout: Duration,
}
impl UnixGracefulPhase {
#[must_use]
pub const fn interrupt(timeout: Duration) -> Self {
Self {
signal: UnixGracefulSignal::Interrupt,
timeout,
}
}
#[must_use]
pub const fn terminate(timeout: Duration) -> Self {
Self {
signal: UnixGracefulSignal::Terminate,
timeout,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnixGracefulShutdown {
phases: Vec<UnixGracefulPhase>,
}
impl UnixGracefulShutdown {
#[must_use]
pub fn terminate_only(timeout: Duration) -> Self {
Self::single(UnixGracefulPhase::terminate(timeout))
}
#[must_use]
pub fn interrupt_only(timeout: Duration) -> Self {
Self::single(UnixGracefulPhase::interrupt(timeout))
}
#[must_use]
pub(crate) fn single(phase: UnixGracefulPhase) -> Self {
Self {
phases: vec![phase],
}
}
#[must_use]
pub fn from_phases(phases: impl IntoIterator<Item = UnixGracefulPhase>) -> Self {
let phases: Vec<_> = phases.into_iter().collect();
assert!(
!phases.is_empty(),
"UnixGracefulShutdown must contain at least one phase",
);
Self { phases }
}
#[must_use]
pub fn phases(&self) -> &[UnixGracefulPhase] {
&self.phases
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WindowsGracefulShutdown {
pub timeout: Duration,
}
impl WindowsGracefulShutdown {
#[must_use]
pub const fn new(timeout: Duration) -> Self {
Self { timeout }
}
}
#[cfg(test)]
mod tests {
use super::*;
use assertr::prelude::*;
mod builder {
use super::*;
#[test]
fn populates_unix_terminate_only_phase() {
let shutdown = GracefulShutdown::builder()
.unix(UnixGracefulShutdown::terminate_only(Duration::from_secs(5)))
.windows(WindowsGracefulShutdown::new(Duration::from_secs(7)))
.build();
#[cfg(unix)]
{
let phases = shutdown.unix.phases();
assert_that!(phases.len()).is_equal_to(1);
assert_that!(phases[0].timeout).is_equal_to(Duration::from_secs(5));
}
#[cfg(windows)]
{
assert_that!(shutdown.windows.timeout).is_equal_to(Duration::from_secs(7));
}
}
#[test]
fn uses_interrupt_signal_for_unix_interrupt_only() {
let shutdown = GracefulShutdown::builder()
.unix(UnixGracefulShutdown::interrupt_only(Duration::from_secs(3)))
.windows(WindowsGracefulShutdown::new(Duration::from_secs(7)))
.build();
#[cfg(unix)]
{
let phases = shutdown.unix.phases();
assert_that!(phases.len()).is_equal_to(1);
assert_that!(phases[0].timeout).is_equal_to(Duration::from_secs(3));
}
#[cfg(windows)]
{
assert_that!(shutdown.windows.timeout).is_equal_to(Duration::from_secs(7));
}
}
}
#[cfg(unix)]
mod unix_sequence {
use super::*;
use crate::{UnixGracefulPhase, UnixGracefulSignal};
#[test]
fn from_phases_preserves_iteration_order() {
let sequence = UnixGracefulShutdown::from_phases([
UnixGracefulPhase::interrupt(Duration::from_secs(1)),
UnixGracefulPhase::terminate(Duration::from_secs(2)),
]);
let phases = sequence.phases();
assert_that!(phases.len()).is_equal_to(2);
assert_that!(phases[0].signal).is_equal_to(UnixGracefulSignal::Interrupt);
assert_that!(phases[0].timeout).is_equal_to(Duration::from_secs(1));
assert_that!(phases[1].signal).is_equal_to(UnixGracefulSignal::Terminate);
assert_that!(phases[1].timeout).is_equal_to(Duration::from_secs(2));
}
#[test]
#[should_panic(expected = "UnixGracefulShutdown must contain at least one phase")]
fn from_phases_panics_on_empty_input() {
let _ = UnixGracefulShutdown::from_phases(std::iter::empty());
}
}
}