use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use aion_core::{
ActivityId, InterventionCapabilities, InterventionCommand, InterventionOutcome, WorkflowId,
};
use tokio::sync::{mpsc, oneshot};
use super::agent::ControlMessage;
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct SessionKey {
pub workflow_id: WorkflowId,
pub activity_id: ActivityId,
pub attempt: u32,
}
impl SessionKey {
#[must_use]
pub const fn new(workflow_id: WorkflowId, activity_id: ActivityId, attempt: u32) -> Self {
Self {
workflow_id,
activity_id,
attempt,
}
}
#[must_use]
pub fn of_command(command: &InterventionCommand) -> Self {
Self::new(
command.workflow_id.clone(),
command.activity_id.clone(),
command.attempt,
)
}
}
#[derive(Clone, Debug)]
struct SessionControl {
control: mpsc::UnboundedSender<ControlMessage>,
capabilities: InterventionCapabilities,
}
#[derive(Clone, Debug, Default)]
pub struct ControlRegistry {
inner: Arc<Mutex<HashMap<SessionKey, SessionControl>>>,
}
impl ControlRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn register(
&self,
key: SessionKey,
control: mpsc::UnboundedSender<ControlMessage>,
capabilities: InterventionCapabilities,
) -> SessionGuard {
if let Ok(mut index) = self.inner.lock() {
index.insert(
key.clone(),
SessionControl {
control,
capabilities,
},
);
}
SessionGuard {
registry: self.clone(),
key,
}
}
pub async fn deliver(&self, command: InterventionCommand) -> InterventionOutcome {
let key = SessionKey::of_command(&command);
let primitive = command.kind.primitive();
let Some(session) = self.lookup(&key) else {
return InterventionOutcome::stale_target(format!(
"no live session for attempt {} of activity {} in workflow {}",
key.attempt, key.activity_id, key.workflow_id
));
};
if !session.capabilities.supports(&command.kind) {
return InterventionOutcome::capability_not_supported(primitive);
}
let (ack_tx, ack_rx) = oneshot::channel();
if session
.control
.send(ControlMessage::with_ack(command, ack_tx))
.is_err()
{
return InterventionOutcome::stale_target(format!(
"session for attempt {} ended before the command was applied",
key.attempt
));
}
match ack_rx.await {
Ok(outcome) => outcome,
Err(_) => InterventionOutcome::stale_target(format!(
"session for attempt {} ended before acking the command",
key.attempt
)),
}
}
fn lookup(&self, key: &SessionKey) -> Option<SessionControl> {
self.inner.lock().ok()?.get(key).cloned()
}
fn remove(&self, key: &SessionKey) {
if let Ok(mut index) = self.inner.lock() {
index.remove(key);
}
}
}
#[derive(Debug)]
pub struct SessionGuard {
registry: ControlRegistry,
key: SessionKey,
}
impl Drop for SessionGuard {
fn drop(&mut self) {
self.registry.remove(&self.key);
}
}
#[cfg(test)]
#[path = "intervention_tests.rs"]
mod tests;