use aion_core::{ClusterCommand, WorkflowId};
use aion_proto::WireError;
use aion_store::RedriveMode;
use axum::{
Json,
extract::State,
response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::auth::HttpCaller;
use super::error::HttpWireError;
use crate::ServerState;
use crate::namespace::CallerIdentity;
use crate::worker::{RedriveRefused, redrive_dead_lettered_row};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct RedriveOutboxRowResponse {
pub dispatch_key: String,
pub workflow_id: String,
pub ordinal: u64,
pub activity_type: String,
pub status: String,
pub attempt: u32,
}
pub(crate) async fn cluster_command(
State(state): State<ServerState>,
HttpCaller(caller): HttpCaller,
Json(command): Json<ClusterCommand>,
) -> Result<Response, HttpWireError> {
deploy_gate(&caller)?;
match command {
ClusterCommand::RequestClusterSnapshot {} => {
let snapshot = crate::stream::cluster_stream::build_snapshot(&state, &caller)
.await
.map_err(|error| HttpWireError(error.to_wire_error()))?;
Ok(Json(snapshot).into_response())
}
ClusterCommand::RedriveOutboxRow {
namespace,
workflow_id,
ordinal,
} => {
let response = redrive_outbox_row(&state, &namespace, &workflow_id, ordinal).await?;
Ok(Json(response).into_response())
}
ClusterCommand::CancelWorkflow { .. }
| ClusterCommand::ReopenWorkflow { .. }
| ClusterCommand::DrainNode { .. }
| ClusterCommand::PlannedHandoff { .. }
| ClusterCommand::ChaosKillNode { .. } => Err(HttpWireError(WireError::backend_with_type(
"Unimplemented",
"this cluster command is part of the ADR-020 seam but is not implemented in Phase 1",
))),
}
}
async fn redrive_outbox_row(
state: &ServerState,
namespace: &str,
workflow_id: &str,
ordinal: u64,
) -> Result<RedriveOutboxRowResponse, HttpWireError> {
let Some(outbox_store) = state.outbox_store() else {
return Err(HttpWireError(WireError::invalid_state(
"the durable outbox is not commissioned on this server, so there are no dead-lettered \
rows to redrive",
)));
};
let workflow_id = Uuid::parse_str(workflow_id)
.map(WorkflowId::new)
.map_err(|_error| HttpWireError(WireError::invalid_input("workflow_id must be a UUID")))?;
let engine = state
.namespace_guard()
.resolver()
.engine()
.map_err(|error| HttpWireError(error.to_wire_error()))?;
let row = redrive_dead_lettered_row(
engine.store().as_ref(),
outbox_store.as_ref(),
&workflow_id,
ordinal,
RedriveMode::Eligible,
)
.await
.map_err(|refusal| redrive_wire_error(namespace, &refusal))?;
Ok(RedriveOutboxRowResponse {
dispatch_key: row.dispatch_key,
workflow_id: row.workflow_id.to_string(),
ordinal: row.ordinal,
activity_type: row.activity_type,
status: row.status.as_str().to_owned(),
attempt: row.attempt,
})
}
fn redrive_wire_error(namespace: &str, refusal: &RedriveRefused) -> HttpWireError {
let detail = format!("outbox redrive refused in namespace '{namespace}': {refusal}");
HttpWireError(match refusal {
RedriveRefused::Row(aion_store::RedriveRefusal::NoSuchRow { .. }) => {
WireError::not_found(detail)
}
RedriveRefused::Row(
aion_store::RedriveRefusal::NotDeadLettered { .. }
| aion_store::RedriveRefusal::AlreadyJudged { .. },
)
| RedriveRefused::WorkflowTerminal { .. }
| RedriveRefused::HistoryRecordsOutcome { .. } => WireError::invalid_state(detail),
RedriveRefused::Store(error) => crate::ServerError::from(error.clone()).to_wire_error(),
})
}
pub(super) fn deploy_gate(caller: &CallerIdentity) -> Result<(), HttpWireError> {
if caller.deploy_granted() {
Ok(())
} else {
Err(HttpWireError(WireError::deploy_denied(
"cluster commands require the deployment-wide deploy grant",
)))
}
}