mirror of
https://github.com/RightNow-AI/openfang.git
synced 2026-07-30 23:05:08 +00:00
Open-source Agent OS built in Rust. - 14 crates, 1,767+ tests, zero clippy warnings - 7 autonomous Hands (Clip, Lead, Collector, Predictor, Researcher, Twitter, Browser) - 16 security systems (WASM sandbox, Merkle audit trail, taint tracking, Ed25519 signing, SSRF protection, secret zeroization, HMAC-SHA256 mutual auth, and more) - 30 pre-built agents across 4 performance tiers - 40 channel adapters (Telegram, Discord, Slack, WhatsApp, Teams, and 35 more) - 38 built-in tools + MCP client/server + A2A protocol - 26 LLM providers with intelligent routing and cost tracking - 60+ bundled skills with FangHub marketplace - Tauri 2.0 native desktop app - 140+ REST/WS/SSE API endpoints with Alpine.js dashboard - OpenAI-compatible /v1/chat/completions endpoint - One-command install, production-ready
228 lines
6.8 KiB
Rust
228 lines
6.8 KiB
Rust
//! Process supervision — graceful shutdown, signal handling, and health monitoring.
|
|
|
|
use dashmap::DashMap;
|
|
use openfang_types::agent::AgentId;
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
use tokio::sync::watch;
|
|
use tracing::{info, warn};
|
|
|
|
/// Shutdown signal manager with health monitoring.
|
|
pub struct Supervisor {
|
|
/// Send side of the shutdown signal.
|
|
shutdown_tx: watch::Sender<bool>,
|
|
/// Receive side of the shutdown signal (clonable).
|
|
shutdown_rx: watch::Receiver<bool>,
|
|
/// Restart count (how many times agents have been restarted).
|
|
restart_count: AtomicU64,
|
|
/// Total panics caught across all agents.
|
|
panic_count: AtomicU64,
|
|
/// Per-agent restart counts for enforcing max_restarts.
|
|
agent_restarts: DashMap<AgentId, u32>,
|
|
}
|
|
|
|
impl Supervisor {
|
|
/// Create a new supervisor.
|
|
pub fn new() -> Self {
|
|
let (tx, rx) = watch::channel(false);
|
|
Self {
|
|
shutdown_tx: tx,
|
|
shutdown_rx: rx,
|
|
restart_count: AtomicU64::new(0),
|
|
panic_count: AtomicU64::new(0),
|
|
agent_restarts: DashMap::new(),
|
|
}
|
|
}
|
|
|
|
/// Get a receiver that will be notified on shutdown.
|
|
pub fn subscribe(&self) -> watch::Receiver<bool> {
|
|
self.shutdown_rx.clone()
|
|
}
|
|
|
|
/// Trigger a graceful shutdown.
|
|
pub fn shutdown(&self) {
|
|
info!("Supervisor: initiating graceful shutdown");
|
|
let _ = self.shutdown_tx.send(true);
|
|
}
|
|
|
|
/// Check if shutdown has been requested.
|
|
pub fn is_shutting_down(&self) -> bool {
|
|
*self.shutdown_rx.borrow()
|
|
}
|
|
|
|
/// Record that a panic was caught during agent execution.
|
|
pub fn record_panic(&self) {
|
|
self.panic_count.fetch_add(1, Ordering::Relaxed);
|
|
warn!(
|
|
total_panics = self.panic_count.load(Ordering::Relaxed),
|
|
"Agent panic recorded"
|
|
);
|
|
}
|
|
|
|
/// Record that an agent was restarted.
|
|
pub fn record_restart(&self) {
|
|
self.restart_count.fetch_add(1, Ordering::Relaxed);
|
|
}
|
|
|
|
/// Get the total number of panics caught.
|
|
pub fn panic_count(&self) -> u64 {
|
|
self.panic_count.load(Ordering::Relaxed)
|
|
}
|
|
|
|
/// Get the total number of restarts.
|
|
pub fn restart_count(&self) -> u64 {
|
|
self.restart_count.load(Ordering::Relaxed)
|
|
}
|
|
|
|
/// Record a restart for a specific agent and check if limit is exceeded.
|
|
///
|
|
/// Returns Ok(restart_count) if within limit, or Err(count) if limit exceeded.
|
|
pub fn record_agent_restart(&self, agent_id: AgentId, max_restarts: u32) -> Result<u32, u32> {
|
|
let mut count = self.agent_restarts.entry(agent_id).or_insert(0);
|
|
*count += 1;
|
|
self.record_restart();
|
|
|
|
if max_restarts > 0 && *count > max_restarts {
|
|
warn!(
|
|
agent = %agent_id,
|
|
restarts = *count,
|
|
max = max_restarts,
|
|
"Agent exceeded max restart limit"
|
|
);
|
|
Err(*count)
|
|
} else {
|
|
Ok(*count)
|
|
}
|
|
}
|
|
|
|
/// Get the restart count for a specific agent.
|
|
pub fn agent_restart_count(&self, agent_id: AgentId) -> u32 {
|
|
self.agent_restarts.get(&agent_id).map(|r| *r).unwrap_or(0)
|
|
}
|
|
|
|
/// Reset restart counter for an agent (e.g., on manual intervention).
|
|
pub fn reset_agent_restarts(&self, agent_id: AgentId) {
|
|
self.agent_restarts.remove(&agent_id);
|
|
}
|
|
|
|
/// Get a health summary.
|
|
pub fn health(&self) -> SupervisorHealth {
|
|
SupervisorHealth {
|
|
is_shutting_down: self.is_shutting_down(),
|
|
panic_count: self.panic_count(),
|
|
restart_count: self.restart_count(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for Supervisor {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// Health report from the supervisor.
|
|
#[derive(Debug, Clone)]
|
|
pub struct SupervisorHealth {
|
|
pub is_shutting_down: bool,
|
|
pub panic_count: u64,
|
|
pub restart_count: u64,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_shutdown() {
|
|
let supervisor = Supervisor::new();
|
|
assert!(!supervisor.is_shutting_down());
|
|
supervisor.shutdown();
|
|
assert!(supervisor.is_shutting_down());
|
|
}
|
|
|
|
#[test]
|
|
fn test_subscribe() {
|
|
let supervisor = Supervisor::new();
|
|
let rx = supervisor.subscribe();
|
|
assert!(!*rx.borrow());
|
|
supervisor.shutdown();
|
|
assert!(rx.has_changed().unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn test_panic_tracking() {
|
|
let supervisor = Supervisor::new();
|
|
assert_eq!(supervisor.panic_count(), 0);
|
|
supervisor.record_panic();
|
|
supervisor.record_panic();
|
|
assert_eq!(supervisor.panic_count(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_restart_tracking() {
|
|
let supervisor = Supervisor::new();
|
|
assert_eq!(supervisor.restart_count(), 0);
|
|
supervisor.record_restart();
|
|
assert_eq!(supervisor.restart_count(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_health() {
|
|
let supervisor = Supervisor::new();
|
|
let health = supervisor.health();
|
|
assert!(!health.is_shutting_down);
|
|
assert_eq!(health.panic_count, 0);
|
|
assert_eq!(health.restart_count, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_agent_restart_within_limit() {
|
|
let supervisor = Supervisor::new();
|
|
let agent_id = AgentId::new();
|
|
|
|
// Allow up to 3 restarts
|
|
assert!(supervisor.record_agent_restart(agent_id, 3).is_ok());
|
|
assert_eq!(supervisor.agent_restart_count(agent_id), 1);
|
|
assert!(supervisor.record_agent_restart(agent_id, 3).is_ok());
|
|
assert!(supervisor.record_agent_restart(agent_id, 3).is_ok());
|
|
assert_eq!(supervisor.agent_restart_count(agent_id), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_agent_restart_exceeds_limit() {
|
|
let supervisor = Supervisor::new();
|
|
let agent_id = AgentId::new();
|
|
|
|
assert!(supervisor.record_agent_restart(agent_id, 2).is_ok());
|
|
assert!(supervisor.record_agent_restart(agent_id, 2).is_ok());
|
|
// 3rd restart exceeds max_restarts=2
|
|
let result = supervisor.record_agent_restart(agent_id, 2);
|
|
assert!(result.is_err());
|
|
assert_eq!(result.unwrap_err(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_agent_restart_zero_limit_unlimited() {
|
|
let supervisor = Supervisor::new();
|
|
let agent_id = AgentId::new();
|
|
|
|
// max_restarts=0 means unlimited
|
|
for _ in 0..100 {
|
|
assert!(supervisor.record_agent_restart(agent_id, 0).is_ok());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_reset_agent_restarts() {
|
|
let supervisor = Supervisor::new();
|
|
let agent_id = AgentId::new();
|
|
|
|
supervisor.record_agent_restart(agent_id, 10).unwrap();
|
|
supervisor.record_agent_restart(agent_id, 10).unwrap();
|
|
assert_eq!(supervisor.agent_restart_count(agent_id), 2);
|
|
|
|
supervisor.reset_agent_restarts(agent_id);
|
|
assert_eq!(supervisor.agent_restart_count(agent_id), 0);
|
|
}
|
|
}
|