use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use meerkat_core::lifecycle::core_executor::{CoreApplyOutput, CoreExecutorError};
use meerkat_core::lifecycle::run_primitive::RunPrimitive;
use meerkat_core::lifecycle::{CoreExecutor, InputId, RunId};
use crate::meerkat_machine::dsl as mm_dsl;
#[cfg(not(target_arch = "wasm32"))]
pub(crate) use crate::tokio::time::Instant;
#[cfg(target_arch = "wasm32")]
pub(crate) use meerkat_core::time_compat::Instant;
pub(crate) const RUN_EXECUTION_START_NOTICE: Duration = Duration::from_secs(120);
pub(crate) const RUN_EXECUTION_START_BOUND: Duration = Duration::from_secs(3_600);
const _: () = assert!(
RUN_EXECUTION_START_NOTICE.as_secs() < RUN_EXECUTION_START_BOUND.as_secs(),
"the run-execution notice tier must fire strictly before the hard bound"
);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TurnStartSignal {
Signalled,
NotSignalled,
}
#[derive(Clone, Default)]
pub(crate) struct TurnStartSignalCell(Arc<AtomicBool>);
impl TurnStartSignalCell {
pub(crate) fn mark_signalled(&self) {
self.0.store(true, Ordering::SeqCst);
}
fn signal(&self) -> TurnStartSignal {
if self.0.load(Ordering::SeqCst) {
TurnStartSignal::Signalled
} else {
TurnStartSignal::NotSignalled
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct RunTurnStateFacts {
pub(crate) runtime_bound: bool,
pub(crate) run_is_current: bool,
pub(crate) applying_primitive: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RunExecutionProgress {
Executing,
PrimitiveUnapplied,
RunNotCurrent,
RuntimeUnbound,
ExecutionStartUnobservable,
Unreadable,
}
impl RunExecutionProgress {
pub(crate) fn proves_execution_never_started(self) -> bool {
matches!(self, Self::PrimitiveUnapplied)
}
pub(crate) fn closes_execution_start_window(self) -> bool {
matches!(self, Self::Executing | Self::RunNotCurrent)
}
pub(crate) fn execution_start_is_unobservable(self) -> bool {
matches!(
self,
Self::RuntimeUnbound | Self::ExecutionStartUnobservable
)
}
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Executing => "Executing",
Self::PrimitiveUnapplied => "PrimitiveUnapplied",
Self::RunNotCurrent => "RunNotCurrent",
Self::RuntimeUnbound => "RuntimeUnbound",
Self::ExecutionStartUnobservable => "ExecutionStartUnobservable",
Self::Unreadable => "Unreadable",
}
}
}
pub(crate) fn classify_execution_start(
facts: RunTurnStateFacts,
turn_start: TurnStartSignal,
) -> RunExecutionProgress {
if !facts.runtime_bound {
return RunExecutionProgress::RuntimeUnbound;
}
if turn_start == TurnStartSignal::NotSignalled {
return RunExecutionProgress::ExecutionStartUnobservable;
}
if !facts.run_is_current {
return RunExecutionProgress::RunNotCurrent;
}
if facts.applying_primitive {
RunExecutionProgress::PrimitiveUnapplied
} else {
RunExecutionProgress::Executing
}
}
pub(crate) trait RunExecutionProgressSource: Send + Sync {
fn observe(&self, run_id: &RunId) -> RunExecutionProgress;
}
pub(crate) struct AuthorityRunExecutionProgress {
authority: crate::driver::ephemeral::SharedIngressDslAuthority,
turn_start: TurnStartSignalCell,
}
impl AuthorityRunExecutionProgress {
pub(crate) fn new(
authority: crate::driver::ephemeral::SharedIngressDslAuthority,
turn_start: TurnStartSignalCell,
) -> Self {
Self {
authority,
turn_start,
}
}
}
impl RunExecutionProgressSource for AuthorityRunExecutionProgress {
fn observe(&self, run_id: &RunId) -> RunExecutionProgress {
let authority = match self.authority.try_lock() {
Ok(authority) => authority,
Err(std::sync::TryLockError::Poisoned(poisoned)) => poisoned.into_inner(),
Err(std::sync::TryLockError::WouldBlock) => return RunExecutionProgress::Unreadable,
};
let state = authority.state();
let current = state
.current_run_id
.as_ref()
.and_then(crate::meerkat_machine::dsl_authority::current_run_id_from_dsl);
let facts = RunTurnStateFacts {
runtime_bound: state.active_runtime_id.is_some(),
run_is_current: current.as_ref() == Some(run_id),
applying_primitive: state.turn_phase == mm_dsl::TurnPhase::ApplyingPrimitive,
};
classify_execution_start(facts, self.turn_start.signal())
}
}
pub(crate) struct StagedRunStartWatchdog {
handle: crate::tokio::task::JoinHandle<()>,
}
impl StagedRunStartWatchdog {
pub(crate) fn spawn(
progress: Arc<dyn RunExecutionProgressSource + 'static>,
run_id: RunId,
input_ids: Vec<InputId>,
staged_at: Instant,
notice_every: Duration,
) -> Self {
let handle = crate::tokio::spawn(async move {
loop {
crate::tokio::time::sleep(notice_every).await;
let observed = progress.observe(&run_id);
if observed.closes_execution_start_window() {
return;
}
let staged_secs = staged_at.elapsed().as_secs();
let inputs = input_ids
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(",");
if observed.execution_start_is_unobservable() {
tracing::warn!(
%run_id,
%inputs,
observed = observed.as_str(),
staged_secs,
"staged run has not returned and its execution start is unobservable; \
the execution-start bound cannot arm for this run"
);
} else {
tracing::error!(
%run_id,
%inputs,
observed = observed.as_str(),
staged_secs,
bound_secs = RUN_EXECUTION_START_BOUND.as_secs(),
"staged run has not begun executing; its consumer accepted the run and \
applied nothing"
);
}
}
});
Self { handle }
}
}
impl Drop for StagedRunStartWatchdog {
fn drop(&mut self) {
self.handle.abort();
}
}
pub(crate) async fn apply_with_execution_start_bound(
executor: &mut dyn CoreExecutor,
progress: &dyn RunExecutionProgressSource,
run_id: RunId,
primitive: RunPrimitive,
staged_at: Instant,
bound: Duration,
) -> Result<CoreApplyOutput, CoreExecutorError> {
let apply_future = executor.apply(run_id.clone(), primitive);
let mut apply_future = std::pin::pin!(apply_future);
let deadline = crate::tokio::time::sleep(bound.saturating_sub(staged_at.elapsed()));
let mut deadline = std::pin::pin!(deadline);
crate::tokio::select! {
biased;
result = &mut apply_future => return result,
() = deadline.as_mut() => {}
}
let observed = progress.observe(&run_id);
let staged_secs = staged_at.elapsed().as_secs();
if !observed.proves_execution_never_started() {
if observed.execution_start_is_unobservable() {
tracing::warn!(
%run_id,
observed = observed.as_str(),
staged_secs,
bound_secs = bound.as_secs(),
"staged run passed its execution-start bound with its start unobservable; \
leaving the run in flight"
);
} else {
tracing::error!(
%run_id,
observed = observed.as_str(),
staged_secs,
bound_secs = bound.as_secs(),
"staged run passed its execution-start bound but non-progress is unproven; \
leaving the run in flight"
);
}
return apply_future.await;
}
tracing::error!(
%run_id,
observed = observed.as_str(),
staged_secs,
bound_secs = bound.as_secs(),
"runtime loop concluded its executor never began executing a staged run; terminalizing the run and handing the executor off"
);
Err(
CoreExecutorError::executor_not_progressing_requires_teardown(format!(
"runtime loop observed run {run_id} with its primitive still un-applied {staged_secs} seconds after staging; the executor never began executing it"
)),
)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use meerkat_core::lifecycle::core_executor::CoreExecutorTeardownReason;
use std::sync::atomic::{AtomicU8, AtomicUsize};
struct ScriptedProgress {
observations: std::sync::Mutex<Vec<RunExecutionProgress>>,
cursor: AtomicU8,
}
impl ScriptedProgress {
fn new(observations: Vec<RunExecutionProgress>) -> Arc<Self> {
Arc::new(Self {
observations: std::sync::Mutex::new(observations),
cursor: AtomicU8::new(0),
})
}
fn observation_count(&self) -> u8 {
self.cursor.load(Ordering::SeqCst)
}
}
impl RunExecutionProgressSource for ScriptedProgress {
fn observe(&self, _run_id: &RunId) -> RunExecutionProgress {
let index = usize::from(self.cursor.fetch_add(1, Ordering::SeqCst));
let observations = self
.observations
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
observations
.get(index)
.copied()
.or_else(|| observations.last().copied())
.unwrap_or(RunExecutionProgress::Unreadable)
}
}
struct ScriptedExecutor {
delay: Option<Duration>,
cancelled: Arc<AtomicBool>,
}
impl ScriptedExecutor {
fn wedged(cancelled: Arc<AtomicBool>) -> Self {
Self {
delay: None,
cancelled,
}
}
fn slow(delay: Duration, cancelled: Arc<AtomicBool>) -> Self {
Self {
delay: Some(delay),
cancelled,
}
}
}
struct CancelWitness {
cancelled: Arc<AtomicBool>,
completed: bool,
}
impl Drop for CancelWitness {
fn drop(&mut self) {
if !self.completed {
self.cancelled.store(true, Ordering::SeqCst);
}
}
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
impl CoreExecutor for ScriptedExecutor {
async fn apply(
&mut self,
_run_id: RunId,
_primitive: RunPrimitive,
) -> Result<CoreApplyOutput, CoreExecutorError> {
let mut witness = CancelWitness {
cancelled: Arc::clone(&self.cancelled),
completed: false,
};
match self.delay {
Some(delay) => {
crate::tokio::time::sleep(delay).await;
witness.completed = true;
Err(CoreExecutorError::Internal("scripted completion".into()))
}
None => {
std::future::pending::<()>().await;
unreachable!("wedged executor never returns")
}
}
}
async fn cancel_after_boundary(
&mut self,
_reason: String,
) -> Result<(), CoreExecutorError> {
Ok(())
}
async fn stop_runtime_executor(
&mut self,
_reason: String,
) -> Result<(), CoreExecutorError> {
Ok(())
}
}
fn run_id() -> RunId {
RunId::new()
}
fn staged_primitive() -> RunPrimitive {
RunPrimitive::StagedInput(meerkat_core::lifecycle::run_primitive::StagedRunInput {
boundary: meerkat_core::lifecycle::run_primitive::RunApplyBoundary::RunStart,
appends: Vec::new(),
contributing_input_ids: Vec::new(),
turn_metadata: None,
})
}
const TEST_BOUND: Duration = Duration::from_secs(900);
#[tokio::test(start_paused = true)]
async fn wedged_consumer_produces_a_typed_bounded_outcome() {
let cancelled = Arc::new(AtomicBool::new(false));
let mut executor = ScriptedExecutor::wedged(Arc::clone(&cancelled));
let progress = ScriptedProgress::new(vec![RunExecutionProgress::PrimitiveUnapplied]);
let run_id = run_id();
let error = crate::tokio::time::timeout(
TEST_BOUND * 4,
apply_with_execution_start_bound(
&mut executor,
progress.as_ref(),
run_id.clone(),
staged_primitive(),
Instant::now(),
TEST_BOUND,
),
)
.await
.expect("a consumer that never began executing must not hang mute")
.expect_err("a consumer that never began executing must produce a typed outcome");
assert!(
matches!(
error,
CoreExecutorError::TeardownRequired {
reason: CoreExecutorTeardownReason::ExecutorNotProgressing,
..
}
),
"expected a typed ExecutorNotProgressing teardown, got {error:?}"
);
assert!(
error.requires_runtime_teardown(),
"a wedged consumer must hand its executor off instead of receiving the next batch"
);
assert_eq!(
progress.observation_count(),
1,
"escalation must rest on exactly one observation, taken at the bound"
);
}
#[tokio::test(start_paused = true)]
async fn slow_but_executing_turn_is_not_disturbed_by_the_bound() {
let cancelled = Arc::new(AtomicBool::new(false));
let mut executor = ScriptedExecutor::slow(TEST_BOUND * 10, Arc::clone(&cancelled));
let progress = ScriptedProgress::new(vec![RunExecutionProgress::Executing]);
let result = apply_with_execution_start_bound(
&mut executor,
progress.as_ref(),
run_id(),
staged_primitive(),
Instant::now(),
TEST_BOUND,
)
.await;
assert!(
matches!(result, Err(CoreExecutorError::Internal(message)) if message == "scripted completion"),
"an executing turn must return its own outcome"
);
assert!(
!cancelled.load(Ordering::SeqCst),
"a live turn must never be cancelled by the execution-start bound"
);
}
#[tokio::test(start_paused = true)]
async fn the_bound_is_measured_from_staging_not_from_apply_entry() {
let cancelled = Arc::new(AtomicBool::new(false));
let mut executor = ScriptedExecutor::wedged(Arc::clone(&cancelled));
let progress = ScriptedProgress::new(vec![RunExecutionProgress::PrimitiveUnapplied]);
let staged_at = Instant::now();
crate::tokio::time::sleep(TEST_BOUND).await;
let elapsed_before = Instant::now();
let error = crate::tokio::time::timeout(
TEST_BOUND * 4,
apply_with_execution_start_bound(
&mut executor,
progress.as_ref(),
run_id(),
staged_primitive(),
staged_at,
TEST_BOUND,
),
)
.await
.expect("a run already past its window must not be granted a fresh one")
.expect_err("a run already past its window must produce a typed outcome");
assert!(
matches!(
error,
CoreExecutorError::TeardownRequired {
reason: CoreExecutorTeardownReason::ExecutorNotProgressing,
..
}
),
"expected a typed ExecutorNotProgressing teardown, got {error:?}"
);
assert!(
elapsed_before.elapsed() < TEST_BOUND,
"pre-apply time must count against the window, not extend it"
);
}
#[tokio::test(start_paused = true)]
async fn unprovable_observations_leave_even_a_wedged_run_in_flight() {
for observed in [
RunExecutionProgress::Unreadable,
RunExecutionProgress::RuntimeUnbound,
RunExecutionProgress::ExecutionStartUnobservable,
RunExecutionProgress::RunNotCurrent,
RunExecutionProgress::Executing,
] {
let cancelled = Arc::new(AtomicBool::new(false));
let mut executor = ScriptedExecutor::wedged(Arc::clone(&cancelled));
let progress = ScriptedProgress::new(vec![observed]);
let outcome = crate::tokio::time::timeout(
TEST_BOUND * 4,
apply_with_execution_start_bound(
&mut executor,
progress.as_ref(),
run_id(),
staged_primitive(),
Instant::now(),
TEST_BOUND,
),
)
.await;
assert!(
outcome.is_err(),
"{} must refuse to terminalize and keep awaiting its consumer",
observed.as_str()
);
}
}
#[tokio::test(start_paused = true)]
async fn unprovable_observations_do_not_disturb_a_slow_but_live_turn() {
for observed in [
RunExecutionProgress::Unreadable,
RunExecutionProgress::RuntimeUnbound,
RunExecutionProgress::ExecutionStartUnobservable,
RunExecutionProgress::RunNotCurrent,
] {
let cancelled = Arc::new(AtomicBool::new(false));
let mut executor = ScriptedExecutor::slow(TEST_BOUND * 10, Arc::clone(&cancelled));
let progress = ScriptedProgress::new(vec![observed]);
let result = apply_with_execution_start_bound(
&mut executor,
progress.as_ref(),
run_id(),
staged_primitive(),
Instant::now(),
TEST_BOUND,
)
.await;
assert!(
matches!(result, Err(CoreExecutorError::Internal(message)) if message == "scripted completion"),
"{} must leave the run in flight rather than terminalize it",
observed.as_str()
);
assert!(
!cancelled.load(Ordering::SeqCst),
"{} must not cancel an in-flight turn",
observed.as_str()
);
}
}
#[test]
fn an_unsignalled_turn_start_is_unobservable_not_executing() {
for applying_primitive in [true, false] {
let facts = RunTurnStateFacts {
runtime_bound: true,
run_is_current: true,
applying_primitive,
};
assert_eq!(
classify_execution_start(facts, TurnStartSignal::NotSignalled),
RunExecutionProgress::ExecutionStartUnobservable,
"an unsignalled turn start must never be read as a fact about this run"
);
}
}
#[test]
fn a_signalled_turn_start_classifies_the_shared_phase_as_this_run() {
let base = RunTurnStateFacts {
runtime_bound: true,
run_is_current: true,
applying_primitive: true,
};
assert_eq!(
classify_execution_start(base, TurnStartSignal::Signalled),
RunExecutionProgress::PrimitiveUnapplied
);
assert_eq!(
classify_execution_start(
RunTurnStateFacts {
applying_primitive: false,
..base
},
TurnStartSignal::Signalled
),
RunExecutionProgress::Executing
);
assert_eq!(
classify_execution_start(
RunTurnStateFacts {
run_is_current: false,
..base
},
TurnStartSignal::Signalled
),
RunExecutionProgress::RunNotCurrent
);
for turn_start in [TurnStartSignal::Signalled, TurnStartSignal::NotSignalled] {
assert_eq!(
classify_execution_start(
RunTurnStateFacts {
runtime_bound: false,
..base
},
turn_start
),
RunExecutionProgress::RuntimeUnbound,
"an unbound runtime is unobservable regardless of the turn-start signal"
);
}
}
#[test]
fn only_positive_facts_close_the_execution_start_window() {
for observed in [
RunExecutionProgress::Executing,
RunExecutionProgress::RunNotCurrent,
] {
assert!(
observed.closes_execution_start_window(),
"{} is a positive fact that closes the window",
observed.as_str()
);
}
for observed in [
RunExecutionProgress::PrimitiveUnapplied,
RunExecutionProgress::Unreadable,
RunExecutionProgress::RuntimeUnbound,
RunExecutionProgress::ExecutionStartUnobservable,
] {
assert!(
!observed.closes_execution_start_window(),
"{} leaves the staged -> executing window open",
observed.as_str()
);
}
}
#[test]
fn only_a_proven_unapplied_primitive_may_terminalize() {
assert!(RunExecutionProgress::PrimitiveUnapplied.proves_execution_never_started());
for observed in [
RunExecutionProgress::Executing,
RunExecutionProgress::RunNotCurrent,
RunExecutionProgress::RuntimeUnbound,
RunExecutionProgress::ExecutionStartUnobservable,
RunExecutionProgress::Unreadable,
] {
assert!(
!observed.proves_execution_never_started(),
"{} is not proof that execution never started",
observed.as_str()
);
}
}
struct CountingProgress {
observation: RunExecutionProgress,
observations: AtomicUsize,
}
impl RunExecutionProgressSource for CountingProgress {
fn observe(&self, _run_id: &RunId) -> RunExecutionProgress {
self.observations.fetch_add(1, Ordering::SeqCst);
self.observation
}
}
#[tokio::test(start_paused = true)]
async fn the_watchdog_keeps_reporting_an_open_window() {
for observation in [
RunExecutionProgress::PrimitiveUnapplied,
RunExecutionProgress::Unreadable,
RunExecutionProgress::ExecutionStartUnobservable,
RunExecutionProgress::RuntimeUnbound,
] {
let progress = Arc::new(CountingProgress {
observation,
observations: AtomicUsize::new(0),
});
let watchdog = StagedRunStartWatchdog::spawn(
Arc::clone(&progress) as Arc<dyn RunExecutionProgressSource>,
run_id(),
vec![InputId::new()],
Instant::now(),
Duration::from_secs(120),
);
crate::tokio::time::sleep(Duration::from_secs(500)).await;
drop(watchdog);
let reported = progress.observations.load(Ordering::SeqCst);
assert!(
reported >= 4,
"{} leaves the window open and must be re-reported every notice interval, got {reported}",
observation.as_str()
);
}
}
#[tokio::test(start_paused = true)]
async fn the_watchdog_stands_down_once_the_run_is_executing() {
let progress = Arc::new(CountingProgress {
observation: RunExecutionProgress::Executing,
observations: AtomicUsize::new(0),
});
let watchdog = StagedRunStartWatchdog::spawn(
Arc::clone(&progress) as Arc<dyn RunExecutionProgressSource>,
run_id(),
vec![InputId::new()],
Instant::now(),
Duration::from_secs(120),
);
crate::tokio::time::sleep(Duration::from_secs(500)).await;
drop(watchdog);
assert_eq!(
progress.observations.load(Ordering::SeqCst),
1,
"a run that began executing must stop being supervised after one observation"
);
}
}