use std::cmp::Reverse;
use std::collections::{BTreeMap, BinaryHeap, VecDeque};
use serde::{Deserialize, Serialize};
use crate::fleet_smoke_profile::{SmokeProfileEvent, SmokeScenario};
use crate::job_identity::{JobIdentity, RemoteBuildId};
use crate::queue_contract::{
AdmissionState, QueueContractResponse, QueueOptions, WaitResult, resolve_queue_contract,
};
pub mod decision {
pub const REMOTE: &str = "remote";
pub const QUEUED: &str = "queued";
pub const LOCAL_FALLBACK: &str = "local_fallback";
pub const QUEUE_TIMEOUT_FALLBACK: &str = "queue_timeout_fallback";
pub const PROOF_REFUSED: &str = "proof_refused";
pub const CANCELLED: &str = "cancelled";
}
pub mod event {
pub const QUEUED: &str = "queued";
pub const STARTED: &str = "started";
pub const PASSED: &str = "passed";
pub const FELL_BACK: &str = "fell_back";
pub const REFUSED: &str = "refused";
pub const CANCELLED: &str = "cancelled";
}
pub const PROOF_REFUSAL_REASON: &str = "RCH-E301";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WorkerEligibility {
Healthy,
Degraded,
TemporaryBypass,
AdminDisabled,
CapabilityInadmissible,
}
impl WorkerEligibility {
#[must_use]
pub const fn is_schedulable(self) -> bool {
matches!(self, Self::Healthy | Self::Degraded)
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Healthy => "healthy",
Self::Degraded => "degraded",
Self::TemporaryBypass => "temporary_bypass",
Self::AdminDisabled => "admin_disabled",
Self::CapabilityInadmissible => "capability_inadmissible",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct StormWorker {
pub id: String,
pub total_slots: u32,
pub speed: f64,
pub eligibility: WorkerEligibility,
}
impl StormWorker {
#[must_use]
pub fn healthy(id: impl Into<String>, total_slots: u32, speed: f64) -> Self {
Self {
id: id.into(),
total_slots,
speed,
eligibility: WorkerEligibility::Healthy,
}
}
#[must_use]
pub fn with_eligibility(mut self, eligibility: WorkerEligibility) -> Self {
self.eligibility = eligibility;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum JobPolicy {
FailOpen,
ForceRemote,
Proof,
}
impl JobPolicy {
#[must_use]
pub const fn allows_local_fallback(self) -> bool {
matches!(self, Self::FailOpen | Self::ForceRemote)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum JobKind {
Build,
Test,
Check,
}
impl JobKind {
#[must_use]
pub const fn fingerprint(self) -> &'static str {
match self {
Self::Build => "cargo build",
Self::Test => "cargo test",
Self::Check => "cargo check",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct StormJob {
pub runtime_ms: u64,
pub slots: u32,
pub project_root: String,
pub policy: JobPolicy,
pub kind: JobKind,
pub cancel_when_queued: bool,
}
impl StormJob {
#[must_use]
pub fn build(runtime_ms: u64, slots: u32, project_root: impl Into<String>) -> Self {
Self {
runtime_ms,
slots,
project_root: project_root.into(),
policy: JobPolicy::FailOpen,
kind: JobKind::Build,
cancel_when_queued: false,
}
}
#[must_use]
pub fn with_policy(mut self, policy: JobPolicy) -> Self {
self.policy = policy;
self
}
#[must_use]
pub fn with_kind(mut self, kind: JobKind) -> Self {
self.kind = kind;
self
}
#[must_use]
pub fn cancelling(mut self) -> Self {
self.cancel_when_queued = true;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StormConfig {
pub run_id: String,
pub bead_id: String,
pub max_queue_depth: usize,
pub queue_timeout_ms: u64,
pub fairness_window: usize,
pub cancel_delay_ms: u64,
}
impl StormConfig {
#[must_use]
pub fn new(run_id: impl Into<String>, bead_id: impl Into<String>) -> Self {
Self {
run_id: run_id.into(),
bead_id: bead_id.into(),
max_queue_depth: 64,
queue_timeout_ms: 30_000,
fairness_window: 16,
cancel_delay_ms: 5,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StormSummary {
pub total_jobs: u32,
pub remote_successes: u32,
pub local_fallbacks: u32,
pub proof_refusals: u32,
pub queue_timeouts: u32,
pub cancellations: u32,
pub p95_queue_wait_ms: u64,
pub p95_end_to_end_ms: u64,
pub per_worker_slot_utilization: BTreeMap<String, f64>,
}
#[derive(Debug, Clone)]
pub struct StormRun {
pub events: Vec<SmokeProfileEvent>,
pub summary: StormSummary,
}
impl StormRun {
pub fn to_jsonl(&self) -> Result<String, serde_json::Error> {
let mut out = String::new();
for ev in &self.events {
out.push_str(&serde_json::to_string(ev)?);
out.push('\n');
}
Ok(out)
}
}
#[derive(Debug, Clone, Copy)]
enum Disposition {
Remote { remote_id: RemoteBuildId },
LocalFallback,
QueueTimeoutFallback,
ProofRefused,
Cancelled,
}
#[derive(Debug)]
struct JobState {
local_id: String,
enqueued_at: Option<u64>,
queue_wait_ms: u64,
started_at: Option<u64>,
disposition: Option<Disposition>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum EvKind {
Arrival(usize),
Completion {
worker: usize,
slots: u32,
job: usize,
},
Timeout(usize),
Cancel(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Ev {
time: u64,
seq: u64,
kind: EvKind,
}
impl Ord for Ev {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.time.cmp(&other.time).then(self.seq.cmp(&other.seq))
}
}
impl PartialOrd for Ev {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[must_use]
pub fn simulate_storm(workers: &[StormWorker], jobs: &[StormJob], cfg: &StormConfig) -> StormRun {
let mut sim = Sim::new(workers, jobs, cfg);
sim.run();
sim.finish()
}
struct Sim<'a> {
workers: &'a [StormWorker],
jobs: &'a [StormJob],
cfg: &'a StormConfig,
used: Vec<u32>,
busy_slot_ms: Vec<u128>,
selection_log: VecDeque<usize>,
state: Vec<JobState>,
queue: VecDeque<usize>,
heap: BinaryHeap<Reverse<Ev>>,
events: Vec<SmokeProfileEvent>,
next_remote_id: RemoteBuildId,
next_seq: u64,
clock: u64,
makespan: u64,
queue_timeouts: u32,
}
impl<'a> Sim<'a> {
fn new(workers: &'a [StormWorker], jobs: &'a [StormJob], cfg: &'a StormConfig) -> Self {
let state = (0..jobs.len())
.map(|i| JobState {
local_id: format!("{}{i:04}", crate::job_identity::LOCAL_WRAPPER_ID_PREFIX),
enqueued_at: None,
queue_wait_ms: 0,
started_at: None,
disposition: None,
})
.collect();
let mut heap = BinaryHeap::new();
for (i, _) in jobs.iter().enumerate() {
heap.push(Reverse(Ev {
time: 0,
seq: i as u64,
kind: EvKind::Arrival(i),
}));
}
Self {
workers,
jobs,
cfg,
used: vec![0; workers.len()],
busy_slot_ms: vec![0; workers.len()],
selection_log: VecDeque::new(),
state,
queue: VecDeque::new(),
heap,
events: Vec::new(),
next_remote_id: 1,
next_seq: jobs.len() as u64,
clock: 0,
makespan: 0,
queue_timeouts: 0,
}
}
fn seq(&mut self) -> u64 {
let s = self.next_seq;
self.next_seq += 1;
s
}
fn run(&mut self) {
while let Some(Reverse(ev)) = self.heap.pop() {
self.clock = ev.time;
match ev.kind {
EvKind::Arrival(job) => self.on_arrival(job),
EvKind::Completion { worker, slots, job } => self.on_completion(worker, slots, job),
EvKind::Timeout(job) => self.on_timeout(job),
EvKind::Cancel(job) => self.on_cancel(job),
}
}
let stragglers: Vec<usize> = self.queue.drain(..).collect();
for job in stragglers {
if self.state[job].disposition.is_none() {
self.dispose_timeout(job);
}
}
}
fn try_select(&self, slots: u32) -> Option<usize> {
let mut best: Option<(usize, f64)> = None;
for (i, w) in self.workers.iter().enumerate() {
if !w.eligibility.is_schedulable() {
continue;
}
if w.total_slots < slots {
continue; }
if w.total_slots - self.used[i] < slots {
continue; }
let recent = self.selection_log.iter().filter(|&&x| x == i).count();
let weight = w.speed / (1.0 + recent as f64);
match best {
Some((_, bw)) if weight <= bw => {}
_ => best = Some((i, weight)),
}
}
best.map(|(i, _)| i)
}
fn ever_feasible(&self, slots: u32) -> bool {
self.workers
.iter()
.any(|w| w.eligibility.is_schedulable() && w.total_slots >= slots)
}
fn queue_depth_u32(&self) -> u32 {
u32::try_from(self.queue.len()).unwrap_or(u32::MAX)
}
fn on_arrival(&mut self, job: usize) {
let spec = &self.jobs[job];
if !self.ever_feasible(spec.slots) {
if spec.policy.allows_local_fallback() {
self.dispose_local_fallback(job, decision::LOCAL_FALLBACK);
} else {
self.dispose_proof_refused(job);
}
return;
}
if let Some(w) = self.try_select(spec.slots) {
self.place(job, w);
return;
}
if !spec.policy.allows_local_fallback() {
self.dispose_proof_refused(job);
return;
}
if self.queue.len() >= self.cfg.max_queue_depth {
self.dispose_local_fallback(job, decision::LOCAL_FALLBACK);
return;
}
self.enqueue(job);
}
fn enqueue(&mut self, job: usize) {
self.state[job].enqueued_at = Some(self.clock);
self.queue.push_back(job);
let depth = self.queue_depth_u32();
let guidance = queued_guidance(&self.state[job].local_id);
self.emit(
job,
event::QUEUED,
"queued",
decision::QUEUED,
None,
None,
Some(depth),
0,
Some(guidance),
None,
);
let timeout_at = self.clock + self.cfg.queue_timeout_ms;
let s = self.seq();
self.heap.push(Reverse(Ev {
time: timeout_at,
seq: s,
kind: EvKind::Timeout(job),
}));
if self.jobs[job].cancel_when_queued {
let cancel_at = self.clock + self.cfg.cancel_delay_ms;
let s = self.seq();
self.heap.push(Reverse(Ev {
time: cancel_at,
seq: s,
kind: EvKind::Cancel(job),
}));
}
}
fn place(&mut self, job: usize, worker: usize) {
let spec = &self.jobs[job];
let slots = spec.slots;
let runtime = spec.runtime_ms;
self.used[worker] += slots;
self.busy_slot_ms[worker] += u128::from(slots) * u128::from(runtime);
self.selection_log.push_back(worker);
while self.selection_log.len() > self.cfg.fairness_window {
self.selection_log.pop_front();
}
let wait = self.state[job]
.enqueued_at
.map_or(0, |e| self.clock.saturating_sub(e));
self.state[job].queue_wait_ms = wait;
self.state[job].started_at = Some(self.clock);
let remote_id = self.next_remote_id;
self.next_remote_id += 1;
let mut identity = JobIdentity::new_local();
identity.local_wrapper_id = self.state[job].local_id.clone();
identity.admit(remote_id);
self.state[job].disposition = Some(Disposition::Remote { remote_id });
let worker_id = self.workers[worker].id.clone();
let depth = self.queue_depth_u32();
self.emit(
job,
event::STARTED,
"run",
decision::REMOTE,
Some(worker_id.clone()),
Some(worker_id.clone()),
Some(depth),
0,
Some("admitted and running".to_string()),
Some(remote_id),
);
let finish = self.clock + runtime;
self.makespan = self.makespan.max(finish);
let s = self.seq();
self.heap.push(Reverse(Ev {
time: finish,
seq: s,
kind: EvKind::Completion { worker, slots, job },
}));
}
fn on_completion(&mut self, worker: usize, slots: u32, job: usize) {
self.used[worker] = self.used[worker].saturating_sub(slots);
let runtime = self.jobs[job].runtime_ms;
let worker_id = self.workers[worker].id.clone();
let remote_id = match self.state[job].disposition {
Some(Disposition::Remote { remote_id }) => Some(remote_id),
_ => None,
};
self.emit(
job,
event::PASSED,
"ok",
decision::REMOTE,
Some(worker_id.clone()),
Some(worker_id),
None,
runtime,
Some("completed remotely".to_string()),
remote_id,
);
self.drain_queue();
}
fn drain_queue(&mut self) {
while let Some(&job) = self.queue.front() {
if self.state[job].disposition.is_some() {
self.queue.pop_front();
continue;
}
let slots = self.jobs[job].slots;
if let Some(w) = self.try_select(slots) {
self.queue.pop_front();
self.place(job, w);
} else {
break; }
}
}
fn on_timeout(&mut self, job: usize) {
if self.state[job].disposition.is_some() || self.state[job].started_at.is_some() {
return; }
self.remove_from_queue(job);
self.dispose_timeout(job);
}
fn on_cancel(&mut self, job: usize) {
if self.state[job].disposition.is_some() || self.state[job].started_at.is_some() {
return;
}
self.remove_from_queue(job);
self.dispose_cancelled(job);
}
fn remove_from_queue(&mut self, job: usize) {
if let Some(pos) = self.queue.iter().position(|&j| j == job) {
self.queue.remove(pos);
}
}
fn dispose_timeout(&mut self, job: usize) {
self.queue_timeouts += 1;
let wait = self.state[job]
.enqueued_at
.map_or(self.cfg.queue_timeout_ms, |e| {
(self.clock.saturating_sub(e)).min(self.cfg.queue_timeout_ms)
});
self.state[job].queue_wait_ms = wait;
self.state[job].disposition = Some(Disposition::QueueTimeoutFallback);
let guidance = queue_timeout_guidance(&self.state[job].local_id, self.cfg.queue_timeout_ms);
self.emit(
job,
event::FELL_BACK,
"local",
decision::QUEUE_TIMEOUT_FALLBACK,
None,
None,
None,
self.jobs[job].runtime_ms,
Some(guidance),
None,
);
}
fn dispose_local_fallback(&mut self, job: usize, decision_tok: &str) {
self.state[job].disposition = Some(Disposition::LocalFallback);
let guidance = local_fallback_guidance();
self.emit(
job,
event::FELL_BACK,
"local",
decision_tok,
None,
None,
None,
self.jobs[job].runtime_ms,
Some(guidance),
None,
);
}
fn dispose_proof_refused(&mut self, job: usize) {
self.state[job].disposition = Some(Disposition::ProofRefused);
self.emit(
job,
event::REFUSED,
"refused",
decision::PROOF_REFUSED,
None,
None,
None,
0,
Some(
"proof mode requires immediate remote admission; refused local fallback"
.to_string(),
),
None,
);
if let Some(ev) = self.events.last_mut() {
ev.reason_code = Some(PROOF_REFUSAL_REASON.to_string());
}
}
fn dispose_cancelled(&mut self, job: usize) {
let wait = self.state[job]
.enqueued_at
.map_or(0, |e| self.clock.saturating_sub(e));
self.state[job].queue_wait_ms = wait;
self.state[job].disposition = Some(Disposition::Cancelled);
let guidance = cancel_guidance(&self.state[job].local_id);
self.emit(
job,
event::CANCELLED,
"cancelled",
decision::CANCELLED,
None,
None,
None,
0,
Some(guidance),
None,
);
}
#[allow(clippy::too_many_arguments)] fn emit(
&mut self,
job: usize,
event_tok: &str,
status_tok: &str,
decision_tok: &str,
worker_id: Option<String>,
selected_worker: Option<String>,
queue_depth: Option<u32>,
duration_ms: u64,
detail: Option<String>,
remote_id: Option<RemoteBuildId>,
) {
let ev = make_load_event(
&self.cfg.run_id,
&self.cfg.bead_id,
self.jobs[job].kind,
&self.state[job].local_id,
event_tok,
status_tok,
decision_tok,
worker_id,
selected_worker,
queue_depth,
duration_ms,
detail,
remote_id,
);
self.events.push(ev);
}
fn finish(self) -> StormRun {
let mut remote_successes = 0u32;
let mut local_fallbacks = 0u32;
let mut proof_refusals = 0u32;
let mut cancellations = 0u32;
let mut queue_waits = Vec::new();
let mut end_to_ends = Vec::new();
for (i, st) in self.state.iter().enumerate() {
let runtime = self.jobs[i].runtime_ms;
queue_waits.push(st.queue_wait_ms);
match st.disposition {
Some(Disposition::Remote { .. }) => {
remote_successes += 1;
end_to_ends.push(st.queue_wait_ms + runtime);
}
Some(Disposition::LocalFallback) => {
local_fallbacks += 1;
end_to_ends.push(st.queue_wait_ms + runtime);
}
Some(Disposition::QueueTimeoutFallback) => {
local_fallbacks += 1;
end_to_ends.push(st.queue_wait_ms + runtime);
}
Some(Disposition::ProofRefused) => {
proof_refusals += 1;
end_to_ends.push(st.queue_wait_ms);
}
Some(Disposition::Cancelled) => {
cancellations += 1;
end_to_ends.push(st.queue_wait_ms);
}
None => {
end_to_ends.push(st.queue_wait_ms);
}
}
}
let makespan = self.makespan.max(1);
let mut per_worker_slot_utilization = BTreeMap::new();
for (i, w) in self.workers.iter().enumerate() {
let capacity = u128::from(w.total_slots) * u128::from(makespan);
#[allow(clippy::cast_precision_loss)]
let util = if capacity == 0 {
0.0
} else {
self.busy_slot_ms[i] as f64 / capacity as f64
};
per_worker_slot_utilization.insert(w.id.clone(), util);
}
let summary = StormSummary {
total_jobs: u32::try_from(self.jobs.len()).unwrap_or(u32::MAX),
remote_successes,
local_fallbacks,
proof_refusals,
queue_timeouts: self.queue_timeouts,
cancellations,
p95_queue_wait_ms: percentile(&mut queue_waits, 95),
p95_end_to_end_ms: percentile(&mut end_to_ends, 95),
per_worker_slot_utilization,
};
StormRun {
events: self.events,
summary,
}
}
}
#[must_use]
fn percentile(values: &mut [u64], p: u64) -> u64 {
if values.is_empty() {
return 0;
}
values.sort_unstable();
let n = values.len() as u64;
let rank = (p * n).div_ceil(100).max(1);
let idx = (rank - 1).min(n - 1) as usize;
values[idx]
}
#[allow(clippy::too_many_arguments)] fn make_load_event(
run_id: &str,
bead_id: &str,
kind: JobKind,
local_id: &str,
event_tok: &str,
status_tok: &str,
decision_tok: &str,
worker_id: Option<String>,
selected_worker: Option<String>,
queue_depth: Option<u32>,
duration_ms: u64,
detail: Option<String>,
remote_id: Option<RemoteBuildId>,
) -> SmokeProfileEvent {
let mut ev = SmokeProfileEvent::started(
run_id.to_string(),
bead_id.to_string(),
worker_id,
SmokeScenario::LoadStormControl,
);
ev.event = event_tok.to_string();
ev.status = status_tok.to_string();
ev.duration_ms = duration_ms;
ev.command_fingerprint = Some(kind.fingerprint().to_string());
ev = ev
.with_job_ids(Some(local_id.to_string()), remote_id)
.with_selected_worker(selected_worker)
.with_fallback_decision(decision_tok);
if let Some(d) = queue_depth {
ev = ev.with_queue_depth(d);
}
if let Some(d) = detail {
ev = ev.with_detail(d);
}
ev
}
fn queued_guidance(local_id: &str) -> String {
QueueContractResponse::build(
&resolve_queue_contract(&AdmissionState::Queued, &QueueOptions::default(), None),
Some(local_id.to_string()),
)
.render()
}
fn local_fallback_guidance() -> String {
QueueContractResponse::build(
&resolve_queue_contract(
&AdmissionState::RejectedBeforeAdmission(
"fleet at capacity; ran locally (fail-open)".to_string(),
),
&QueueOptions::default(),
None,
),
None,
)
.detail
}
fn queue_timeout_guidance(local_id: &str, queue_timeout_ms: u64) -> String {
QueueContractResponse::build(
&resolve_queue_contract(
&AdmissionState::Queued,
&QueueOptions {
wait: true,
wait_timeout_secs: Some(queue_timeout_ms / 1000),
..QueueOptions::default()
},
Some(WaitResult::TimedOut),
),
Some(local_id.to_string()),
)
.render()
}
fn cancel_guidance(local_id: &str) -> String {
QueueContractResponse::build(
&resolve_queue_contract(
&AdmissionState::Queued,
&QueueOptions {
wait: true,
..QueueOptions::default()
},
Some(WaitResult::CancelledBeforeStart),
),
Some(local_id.to_string()),
)
.render()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LiveDisposition {
Remote,
LocalFallback,
QueueTimeoutFallback,
ProofRefused,
Cancelled,
}
impl LiveDisposition {
#[must_use]
pub const fn is_remote(self) -> bool {
matches!(self, Self::Remote)
}
#[must_use]
pub const fn is_local_fallback(self) -> bool {
matches!(self, Self::LocalFallback | Self::QueueTimeoutFallback)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct LiveJobOutcome {
pub local_job_id: String,
pub remote_job_id: Option<RemoteBuildId>,
pub selected_worker: Option<String>,
pub slots: u32,
pub kind: JobKind,
pub disposition: LiveDisposition,
pub queued: bool,
pub queue_wait_ms: u64,
pub runtime_ms: u64,
pub queue_depth: Option<u32>,
}
impl LiveJobOutcome {
#[must_use]
pub fn remote(
local_job_id: impl Into<String>,
worker: impl Into<String>,
remote_id: RemoteBuildId,
slots: u32,
runtime_ms: u64,
) -> Self {
Self {
local_job_id: local_job_id.into(),
remote_job_id: Some(remote_id),
selected_worker: Some(worker.into()),
slots,
kind: JobKind::Check,
disposition: LiveDisposition::Remote,
queued: false,
queue_wait_ms: 0,
runtime_ms,
queue_depth: None,
}
}
#[must_use]
pub fn local_fallback(local_job_id: impl Into<String>, slots: u32, runtime_ms: u64) -> Self {
Self {
local_job_id: local_job_id.into(),
remote_job_id: None,
selected_worker: None,
slots,
kind: JobKind::Check,
disposition: LiveDisposition::LocalFallback,
queued: false,
queue_wait_ms: 0,
runtime_ms,
queue_depth: None,
}
}
#[must_use]
pub fn with_kind(mut self, kind: JobKind) -> Self {
self.kind = kind;
self
}
#[must_use]
pub fn queued_for(mut self, wait_ms: u64, depth: Option<u32>) -> Self {
self.queued = true;
self.queue_wait_ms = wait_ms;
self.queue_depth = depth;
self
}
#[must_use]
pub fn with_disposition(mut self, disposition: LiveDisposition) -> Self {
self.disposition = disposition;
self
}
}
#[must_use]
pub fn build_live_storm_run(
cfg: &StormConfig,
workers: &[StormWorker],
outcomes: &[LiveJobOutcome],
) -> StormRun {
let mut events = Vec::new();
for o in outcomes {
if o.queued {
events.push(make_load_event(
&cfg.run_id,
&cfg.bead_id,
o.kind,
&o.local_job_id,
event::QUEUED,
"queued",
decision::QUEUED,
None,
None,
o.queue_depth,
0,
Some(queued_guidance(&o.local_job_id)),
None,
));
}
match o.disposition {
LiveDisposition::Remote => {
events.push(make_load_event(
&cfg.run_id,
&cfg.bead_id,
o.kind,
&o.local_job_id,
event::STARTED,
"run",
decision::REMOTE,
o.selected_worker.clone(),
o.selected_worker.clone(),
o.queue_depth,
0,
Some("admitted and running".to_string()),
o.remote_job_id,
));
events.push(make_load_event(
&cfg.run_id,
&cfg.bead_id,
o.kind,
&o.local_job_id,
event::PASSED,
"ok",
decision::REMOTE,
o.selected_worker.clone(),
o.selected_worker.clone(),
None,
o.runtime_ms,
Some("completed remotely".to_string()),
o.remote_job_id,
));
}
LiveDisposition::LocalFallback => {
events.push(make_load_event(
&cfg.run_id,
&cfg.bead_id,
o.kind,
&o.local_job_id,
event::FELL_BACK,
"local",
decision::LOCAL_FALLBACK,
None,
None,
None,
o.runtime_ms,
Some(local_fallback_guidance()),
None,
));
}
LiveDisposition::QueueTimeoutFallback => {
events.push(make_load_event(
&cfg.run_id,
&cfg.bead_id,
o.kind,
&o.local_job_id,
event::FELL_BACK,
"local",
decision::QUEUE_TIMEOUT_FALLBACK,
None,
None,
None,
o.runtime_ms,
Some(queue_timeout_guidance(
&o.local_job_id,
cfg.queue_timeout_ms,
)),
None,
));
}
LiveDisposition::ProofRefused => {
let mut ev = make_load_event(
&cfg.run_id,
&cfg.bead_id,
o.kind,
&o.local_job_id,
event::REFUSED,
"refused",
decision::PROOF_REFUSED,
None,
None,
None,
0,
Some(
"proof mode requires immediate remote admission; refused local fallback"
.to_string(),
),
None,
);
ev.reason_code = Some(PROOF_REFUSAL_REASON.to_string());
events.push(ev);
}
LiveDisposition::Cancelled => {
events.push(make_load_event(
&cfg.run_id,
&cfg.bead_id,
o.kind,
&o.local_job_id,
event::CANCELLED,
"cancelled",
decision::CANCELLED,
None,
None,
None,
0,
Some(cancel_guidance(&o.local_job_id)),
None,
));
}
}
}
let summary = summarize_live(workers, outcomes);
StormRun { events, summary }
}
fn summarize_live(workers: &[StormWorker], outcomes: &[LiveJobOutcome]) -> StormSummary {
let mut remote_successes = 0u32;
let mut local_fallbacks = 0u32;
let mut proof_refusals = 0u32;
let mut queue_timeouts = 0u32;
let mut cancellations = 0u32;
let mut queue_waits = Vec::with_capacity(outcomes.len());
let mut end_to_ends = Vec::with_capacity(outcomes.len());
let mut busy_slot_ms: BTreeMap<&str, u128> = BTreeMap::new();
let mut makespan = 0u64;
for o in outcomes {
queue_waits.push(o.queue_wait_ms);
let end_to_end = o.queue_wait_ms.saturating_add(o.runtime_ms);
match o.disposition {
LiveDisposition::Remote => {
remote_successes += 1;
end_to_ends.push(end_to_end);
makespan = makespan.max(end_to_end);
if let Some(w) = o.selected_worker.as_deref() {
*busy_slot_ms.entry(w).or_default() +=
u128::from(o.slots) * u128::from(o.runtime_ms);
}
}
LiveDisposition::LocalFallback => {
local_fallbacks += 1;
end_to_ends.push(end_to_end);
makespan = makespan.max(end_to_end);
}
LiveDisposition::QueueTimeoutFallback => {
local_fallbacks += 1;
queue_timeouts += 1;
end_to_ends.push(end_to_end);
makespan = makespan.max(end_to_end);
}
LiveDisposition::ProofRefused => {
proof_refusals += 1;
end_to_ends.push(o.queue_wait_ms);
}
LiveDisposition::Cancelled => {
cancellations += 1;
end_to_ends.push(o.queue_wait_ms);
}
}
}
let makespan = makespan.max(1);
let mut per_worker_slot_utilization = BTreeMap::new();
for w in workers {
let capacity = u128::from(w.total_slots) * u128::from(makespan);
let busy = busy_slot_ms.get(w.id.as_str()).copied().unwrap_or(0);
#[allow(clippy::cast_precision_loss)]
let util = if capacity == 0 {
0.0
} else {
busy as f64 / capacity as f64
};
per_worker_slot_utilization.insert(w.id.clone(), util);
}
StormSummary {
total_jobs: u32::try_from(outcomes.len()).unwrap_or(u32::MAX),
remote_successes,
local_fallbacks,
proof_refusals,
queue_timeouts,
cancellations,
p95_queue_wait_ms: percentile(&mut queue_waits, 95),
p95_end_to_end_ms: percentile(&mut end_to_ends, 95),
per_worker_slot_utilization,
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InvariantReport {
pub name: String,
pub passed: bool,
pub detail: String,
pub violations: Vec<String>,
}
impl InvariantReport {
fn pass(name: &str, detail: impl Into<String>) -> Self {
Self {
name: name.to_string(),
passed: true,
detail: detail.into(),
violations: Vec::new(),
}
}
fn fail(name: &str, detail: impl Into<String>, violations: Vec<String>) -> Self {
Self {
name: name.to_string(),
passed: false,
detail: detail.into(),
violations,
}
}
}
#[must_use]
pub fn check_load_fairness(
events: &[SmokeProfileEvent],
workers: &[StormWorker],
tolerance: f64,
) -> InvariantReport {
const NAME: &str = "load_fairness";
let schedulable: Vec<&StormWorker> = workers
.iter()
.filter(|w| w.eligibility.is_schedulable())
.collect();
if schedulable.len() <= 1 {
return InvariantReport::pass(NAME, "single schedulable worker; fairness trivial");
}
let mut counts: BTreeMap<&str, u32> = BTreeMap::new();
for w in &schedulable {
counts.insert(w.id.as_str(), 0);
}
let mut total = 0u32;
for ev in events {
if ev.event == event::STARTED
&& let Some(sel) = ev.selected_worker.as_deref()
&& let Some(c) = counts.get_mut(sel)
{
*c += 1;
total += 1;
}
}
if total == 0 {
return InvariantReport::pass(NAME, "no remote placements to spread");
}
let fair_share = f64::from(total) / schedulable.len() as f64;
let cap = fair_share * tolerance;
let mut violations = Vec::new();
for (id, c) in &counts {
if f64::from(*c) > cap {
violations.push(format!(
"worker {id} took {c} of {total} placements (cap {cap:.1}, fair share {fair_share:.1})"
));
}
}
if violations.is_empty() {
InvariantReport::pass(
NAME,
format!("{total} placements spread within {tolerance:.2}× fair share"),
)
} else {
InvariantReport::fail(NAME, "load not spread fairly", violations)
}
}
#[must_use]
pub fn check_no_duplicate_remote_job_ids(events: &[SmokeProfileEvent]) -> InvariantReport {
const NAME: &str = "no_duplicate_remote_job_ids";
let mut owners: BTreeMap<u64, std::collections::BTreeSet<String>> = BTreeMap::new();
for ev in events {
if let Some(rid) = ev.remote_job_id {
let local = ev.local_job_id.clone().unwrap_or_default();
owners.entry(rid).or_default().insert(local);
}
}
let mut violations = Vec::new();
for (rid, locals) in &owners {
if locals.len() > 1 {
violations.push(format!(
"remote job id {rid} shared by local jobs {locals:?}"
));
}
}
if violations.is_empty() {
InvariantReport::pass(
NAME,
format!("{} distinct remote job ids, none shared", owners.len()),
)
} else {
InvariantReport::fail(NAME, "remote job id collision", violations)
}
}
#[must_use]
pub fn check_no_unbounded_local_fallback_storm(
events: &[SmokeProfileEvent],
summary: &StormSummary,
max_fallback_ratio: f64,
) -> InvariantReport {
const NAME: &str = "no_unbounded_local_fallback_storm";
let _ = events;
if summary.total_jobs == 0 {
return InvariantReport::pass(NAME, "no jobs");
}
let ratio = f64::from(summary.local_fallbacks) / f64::from(summary.total_jobs);
if ratio <= max_fallback_ratio {
InvariantReport::pass(
NAME,
format!(
"{}/{} jobs fell back to local ({:.1}% <= {:.1}% cap)",
summary.local_fallbacks,
summary.total_jobs,
ratio * 100.0,
max_fallback_ratio * 100.0
),
)
} else {
InvariantReport::fail(
NAME,
"local fallback storm: too many jobs bypassed the fleet",
vec![format!(
"{}/{} fell back ({:.1}% > {:.1}% cap)",
summary.local_fallbacks,
summary.total_jobs,
ratio * 100.0,
max_fallback_ratio * 100.0
)],
)
}
}
#[must_use]
pub fn check_attach_cancel_guidance(events: &[SmokeProfileEvent]) -> InvariantReport {
const NAME: &str = "attach_cancel_guidance";
use std::collections::BTreeSet;
let terminal = [
event::PASSED,
event::FELL_BACK,
event::REFUSED,
event::CANCELLED,
];
let not_started_terminal = [event::FELL_BACK, event::CANCELLED];
let mut all_jobs: BTreeSet<String> = BTreeSet::new();
let mut terminated: BTreeSet<String> = BTreeSet::new();
let mut violations = Vec::new();
for ev in events {
let Some(local) = ev.local_job_id.clone() else {
continue;
};
all_jobs.insert(local.clone());
if terminal.contains(&ev.event.as_str()) {
terminated.insert(local.clone());
}
if not_started_terminal.contains(&ev.event.as_str()) || ev.event == event::QUEUED {
let has_guidance = ev.detail.as_deref().is_some_and(|d| !d.trim().is_empty());
if !has_guidance {
violations.push(format!(
"job {local} event '{}' lacks attach/cancel guidance",
ev.event
));
}
}
}
for job in all_jobs.difference(&terminated) {
violations.push(format!(
"job {job} never reached a terminal event (stuck wrapper)"
));
}
if violations.is_empty() {
InvariantReport::pass(
NAME,
format!("{} jobs all terminated with guidance", terminated.len()),
)
} else {
InvariantReport::fail(NAME, "stuck wrapper or missing guidance", violations)
}
}
#[must_use]
pub fn check_no_ineligible_worker_selected(
events: &[SmokeProfileEvent],
workers: &[StormWorker],
) -> InvariantReport {
const NAME: &str = "no_ineligible_worker_selected";
let ineligible: BTreeMap<&str, WorkerEligibility> = workers
.iter()
.filter(|w| !w.eligibility.is_schedulable())
.map(|w| (w.id.as_str(), w.eligibility))
.collect();
let mut violations = Vec::new();
for ev in events {
for candidate in [ev.selected_worker.as_deref(), ev.worker_id.as_deref()]
.into_iter()
.flatten()
{
if let Some(elig) = ineligible.get(candidate)
&& (ev.event == event::STARTED || ev.event == event::PASSED)
{
violations.push(format!(
"ineligible worker {candidate} ({}) received work on event '{}' (job {:?})",
elig.as_str(),
ev.event,
ev.local_job_id
));
}
}
}
if violations.is_empty() {
InvariantReport::pass(
NAME,
format!("{} ineligible worker(s) received no work", ineligible.len()),
)
} else {
InvariantReport::fail(NAME, "ineligible worker received work", violations)
}
}
#[must_use]
pub fn check_all_invariants(
run: &StormRun,
workers: &[StormWorker],
fairness_tolerance: f64,
max_fallback_ratio: f64,
) -> Vec<InvariantReport> {
vec![
check_load_fairness(&run.events, workers, fairness_tolerance),
check_no_duplicate_remote_job_ids(&run.events),
check_no_unbounded_local_fallback_storm(&run.events, &run.summary, max_fallback_ratio),
check_attach_cancel_guidance(&run.events),
check_no_ineligible_worker_selected(&run.events, workers),
]
}
#[must_use]
pub fn all_passed(reports: &[InvariantReport]) -> bool {
reports.iter().all(|r| r.passed)
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg() -> StormConfig {
StormConfig::new("storm-run-1", "bd-session-history-remediation-ocv9i.10.4")
}
fn healthy_fleet() -> Vec<StormWorker> {
vec![
StormWorker::healthy("w1", 4, 100.0),
StormWorker::healthy("w2", 4, 100.0),
StormWorker::healthy("w3", 4, 100.0),
]
}
fn build_jobs(n: usize, runtime: u64, slots: u32) -> Vec<StormJob> {
(0..n)
.map(|i| StormJob::build(runtime, slots, format!("/data/projects/p{}", i % 4)))
.collect()
}
#[test]
fn percentile_nearest_rank() {
let mut v = vec![10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
assert_eq!(percentile(&mut v, 95), 100);
let mut v2 = vec![5];
assert_eq!(percentile(&mut v2, 95), 5);
let mut empty: Vec<u64> = vec![];
assert_eq!(percentile(&mut empty, 95), 0);
let mut v3 = vec![1, 2, 3, 4];
assert_eq!(percentile(&mut v3, 50), 2); }
#[test]
fn healthy_storm_upholds_all_invariants() {
let workers = healthy_fleet();
let jobs = build_jobs(24, 1000, 1);
let run = simulate_storm(&workers, &jobs, &cfg());
let reports = check_all_invariants(&run, &workers, 1.5, 0.25);
for r in &reports {
assert!(r.passed, "invariant {} failed: {:?}", r.name, r.violations);
}
assert_eq!(run.summary.total_jobs, 24);
assert_eq!(run.summary.remote_successes, 24);
assert_eq!(run.summary.local_fallbacks, 0);
}
#[test]
fn fairness_spreads_load_across_homogeneous_fleet() {
let workers = healthy_fleet();
let jobs = build_jobs(30, 500, 1);
let run = simulate_storm(&workers, &jobs, &cfg());
let report = check_load_fairness(&run.events, &workers, 1.4);
assert!(report.passed, "fairness: {:?}", report.violations);
for w in &workers {
let util = run.summary.per_worker_slot_utilization[&w.id];
assert!(util > 0.0, "worker {} starved (util 0)", w.id);
}
}
#[test]
fn unique_remote_job_ids_under_storm() {
let workers = healthy_fleet();
let jobs = build_jobs(50, 300, 1);
let run = simulate_storm(&workers, &jobs, &cfg());
let report = check_no_duplicate_remote_job_ids(&run.events);
assert!(report.passed, "{:?}", report.violations);
}
#[test]
fn queue_full_falls_back_locally_but_not_unboundedly() {
let workers = vec![StormWorker::healthy("only", 1, 100.0)];
let mut c = cfg();
c.max_queue_depth = 2;
c.queue_timeout_ms = 10; let jobs = build_jobs(20, 1000, 1);
let run = simulate_storm(&workers, &jobs, &c);
assert_eq!(run.summary.total_jobs, 20);
let guidance = check_attach_cancel_guidance(&run.events);
assert!(guidance.passed, "{:?}", guidance.violations);
let total_resolved = run.summary.remote_successes
+ run.summary.local_fallbacks
+ run.summary.proof_refusals
+ run.summary.cancellations;
assert_eq!(total_resolved, 20, "every job must resolve");
let strict = check_no_unbounded_local_fallback_storm(&run.events, &run.summary, 0.10);
assert!(!strict.passed, "low cap should flag the fallback storm");
}
#[test]
fn proof_jobs_refuse_rather_than_fall_back() {
let workers = vec![StormWorker::healthy("only", 1, 100.0)];
let jobs: Vec<StormJob> = (0..10)
.map(|i| StormJob::build(1000, 1, format!("/p{i}")).with_policy(JobPolicy::Proof))
.collect();
let run = simulate_storm(&workers, &jobs, &cfg());
assert_eq!(run.summary.local_fallbacks, 0, "proof must never fall back");
assert!(run.summary.proof_refusals >= 9, "most proof jobs refuse");
let refused: Vec<_> = run
.events
.iter()
.filter(|e| e.event == event::REFUSED)
.collect();
assert!(!refused.is_empty());
assert!(
refused
.iter()
.all(|e| e.reason_code.as_deref() == Some(PROOF_REFUSAL_REASON))
);
assert!(check_no_unbounded_local_fallback_storm(&run.events, &run.summary, 0.0).passed);
}
#[test]
fn ineligible_workers_never_receive_work() {
let workers = vec![
StormWorker::healthy("good", 2, 100.0),
StormWorker::healthy("bypassed", 8, 200.0)
.with_eligibility(WorkerEligibility::TemporaryBypass),
StormWorker::healthy("disabled", 8, 200.0)
.with_eligibility(WorkerEligibility::AdminDisabled),
StormWorker::healthy("incapable", 8, 200.0)
.with_eligibility(WorkerEligibility::CapabilityInadmissible),
];
let jobs = build_jobs(20, 500, 1);
let run = simulate_storm(&workers, &jobs, &cfg());
let report = check_no_ineligible_worker_selected(&run.events, &workers);
assert!(report.passed, "{:?}", report.violations);
for ev in &run.events {
if let Some(sel) = &ev.selected_worker {
assert_eq!(sel, "good", "only the eligible worker may be selected");
}
}
assert_eq!(run.summary.per_worker_slot_utilization["bypassed"], 0.0);
assert_eq!(run.summary.per_worker_slot_utilization["disabled"], 0.0);
assert_eq!(run.summary.per_worker_slot_utilization["incapable"], 0.0);
}
#[test]
fn cancellation_before_start_is_clean() {
let workers = vec![StormWorker::healthy("only", 1, 100.0)];
let mut jobs = build_jobs(6, 1000, 1);
for j in jobs.iter_mut().skip(2) {
j.cancel_when_queued = true;
}
let mut c = cfg();
c.cancel_delay_ms = 5;
c.queue_timeout_ms = 100_000;
let run = simulate_storm(&workers, &jobs, &c);
assert!(run.summary.cancellations >= 1, "some jobs cancel");
let guidance = check_attach_cancel_guidance(&run.events);
assert!(guidance.passed, "{:?}", guidance.violations);
let cancelled: Vec<_> = run
.events
.iter()
.filter(|e| e.event == event::CANCELLED)
.collect();
assert!(!cancelled.is_empty());
assert!(
cancelled
.iter()
.all(|e| e.detail.as_deref().is_some_and(|d| !d.is_empty()))
);
}
#[test]
fn jsonl_carries_every_required_field() {
let workers = healthy_fleet();
let jobs = build_jobs(12, 500, 1);
let run = simulate_storm(&workers, &jobs, &cfg());
let jsonl = run.to_jsonl().expect("serialize");
assert!(!jsonl.is_empty());
let started: Vec<&SmokeProfileEvent> = run
.events
.iter()
.filter(|e| e.event == event::STARTED)
.collect();
assert!(!started.is_empty());
for ev in &started {
assert!(ev.local_job_id.is_some(), "local_job_id present");
assert!(ev.remote_job_id.is_some(), "remote_job_id present");
assert!(ev.selected_worker.is_some(), "selected_worker present");
assert!(ev.queue_depth.is_some(), "queue_depth present");
assert!(ev.fallback_decision.is_some(), "fallback_decision present");
assert_eq!(ev.bead_id, "bd-session-history-remediation-ocv9i.10.4");
}
for line in jsonl.lines() {
let v: serde_json::Value = serde_json::from_str(line).expect("valid json line");
assert_eq!(v["run_id"], "storm-run-1");
assert_eq!(v["scenario"], "load_storm_control");
}
}
#[test]
fn detector_catches_duplicate_remote_ids() {
let mk = |local: &str, rid: u64| {
SmokeProfileEvent::started("r", "b", Some("w".into()), SmokeScenario::LoadStormControl)
.with_job_ids(Some(local.into()), Some(rid))
};
let events = vec![mk("rchw-0", 7), mk("rchw-1", 7)];
let report = check_no_duplicate_remote_job_ids(&events);
assert!(!report.passed);
assert_eq!(report.violations.len(), 1);
}
#[test]
fn detector_catches_ineligible_selection() {
let workers = vec![
StormWorker::healthy("good", 2, 100.0),
StormWorker::healthy("disabled", 2, 100.0)
.with_eligibility(WorkerEligibility::AdminDisabled),
];
let mut bad = SmokeProfileEvent::started(
"r",
"b",
Some("disabled".into()),
SmokeScenario::LoadStormControl,
);
bad.event = event::STARTED.to_string();
let bad = bad.with_selected_worker(Some("disabled".into()));
let report = check_no_ineligible_worker_selected(&[bad], &workers);
assert!(!report.passed);
}
#[test]
fn detector_catches_stuck_wrapper() {
let ev =
SmokeProfileEvent::started("r", "b", Some("w".into()), SmokeScenario::LoadStormControl)
.with_job_ids(Some("rchw-stuck".into()), Some(1));
let report = check_attach_cancel_guidance(&[ev]);
assert!(!report.passed);
assert!(
report
.violations
.iter()
.any(|v| v.contains("stuck wrapper"))
);
}
#[test]
fn deterministic_runs_are_identical() {
let workers = healthy_fleet();
let jobs = build_jobs(40, 700, 1);
let a = simulate_storm(&workers, &jobs, &cfg());
let b = simulate_storm(&workers, &jobs, &cfg());
assert_eq!(a.to_jsonl().unwrap(), b.to_jsonl().unwrap());
assert_eq!(a.summary, b.summary);
}
#[test]
fn summary_serde_roundtrip() {
let workers = healthy_fleet();
let jobs = build_jobs(10, 500, 1);
let run = simulate_storm(&workers, &jobs, &cfg());
let json = serde_json::to_string(&run.summary).unwrap();
let back: StormSummary = serde_json::from_str(&json).unwrap();
assert_eq!(run.summary, back);
}
#[test]
fn varied_jobs_mixed_policies_kinds_slots() {
let workers = healthy_fleet();
let mut jobs = Vec::new();
for i in 0..18 {
let kind = match i % 3 {
0 => JobKind::Build,
1 => JobKind::Test,
_ => JobKind::Check,
};
let policy = if i % 5 == 0 {
JobPolicy::ForceRemote
} else {
JobPolicy::FailOpen
};
jobs.push(
StormJob::build(
300 + (i as u64 % 4) * 250,
1 + (i as u32 % 3),
format!("/r{}", i % 4),
)
.with_kind(kind)
.with_policy(policy),
);
}
let run = simulate_storm(&workers, &jobs, &cfg());
let reports = check_all_invariants(&run, &workers, 1.6, 0.30);
for r in &reports {
assert!(r.passed, "invariant {} failed: {:?}", r.name, r.violations);
}
}
#[test]
fn live_healthy_run_upholds_all_invariants() {
let workers = healthy_fleet();
let outcomes: Vec<LiveJobOutcome> = (0..30)
.map(|i| {
let w = &workers[i % workers.len()].id;
LiveJobOutcome::remote(format!("rchw-{i:04}"), w.clone(), (i + 1) as u64, 1, 500)
})
.collect();
let run = build_live_storm_run(&cfg(), &workers, &outcomes);
let reports = check_all_invariants(&run, &workers, 1.5, 0.25);
for r in &reports {
assert!(r.passed, "invariant {} failed: {:?}", r.name, r.violations);
}
assert_eq!(run.summary.total_jobs, 30);
assert_eq!(run.summary.remote_successes, 30);
assert_eq!(run.summary.local_fallbacks, 0);
for w in &workers {
assert!(
run.summary.per_worker_slot_utilization[&w.id] > 0.0,
"worker {} starved",
w.id
);
}
}
#[test]
fn live_mixed_run_with_bounded_fallback_passes() {
let workers = healthy_fleet();
let mut outcomes = Vec::new();
for i in 0..24 {
let w = &workers[i % workers.len()].id;
outcomes.push(LiveJobOutcome::remote(
format!("rchw-{i:04}"),
w.clone(),
(i + 1) as u64,
1,
400,
));
}
for i in 24..28 {
outcomes.push(LiveJobOutcome::local_fallback(
format!("rchw-{i:04}"),
1,
400,
));
}
let run = build_live_storm_run(&cfg(), &workers, &outcomes);
let reports = check_all_invariants(&run, &workers, 1.5, 0.25);
for r in &reports {
assert!(r.passed, "invariant {} failed: {:?}", r.name, r.violations);
}
assert_eq!(run.summary.remote_successes, 24);
assert_eq!(run.summary.local_fallbacks, 4);
let strict = check_no_unbounded_local_fallback_storm(&run.events, &run.summary, 0.10);
assert!(!strict.passed, "10% cap must flag a 14% fallback rate");
}
#[test]
fn live_summary_accounts_every_disposition() {
let workers = healthy_fleet();
let outcomes = vec![
LiveJobOutcome::remote("rchw-0000", "w1", 1, 1, 300),
LiveJobOutcome::remote("rchw-0001", "w2", 2, 1, 300),
LiveJobOutcome::local_fallback("rchw-0002", 1, 300),
LiveJobOutcome::local_fallback("rchw-0003", 1, 300)
.queued_for(40, Some(2))
.with_disposition(LiveDisposition::QueueTimeoutFallback),
LiveJobOutcome::local_fallback("rchw-0004", 1, 0)
.with_disposition(LiveDisposition::ProofRefused),
LiveJobOutcome::local_fallback("rchw-0005", 1, 0)
.queued_for(10, Some(1))
.with_disposition(LiveDisposition::Cancelled),
];
let run = build_live_storm_run(&cfg(), &workers, &outcomes);
assert_eq!(run.summary.total_jobs, 6);
assert_eq!(run.summary.remote_successes, 2);
assert_eq!(run.summary.local_fallbacks, 2);
assert_eq!(run.summary.queue_timeouts, 1);
assert_eq!(run.summary.proof_refusals, 1);
assert_eq!(run.summary.cancellations, 1);
assert!(check_attach_cancel_guidance(&run.events).passed);
}
#[test]
fn live_jsonl_carries_load_fields() {
let workers = healthy_fleet();
let outcomes: Vec<LiveJobOutcome> = (0..6)
.map(|i| {
LiveJobOutcome::remote(
format!("rchw-{i:04}"),
workers[i % workers.len()].id.clone(),
(i + 1) as u64,
1,
250,
)
.queued_for(5, Some(1))
})
.collect();
let run = build_live_storm_run(&cfg(), &workers, &outcomes);
let jsonl = run.to_jsonl().expect("serialize");
assert!(!jsonl.is_empty());
let started: Vec<&SmokeProfileEvent> = run
.events
.iter()
.filter(|e| e.event == event::STARTED)
.collect();
assert_eq!(started.len(), 6);
for ev in &started {
assert!(ev.local_job_id.is_some());
assert!(ev.remote_job_id.is_some());
assert!(ev.selected_worker.is_some());
assert!(ev.queue_depth.is_some());
assert_eq!(ev.fallback_decision.as_deref(), Some(decision::REMOTE));
}
for line in jsonl.lines() {
let v: serde_json::Value = serde_json::from_str(line).expect("valid json line");
assert_eq!(v["scenario"], "load_storm_control");
}
}
#[test]
fn live_detector_catches_ineligible_selection() {
let workers = vec![
StormWorker::healthy("good", 4, 100.0),
StormWorker::healthy("disabled", 4, 100.0)
.with_eligibility(WorkerEligibility::AdminDisabled),
];
let outcomes = vec![
LiveJobOutcome::remote("rchw-0000", "good", 1, 1, 300),
LiveJobOutcome::remote("rchw-0001", "disabled", 2, 1, 300),
];
let run = build_live_storm_run(&cfg(), &workers, &outcomes);
let report = check_no_ineligible_worker_selected(&run.events, &workers);
assert!(!report.passed);
assert!(report.violations.iter().any(|v| v.contains("disabled")));
}
#[test]
fn live_detector_catches_duplicate_remote_ids() {
let workers = healthy_fleet();
let outcomes = vec![
LiveJobOutcome::remote("rchw-0000", "w1", 7, 1, 300),
LiveJobOutcome::remote("rchw-0001", "w2", 7, 1, 300),
];
let run = build_live_storm_run(&cfg(), &workers, &outcomes);
let report = check_no_duplicate_remote_job_ids(&run.events);
assert!(!report.passed);
}
#[test]
fn live_proof_refusal_carries_reason_code() {
let workers = healthy_fleet();
let outcomes = vec![
LiveJobOutcome::local_fallback("rchw-0000", 1, 0)
.with_disposition(LiveDisposition::ProofRefused),
];
let run = build_live_storm_run(&cfg(), &workers, &outcomes);
assert_eq!(run.summary.proof_refusals, 1);
assert_eq!(run.summary.local_fallbacks, 0);
let refused: Vec<&SmokeProfileEvent> = run
.events
.iter()
.filter(|e| e.event == event::REFUSED)
.collect();
assert_eq!(refused.len(), 1);
assert_eq!(
refused[0].reason_code.as_deref(),
Some(PROOF_REFUSAL_REASON)
);
assert!(check_no_unbounded_local_fallback_storm(&run.events, &run.summary, 0.0).passed);
}
#[test]
fn live_builder_matches_simulator_remote_event_shape() {
let workers = vec![StormWorker::healthy("w1", 4, 100.0)];
let jobs = vec![StormJob::build(500, 1, "/p0").with_kind(JobKind::Check)];
let sim = simulate_storm(&workers, &jobs, &cfg());
let sim_started = sim
.events
.iter()
.find(|e| e.event == event::STARTED)
.expect("sim started");
let outcomes = vec![LiveJobOutcome::remote(
sim.events
.iter()
.find_map(|e| e.local_job_id.clone())
.unwrap(),
"w1",
1,
1,
500,
)];
let live = build_live_storm_run(&cfg(), &workers, &outcomes);
let live_started = live
.events
.iter()
.find(|e| e.event == event::STARTED)
.expect("live started");
assert_eq!(sim_started.scenario, live_started.scenario);
assert_eq!(sim_started.status, live_started.status);
assert_eq!(
sim_started.fallback_decision,
live_started.fallback_decision
);
assert_eq!(
sim_started.command_fingerprint,
live_started.command_fingerprint
);
assert_eq!(sim_started.selected_worker, live_started.selected_worker);
assert_eq!(sim_started.detail, live_started.detail);
}
}