use std::collections::HashMap;
use std::sync::Arc;
use async_nats::jetstream::consumer::Consumer;
use async_nats::jetstream::consumer::pull::Config as PullConfig;
use async_nats::jetstream::kv::Store;
use async_nats::jetstream::message::Acker;
use tokio::sync::Mutex;
use crate::error::{DaimonError, Result};
use super::broker::TaskBroker;
use super::types::{AgentTask, TaskResult, TaskStatus};
pub struct NatsBroker {
client: async_nats::Client,
jetstream: async_nats::jetstream::Context,
stream_name: String,
subject_prefix: String,
status_kv: Store,
consumer: Arc<Mutex<Option<Consumer<PullConfig>>>>,
in_flight: Arc<Mutex<HashMap<String, Acker>>>,
}
impl NatsBroker {
pub async fn connect(url: &str, stream_name: impl Into<String>) -> Result<Self> {
let stream_name = stream_name.into();
let client = async_nats::connect(url)
.await
.map_err(|e| DaimonError::Other(format!("nats connect: {e}")))?;
let jetstream = async_nats::jetstream::new(client.clone());
let subject_prefix = format!("{stream_name}.tasks");
jetstream
.get_or_create_stream(async_nats::jetstream::stream::Config {
name: stream_name.clone(),
subjects: vec![format!("{subject_prefix}.>")],
retention: async_nats::jetstream::stream::RetentionPolicy::WorkQueue,
..Default::default()
})
.await
.map_err(|e| DaimonError::Other(format!("nats create stream: {e}")))?;
let bucket = format!("{stream_name}-status");
let status_kv = match jetstream
.create_key_value(async_nats::jetstream::kv::Config {
bucket: bucket.clone(),
history: 1,
..Default::default()
})
.await
{
Ok(store) => store,
Err(_) => jetstream
.get_key_value(&bucket)
.await
.map_err(|e| DaimonError::Other(format!("nats open kv bucket: {e}")))?,
};
Ok(Self {
client,
jetstream,
stream_name,
subject_prefix,
status_kv,
consumer: Arc::new(Mutex::new(None)),
in_flight: Arc::new(Mutex::new(HashMap::new())),
})
}
pub fn client(&self) -> &async_nats::Client {
&self.client
}
fn task_subject(&self) -> String {
format!("{}.submit", self.subject_prefix)
}
async fn ensure_consumer(&self) -> Result<Consumer<PullConfig>> {
let mut guard = self.consumer.lock().await;
if let Some(ref consumer) = *guard {
return Ok(consumer.clone());
}
let stream = self
.jetstream
.get_stream(&self.stream_name)
.await
.map_err(|e| DaimonError::Other(format!("nats get stream: {e}")))?;
let consumer: Consumer<PullConfig> = stream
.get_or_create_consumer(
"daimon-worker",
PullConfig {
durable_name: Some("daimon-worker".into()),
filter_subject: self.task_subject(),
ack_policy: async_nats::jetstream::consumer::AckPolicy::Explicit,
..Default::default()
},
)
.await
.map_err(|e| DaimonError::Other(format!("nats create consumer: {e}")))?;
*guard = Some(consumer.clone());
Ok(consumer)
}
}
impl TaskBroker for NatsBroker {
async fn submit(&self, task: AgentTask) -> Result<String> {
let id = task.task_id.clone();
let json = serde_json::to_string(&task)
.map_err(|e| DaimonError::Other(format!("serialize task: {e}")))?;
self.status_kv
.put(&id, status_marker(&TaskStatus::Pending).into())
.await
.map_err(|e| DaimonError::Other(format!("nats kv put pending: {e}")))?;
self.jetstream
.publish(self.task_subject(), json.into())
.await
.map_err(|e| DaimonError::Other(format!("nats publish: {e}")))?
.await
.map_err(|e| DaimonError::Other(format!("nats publish ack: {e}")))?;
Ok(id)
}
async fn status(&self, task_id: &str) -> Result<TaskStatus> {
let raw = self
.status_kv
.get(task_id)
.await
.map_err(|e| DaimonError::Other(format!("nats kv get status: {e}")))?;
let Some(bytes) = raw else {
return Ok(TaskStatus::Pending);
};
let status_str = String::from_utf8_lossy(&bytes);
match parse_status_marker(&status_str) {
StatusMarker::Pending => Ok(TaskStatus::Pending),
StatusMarker::Running => Ok(TaskStatus::Running),
StatusMarker::Failed(msg) => Ok(TaskStatus::Failed(msg)),
StatusMarker::Completed => {
let result_json = self
.status_kv
.get(result_key(task_id))
.await
.map_err(|e| DaimonError::Other(format!("nats kv get result: {e}")))?;
match result_json {
Some(bytes) => {
let result: TaskResult = serde_json::from_slice(&bytes)
.map_err(|e| DaimonError::Other(format!("deserialize result: {e}")))?;
Ok(TaskStatus::Completed(result))
}
None => Ok(TaskStatus::Completed(TaskResult {
task_id: task_id.to_string(),
output: String::new(),
iterations: 0,
cost: 0.0,
error: None,
})),
}
}
}
}
async fn receive(&self) -> Result<Option<AgentTask>> {
use futures::StreamExt;
let consumer = self.ensure_consumer().await?;
let mut messages = consumer
.fetch()
.max_messages(1)
.messages()
.await
.map_err(|e| DaimonError::Other(format!("nats fetch: {e}")))?;
match messages.next().await {
Some(Ok(msg)) => {
let (msg, acker) = msg.split();
let task: AgentTask = match serde_json::from_slice(&msg.payload) {
Ok(task) => task,
Err(e) => {
if let Err(term_err) =
acker.ack_with(async_nats::jetstream::AckKind::Term).await
{
tracing::warn!(
error = %term_err,
"failed to terminate poison NATS message"
);
}
return Err(DaimonError::Other(format!("deserialize task: {e}")));
}
};
{
let mut in_flight = self.in_flight.lock().await;
in_flight.insert(task.task_id.clone(), acker);
}
self.status_kv
.put(&task.task_id, status_marker(&TaskStatus::Running).into())
.await
.map_err(|e| DaimonError::Other(format!("nats kv put running: {e}")))?;
Ok(Some(task))
}
Some(Err(e)) => Err(DaimonError::Other(format!("nats message error: {e}"))),
None => Ok(None),
}
}
async fn complete(&self, task_id: &str, result: TaskResult) -> Result<()> {
let acker = self.in_flight.lock().await.remove(task_id);
if let Some(acker) = acker {
acker
.ack()
.await
.map_err(|e| DaimonError::Other(format!("nats ack: {e}")))?;
}
let json = serde_json::to_string(&result)
.map_err(|e| DaimonError::Other(format!("serialize result: {e}")))?;
self.status_kv
.put(result_key(task_id), json.into())
.await
.map_err(|e| DaimonError::Other(format!("nats kv put result: {e}")))?;
self.status_kv
.put(
task_id,
status_marker(&TaskStatus::Completed(result)).into(),
)
.await
.map_err(|e| DaimonError::Other(format!("nats kv put completed: {e}")))?;
Ok(())
}
async fn fail(&self, task_id: &str, error: String) -> Result<()> {
let acker = self.in_flight.lock().await.remove(task_id);
if let Some(acker) = acker {
acker
.ack()
.await
.map_err(|e| DaimonError::Other(format!("nats ack: {e}")))?;
}
self.status_kv
.put(task_id, status_marker(&TaskStatus::Failed(error)).into())
.await
.map_err(|e| DaimonError::Other(format!("nats kv put failed: {e}")))?;
Ok(())
}
}
fn result_key(task_id: &str) -> String {
format!("{task_id}.result")
}
#[derive(Debug, PartialEq, Eq)]
enum StatusMarker {
Pending,
Running,
Completed,
Failed(String),
}
fn status_marker(status: &TaskStatus) -> String {
match status {
TaskStatus::Pending => "pending".to_string(),
TaskStatus::Running => "running".to_string(),
TaskStatus::Completed(_) => "completed".to_string(),
TaskStatus::Failed(msg) => format!("failed:{msg}"),
}
}
fn parse_status_marker(s: &str) -> StatusMarker {
match s {
"pending" => StatusMarker::Pending,
"running" => StatusMarker::Running,
"completed" => StatusMarker::Completed,
other => match other.strip_prefix("failed:") {
Some(msg) => StatusMarker::Failed(msg.to_string()),
None => StatusMarker::Pending,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_subject_generation() {
let prefix = "daimon-tasks.tasks";
assert_eq!(format!("{prefix}.submit"), "daimon-tasks.tasks.submit");
}
#[test]
fn test_task_serialization_for_nats() {
let task = AgentTask::new("test input")
.with_run_id("r1")
.with_metadata("priority", serde_json::json!(1));
let json = serde_json::to_string(&task).unwrap();
let deser: AgentTask = serde_json::from_str(&json).unwrap();
assert_eq!(deser.input, "test input");
assert_eq!(deser.run_id.as_deref(), Some("r1"));
assert_eq!(deser.metadata["priority"], 1);
}
#[test]
fn test_result_serialization_for_nats() {
let result = TaskResult {
task_id: "t-nats".into(),
output: "nats result".into(),
iterations: 3,
cost: 0.01,
error: None,
};
let json = serde_json::to_string(&result).unwrap();
let deser: TaskResult = serde_json::from_str(&json).unwrap();
assert_eq!(deser.output, "nats result");
}
#[test]
fn test_result_key_derivation() {
assert_eq!(result_key("task-abc"), "task-abc.result");
}
#[test]
fn test_status_marker_encode() {
assert_eq!(status_marker(&TaskStatus::Pending), "pending");
assert_eq!(status_marker(&TaskStatus::Running), "running");
assert_eq!(
status_marker(&TaskStatus::Completed(TaskResult {
task_id: "t".into(),
output: "ok".into(),
iterations: 1,
cost: 0.0,
error: None,
})),
"completed"
);
assert_eq!(
status_marker(&TaskStatus::Failed("boom".into())),
"failed:boom"
);
}
#[test]
fn test_status_marker_parse() {
assert_eq!(parse_status_marker("pending"), StatusMarker::Pending);
assert_eq!(parse_status_marker("running"), StatusMarker::Running);
assert_eq!(parse_status_marker("completed"), StatusMarker::Completed);
assert_eq!(
parse_status_marker("failed:something broke"),
StatusMarker::Failed("something broke".into())
);
assert_eq!(parse_status_marker("garbage"), StatusMarker::Pending);
}
#[test]
fn test_status_marker_roundtrip_failed_with_colon() {
let msg = "http error: 500: upstream";
let encoded = status_marker(&TaskStatus::Failed(msg.into()));
assert_eq!(
parse_status_marker(&encoded),
StatusMarker::Failed(msg.into())
);
}
}