use crate::channel::ChannelPair;
use crate::error::ReactorError;
use crate::fact::{Fact, FactId, FactIdGenerator, IoType};
use crate::facts_log::FactsLog;
use crate::phase::ReactorPhase;
use crate::stable_detector::StableDetector;
use crate::state::ReactorState;
use crate::{EventReceiver, EventSender, FactReceiver, FactSender};
use evorule_tcb::path::resolve_path_mut;
use evorule_tcb::{execute_transition, JsonValue, TransitionResult};
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use std::collections::HashSet;
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
const DEFAULT_MAX_QUEUE_LEN: usize = 1000;
const IO_TIMEOUT_CHECK_INTERVAL: Duration = Duration::from_secs(5);
const SNAPSHOT_UPDATE_INTERVAL: usize = 100;
#[derive(Debug, Clone, Default)]
pub struct ReactorStateSnapshot {
pub phase: ReactorPhase,
pub version: u64,
pub structural_invariant_violations: u64,
pub pending_io_count: usize,
pub steps: usize,
pub queue_len: usize,
pub finished: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PendingIoEntry {
pub id: FactId,
pub io_type: IoType,
pub started_at: Instant,
}
#[derive(Debug, Clone)]
pub struct ReactorBuilder {
core_eval: Vec<JsonValue>,
max_rounds: usize,
max_queue_len: usize,
io_warn_timeout: Duration,
io_error_timeout: Duration,
io_timeout_check_interval: Duration,
facts_log: FactsLog,
interrupt_flag: Arc<AtomicBool>,
fact_id_start: Option<u64>,
known_io_types: Option<Arc<HashSet<String>>>,
}
impl ReactorBuilder {
pub fn new(core_eval: Vec<JsonValue>) -> Self {
Self {
core_eval,
max_rounds: 10000,
max_queue_len: DEFAULT_MAX_QUEUE_LEN,
io_warn_timeout: Duration::from_secs(30),
io_error_timeout: Duration::from_secs(60),
io_timeout_check_interval: IO_TIMEOUT_CHECK_INTERVAL,
facts_log: FactsLog::new(),
interrupt_flag: Arc::new(AtomicBool::new(false)),
fact_id_start: None,
known_io_types: None,
}
}
pub fn max_rounds(mut self, max_rounds: usize) -> Self {
self.max_rounds = max_rounds;
self
}
pub fn max_queue_len(mut self, max_queue_len: usize) -> Self {
self.max_queue_len = max_queue_len;
self
}
pub fn io_warn_timeout(mut self, timeout: Duration) -> Self {
self.io_warn_timeout = timeout;
self
}
pub fn io_error_timeout(mut self, timeout: Duration) -> Self {
self.io_error_timeout = timeout;
self
}
pub fn io_timeout_check_interval(mut self, interval: Duration) -> Self {
self.io_timeout_check_interval = interval;
self
}
pub fn facts_log(mut self, facts_log: FactsLog) -> Self {
self.facts_log = facts_log;
self
}
pub fn fact_id_start(mut self, start: u64) -> Self {
self.fact_id_start = Some(start);
self
}
pub fn known_io_types(mut self, types: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.known_io_types = Some(Arc::new(types.into_iter().map(Into::into).collect()));
self
}
pub fn build(self) -> Reactor {
Reactor {
core_eval: self.core_eval,
max_rounds: self.max_rounds,
max_queue_len: self.max_queue_len,
io_warn_timeout: self.io_warn_timeout,
io_error_timeout: self.io_error_timeout,
io_timeout_check_interval: self.io_timeout_check_interval,
facts_log: self.facts_log,
interrupt_flag: self.interrupt_flag,
fact_id_start: self.fact_id_start,
known_io_types: self.known_io_types,
}
}
}
pub struct Reactor {
core_eval: Vec<JsonValue>,
max_rounds: usize,
max_queue_len: usize,
io_warn_timeout: Duration,
io_error_timeout: Duration,
io_timeout_check_interval: Duration,
facts_log: FactsLog,
interrupt_flag: Arc<AtomicBool>,
fact_id_start: Option<u64>,
known_io_types: Option<Arc<HashSet<String>>>,
}
impl Reactor {
pub fn builder(core_eval: Vec<JsonValue>) -> ReactorBuilder {
ReactorBuilder::new(core_eval)
}
pub fn spawn(
self,
) -> (
FactSender,
EventReceiver,
EventSender,
ReactorHandle,
FactsLog,
) {
let channels = ChannelPair::new();
let facts_log = self.facts_log.clone();
let event_tx_clone = channels.event_tx.clone();
let snapshot = Arc::new(Mutex::new(ReactorStateSnapshot::default()));
let snapshot_for_run = Arc::clone(&snapshot);
let interrupt_flag = self.interrupt_flag.clone();
let interrupt_flag_for_run = self.interrupt_flag.clone();
let handle = tokio::spawn(self.run(
channels.command_rx,
channels.event_tx,
snapshot_for_run,
interrupt_flag_for_run,
));
(
channels.command_tx,
channels.event_rx,
event_tx_clone,
ReactorHandle {
handle,
snapshot,
interrupt_flag,
},
facts_log,
)
}
#[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
async fn run(
self,
mut cmd_rx: FactReceiver,
event_tx: EventSender,
snapshot: Arc<Mutex<ReactorStateSnapshot>>,
interrupt_flag: Arc<AtomicBool>,
) -> Result<(), ReactorError> {
let mut state = ReactorState::new();
let mut id_gen = self
.fact_id_start
.map_or_else(FactIdGenerator::new, FactIdGenerator::resume);
let mut steps: usize = 0;
tracing::debug!(
"Reactor started (long-running), max_rounds={}",
self.max_rounds
);
'main: loop {
Self::update_snapshot(&snapshot, &state, steps, false);
Self::run_invariant_check(&mut state, steps);
if interrupt_flag.swap(false, std::sync::atomic::Ordering::Acquire) {
state.phase = ReactorPhase::Error;
let id = id_gen.next_id();
let err_fact = Fact::Error {
id,
message: "Execution interrupted by external request".to_string(),
};
Self::emit_fact(&self.facts_log, &event_tx, err_fact);
state.phase = ReactorPhase::Stable;
let stable_id = id_gen.next_id();
let stable_fact = Fact::Stable {
id: stable_id,
final_snapshot: state.payload.clone(),
};
Self::emit_fact(&self.facts_log, &event_tx, stable_fact);
steps = 0;
state.phase = ReactorPhase::Idle;
continue 'main;
}
state.phase = ReactorPhase::Draining;
let mut drained_any = false;
loop {
match cmd_rx.try_recv() {
Ok(fact) => {
drained_any = true;
tracing::trace!(
phase = %state.phase.as_str(),
"Drained fact: {} (id={})",
fact.type_name(),
fact.id()
);
Self::emit_fact(&self.facts_log, &event_tx, fact.clone());
Self::handle_fact(&mut state, fact)?;
}
Err(mpsc::error::TryRecvError::Empty) => break,
Err(mpsc::error::TryRecvError::Disconnected) => {
tracing::debug!(
"Reactor command channel closed during drain, shutting down"
);
Self::update_snapshot(&snapshot, &state, steps, true);
return Ok(());
}
}
}
if StableDetector::is_stable(state.queue.len(), state.pending_io_count) && steps > 0 {
state.phase = ReactorPhase::Stable;
tracing::info!(
phase = %state.phase.as_str(),
"Reactor stable after {} steps, version {}",
steps,
state.version
);
let id = id_gen.next_id();
let fact = Fact::Stable {
id,
final_snapshot: state.payload.clone(),
};
Self::emit_fact(&self.facts_log, &event_tx, fact);
steps = 0;
state.phase = ReactorPhase::Idle;
continue 'main;
}
if (state.queue.is_empty() || state.pending_io_count > 0) && !drained_any {
state.phase = ReactorPhase::AwaitingIo;
let fact = match tokio::time::timeout(self.io_timeout_check_interval, cmd_rx.recv())
.await
{
Ok(Some(f)) => f,
Ok(None) => {
tracing::debug!("Reactor command channel closed, shutting down");
Self::update_snapshot(&snapshot, &state, steps, true);
return Ok(());
}
Err(_) => {
Self::check_io_timeouts(
&mut state,
&self.facts_log,
&event_tx,
&mut id_gen,
self.io_warn_timeout,
self.io_error_timeout,
);
continue 'main;
}
};
tracing::trace!("Processing fact: {} (id={})", fact.type_name(), fact.id());
Self::emit_fact(&self.facts_log, &event_tx, fact.clone());
Self::handle_fact(&mut state, fact)?;
}
state.phase = ReactorPhase::Executing;
while state.pending_io_count == 0 {
let warn_threshold = self.max_rounds * 4 / 5;
if warn_threshold > 0 && steps == warn_threshold {
tracing::warn!(
phase = %state.phase.as_str(),
steps,
max_rounds = self.max_rounds,
threshold_pct = 80,
"指令执行步数达到 max_rounds 的 80%({} / {}),即将接近上限",
steps,
self.max_rounds
);
}
if steps >= self.max_rounds {
state.phase = ReactorPhase::Error;
let id = id_gen.next_id();
let err = ReactorError::MaxRoundsExceeded {
rounds: steps,
max_rounds: self.max_rounds,
};
tracing::error!(phase = %state.phase.as_str(), "{}", err);
let fact = Fact::Error {
id,
message: err.to_string(),
};
Self::emit_fact(&self.facts_log, &event_tx, fact);
state.clear_queue();
steps = 0;
state.phase = ReactorPhase::Stable;
let stable_id = id_gen.next_id();
let stable_fact = Fact::Stable {
id: stable_id,
final_snapshot: state.payload.clone(),
};
Self::emit_fact(&self.facts_log, &event_tx, stable_fact);
state.phase = ReactorPhase::Idle;
continue 'main;
}
let (instruction, cause) = match state.pop_instruction() {
Some(pair) => pair,
None => break, };
steps += 1;
if steps % SNAPSHOT_UPDATE_INTERVAL == 0 {
Self::update_snapshot(&snapshot, &state, steps, false);
}
tracing::trace!(
phase = %state.phase.as_str(),
"Executing instruction (step {}): {:?}",
steps,
instruction
);
let result = execute_transition(
&self.core_eval,
&instruction,
&state.payload,
&state.queue.iter().cloned().collect::<Vec<_>>(),
);
match result {
Ok(TransitionResult::State {
new_payload,
new_queue,
}) => {
state.payload = new_payload;
state.update_queue_with_causes(new_queue, cause);
let queue_len = state.queue.len();
let queue_warn_threshold = self.max_queue_len * 4 / 5;
if queue_len >= self.max_queue_len && self.max_queue_len > 0 {
state.phase = ReactorPhase::Error;
tracing::error!(
phase = %state.phase.as_str(),
queue_len,
max_queue_len = self.max_queue_len,
"队列长度超过上限,发射 Error 并清空队列"
);
let err_id = id_gen.next_id();
let err_fact = Fact::Error {
id: err_id,
message: format!(
"Queue length {} exceeds max {}",
queue_len, self.max_queue_len
),
};
Self::emit_fact(&self.facts_log, &event_tx, err_fact);
state.clear_queue();
state.phase = ReactorPhase::Stable;
let stable_id = id_gen.next_id();
let stable_fact = Fact::Stable {
id: stable_id,
final_snapshot: state.payload.clone(),
};
Self::emit_fact(&self.facts_log, &event_tx, stable_fact);
steps = 0;
state.phase = ReactorPhase::Idle;
continue 'main;
} else if queue_len >= queue_warn_threshold && queue_warn_threshold > 0 {
tracing::warn!(
phase = %state.phase.as_str(),
queue_len,
max_queue_len = self.max_queue_len,
threshold_pct = 80,
"队列长度接近上限(80%):{} / {}",
queue_len,
self.max_queue_len
);
}
if state.io_recovery {
state.clear_io_recovery();
}
state.bump_version();
let id = id_gen.next_id();
let fact = Fact::StateTransition {
id,
cause, new_payload: state.payload.clone(),
new_queue: state.queue.iter().cloned().collect(),
};
Self::emit_fact(&self.facts_log, &event_tx, fact);
}
Ok(TransitionResult::IoRequired {
io_type: io_type_str,
params,
}) => {
let id = id_gen.next_id();
if let Some(known) = &self.known_io_types {
if !known.contains(&io_type_str) {
state.phase = ReactorPhase::Error;
let msg = format!(
"unknown io_type: {} (not in known_io_types, instruction: {:?})",
io_type_str, instruction
);
tracing::error!(phase = %state.phase.as_str(), "{}", msg);
let fact = Fact::Error { id, message: msg };
Self::emit_fact(&self.facts_log, &event_tx, fact);
state.phase = ReactorPhase::Idle;
continue 'main;
}
}
let io_type = IoType::new(&io_type_str);
state.register_io_request(id, io_type.clone());
state.save_io_instruction(id, instruction.clone(), cause);
state.phase = ReactorPhase::AwaitingIo;
tracing::debug!(
phase = %state.phase.as_str(),
"IoRequest {} (io_type={})",
id,
io_type
);
let fact = Fact::IoRequest {
id,
cause, io_type,
params,
};
Self::emit_fact(&self.facts_log, &event_tx, fact);
break; }
Err(err) => {
state.phase = ReactorPhase::Error;
let id = id_gen.next_id();
let msg = format!(
"TCB error at step {}: {} (instruction: {:?})",
steps, err, instruction
);
tracing::error!(phase = %state.phase.as_str(), "{}", msg);
let fact = Fact::Error { id, message: msg };
Self::emit_fact(&self.facts_log, &event_tx, fact);
state.phase = ReactorPhase::Idle;
continue 'main;
}
}
}
}
}
fn update_snapshot(
snapshot: &Arc<Mutex<ReactorStateSnapshot>>,
state: &ReactorState,
steps: usize,
finished: bool,
) {
if let Ok(mut snap) = snapshot.lock() {
snap.phase = state.phase;
snap.version = state.version;
snap.structural_invariant_violations = state.structural_invariant_violations;
snap.pending_io_count = state.pending_io_count;
snap.steps = steps;
snap.queue_len = state.queue.len();
snap.finished = finished;
} else {
tracing::warn!("ReactorStateSnapshot mutex poisoned, tier2 queries will be stale");
}
}
fn emit_fact(facts_log: &FactsLog, event_tx: &EventSender, fact: Fact) {
if let Err(e) = facts_log.append(fact.clone()) {
tracing::warn!("FactsLog append failed: {}", e);
}
if event_tx.send(fact).is_err() {
tracing::debug!("Event broadcast channel has no receivers, fact not delivered");
}
}
fn run_invariant_check(state: &mut ReactorState, steps: usize) {
let structural = crate::invariants::check_invariants(state, steps);
let structural_count = structural.len() as u64;
state.structural_invariant_violations = state
.structural_invariant_violations
.saturating_add(structural_count);
for v in &structural {
tracing::error!(
phase = %state.phase.as_str(),
violation = v.as_str(),
total_violations = state.structural_invariant_violations,
"不变式违规: {}",
v
);
}
}
fn check_io_timeouts(
state: &mut ReactorState,
facts_log: &FactsLog,
event_tx: &EventSender,
id_gen: &mut FactIdGenerator,
warn_timeout: Duration,
error_timeout: Duration,
) {
if state.pending_io_count == 0 {
return;
}
let now = Instant::now();
let mut warn_ids = Vec::new();
let mut error_ids = Vec::new();
for (id, timestamp) in &state.pending_io_timestamps {
let elapsed = now.duration_since(*timestamp);
if elapsed >= error_timeout {
error_ids.push((*id, error_timeout.as_secs()));
} else if elapsed >= warn_timeout {
warn_ids.push((*id, warn_timeout.as_secs()));
}
}
for (id, warn_secs) in &warn_ids {
tracing::warn!(
io_request_id = %id,
warn_timeout_secs = warn_secs,
"I/O 请求超时警告:pending I/O 超过 {}s 未响应",
warn_secs
);
}
for (id, error_secs) in error_ids {
tracing::error!(
io_request_id = %id,
error_timeout_secs = error_secs,
"I/O 请求超时错误:pending I/O 超过 {}s 未响应,发射 Error 恢复反应器",
error_secs
);
let err_fact_id = id_gen.next_id();
let err_fact = Fact::Error {
id: err_fact_id,
message: format!("I/O request {} timed out after {}s", id, error_secs),
};
Self::emit_fact(facts_log, event_tx, err_fact);
state.force_remove_io_request(id);
}
}
#[allow(clippy::cognitive_complexity)]
fn handle_fact(state: &mut ReactorState, fact: Fact) -> Result<(), ReactorError> {
match fact {
Fact::Command { id, instruction } => {
tracing::debug!("Received Command");
state.push_back(instruction, id);
}
Fact::PayloadUpdate { id: _, path, value } => {
tracing::debug!("Received PayloadUpdate: {}", path);
Self::update_payload(state, &path, value)?;
state.bump_version();
}
Fact::IoResponse {
id: _,
request_id,
result,
error,
} => {
tracing::debug!("Received IoResponse for {}", request_id);
if let Some(err_msg) = &error {
tracing::warn!("IoResponse carries error: {}", err_msg);
}
if state.complete_io_request(request_id) {
Self::inject_io_result(state, result)?;
if let Some((orig_instruction, orig_cause)) =
state.take_io_instruction(request_id)
{
state.push_front(orig_instruction, orig_cause);
state.io_recovery = true;
}
state.bump_version();
} else {
tracing::warn!("Unknown IoResponse: {}, ignoring", request_id);
}
}
Fact::IoRequest { .. }
| Fact::StateTransition { .. }
| Fact::Stable { .. }
| Fact::Error { .. } => {
tracing::trace!("Ignoring self-produced fact");
}
}
Ok(())
}
fn update_payload(
state: &mut ReactorState,
path: &str,
value: JsonValue,
) -> Result<(), ReactorError> {
if let Some(target) = resolve_path_mut(&mut state.payload, path) {
*target = value;
return Ok(());
}
let parts: Vec<&str> = path.split('.').collect();
if parts.is_empty() {
return Err(ReactorError::InvalidState {
field: "payload path is empty",
});
}
let field = parts.last().ok_or(ReactorError::InvalidState {
field: "payload path is empty",
})?;
let parent_obj = if parts.len() == 1 {
if let JsonValue::Object(map) = &mut state.payload {
map
} else {
return Err(ReactorError::InvalidState {
field: "payload is not an object",
});
}
} else {
let mut current = &mut state.payload;
for &part in parts.get(..parts.len() - 1).unwrap_or(&[]) {
if let JsonValue::Object(map) = current {
if !map.contains_key(part) {
map.insert(part.to_string(), JsonValue::empty_object());
}
current = map.get_mut(part).ok_or(ReactorError::InvalidState {
field: "failed to access nested path",
})?;
} else {
return Err(ReactorError::InvalidState {
field: "intermediate path is not an object",
});
}
}
if let JsonValue::Object(map) = current {
map
} else {
return Err(ReactorError::InvalidState {
field: "parent path is not an object",
});
}
};
parent_obj.insert(field.to_string(), value);
Ok(())
}
fn inject_io_result(state: &mut ReactorState, result: JsonValue) -> Result<(), ReactorError> {
if let Some(target) = resolve_path_mut(&mut state.payload, "__io_result__") {
*target = result;
Ok(())
} else if let JsonValue::Object(map) = &mut state.payload {
map.insert("__io_result__".to_string(), result);
Ok(())
} else {
Err(ReactorError::InvalidState {
field: "__io_result__",
})
}
}
}
pub struct ReactorHandle {
handle: JoinHandle<Result<(), ReactorError>>,
snapshot: Arc<Mutex<ReactorStateSnapshot>>,
interrupt_flag: Arc<AtomicBool>,
}
impl ReactorHandle {
pub async fn join(self) -> Result<(), ReactorError> {
self.handle.await.map_err(|e| ReactorError::TaskJoinError {
message: e.to_string(),
})?
}
pub fn abort(&self) {
self.handle.abort();
}
pub fn is_finished(&self) -> bool {
if self.handle.is_finished() {
return true;
}
self.snapshot.lock().map(|s| s.finished).unwrap_or(false)
}
pub fn current_phase(&self) -> Option<ReactorPhase> {
let snap = self.snapshot.lock().ok()?;
if snap.finished {
return None;
}
Some(snap.phase)
}
pub fn causal_depth(&self) -> Option<usize> {
let snap = self.snapshot.lock().ok()?;
if snap.finished {
return None;
}
usize::try_from(snap.version).ok()
}
pub fn structural_invariant_violations(&self) -> u64 {
self.snapshot
.lock()
.map(|s| s.structural_invariant_violations)
.unwrap_or(0)
}
pub fn pending_io_count(&self) -> Option<usize> {
let snap = self.snapshot.lock().ok()?;
if snap.finished {
return None;
}
Some(snap.pending_io_count)
}
pub fn current_step(&self) -> Option<usize> {
let snap = self.snapshot.lock().ok()?;
if snap.finished {
return None;
}
Some(snap.steps)
}
pub fn snapshot(&self) -> Option<ReactorStateSnapshot> {
self.snapshot.lock().ok().map(|s| s.clone())
}
pub fn interrupt(&self) {
self.interrupt_flag
.store(true, std::sync::atomic::Ordering::Release);
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::panic, clippy::expect_used)]
use super::*;
#[test]
fn test_update_payload_existing_path() {
let mut state = ReactorState::new();
state.payload = JsonValue::object_from_pairs(&[("x", JsonValue::Integer(1))]);
Reactor::update_payload(&mut state, "x", JsonValue::Integer(42)).unwrap();
assert_eq!(state.payload.get("x"), Some(&JsonValue::Integer(42)));
}
#[test]
fn test_update_payload_top_level_create() {
let mut state = ReactorState::new();
Reactor::update_payload(&mut state, "new_field", JsonValue::string("hello")).unwrap();
assert_eq!(
state.payload.get("new_field").and_then(|v| v.as_str()),
Some("hello")
);
}
#[test]
fn test_update_payload_nested_nonexistent_creates() {
let mut state = ReactorState::new();
let result = Reactor::update_payload(&mut state, "a.b.c", JsonValue::Integer(1));
assert!(result.is_ok());
assert_eq!(
state
.payload
.get("a")
.and_then(|v| v.as_object())
.and_then(|m| m.get("b"))
.and_then(|v| v.as_object())
.and_then(|m| m.get("c"))
.and_then(|v| v.as_i64()),
Some(1)
);
}
#[test]
fn test_inject_io_result() {
let mut state = ReactorState::new();
Reactor::inject_io_result(&mut state, JsonValue::string("llm_response")).unwrap();
assert_eq!(
state.payload.get("__io_result__").and_then(|v| v.as_str()),
Some("llm_response")
);
}
#[test]
fn test_builder_defaults() {
let builder = ReactorBuilder::new(vec![]);
assert_eq!(builder.max_rounds, 10000);
assert_eq!(builder.max_queue_len, DEFAULT_MAX_QUEUE_LEN);
assert_eq!(builder.io_warn_timeout, Duration::from_secs(30));
assert_eq!(builder.io_error_timeout, Duration::from_secs(60));
assert_eq!(builder.io_timeout_check_interval, IO_TIMEOUT_CHECK_INTERVAL);
}
#[test]
fn test_builder_max_queue_len() {
let builder = ReactorBuilder::new(vec![]).max_queue_len(500);
assert_eq!(builder.max_queue_len, 500);
}
#[test]
fn test_builder_io_timeouts() {
let builder = ReactorBuilder::new(vec![])
.io_warn_timeout(Duration::from_secs(10))
.io_error_timeout(Duration::from_secs(20));
assert_eq!(builder.io_warn_timeout, Duration::from_secs(10));
assert_eq!(builder.io_error_timeout, Duration::from_secs(20));
}
#[test]
fn test_builder_all_p3_11_options() {
let builder = ReactorBuilder::new(vec![])
.max_rounds(100)
.max_queue_len(200)
.io_warn_timeout(Duration::from_secs(15))
.io_error_timeout(Duration::from_secs(45));
assert_eq!(builder.max_rounds, 100);
assert_eq!(builder.max_queue_len, 200);
assert_eq!(builder.io_warn_timeout, Duration::from_secs(15));
assert_eq!(builder.io_error_timeout, Duration::from_secs(45));
}
#[test]
fn test_max_rounds_80_percent_threshold() {
assert_eq!(100usize * 4 / 5, 80);
assert_eq!(1000usize * 4 / 5, 800);
assert_eq!(5usize * 4 / 5, 4);
assert_eq!(3usize * 4 / 5, 2);
}
#[test]
fn test_queue_80_percent_threshold() {
assert_eq!(1000usize * 4 / 5, 800);
assert_eq!(100usize * 4 / 5, 80);
assert_eq!(10usize * 4 / 5, 8);
}
#[tokio::test]
async fn test_handle_snapshot_initial_state() {
let reactor = Reactor::builder(vec![]).build();
let (cmd_tx, _event_rx, _event_tx, handle, _facts_log) = reactor.spawn();
tokio::time::sleep(Duration::from_millis(50)).await;
let depth = handle.causal_depth();
assert!(depth.is_some(), "causal_depth 应返回 Some");
assert_eq!(depth.unwrap(), 0);
assert_eq!(handle.structural_invariant_violations(), 0);
let pending = handle.pending_io_count();
assert!(pending.is_some(), "pending_io_count 应返回 Some");
assert_eq!(pending.unwrap(), 0);
let step = handle.current_step();
assert!(step.is_some(), "current_step 应返回 Some");
assert_eq!(step.unwrap(), 0);
let phase = handle.current_phase();
assert!(phase.is_some(), "current_phase 应返回 Some");
let p = phase.unwrap();
assert!(
p == ReactorPhase::Idle || p == ReactorPhase::Draining,
"初始阶段应为 Idle 或 Draining,实际: {:?}",
p
);
let snap = handle.snapshot();
assert!(snap.is_some(), "snapshot 应返回 Some");
assert_eq!(snap.unwrap().version, 0);
drop(cmd_tx);
tokio::time::sleep(Duration::from_millis(50)).await;
}
#[tokio::test]
async fn test_handle_snapshot_returns_none_after_shutdown() {
let reactor = Reactor::builder(vec![]).build();
let (cmd_tx, _event_rx, _event_tx, handle, _facts_log) = reactor.spawn();
tokio::time::sleep(Duration::from_millis(50)).await;
drop(cmd_tx);
tokio::time::sleep(Duration::from_millis(100)).await;
assert!(handle.is_finished(), "反应器应在 command_tx 丢弃后结束");
assert_eq!(
handle.current_phase(),
None,
"结束后 current_phase 应返回 None"
);
assert_eq!(
handle.causal_depth(),
None,
"结束后 causal_depth 应返回 None"
);
assert_eq!(
handle.pending_io_count(),
None,
"结束后 pending_io_count 应返回 None"
);
assert_eq!(
handle.current_step(),
None,
"结束后 current_step 应返回 None"
);
assert_eq!(
handle.structural_invariant_violations(),
0,
"结束后 structural_invariant_violations 仍可读(累计计数)"
);
let snap = handle.snapshot();
assert!(snap.is_some(), "结束后 snapshot 仍可读");
assert!(snap.unwrap().finished, "快照应标记 finished=true");
}
}