use base64::Engine as _;
use connectrpc::{ConnectError, ErrorCode, ErrorDetail};
use polyc_proto::proto::polychrome::state::v1 as pb;
use polyc_state::{
command::{FencingToken, ObservedState, Precondition},
digest::ContentDigest,
error::{AmbiguityReason, BoundKind, OutageReach, StateError},
id::{CommandId, OperationFamily, PartitionId},
revision::{CommitRoot, JournalPosition, Revision},
};
use crate::wire::{Kernel, duration_from_nanos, nanos_from_duration};
pub const STATE_ERROR_DETAIL_TYPE: &str = "polychrome.state.v1.StateErrorDetail";
#[must_use]
pub const fn code_for(error: &StateError) -> ErrorCode {
match error {
StateError::RevisionConflict { .. }
| StateError::IncarnationConflict { .. }
| StateError::StaleFence { .. } => ErrorCode::FailedPrecondition,
StateError::DuplicateCommand { .. } | StateError::PartitionHeld { .. } => {
ErrorCode::Aborted
}
StateError::DigestConflict { .. } => ErrorCode::AlreadyExists,
StateError::DeadlineExpired { .. } => ErrorCode::DeadlineExceeded,
StateError::BoundsExceeded { .. } => ErrorCode::ResourceExhausted,
StateError::Cancelled { .. } => ErrorCode::Canceled,
StateError::Unavailable { .. } | StateError::AmbiguousOutcome { .. } => {
ErrorCode::Unavailable
}
StateError::Denied { .. } => ErrorCode::PermissionDenied,
StateError::CompactedRange { .. } | StateError::RetiredRevision { .. } => {
ErrorCode::OutOfRange
}
StateError::Malformed { .. } => ErrorCode::InvalidArgument,
}
}
impl From<Kernel<&StateError>> for pb::StateErrorDetail {
#[allow(clippy::too_many_lines)]
fn from(value: Kernel<&StateError>) -> Self {
use pb::__buffa::oneof::state_error_detail::Outcome;
let unknown = buffa::UnknownFields::default;
let outcome = match value.0 {
StateError::RevisionConflict {
command_id,
expected,
observed,
} => Outcome::from(pb::RevisionConflictDetail {
command_id: command_id.as_str().to_owned(),
expected: buffa::MessageField::some(pb::Precondition::from(Kernel(*expected))),
observed: buffa::MessageField::some(pb::ObservedState::from(Kernel(*observed))),
__buffa_unknown_fields: unknown(),
}),
StateError::IncarnationConflict {
command_id,
partition,
expected,
observed,
} => Outcome::from(pb::IncarnationConflictDetail {
command_id: command_id.as_str().to_owned(),
partition: partition.as_str().to_owned(),
expected_root: expected.as_bytes().to_vec(),
observed_root: observed.map(|root| root.as_bytes().to_vec()),
__buffa_unknown_fields: unknown(),
}),
StateError::StaleFence {
command_id,
presented,
current,
} => Outcome::from(pb::StaleFenceDetail {
command_id: command_id.as_str().to_owned(),
presented: presented.get(),
current: current.get(),
__buffa_unknown_fields: unknown(),
}),
StateError::DuplicateCommand { command_id } => {
Outcome::from(pb::DuplicateCommandDetail {
command_id: command_id.as_str().to_owned(),
__buffa_unknown_fields: unknown(),
})
}
StateError::PartitionHeld {
partition,
command_id,
} => Outcome::from(pb::PartitionHeldDetail {
partition: partition.as_str().to_owned(),
command_id: command_id.as_str().to_owned(),
__buffa_unknown_fields: unknown(),
}),
StateError::DigestConflict {
command_id,
recorded,
presented,
} => Outcome::from(pb::DigestConflictDetail {
command_id: command_id.as_str().to_owned(),
recorded: recorded.as_bytes().to_vec(),
presented: presented.as_bytes().to_vec(),
__buffa_unknown_fields: unknown(),
}),
StateError::DeadlineExpired { family, overrun } => {
Outcome::from(pb::DeadlineExpiredDetail {
family: family.as_str().to_owned(),
overrun_nanos: nanos_from_duration(*overrun),
__buffa_unknown_fields: unknown(),
})
}
StateError::BoundsExceeded {
bound,
limit,
requested,
} => Outcome::from(pb::BoundsExceededDetail {
bound: pb::BoundKind::from(Kernel(*bound)).into(),
limit: *limit,
requested: *requested,
__buffa_unknown_fields: unknown(),
}),
StateError::Unavailable { family, reach } => Outcome::from(pb::UnavailableDetail {
family: family.as_str().to_owned(),
reach: pb::OutageReach::from(Kernel(*reach)).into(),
__buffa_unknown_fields: unknown(),
}),
StateError::Cancelled { family } => Outcome::from(pb::CancelledDetail {
family: family.as_str().to_owned(),
__buffa_unknown_fields: unknown(),
}),
StateError::AmbiguousOutcome { command_id, reason } => {
Outcome::from(pb::AmbiguousOutcomeDetail {
command_id: command_id.as_str().to_owned(),
reason: pb::AmbiguityReason::from(Kernel(*reason)).into(),
__buffa_unknown_fields: unknown(),
})
}
StateError::Denied { family } => Outcome::from(pb::DeniedDetail {
family: family.as_str().to_owned(),
__buffa_unknown_fields: unknown(),
}),
StateError::CompactedRange {
partition,
requested,
earliest,
} => Outcome::from(pb::CompactedRangeDetail {
partition: partition.as_str().to_owned(),
requested: requested.get(),
earliest: earliest.get(),
__buffa_unknown_fields: unknown(),
}),
StateError::RetiredRevision {
requested,
earliest,
} => Outcome::from(pb::RetiredRevisionDetail {
requested: requested.get(),
earliest: earliest.get(),
__buffa_unknown_fields: unknown(),
}),
StateError::Malformed { field, reason } => Outcome::from(pb::MalformedDetail {
field: field.clone(),
reason: reason.clone(),
__buffa_unknown_fields: unknown(),
}),
};
Self {
outcome: Some(outcome),
__buffa_unknown_fields: unknown(),
}
}
}
impl From<Kernel<BoundKind>> for pb::BoundKind {
fn from(value: Kernel<BoundKind>) -> Self {
match value.0 {
BoundKind::PayloadBytes => Self::BOUND_KIND_PAYLOAD_BYTES,
BoundKind::CommandRecords => Self::BOUND_KIND_COMMAND_RECORDS,
BoundKind::PageRecords => Self::BOUND_KIND_PAGE_RECORDS,
BoundKind::ChunkRecords => Self::BOUND_KIND_CHUNK_RECORDS,
}
}
}
impl From<Kernel<OutageReach>> for pb::OutageReach {
fn from(value: Kernel<OutageReach>) -> Self {
match value.0 {
OutageReach::NeverDispatched => Self::OUTAGE_REACH_NEVER_DISPATCHED,
OutageReach::PossiblyApplied => Self::OUTAGE_REACH_POSSIBLY_APPLIED,
}
}
}
impl From<Kernel<AmbiguityReason>> for pb::AmbiguityReason {
fn from(value: Kernel<AmbiguityReason>) -> Self {
match value.0 {
AmbiguityReason::ResponseLost => Self::AMBIGUITY_REASON_RESPONSE_LOST,
AmbiguityReason::CommitUnknown => Self::AMBIGUITY_REASON_COMMIT_UNKNOWN,
AmbiguityReason::EffectDeliveryUnknown => {
Self::AMBIGUITY_REASON_EFFECT_DELIVERY_UNKNOWN
}
}
}
}
fn bound_kind(value: buffa::EnumValue<pb::BoundKind>) -> BoundKind {
match value.as_known() {
Some(pb::BoundKind::BOUND_KIND_COMMAND_RECORDS) => BoundKind::CommandRecords,
Some(pb::BoundKind::BOUND_KIND_PAGE_RECORDS) => BoundKind::PageRecords,
Some(pb::BoundKind::BOUND_KIND_CHUNK_RECORDS) => BoundKind::ChunkRecords,
Some(pb::BoundKind::BOUND_KIND_PAYLOAD_BYTES | pb::BoundKind::BOUND_KIND_UNSPECIFIED)
| None => BoundKind::PayloadBytes,
}
}
fn outage_reach(value: buffa::EnumValue<pb::OutageReach>) -> OutageReach {
match value.as_known() {
Some(pb::OutageReach::OUTAGE_REACH_NEVER_DISPATCHED) => OutageReach::NeverDispatched,
Some(
pb::OutageReach::OUTAGE_REACH_POSSIBLY_APPLIED
| pb::OutageReach::OUTAGE_REACH_UNSPECIFIED,
)
| None => OutageReach::PossiblyApplied,
}
}
fn ambiguity_reason(value: buffa::EnumValue<pb::AmbiguityReason>) -> AmbiguityReason {
match value.as_known() {
Some(pb::AmbiguityReason::AMBIGUITY_REASON_RESPONSE_LOST) => AmbiguityReason::ResponseLost,
Some(pb::AmbiguityReason::AMBIGUITY_REASON_EFFECT_DELIVERY_UNKNOWN) => {
AmbiguityReason::EffectDeliveryUnknown
}
Some(
pb::AmbiguityReason::AMBIGUITY_REASON_COMMIT_UNKNOWN
| pb::AmbiguityReason::AMBIGUITY_REASON_UNSPECIFIED,
)
| None => AmbiguityReason::CommitUnknown,
}
}
fn digest_or_zero(bytes: &[u8]) -> ContentDigest {
<[u8; ContentDigest::LEN]>::try_from(bytes).map_or_else(
|_| ContentDigest::from_bytes([0; ContentDigest::LEN]),
ContentDigest::from_bytes,
)
}
impl TryFrom<pb::StateErrorDetail> for Kernel<StateError> {
type Error = pb::StateErrorDetail;
fn try_from(value: pb::StateErrorDetail) -> Result<Self, Self::Error> {
use pb::__buffa::oneof::state_error_detail::Outcome;
let Some(outcome) = value.outcome.clone() else {
return Err(value);
};
Ok(Self(match outcome {
Outcome::RevisionConflict(detail) => StateError::RevisionConflict {
command_id: CommandId::new(detail.command_id),
expected: detail
.expected
.into_option()
.and_then(|expected| Kernel::<Precondition>::try_from(expected).ok())
.map_or(Precondition::Unconditional, Kernel::into_inner),
observed: detail
.observed
.into_option()
.and_then(|observed| Kernel::<ObservedState>::try_from(observed).ok())
.map_or(ObservedState::Absent, Kernel::into_inner),
},
Outcome::IncarnationConflict(detail) => StateError::IncarnationConflict {
command_id: CommandId::new(detail.command_id),
partition: PartitionId::new(detail.partition),
expected: CommitRoot::from_bytes(
<[u8; CommitRoot::LEN]>::try_from(detail.expected_root.as_slice())
.unwrap_or([0; CommitRoot::LEN]),
),
observed: detail.observed_root.map(|bytes| {
CommitRoot::from_bytes(
<[u8; CommitRoot::LEN]>::try_from(bytes.as_slice())
.unwrap_or([0; CommitRoot::LEN]),
)
}),
},
Outcome::StaleFence(detail) => StateError::StaleFence {
command_id: CommandId::new(detail.command_id),
presented: FencingToken::new(detail.presented),
current: FencingToken::new(detail.current),
},
Outcome::DuplicateCommand(detail) => StateError::DuplicateCommand {
command_id: CommandId::new(detail.command_id),
},
Outcome::PartitionHeld(detail) => StateError::PartitionHeld {
partition: PartitionId::new(detail.partition),
command_id: CommandId::new(detail.command_id),
},
Outcome::DigestConflict(detail) => StateError::DigestConflict {
command_id: CommandId::new(detail.command_id),
recorded: digest_or_zero(&detail.recorded),
presented: digest_or_zero(&detail.presented),
},
Outcome::DeadlineExpired(detail) => StateError::DeadlineExpired {
family: OperationFamily::new(detail.family),
overrun: duration_from_nanos(detail.overrun_nanos),
},
Outcome::BoundsExceeded(detail) => StateError::BoundsExceeded {
bound: bound_kind(detail.bound),
limit: detail.limit,
requested: detail.requested,
},
Outcome::Unavailable(detail) => StateError::Unavailable {
family: OperationFamily::new(detail.family),
reach: outage_reach(detail.reach),
},
Outcome::Cancelled(detail) => StateError::Cancelled {
family: OperationFamily::new(detail.family),
},
Outcome::AmbiguousOutcome(detail) => StateError::AmbiguousOutcome {
command_id: CommandId::new(detail.command_id),
reason: ambiguity_reason(detail.reason),
},
Outcome::Denied(detail) => StateError::Denied {
family: OperationFamily::new(detail.family),
},
Outcome::CompactedRange(detail) => StateError::CompactedRange {
partition: PartitionId::new(detail.partition),
requested: JournalPosition::new(detail.requested),
earliest: JournalPosition::new(detail.earliest),
},
Outcome::RetiredRevision(detail) => StateError::RetiredRevision {
requested: Revision::new(detail.requested),
earliest: Revision::new(detail.earliest),
},
Outcome::Malformed(detail) => StateError::Malformed {
field: detail.field,
reason: detail.reason,
},
}))
}
}
#[must_use]
pub fn to_connect_error(error: &StateError) -> ConnectError {
ConnectError::new(code_for(error), error.to_string()).with_detail(ErrorDetail::from_message(
STATE_ERROR_DETAIL_TYPE,
&pb::StateErrorDetail::from(Kernel(error)),
))
}
#[must_use]
pub fn from_connect_error(error: &ConnectError, fallback: &TransportFallback) -> StateError {
for detail in &error.details {
if detail.type_url != STATE_ERROR_DETAIL_TYPE {
continue;
}
let Some(encoded) = detail.value.as_deref() else {
continue;
};
let Ok(bytes) = decode_detail(encoded) else {
continue;
};
let Ok(message) = <pb::StateErrorDetail as buffa::Message>::decode_from_slice(&bytes)
else {
continue;
};
if let Ok(typed) = Kernel::<StateError>::try_from(message) {
return typed.into_inner();
}
}
fallback.for_code(error.code)
}
pub(crate) fn decode_detail(encoded: &str) -> Result<Vec<u8>, base64::DecodeError> {
base64::engine::general_purpose::STANDARD_NO_PAD
.decode(encoded)
.or_else(|_| base64::engine::general_purpose::STANDARD.decode(encoded))
}
#[derive(Debug, Clone)]
pub struct TransportFallback {
family: OperationFamily,
wire_bound_bytes: u64,
attempted_bytes: u64,
}
impl TransportFallback {
#[must_use]
pub const fn new(family: OperationFamily, wire_bound_bytes: u64, attempted_bytes: u64) -> Self {
Self {
family,
wire_bound_bytes,
attempted_bytes,
}
}
#[must_use]
pub fn for_code(&self, code: ErrorCode) -> StateError {
match code {
ErrorCode::ResourceExhausted => StateError::BoundsExceeded {
bound: BoundKind::PayloadBytes,
limit: self.wire_bound_bytes,
requested: self
.attempted_bytes
.max(self.wire_bound_bytes.saturating_add(1)),
},
ErrorCode::DeadlineExceeded => StateError::DeadlineExpired {
family: self.family.clone(),
overrun: std::time::Duration::ZERO,
},
ErrorCode::Canceled => StateError::Cancelled {
family: self.family.clone(),
},
ErrorCode::PermissionDenied | ErrorCode::Unauthenticated => StateError::Denied {
family: self.family.clone(),
},
ErrorCode::InvalidArgument => StateError::Malformed {
field: "request".to_owned(),
reason: "the listener could not interpret the request".to_owned(),
},
ErrorCode::OutOfRange => StateError::CompactedRange {
partition: PartitionId::new(""),
requested: JournalPosition::ORIGIN,
earliest: JournalPosition::ORIGIN,
},
ErrorCode::Unavailable => StateError::Unavailable {
family: self.family.clone(),
reach: OutageReach::PossiblyApplied,
},
_ => StateError::AmbiguousOutcome {
command_id: CommandId::new(""),
reason: AmbiguityReason::CommitUnknown,
},
}
}
#[must_use]
pub fn never_dispatched(&self) -> StateError {
StateError::Unavailable {
family: self.family.clone(),
reach: OutageReach::NeverDispatched,
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs, clippy::unwrap_used)]
use super::*;
use polyc_state::{
conformance::family,
error::RetryClass,
revision::{JournalPosition, Revision},
};
use std::time::Duration;
fn fallback() -> TransportFallback {
TransportFallback::new(OperationFamily::new(family::FAMILY), 64, 128)
}
fn every_variant() -> Vec<StateError> {
vec![
StateError::RevisionConflict {
command_id: CommandId::new("cmd-1"),
expected: Precondition::Revision(Revision::new(2)),
observed: ObservedState::JournalHead(JournalPosition::new(5)),
},
StateError::StaleFence {
command_id: CommandId::new("cmd-2"),
presented: FencingToken::new(1),
current: FencingToken::new(2),
},
StateError::DuplicateCommand {
command_id: CommandId::new("cmd-3"),
},
StateError::DigestConflict {
command_id: CommandId::new("cmd-4"),
recorded: ContentDigest::from_bytes([1; ContentDigest::LEN]),
presented: ContentDigest::from_bytes([2; ContentDigest::LEN]),
},
StateError::DeadlineExpired {
family: OperationFamily::new(family::FAMILY),
overrun: Duration::from_millis(7),
},
StateError::BoundsExceeded {
bound: BoundKind::ChunkRecords,
limit: 4,
requested: 9,
},
StateError::Cancelled {
family: OperationFamily::new(family::FAMILY),
},
StateError::Unavailable {
family: OperationFamily::new(family::FAMILY),
reach: OutageReach::NeverDispatched,
},
StateError::Unavailable {
family: OperationFamily::new(family::FAMILY),
reach: OutageReach::PossiblyApplied,
},
StateError::AmbiguousOutcome {
command_id: CommandId::new("cmd-5"),
reason: AmbiguityReason::ResponseLost,
},
StateError::Denied {
family: OperationFamily::new(family::FAMILY),
},
StateError::Malformed {
field: "protocol_version".to_owned(),
reason: "this module speaks v1".to_owned(),
},
StateError::CompactedRange {
partition: PartitionId::new("conv-1"),
requested: JournalPosition::new(3),
earliest: JournalPosition::new(9),
},
StateError::RetiredRevision {
requested: Revision::new(3),
earliest: Revision::new(9),
},
]
}
#[test]
fn every_variant_round_trips_through_a_connect_error() {
for error in every_variant() {
let wire = to_connect_error(&error);
let back = from_connect_error(&wire, &fallback());
assert_eq!(back, error, "variant did not survive the wire");
assert_eq!(back.retry_class(), error.retry_class());
}
}
#[test]
fn a_bare_transport_code_falls_back_to_a_typed_outcome() {
let refused = from_connect_error(
&ConnectError::resource_exhausted("message size 128 exceeds limit 64"),
&fallback(),
);
assert_eq!(
refused,
StateError::BoundsExceeded {
bound: BoundKind::PayloadBytes,
limit: 64,
requested: 128,
}
);
assert_eq!(refused.retry_class(), RetryClass::Terminal);
let expired = from_connect_error(&ConnectError::deadline_exceeded("gone"), &fallback());
assert!(matches!(expired, StateError::DeadlineExpired { .. }));
assert!(expired.is_ambiguous());
let withdrawn = from_connect_error(&ConnectError::canceled("gone"), &fallback());
assert!(matches!(withdrawn, StateError::Cancelled { .. }));
assert!(withdrawn.is_ambiguous());
let draining = from_connect_error(&ConnectError::unavailable("draining"), &fallback());
assert_eq!(
draining,
StateError::Unavailable {
family: OperationFamily::new(family::FAMILY),
reach: OutageReach::PossiblyApplied,
}
);
assert!(draining.is_outage(), "a draining listener is an outage");
assert!(
draining.is_ambiguous(),
"an unreachable listener settles nothing"
);
let unexpected = from_connect_error(
&ConnectError::new(ErrorCode::FailedPrecondition, "no detail"),
&fallback(),
);
assert!(!unexpected.is_outage());
assert!(unexpected.is_ambiguous());
}
#[test]
fn an_outage_crosses_the_wire_as_an_outage_and_keeps_its_reach() {
for reach in [OutageReach::NeverDispatched, OutageReach::PossiblyApplied] {
let refused = StateError::Unavailable {
family: OperationFamily::new(family::FAMILY),
reach,
};
let wire = to_connect_error(&refused);
assert_eq!(wire.code, ErrorCode::Unavailable);
let back = from_connect_error(&wire, &fallback());
assert_eq!(back, refused, "the reach did not survive the wire");
assert!(back.is_outage());
assert_eq!(back.retry_class(), refused.retry_class());
}
let outage = from_connect_error(
&to_connect_error(&StateError::Unavailable {
family: OperationFamily::new(family::FAMILY),
reach: OutageReach::PossiblyApplied,
}),
&fallback(),
);
let exhausted = from_connect_error(
&to_connect_error(&StateError::BoundsExceeded {
bound: BoundKind::PayloadBytes,
limit: 4,
requested: 9,
}),
&fallback(),
);
assert!(outage.is_outage() && !exhausted.is_outage());
assert_eq!(outage.retry_class(), RetryClass::Ambiguous);
assert_eq!(exhausted.retry_class(), RetryClass::Terminal);
}
#[test]
fn an_unreadable_reach_never_arrives_as_a_request_that_stayed_home() {
let never = from_connect_error(
&to_connect_error(&StateError::Unavailable {
family: OperationFamily::new(family::FAMILY),
reach: OutageReach::NeverDispatched,
}),
&fallback(),
);
assert_eq!(never.retry_class(), RetryClass::Transient);
assert!(!never.is_ambiguous());
for unreadable in [
buffa::EnumValue::from(pb::OutageReach::OUTAGE_REACH_UNSPECIFIED),
buffa::EnumValue::from(97),
] {
let skewed = pb::StateErrorDetail {
outcome: Some(pb::__buffa::oneof::state_error_detail::Outcome::from(
pb::UnavailableDetail {
family: family::FAMILY.to_owned(),
reach: unreadable,
__buffa_unknown_fields: buffa::UnknownFields::default(),
},
)),
__buffa_unknown_fields: buffa::UnknownFields::default(),
};
let wire = ConnectError::unavailable("gone")
.with_detail(ErrorDetail::from_message(STATE_ERROR_DETAIL_TYPE, &skewed));
let read_back = from_connect_error(&wire, &fallback());
assert_eq!(
read_back,
StateError::Unavailable {
family: OperationFamily::new(family::FAMILY),
reach: OutageReach::PossiblyApplied,
},
"an unrecognized reach must resolve to the weaker claim"
);
assert!(read_back.is_outage() && read_back.is_ambiguous());
}
}
#[test]
fn a_refused_dial_is_the_one_outage_that_proves_nothing_landed() {
let never = fallback().never_dispatched();
assert_eq!(
never,
StateError::Unavailable {
family: OperationFamily::new(family::FAMILY),
reach: OutageReach::NeverDispatched,
}
);
assert_eq!(never.retry_class(), RetryClass::Transient);
assert!(never.is_outage() && never.is_retry_safe() && !never.is_ambiguous());
}
#[test]
fn a_reply_refused_against_the_bound_reports_a_floor_not_the_request() {
let tiny_request = TransportFallback::new(OperationFamily::new(family::FAMILY), 64, 12);
let refused = from_connect_error(
&ConnectError::resource_exhausted("message size 900 exceeds limit 64"),
&tiny_request,
);
match refused {
StateError::BoundsExceeded {
bound,
limit,
requested,
} => {
assert_eq!(bound, BoundKind::PayloadBytes);
assert_eq!(limit, 64);
assert!(
requested > limit,
"a bound reported as unbroken would be nonsense: {requested} vs {limit}"
);
}
other => panic!("expected the wire bound to be exceeded, got {other}"),
}
assert_eq!(refused.retry_class(), RetryClass::Terminal);
}
#[test]
fn a_compacted_range_survives_the_wire_with_the_floor_it_named() {
let refused = StateError::CompactedRange {
partition: PartitionId::new("conv-1"),
requested: JournalPosition::new(3),
earliest: JournalPosition::new(9),
};
let wire = to_connect_error(&refused);
assert_eq!(wire.code, ErrorCode::OutOfRange);
let back = from_connect_error(&wire, &fallback());
assert_eq!(back, refused);
assert_eq!(back.retry_class(), RetryClass::Terminal);
assert!(!back.is_retry_safe(), "retrying the same cursor is futile");
let bare = from_connect_error(
&ConnectError::new(ErrorCode::OutOfRange, "gone"),
&fallback(),
);
assert!(matches!(bare, StateError::CompactedRange { .. }));
assert_eq!(bare.retry_class(), RetryClass::Terminal);
}
#[test]
fn a_retired_revision_keeps_a_terminal_code_without_its_detail() {
let retired = StateError::RetiredRevision {
requested: Revision::new(3),
earliest: Revision::new(9),
};
let wire = to_connect_error(&retired);
assert_eq!(wire.code, ErrorCode::OutOfRange);
let bare = from_connect_error(
&ConnectError::new(wire.code, "detail removed by an older peer"),
&fallback(),
);
assert_eq!(bare.retry_class(), RetryClass::Terminal);
assert!(!bare.is_retry_safe());
}
#[test]
fn an_unreadable_detail_falls_back_to_the_code() {
let error = ConnectError::deadline_exceeded("gone").with_detail(ErrorDetail {
type_url: STATE_ERROR_DETAIL_TYPE.to_owned(),
value: Some("!!!not base64!!!".to_owned()),
debug: None,
});
assert!(matches!(
from_connect_error(&error, &fallback()),
StateError::DeadlineExpired { .. }
));
let empty = ConnectError::permission_denied("no").with_detail(ErrorDetail::from_message(
STATE_ERROR_DETAIL_TYPE,
&pb::StateErrorDetail::default(),
));
assert!(matches!(
from_connect_error(&empty, &fallback()),
StateError::Denied { .. }
));
}
#[test]
fn a_foreign_detail_is_ignored() {
let error = ConnectError::canceled("gone").with_detail(ErrorDetail {
type_url: "google.rpc.RetryInfo".to_owned(),
value: Some(String::new()),
debug: None,
});
assert!(matches!(
from_connect_error(&error, &fallback()),
StateError::Cancelled { .. }
));
}
#[test]
fn each_variant_carries_a_distinct_transport_code() {
assert_eq!(
code_for(&StateError::Denied {
family: OperationFamily::new(family::FAMILY)
}),
ErrorCode::PermissionDenied
);
assert_eq!(
code_for(&StateError::Malformed {
field: "f".to_owned(),
reason: "r".to_owned()
}),
ErrorCode::InvalidArgument
);
assert_eq!(
code_for(&StateError::BoundsExceeded {
bound: BoundKind::PayloadBytes,
limit: 1,
requested: 2
}),
ErrorCode::ResourceExhausted
);
assert_eq!(
code_for(&StateError::Unavailable {
family: OperationFamily::new(family::FAMILY),
reach: OutageReach::NeverDispatched,
}),
ErrorCode::Unavailable
);
assert_eq!(
code_for(&StateError::RetiredRevision {
requested: Revision::new(3),
earliest: Revision::new(9),
}),
ErrorCode::OutOfRange
);
}
}