use aion_core::{
ActivityId, InterventionCapabilities, InterventionCommand, InterventionKind,
InterventionOutcome, RunId, WorkflowId,
};
use axum::{Json, extract::State};
use serde::{Deserialize, Serialize};
use super::auth::HttpCaller;
use super::error::HttpWireError;
use crate::namespace::WorkflowTarget;
use crate::worker::AttemptKey;
use crate::{NamespaceOperation, ServerError, ServerState};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct InterveneRequest {
pub namespace: String,
pub workflow_id: WorkflowId,
pub run_id: RunId,
pub activity_id: ActivityId,
pub attempt: u32,
pub kind: InterventionKind,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct InterveneResponse {
pub outcome: InterventionOutcome,
}
pub(crate) async fn intervene(
State(state): State<ServerState>,
HttpCaller(caller): HttpCaller,
Json(request): Json<InterveneRequest>,
) -> Result<Json<InterveneResponse>, HttpWireError> {
let outcome = run_intervention(&state, &caller, request)
.await
.map_err(|error| HttpWireError(error.to_wire_error()))?;
Ok(Json(InterveneResponse { outcome }))
}
async fn run_intervention(
state: &ServerState,
caller: &crate::CallerIdentity,
request: InterveneRequest,
) -> Result<InterventionOutcome, ServerError> {
let target = WorkflowTarget::workflow(&request.workflow_id);
let operation = NamespaceOperation::intervene(&request.namespace, target);
state.namespace_guard().scope(caller, &operation).await?;
let issued_by = match caller.subject() {
"" => None,
subject => Some(subject.to_owned()),
};
let command = InterventionCommand {
workflow_id: request.workflow_id,
run_id: request.run_id,
activity_id: request.activity_id,
attempt: request.attempt,
issued_by,
issued_at: chrono::Utc::now(),
kind: request.kind,
};
state.intervention_router().route(command).await
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct AttemptsRequest {
pub namespace: String,
pub workflow_id: WorkflowId,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct AttemptCapabilities {
pub run_id: RunId,
pub activity_id: ActivityId,
pub attempt: u32,
pub capabilities: InterventionCapabilities,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct AttemptsResponse {
pub attempts: Vec<AttemptCapabilities>,
}
pub(crate) async fn list_attempts(
State(state): State<ServerState>,
HttpCaller(caller): HttpCaller,
Json(request): Json<AttemptsRequest>,
) -> Result<Json<AttemptsResponse>, HttpWireError> {
let attempts = run_list_attempts(&state, &caller, request)
.await
.map_err(|error| HttpWireError(error.to_wire_error()))?;
Ok(Json(AttemptsResponse { attempts }))
}
async fn run_list_attempts(
state: &ServerState,
caller: &crate::CallerIdentity,
request: AttemptsRequest,
) -> Result<Vec<AttemptCapabilities>, ServerError> {
let target = WorkflowTarget::workflow(&request.workflow_id);
let operation = NamespaceOperation::intervene(&request.namespace, target);
state.namespace_guard().scope(caller, &operation).await?;
let router = state.intervention_router();
let attempts = router.intervenable_attempts(&request.workflow_id)?;
Ok(attempts.into_iter().map(attempt_capabilities).collect())
}
fn attempt_capabilities(
(key, capabilities): (AttemptKey, InterventionCapabilities),
) -> AttemptCapabilities {
AttemptCapabilities {
run_id: key.run_id,
activity_id: key.activity_id,
attempt: key.attempt,
capabilities,
}
}
#[cfg(test)]
mod tests {
use super::{
AttemptCapabilities, AttemptsRequest, AttemptsResponse, InterveneRequest, InterveneResponse,
};
use aion_core::{
ActivityId, InjectPriority, InterventionCapabilities, InterventionKind,
InterventionOutcome, InterventionPrimitive, RunId, WorkflowId,
};
#[test]
fn request_body_round_trips() -> Result<(), Box<dyn std::error::Error>> {
let run_id = RunId::new(uuid::Uuid::from_u128(0x11));
let request = InterveneRequest {
namespace: "tenant-a".to_owned(),
workflow_id: WorkflowId::new(uuid::Uuid::nil()),
run_id: run_id.clone(),
activity_id: ActivityId::from_sequence_position(3),
attempt: 1,
kind: InterventionKind::InjectMessage {
text: "steer".to_owned(),
priority: InjectPriority::Interrupt,
},
};
let json = serde_json::to_string(&request)?;
let decoded: InterveneRequest = serde_json::from_str(&json)?;
assert_eq!(decoded.namespace, "tenant-a");
assert_eq!(decoded.run_id, run_id);
assert_eq!(decoded.attempt, 1);
assert!(matches!(
decoded.kind,
InterventionKind::InjectMessage { .. }
));
assert!(!json.contains("issued_by"));
assert!(!json.contains("issued_at"));
Ok(())
}
#[test]
fn response_body_round_trips_each_outcome() -> Result<(), Box<dyn std::error::Error>> {
let outcomes = [
InterventionOutcome::Applied,
InterventionOutcome::capability_not_supported(InterventionPrimitive::PauseResume),
InterventionOutcome::stale_target("attempt superseded"),
];
for outcome in outcomes {
let response = InterveneResponse {
outcome: outcome.clone(),
};
let json = serde_json::to_string(&response)?;
let decoded: InterveneResponse = serde_json::from_str(&json)?;
assert_eq!(decoded.outcome, outcome);
}
Ok(())
}
#[test]
fn attempts_request_round_trips() -> Result<(), Box<dyn std::error::Error>> {
let request = AttemptsRequest {
namespace: "tenant-a".to_owned(),
workflow_id: WorkflowId::new(uuid::Uuid::nil()),
};
let json = serde_json::to_string(&request)?;
let decoded: AttemptsRequest = serde_json::from_str(&json)?;
assert_eq!(decoded.namespace, "tenant-a");
assert_eq!(decoded.workflow_id, request.workflow_id);
Ok(())
}
#[test]
fn attempts_response_round_trips_with_capabilities() -> Result<(), Box<dyn std::error::Error>> {
let response = AttemptsResponse {
attempts: vec![
AttemptCapabilities {
run_id: RunId::new(uuid::Uuid::from_u128(0x11)),
activity_id: ActivityId::from_sequence_position(3),
attempt: 1,
capabilities: InterventionCapabilities::from_primitives([
InterventionPrimitive::InjectMessage,
InterventionPrimitive::Cancel,
]),
},
AttemptCapabilities {
run_id: RunId::new(uuid::Uuid::from_u128(0x22)),
activity_id: ActivityId::from_sequence_position(4),
attempt: 2,
capabilities: InterventionCapabilities::none(),
},
],
};
let json = serde_json::to_string(&response)?;
let decoded: AttemptsResponse = serde_json::from_str(&json)?;
assert_eq!(decoded.attempts.len(), 2);
assert_eq!(decoded.attempts[0].attempt, 1);
assert_eq!(
decoded.attempts[0].run_id,
RunId::new(uuid::Uuid::from_u128(0x11))
);
assert_eq!(
decoded.attempts[1].run_id,
RunId::new(uuid::Uuid::from_u128(0x22))
);
assert!(
decoded.attempts[0]
.capabilities
.supports_primitive(InterventionPrimitive::InjectMessage)
);
assert!(decoded.attempts[1].capabilities.is_empty());
Ok(())
}
}