use crate::RuntimeError;
use crate::runtime_state::{RuntimeConfig, RuntimeInner, recover_poisoned};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock, Weak};
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RuntimeCheckpoint {
ShutdownWaitRegistered,
AdmissionCounted,
AdmissionRegistered,
ScopeCloseTransition,
ScopeWaitObserved(usize),
}
#[derive(Clone, Copy, Eq, PartialEq)]
enum CheckpointPhase {
Armed,
Paused,
Released,
}
struct CheckpointState {
closed: bool,
checkpoint: Option<(RuntimeCheckpoint, CheckpointPhase)>,
}
pub(crate) struct RuntimeSchedule {
state: Mutex<CheckpointState>,
changed: Condvar,
runtime: OnceLock<Weak<RuntimeInner>>,
conflicted: AtomicBool,
}
impl RuntimeSchedule {
fn new() -> Self {
Self {
state: Mutex::new(CheckpointState {
closed: false,
checkpoint: None,
}),
changed: Condvar::new(),
runtime: OnceLock::new(),
conflicted: AtomicBool::new(false),
}
}
pub(crate) fn attach_runtime(&self, runtime: &Arc<RuntimeInner>) {
match self.runtime.set(Arc::downgrade(runtime)) {
Ok(()) => {}
Err(_) => self.record_conflict(),
}
}
fn record_conflict(&self) {
self.conflicted.store(true, Ordering::Release);
tracing::warn!("scheduling controller already has a runtime attached");
}
fn attached_runtime(&self) -> Result<Arc<RuntimeInner>, RuntimeError> {
match (self.conflicted.load(Ordering::Acquire), self.runtime.get()) {
(true, _) => Err(Self::invalid(
"scheduling controller was attached to more than one runtime",
)),
(false, None) => Err(Self::invalid(
"scheduling controller has no runtime attached",
)),
(false, Some(runtime)) => Weak::upgrade(runtime).ok_or_else(|| {
Self::invalid("scheduling controller's attached runtime was already dropped")
}),
}
}
fn invalid(message: &'static str) -> RuntimeError {
RuntimeError::InvalidArgument(message.into())
}
fn state(&self) -> MutexGuard<'_, CheckpointState> {
recover_poisoned(self.state.lock())
}
fn wait_changed<'a>(
&self,
state: MutexGuard<'a, CheckpointState>,
) -> MutexGuard<'a, CheckpointState> {
recover_poisoned(self.changed.wait(state))
}
fn arm(&self, checkpoint: RuntimeCheckpoint) -> Result<(), RuntimeError> {
let mut state = self.state();
match (state.closed, state.checkpoint) {
(true, _) => Err(Self::invalid("runtime scheduling controller is closed")),
(false, Some((_, CheckpointPhase::Armed | CheckpointPhase::Paused))) => Err(
Self::invalid("runtime scheduling checkpoint is already armed"),
),
(false, None | Some((_, CheckpointPhase::Released))) => {
state.checkpoint = Some((checkpoint, CheckpointPhase::Armed));
Ok(())
}
}
}
fn in_phase(&self, checkpoint: RuntimeCheckpoint, phase: CheckpointPhase) -> bool {
let state = self.state();
matches!(
(state.closed, state.checkpoint),
(false, Some((held, held_phase))) if held == checkpoint && held_phase == phase
)
}
pub(crate) fn is_armed(&self, checkpoint: RuntimeCheckpoint) -> bool {
self.in_phase(checkpoint, CheckpointPhase::Armed)
}
pub(crate) fn pause(&self, checkpoint: RuntimeCheckpoint) {
let mut state = self.state();
match (state.closed, state.checkpoint) {
(false, Some((armed, CheckpointPhase::Armed))) if armed == checkpoint => {
state.checkpoint = Some((checkpoint, CheckpointPhase::Paused));
self.changed.notify_all();
}
_ => return,
}
while matches!(
state.checkpoint,
Some((paused, CheckpointPhase::Paused)) if paused == checkpoint
) && !state.closed
{
state = self.wait_changed(state);
}
}
fn is_paused(&self, checkpoint: RuntimeCheckpoint) -> bool {
self.in_phase(checkpoint, CheckpointPhase::Paused)
}
fn wait_until_paused(&self, checkpoint: RuntimeCheckpoint) -> Result<(), RuntimeError> {
let mut state = self.state();
loop {
match (state.closed, state.checkpoint) {
(true, _) => {
return Err(Self::invalid("runtime scheduling controller is closed"));
}
(false, Some((paused, CheckpointPhase::Paused))) if paused == checkpoint => {
return Ok(());
}
(false, Some((released, CheckpointPhase::Released))) if released == checkpoint => {
return Err(Self::invalid(
"runtime scheduling checkpoint was already released",
));
}
(false, Some((armed, _))) if armed != checkpoint => {
return Err(Self::invalid(
"runtime scheduling checkpoint does not match the armed checkpoint",
));
}
(false, None) => {
return Err(Self::invalid("runtime scheduling checkpoint is not armed"));
}
(false, Some(_)) => state = self.wait_changed(state),
}
}
}
fn release(&self, checkpoint: RuntimeCheckpoint) -> Result<(), RuntimeError> {
let mut state = self.state();
match (state.closed, state.checkpoint) {
(true, _) => Err(Self::invalid("runtime scheduling controller is closed")),
(false, Some((paused, CheckpointPhase::Paused))) if paused == checkpoint => {
state.checkpoint = Some((checkpoint, CheckpointPhase::Released));
self.changed.notify_all();
Ok(())
}
(false, Some((armed, _))) if armed != checkpoint => Err(Self::invalid(
"runtime scheduling checkpoint does not match the armed checkpoint",
)),
(false, _) => Err(Self::invalid("runtime scheduling checkpoint is not paused")),
}
}
fn disarm(&self) {
let mut state = self.state();
state.checkpoint = match state.checkpoint {
Some((checkpoint, CheckpointPhase::Paused)) => {
Some((checkpoint, CheckpointPhase::Released))
}
_ => None,
};
self.changed.notify_all();
}
fn close(&self) {
let mut state = self.state();
state.closed = true;
self.changed.notify_all();
}
}
#[doc(hidden)]
pub struct RuntimeController {
schedule: Arc<RuntimeSchedule>,
}
impl RuntimeController {
pub fn pause_once(&self, checkpoint: RuntimeCheckpoint) -> Result<(), RuntimeError> {
self.schedule.arm(checkpoint)
}
pub fn wait_until_paused(&self, checkpoint: RuntimeCheckpoint) -> Result<(), RuntimeError> {
self.schedule.wait_until_paused(checkpoint)
}
pub fn is_paused(&self, checkpoint: RuntimeCheckpoint) -> bool {
self.schedule.is_paused(checkpoint)
}
pub fn release(&self, checkpoint: RuntimeCheckpoint) -> Result<(), RuntimeError> {
self.schedule.release(checkpoint)
}
pub fn disarm(&self) {
self.schedule.disarm();
}
pub fn scope_registry_len(&self) -> Result<usize, RuntimeError> {
Ok(self.schedule.attached_runtime()?.scope_registry_len())
}
pub fn scope_joined_count(&self) -> Result<usize, RuntimeError> {
Ok(self.schedule.attached_runtime()?.scope_joined_count())
}
pub(crate) fn schedule(&self) -> Arc<RuntimeSchedule> {
Arc::clone(&self.schedule)
}
}
impl Drop for RuntimeController {
fn drop(&mut self) {
self.schedule.close();
}
}
#[doc(hidden)]
pub fn runtime_schedule() -> RuntimeController {
RuntimeController {
schedule: Arc::new(RuntimeSchedule::new()),
}
}
#[doc(hidden)]
pub use crate::runtime_state::TestRuntimeContext;
#[doc(hidden)]
#[must_use = "the context is uninstalled as soon as the guard is dropped"]
pub fn install_runtime_context() -> TestRuntimeContext {
let (inner, context) = crate::runtime::establish_runtime(
tokio::runtime::Handle::try_current().ok(),
RuntimeConfig::default(),
None,
None,
None,
);
TestRuntimeContext::new(inner, context)
}
#[doc(hidden)]
pub async fn wait_scope_closing() {
match crate::runtime::try_current_runtime() {
Some(runtime) => runtime.scope_closing().wait().await,
None => std::future::pending().await,
}
}
#[doc(hidden)]
pub fn admit_signal_watcher_for_test() -> Result<(), RuntimeError> {
crate::runtime::admit_signal_watcher(&crate::runtime::runtime_context()?)
}
#[cfg(feature = "acme")]
#[doc(hidden)]
pub fn admit_acme_renewal_for_test(
events: Box<[Result<Box<str>, Box<str>>]>,
) -> Result<(), RuntimeError> {
use futures_util::StreamExt;
let scripted = futures_util::stream::iter(events)
.chain(futures_util::stream::pending::<Result<Box<str>, Box<str>>>());
crate::task::admit_signalled_subsystem_on(
&crate::runtime::runtime_context()?,
"acme renewal",
move |signals| crate::acme::acme_renewal_loop(scripted, signals),
)
}
#[cfg(feature = "dns01")]
#[doc(hidden)]
pub fn admit_dns01_renewal_for_test<P>(
store: crate::tls::CertStore,
provider: P,
) -> Result<(), RuntimeError>
where
P: crate::dns01::DnsProvider + 'static,
{
let acme = crate::dns01::AcmeDns01::new("camber-dns01-renewal-test", ["localhost"]);
crate::task::admit_signalled_subsystem_on(
&crate::runtime::runtime_context()?,
"dns01 renewal",
move |signals| crate::dns01::dns01_renewal_loop(acme, provider, store, signals),
)
}