Files
crank/crates/crank-core/src/execution.rs
T

522 lines
21 KiB
Rust

use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{AgentId, CorrelationContext, OperationId};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExecutionOrigin {
AdminDraft,
AgentSnapshot,
}
impl ExecutionOrigin {
pub fn validate_agent(self, agent_id: Option<&AgentId>) -> Result<(), ExecutionOriginError> {
match (self, agent_id) {
(Self::AdminDraft, None) | (Self::AgentSnapshot, Some(_)) => Ok(()),
(Self::AdminDraft, Some(_)) => Err(ExecutionOriginError::UnexpectedAgent),
(Self::AgentSnapshot, None) => Err(ExecutionOriginError::MissingAgent),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
pub enum ExecutionOriginError {
#[error("admin draft execution cannot carry an agent identity")]
UnexpectedAgent,
#[error("agent snapshot execution requires an agent identity")]
MissingAgent,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExecutionStage {
Authorization,
InputSchema,
InputMapping,
RequestPreparation,
Admission,
Adapter,
Upstream,
OutputMapping,
OutputSchema,
MandatoryPersistence,
Runtime,
}
impl ExecutionStage {
pub const fn as_str(self) -> &'static str {
match self {
Self::Authorization => "authorization",
Self::InputSchema => "input_schema",
Self::InputMapping => "input_mapping",
Self::RequestPreparation => "request_preparation",
Self::Admission => "admission",
Self::Adapter => "adapter",
Self::Upstream => "upstream",
Self::OutputMapping => "output_mapping",
Self::OutputSchema => "output_schema",
Self::MandatoryPersistence => "mandatory_persistence",
Self::Runtime => "runtime",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Retryability {
Never,
Safe,
AfterDelay,
ManualReconcile,
RequiresConfirmation,
}
impl Retryability {
pub const fn as_str(self) -> &'static str {
match self {
Self::Never => "never",
Self::Safe => "safe",
Self::AfterDelay => "after_delay",
Self::ManualReconcile => "manual_reconcile",
Self::RequiresConfirmation => "requires_confirmation",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OutcomeCertainty {
Certain,
OutcomeUnknown,
}
impl OutcomeCertainty {
pub const fn as_str(self) -> &'static str {
match self {
Self::Certain => "certain",
Self::OutcomeUnknown => "outcome_unknown",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExecutionErrorCode {
AuthorizationDenied,
AuthProfileNotFound,
SecretNotFound,
SecretInvalid,
InputSchemaInvalid,
InputMappingInvalid,
PreparedRequestInvalid,
ExecutionOverloaded,
SafetyStoreUnavailable,
ProtocolUnsupported,
ExecutionModeUnsupported,
AdapterConfigurationInvalid,
OutboundTargetRejected,
UpstreamAuthError,
UpstreamNotFound,
UpstreamRateLimited,
UpstreamServerError,
UpstreamStatusError,
UpstreamTimeout,
UpstreamTransportError,
UpstreamRequestTooLarge,
UpstreamResponseTooLarge,
OutputMappingInvalid,
OutputSchemaInvalid,
PersistenceUnavailable,
RuntimeInternal,
ConfirmationRequired,
ConfirmationInvalid,
IdempotencyInProgress,
IdempotencyConflict,
IdempotencyOutcomeUnknown,
}
impl ExecutionErrorCode {
pub const ALL: [Self; 31] = [
Self::AuthorizationDenied,
Self::AuthProfileNotFound,
Self::SecretNotFound,
Self::SecretInvalid,
Self::InputSchemaInvalid,
Self::InputMappingInvalid,
Self::PreparedRequestInvalid,
Self::ExecutionOverloaded,
Self::SafetyStoreUnavailable,
Self::ProtocolUnsupported,
Self::ExecutionModeUnsupported,
Self::AdapterConfigurationInvalid,
Self::OutboundTargetRejected,
Self::UpstreamAuthError,
Self::UpstreamNotFound,
Self::UpstreamRateLimited,
Self::UpstreamServerError,
Self::UpstreamStatusError,
Self::UpstreamTimeout,
Self::UpstreamTransportError,
Self::UpstreamRequestTooLarge,
Self::UpstreamResponseTooLarge,
Self::OutputMappingInvalid,
Self::OutputSchemaInvalid,
Self::PersistenceUnavailable,
Self::RuntimeInternal,
Self::ConfirmationRequired,
Self::ConfirmationInvalid,
Self::IdempotencyInProgress,
Self::IdempotencyConflict,
Self::IdempotencyOutcomeUnknown,
];
pub const fn as_str(self) -> &'static str {
match self {
Self::AuthorizationDenied => "authorization_denied",
Self::AuthProfileNotFound => "auth_profile_not_found",
Self::SecretNotFound => "secret_not_found",
Self::SecretInvalid => "secret_invalid",
Self::InputSchemaInvalid => "input_schema_invalid",
Self::InputMappingInvalid => "input_mapping_invalid",
Self::PreparedRequestInvalid => "prepared_request_invalid",
Self::ExecutionOverloaded => "execution_overloaded",
Self::SafetyStoreUnavailable => "safety_store_unavailable",
Self::ProtocolUnsupported => "protocol_unsupported",
Self::ExecutionModeUnsupported => "execution_mode_unsupported",
Self::AdapterConfigurationInvalid => "adapter_configuration_invalid",
Self::OutboundTargetRejected => "outbound_target_rejected",
Self::UpstreamAuthError => "upstream_auth_error",
Self::UpstreamNotFound => "upstream_not_found",
Self::UpstreamRateLimited => "upstream_rate_limited",
Self::UpstreamServerError => "upstream_server_error",
Self::UpstreamStatusError => "upstream_status_error",
Self::UpstreamTimeout => "upstream_timeout",
Self::UpstreamTransportError => "upstream_transport_error",
Self::UpstreamRequestTooLarge => "upstream_request_too_large",
Self::UpstreamResponseTooLarge => "upstream_response_too_large",
Self::OutputMappingInvalid => "output_mapping_invalid",
Self::OutputSchemaInvalid => "output_schema_invalid",
Self::PersistenceUnavailable => "persistence_unavailable",
Self::RuntimeInternal => "runtime_internal",
Self::ConfirmationRequired => "confirmation_required",
Self::ConfirmationInvalid => "confirmation_invalid",
Self::IdempotencyInProgress => "idempotency_in_progress",
Self::IdempotencyConflict => "idempotency_conflict",
Self::IdempotencyOutcomeUnknown => "idempotency_outcome_unknown",
}
}
pub const fn stage(self) -> ExecutionStage {
match self {
Self::AuthorizationDenied
| Self::AuthProfileNotFound
| Self::SecretNotFound
| Self::SecretInvalid => ExecutionStage::Authorization,
Self::InputSchemaInvalid => ExecutionStage::InputSchema,
Self::InputMappingInvalid => ExecutionStage::InputMapping,
Self::PreparedRequestInvalid => ExecutionStage::RequestPreparation,
Self::ExecutionOverloaded
| Self::SafetyStoreUnavailable
| Self::ConfirmationRequired
| Self::ConfirmationInvalid
| Self::IdempotencyInProgress
| Self::IdempotencyConflict
| Self::IdempotencyOutcomeUnknown => ExecutionStage::Admission,
Self::ProtocolUnsupported
| Self::ExecutionModeUnsupported
| Self::AdapterConfigurationInvalid
| Self::OutboundTargetRejected => ExecutionStage::Adapter,
Self::UpstreamRequestTooLarge => ExecutionStage::RequestPreparation,
Self::UpstreamAuthError
| Self::UpstreamNotFound
| Self::UpstreamRateLimited
| Self::UpstreamServerError
| Self::UpstreamStatusError
| Self::UpstreamTimeout
| Self::UpstreamTransportError
| Self::UpstreamResponseTooLarge => ExecutionStage::Upstream,
Self::OutputMappingInvalid => ExecutionStage::OutputMapping,
Self::OutputSchemaInvalid => ExecutionStage::OutputSchema,
Self::PersistenceUnavailable => ExecutionStage::MandatoryPersistence,
Self::RuntimeInternal => ExecutionStage::Runtime,
}
}
pub const fn retryability(self) -> Retryability {
match self {
Self::ExecutionOverloaded
| Self::SafetyStoreUnavailable
| Self::UpstreamRateLimited
| Self::UpstreamServerError
| Self::UpstreamTimeout
| Self::UpstreamTransportError
| Self::PersistenceUnavailable
| Self::IdempotencyInProgress => Retryability::AfterDelay,
Self::ConfirmationRequired => Retryability::RequiresConfirmation,
Self::IdempotencyOutcomeUnknown => Retryability::ManualReconcile,
_ => Retryability::Never,
}
}
pub const fn outcome_certainty(self) -> OutcomeCertainty {
match self {
Self::IdempotencyOutcomeUnknown => OutcomeCertainty::OutcomeUnknown,
_ => OutcomeCertainty::Certain,
}
}
pub const fn message(self, locale: ExecutionLocale) -> &'static str {
match locale {
ExecutionLocale::Ru => self.message_ru(),
ExecutionLocale::En => self.message_en(),
}
}
const fn message_ru(self) -> &'static str {
match self {
Self::AuthorizationDenied => "Выполнение операции запрещено.",
Self::AuthProfileNotFound => "Профиль авторизации не найден.",
Self::SecretNotFound => "Секрет авторизации не найден.",
Self::SecretInvalid => "Секрет авторизации имеет неподходящий формат.",
Self::InputSchemaInvalid => "Входные параметры не прошли проверку схемы.",
Self::InputMappingInvalid => "Не удалось сопоставить входные параметры.",
Self::PreparedRequestInvalid => "Не удалось подготовить корректный API-запрос.",
Self::ExecutionOverloaded => "Сервис временно перегружен.",
Self::SafetyStoreUnavailable => "Обязательное хранилище безопасности недоступно.",
Self::ProtocolUnsupported => "Протокол операции не поддерживается.",
Self::ExecutionModeUnsupported => "Режим выполнения не поддерживается.",
Self::AdapterConfigurationInvalid => "Конфигурация адаптера некорректна.",
Self::OutboundTargetRejected => "Целевой адрес отклонён политикой безопасности.",
Self::UpstreamAuthError => "Внешний API отклонил авторизацию.",
Self::UpstreamNotFound => "Ресурс внешнего API не найден.",
Self::UpstreamRateLimited => "Внешний API ограничил частоту запросов.",
Self::UpstreamServerError => "Внешний API временно недоступен.",
Self::UpstreamStatusError => "Внешний API вернул ошибочный статус.",
Self::UpstreamTimeout => "Истекло время ожидания внешнего API.",
Self::UpstreamTransportError => "Не удалось подключиться к внешнему API.",
Self::UpstreamRequestTooLarge => "Запрос во внешний API превышает лимит.",
Self::UpstreamResponseTooLarge => "Ответ внешнего API превышает лимит.",
Self::OutputMappingInvalid => "Не удалось сопоставить ответ внешнего API.",
Self::OutputSchemaInvalid => "Ответ не прошёл проверку схемы.",
Self::PersistenceUnavailable => "Обязательное сохранение результата недоступно.",
Self::RuntimeInternal => "Внутренняя ошибка выполнения.",
Self::ConfirmationRequired => "Операция требует подтверждения.",
Self::ConfirmationInvalid => "Подтверждение недействительно или истекло.",
Self::IdempotencyInProgress => "Операция с этим ключом уже выполняется.",
Self::IdempotencyConflict => "Ключ идемпотентности использован с другими параметрами.",
Self::IdempotencyOutcomeUnknown => {
"Результат предыдущего выполнения неизвестен; автоматический повтор запрещён."
}
}
}
const fn message_en(self) -> &'static str {
match self {
Self::AuthorizationDenied => "Operation execution is denied.",
Self::AuthProfileNotFound => "Authorization profile was not found.",
Self::SecretNotFound => "Authorization secret was not found.",
Self::SecretInvalid => "Authorization secret has an invalid format.",
Self::InputSchemaInvalid => "Input does not satisfy the operation schema.",
Self::InputMappingInvalid => "Input parameters could not be mapped.",
Self::PreparedRequestInvalid => "A valid upstream request could not be prepared.",
Self::ExecutionOverloaded => "The service is temporarily overloaded.",
Self::SafetyStoreUnavailable => "A mandatory safety store is unavailable.",
Self::ProtocolUnsupported => "The operation protocol is unsupported.",
Self::ExecutionModeUnsupported => "The execution mode is unsupported.",
Self::AdapterConfigurationInvalid => "The adapter configuration is invalid.",
Self::OutboundTargetRejected => "The target was rejected by the safety policy.",
Self::UpstreamAuthError => "The upstream API rejected authorization.",
Self::UpstreamNotFound => "The upstream resource was not found.",
Self::UpstreamRateLimited => "The upstream API rate-limited the request.",
Self::UpstreamServerError => "The upstream API is temporarily unavailable.",
Self::UpstreamStatusError => "The upstream API returned an error status.",
Self::UpstreamTimeout => "The upstream API timed out.",
Self::UpstreamTransportError => "The upstream API could not be reached.",
Self::UpstreamRequestTooLarge => "The upstream request exceeded its limit.",
Self::UpstreamResponseTooLarge => "The upstream response exceeded its limit.",
Self::OutputMappingInvalid => "The upstream response could not be mapped.",
Self::OutputSchemaInvalid => "The output does not satisfy the operation schema.",
Self::PersistenceUnavailable => "Mandatory result persistence is unavailable.",
Self::RuntimeInternal => "Internal execution failure.",
Self::ConfirmationRequired => "The operation requires confirmation.",
Self::ConfirmationInvalid => "The confirmation is invalid or expired.",
Self::IdempotencyInProgress => "The operation is already running for this key.",
Self::IdempotencyConflict => "The idempotency key was used with different input.",
Self::IdempotencyOutcomeUnknown => {
"The previous outcome is unknown; automatic retry is unsafe."
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ExecutionLocale {
Ru,
En,
}
#[derive(Clone, PartialEq, Eq)]
pub struct ConfirmationChallenge {
token: String,
expires_in_ms: u64,
}
impl ConfirmationChallenge {
fn try_new(
token: impl Into<String>,
expires_in_ms: u64,
) -> Result<Self, ExecutionFailureBuildError> {
let token = token.into();
if token.is_empty() || token.len() > 4_096 || token.chars().any(char::is_control) {
return Err(ExecutionFailureBuildError::InvalidConfirmation);
}
if !(1..=3_600_000).contains(&expires_in_ms) {
return Err(ExecutionFailureBuildError::InvalidConfirmation);
}
Ok(Self {
token,
expires_in_ms,
})
}
pub fn token(&self) -> &str {
&self.token
}
pub fn expires_in_ms(&self) -> u64 {
self.expires_in_ms
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
pub enum ExecutionFailureBuildError {
#[error("failure metadata is incompatible with the execution error code")]
IncompatibleMetadata,
#[error("confirmation metadata is invalid or exceeds its bound")]
InvalidConfirmation,
#[error("retry metadata is invalid or exceeds its bound")]
InvalidRetryAfter,
}
impl std::fmt::Debug for ConfirmationChallenge {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("ConfirmationChallenge")
.field("token", &"[REDACTED]")
.field("expires_in_ms", &self.expires_in_ms)
.finish()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExecutionFailure {
code: ExecutionErrorCode,
retryability: Retryability,
outcome_certainty: OutcomeCertainty,
correlation: CorrelationContext,
upstream_status: Option<u16>,
retry_after_ms: Option<u64>,
confirmation: Option<ConfirmationChallenge>,
}
impl ExecutionFailure {
pub fn new(code: ExecutionErrorCode, correlation: CorrelationContext) -> Self {
Self {
code,
retryability: code.retryability(),
outcome_certainty: code.outcome_certainty(),
correlation,
upstream_status: None,
retry_after_ms: None,
confirmation: None,
}
}
pub fn error_code(&self) -> ExecutionErrorCode {
self.code
}
pub fn stage(&self) -> ExecutionStage {
self.code.stage()
}
pub fn retryability(&self) -> Retryability {
self.retryability
}
pub fn outcome_certainty(&self) -> OutcomeCertainty {
self.outcome_certainty
}
pub fn correlation(&self) -> &CorrelationContext {
&self.correlation
}
pub fn upstream_status(&self) -> Option<u16> {
self.upstream_status
}
pub fn retry_after_ms(&self) -> Option<u64> {
self.retry_after_ms
}
pub fn confirmation(&self) -> Option<&ConfirmationChallenge> {
self.confirmation.as_ref()
}
pub fn with_upstream_status(mut self, status: u16) -> Self {
self.upstream_status = (100..=599).contains(&status).then_some(status);
self
}
pub fn try_with_retry_after_ms(
mut self,
retry_after_ms: u64,
) -> Result<Self, ExecutionFailureBuildError> {
if self.retryability != Retryability::AfterDelay {
return Err(ExecutionFailureBuildError::IncompatibleMetadata);
}
if !(1..=3_600_000).contains(&retry_after_ms) {
return Err(ExecutionFailureBuildError::InvalidRetryAfter);
}
self.retry_after_ms = Some(retry_after_ms);
Ok(self)
}
pub fn try_with_confirmation(
mut self,
token: impl Into<String>,
expires_in_ms: u64,
) -> Result<Self, ExecutionFailureBuildError> {
if self.code != ExecutionErrorCode::ConfirmationRequired {
return Err(ExecutionFailureBuildError::IncompatibleMetadata);
}
self.confirmation = Some(ConfirmationChallenge::try_new(token, expires_in_ms)?);
Ok(self)
}
pub fn with_dispatch_uncertainty(mut self) -> Self {
if matches!(
self.code,
ExecutionErrorCode::UpstreamTimeout
| ExecutionErrorCode::UpstreamTransportError
| ExecutionErrorCode::IdempotencyOutcomeUnknown
| ExecutionErrorCode::PersistenceUnavailable
) {
self.retryability = Retryability::ManualReconcile;
self.outcome_certainty = OutcomeCertainty::OutcomeUnknown;
}
self
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ExecutionSuccess {
pub operation_id: OperationId,
pub operation_version: u32,
pub origin: ExecutionOrigin,
pub correlation: CorrelationContext,
pub request_preview: Value,
pub output: Value,
}