mirror of
https://github.com/telemt/telemt.git
synced 2026-10-07 01:45:58 +03:00
291 lines
9.4 KiB
Rust
291 lines
9.4 KiB
Rust
use std::future::Future;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
|
use std::time::Duration;
|
|
|
|
use tokio::sync::Notify;
|
|
use tokio_util::sync::CancellationToken;
|
|
use tokio_util::task::TaskTracker;
|
|
|
|
const CONTROL_TASK_ADMISSION_CLOSED: usize = 1 << (usize::BITS - 1);
|
|
const CONTROL_TASK_REGISTRATION_COUNT: usize = CONTROL_TASK_ADMISSION_CLOSED - 1;
|
|
|
|
struct ControlTaskAdmission {
|
|
state: AtomicUsize,
|
|
registrations_drained: Notify,
|
|
}
|
|
|
|
struct ControlTaskRegistration<'a> {
|
|
admission: &'a ControlTaskAdmission,
|
|
}
|
|
|
|
impl ControlTaskAdmission {
|
|
fn new() -> Self {
|
|
Self {
|
|
state: AtomicUsize::new(0),
|
|
registrations_drained: Notify::new(),
|
|
}
|
|
}
|
|
|
|
fn try_register(&self) -> Option<ControlTaskRegistration<'_>> {
|
|
let mut state = self.state.load(Ordering::Acquire);
|
|
loop {
|
|
if state & CONTROL_TASK_ADMISSION_CLOSED != 0
|
|
|| state & CONTROL_TASK_REGISTRATION_COUNT == CONTROL_TASK_REGISTRATION_COUNT
|
|
{
|
|
return None;
|
|
}
|
|
match self.state.compare_exchange_weak(
|
|
state,
|
|
state + 1,
|
|
Ordering::AcqRel,
|
|
Ordering::Acquire,
|
|
) {
|
|
Ok(_) => return Some(ControlTaskRegistration { admission: self }),
|
|
Err(observed) => state = observed,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn close(&self) {
|
|
self.state
|
|
.fetch_or(CONTROL_TASK_ADMISSION_CLOSED, Ordering::AcqRel);
|
|
}
|
|
|
|
async fn wait_for_registrations(&self) {
|
|
loop {
|
|
let notified = self.registrations_drained.notified();
|
|
tokio::pin!(notified);
|
|
notified.as_mut().enable();
|
|
if self.state.load(Ordering::Acquire) & CONTROL_TASK_REGISTRATION_COUNT == 0 {
|
|
return;
|
|
}
|
|
notified.await;
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for ControlTaskRegistration<'_> {
|
|
fn drop(&mut self) {
|
|
let previous = self.admission.state.fetch_sub(1, Ordering::AcqRel);
|
|
if previous & CONTROL_TASK_REGISTRATION_COUNT == 1 {
|
|
self.admission.registrations_drained.notify_waiters();
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ProcessControlPlaneInner {
|
|
admission: ControlTaskAdmission,
|
|
cancellation: CancellationToken,
|
|
tasks: TaskTracker,
|
|
shutdown_completed: AtomicBool,
|
|
}
|
|
|
|
/// Process-owned cancellation and join scope for API, metrics, and signal tasks.
|
|
#[derive(Clone)]
|
|
pub(crate) struct ProcessControlPlane {
|
|
inner: Arc<ProcessControlPlaneInner>,
|
|
}
|
|
|
|
impl ProcessControlPlane {
|
|
/// Creates an open process control-plane scope.
|
|
pub(crate) fn new() -> Self {
|
|
Self {
|
|
inner: Arc::new(ProcessControlPlaneInner {
|
|
admission: ControlTaskAdmission::new(),
|
|
cancellation: CancellationToken::new(),
|
|
tasks: TaskTracker::new(),
|
|
shutdown_completed: AtomicBool::new(false),
|
|
}),
|
|
}
|
|
}
|
|
|
|
/// Registers a cancellable process control-plane task before it can be unpolled.
|
|
pub(crate) fn spawn<F>(&self, future: F) -> Result<(), F>
|
|
where
|
|
F: Future<Output = ()> + Send + 'static,
|
|
{
|
|
let Some(registration) = self.inner.admission.try_register() else {
|
|
return Err(future);
|
|
};
|
|
let cancellation = self.inner.cancellation.clone();
|
|
self.inner.tasks.spawn(async move {
|
|
tokio::select! {
|
|
biased;
|
|
_ = cancellation.cancelled() => {}
|
|
_ = future => {}
|
|
}
|
|
});
|
|
drop(registration);
|
|
Ok(())
|
|
}
|
|
|
|
/// Registers work that must finish once accepted, even after shutdown cancellation starts.
|
|
pub(crate) fn spawn_completion<F>(&self, future: F) -> Result<(), F>
|
|
where
|
|
F: Future<Output = ()> + Send + 'static,
|
|
{
|
|
let Some(registration) = self.inner.admission.try_register() else {
|
|
return Err(future);
|
|
};
|
|
self.inner.tasks.spawn(future);
|
|
drop(registration);
|
|
Ok(())
|
|
}
|
|
|
|
/// Registers a cooperatively cancelled task whose cleanup future must finish.
|
|
pub(crate) fn spawn_cooperative<S, F>(&self, spawn: S) -> Result<(), S>
|
|
where
|
|
S: FnOnce(CancellationToken) -> F,
|
|
F: Future<Output = ()> + Send + 'static,
|
|
{
|
|
let Some(registration) = self.inner.admission.try_register() else {
|
|
return Err(spawn);
|
|
};
|
|
let cancellation = self.inner.cancellation.clone();
|
|
self.inner.tasks.spawn(spawn(cancellation));
|
|
drop(registration);
|
|
Ok(())
|
|
}
|
|
|
|
/// Closes task admission, cancels all owned work, and joins it within the deadline.
|
|
pub(crate) async fn shutdown(&self, timeout: Duration) -> bool {
|
|
let deadline = tokio::time::Instant::now() + timeout;
|
|
self.inner.admission.close();
|
|
self.inner.cancellation.cancel();
|
|
self.inner.tasks.close();
|
|
if self.inner.shutdown_completed.load(Ordering::Acquire) {
|
|
return true;
|
|
}
|
|
let registrations_stopped =
|
|
tokio::time::timeout_at(deadline, self.inner.admission.wait_for_registrations())
|
|
.await
|
|
.is_ok();
|
|
let tasks_stopped = tokio::time::timeout_at(deadline, self.inner.tasks.wait())
|
|
.await
|
|
.is_ok();
|
|
let outcome = registrations_stopped && tasks_stopped;
|
|
if outcome {
|
|
self.inner.shutdown_completed.store(true, Ordering::Release);
|
|
}
|
|
outcome
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn shutdown_cancels_owned_tasks_and_rejects_late_registration() {
|
|
struct DropSignal(Arc<AtomicBool>);
|
|
|
|
impl Drop for DropSignal {
|
|
fn drop(&mut self) {
|
|
self.0.store(true, Ordering::Release);
|
|
}
|
|
}
|
|
|
|
let scope = ProcessControlPlane::new();
|
|
let dropped = Arc::new(AtomicBool::new(false));
|
|
let drop_signal = DropSignal(dropped.clone());
|
|
assert!(
|
|
scope
|
|
.spawn(async move {
|
|
let _drop_signal = drop_signal;
|
|
std::future::pending::<()>().await;
|
|
})
|
|
.is_ok()
|
|
);
|
|
|
|
assert!(scope.shutdown(Duration::from_secs(1)).await);
|
|
assert!(dropped.load(Ordering::Acquire));
|
|
assert!(scope.spawn(async {}).is_err());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn concurrent_shutdown_callers_wait_for_completion() {
|
|
let scope = ProcessControlPlane::new();
|
|
let registration = scope.inner.admission.try_register().unwrap();
|
|
let first_scope = scope.clone();
|
|
let first = tokio::spawn(async move { first_scope.shutdown(Duration::from_secs(1)).await });
|
|
tokio::task::yield_now().await;
|
|
let second_scope = scope.clone();
|
|
let second =
|
|
tokio::spawn(async move { second_scope.shutdown(Duration::from_secs(1)).await });
|
|
|
|
tokio::task::yield_now().await;
|
|
assert!(!first.is_finished());
|
|
assert!(!second.is_finished());
|
|
drop(registration);
|
|
|
|
assert!(first.await.unwrap());
|
|
assert!(second.await.unwrap());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn cancelled_shutdown_caller_cannot_orphan_the_control_plane() {
|
|
let scope = ProcessControlPlane::new();
|
|
let registration = scope.inner.admission.try_register().unwrap();
|
|
let first_scope = scope.clone();
|
|
let first =
|
|
tokio::spawn(async move { first_scope.shutdown(Duration::from_secs(30)).await });
|
|
tokio::task::yield_now().await;
|
|
|
|
first.abort();
|
|
assert!(first.await.unwrap_err().is_cancelled());
|
|
assert!(scope.spawn(async {}).is_err());
|
|
drop(registration);
|
|
|
|
assert!(scope.shutdown(Duration::from_secs(1)).await);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn shutdown_waits_for_accepted_completion_without_cancelling_it() {
|
|
let scope = ProcessControlPlane::new();
|
|
let (release_tx, release_rx) = tokio::sync::oneshot::channel();
|
|
let completed = Arc::new(AtomicBool::new(false));
|
|
let completed_task = completed.clone();
|
|
assert!(
|
|
scope
|
|
.spawn_completion(async move {
|
|
let _ = release_rx.await;
|
|
completed_task.store(true, Ordering::Release);
|
|
})
|
|
.is_ok()
|
|
);
|
|
|
|
let shutdown_scope = scope.clone();
|
|
let shutdown =
|
|
tokio::spawn(async move { shutdown_scope.shutdown(Duration::from_secs(1)).await });
|
|
tokio::task::yield_now().await;
|
|
assert!(!shutdown.is_finished());
|
|
assert!(!completed.load(Ordering::Acquire));
|
|
|
|
release_tx.send(()).unwrap();
|
|
assert!(shutdown.await.unwrap());
|
|
assert!(completed.load(Ordering::Acquire));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn cooperative_task_observes_cancellation_and_finishes_cleanup() {
|
|
let scope = ProcessControlPlane::new();
|
|
let completed = Arc::new(AtomicBool::new(false));
|
|
let completed_task = Arc::clone(&completed);
|
|
assert!(
|
|
scope
|
|
.spawn_cooperative(move |cancellation| async move {
|
|
cancellation.cancelled().await;
|
|
completed_task.store(true, Ordering::Release);
|
|
})
|
|
.is_ok()
|
|
);
|
|
|
|
assert!(scope.shutdown(Duration::from_secs(1)).await);
|
|
assert!(completed.load(Ordering::Acquire));
|
|
}
|
|
}
|