use crate::supervisor::trait_::{Status, Supervisor, SupervisorError, SupervisorEvent};
use crate::types::{AgentId, AgentMeta, KillReason, KillTrigger, RuntimeState};
use async_trait::async_trait;
use bytes::Bytes;
use chrono::{DateTime, Duration, Utc};
use dashmap::DashMap;
use futures_core::stream::BoxStream;
use klieo_core::KvStore;
use std::sync::Arc;
use tokio::sync::broadcast;
use tokio_stream::wrappers::BroadcastStream;
use tokio_stream::StreamExt;
use tracing;
const BUCKET_HEARTBEAT: &str = "ops.supervisor.heartbeat";
const BUCKET_STATE: &str = "ops.supervisor.state";
const KEY_RUNTIME_STATE: &str = "runtime_state";
pub struct KvSupervisor {
kv: Arc<dyn KvStore>,
heartbeat_ttl: Duration,
metas: DashMap<AgentId, AgentMeta>,
events: broadcast::Sender<SupervisorEvent>,
}
impl KvSupervisor {
#[must_use]
pub fn with_defaults(kv: Arc<dyn KvStore>) -> Self {
Self::new(kv, Duration::seconds(5))
}
#[must_use]
pub fn new(kv: Arc<dyn KvStore>, heartbeat_ttl: Duration) -> Self {
let (tx, _rx) = broadcast::channel(256);
Self {
kv,
heartbeat_ttl,
metas: DashMap::new(),
events: tx,
}
}
fn emit(&self, ev: SupervisorEvent) {
if let Err(err) = self.events.send(ev) {
tracing::warn!(
event = ?err.0,
"supervisor event has no live receivers; subscriber may have dropped"
);
}
}
async fn read_runtime_state(&self) -> RuntimeState {
match self.kv.get(BUCKET_STATE, KEY_RUNTIME_STATE).await {
Ok(Some(entry)) => {
let raw = std::str::from_utf8(&entry.value).unwrap_or("");
match raw {
"draining" => RuntimeState::Draining,
"halted" => RuntimeState::Halted,
"running" => RuntimeState::Running,
_ => {
tracing::warn!(
raw,
"unrecognised runtime_state payload; defaulting to Halted"
);
RuntimeState::Halted
}
}
}
Ok(None) => RuntimeState::Running,
Err(_) => RuntimeState::Halted,
}
}
async fn write_runtime_state(&self, s: RuntimeState) -> Result<(), SupervisorError> {
self.kv
.put(BUCKET_STATE, KEY_RUNTIME_STATE, Bytes::from(s.to_string()))
.await
.map(|_| ())
.map_err(|e| SupervisorError::Storage(e.to_string()))
}
}
#[async_trait]
impl Supervisor for KvSupervisor {
async fn register(&self, agent: AgentId, meta: AgentMeta) -> Result<(), SupervisorError> {
self.metas.insert(agent.clone(), meta);
self.emit(SupervisorEvent::Registered(agent));
Ok(())
}
async fn heartbeat(&self, agent: AgentId) -> Result<(), SupervisorError> {
if !self.metas.contains_key(&agent) {
return Err(SupervisorError::UnknownAgent(agent));
}
let expiry: DateTime<Utc> = Utc::now() + self.heartbeat_ttl;
let bytes = Bytes::from(expiry.to_rfc3339());
self.kv
.put(BUCKET_HEARTBEAT, &agent.0, bytes)
.await
.map_err(|e| SupervisorError::Storage(e.to_string()))?;
self.emit(SupervisorEvent::Heartbeat(agent));
Ok(())
}
async fn report(&self, agent: AgentId, _status: Status) -> Result<(), SupervisorError> {
if !self.metas.contains_key(&agent) {
return Err(SupervisorError::UnknownAgent(agent));
}
Ok(())
}
async fn watch(&self) -> BoxStream<'static, SupervisorEvent> {
let rx = self.events.subscribe();
let stream = BroadcastStream::new(rx).filter_map(|res| res.ok());
Box::pin(stream)
}
async fn trip_kill_switch(
&self,
reason: KillReason,
_trigger: KillTrigger,
) -> Result<(), SupervisorError> {
self.write_runtime_state(RuntimeState::Halted).await?;
self.emit(SupervisorEvent::KillSwitchTripped { reason: reason.0 });
Ok(())
}
async fn runtime_state(&self) -> RuntimeState {
self.read_runtime_state().await
}
}
#[cfg(test)]
mod tests {
use super::*;
use bytes::Bytes;
use klieo_bus_memory::MemoryBus;
#[tokio::test]
async fn unknown_runtime_state_payload_is_halted() {
let bus = MemoryBus::new();
let kv: Arc<dyn klieo_core::KvStore> = bus.kv.clone();
let sup = KvSupervisor::with_defaults(kv.clone());
kv.put(BUCKET_STATE, KEY_RUNTIME_STATE, Bytes::from("xyzzy"))
.await
.expect("put ok");
assert_eq!(
sup.read_runtime_state().await,
RuntimeState::Halted,
"unrecognised payload must default to Halted (fail-CLOSED per spec §3.5)"
);
}
}