use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::{Mutex, OnceLock},
};
use tokio::sync::{broadcast, watch};
use super::async_worker::{AsyncWorkerKey, build_worker_key};
use crate::transports::WorkerReadinessState;
const CHOICES_QUEUE_BROADCAST_CAPACITY: usize = 64;
#[derive(Clone, Debug)]
pub enum ChoicesQueueEvent {
Enqueued {
choice_request_id: String,
message_id: String,
target_session: String,
},
Resolved {
choice_request_id: String,
target_session: String,
},
Invalidated {
choice_request_id: String,
target_session: String,
reason_code: String,
},
}
static WORKER_READINESS_PUBLISHERS: OnceLock<
Mutex<HashMap<AsyncWorkerKey, watch::Sender<Option<WorkerReadinessState>>>>,
> = OnceLock::new();
static CHOICES_QUEUE_PUBLISHERS: OnceLock<
Mutex<HashMap<PathBuf, broadcast::Sender<ChoicesQueueEvent>>>,
> = OnceLock::new();
fn worker_state_publishers()
-> &'static Mutex<HashMap<AsyncWorkerKey, watch::Sender<Option<WorkerReadinessState>>>> {
WORKER_READINESS_PUBLISHERS.get_or_init(|| Mutex::new(HashMap::new()))
}
fn choices_queue_publishers()
-> &'static Mutex<HashMap<PathBuf, broadcast::Sender<ChoicesQueueEvent>>> {
CHOICES_QUEUE_PUBLISHERS.get_or_init(|| Mutex::new(HashMap::new()))
}
pub fn subscribe_worker_readiness(
namespace: &str,
runtime_directory: &Path,
target_session: &str,
) -> watch::Receiver<Option<WorkerReadinessState>> {
let key = build_worker_key(namespace, runtime_directory, target_session);
let mut publishers = worker_state_publishers()
.lock()
.expect("worker state publishers mutex poisoned");
publishers
.entry(key)
.or_insert_with(|| watch::channel(None).0)
.subscribe()
}
pub fn subscribe_choices_queue_events(
runtime_directory: &Path,
) -> broadcast::Receiver<ChoicesQueueEvent> {
let mut publishers = choices_queue_publishers()
.lock()
.expect("choices queue publishers mutex poisoned");
publishers
.entry(runtime_directory.to_path_buf())
.or_insert_with(|| broadcast::channel(CHOICES_QUEUE_BROADCAST_CAPACITY).0)
.subscribe()
}
pub(super) fn publish_worker_readiness(key: &AsyncWorkerKey, state: WorkerReadinessState) {
let mut publishers = worker_state_publishers()
.lock()
.expect("worker state publishers mutex poisoned");
let sender = publishers
.entry(key.clone())
.or_insert_with(|| watch::channel(None).0);
let _ = sender.send(Some(state));
}
pub(super) fn publish_choices_queue_event(runtime_directory: &Path, event: ChoicesQueueEvent) {
let mut publishers = choices_queue_publishers()
.lock()
.expect("choices queue publishers mutex poisoned");
let sender = publishers
.entry(runtime_directory.to_path_buf())
.or_insert_with(|| broadcast::channel(CHOICES_QUEUE_BROADCAST_CAPACITY).0);
let _ = sender.send(event);
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[tokio::test(flavor = "current_thread")]
async fn publishers_deliver_state_and_queue_events_to_subscribers() {
let runtime_directory = PathBuf::from("/tmp/agentmux-observability-test-fake-runtime");
let namespace = "obstest_bundle";
let target_session = "obstest_target";
let mut state_receiver =
subscribe_worker_readiness(namespace, runtime_directory.as_path(), target_session);
assert_eq!(*state_receiver.borrow_and_update(), None);
let key = build_worker_key(namespace, runtime_directory.as_path(), target_session);
publish_worker_readiness(&key, WorkerReadinessState::Unavailable);
state_receiver
.changed()
.await
.expect("watch sender outlives subscriber");
assert_eq!(
*state_receiver.borrow(),
Some(WorkerReadinessState::Unavailable)
);
let mut queue_receiver = subscribe_choices_queue_events(runtime_directory.as_path());
publish_choices_queue_event(
runtime_directory.as_path(),
ChoicesQueueEvent::Enqueued {
choice_request_id: "perm-1".to_string(),
message_id: "msg-1".to_string(),
target_session: target_session.to_string(),
},
);
publish_choices_queue_event(
runtime_directory.as_path(),
ChoicesQueueEvent::Invalidated {
choice_request_id: "perm-1".to_string(),
target_session: target_session.to_string(),
reason_code: "runtime_choices_request_invalidated_by_respawn".to_string(),
},
);
match queue_receiver.recv().await.expect("first event") {
ChoicesQueueEvent::Enqueued {
choice_request_id,
message_id,
target_session: actual_target,
} => {
assert_eq!(choice_request_id, "perm-1");
assert_eq!(message_id, "msg-1");
assert_eq!(actual_target, target_session);
}
other => panic!("expected Enqueued, got {other:?}"),
}
match queue_receiver.recv().await.expect("second event") {
ChoicesQueueEvent::Invalidated {
choice_request_id,
target_session: actual_target,
reason_code,
} => {
assert_eq!(choice_request_id, "perm-1");
assert_eq!(actual_target, target_session);
assert_eq!(
reason_code,
"runtime_choices_request_invalidated_by_respawn"
);
}
other => panic!("expected Invalidated, got {other:?}"),
}
}
}