use std::fmt;
use std::{future::Future, sync::Arc, time::Duration};
#[cfg(unix)]
use tokio::signal::unix::{SignalKind, signal};
use tokio::sync::watch;
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use tokio_util::task::TaskTracker;
use tracing::{debug, info, warn};
use crate::runtime::failure::ErrorShutdown;
use crate::runtime::lifecycle::{BoxError, BoxFuture, ConnectedLifecycle};
use crate::runtime::publish_source::Bound;
use crate::{Broker, Connected, PairError, PublishPolicy};
use super::health::{self, HealthProbe, HealthState};
use super::service::RegisteredBroker;
use super::{LifecycleHook, RustStream, RustStreamError};
pub(crate) struct ConnectedEntry {
pub(crate) lifecycle: Box<dyn ConnectedLifecycle>,
pub(crate) label: Option<String>,
}
type BoundHook = Box<dyn FnOnce() -> BoxFuture<'static, Result<(), BoxError>> + Send>;
fn bind_hooks<State: Send + Sync + 'static>(
hooks: Vec<LifecycleHook<State>>,
state: &Arc<State>,
) -> Vec<BoundHook> {
hooks
.into_iter()
.map(|hook| {
let state = Arc::clone(state);
Box::new(move || hook(state)) as BoundHook
})
.collect()
}
impl<Layers: Send, State: Send + Sync + 'static, Pipeline, Phase>
RustStream<Layers, State, Pipeline, Phase>
{
pub async fn run(self) -> Result<(), RustStreamError> {
self.run_until(wait_for_signal()).await
}
pub async fn run_until<F>(self, shutdown: F) -> Result<(), RustStreamError>
where
F: Future<Output = ()> + Send,
{
let running = self.start().await?;
tokio::select! {
() = shutdown => info!(target: "ruststream::lifecycle", "shutdown signal received"),
() = running.stopping() => {
info!(target: "ruststream::lifecycle", "fail-fast shutdown triggered");
}
}
running.shutdown().await
}
pub async fn start(self) -> Result<RunningApp, RustStreamError> {
let Self {
info,
brokers,
starters,
handlers,
state_init,
after_startup,
on_shutdown,
after_shutdown,
shutdown_timeout,
continuations,
..
} = self;
info!(
target: "ruststream::lifecycle",
service = %info.title,
version = %info.version,
brokers = brokers.len(),
subscribers = starters.len(),
"starting service",
);
debug!(target: "ruststream::lifecycle", "producing application state");
let state = state_init().await.map_err(RustStreamError::Startup)?;
let state = Arc::new(state);
let mut connected = Vec::with_capacity(brokers.len());
for RegisteredBroker { lifecycle, label } in brokers {
let lifecycle = match lifecycle.connect().await {
Ok(lifecycle) => lifecycle,
Err(err) => {
unwind_connected(connected).await;
return Err(RustStreamError::Connect(err));
}
};
info!(
target: "ruststream::lifecycle",
broker = label.as_deref().unwrap_or_else(|| lifecycle.name()),
"broker connected",
);
connected.push(ConnectedEntry { lifecycle, label });
}
let token = CancellationToken::new();
let error_shutdown = ErrorShutdown::new(token.clone());
let mut handles = Vec::with_capacity(starters.len());
for (starter, meta) in starters.into_iter().zip(handlers) {
let handle = match starter(state.clone(), error_shutdown.clone(), token.clone()).await {
Ok(handle) => handle,
Err(err) => {
unwind_started(&token, handles, shutdown_timeout, connected, continuations)
.await;
return Err(RustStreamError::Subscribe(err));
}
};
info!(
target: "ruststream::dispatch",
subscriber = %meta.name,
input = meta.input_type,
"subscriber started",
);
handles.push(handle);
}
if !after_startup.is_empty() {
debug!(target: "ruststream::lifecycle", count = after_startup.len(), "running after_startup hooks");
}
for hook in after_startup {
if let Err(err) = hook(Arc::clone(&state)).await {
unwind_started(&token, handles, shutdown_timeout, connected, continuations).await;
return Err(RustStreamError::Startup(err));
}
}
info!(target: "ruststream::lifecycle", subscribers = handles.len(), "service running");
let health = health::channel();
{
let health = health.clone();
let error_shutdown = error_shutdown.clone();
let token = token.clone();
tokio::spawn(async move {
token.cancelled().await;
if let Some(reason) = error_shutdown.peek_failure() {
health.send_replace(HealthState::Failed { reason });
}
});
}
Ok(RunningApp {
token,
error_shutdown,
handles,
on_shutdown: bind_hooks(on_shutdown, &state),
after_shutdown: bind_hooks(after_shutdown, &state),
brokers: connected,
shutdown_timeout,
continuations,
health,
})
}
}
#[must_use = "dropping the handle detaches the service without graceful shutdown"]
pub struct RunningApp {
token: CancellationToken,
error_shutdown: ErrorShutdown,
handles: Vec<JoinHandle<()>>,
on_shutdown: Vec<BoundHook>,
after_shutdown: Vec<BoundHook>,
brokers: Vec<ConnectedEntry>,
shutdown_timeout: Option<Duration>,
continuations: TaskTracker,
health: watch::Sender<HealthState>,
}
impl fmt::Debug for RunningApp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RunningApp")
.field("subscribers", &self.handles.len())
.field("brokers", &self.brokers.len())
.field("shutdown_timeout", &self.shutdown_timeout)
.finish_non_exhaustive()
}
}
impl RunningApp {
pub fn stopping(&self) -> impl Future<Output = ()> + Send + 'static {
self.token.clone().cancelled_owned()
}
#[must_use]
pub fn health(&self) -> HealthProbe {
HealthProbe::new(self.health.subscribe())
}
pub fn publisher<B2, S>(
&self,
token: Bound<B2, S>,
) -> impl Future<Output = Result<S::Live, PairError>> + Send
where
B2: Broker + 'static,
S: PublishPolicy<Connected<B2>> + Send,
{
token.live()
}
pub async fn shutdown(self) -> Result<(), RustStreamError> {
let Self {
token,
error_shutdown,
handles,
on_shutdown,
after_shutdown,
brokers,
shutdown_timeout,
continuations,
health,
} = self;
health.send_replace(HealthState::ShuttingDown);
let outcome = teardown(
token,
handles,
on_shutdown,
after_shutdown,
brokers,
shutdown_timeout,
continuations,
)
.await;
match outcome {
Ok(()) => {
if let Some(reason) = error_shutdown.taken_failure() {
health.send_replace(HealthState::Failed {
reason: reason.clone(),
});
return Err(RustStreamError::Dispatch(reason));
}
health.send_replace(HealthState::Stopped);
Ok(())
}
Err(err) => {
health.send_replace(HealthState::Failed {
reason: err.to_string(),
});
Err(err)
}
}
}
}
async fn unwind_started(
token: &CancellationToken,
handles: Vec<JoinHandle<()>>,
shutdown_timeout: Option<Duration>,
brokers: Vec<ConnectedEntry>,
continuations: TaskTracker,
) {
token.cancel();
if let Err(err) = drain_handles(handles, shutdown_timeout).await {
warn!(
target: "ruststream::lifecycle",
error = %err,
"draining dispatch tasks failed during the startup unwind",
);
}
drain_continuations(continuations, shutdown_timeout).await;
unwind_connected(brokers).await;
}
async fn unwind_connected(brokers: Vec<ConnectedEntry>) {
for ConnectedEntry { lifecycle, label } in brokers.into_iter().rev() {
let name = lifecycle.name();
match lifecycle.shutdown().await {
Ok(()) => debug!(
target: "ruststream::lifecycle",
broker = label.as_deref().unwrap_or(name),
"broker shut down after a startup failure",
),
Err(err) => warn!(
target: "ruststream::lifecycle",
broker = label.as_deref().unwrap_or(name),
error = %err,
"broker shutdown failed during the startup unwind",
),
}
}
}
async fn teardown(
token: CancellationToken,
handles: Vec<JoinHandle<()>>,
on_shutdown: Vec<BoundHook>,
after_shutdown: Vec<BoundHook>,
brokers: Vec<ConnectedEntry>,
shutdown_timeout: Option<Duration>,
continuations: TaskTracker,
) -> Result<(), RustStreamError> {
for hook in on_shutdown {
if let Err(err) = hook().await {
warn!(target: "ruststream::lifecycle", error = %err, "on_shutdown hook failed");
}
}
token.cancel();
debug!(target: "ruststream::lifecycle", "draining in-flight handlers");
drain_handles(handles, shutdown_timeout).await?;
drain_continuations(continuations, shutdown_timeout).await;
for ConnectedEntry { lifecycle, label } in brokers.into_iter().rev() {
let name = lifecycle.name();
lifecycle
.shutdown()
.await
.map_err(RustStreamError::Shutdown)?;
debug!(
target: "ruststream::lifecycle",
broker = label.as_deref().unwrap_or(name),
"broker shut down",
);
}
for hook in after_shutdown {
if let Err(err) = hook().await {
warn!(target: "ruststream::lifecycle", error = %err, "after_shutdown hook failed");
}
}
info!(target: "ruststream::lifecycle", "service stopped");
Ok(())
}
async fn drain_handles(
handles: Vec<JoinHandle<()>>,
timeout: Option<Duration>,
) -> Result<(), RustStreamError> {
let Some(timeout) = timeout else {
for handle in handles {
handle.await.map_err(RustStreamError::Join)?;
}
return Ok(());
};
let aborts: Vec<_> = handles.iter().map(JoinHandle::abort_handle).collect();
if tokio::time::timeout(timeout, futures::future::join_all(handles))
.await
.is_err()
{
warn!(
target: "ruststream::lifecycle",
"graceful shutdown timed out; aborting in-flight handlers",
);
for abort in aborts {
abort.abort();
}
}
Ok(())
}
async fn drain_continuations(continuations: TaskTracker, timeout: Option<Duration>) {
continuations.close();
if continuations.is_empty() {
return;
}
debug!(target: "ruststream::lifecycle", "draining post-settle continuations");
match timeout {
Some(timeout) => {
if tokio::time::timeout(timeout, continuations.wait())
.await
.is_err()
{
warn!(
target: "ruststream::lifecycle",
"graceful shutdown timed out; abandoning in-flight continuations",
);
}
}
None => continuations.wait().await,
}
}
async fn wait_for_signal() {
#[cfg(unix)]
{
let Ok(mut term) = signal(SignalKind::terminate()) else {
let _ = tokio::signal::ctrl_c().await;
return;
};
tokio::select! {
_ = tokio::signal::ctrl_c() => {}
_ = term.recv() => {}
}
}
#[cfg(not(unix))]
{
let _ = tokio::signal::ctrl_c().await;
}
}