use crate::resource::HealthState;
use crate::runtime_test_support::{RuntimeCheckpoint, RuntimeSchedule};
use crate::tls::CertStore;
use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::ops::ControlFlow;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::time::Duration;
pub(crate) const DEFAULT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(30);
pub(crate) const DEFAULT_KEEPALIVE_TIMEOUT: Duration = Duration::from_secs(60);
pub(crate) const DEFAULT_HEALTH_INTERVAL: Duration = Duration::from_secs(10);
const FORCED_JOIN_GRACE: Duration = Duration::from_millis(100);
pub(crate) type TlsConfig = Arc<rustls::ServerConfig>;
pub(crate) fn recover_poisoned<T>(result: std::sync::LockResult<T>) -> T {
result.unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub(crate) fn default_worker_threads() -> usize {
std::thread::available_parallelism()
.map(|n| n.get() * 4)
.unwrap_or(16)
}
#[derive(Clone)]
pub(crate) struct RuntimeConfig {
pub(crate) worker_threads: usize,
pub(crate) shutdown_timeout: Duration,
pub(crate) keepalive_timeout: Duration,
pub(crate) tracing_enabled: bool,
pub(crate) metrics_enabled: bool,
#[cfg(feature = "profiling")]
pub(crate) profiling_enabled: bool,
pub(crate) health_interval: Duration,
pub(crate) connection_limit: Option<usize>,
pub(crate) tls_config: Option<TlsConfig>,
pub(crate) cert_store: Option<CertStore>,
}
impl Default for RuntimeConfig {
fn default() -> Self {
Self {
worker_threads: default_worker_threads(),
shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
keepalive_timeout: DEFAULT_KEEPALIVE_TIMEOUT,
tracing_enabled: false,
metrics_enabled: false,
#[cfg(feature = "profiling")]
profiling_enabled: false,
health_interval: DEFAULT_HEALTH_INTERVAL,
connection_limit: None,
tls_config: None,
cert_store: None,
}
}
}
pub(crate) struct RuntimeInner {
shutdown: LatchSignal,
scope: TaskScope,
test_schedule: Option<Arc<RuntimeSchedule>>,
cancel_task: Mutex<CancelWatcherState>,
pub(crate) config: RuntimeConfig,
pub(crate) metrics_handle: Option<metrics_exporter_prometheus::PrometheusHandle>,
pub(crate) tokio_handle: Option<tokio::runtime::Handle>,
pub(crate) health_state: Option<HealthState>,
}
struct CancelWatcherState {
current: Option<CancelWatcher>,
}
struct CancelWatcher {
identity: Arc<tokio::sync::Notify>,
handle: tokio::task::JoinHandle<()>,
}
#[derive(Clone)]
pub(crate) struct LatchSignal {
fired: Arc<AtomicBool>,
notify: Arc<tokio::sync::Notify>,
}
pub(crate) type ShutdownSignal = LatchSignal;
impl LatchSignal {
fn new() -> Self {
Self {
fired: Arc::new(AtomicBool::new(false)),
notify: Arc::new(tokio::sync::Notify::new()),
}
}
pub(crate) fn is_fired(&self) -> bool {
self.fired.load(Ordering::Acquire)
}
pub(crate) fn fire(&self) {
self.fired.store(true, Ordering::Release);
self.notify.notify_waiters();
}
pub(crate) fn flag(&self) -> Arc<AtomicBool> {
Arc::clone(&self.fired)
}
pub(crate) fn from_parts(fired: Arc<AtomicBool>, notify: Arc<tokio::sync::Notify>) -> Self {
Self { fired, notify }
}
pub(crate) async fn wait(&self) {
self.wait_observed(|| std::future::ready(())).await;
}
pub(crate) async fn wait_observed<F, Fut>(&self, observe: F)
where
F: Fn() -> Fut,
Fut: Future<Output = ()>,
{
loop {
let notified = self.notify.notified();
tokio::pin!(notified);
notified.as_mut().enable();
observe().await;
match self.is_fired() {
true => return,
false => notified.await,
}
}
}
}
#[derive(Clone)]
pub(crate) struct LifecycleSignals {
shutdown: LatchSignal,
closing: LatchSignal,
}
impl LifecycleSignals {
pub(crate) fn current() -> Self {
match try_current_runtime() {
Some(inner) => Self::from_runtime(&inner),
None => Self {
shutdown: LatchSignal::new(),
closing: LatchSignal::new(),
},
}
}
pub(crate) fn from_runtime(inner: &RuntimeInner) -> Self {
Self {
shutdown: inner.shutdown_signal(),
closing: inner.scope_closing(),
}
}
pub(crate) fn is_fired(&self) -> bool {
self.shutdown.is_fired() || self.closing.is_fired()
}
pub(crate) async fn wait(&self) {
tokio::select! {
() = self.shutdown.wait() => {}
() = self.closing.wait() => {}
}
}
pub(crate) async fn tick(&self, interval: Duration) -> ControlFlow<()> {
tokio::select! {
() = tokio::time::sleep(interval) => {}
() = self.wait() => return ControlFlow::Break(()),
}
match self.is_fired() {
true => ControlFlow::Break(()),
false => ControlFlow::Continue(()),
}
}
pub(crate) async fn guard<Fut>(&self, work: Fut) -> ControlFlow<(), Fut::Output>
where
Fut: Future,
{
tokio::select! {
output = work => ControlFlow::Continue(output),
() = self.wait() => ControlFlow::Break(()),
}
}
}
impl RuntimeInner {
pub(crate) fn with_config_and_schedule(
config: RuntimeConfig,
test_schedule: Option<Arc<RuntimeSchedule>>,
) -> Self {
Self {
shutdown: LatchSignal::new(),
scope: TaskScope::new(),
test_schedule,
cancel_task: Mutex::new(CancelWatcherState { current: None }),
config,
metrics_handle: None,
tokio_handle: None,
health_state: None,
}
}
pub(crate) fn request_shutdown(&self) {
self.close_scope();
self.shutdown.fire();
}
pub(crate) fn shutdown_signal(&self) -> ShutdownSignal {
self.shutdown.clone()
}
pub(crate) fn is_shutdown_requested(&self) -> bool {
self.shutdown.is_fired()
}
pub(crate) fn close_scope(&self) {
self.pause_test_schedule(RuntimeCheckpoint::ScopeCloseTransition);
self.scope.close();
}
pub(crate) fn scope_closing(&self) -> LatchSignal {
self.scope.closing()
}
pub(crate) fn executor(&self) -> Result<&tokio::runtime::Handle, crate::RuntimeError> {
self.tokio_handle
.as_ref()
.ok_or(crate::RuntimeError::NoRuntime)
}
pub(crate) fn admit_blocking(self: &Arc<Self>) -> Result<ScopeSlot, crate::RuntimeError> {
self.admit(ChildKind::Blocking)
}
pub(crate) fn admit_internal_async<F>(
self: &Arc<Self>,
body: F,
) -> Result<(), crate::RuntimeError>
where
F: Future<Output = ()> + Send + 'static,
{
let sink = Arc::clone(self);
self.admit_async(capture_child_panic(body, sink))
}
pub(crate) fn admit_async<F>(self: &Arc<Self>, body: F) -> Result<(), crate::RuntimeError>
where
F: Future<Output = ()> + Send + 'static,
{
let executor = self.executor()?;
let slot = self.admit(ChildKind::Async)?;
let id = slot.id;
let gate = Arc::new(tokio::sync::Notify::new());
let handle = executor.spawn(
TASK_RUNTIME.scope(Arc::clone(self), gated_child(Arc::clone(&gate), body, slot)),
);
if let Some(swept) = self.scope.register_async(id, handle) {
swept.abort();
return Err(crate::RuntimeError::ScopeClosed);
}
self.pause_test_schedule(RuntimeCheckpoint::AdmissionRegistered);
gate.notify_one();
Ok(())
}
fn admit(self: &Arc<Self>, kind: ChildKind) -> Result<ScopeSlot, crate::RuntimeError> {
match self.scope.admit(kind) {
None => Err(crate::RuntimeError::ScopeClosed),
Some(id) => {
self.pause_test_schedule(RuntimeCheckpoint::AdmissionCounted);
Ok(ScopeSlot {
runtime: Arc::clone(self),
id,
kind,
})
}
}
}
pub(crate) fn scope_registry_len(&self) -> usize {
self.scope.registry_len()
}
pub(crate) fn scope_joined_count(&self) -> usize {
self.scope.joined_count()
}
pub(crate) fn take_internal_panic(&self) -> Option<crate::RuntimeError> {
self.scope.take_internal_panic()
}
fn observe_drain_end(&self) {
let outstanding = self.scope.count();
self.pause_test_schedule(RuntimeCheckpoint::ScopeWaitObserved(outstanding));
}
pub(crate) fn publish_to_test_schedule(self: &Arc<Self>) {
if let Some(schedule) = self.test_schedule.as_ref() {
schedule.attach_runtime(self);
}
}
pub(crate) fn pause_test_schedule(&self, checkpoint: RuntimeCheckpoint) {
if let Some(schedule) = self.test_schedule.as_ref() {
schedule.pause(checkpoint);
}
}
fn watch_cancel<F>(self: &Arc<Self>, future: F)
where
F: Future<Output = ()> + Send + 'static,
{
match self.tokio_handle.as_ref() {
None => tracing::warn!(
"external cancellation watcher not registered: the runtime has no executor"
),
Some(executor) => {
let gate = Arc::new(tokio::sync::Notify::new());
let task_inner = Arc::clone(self);
let mut state = recover_poisoned(self.cancel_task.lock());
let task_gate = Arc::clone(&gate);
let handle = executor.spawn(async move {
task_gate.notified().await;
let outcome = crate::task::catch_panic_async(future).await;
task_inner.finish_cancel_watcher(&task_gate, outcome);
});
let displaced = state.current.replace(CancelWatcher {
identity: Arc::clone(&gate),
handle,
});
drop(state);
abort_cancel_watcher(displaced);
gate.notify_one();
}
}
}
fn finish_cancel_watcher(
&self,
identity: &Arc<tokio::sync::Notify>,
outcome: Result<(), crate::RuntimeError>,
) {
match outcome {
Ok(()) => self.request_shutdown_if_current(identity),
Err(error) => {
drop(self.take_cancel_watcher(identity));
self.scope.record_internal_panic(error);
}
}
}
fn request_shutdown_if_current(&self, identity: &Arc<tokio::sync::Notify>) {
let mut state = recover_poisoned(self.cancel_task.lock());
match state.current.as_ref() {
Some(current) if Arc::ptr_eq(¤t.identity, identity) => {
let completed = state.current.take();
self.request_shutdown();
drop(state);
drop(completed);
}
Some(_) | None => {}
}
}
fn take_cancel_watcher(&self, identity: &Arc<tokio::sync::Notify>) -> Option<CancelWatcher> {
let mut state = recover_poisoned(self.cancel_task.lock());
match state.current.as_ref() {
Some(current) if Arc::ptr_eq(¤t.identity, identity) => state.current.take(),
Some(_) | None => None,
}
}
fn take_current_cancel_watcher(&self) -> Option<CancelWatcher> {
recover_poisoned(self.cancel_task.lock()).current.take()
}
}
fn abort_cancel_watcher(watcher: Option<CancelWatcher>) {
if let Some(watcher) = watcher {
watcher.handle.abort();
}
}
#[derive(Clone, Copy)]
enum Admission {
Open,
Closing,
Closed,
}
impl Admission {
fn on_close(self, count: usize) -> Self {
match (self, count) {
(Self::Open, 0) => Self::Closed,
(Self::Open, _) => Self::Closing,
(already_closed, _) => already_closed,
}
}
fn on_drained(self) -> Self {
match self {
Self::Closing | Self::Closed => Self::Closed,
Self::Open => Self::Open,
}
}
}
type TaskId = u64;
#[derive(Clone, Copy)]
enum ChildKind {
Async,
Blocking,
}
struct ScopeState {
admission: Admission,
count: usize,
next_id: TaskId,
async_children: HashMap<TaskId, Option<tokio::task::JoinHandle<()>>>,
blocking_children: HashSet<TaskId>,
joined: usize,
stopped: bool,
}
impl ScopeState {
fn count_child(&mut self, kind: ChildKind) -> TaskId {
let id = self.next_id;
self.next_id = id.wrapping_add(1);
self.count += 1;
match kind {
ChildKind::Async => {
self.async_children.insert(id, None);
}
ChildKind::Blocking => {
self.blocking_children.insert(id);
}
}
id
}
fn fill_async_handle(
&mut self,
id: TaskId,
handle: tokio::task::JoinHandle<()>,
) -> Option<tokio::task::JoinHandle<()>> {
match (self.stopped, self.async_children.get_mut(&id)) {
(false, Some(entry)) => {
*entry = Some(handle);
None
}
_ => Some(handle),
}
}
fn remove_child(&mut self, id: TaskId, kind: ChildKind) {
match kind {
ChildKind::Async => {
self.async_children.remove(&id);
}
ChildKind::Blocking => {
self.blocking_children.remove(&id);
}
}
}
fn entries(&self) -> usize {
let registered = self
.async_children
.values()
.filter(|handle| handle.is_some())
.count();
registered + self.blocking_children.len()
}
}
struct TaskScope {
state: Mutex<ScopeState>,
idle: Condvar,
closing: LatchSignal,
internal_panic: Mutex<Option<crate::RuntimeError>>,
}
impl TaskScope {
fn new() -> Self {
Self {
state: Mutex::new(ScopeState {
admission: Admission::Open,
count: 0,
next_id: 0,
async_children: HashMap::new(),
blocking_children: HashSet::new(),
joined: 0,
stopped: false,
}),
idle: Condvar::new(),
closing: LatchSignal::new(),
internal_panic: Mutex::new(None),
}
}
fn record_internal_panic(&self, error: crate::RuntimeError) {
let mut slot = self.panic_slot();
match slot.as_ref() {
Some(_) => tracing::warn!(%error, "further internally-owned child panic"),
None => {
tracing::error!(%error, "internally-owned child panicked");
*slot = Some(error);
}
}
}
fn take_internal_panic(&self) -> Option<crate::RuntimeError> {
self.panic_slot().take()
}
fn closing(&self) -> LatchSignal {
self.closing.clone()
}
fn lock(&self) -> std::sync::MutexGuard<'_, ScopeState> {
recover_poisoned(self.state.lock())
}
fn panic_slot(&self) -> std::sync::MutexGuard<'_, Option<crate::RuntimeError>> {
recover_poisoned(self.internal_panic.lock())
}
fn admit(&self, kind: ChildKind) -> Option<TaskId> {
let mut state = self.lock();
match state.admission {
Admission::Open => Some(state.count_child(kind)),
Admission::Closing | Admission::Closed => None,
}
}
#[must_use = "an unregistered handle must be aborted by the caller"]
fn register_async(
&self,
id: TaskId,
handle: tokio::task::JoinHandle<()>,
) -> Option<tokio::task::JoinHandle<()>> {
self.lock().fill_async_handle(id, handle)
}
fn registry_len(&self) -> usize {
self.lock().entries()
}
fn joined_count(&self) -> usize {
self.lock().joined
}
fn count(&self) -> usize {
self.lock().count
}
fn take_async_children(&self) -> Box<[tokio::task::JoinHandle<()>]> {
let mut state = self.lock();
state.stopped = true;
state
.async_children
.drain()
.filter_map(|(_, handle)| handle)
.collect()
}
fn record_join(&self) {
self.lock().joined += 1;
}
async fn force_stop(&self, grace: Duration) {
let handles = self.take_async_children();
for handle in handles.iter() {
handle.abort();
}
let mut joins: futures_util::stream::FuturesUnordered<_> =
handles.into_vec().into_iter().collect();
let drain = async {
while let Some(joined) = futures_util::StreamExt::next(&mut joins).await {
report_forced_join(joined);
self.record_join();
}
};
if tokio::time::timeout(grace, drain).await.is_err() {
tracing::warn!("root scope drain grace expired with children still unstoppable");
}
}
fn finish(&self, id: TaskId, kind: ChildKind) {
let mut state = self.lock();
state.remove_child(id, kind);
match state.count {
0 => {
tracing::error!("runtime task scope completed an unadmitted child");
return;
}
1 => {
state.count = 0;
state.admission = state.admission.on_drained();
}
current => state.count = current - 1,
}
self.idle.notify_all();
}
fn close(&self) {
let mut state = self.lock();
state.admission = state.admission.on_close(state.count);
drop(state);
self.closing.fire();
}
fn wait_timeout(&self, timeout: Duration, schedule: Option<&RuntimeSchedule>) -> usize {
let started = std::time::Instant::now();
let mut budget = timeout;
let mut state = self.lock();
while state.count > 0 {
let checkpoint = RuntimeCheckpoint::ScopeWaitObserved(state.count);
if let Some(schedule) = schedule.filter(|schedule| schedule.is_armed(checkpoint)) {
drop(state);
let held = std::time::Instant::now();
schedule.pause(checkpoint);
budget = budget.saturating_add(held.elapsed());
state = self.lock();
continue;
}
let remaining = budget.saturating_sub(started.elapsed());
if remaining.is_zero() {
return state.count;
}
let (next_state, result) = recover_poisoned(self.idle.wait_timeout(state, remaining));
state = next_state;
if result.timed_out() {
return state.count;
}
}
0
}
}
fn report_forced_join(joined: Result<(), tokio::task::JoinError>) {
match joined {
Err(error) if error.is_panic() => {
tracing::error!(%error, "root scope child panicked during the forced stop");
}
Ok(()) | Err(_) => {}
}
}
#[must_use = "dropping the slot releases the child's claim on the root scope"]
pub(crate) struct ScopeSlot {
runtime: Arc<RuntimeInner>,
id: TaskId,
kind: ChildKind,
}
impl Drop for ScopeSlot {
fn drop(&mut self) {
self.runtime.scope.finish(self.id, self.kind);
}
}
async fn gated_child<F>(gate: Arc<tokio::sync::Notify>, body: F, slot: ScopeSlot)
where
F: Future<Output = ()>,
{
gate.notified().await;
body.await;
drop(slot);
}
async fn capture_child_panic<F>(body: F, runtime: Arc<RuntimeInner>)
where
F: Future<Output = ()>,
{
if let Err(error) = crate::task::catch_panic_async(body).await {
runtime.scope.record_internal_panic(error);
}
}
tokio::task_local! {
static TASK_RUNTIME: Arc<RuntimeInner>;
}
thread_local! {
static RUNTIME: std::cell::RefCell<Option<Arc<RuntimeInner>>> = const { std::cell::RefCell::new(None) };
static CANCEL_FLAG: std::cell::RefCell<Option<Arc<AtomicBool>>> = const { std::cell::RefCell::new(None) };
static CANCEL_CHANNEL: std::cell::RefCell<Option<crossbeam_channel::Receiver<()>>> = const { std::cell::RefCell::new(None) };
}
pub struct RuntimeContextGuard {
previous: Option<Arc<RuntimeInner>>,
}
impl Drop for RuntimeContextGuard {
fn drop(&mut self) {
RUNTIME.with(|cell| {
*cell.borrow_mut() = self.previous.take();
});
}
}
pub struct TestRuntimeContext {
inner: Arc<RuntimeInner>,
context: Option<RuntimeContextGuard>,
}
impl TestRuntimeContext {
pub(crate) fn new(inner: Arc<RuntimeInner>, context: RuntimeContextGuard) -> Self {
Self {
inner,
context: Some(context),
}
}
}
impl Drop for TestRuntimeContext {
fn drop(&mut self) {
teardown_runtime(&self.inner);
self.inner.close_scope();
drop(self.context.take());
}
}
pub(crate) struct CancelContextGuard {
previous_flag: Option<Arc<AtomicBool>>,
previous_channel: Option<crossbeam_channel::Receiver<()>>,
}
impl Drop for CancelContextGuard {
fn drop(&mut self) {
CANCEL_FLAG.with(|cell| {
*cell.borrow_mut() = self.previous_flag.take();
});
CANCEL_CHANNEL.with(|cell| {
*cell.borrow_mut() = self.previous_channel.take();
});
}
}
pub(crate) fn install_cancel_context(
flag: Arc<AtomicBool>,
channel: crossbeam_channel::Receiver<()>,
) -> CancelContextGuard {
let previous_flag = CANCEL_FLAG.with(|cell| cell.borrow_mut().replace(flag));
let previous_channel = CANCEL_CHANNEL.with(|cell| cell.borrow_mut().replace(channel));
CancelContextGuard {
previous_flag,
previous_channel,
}
}
pub(crate) fn cancel_channel() -> Option<crossbeam_channel::Receiver<()>> {
CANCEL_CHANNEL.with(|cell| cell.borrow().clone())
}
pub(crate) fn check_cancel() -> Result<(), crate::RuntimeError> {
CANCEL_FLAG.with(|cell| {
let borrow = cell.borrow();
match borrow.as_ref() {
Some(flag) if flag.load(Ordering::Acquire) => Err(crate::RuntimeError::Cancelled),
_ => Ok(()),
}
})
}
pub fn on_cancel<F>(future: F)
where
F: Future<Output = ()> + Send + 'static,
{
if let Some(inner) = try_current_runtime() {
inner.watch_cancel(future);
}
}
pub(crate) fn try_current_runtime() -> Option<Arc<RuntimeInner>> {
match TASK_RUNTIME.try_with(Arc::clone) {
Ok(inner) => Some(inner),
Err(_) => RUNTIME.with(|cell| cell.borrow().as_ref().map(Arc::clone)),
}
}
pub(crate) fn runtime_context() -> Result<Arc<RuntimeInner>, crate::RuntimeError> {
try_current_runtime().ok_or(crate::RuntimeError::NoRuntime)
}
pub fn request_shutdown() {
if let Some(inner) = try_current_runtime() {
inner.request_shutdown();
}
}
pub fn tokio_handle() -> tokio::runtime::Handle {
tokio::runtime::Handle::current()
}
pub fn is_shutting_down() -> bool {
match try_current_runtime() {
Some(inner) => inner.is_shutdown_requested(),
None => false,
}
}
pub(crate) fn has_runtime() -> bool {
TASK_RUNTIME.try_with(|_| ()).is_ok() || RUNTIME.with(|cell| cell.borrow().is_some())
}
pub fn block_on<F: std::future::Future>(f: F) -> F::Output {
tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(f))
}
pub(crate) fn install_runtime(inner: Arc<RuntimeInner>) -> RuntimeContextGuard {
let previous = RUNTIME.with(|cell| cell.borrow_mut().replace(inner));
RuntimeContextGuard { previous }
}
pub(crate) async fn scope_runtime<F>(inner: Arc<RuntimeInner>, future: F) -> F::Output
where
F: Future,
{
TASK_RUNTIME.scope(inner, future).await
}
pub(crate) async fn carry_runtime<F>(context: Option<Arc<RuntimeInner>>, future: F) -> F::Output
where
F: Future,
{
match context {
Some(inner) => scope_runtime(inner, future).await,
None => future.await,
}
}
pub(crate) fn teardown_runtime(inner: &RuntimeInner) {
abort_cancel_watcher(inner.take_current_cancel_watcher());
}
pub(crate) fn stop_cancel_watcher(inner: &RuntimeInner, executor: &tokio::runtime::Handle) {
let watcher = inner.take_current_cancel_watcher();
let handle = match watcher {
Some(watcher) => watcher.handle,
None => return,
};
handle.abort();
let joined =
executor.block_on(async move { tokio::time::timeout(FORCED_JOIN_GRACE, handle).await });
match joined {
Ok(Err(error)) if error.is_panic() => inner
.scope
.record_internal_panic(crate::task::panic_to_error(error.into_panic())),
Ok(Ok(())) | Ok(Err(_)) => {}
Err(_) => tracing::warn!(
"external cancellation watcher did not stop within the forced join grace"
),
}
}
pub(crate) fn drain_root_scope(
inner: &RuntimeInner,
executor: &tokio::runtime::Handle,
) -> Option<crate::RuntimeError> {
let shutdown_timeout = inner.config.shutdown_timeout;
let outstanding = inner
.scope
.wait_timeout(shutdown_timeout, inner.test_schedule.as_deref());
let outcome = match outstanding {
0 => None,
count => {
executor.block_on(inner.scope.force_stop(FORCED_JOIN_GRACE));
Some(crate::RuntimeError::ScopeDrainTimeout(count))
}
};
inner.observe_drain_end();
outcome
}