use crate::error::{Error, Result};
use std::future::Future;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use crate::error::aggregate_errors;
use crate::future::BoxFuture;
use crate::shutdown::Shutdowner;
use crate::trace::elapsed;
use crate::trace::emit_unfinished;
use crate::trace::on_start_cancelled;
use crate::trace::on_start_executed;
use crate::trace::on_start_executing;
use crate::trace::on_start_failed;
use crate::trace::on_start_panicked;
use crate::trace::on_stop_cancelled;
use crate::trace::on_stop_executed;
use crate::trace::on_stop_executing;
use crate::trace::on_stop_failed;
use crate::trace::on_stop_panicked;
use crate::trace::start_timer;
use std::fmt;
use std::time::Duration;
type Callback = Box<dyn FnOnce() -> BoxFuture<'static, Result<()>> + Send>;
type StopCallback = Arc<dyn Fn() -> BoxFuture<'static, Result<()>> + Send + Sync>;
pub trait Hook: Send + 'static {
fn name(&self) -> &'static str {
"unnamed"
}
fn has_start(&self) -> bool {
true
}
fn has_stop(&self) -> bool {
true
}
fn on_start(&mut self) -> impl Future<Output = Result<()>> + Send {
async { Ok(()) }
}
fn on_stop(&mut self) -> impl Future<Output = Result<()>> + Send {
async { Ok(()) }
}
fn attach_shutdown(&mut self, _shutdown: Shutdowner) {}
}
pub struct HookFn {
name: &'static str,
on_start: Option<Callback>,
on_stop: Option<StopCallback>,
}
#[must_use]
pub fn hook() -> HookFn {
HookFn::new()
}
impl fmt::Debug for HookFn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HookFn")
.field("name", &self.name)
.field("on_start", &self.on_start.is_some())
.field("on_stop", &self.on_stop.is_some())
.finish()
}
}
impl Default for HookFn {
fn default() -> Self {
Self::new()
}
}
impl HookFn {
#[must_use]
pub fn new() -> Self {
Self {
name: "unnamed",
on_start: None,
on_stop: None,
}
}
#[must_use]
pub fn name(mut self, name: &'static str) -> Self {
debug_assert!(!name.is_empty(), "hook name must not be empty");
self.name = name;
self
}
#[must_use]
pub fn on_start<F, Fut>(mut self, f: F) -> Self
where
F: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = Result<()>> + Send + 'static,
{
self.on_start = Some(Box::new(move || Box::pin(f())));
self
}
#[must_use]
pub fn on_stop<F, Fut>(mut self, f: F) -> Self
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<()>> + Send + 'static,
{
let f = Arc::new(f);
self.on_stop = Some(Arc::new(move || Box::pin(f())));
self
}
}
impl Hook for HookFn {
fn name(&self) -> &'static str {
self.name
}
fn has_start(&self) -> bool {
self.on_start.is_some()
}
fn has_stop(&self) -> bool {
self.on_stop.is_some()
}
fn on_start(&mut self) -> impl Future<Output = Result<()>> + Send {
let f = self.on_start.take();
async move {
match f {
Some(f) => f().await,
None => Ok(()),
}
}
}
fn on_stop(&mut self) -> impl Future<Output = Result<()>> + Send {
let f = self.on_stop.clone();
async move {
match f {
Some(f) => f().await,
None => Ok(()),
}
}
}
}
trait ErasedHook: Send {
fn has_start(&self) -> bool;
fn has_stop(&self) -> bool;
fn on_start(&mut self) -> BoxFuture<'_, Result<()>>;
fn on_stop(&mut self) -> BoxFuture<'_, Result<()>>;
}
impl<H: Hook> ErasedHook for H {
fn has_start(&self) -> bool {
Hook::has_start(self)
}
fn has_stop(&self) -> bool {
Hook::has_stop(self)
}
fn on_start(&mut self) -> BoxFuture<'_, Result<()>> {
Box::pin(Hook::on_start(self))
}
fn on_stop(&mut self) -> BoxFuture<'_, Result<()>> {
Box::pin(Hook::on_stop(self))
}
}
struct HookEntry {
name: &'static str,
inner: Option<Box<dyn ErasedHook>>,
}
struct InflightHook {
lifecycle: Option<Lifecycle>,
idx: usize,
name: &'static str,
finished: bool,
}
impl InflightHook {
fn start(lifecycle: Lifecycle, idx: usize, name: &'static str) -> Self {
on_start_executing(idx, name);
Self {
lifecycle: Some(lifecycle),
idx,
name,
finished: false,
}
}
fn ok(&mut self, runtime: Duration) {
self.finished = true;
on_start_executed(self.idx, self.name, runtime);
}
fn fail(&mut self, err: &Error) {
self.finished = true;
on_start_failed(self.idx, self.name, err);
}
}
impl Drop for InflightHook {
fn drop(&mut self) {
emit_unfinished(
self.finished,
|| on_start_panicked(self.idx, self.name),
|| on_start_cancelled(self.idx, self.name),
);
if !self.finished {
if let Some(lc) = self.lifecycle.take() {
lc.prepare_for_unwind();
}
}
}
}
struct StopGuard {
lifecycle: Lifecycle,
idx: usize,
name: &'static str,
hook: Option<Box<dyn ErasedHook>>,
finished: bool,
}
impl StopGuard {
fn new(
lifecycle: Lifecycle,
idx: usize,
name: &'static str,
hook: Box<dyn ErasedHook>,
) -> Self {
on_stop_executing(idx, name);
Self {
lifecycle,
idx,
name,
hook: Some(hook),
finished: false,
}
}
async fn run(mut self) -> Result<()> {
let started_at = start_timer();
let result = {
let hook = self.hook.as_mut().expect("hook present");
match hook.on_stop().await {
Ok(()) => Ok(()),
Err(err) => Err(err.with_hook_name(self.name)),
}
};
match &result {
Ok(()) => on_stop_executed(self.idx, self.name, elapsed(started_at)),
Err(err) => on_stop_failed(self.idx, self.name, err),
}
self.finished = true;
self.hook = None;
result
}
}
impl Drop for StopGuard {
fn drop(&mut self) {
if !self.finished {
if let Some(hook) = self.hook.take() {
let mut state = self.lifecycle.state();
state.hooks[self.idx].inner = Some(hook);
state.started += 1;
}
emit_unfinished(
false,
|| on_stop_panicked(self.idx, self.name),
|| on_stop_cancelled(self.idx, self.name),
);
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Phase {
Registering,
Starting,
Started,
Stopping,
}
struct State {
hooks: Vec<HookEntry>,
started: usize,
phase: Phase,
}
#[derive(Clone)]
pub struct Lifecycle {
inner: Arc<Mutex<State>>,
shutdown: Shutdowner,
}
impl fmt::Debug for Lifecycle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let state = self.state();
f.debug_struct("Lifecycle")
.field("phase", &state.phase)
.field("hooks", &state.hooks.len())
.field("started", &state.started)
.finish()
}
}
impl Lifecycle {
pub(crate) fn new(shutdown: Shutdowner) -> Self {
Self {
inner: Arc::new(Mutex::new(State {
hooks: Vec::new(),
started: 0,
phase: Phase::Registering,
})),
shutdown,
}
}
fn state(&self) -> MutexGuard<'_, State> {
self.inner.lock().unwrap_or_else(PoisonError::into_inner)
}
pub fn append<H: Hook>(&self, mut hook: H) -> Result<()> {
hook.attach_shutdown(self.shutdown.clone());
debug_assert!(!hook.name().is_empty(), "hook name must not be empty");
let mut state = self.state();
match state.phase {
Phase::Registering | Phase::Starting => {
state.hooks.push(HookEntry {
name: hook.name(),
inner: Some(Box::new(hook)),
});
Ok(())
}
Phase::Started => Err(Error::AppendAfterStart),
Phase::Stopping => Err(Error::AppendWhileStopping),
}
}
pub(crate) async fn start(&self) -> Result<()> {
{
let mut state = self.state();
state.phase = Phase::Starting;
}
self.start_inner().await
}
async fn start_inner(&self) -> Result<()> {
loop {
let start = {
let mut state = self.state();
loop {
if state.started >= state.hooks.len() {
state.phase = Phase::Started;
break None;
}
let idx = state.started;
let skip = match state.hooks[idx].inner.as_ref() {
Some(hook) => !hook.has_start(),
None => true,
};
if skip {
state.started += 1;
continue;
}
let hook = state.hooks[idx]
.inner
.take()
.expect("hook with OnStart present");
let name = state.hooks[idx].name;
break Some((idx, name, hook));
}
};
let Some((idx, name, mut hook)) = start else {
return Ok(());
};
let mut inflight = InflightHook::start(self.clone(), idx, name);
let started_at = start_timer();
match hook.on_start().await {
Ok(()) => {
inflight.ok(elapsed(started_at));
let mut state = self.state();
state.hooks[idx].inner = Some(hook);
state.started += 1;
}
Err(err) => {
let err = err.with_hook_name(name);
inflight.fail(&err);
self.prepare_for_unwind();
return Err(err);
}
}
}
}
pub(crate) fn prepare_for_unwind(&self) {
let mut state = self.state();
state.phase = Phase::Stopping;
while state.started < state.hooks.len() {
match state.hooks[state.started].inner.as_ref() {
None => {
state.started += 1;
}
Some(hook) if !hook.has_start() => {
state.started += 1;
}
Some(_) => {
let idx = state.started;
state.hooks[idx].inner = None;
state.started += 1;
}
}
}
}
pub(crate) fn pending_stops(&self) -> usize {
self.state().started
}
pub(crate) async fn unwind_started(&self) -> Result<()> {
let started = {
let mut state = self.state();
state.phase = Phase::Stopping;
state.started
};
self.stop_first(started).await
}
pub(crate) async fn stop(&self) -> Result<()> {
{
let mut state = self.state();
state.phase = Phase::Stopping;
}
let mut errors = Vec::new();
while let Some((idx, name, hook)) = self.take_next_stop() {
match StopGuard::new(self.clone(), idx, name, hook).run().await {
Ok(()) => {}
Err(err) => errors.push(err),
}
}
aggregate_errors(errors)
}
async fn stop_first(&self, count: usize) -> Result<()> {
let mut errors = Vec::new();
for _ in 0..count {
if let Some((idx, name, hook)) = self.take_next_stop() {
match StopGuard::new(self.clone(), idx, name, hook).run().await {
Ok(()) => {}
Err(err) => errors.push(err),
}
} else {
break;
}
}
aggregate_errors(errors)
}
fn take_next_stop(&self) -> Option<(usize, &'static str, Box<dyn ErasedHook>)> {
let mut state = self.state();
loop {
if state.started == 0 {
return None;
}
state.started -= 1;
let idx = state.started;
let Some(hook) = state.hooks[idx].inner.take() else {
continue;
};
if !hook.has_stop() {
continue;
}
return Some((idx, state.hooks[idx].name, hook));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shutdown::Shutdowner;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::time::sleep;
#[tokio::test]
async fn stop_guard_writes_back_cancelled_on_stop() {
let lc = Lifecycle::new(Shutdowner::new());
let log = Arc::new(Mutex::new(Vec::new()));
let l = Arc::clone(&log);
let l2 = Arc::clone(&l);
lc.append(hook().on_stop(move || {
let l = Arc::clone(&l2);
async move {
l.lock().unwrap().push("begin");
sleep(Duration::from_millis(200)).await;
l.lock().unwrap().push("end");
Ok(())
}
}))
.unwrap();
lc.prepare_for_unwind();
let lc2 = lc.clone();
tokio::select! {
_ = lc.unwind_started() => {}
_ = tokio::time::sleep(Duration::from_millis(20)) => {}
}
assert_eq!(log.lock().unwrap().as_slice(), ["begin"]);
lc2.unwind_started().await.unwrap();
assert_eq!(log.lock().unwrap().as_slice(), ["begin", "begin", "end"]);
}
#[tokio::test]
async fn prepare_for_unwind_runs_stop_only_after_unstarted_hooks() {
let lc = Lifecycle::new(Shutdowner::new());
let log = Arc::new(Mutex::new(Vec::new()));
let started = Arc::clone(&log);
let stopped = Arc::clone(&log);
lc.append(
hook()
.on_start(move || {
let started = Arc::clone(&started);
async move {
started.lock().unwrap().push("start");
Ok(())
}
})
.on_stop(move || {
let stopped = Arc::clone(&stopped);
async move {
stopped.lock().unwrap().push("stop-started");
Ok(())
}
}),
)
.unwrap();
let only = Arc::clone(&log);
lc.append(hook().on_stop(move || {
let only = Arc::clone(&only);
async move {
only.lock().unwrap().push("stop-only");
Ok(())
}
}))
.unwrap();
lc.prepare_for_unwind();
lc.unwind_started().await.unwrap();
assert_eq!(log.lock().unwrap().as_slice(), ["stop-only"]);
}
#[tokio::test]
async fn failed_start_runs_stop_only_after_unstarted_hooks() {
let lc = Lifecycle::new(Shutdowner::new());
let log = Arc::new(Mutex::new(Vec::new()));
lc.append(hook().on_start(|| async { Err(Error::hook("boom")) }))
.unwrap();
lc.append(hook().on_start(|| async {
panic!("later start hook must not run after failed start");
}))
.unwrap();
let only = Arc::clone(&log);
lc.append(hook().on_stop(move || {
let only = Arc::clone(&only);
async move {
only.lock().unwrap().push("stop-only");
Ok(())
}
}))
.unwrap();
let err = lc.start().await.unwrap_err();
assert!(format!("{err}").contains("boom"), "unexpected: {err}");
lc.unwind_started().await.unwrap();
assert_eq!(log.lock().unwrap().as_slice(), ["stop-only"]);
}
}