use tonic::Status;
use aion_proto::generated;
use crate::routing::{not_owner_wire, owner_refusal};
use super::WorkflowGrpcService;
use super::status::status_from_wire_error;
impl WorkflowGrpcService {
pub(super) async fn resolve_route(
&self,
workflow_id: Option<aion_proto::ProtoWorkflowId>,
metadata: &tonic::metadata::MetadataMap,
request: crate::routing::ForwardRequest,
) -> RouteResolution {
use crate::routing::{RouteDecision, route_mutation};
let Some(cluster_store) = self.state.cluster_store() else {
return RouteResolution::Local;
};
let Some(proto) = workflow_id else {
return RouteResolution::Local;
};
let Ok(workflow_id) = aion_core::WorkflowId::try_from(proto) else {
return RouteResolution::Local;
};
let directory = self
.state
.shard_directory()
.map(|directory| directory.as_ref() as &dyn crate::routing::ShardDirectory);
match route_mutation(Some(cluster_store.as_ref()), directory, &workflow_id) {
RouteDecision::Local => RouteResolution::Local,
RouteDecision::NotOwner { shard } => RouteResolution::Reject(not_owner_status(shard)),
RouteDecision::Forward { owner, shard } => {
self.forward_or_reject(owner, shard, metadata, request)
.await
}
}
}
async fn forward_or_reject(
&self,
owner: crate::routing::NodeRef,
shard: usize,
metadata: &tonic::metadata::MetadataMap,
request: crate::routing::ForwardRequest,
) -> RouteResolution {
use crate::routing::{MAX_FORWARD_HOPS, current_hops};
if current_hops(metadata) >= MAX_FORWARD_HOPS {
return RouteResolution::Reject(not_owner_status(shard));
}
let Some(target) = owner.grpc_addr else {
return RouteResolution::Reject(not_owner_status(shard));
};
let Some(forwarder) = self.state.request_forwarder() else {
return RouteResolution::Reject(not_owner_status(shard));
};
match forwarder.forward(target, metadata.clone(), request).await {
Ok(reply) => RouteResolution::Reply(reply),
Err(status) => RouteResolution::Reject(relay_forward_failure(status, shard)),
}
}
fn start_placement(&self) -> Option<aion_core::WorkflowId> {
use crate::routing::{RemintOutcome, route_start};
match route_start(self.state.cluster_store().map(AsRef::as_ref)) {
RemintOutcome::UseId(workflow_id) => Some(workflow_id),
RemintOutcome::EngineMint => None,
}
}
pub(super) async fn resolve_start(
&self,
request: &generated::StartWorkflowRequest,
metadata: &tonic::metadata::MetadataMap,
) -> StartResolution {
use crate::routing::{SteerDecision, route_start_steered};
let routing_key = request.routing_key.as_deref().filter(|key| !key.is_empty());
let Some(routing_key) = routing_key else {
return StartResolution::Local(self.start_placement());
};
let Some(cluster_store) = self.state.cluster_store() else {
return StartResolution::Local(None);
};
let directory = self
.state
.shard_directory()
.map(|directory| directory.as_ref() as &dyn crate::routing::ShardDirectory);
match route_start_steered(cluster_store.as_ref(), directory, routing_key) {
SteerDecision::Local(workflow_id) => StartResolution::Local(Some(workflow_id)),
SteerDecision::NotOwner { shard } => StartResolution::Reject(not_owner_status(shard)),
SteerDecision::Forward { owner, shard } => {
self.forward_or_reject_start(owner, shard, metadata, request.clone())
.await
}
}
}
async fn forward_or_reject_start(
&self,
owner: crate::routing::NodeRef,
shard: usize,
metadata: &tonic::metadata::MetadataMap,
request: generated::StartWorkflowRequest,
) -> StartResolution {
use crate::routing::{ForwardReply, ForwardRequest, MAX_FORWARD_HOPS, current_hops};
if current_hops(metadata) >= MAX_FORWARD_HOPS {
return StartResolution::Reject(not_owner_status(shard));
}
let Some(target) = owner.grpc_addr else {
return StartResolution::Reject(not_owner_status(shard));
};
let Some(forwarder) = self.state.request_forwarder() else {
return StartResolution::Reject(not_owner_status(shard));
};
match forwarder
.forward(target, metadata.clone(), ForwardRequest::Start(request))
.await
{
Ok(ForwardReply::Start(reply)) => StartResolution::Reply(reply),
Ok(_) => {
StartResolution::Reject(Status::internal("forwarder returned a mismatched reply"))
}
Err(status) => StartResolution::Reject(relay_forward_failure(status, shard)),
}
}
}
fn relay_forward_failure(status: Status, shard: usize) -> Status {
if owner_refusal(&status).is_some() {
return status;
}
tracing::warn!(
shard,
grpc_code = %status.code(),
grpc_message = status.message(),
"forwarded request never reached the shard owner; answering NotOwner so the caller \
re-resolves"
);
not_owner_status(shard)
}
pub(super) enum StartResolution {
Local(Option<aion_core::WorkflowId>),
Reply(generated::StartWorkflowResponse),
Reject(Status),
}
pub(super) enum RouteResolution {
Local,
Reply(crate::routing::ForwardReply),
Reject(Status),
}
fn not_owner_status(shard: usize) -> Status {
status_from_wire_error(not_owner_wire(shard))
}
#[cfg(test)]
mod tests {
use aion_proto::{ProtoWireError, WireError};
use prost::Message as _;
use tonic::{Code, Status};
use super::relay_forward_failure;
const SHARD: usize = 3;
fn caller_sees(status: &Status) -> Option<WireError> {
if status.details().is_empty() {
return None;
}
let proto = ProtoWireError::decode(status.details()).ok()?;
WireError::try_from(proto).ok()
}
#[test]
fn an_owners_refusal_is_relayed_verbatim() -> Result<(), Box<dyn std::error::Error>> {
let refusals = [
WireError::not_found("workflow 7 has no recorded history"),
WireError::invalid_input("display_name must not be blank"),
WireError::namespace_denied("tenant-b is not visible to this caller"),
WireError::invalid_state("run has been superseded by a later run"),
];
for refusal in refusals {
let owner = super::status_from_wire_error(refusal.clone());
let code = owner.code();
let relayed = relay_forward_failure(owner, SHARD);
assert_eq!(relayed.code(), code, "the owner's gRPC code must survive");
assert_eq!(relayed.message(), refusal.message);
let seen = caller_sees(&relayed).ok_or("the typed detail must survive the relay")?;
assert_eq!(seen.code, refusal.code);
assert_ne!(
seen.code,
aion_proto::WireErrorCode::NotOwner,
"flattening the owner's answer is the defect this pins"
);
}
Ok(())
}
#[test]
fn a_transport_failure_becomes_the_typed_not_owner() -> Result<(), Box<dyn std::error::Error>> {
let relayed = relay_forward_failure(Status::unavailable("forward dial failed"), SHARD);
assert_eq!(relayed.code(), Code::Aborted);
let seen = caller_sees(&relayed).ok_or("NotOwner must carry its typed detail")?;
assert_eq!(seen.code, aion_proto::WireErrorCode::NotOwner);
assert!(
seen.message.contains(&format!("shard {SHARD}")),
"the refusal names the shard to re-resolve: {}",
seen.message
);
Ok(())
}
}