WEB Ephemeral Lifecycle

Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
Alexey
2026-08-28 22:39:18 +03:00
parent ac71d92ec4
commit 012dc07a98
18 changed files with 1434 additions and 65 deletions
+550
View File
@@ -0,0 +1,550 @@
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::time::{Duration, Instant};
use arc_swap::ArcSwap;
use parking_lot::Mutex;
use tokio::sync::{Mutex as AsyncMutex, Notify};
use tokio::time::Instant as TokioInstant;
use tokio_util::sync::CancellationToken;
use super::WebProcessRuntime;
// Serialized state-machine values stay separate from synchronization mechanics.
mod status;
pub(crate) use status::{
OperatorDrainOutcome, OperatorDrainState, OperatorDrainStatus, OperatorLifecycleState,
OperatorLifecycleStatus,
};
const OPERATOR_ADMISSION_CLOSED: usize = 1 << (usize::BITS - 1);
const OPERATOR_REGISTRATION_COUNT: usize = OPERATOR_ADMISSION_CLOSED - 1;
const DRAIN_REF_VERSION: &str = "wd1";
/// Stable operator-control rejection category.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum OperatorLifecycleError {
/// Process shutdown terminally closed the command gate.
Closed,
/// One drain already owns the single active operation slot.
OperationInProgress,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
struct WorkCounts {
sessions: usize,
streams: usize,
websockets: usize,
}
impl WorkCounts {
fn is_zero(self) -> bool {
self.sessions == 0 && self.streams == 0 && self.websockets == 0
}
}
struct OperatorAdmission {
state: AtomicUsize,
registrations_drained: Notify,
}
pub(super) struct OperatorRegistration<'a> {
admission: &'a OperatorAdmission,
}
impl OperatorAdmission {
fn new() -> Self {
Self {
state: AtomicUsize::new(0),
registrations_drained: Notify::new(),
}
}
fn try_register(&self) -> Option<OperatorRegistration<'_>> {
let mut state = self.state.load(Ordering::Acquire);
loop {
if state & OPERATOR_ADMISSION_CLOSED != 0
|| state & OPERATOR_REGISTRATION_COUNT == OPERATOR_REGISTRATION_COUNT
{
return None;
}
match self.state.compare_exchange_weak(
state,
state + 1,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => return Some(OperatorRegistration { admission: self }),
Err(observed) => state = observed,
}
}
}
fn close(&self) {
self.state
.fetch_or(OPERATOR_ADMISSION_CLOSED, Ordering::AcqRel);
}
fn reopen(&self) {
self.state
.fetch_and(!OPERATOR_ADMISSION_CLOSED, Ordering::AcqRel);
}
fn is_closed(&self) -> bool {
self.state.load(Ordering::Acquire) & OPERATOR_ADMISSION_CLOSED != 0
}
async fn wait_for_registrations(&self) {
loop {
let notified = self.registrations_drained.notified();
if self.state.load(Ordering::Acquire) & OPERATOR_REGISTRATION_COUNT == 0 {
return;
}
notified.await;
}
}
}
impl Drop for OperatorRegistration<'_> {
fn drop(&mut self) {
let previous = self.admission.state.fetch_sub(1, Ordering::AcqRel);
if previous & OPERATOR_REGISTRATION_COUNT == 1 {
self.admission.registrations_drained.notify_waiters();
}
}
}
struct ActiveDrain {
sequence: u64,
cancellation: CancellationToken,
}
struct OperatorLifecycleInner {
state: OperatorLifecycleState,
epoch: u64,
since: Instant,
terminal: bool,
active: Option<ActiveDrain>,
drain: Option<OperatorDrainStatus>,
}
#[derive(Clone)]
struct OperatorSnapshot {
state: OperatorLifecycleState,
epoch: u64,
since: Instant,
terminal: bool,
drain: Option<OperatorDrainStatus>,
}
pub(super) struct OperatorLifecycle {
runtime_instance: Arc<str>,
admission: OperatorAdmission,
commands: AsyncMutex<()>,
inner: Mutex<OperatorLifecycleInner>,
published: ArcSwap<OperatorSnapshot>,
work_changed: Notify,
next_operation_id: AtomicU64,
}
impl OperatorLifecycle {
pub(super) fn new(runtime_instance: Arc<str>) -> Self {
let since = Instant::now();
let snapshot = OperatorSnapshot {
state: OperatorLifecycleState::Running,
epoch: 0,
since,
terminal: false,
drain: None,
};
Self {
runtime_instance,
admission: OperatorAdmission::new(),
commands: AsyncMutex::new(()),
inner: Mutex::new(OperatorLifecycleInner {
state: snapshot.state,
epoch: snapshot.epoch,
since,
terminal: false,
active: None,
drain: None,
}),
published: ArcSwap::from_pointee(snapshot),
work_changed: Notify::new(),
next_operation_id: AtomicU64::new(1),
}
}
pub(super) fn try_register(&self) -> Option<OperatorRegistration<'_>> {
self.admission.try_register()
}
pub(super) fn notify_work_changed(&self) {
if self.admission.is_closed() {
self.work_changed.notify_waiters();
}
}
pub(super) fn status(&self, config_enabled: bool) -> OperatorLifecycleStatus {
let snapshot = self.published.load();
let admission_open = !snapshot.terminal
&& snapshot.state == OperatorLifecycleState::Running;
OperatorLifecycleStatus {
state: snapshot.state,
epoch: snapshot.epoch,
age_ms: millis(Instant::now().saturating_duration_since(snapshot.since)),
admission_open,
effective_new_work_admission: admission_open && config_enabled,
drain: snapshot.drain.clone(),
}
}
fn is_terminal(&self) -> bool {
self.published.load().terminal
}
fn publish_locked(&self, inner: &OperatorLifecycleInner) {
self.published.store(Arc::new(OperatorSnapshot {
state: inner.state,
epoch: inner.epoch,
since: inner.since,
terminal: inner.terminal,
drain: inner.drain.clone(),
}));
}
fn transition_locked(
&self,
inner: &mut OperatorLifecycleInner,
state: OperatorLifecycleState,
) {
if inner.state != state {
inner.state = state;
inner.epoch = inner.epoch.saturating_add(1);
inner.since = Instant::now();
}
}
fn update_counts(&self, sequence: u64, counts: WorkCounts) -> bool {
let mut inner = self.inner.lock();
if inner
.active
.as_ref()
.is_none_or(|active| active.sequence != sequence)
{
return false;
}
let Some(drain) = inner.drain.as_mut() else {
return false;
};
if drain.remaining_sessions != counts.sessions
|| drain.remaining_streams != counts.streams
|| drain.remaining_websockets != counts.websockets
{
drain.remaining_sessions = counts.sessions;
drain.remaining_streams = counts.streams;
drain.remaining_websockets = counts.websockets;
self.publish_locked(&inner);
}
true
}
fn commit_force(&self, sequence: u64, counts: WorkCounts) -> bool {
let mut inner = self.inner.lock();
if inner
.active
.as_ref()
.is_none_or(|active| active.sequence != sequence)
{
return false;
}
self.transition_locked(&mut inner, OperatorLifecycleState::ForceClosing);
let Some(drain) = inner.drain.as_mut() else {
return false;
};
drain.state = OperatorDrainState::ForceClosing;
drain.remaining_sessions = counts.sessions;
drain.remaining_streams = counts.streams;
drain.remaining_websockets = counts.websockets;
drain.force_close_signalled = true;
self.publish_locked(&inner);
true
}
fn complete(&self, sequence: u64, forced: bool) {
let mut inner = self.inner.lock();
if inner
.active
.as_ref()
.is_none_or(|active| active.sequence != sequence)
{
return;
}
inner.active = None;
self.transition_locked(&mut inner, OperatorLifecycleState::Drained);
if let Some(drain) = inner.drain.as_mut() {
drain.state = OperatorDrainState::Completed;
drain.outcome = Some(if forced {
OperatorDrainOutcome::Forced
} else {
OperatorDrainOutcome::Graceful
});
drain.completed_epoch_millis = Some(crate::web::trace::store_epoch_millis());
drain.remaining_sessions = 0;
drain.remaining_streams = 0;
drain.remaining_websockets = 0;
}
self.publish_locked(&inner);
}
fn close_terminal(&self) {
let mut inner = self.inner.lock();
self.admission.close();
if inner.terminal {
return;
}
inner.terminal = true;
if let Some(active) = inner.active.take() {
active.cancellation.cancel();
if let Some(drain) = inner.drain.as_mut() {
drain.state = OperatorDrainState::Cancelled;
drain.outcome = Some(OperatorDrainOutcome::Cancelled);
drain.completed_epoch_millis = Some(crate::web::trace::store_epoch_millis());
}
}
self.publish_locked(&inner);
self.work_changed.notify_waiters();
}
}
impl WebProcessRuntime {
/// Returns the current lock-free operator lifecycle snapshot.
pub(crate) fn operator_lifecycle_status(&self) -> OperatorLifecycleStatus {
let config_enabled = self.active_generation().config().web.enabled;
self.operator_lifecycle.status(config_enabled)
}
/// Pauses new WEB work after every pre-cutover admission section completes.
pub(crate) async fn pause_operator(
self: &Arc<Self>,
) -> Result<OperatorLifecycleStatus, OperatorLifecycleError> {
let _command = self.operator_lifecycle.commands.lock().await;
{
let mut inner = self.operator_lifecycle.inner.lock();
if inner.terminal || self.shutdown.is_cancelled() {
return Err(OperatorLifecycleError::Closed);
}
self.operator_lifecycle.admission.close();
if matches!(
inner.state,
OperatorLifecycleState::Running | OperatorLifecycleState::Drained
) {
self.operator_lifecycle
.transition_locked(&mut inner, OperatorLifecycleState::Paused);
}
self.operator_lifecycle.publish_locked(&inner);
}
self.operator_lifecycle
.admission
.wait_for_registrations()
.await;
if self.shutdown.is_cancelled() || self.operator_lifecycle.is_terminal() {
return Err(OperatorLifecycleError::Closed);
}
Ok(self.operator_lifecycle_status())
}
/// Starts one asynchronous graceful WEB drain under an absolute deadline.
pub(crate) async fn drain_operator(
self: &Arc<Self>,
timeout: Duration,
) -> Result<OperatorLifecycleStatus, OperatorLifecycleError> {
let _command = self.operator_lifecycle.commands.lock().await;
let sequence = self
.operator_lifecycle
.next_operation_id
.fetch_add(1, Ordering::Relaxed);
let cancellation = CancellationToken::new();
let started = TokioInstant::now();
let deadline = started + timeout;
let started_epoch_millis = crate::web::trace::store_epoch_millis();
{
let mut inner = self.operator_lifecycle.inner.lock();
if inner.terminal || self.shutdown.is_cancelled() {
return Err(OperatorLifecycleError::Closed);
}
if inner.active.is_some()
|| matches!(
inner.state,
OperatorLifecycleState::Draining | OperatorLifecycleState::ForceClosing
)
{
return Err(OperatorLifecycleError::OperationInProgress);
}
self.operator_lifecycle.admission.close();
inner.active = Some(ActiveDrain {
sequence,
cancellation: cancellation.clone(),
});
inner.drain = Some(OperatorDrainStatus {
operation_id: format!(
"{DRAIN_REF_VERSION}.{}.{sequence:016x}",
self.operator_lifecycle.runtime_instance
),
state: OperatorDrainState::Draining,
outcome: None,
timeout_secs: timeout.as_secs(),
started_epoch_millis,
deadline_epoch_millis: started_epoch_millis
.saturating_add(timeout.as_millis().min(u128::from(u64::MAX)) as u64),
completed_epoch_millis: None,
remaining_sessions: 0,
remaining_streams: 0,
remaining_websockets: 0,
force_close_signalled: false,
});
self.operator_lifecycle
.transition_locked(&mut inner, OperatorLifecycleState::Draining);
self.operator_lifecycle.publish_locked(&inner);
}
self.operator_lifecycle
.admission
.wait_for_registrations()
.await;
if self.shutdown.is_cancelled() || self.operator_lifecycle.is_terminal() {
return Err(OperatorLifecycleError::Closed);
}
let counts = self.operator_work_counts();
if !self.operator_lifecycle.update_counts(sequence, counts) {
return Err(OperatorLifecycleError::Closed);
}
let runtime = Arc::clone(self);
self.spawn_auxiliary(async move {
runtime
.run_operator_drain(sequence, deadline, cancellation)
.await;
});
Ok(self.operator_lifecycle_status())
}
/// Resumes operator admission and invalidates any active drain waiter.
pub(crate) async fn resume_operator(
self: &Arc<Self>,
) -> Result<OperatorLifecycleStatus, OperatorLifecycleError> {
let _command = self.operator_lifecycle.commands.lock().await;
let counts = self.operator_work_counts();
let mut inner = self.operator_lifecycle.inner.lock();
if inner.terminal || self.shutdown.is_cancelled() {
return Err(OperatorLifecycleError::Closed);
}
if let Some(active) = inner.active.take() {
active.cancellation.cancel();
if let Some(drain) = inner.drain.as_mut() {
drain.state = OperatorDrainState::Cancelled;
drain.outcome = Some(OperatorDrainOutcome::Cancelled);
drain.completed_epoch_millis = Some(crate::web::trace::store_epoch_millis());
drain.remaining_sessions = counts.sessions;
drain.remaining_streams = counts.streams;
drain.remaining_websockets = counts.websockets;
}
}
self.operator_lifecycle
.transition_locked(&mut inner, OperatorLifecycleState::Running);
self.operator_lifecycle.admission.reopen();
self.operator_lifecycle.publish_locked(&inner);
drop(inner);
self.operator_lifecycle.work_changed.notify_waiters();
Ok(self.operator_lifecycle_status())
}
pub(super) fn try_operator_admission(
&self,
) -> Result<OperatorRegistration<'_>, super::ManagerError> {
self.operator_lifecycle
.try_register()
.ok_or(super::ManagerError::AdmissionPaused)
}
pub(super) fn notify_operator_work_changed(&self) {
self.operator_lifecycle.notify_work_changed();
}
pub(super) fn close_operator_lifecycle(&self) {
self.operator_lifecycle.close_terminal();
}
fn operator_work_counts(&self) -> WorkCounts {
let sessions = self.state.lock().sessions.len();
let streams = self.stream_admission.lock().streams_live;
let websockets = self.websockets.lock().status().entries;
WorkCounts {
sessions,
streams,
websockets,
}
}
async fn run_operator_drain(
self: Arc<Self>,
sequence: u64,
deadline: TokioInstant,
cancellation: CancellationToken,
) {
let mut forced = false;
loop {
let notified = self.operator_lifecycle.work_changed.notified();
let counts = self.operator_work_counts();
if !self.operator_lifecycle.update_counts(sequence, counts) {
return;
}
if counts.is_zero() {
self.operator_lifecycle.complete(sequence, forced);
return;
}
if forced {
tokio::select! {
biased;
_ = cancellation.cancelled() => return,
_ = notified => {}
}
continue;
}
tokio::select! {
biased;
_ = cancellation.cancelled() => return,
_ = notified => {},
_ = tokio::time::sleep_until(deadline) => {
let counts = self.operator_work_counts();
if counts.is_zero() {
self.operator_lifecycle.complete(sequence, false);
return;
}
// Freeze the close set while admission is still closed. If resume
// wins the following epoch check, this snapshot is discarded.
let sessions = self
.state
.lock()
.sessions
.values()
.cloned()
.collect::<Vec<_>>();
if !self.operator_lifecycle.commit_force(sequence, counts) {
return;
}
forced = true;
for session in sessions {
session.close();
}
}
}
}
}
}
fn millis(duration: Duration) -> u64 {
duration.as_millis().min(u128::from(u64::MAX)) as u64
}
#[cfg(test)]
#[path = "operator_lifecycle/tests.rs"]
mod tests;