WIP: recover runtime implementation after accidental git clean
This commit is contained in:
1 parent
5abbf5123e
commit
6a04d7f793
10 files changed
+563
No files matched your search
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,13 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||
pub struct RefreshToken {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
#[serde(skip_serializing)]
|
||||
pub token_hash: String,
|
||||
pub expires_at: String,
|
||||
pub created_at: String,
|
||||
pub revoked: bool,
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
//! Runtime-wide shutdown coordination using `CancellationToken`.
|
||||
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ShutdownCoordinator {
|
||||
root: CancellationToken,
|
||||
}
|
||||
|
||||
impl ShutdownCoordinator {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
root: CancellationToken::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn token(&self) -> &CancellationToken {
|
||||
&self.root
|
||||
}
|
||||
|
||||
pub fn child_token(&self) -> CancellationToken {
|
||||
self.root.child_token()
|
||||
}
|
||||
|
||||
pub fn cancel(&self) {
|
||||
self.root.cancel();
|
||||
}
|
||||
|
||||
pub fn is_cancelled(&self) -> bool {
|
||||
self.root.is_cancelled()
|
||||
}
|
||||
|
||||
pub async fn cancelled(&self) {
|
||||
self.root.cancelled().await;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ShutdownCoordinator {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
//! Prioritized, idempotent shutdown hooks.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum ShutdownPriority {
|
||||
First = 0,
|
||||
Normal = 1,
|
||||
Last = 2,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait ShutdownHook: Send + Sync {
|
||||
fn name(&self) -> &'static str;
|
||||
|
||||
fn priority(&self) -> ShutdownPriority {
|
||||
ShutdownPriority::Normal
|
||||
}
|
||||
|
||||
async fn shutdown(&self) -> Result<()>;
|
||||
}
|
||||
|
||||
pub struct HookRegistry {
|
||||
hooks: Vec<Box<dyn ShutdownHook>>,
|
||||
}
|
||||
|
||||
impl HookRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self { hooks: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn register(&mut self, hook: Box<dyn ShutdownHook>) {
|
||||
tracing::debug!(hook = hook.name(), priority = ?hook.priority(), "shutdown hook registered");
|
||||
self.hooks.push(hook);
|
||||
}
|
||||
|
||||
pub async fn execute_all(&self) {
|
||||
if self.hooks.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut indices: Vec<usize> = (0..self.hooks.len()).collect();
|
||||
indices.sort_by_key(|&i| self.hooks[i].priority());
|
||||
|
||||
for i in indices {
|
||||
let hook = &self.hooks[i];
|
||||
let start = std::time::Instant::now();
|
||||
tracing::info!(hook = hook.name(), priority = ?hook.priority(), "executing shutdown hook");
|
||||
match hook.shutdown().await {
|
||||
Ok(()) => {
|
||||
tracing::info!(
|
||||
hook = hook.name(),
|
||||
duration_ms = start.elapsed().as_millis(),
|
||||
"shutdown hook completed successfully"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
hook = hook.name(),
|
||||
duration_ms = start.elapsed().as_millis(),
|
||||
error = %e,
|
||||
"shutdown hook failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.hooks.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.hooks.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for HookRegistry {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//! Common lifecycle contract for runtime components.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
/// Lifecycle contract for the application runtime.
|
||||
#[async_trait::async_trait]
|
||||
pub trait Lifecycle {
|
||||
/// Initialize subsystems.
|
||||
async fn initialize(&mut self) -> Result<()>;
|
||||
|
||||
/// Start serving.
|
||||
async fn start(&mut self) -> Result<()>;
|
||||
|
||||
/// Perform graceful shutdown.
|
||||
async fn shutdown(&mut self) -> Result<()>;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//! Unified Enterprise Runtime Lifecycle for `nx9-auth`.
|
||||
//!
|
||||
//! Provides application assembly (`ApplicationBuilder`), lifecycle management
|
||||
//! (`Application`, `Lifecycle`), atomic state machine (`RuntimeState`),
|
||||
//! signal coordination (`SignalManager`), prioritized shutdown hooks
|
||||
//! (`ShutdownHook`), worker management (`WorkerManager`), and operational
|
||||
//! metrics (`RuntimeMetrics`).
|
||||
|
||||
pub mod application;
|
||||
pub mod builder;
|
||||
pub mod cancellation;
|
||||
pub mod hooks;
|
||||
pub mod lifecycle;
|
||||
pub mod metrics;
|
||||
pub mod signals;
|
||||
pub mod state;
|
||||
pub mod workers;
|
||||
|
||||
pub use application::Application;
|
||||
pub use builder::ApplicationBuilder;
|
||||
pub use cancellation::ShutdownCoordinator;
|
||||
pub use hooks::{HookRegistry, ShutdownHook, ShutdownPriority};
|
||||
pub use lifecycle::Lifecycle;
|
||||
pub use metrics::RuntimeMetrics;
|
||||
pub use signals::SignalManager;
|
||||
pub use state::{AtomicRuntimeState, RuntimeState};
|
||||
pub use workers::{TaskGroup, WorkerManager};
|
||||
@@ -0,0 +1,67 @@
|
||||
//! Unix signal handling for graceful and forced shutdown.
|
||||
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SignalManager {
|
||||
signal_count: Arc<AtomicUsize>,
|
||||
force_shutdown: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl SignalManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
signal_count: Arc::new(AtomicUsize::new(0)),
|
||||
force_shutdown: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_force_shutdown(&self) -> bool {
|
||||
self.force_shutdown.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
pub fn signal_count(&self) -> usize {
|
||||
self.signal_count.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
pub fn record_signal(&self) -> usize {
|
||||
let count = self.signal_count.fetch_add(1, Ordering::AcqRel) + 1;
|
||||
if count >= 2 {
|
||||
self.force_shutdown.store(true, Ordering::Release);
|
||||
}
|
||||
count
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SignalManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn wait_for_shutdown_signal() -> &'static str {
|
||||
let ctrl_c = async {
|
||||
tokio::signal::ctrl_c()
|
||||
.await
|
||||
.expect("failed to install SIGINT handler");
|
||||
"SIGINT"
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
let sigterm = async {
|
||||
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
||||
.expect("failed to install SIGTERM handler")
|
||||
.recv()
|
||||
.await;
|
||||
"SIGTERM"
|
||||
};
|
||||
|
||||
#[cfg(not(unix))]
|
||||
let sigterm = std::future::pending::<&str>();
|
||||
|
||||
tokio::select! {
|
||||
name = ctrl_c => name,
|
||||
name = sigterm => name,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
//! Lock-free atomic runtime state machine.
|
||||
//!
|
||||
//! Tracks the application through granular lifecycle phases using `AtomicU8`
|
||||
//! with `compare_exchange` transitions. This avoids mutex contention and
|
||||
//! provides deterministic, race-free state management across async tasks.
|
||||
|
||||
use std::fmt;
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
|
||||
/// Granular runtime lifecycle states for enterprise production observability.
|
||||
///
|
||||
/// Each transition is deterministic and logged. Only forward transitions
|
||||
/// are permitted during normal operation; the state machine never moves
|
||||
/// backward.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[repr(u8)]
|
||||
pub enum RuntimeState {
|
||||
/// Runtime is being configured and dependencies are being assembled.
|
||||
Initializing = 0,
|
||||
/// Subsystems are starting (database, HTTP listener, workers).
|
||||
Starting = 1,
|
||||
/// Application is fully operational and serving requests.
|
||||
Running = 2,
|
||||
/// Shutdown signal received; HTTP listener stopped, draining active requests.
|
||||
Draining = 3,
|
||||
/// Active requests drained; cancelling and awaiting background workers.
|
||||
StoppingWorkers = 4,
|
||||
/// Workers stopped; executing registered shutdown hooks.
|
||||
ExecutingHooks = 5,
|
||||
/// Hooks executed; closing database connection pools and flushing logs.
|
||||
ClosingResources = 6,
|
||||
/// All resources released; process is ready to exit.
|
||||
Stopped = 7,
|
||||
}
|
||||
|
||||
impl RuntimeState {
|
||||
/// Convert a raw `u8` value back into a `RuntimeState`.
|
||||
fn from_u8(val: u8) -> Option<Self> {
|
||||
match val {
|
||||
0 => Some(Self::Initializing),
|
||||
1 => Some(Self::Starting),
|
||||
2 => Some(Self::Running),
|
||||
3 => Some(Self::Draining),
|
||||
4 => Some(Self::StoppingWorkers),
|
||||
5 => Some(Self::ExecutingHooks),
|
||||
6 => Some(Self::ClosingResources),
|
||||
7 => Some(Self::Stopped),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the runtime is in any shutdown phase.
|
||||
pub fn is_shutting_down(self) -> bool {
|
||||
(self as u8) >= (Self::Draining as u8)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for RuntimeState {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Initializing => write!(f, "Initializing"),
|
||||
Self::Starting => write!(f, "Starting"),
|
||||
Self::Running => write!(f, "Running"),
|
||||
Self::Draining => write!(f, "Draining"),
|
||||
Self::StoppingWorkers => write!(f, "StoppingWorkers"),
|
||||
Self::ExecutingHooks => write!(f, "ExecutingHooks"),
|
||||
Self::ClosingResources => write!(f, "ClosingResources"),
|
||||
Self::Stopped => write!(f, "Stopped"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Lock-free atomic runtime state container.
|
||||
///
|
||||
/// Uses `AtomicU8` with `compare_exchange` to ensure deterministic,
|
||||
/// race-free state transitions without mutex contention.
|
||||
pub struct AtomicRuntimeState {
|
||||
state: AtomicU8,
|
||||
}
|
||||
|
||||
impl AtomicRuntimeState {
|
||||
/// Create a new state machine in the `Initializing` state.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: AtomicU8::new(RuntimeState::Initializing as u8),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the current state (acquire ordering for visibility).
|
||||
pub fn load(&self) -> RuntimeState {
|
||||
RuntimeState::from_u8(self.state.load(Ordering::Acquire))
|
||||
.unwrap_or(RuntimeState::Stopped)
|
||||
}
|
||||
|
||||
/// Attempt an atomic state transition from `expected` to `new`.
|
||||
///
|
||||
/// Returns `Ok(new)` if the transition succeeded, or `Err(actual)` if the
|
||||
/// current state did not match `expected`.
|
||||
pub fn transition(
|
||||
&self,
|
||||
expected: RuntimeState,
|
||||
new: RuntimeState,
|
||||
) -> Result<RuntimeState, RuntimeState> {
|
||||
match self.state.compare_exchange(
|
||||
expected as u8,
|
||||
new as u8,
|
||||
Ordering::AcqRel,
|
||||
Ordering::Acquire,
|
||||
) {
|
||||
Ok(_) => {
|
||||
tracing::info!(from = %expected, to = %new, "runtime state transition");
|
||||
Ok(new)
|
||||
}
|
||||
Err(actual) => {
|
||||
let actual_state =
|
||||
RuntimeState::from_u8(actual).unwrap_or(RuntimeState::Stopped);
|
||||
tracing::debug!(
|
||||
expected = %expected,
|
||||
actual = %actual_state,
|
||||
target = %new,
|
||||
"state transition skipped (unexpected current state)"
|
||||
);
|
||||
Err(actual_state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Unconditionally advance the state. Used during forced shutdown when
|
||||
/// intermediate states may have been skipped.
|
||||
pub fn force_set(&self, new: RuntimeState) {
|
||||
let prev = self.state.swap(new as u8, Ordering::AcqRel);
|
||||
let prev_state = RuntimeState::from_u8(prev).unwrap_or(RuntimeState::Stopped);
|
||||
if prev_state != new {
|
||||
tracing::info!(from = %prev_state, to = %new, "runtime state forced");
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to transition from `Running` to `Draining`.
|
||||
///
|
||||
/// Returns `true` if this call initiated shutdown (first caller wins).
|
||||
/// Returns `false` if shutdown was already in progress or the runtime
|
||||
/// was not yet `Running`.
|
||||
pub fn initiate_shutdown(&self) -> bool {
|
||||
self.transition(RuntimeState::Running, RuntimeState::Draining)
|
||||
.is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AtomicRuntimeState {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_state_transitions() {
|
||||
let state = AtomicRuntimeState::new();
|
||||
assert_eq!(state.load(), RuntimeState::Initializing);
|
||||
|
||||
assert!(
|
||||
state
|
||||
.transition(RuntimeState::Initializing, RuntimeState::Starting)
|
||||
.is_ok()
|
||||
);
|
||||
assert_eq!(state.load(), RuntimeState::Starting);
|
||||
|
||||
assert!(
|
||||
state
|
||||
.transition(RuntimeState::Starting, RuntimeState::Running)
|
||||
.is_ok()
|
||||
);
|
||||
assert_eq!(state.load(), RuntimeState::Running);
|
||||
|
||||
// Duplicate shutdown protection
|
||||
assert!(state.initiate_shutdown());
|
||||
assert!(!state.initiate_shutdown()); // second call fails
|
||||
assert_eq!(state.load(), RuntimeState::Draining);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_force_set() {
|
||||
let state = AtomicRuntimeState::new();
|
||||
state.force_set(RuntimeState::ClosingResources);
|
||||
assert_eq!(state.load(), RuntimeState::ClosingResources);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_shutting_down() {
|
||||
assert!(!RuntimeState::Initializing.is_shutting_down());
|
||||
assert!(!RuntimeState::Starting.is_shutting_down());
|
||||
assert!(!RuntimeState::Running.is_shutting_down());
|
||||
assert!(RuntimeState::Draining.is_shutting_down());
|
||||
assert!(RuntimeState::StoppingWorkers.is_shutting_down());
|
||||
assert!(RuntimeState::ExecutingHooks.is_shutting_down());
|
||||
assert!(RuntimeState::ClosingResources.is_shutting_down());
|
||||
assert!(RuntimeState::Stopped.is_shutting_down());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
//! Background worker management using `tokio::task::JoinSet`.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
pub struct TaskGroup {
|
||||
name: String,
|
||||
tasks: JoinSet<()>,
|
||||
}
|
||||
|
||||
impl TaskGroup {
|
||||
pub fn new(name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
tasks: JoinSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn spawn<F>(&mut self, future: F)
|
||||
where
|
||||
F: Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
self.tasks.spawn(future);
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.tasks.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.tasks.is_empty()
|
||||
}
|
||||
|
||||
pub fn abort_all(&mut self) {
|
||||
self.tasks.abort_all();
|
||||
}
|
||||
|
||||
pub async fn shutdown(&mut self, timeout: Duration) {
|
||||
if self.tasks.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let task_count = self.tasks.len();
|
||||
tracing::info!(
|
||||
group = %self.name,
|
||||
tasks = task_count,
|
||||
timeout_secs = timeout.as_secs(),
|
||||
"waiting for task group to complete"
|
||||
);
|
||||
|
||||
let result = tokio::time::timeout(timeout, async {
|
||||
while self.tasks.join_next().await.is_some() {}
|
||||
})
|
||||
.await;
|
||||
|
||||
if result.is_err() {
|
||||
let remaining = self.tasks.len();
|
||||
tracing::warn!(
|
||||
group = %self.name,
|
||||
remaining,
|
||||
"task group timed out, aborting remaining tasks"
|
||||
);
|
||||
self.tasks.abort_all();
|
||||
while self.tasks.join_next().await.is_some() {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WorkerManager {
|
||||
groups: HashMap<String, TaskGroup>,
|
||||
}
|
||||
|
||||
impl WorkerManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
groups: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn group(&mut self, name: &str) -> &mut TaskGroup {
|
||||
self.groups
|
||||
.entry(name.to_string())
|
||||
.or_insert_with(|| TaskGroup::new(name))
|
||||
}
|
||||
|
||||
pub fn active_tasks(&self) -> usize {
|
||||
self.groups.values().map(|g| g.len()).sum()
|
||||
}
|
||||
|
||||
pub fn abort_all(&mut self) {
|
||||
for group in self.groups.values_mut() {
|
||||
group.abort_all();
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn shutdown_all(&mut self, timeout: Duration) {
|
||||
for group in self.groups.values_mut() {
|
||||
group.shutdown(timeout).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WorkerManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user