use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, atomic::{AtomicBool, AtomicU8}};
use std::thread::JoinHandle;
use std::time::Instant;
use zenith_capability::detection::{CapabilitySnapshot, EnvironmentDetector};
use zenith_capability::profile::{Profile, ProfileSelector};
use zenith_foundation::{FramePool, LedgerQuota, LedgerType, ResourceLedger, ResourceType};
#[cfg(target_os = "linux")]
use zenith_ebpf::{BankId, DualBankManager, XdpLoader, XdpProgramName};
#[cfg(target_os = "linux")]
use zenith_linux::xsk::XskConfig;
use zenith_net::source_admission::{AdmissionRule, SourceAdmissionEngine};
use zenith_net::worker::{Worker, WorkerState, WorkerStats};
use zenith_observability::{FaultRecorder, FaultType, MetricsCollector};
use crate::auto_optimizer::{AutoOptimizer, LoadSample, QosLevel};
#[cfg(target_os = "linux")]
use crate::changeset::ChangeSet;
use crate::cpu_affinity::{CpuAffinityConfig, CpuAffinityError};
use crate::extreme_planner::ExtremePlanner;
use crate::graph::{
DomainSnapshot, ExecutionDomain, PlannedTopology, ResourceNode, RuntimeGraph,
};
use crate::supervisor::{
DomainSupervisor, ExitReason, HealthReport, NodeSupervisor, QueueSupervisor, RestartStrategy,
SupervisorConfig,
};
const FRAME_BYTES: u64 = 4096;
const DEFAULT_CONNECTION_QUOTA: u64 = 1024;
type CacheWarmupHook = Box<dyn Fn(&[Box<str>]) + Send>;
#[derive(Debug, Clone)]
pub struct WorkerRuntimeConfig {
pub ifindex: u32,
pub queue_id: u32,
#[cfg(target_os = "linux")]
pub xsk_config: XskConfig,
pub frame_pool_capacity: u32,
#[cfg(target_os = "linux")]
pub ebpf_program: XdpProgramName,
pub enable_dual_bank: bool,
pub enable_quarantine: bool,
pub max_cycles: u64,
pub domain_id: u32,
pub restart_strategy: RestartStrategy,
}
impl Default for WorkerRuntimeConfig {
fn default() -> Self {
Self {
ifindex: 0,
queue_id: 0,
#[cfg(target_os = "linux")]
xsk_config: XskConfig::default(),
frame_pool_capacity: 1024,
#[cfg(target_os = "linux")]
ebpf_program: XdpProgramName::Main,
enable_dual_bank: false,
enable_quarantine: true,
max_cycles: 0,
domain_id: 0,
restart_strategy: RestartStrategy::Permanent,
}
}
}
impl WorkerRuntimeConfig {
pub fn new(ifindex: u32) -> Self {
Self {
ifindex,
#[cfg(target_os = "linux")]
xsk_config: XskConfig {
ifindex,
..Default::default()
},
..Default::default()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuntimeState {
Created,
Initialized,
Running,
Paused,
Stopped,
Error,
}
impl RuntimeState {
pub fn is_running(&self) -> bool {
matches!(self, RuntimeState::Running)
}
}
#[derive(Debug, Clone, Default)]
pub struct RuntimeStats {
pub total_packets: u64,
pub total_rx: u64,
pub total_tx: u64,
pub total_rejected: u64,
pub total_parse_errors: u64,
pub total_quarantined: u64,
pub cycles_completed: u64,
pub ebpf_switches: u64,
#[cfg(target_os = "linux")]
pub active_bank: Option<BankId>,
pub uptime_seconds: u64,
}
#[derive(Debug, Clone, Copy)]
struct PendingRestart {
worker_index: usize,
supervised_id: u64,
next_retry_at: Instant,
}
pub struct WorkerRuntime {
config: WorkerRuntimeConfig,
state: RuntimeState,
#[cfg(target_os = "linux")]
xdp_loader: Option<XdpLoader>,
#[cfg(target_os = "linux")]
dual_bank: Option<DualBankManager>,
worker: Worker,
extra_workers: Vec<Worker>,
admission_rules: Vec<AdmissionRule>,
#[cfg(target_os = "linux")]
udp_sink: Option<Box<dyn FnMut(zenith_net::worker::UdpDatagram) + Send>>,
ledger: ResourceLedger,
quarantined_frames: AtomicU64,
stats: RuntimeStats,
cycle_count: AtomicU64,
auto_optimizer: Arc<AutoOptimizer>,
qos_level: Arc<AtomicU8>,
control_running: Arc<AtomicBool>,
control_thread: Option<JoinHandle<()>>,
planner: ExtremePlanner,
affinity_config: Option<CpuAffinityConfig>,
last_topology: Option<PlannedTopology>,
replan_pending: Arc<AtomicBool>,
target_worker_count: Arc<AtomicU64>,
topology_graph: RuntimeGraph<64, 256>,
domain_snapshots: Vec<DomainSnapshot>,
cache_warmup_hook: Arc<Mutex<Option<CacheWarmupHook>>>,
last_applied_qos: QosLevel,
replan_apply_error: Option<String>,
node_supervisor: NodeSupervisor,
domain_supervisor: DomainSupervisor,
queue_supervisor: QueueSupervisor,
supervised_worker_ids: Vec<u64>,
next_supervised_id: u64,
last_restart_backoff: Option<std::time::Duration>,
pending_restarts: Vec<PendingRestart>,
capability_snapshot: CapabilitySnapshot,
data_plane_profile: Profile,
queue_ledger_name: String,
ledger_synced_frames: u64,
metrics: Option<Arc<Mutex<MetricsCollector>>>,
fault_recorder: Option<Arc<Mutex<FaultRecorder>>>,
metrics_last_processed: u64,
metrics_last_rejected: u64,
metrics_last_dropped: u64,
}
impl WorkerRuntime {
pub fn new(config: WorkerRuntimeConfig) -> Result<Self, RuntimeError> {
let admission = SourceAdmissionEngine::allow_all();
#[cfg(target_os = "linux")]
let worker = Worker::new(
config.queue_id,
config.xsk_config.clone(),
config.frame_pool_capacity,
admission,
)?;
#[cfg(not(target_os = "linux"))]
let worker = Worker::new(config.queue_id, config.frame_pool_capacity, admission)?;
let capability_snapshot = EnvironmentDetector::new().detect();
let data_plane_profile = ProfileSelector::new().select(&capability_snapshot);
let frame_capacity = u64::from(config.frame_pool_capacity);
let quota = LedgerQuota {
memory_bytes: frame_capacity.saturating_mul(FRAME_BYTES),
frame_count: frame_capacity,
connection_count: DEFAULT_CONNECTION_QUOTA,
};
let mut ledger = ResourceLedger::new("worker-runtime", LedgerType::Domain, quota.clone());
let queue_ledger_name = format!("queue-{}", config.queue_id);
ledger
.create_child(&queue_ledger_name, LedgerType::Queue, quota)
.map_err(|e| RuntimeError::InvalidConfig(format!("ledger init: {e}")))?;
let mut node_supervisor = NodeSupervisor::new(u64::from(config.domain_id));
node_supervisor
.spawn_domain(u64::from(config.domain_id))
.map_err(|e| RuntimeError::InvalidConfig(format!("supervisor init: {e}")))?;
let mut domain_supervisor = DomainSupervisor::new(config.domain_id);
domain_supervisor
.spawn_queue(u64::from(config.queue_id))
.map_err(|e| RuntimeError::InvalidConfig(format!("supervisor init: {e}")))?;
let mut queue_supervisor = QueueSupervisor::with_config(
config.queue_id,
SupervisorConfig {
restart_strategy: config.restart_strategy,
..Default::default()
},
);
let initial_worker_id = u64::from(config.queue_id);
queue_supervisor
.spawn_worker(initial_worker_id)
.map_err(|e| RuntimeError::InvalidConfig(format!("supervisor init: {e}")))?;
let mut topology_graph: RuntimeGraph<64, 256> = RuntimeGraph::new();
let node = ResourceNode::new(
0,
ExecutionDomain::DataPlane,
(capability_snapshot.cpu_cores as u32).max(1),
(capability_snapshot.locked_memory_kb / 1024)
.clamp(4096, u64::from(u32::MAX)) as u32,
(capability_snapshot.nic_queues as u32).max(1),
)
.with_numa(0);
topology_graph
.add_node(node)
.map_err(|e| RuntimeError::InvalidConfig(format!("topology init: {e}")))?;
let domain_snapshots = vec![DomainSnapshot::from_domain(ExecutionDomain::DataPlane, 1)];
let auto_optimizer = Arc::new(AutoOptimizer::new(1000).with_hot_threshold(10));
let qos_level = Arc::new(AtomicU8::new(QosLevel::Normal.to_u8()));
let control_running = Arc::new(AtomicBool::new(false));
let planner = ExtremePlanner;
Ok(Self {
config,
state: RuntimeState::Created,
#[cfg(target_os = "linux")]
xdp_loader: None,
#[cfg(target_os = "linux")]
dual_bank: None,
worker,
extra_workers: Vec::new(),
admission_rules: Vec::new(),
#[cfg(target_os = "linux")]
udp_sink: None,
ledger,
quarantined_frames: AtomicU64::new(0),
stats: RuntimeStats::default(),
cycle_count: AtomicU64::new(0),
auto_optimizer,
qos_level,
control_running,
control_thread: None,
planner,
affinity_config: None,
last_topology: None,
replan_pending: Arc::new(AtomicBool::new(false)),
target_worker_count: Arc::new(AtomicU64::new(1)),
topology_graph,
domain_snapshots,
cache_warmup_hook: Arc::new(Mutex::new(None)),
last_applied_qos: QosLevel::Normal,
replan_apply_error: None,
node_supervisor,
domain_supervisor,
queue_supervisor,
supervised_worker_ids: vec![initial_worker_id],
next_supervised_id: initial_worker_id.saturating_add(1),
last_restart_backoff: None,
pending_restarts: Vec::new(),
capability_snapshot,
data_plane_profile,
queue_ledger_name,
ledger_synced_frames: 0,
metrics: None,
fault_recorder: None,
metrics_last_processed: 0,
metrics_last_rejected: 0,
metrics_last_dropped: 0,
})
}
pub fn with_metrics(mut self, metrics: Arc<Mutex<MetricsCollector>>) -> Self {
self.metrics = Some(metrics);
self
}
pub fn with_fault_recorder(mut self, recorder: Arc<Mutex<FaultRecorder>>) -> Self {
self.fault_recorder = Some(recorder);
self
}
#[cfg(target_os = "linux")]
pub fn init_ebpf(&mut self) -> Result<(), RuntimeError> {
let mut loader = XdpLoader::load(self.config.ebpf_program)?;
let ifindex = self.config.ifindex as i32;
loader.attach_xdp(ifindex)?;
self.xdp_loader = Some(loader);
self.state = RuntimeState::Initialized;
Ok(())
}
#[cfg(not(target_os = "linux"))]
pub fn init_ebpf(&mut self) -> Result<(), RuntimeError> {
Err(RuntimeError::EbpfError(
"eBPF/XDP 数据面仅 Linux 支持,当前平台不可用".into(),
))
}
#[cfg(target_os = "linux")]
pub fn init_dual_bank(&mut self) -> Result<(), RuntimeError> {
if !self.config.enable_dual_bank {
return Ok(());
}
if !self.capability_snapshot.bpf_supported {
return Err(RuntimeError::EbpfError(
"当前环境不支持 BPF,无法启用双 Bank 热更新".into(),
));
}
let manager = DualBankManager::new()?;
self.dual_bank = Some(manager);
Ok(())
}
#[cfg(not(target_os = "linux"))]
pub fn init_dual_bank(&mut self) -> Result<(), RuntimeError> {
if !self.config.enable_dual_bank {
return Ok(());
}
Err(RuntimeError::EbpfError(
"eBPF 双 Bank 热更新仅 Linux 支持,当前平台不可用".into(),
))
}
#[cfg(target_os = "linux")]
pub fn set_udp_sink(&mut self, sink: Box<dyn FnMut(zenith_net::worker::UdpDatagram) + Send>) {
self.udp_sink = Some(sink);
}
pub fn start(&mut self) -> Result<(), RuntimeError> {
self.worker.start();
for w in &mut self.extra_workers {
w.start();
}
self.state = RuntimeState::Running;
if let Err(e) = self.start_control_thread() {
self.worker.stop();
for w in &mut self.extra_workers {
w.stop();
}
self.state = RuntimeState::Created;
return Err(e);
}
Ok(())
}
pub fn stop(&mut self) {
self.stop_control_thread();
self.worker.stop();
for w in &mut self.extra_workers {
w.stop();
}
self.state = RuntimeState::Stopped;
self.queue_supervisor.shutdown();
self.domain_supervisor.shutdown();
self.node_supervisor.shutdown();
#[cfg(target_os = "linux")]
if let Some(loader) = &mut self.xdp_loader {
let _ = loader.destroy_link();
}
}
pub fn run_cycle(&mut self) -> Result<u32, RuntimeError> {
if !self.state.is_running() {
return Err(RuntimeError::NotRunning);
}
self.process_pending_restarts();
let mut total_processed = 0u32;
let mut fault: Option<(usize, zenith_net::error::NetError)> = None;
#[cfg(target_os = "linux")]
{
let sink = &mut self.udp_sink;
if self.worker.state() == WorkerState::Running {
let r = if let Some(s) = sink.as_mut() {
self.worker.process_cycle_with_udp_sink(s)
} else {
self.worker.process_cycle()
};
match r {
Ok(p) => total_processed = p,
Err(e) => fault = Some((0, e)),
}
}
if fault.is_none() {
for (i, w) in self.extra_workers.iter_mut().enumerate() {
if w.state() != WorkerState::Running {
continue;
}
let r = if let Some(s) = sink.as_mut() {
w.process_cycle_with_udp_sink(s)
} else {
w.process_cycle()
};
match r {
Ok(p) => total_processed = total_processed.saturating_add(p),
Err(e) => {
fault = Some((i.saturating_add(1), e));
break;
}
}
}
}
}
#[cfg(not(target_os = "linux"))]
{
if self.worker.state() == WorkerState::Running {
match self.worker.process_cycle() {
Ok(p) => total_processed = p,
Err(e) => fault = Some((0, e)),
}
}
if fault.is_none() {
for (i, w) in self.extra_workers.iter_mut().enumerate() {
if w.state() != WorkerState::Running {
continue;
}
match w.process_cycle() {
Ok(p) => total_processed = total_processed.saturating_add(p),
Err(e) => {
fault = Some((i.saturating_add(1), e));
break;
}
}
}
}
}
if let Some((worker_index, e)) = fault {
self.handle_worker_fault(worker_index);
return Err(e.into());
}
let processed = total_processed;
self.apply_qos_to_worker();
self.sync_ledger_with_pool();
self.run_pending_replan();
self.reconcile_worker_count();
let wstats = self.aggregated_worker_stats();
self.update_runtime_stats(&wstats);
self.report_cycle_metrics(&wstats);
let cycles = self.cycle_count.fetch_add(1, Ordering::Relaxed).saturating_add(1);
self.stats.cycles_completed = cycles;
self.record_load_sample(&wstats);
if self.config.max_cycles > 0 && cycles >= self.config.max_cycles {
self.stop();
}
Ok(processed)
}
pub fn run_full_lifecycle(&mut self, cycles: u64) -> Result<u64, RuntimeError> {
if cycles == 0 {
return Err(RuntimeError::InvalidConfig("cycles must be > 0".into()));
}
self.start()?;
let result = (|| {
for _ in 0..cycles {
self.run_cycle()?;
}
Ok(self.stats.total_packets)
})();
self.stop();
result
}
#[cfg(target_os = "linux")]
pub fn switch_ebpf_bank(&mut self) -> Result<BankId, RuntimeError> {
let manager = self
.dual_bank
.as_mut()
.ok_or(RuntimeError::DualBankNotEnabled)?;
let active_gen = u64::from(manager.global_version());
let target_gen = active_gen.saturating_add(1);
let mut cs = ChangeSet::new(active_gen, target_gen);
cs.prepare()
.map_err(|e| RuntimeError::EbpfError(format!("changeset prepare: {e}")))?;
cs.validate()
.map_err(|e| RuntimeError::EbpfError(format!("changeset validate: {e}")))?;
cs.resource_proof(|| {
if manager.is_ebpf_fully_loaded() {
Ok(())
} else {
Err("ebpf banks not fully loaded")
}
})
.map_err(|e| RuntimeError::EbpfError(format!("changeset resource proof: {e}")))?;
cs.shadow(|| {
if manager.is_ebpf_active_bank_loaded() {
Ok(())
} else {
Err("active bank not loaded")
}
})
.map_err(|e| RuntimeError::EbpfError(format!("changeset shadow: {e}")))?;
cs.activate(active_gen)
.map_err(|e| RuntimeError::EbpfError(format!("changeset activate: {e}")))?;
let ifindex = self.config.ifindex as i32;
match manager.switch(ifindex) {
Ok(new_bank) => {
if let Err(e) = cs
.stop_old_admission()
.and_then(|_| cs.drain(|| Ok(None)))
.and_then(|_| cs.commit())
{
let _ = cs.rollback(None);
return Err(RuntimeError::EbpfError(format!("changeset post-switch: {e}")));
}
manager.increment_version();
self.stats.ebpf_switches = self.stats.ebpf_switches.saturating_add(1);
self.stats.active_bank = Some(new_bank);
Ok(new_bank)
}
Err(e) => {
let _outcome = cs.rollback(None);
Err(RuntimeError::from(e))
}
}
}
#[cfg(target_os = "linux")]
pub fn active_bank(&self) -> Option<BankId> {
self.dual_bank.as_ref().map(|m| m.active_bank_id())
}
#[cfg(target_os = "linux")]
pub fn ebpf_switch_count(&self) -> u32 {
self.dual_bank.as_ref().map(|m| m.switch_count()).unwrap_or(0)
}
#[cfg(not(target_os = "linux"))]
pub fn ebpf_switch_count(&self) -> u32 {
0
}
pub fn add_admission_rule(
&mut self,
rule: AdmissionRule,
) -> Result<(), RuntimeError> {
self.worker.admission_mut().add_rule(rule)?;
for w in &mut self.extra_workers {
w.admission_mut().add_rule(rule)?;
}
self.admission_rules.push(rule);
Ok(())
}
pub fn admission_mut(&mut self) -> &mut SourceAdmissionEngine {
self.worker.admission_mut()
}
pub fn admission(&self) -> &SourceAdmissionEngine {
self.worker.admission()
}
pub fn add_admission_rules(
&mut self,
rules: Vec<AdmissionRule>,
) -> Result<(), RuntimeError> {
for rule in rules {
self.add_admission_rule(rule)?;
}
Ok(())
}
pub fn quarantine_frame(
&mut self,
frame_id: zenith_foundation::FrameId,
reason: &str,
) -> Result<(), RuntimeError> {
if !self.config.enable_quarantine {
return Ok(());
}
let expected_gen = self
.worker
.frame_pool()
.get_frame_info(frame_id)
.ok_or_else(|| RuntimeError::EbpfError(format!("frame {} not found", frame_id.value())))?
.generation();
self.worker
.frame_pool_mut()
.quarantine_by_id(frame_id, expected_gen, reason)
.map_err(|e| RuntimeError::EbpfError(format!("quarantine failed: {e:?}")))?;
self.quarantined_frames.fetch_add(1, Ordering::SeqCst);
self.stats.total_quarantined = self.quarantined_frames.load(Ordering::SeqCst);
Ok(())
}
pub fn quarantined_count(&self) -> u64 {
self.worker.frame_pool().quarantined_count() as u64
}
pub fn recover_frame(&mut self, frame_id: zenith_foundation::FrameId) -> Result<(), RuntimeError> {
self.worker
.frame_pool_mut()
.recover_from_quarantine(frame_id)
.map_err(|e| RuntimeError::EbpfError(format!("recover failed: {e:?}")))?;
let mut current = self.quarantined_frames.load(Ordering::SeqCst);
loop {
let new = current.saturating_sub(1);
match self.quarantined_frames.compare_exchange(
current,
new,
Ordering::SeqCst,
Ordering::SeqCst,
) {
Ok(_) => break,
Err(actual) => current = actual,
}
}
Ok(())
}
pub fn recover_all_quarantined(&mut self) -> Result<u64, RuntimeError> {
let mut recovered: u64 = 0;
let quarantined_ids: Vec<u32> = self
.worker
.frame_pool()
.frames()
.iter()
.filter(|info| info.state() == zenith_foundation::FrameState::Quarantine)
.map(|info| info.id().value())
.collect();
for fid in quarantined_ids {
if self
.worker
.frame_pool_mut()
.recover_from_quarantine(zenith_foundation::FrameId::new(fid))
.is_ok()
{
recovered += 1;
}
}
for w in &mut self.extra_workers {
let extra_quarantined_ids: Vec<u32> = w
.frame_pool()
.frames()
.iter()
.filter(|info| info.state() == zenith_foundation::FrameState::Quarantine)
.map(|info| info.id().value())
.collect();
for fid in extra_quarantined_ids {
if w
.frame_pool_mut()
.recover_from_quarantine(zenith_foundation::FrameId::new(fid))
.is_ok()
{
recovered += 1;
}
}
}
let total_quarantined = self
.extra_workers
.iter()
.fold(
u64::from(self.worker.frame_pool().quarantined_count()),
|acc, w| acc.saturating_add(u64::from(w.frame_pool().quarantined_count())),
);
self.quarantined_frames
.store(total_quarantined, Ordering::SeqCst);
Ok(recovered)
}
pub fn ledger(&self) -> &ResourceLedger {
&self.ledger
}
pub fn frame_pool(&self) -> &FramePool {
self.worker.frame_pool()
}
pub fn frame_pool_mut(&mut self) -> &mut FramePool {
self.worker.frame_pool_mut()
}
#[inline]
pub fn cycle_count(&self) -> u64 {
self.cycle_count.load(Ordering::Relaxed)
}
pub fn verify_conservation(&self) -> bool {
self.worker.verify_conservation()
&& self.extra_workers.iter().all(|w| w.verify_conservation())
}
pub fn state(&self) -> RuntimeState {
self.state
}
pub fn stats(&self) -> RuntimeStats {
self.stats.clone()
}
pub fn worker_id(&self) -> u32 {
self.worker.id()
}
pub fn worker_state(&self) -> zenith_net::worker::WorkerState {
self.worker.state()
}
pub fn worker_stats(&self) -> WorkerStats {
self.worker.stats()
}
pub fn config(&self) -> &WorkerRuntimeConfig {
&self.config
}
#[cfg(target_os = "linux")]
pub fn xdp_loader(&self) -> Option<&XdpLoader> {
self.xdp_loader.as_ref()
}
#[cfg(target_os = "linux")]
pub fn dual_bank_manager(&self) -> Option<&DualBankManager> {
self.dual_bank.as_ref()
}
#[inline]
pub fn current_qos_level(&self) -> QosLevel {
QosLevel::from_priority(self.qos_level.load(Ordering::Relaxed))
}
pub fn set_qos_level(&self, level: QosLevel) {
self.qos_level.store(level.to_u8(), Ordering::Relaxed);
self.auto_optimizer.set_qos_level(level);
}
pub fn auto_optimizer(&self) -> &Arc<AutoOptimizer> {
&self.auto_optimizer
}
#[inline]
pub fn record_load_sample(&self, stats: &WorkerStats) {
let sample = LoadSample {
timestamp: Instant::now(),
cpu_usage: stats.cpu_usage,
queue_depth: stats.queue_depth,
latency_p99: stats.latency_p99,
throughput: stats.throughput,
};
self.auto_optimizer.record_sample(sample);
}
#[inline]
pub fn record_cache_access(&self, key: &str) {
self.auto_optimizer.record_cache_access(key);
}
pub fn get_hot_cache_keys(&self) -> Vec<Box<str>> {
self.auto_optimizer.get_hot_cache_keys()
}
pub fn set_cache_warmup_hook(&self, hook: CacheWarmupHook) {
if let Ok(mut guard) = self.cache_warmup_hook.lock() {
*guard = Some(hook);
}
}
pub fn supervisor_health(&mut self) -> HealthReport {
self.node_supervisor.health_check()
}
pub fn supervised_worker_count(&self) -> usize {
self.supervised_worker_ids.len()
}
pub fn worker_count(&self) -> usize {
self.extra_workers.len().saturating_add(1)
}
pub fn target_worker_count(&self) -> u64 {
self.target_worker_count.load(Ordering::Relaxed)
}
pub fn last_restart_backoff(&self) -> Option<std::time::Duration> {
self.last_restart_backoff
}
pub fn last_planned_topology(&self) -> Option<&PlannedTopology> {
self.last_topology.as_ref()
}
pub fn replan_apply_error(&self) -> Option<&str> {
self.replan_apply_error.as_deref()
}
pub fn capability_snapshot(&self) -> &CapabilitySnapshot {
&self.capability_snapshot
}
pub fn data_plane_profile(&self) -> Profile {
self.data_plane_profile
}
fn start_control_thread(&mut self) -> Result<(), RuntimeError> {
if self.control_running.load(Ordering::SeqCst) {
return Ok(());
}
self.control_running.store(true, Ordering::SeqCst);
let optimizer = self.auto_optimizer.clone();
let qos_level = self.qos_level.clone();
let running = self.control_running.clone();
let replan_pending = self.replan_pending.clone();
let target_worker_count = self.target_worker_count.clone();
let warmup_hook = self.cache_warmup_hook.clone();
let metrics = self.metrics.clone();
match std::thread::Builder::new()
.name("zenith-auto-optimizer".to_string())
.spawn(move || {
let mut last_reported_qos: Option<QosLevel> = None;
while running.load(Ordering::SeqCst) {
let new_level = optimizer.evaluate_qos();
qos_level.store(new_level.to_u8(), Ordering::Relaxed);
let replan_due = optimizer.should_replan();
if replan_due {
replan_pending.store(true, Ordering::SeqCst);
}
let current = target_worker_count.load(Ordering::Relaxed) as usize;
let optimal = optimizer.get_optimal_worker_count(current.max(1));
target_worker_count.store(optimal.max(1) as u64, Ordering::Relaxed);
if let Some(mtx) = &metrics
&& let Ok(mut m) = mtx.lock()
{
if last_reported_qos != Some(new_level) {
let _ = m.counter_inc(
"zenith_runtime_qos_level_changes_total",
&[("level", new_level.as_str())],
);
let _ = m.gauge_set(
"zenith_runtime_qos_level",
i64::from(new_level.to_u8()),
&[],
);
last_reported_qos = Some(new_level);
}
if replan_due {
let _ = m.counter_inc("zenith_runtime_replan_total", &[]);
}
if optimal != current {
let direction = if optimal > current { "up" } else { "down" };
let _ = m.counter_inc(
"zenith_runtime_worker_scale_events_total",
&[("direction", direction)],
);
let _ = m.gauge_set(
"zenith_runtime_target_workers",
(optimal as u64).min(i64::MAX as u64) as i64,
&[],
);
}
}
let hot_keys = optimizer.get_hot_cache_keys();
if !hot_keys.is_empty()
&& let Ok(guard) = warmup_hook.lock()
&& let Some(hook) = guard.as_ref()
{
hook(&hot_keys);
}
let poll_interval_ms = match new_level {
QosLevel::Critical => 10, QosLevel::High | QosLevel::Normal => 100, QosLevel::Low | QosLevel::Background => 1000, };
std::thread::sleep(std::time::Duration::from_millis(poll_interval_ms));
}
}) {
Ok(handle) => {
self.control_thread = Some(handle);
Ok(())
}
Err(e) => {
self.control_running.store(false, Ordering::SeqCst);
Err(RuntimeError::WorkerError(format!(
"failed to spawn control thread: {e}"
)))
}
}
}
fn stop_control_thread(&mut self) {
self.control_running.store(false, Ordering::SeqCst);
if let Some(handle) = self.control_thread.take() {
let _ = handle.join();
}
}
pub fn apply_affinity(&mut self, config: CpuAffinityConfig) -> Result<(), CpuAffinityError> {
let result = config.apply();
if result.is_ok() {
self.affinity_config = Some(config);
}
result
}
pub fn replan_topology(
&mut self,
graph: &RuntimeGraph<64, 256>,
snapshots: &[DomainSnapshot],
) -> Result<(), RuntimeError> {
let topology = self.planner.plan(graph, snapshots)
.map_err(|e| RuntimeError::WorkerError(e.to_string()))?;
self.apply_planned_topology(topology)
}
fn apply_planned_topology(&mut self, topology: PlannedTopology) -> Result<(), RuntimeError> {
if let Some(assignment) = topology.find_assignment(ExecutionDomain::DataPlane)
&& !assignment.workers.is_empty()
{
let idx = (self.config.queue_id as usize) % assignment.workers.len();
let planned = &assignment.workers[idx];
let online = crate::cpu_affinity::get_online_cpus().max(1);
let cpu_id = (planned.worker_id as usize) % online;
let affinity = CpuAffinityConfig::new(
self.config.queue_id,
vec![cpu_id],
planned.numa_node,
);
match self.apply_affinity(affinity) {
Ok(()) => self.replan_apply_error = None,
Err(e) => self.replan_apply_error = Some(e.to_string()),
}
}
self.last_topology = Some(topology);
Ok(())
}
fn run_pending_replan(&mut self) {
if !self.replan_pending.swap(false, Ordering::SeqCst) {
return;
}
let planner = ExtremePlanner;
let topology = match planner.plan(&self.topology_graph, &self.domain_snapshots) {
Ok(t) => t,
Err(e) => {
self.replan_apply_error = Some(e.to_string());
return;
}
};
let _ = self.apply_planned_topology(topology);
}
fn apply_qos_to_worker(&mut self) {
let level = self.current_qos_level();
if level == self.last_applied_qos {
return;
}
self.last_applied_qos = level;
let (on_error, on_deny) = match level {
QosLevel::Critical | QosLevel::High => (true, true),
QosLevel::Normal => (true, false),
QosLevel::Low | QosLevel::Background => (false, false),
};
self.worker.set_quarantine_on_error(on_error);
self.worker.set_quarantine_on_deny(on_deny);
for w in &mut self.extra_workers {
w.set_quarantine_on_error(on_error);
w.set_quarantine_on_deny(on_deny);
}
}
fn sync_ledger_with_pool(&mut self) {
let actual = self
.extra_workers
.iter()
.fold(u64::from(self.worker.frame_pool().allocated_count()), |acc, w| {
acc.saturating_add(u64::from(w.frame_pool().allocated_count()))
});
let path = [self.queue_ledger_name.as_str()];
if actual > self.ledger_synced_frames {
let delta = actual - self.ledger_synced_frames;
if self
.ledger
.allocate_in_child(&path, ResourceType::Frames, delta)
.is_ok()
{
self.ledger_synced_frames = actual;
}
} else if actual < self.ledger_synced_frames {
let delta = self.ledger_synced_frames - actual;
if self
.ledger
.release_in_child(&path, ResourceType::Frames, delta)
.is_ok()
{
self.ledger_synced_frames = actual;
}
}
}
fn reconcile_worker_count(&mut self) {
let target = (self.target_worker_count.load(Ordering::Relaxed) as usize).max(1);
let current = self.worker_count();
if target > current {
for _ in current..target {
if self.spawn_worker().is_err() {
break;
}
}
} else if target < current {
for _ in target..current {
self.remove_worker_at(self.worker_count().saturating_sub(1));
}
}
let _ = self.domain_supervisor.health_check();
let _ = self.node_supervisor.health_check();
if target > current {
self.force_apply_qos_to_new_workers();
}
}
fn force_apply_qos_to_new_workers(&mut self) {
let level = self.current_qos_level();
let (on_error, on_deny) = match level {
QosLevel::Critical | QosLevel::High => (true, true),
QosLevel::Normal => (true, false),
QosLevel::Low | QosLevel::Background => (false, false),
};
self.worker.set_quarantine_on_error(on_error);
self.worker.set_quarantine_on_deny(on_deny);
for w in &mut self.extra_workers {
w.set_quarantine_on_error(on_error);
w.set_quarantine_on_deny(on_deny);
}
}
fn spawn_worker(&mut self) -> Result<(), RuntimeError> {
let supervised_id = self.next_supervised_id;
self.queue_supervisor
.spawn_worker(supervised_id)
.map_err(|e| RuntimeError::WorkerError(format!("supervisor spawn: {e}")))?;
self.next_supervised_id = self.next_supervised_id.saturating_add(1);
let worker_index = self.worker_count();
let nic_queues = (self.capability_snapshot.nic_queues as u32).max(1);
let queue_id = self
.config
.queue_id
.saturating_add(worker_index as u32)
% nic_queues;
let worker_id = supervised_id as u32;
match self.build_worker(worker_id, queue_id) {
Ok(mut worker) => {
worker.start();
self.extra_workers.push(worker);
self.supervised_worker_ids.push(supervised_id);
Ok(())
}
Err(e) => {
let _ = self.queue_supervisor.stop_worker(supervised_id);
Err(e)
}
}
}
#[cfg(target_os = "linux")]
fn build_worker(&self, worker_id: u32, queue_id: u32) -> Result<Worker, RuntimeError> {
let xsk_config = XskConfig {
queue_id,
..self.config.xsk_config.clone()
};
let mut worker = Worker::new(
worker_id,
xsk_config,
self.config.frame_pool_capacity,
SourceAdmissionEngine::allow_all(),
)?;
for rule in &self.admission_rules {
worker.admission_mut().add_rule(*rule)?;
}
Ok(worker)
}
#[cfg(not(target_os = "linux"))]
fn build_worker(&self, worker_id: u32, _queue_id: u32) -> Result<Worker, RuntimeError> {
let mut worker = Worker::new(
worker_id,
self.config.frame_pool_capacity,
SourceAdmissionEngine::allow_all(),
)?;
for rule in &self.admission_rules {
worker.admission_mut().add_rule(*rule)?;
}
Ok(worker)
}
fn remove_worker_at(&mut self, worker_index: usize) {
if worker_index == 0 {
self.worker.stop();
return;
}
let extra_idx = worker_index.saturating_sub(1);
if extra_idx >= self.extra_workers.len() {
return;
}
let mut worker = self.extra_workers.remove(extra_idx);
worker.stop();
if worker_index < self.supervised_worker_ids.len() {
let supervised_id = self.supervised_worker_ids.remove(worker_index);
let _ = self.queue_supervisor.stop_worker(supervised_id);
}
self.pending_restarts.retain_mut(|pr| {
match pr.worker_index.cmp(&worker_index) {
std::cmp::Ordering::Less => true,
std::cmp::Ordering::Equal => false,
std::cmp::Ordering::Greater => {
pr.worker_index = pr.worker_index.saturating_sub(1);
true
}
}
});
}
fn stop_worker_at(&mut self, worker_index: usize) {
if worker_index == 0 {
self.worker.stop();
} else if let Some(w) = self.extra_workers.get_mut(worker_index.saturating_sub(1)) {
w.stop();
}
}
fn start_worker_at(&mut self, worker_index: usize) {
if worker_index == 0 {
self.worker.start();
} else if let Some(w) = self.extra_workers.get_mut(worker_index.saturating_sub(1)) {
w.start();
}
}
fn process_pending_restarts(&mut self) {
if self.pending_restarts.is_empty() {
return;
}
let now = Instant::now();
let mut i = 0;
while i < self.pending_restarts.len() {
if now < self.pending_restarts[i].next_retry_at {
i = i.saturating_add(1);
continue;
}
let pr = self.pending_restarts.remove(i);
if pr.worker_index >= self.worker_count() {
continue;
}
self.stop_worker_at(pr.worker_index);
self.start_worker_at(pr.worker_index);
let _ = self.queue_supervisor.health_check_worker(pr.supervised_id, true);
let _ = self
.domain_supervisor
.mark_queue_healthy(u64::from(self.config.queue_id), true);
let _ = self
.node_supervisor
.mark_domain_healthy(u64::from(self.config.domain_id), true);
}
}
fn handle_worker_fault(&mut self, worker_index: usize) {
let Some(&supervised_id) = self.supervised_worker_ids.get(worker_index) else {
return;
};
let queue_id = u64::from(self.config.queue_id);
let domain_id = u64::from(self.config.domain_id);
let _ = self.queue_supervisor.health_check_worker(supervised_id, false);
let _ = self.domain_supervisor.mark_queue_healthy(queue_id, false);
let _ = self.node_supervisor.mark_domain_healthy(domain_id, false);
if let Some(recorder) = &self.fault_recorder
&& let Ok(mut r) = recorder.lock()
{
r.record(FaultType::WorkerPanic, "zenith-runtime.worker");
}
if let Some(metrics) = &self.metrics
&& let Ok(mut m) = metrics.lock()
{
let _ = m.counter_inc("zenith_runtime_worker_faults_total", &[]);
}
match self
.queue_supervisor
.restart_worker(supervised_id, ExitReason::Abnormal)
{
Ok(backoff) => {
self.last_restart_backoff = Some(backoff);
if backoff == std::time::Duration::ZERO {
self.remove_worker_at(worker_index);
return;
}
self.stop_worker_at(worker_index);
self.pending_restarts.push(PendingRestart {
worker_index,
supervised_id,
next_retry_at: Instant::now()
.checked_add(backoff)
.unwrap_or_else(Instant::now),
});
}
Err(_) => {
self.last_restart_backoff = None;
}
}
}
fn aggregated_worker_stats(&self) -> WorkerStats {
let mut agg = self.worker.stats();
let mut count = 1u64;
for w in &self.extra_workers {
let s = w.stats();
agg.rx_packets = agg.rx_packets.saturating_add(s.rx_packets);
agg.tx_packets = agg.tx_packets.saturating_add(s.tx_packets);
agg.rejected_packets = agg.rejected_packets.saturating_add(s.rejected_packets);
agg.parse_errors = agg.parse_errors.saturating_add(s.parse_errors);
agg.frame_allocs = agg.frame_allocs.saturating_add(s.frame_allocs);
agg.frame_frees = agg.frame_frees.saturating_add(s.frame_frees);
agg.quarantined_frames = agg.quarantined_frames.saturating_add(s.quarantined_frames);
agg.cycles_completed = agg.cycles_completed.saturating_add(s.cycles_completed);
agg.queue_depth = agg.queue_depth.saturating_add(s.queue_depth);
agg.throughput = agg.throughput.saturating_add(s.throughput);
agg.latency_p99 = agg.latency_p99.max(s.latency_p99);
agg.cpu_usage += s.cpu_usage;
count = count.saturating_add(1);
}
agg.cpu_usage /= count as f64;
agg
}
fn report_cycle_metrics(&mut self, wstats: &WorkerStats) {
let Some(metrics) = &self.metrics else {
return;
};
let Ok(mut m) = metrics.lock() else {
return;
};
let processed = wstats.rx_packets.saturating_add(wstats.tx_packets);
let _ = m.counter_add(
"zenith_runtime_packets_processed_total",
processed.saturating_sub(self.metrics_last_processed),
&[],
);
let _ = m.counter_add(
"zenith_runtime_admission_rejected_total",
wstats.rejected_packets.saturating_sub(self.metrics_last_rejected),
&[],
);
let _ = m.counter_add(
"zenith_runtime_parse_errors_total",
wstats.parse_errors.saturating_sub(self.metrics_last_dropped),
&[],
);
let _ = m.gauge_set(
"zenith_runtime_latency_p99_us",
wstats.latency_p99.min(i64::MAX as u64) as i64,
&[],
);
self.metrics_last_processed = processed;
self.metrics_last_rejected = wstats.rejected_packets;
self.metrics_last_dropped = wstats.parse_errors;
}
fn update_runtime_stats(&mut self, wstats: &WorkerStats) {
self.stats.total_rx = wstats.rx_packets;
self.stats.total_tx = wstats.tx_packets;
self.stats.total_rejected = wstats.rejected_packets;
self.stats.total_parse_errors = wstats.parse_errors;
self.stats.total_packets = wstats.rx_packets.saturating_add(wstats.tx_packets);
}
}
impl Drop for WorkerRuntime {
fn drop(&mut self) {
self.stop();
}
}
impl std::fmt::Debug for WorkerRuntime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WorkerRuntime")
.field("state", &self.state)
.field("worker_id", &self.worker.id())
.field("stats", &self.stats)
.field("quarantined", &self.quarantined_frames)
.finish()
}
}
#[derive(Debug)]
pub enum RuntimeError {
NotRunning,
NotInitialized,
WorkerError(String),
EbpfError(String),
MapError(String),
XskError(String),
AdmissionError(String),
FrameError(String),
DualBankNotEnabled,
XskFdNotFound,
InvalidConfig(String),
}
impl std::fmt::Display for RuntimeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RuntimeError::NotRunning => write!(f, "runtime not running"),
RuntimeError::NotInitialized => write!(f, "runtime not initialized"),
RuntimeError::WorkerError(e) => write!(f, "worker error: {}", e),
RuntimeError::EbpfError(e) => write!(f, "ebpf error: {}", e),
RuntimeError::MapError(e) => write!(f, "map error: {}", e),
RuntimeError::XskError(e) => write!(f, "xsk error: {}", e),
RuntimeError::AdmissionError(e) => write!(f, "admission error: {}", e),
RuntimeError::FrameError(e) => write!(f, "frame error: {}", e),
RuntimeError::DualBankNotEnabled => write!(f, "dual bank not enabled"),
RuntimeError::XskFdNotFound => write!(f, "XSK file descriptor not found"),
RuntimeError::InvalidConfig(e) => write!(f, "invalid config: {}", e),
}
}
}
impl std::error::Error for RuntimeError {}
impl From<zenith_net::error::NetError> for RuntimeError {
fn from(e: zenith_net::error::NetError) -> Self {
RuntimeError::WorkerError(e.to_string())
}
}
#[cfg(target_os = "linux")]
impl From<zenith_ebpf::error::LoadError> for RuntimeError {
fn from(e: zenith_ebpf::error::LoadError) -> Self {
RuntimeError::EbpfError(e.to_string())
}
}
#[cfg(target_os = "linux")]
impl From<zenith_ebpf::error::AttachError> for RuntimeError {
fn from(e: zenith_ebpf::error::AttachError) -> Self {
RuntimeError::EbpfError(e.to_string())
}
}
#[cfg(target_os = "linux")]
impl From<zenith_ebpf::error::ManagerError> for RuntimeError {
fn from(e: zenith_ebpf::error::ManagerError) -> Self {
RuntimeError::EbpfError(e.to_string())
}
}
impl From<zenith_foundation::error::CoreError> for RuntimeError {
fn from(e: zenith_foundation::error::CoreError) -> Self {
RuntimeError::FrameError(e.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use zenith_net::source_admission::{AdmissionAction, IpAddr};
fn create_test_config() -> WorkerRuntimeConfig {
WorkerRuntimeConfig::default()
}
#[test]
fn test_runtime_creation() {
let config = create_test_config();
let runtime = WorkerRuntime::new(config);
assert!(runtime.is_ok());
let runtime = runtime.unwrap();
assert_eq!(runtime.state(), RuntimeState::Created);
assert_eq!(runtime.worker_id(), 0);
}
#[test]
fn test_runtime_start_stop() {
let config = create_test_config();
let mut runtime = WorkerRuntime::new(config).unwrap();
assert_eq!(runtime.state(), RuntimeState::Created);
runtime.start().unwrap();
assert_eq!(runtime.state(), RuntimeState::Running);
runtime.stop();
assert_eq!(runtime.state(), RuntimeState::Stopped);
}
#[test]
fn test_runtime_not_running() {
let config = create_test_config();
let mut runtime = WorkerRuntime::new(config).unwrap();
let result = runtime.run_cycle();
assert!(result.is_err());
}
#[test]
fn test_runtime_full_lifecycle() {
let config = create_test_config();
let mut runtime = WorkerRuntime::new(config).unwrap();
let result = runtime.run_full_lifecycle(5);
assert!(result.is_ok());
let packets = result.unwrap();
assert_eq!(packets, 0);
assert_eq!(runtime.state(), RuntimeState::Stopped);
}
#[cfg(target_os = "linux")]
#[test]
fn test_runtime_full_lifecycle_with_data() {
let config = create_test_config();
let mut runtime = WorkerRuntime::new(config).unwrap();
runtime.start().unwrap();
runtime.run_cycle().unwrap();
runtime.worker.simulate_rx_transfer(2, 4096, |_idx, data| {
data.fill(0xAB);
});
let processed = runtime.run_cycle().unwrap();
assert_eq!(processed, 2);
let stats = runtime.stats();
assert_eq!(stats.total_rx, 2);
runtime.stop();
}
#[test]
fn test_runtime_with_admission_rules() {
let config = create_test_config();
let mut runtime = WorkerRuntime::new(config).unwrap();
let rule = AdmissionRule {
id: 1,
src_ip: IpAddr::V4_WILDCARD,
prefix_len: 0,
src_port: 0,
dst_port: 8080,
proto: zenith_net::source_admission::ProtoMatch::Tcp,
action: AdmissionAction::Allow,
enabled: true,
};
runtime.add_admission_rule(rule).unwrap();
let admission = runtime.admission();
assert!(admission.rule_count() >= 1);
}
#[test]
fn test_runtime_quarantine() {
let config = create_test_config();
let mut runtime = WorkerRuntime::new(config).unwrap();
assert_eq!(runtime.quarantined_count(), 0);
runtime.start().unwrap();
let token = runtime.frame_pool_mut().allocate(0).unwrap();
let frame_id = token.frame_id();
runtime.quarantine_frame(frame_id, "test").unwrap();
assert_eq!(runtime.quarantined_count(), 1);
let recovered = runtime.recover_all_quarantined().unwrap();
assert_eq!(recovered, 1);
assert_eq!(runtime.quarantined_count(), 0);
runtime.stop();
}
#[test]
fn test_runtime_debug() {
let config = create_test_config();
let runtime = WorkerRuntime::new(config).unwrap();
let debug_str = format!("{:?}", runtime);
assert!(debug_str.contains("WorkerRuntime"));
assert!(debug_str.contains("Created"));
}
#[test]
fn test_runtime_stats() {
let config = create_test_config();
let mut runtime = WorkerRuntime::new(config).unwrap();
runtime.start().unwrap();
runtime.run_cycle().unwrap();
let stats = runtime.stats();
assert!(stats.cycles_completed >= 1);
assert_eq!(stats.total_packets, 0);
}
#[test]
fn test_verify_conservation() {
let config = create_test_config();
let runtime = WorkerRuntime::new(config).unwrap();
assert!(runtime.verify_conservation());
}
#[test]
fn test_capability_detection_integrated() {
let runtime = WorkerRuntime::new(create_test_config()).unwrap();
assert!(runtime.capability_snapshot().cpu_cores >= 1);
match runtime.data_plane_profile() {
zenith_capability::profile::Profile::Performance
| zenith_capability::profile::Profile::Balanced
| zenith_capability::profile::Profile::Minimal => {}
}
}
#[test]
fn test_ledger_real_quota_and_rollup() {
let mut runtime = WorkerRuntime::new(create_test_config()).unwrap();
assert_eq!(
runtime.ledger().total().frame_count,
u64::from(runtime.config().frame_pool_capacity)
);
assert_eq!(
runtime.ledger().total().memory_bytes,
u64::from(runtime.config().frame_pool_capacity) * FRAME_BYTES
);
runtime.start().unwrap();
runtime.run_cycle().unwrap();
let pool_allocated = u64::from(runtime.frame_pool().allocated_count());
assert_eq!(runtime.ledger().used().frame_count, pool_allocated);
let child = runtime
.ledger()
.get_child("queue-0")
.unwrap();
assert_eq!(child.used().frame_count, pool_allocated);
runtime.stop();
}
#[test]
fn test_supervisor_three_level_integrated() {
let mut runtime = WorkerRuntime::new(create_test_config()).unwrap();
let report = runtime.supervisor_health();
assert!(report.total >= 1);
assert!(report.is_healthy());
assert_eq!(runtime.supervised_worker_count(), 1);
}
#[test]
fn test_worker_fault_restart_via_supervisor() {
let mut runtime = WorkerRuntime::new(create_test_config()).unwrap();
runtime.start().unwrap();
runtime.handle_worker_fault(0);
let backoff = runtime
.last_restart_backoff()
.expect("重启后必须记录退避时长");
assert!(
backoff <= std::time::Duration::from_millis(10),
"首次退避不得超过 initial 10ms,实际 {backoff:?}"
);
assert_eq!(runtime.pending_restarts.len(), 1);
assert_eq!(runtime.worker_state(), zenith_net::worker::WorkerState::Stopped);
std::thread::sleep(std::time::Duration::from_millis(15));
runtime.run_cycle().unwrap();
assert!(runtime.pending_restarts.is_empty());
assert_eq!(runtime.worker_state(), zenith_net::worker::WorkerState::Running);
assert!(runtime.supervisor_health().is_healthy());
runtime.stop();
}
#[test]
fn test_fault_backoff_does_not_block_run_cycle() {
let mut runtime = WorkerRuntime::new(create_test_config()).unwrap();
runtime.start().unwrap();
let t0 = Instant::now();
runtime.handle_worker_fault(0);
let fault_elapsed = t0.elapsed();
assert!(
fault_elapsed < std::time::Duration::from_millis(5),
"handle_worker_fault 疑似阻塞退避: {fault_elapsed:?}"
);
let t1 = Instant::now();
runtime.run_cycle().unwrap();
let cycle_elapsed = t1.elapsed();
assert!(
cycle_elapsed < std::time::Duration::from_millis(50),
"run_cycle 疑似被退避 sleep 阻塞: {cycle_elapsed:?}"
);
runtime.stop();
}
#[test]
fn test_qos_level_applied_to_worker() {
let mut runtime = WorkerRuntime::new(create_test_config()).unwrap();
assert!(!runtime.worker.quarantine_on_deny());
runtime.set_qos_level(QosLevel::Critical);
runtime.apply_qos_to_worker();
assert!(runtime.worker.quarantine_on_deny());
assert!(runtime.worker.quarantine_on_error());
runtime.set_qos_level(QosLevel::Background);
runtime.apply_qos_to_worker();
assert!(!runtime.worker.quarantine_on_deny());
assert!(!runtime.worker.quarantine_on_error());
}
#[test]
fn test_replan_topology_applies_result() {
let mut runtime = WorkerRuntime::new(create_test_config()).unwrap();
let mut graph: RuntimeGraph<64, 256> = RuntimeGraph::new();
graph
.add_node(ResourceNode::new(0, ExecutionDomain::DataPlane, 8, 16384, 8))
.unwrap();
let snapshots = vec![DomainSnapshot::from_domain(ExecutionDomain::DataPlane, 1)];
runtime.replan_topology(&graph, &snapshots).unwrap();
let topology = runtime.last_planned_topology().unwrap();
assert_eq!(topology.domain_count(), 1);
assert_eq!(topology.total_workers(), 1);
#[cfg(target_os = "linux")]
assert!(runtime.replan_apply_error().is_none());
}
#[test]
fn test_pending_replan_consumed_by_run_cycle() {
let mut runtime = WorkerRuntime::new(create_test_config()).unwrap();
runtime.start().unwrap();
runtime.replan_pending.store(true, Ordering::SeqCst);
runtime.run_cycle().unwrap();
assert!(!runtime.replan_pending.load(Ordering::SeqCst));
assert!(
runtime.last_planned_topology().is_some() || runtime.replan_apply_error().is_some()
);
runtime.stop();
}
#[cfg(target_os = "linux")]
#[test]
fn test_switch_ebpf_bank_requires_dual_bank() {
let mut runtime = WorkerRuntime::new(create_test_config()).unwrap();
let err = runtime.switch_ebpf_bank().unwrap_err();
assert!(matches!(err, RuntimeError::DualBankNotEnabled));
}
#[test]
fn test_cache_warmup_hook_invoked() {
use std::sync::atomic::AtomicU64 as AU64;
let hits = Arc::new(AU64::new(0));
let hits_clone = hits.clone();
let mut runtime = WorkerRuntime::new(create_test_config()).unwrap();
runtime.set_cache_warmup_hook(Box::new(move |keys| {
if !keys.is_empty() {
hits_clone.fetch_add(1, Ordering::SeqCst);
}
}));
runtime.start().unwrap();
for _ in 0..30 {
runtime.record_cache_access("hot-session-key");
}
std::thread::sleep(std::time::Duration::from_millis(350));
runtime.stop();
assert!(
hits.load(Ordering::SeqCst) > 0,
"缓存预热回调未被控制线程调用"
);
}
#[test]
fn test_target_worker_count_reconciled() {
let mut runtime = WorkerRuntime::new(create_test_config()).unwrap();
runtime.start().unwrap();
runtime.target_worker_count.store(3, Ordering::Relaxed);
runtime.run_cycle().unwrap();
assert_eq!(runtime.supervised_worker_count(), 3);
assert_eq!(runtime.worker_count(), 3);
runtime.target_worker_count.store(1, Ordering::Relaxed);
runtime.run_cycle().unwrap();
assert_eq!(runtime.supervised_worker_count(), 1);
assert_eq!(runtime.worker_count(), 1);
runtime.stop();
}
#[test]
fn test_multi_worker_spawn_and_drive() {
let mut runtime = WorkerRuntime::new(create_test_config()).unwrap();
runtime.start().unwrap();
assert_eq!(runtime.worker_count(), 1);
runtime.target_worker_count.store(3, Ordering::Relaxed);
runtime.run_cycle().unwrap();
assert_eq!(runtime.worker_count(), 3);
assert_eq!(runtime.supervised_worker_count(), 3);
assert!(runtime.verify_conservation());
runtime.run_cycle().unwrap();
let agg = runtime.aggregated_worker_stats();
assert!(
agg.cycles_completed >= 4,
"多 worker 周期未被全部驱动: {}",
agg.cycles_completed
);
runtime.target_worker_count.store(1, Ordering::Relaxed);
runtime.run_cycle().unwrap();
assert_eq!(runtime.worker_count(), 1);
assert_eq!(runtime.supervised_worker_count(), 1);
runtime.stop();
}
#[cfg(target_os = "linux")]
#[test]
fn test_multi_worker_data_aggregation() {
let mut runtime = WorkerRuntime::new(create_test_config()).unwrap();
runtime.start().unwrap();
runtime.target_worker_count.store(2, Ordering::Relaxed);
runtime.run_cycle().unwrap();
assert_eq!(runtime.worker_count(), 2);
runtime.run_cycle().unwrap();
runtime.worker.simulate_rx_transfer(2, 4096, |_idx, data| {
data.fill(0xAB);
});
runtime.extra_workers[0].simulate_rx_transfer(3, 4096, |_idx, data| {
data.fill(0xCD);
});
let processed = runtime.run_cycle().unwrap();
assert_eq!(processed, 5, "多 worker 处理包数必须汇总");
let stats = runtime.stats();
assert_eq!(stats.total_rx, 5);
runtime.stop();
}
#[test]
fn test_metrics_reporting_accumulates() {
let metrics = Arc::new(Mutex::new(MetricsCollector::new()));
let mut runtime = WorkerRuntime::new(create_test_config())
.unwrap()
.with_metrics(metrics.clone());
runtime.start().unwrap();
for _ in 0..3 {
runtime.run_cycle().unwrap();
}
runtime.stop();
let m = metrics.lock().unwrap();
let snapshot = m.snapshot();
for name in [
"zenith_runtime_packets_processed_total",
"zenith_runtime_admission_rejected_total",
"zenith_runtime_parse_errors_total",
"zenith_runtime_latency_p99_us",
] {
assert!(
snapshot.iter().any(|s| s.name == name),
"指标 {name} 未上报,快照: {snapshot:?}"
);
}
}
#[cfg(target_os = "linux")]
#[test]
fn test_metrics_counter_values_accumulate() {
let metrics = Arc::new(Mutex::new(MetricsCollector::new()));
let mut runtime = WorkerRuntime::new(create_test_config())
.unwrap()
.with_metrics(metrics.clone());
runtime.start().unwrap();
runtime.run_cycle().unwrap();
runtime.worker.simulate_rx_transfer(2, 4096, |_idx, data| {
data.fill(0xAB);
});
runtime.run_cycle().unwrap();
runtime.stop();
let m = metrics.lock().unwrap();
let snapshot = m.snapshot();
let processed = snapshot
.iter()
.find(|s| s.name == "zenith_runtime_packets_processed_total")
.expect("processed 指标必须存在");
assert_eq!(processed.value, 2, "processed counter 必须真实累计");
}
#[test]
fn test_control_thread_reports_qos_events() {
let metrics = Arc::new(Mutex::new(MetricsCollector::new()));
let mut runtime = WorkerRuntime::new(create_test_config())
.unwrap()
.with_metrics(metrics.clone());
runtime.start().unwrap();
std::thread::sleep(std::time::Duration::from_millis(350));
runtime.stop();
let m = metrics.lock().unwrap();
let snapshot = m.snapshot();
assert!(
snapshot
.iter()
.any(|s| s.name == "zenith_runtime_qos_level_changes_total" && s.value >= 1),
"QoS 等级变更事件未上报,快照: {snapshot:?}"
);
assert!(
snapshot.iter().any(|s| s.name == "zenith_runtime_qos_level"),
"QoS 等级 gauge 未上报,快照: {snapshot:?}"
);
}
#[test]
fn test_fault_recorded_to_observability() {
let metrics = Arc::new(Mutex::new(MetricsCollector::new()));
let recorder = Arc::new(Mutex::new(FaultRecorder::new()));
let mut runtime = WorkerRuntime::new(create_test_config())
.unwrap()
.with_metrics(metrics.clone())
.with_fault_recorder(recorder.clone());
runtime.start().unwrap();
runtime.handle_worker_fault(0);
assert_eq!(
recorder.lock().unwrap().len(),
1,
"故障事件未记录到 FaultRecorder"
);
let m = metrics.lock().unwrap();
let snapshot = m.snapshot();
assert!(
snapshot
.iter()
.any(|s| s.name == "zenith_runtime_worker_faults_total" && s.value >= 1),
"故障计数器未累计,快照: {snapshot:?}"
);
runtime.stop();
}
#[test]
fn test_spawned_worker_replays_admission_rules() {
let mut runtime = WorkerRuntime::new(create_test_config()).unwrap();
let rule = AdmissionRule {
id: 7,
src_ip: IpAddr::V4_WILDCARD,
prefix_len: 0,
src_port: 0,
dst_port: 443,
proto: zenith_net::source_admission::ProtoMatch::Tcp,
action: AdmissionAction::Allow,
enabled: true,
};
runtime.add_admission_rule(rule).unwrap();
runtime.start().unwrap();
runtime.target_worker_count.store(2, Ordering::Relaxed);
runtime.run_cycle().unwrap();
assert_eq!(runtime.worker_count(), 2);
assert_eq!(
runtime.extra_workers[0].admission().rule_count(),
runtime.admission().rule_count(),
"扩容 worker 未重放准入规则"
);
runtime.stop();
}
}