522 lines
21 KiB
Rust
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,
|
|
}
|