Skip to main content

mongreldb_types/
errors.rs

1//! Stable cross-language error taxonomy (spec section 9.7, FND-007).
2//!
3//! Implemented in the Stage 0 foundation wave: `ErrorCategory` with exactly
4//! the twenty categories the spec lists, stable numeric codes that are never
5//! reused, and the canonical structural form every language binding maps.
6//!
7//! Retry policy is expressed per category through [`ErrorCategory::retry_class`];
8//! the routing rules of spec section 11.7 (leader hints, metadata refresh, and
9//! the ban on replaying ambiguous writes without a durable idempotency key)
10//! are layered on top of that classification by the gateway.
11
12/// One of the twenty stable error categories of spec section 9.7.
13///
14/// Stability contract (spec section 4.10):
15///
16/// - The numeric [`ErrorCategory::code`] of a category is never reused, even
17///   if a category is retired; new categories are only ever appended with
18///   fresh codes.
19/// - Declaration order is frozen: serde encodes enums by variant index in
20///   binary formats, so variants must never be reordered.
21/// - Variant names are the cross-language wire contract in text formats;
22///   they never change.
23#[derive(
24    Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, thiserror::Error,
25)]
26pub enum ErrorCategory {
27    /// The receiving replica is not the leader for the consensus group.
28    /// Retryable with the returned leader hint (spec section 11.7).
29    #[error("not leader")]
30    NotLeader,
31    /// No leader is currently known for the consensus group; retry after
32    /// re-running leader discovery and refreshing routing metadata.
33    #[error("leader unknown")]
34    LeaderUnknown,
35    /// The caller acted on stale routing or schema metadata; retry after a
36    /// metadata refresh.
37    #[error("stale metadata")]
38    StaleMetadata,
39    /// The target replica cannot serve the request; retry with backoff or
40    /// fail over to another replica.
41    #[error("replica unavailable")]
42    ReplicaUnavailable,
43    /// The consensus group lost quorum; retry with backoff until a quorum is
44    /// restored.
45    #[error("quorum unavailable")]
46    QuorumUnavailable,
47    /// The transaction conflicted with a concurrent transaction; retry the
48    /// whole transaction.
49    #[error("transaction conflict")]
50    TransactionConflict,
51    /// The transaction was aborted (by the system or by the caller); a fresh
52    /// transaction may be retried from the beginning.
53    #[error("transaction aborted")]
54    TransactionAborted,
55    /// A serializable transaction failed its certification; retry the whole
56    /// transaction.
57    #[error("serialization failure")]
58    SerializationFailure,
59    /// The transaction was chosen as a deadlock victim; retry the whole
60    /// transaction.
61    #[error("deadlock")]
62    Deadlock,
63    /// The caller-supplied deadline expired. Not automatically retryable:
64    /// issue a new request with a fresh deadline instead of replaying.
65    #[error("deadline exceeded")]
66    DeadlineExceeded,
67    /// The caller cancelled the operation. Never retried: cancellation is
68    /// caller intent.
69    #[error("cancelled")]
70    Cancelled,
71    /// The outcome of a commit is unknown (contact was lost after propose).
72    /// NEVER blindly retried (spec section 11.7): replay only with a durable
73    /// idempotency key or an unambiguous not-proposed status.
74    #[error("commit outcome unknown")]
75    CommitOutcomeUnknown,
76    /// The commit arrived after its window closed and was not applied. The
77    /// commit itself is final; only a fresh transaction may succeed.
78    #[error("commit too late")]
79    CommitTooLate,
80    /// The tablet moved to another node; retry after refreshing routing
81    /// metadata.
82    #[error("tablet moved")]
83    TabletMoved,
84    /// The tablet is mid-split; retry after the split completes and routing
85    /// metadata has been refreshed.
86    #[error("tablet splitting")]
87    TabletSplitting,
88    /// The request was built against a different schema version; retry after
89    /// refreshing the schema and re-preparing the request.
90    #[error("schema version mismatch")]
91    SchemaVersionMismatch,
92    /// Binary, protocol, or format versions disagree (spec section 11.8).
93    /// Not retryable: only upgrading or rolling back one side changes the
94    /// outcome.
95    #[error("cluster version mismatch")]
96    ClusterVersionMismatch,
97    /// A resource limit (memory, disk, budget, locks) was hit; retry with
98    /// backoff once capacity frees up, or with a smaller request.
99    #[error("resource exhausted")]
100    ResourceExhausted,
101    /// Credentials are missing or invalid. Not retryable until the caller
102    /// obtains different credentials — that is a new request, not a retry.
103    #[error("unauthenticated")]
104    Unauthenticated,
105    /// The authenticated principal lacks the required grant. Never retried:
106    /// replaying the same request as the same principal re-fails.
107    #[error("permission denied")]
108    PermissionDenied,
109}
110
111/// The retry discipline for an [`ErrorCategory`] (spec section 11.7).
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
113pub enum RetryClass {
114    /// Never retried automatically: the same request would re-fail (or the
115    /// failure is caller intent), and only changing the request, the
116    /// principal, or a version changes the outcome.
117    Never,
118    /// Never blindly retried: the outcome is ambiguous, so a replay is only
119    /// permitted with a durable idempotency key or an unambiguous
120    /// not-proposed status (spec section 11.7).
121    IdempotencyKeyRequired,
122    /// Retryable after refreshing routing and/or schema metadata (leader
123    /// hint, endpoint list, tablet map, schema version).
124    AfterMetadataRefresh,
125    /// Retryable after a backoff delay once the transient condition
126    /// (availability, quorum, resource pressure) clears.
127    AfterBackoff,
128    /// Retryable by restarting the whole transaction from the beginning with
129    /// a fresh snapshot.
130    RetryTransaction,
131}
132
133impl ErrorCategory {
134    /// All twenty categories in declaration (and code) order.
135    pub const ALL: [Self; 20] = [
136        Self::NotLeader,
137        Self::LeaderUnknown,
138        Self::StaleMetadata,
139        Self::ReplicaUnavailable,
140        Self::QuorumUnavailable,
141        Self::TransactionConflict,
142        Self::TransactionAborted,
143        Self::SerializationFailure,
144        Self::Deadlock,
145        Self::DeadlineExceeded,
146        Self::Cancelled,
147        Self::CommitOutcomeUnknown,
148        Self::CommitTooLate,
149        Self::TabletMoved,
150        Self::TabletSplitting,
151        Self::SchemaVersionMismatch,
152        Self::ClusterVersionMismatch,
153        Self::ResourceExhausted,
154        Self::Unauthenticated,
155        Self::PermissionDenied,
156    ];
157
158    /// The stable numeric code of this category, in `1..=20`.
159    ///
160    /// Codes are never reused (spec section 4.10): a retired category keeps
161    /// its code forever and new categories are only ever appended, so a code
162    /// a peer emits is always interpretable.
163    pub const fn code(self) -> u32 {
164        match self {
165            Self::NotLeader => 1,
166            Self::LeaderUnknown => 2,
167            Self::StaleMetadata => 3,
168            Self::ReplicaUnavailable => 4,
169            Self::QuorumUnavailable => 5,
170            Self::TransactionConflict => 6,
171            Self::TransactionAborted => 7,
172            Self::SerializationFailure => 8,
173            Self::Deadlock => 9,
174            Self::DeadlineExceeded => 10,
175            Self::Cancelled => 11,
176            Self::CommitOutcomeUnknown => 12,
177            Self::CommitTooLate => 13,
178            Self::TabletMoved => 14,
179            Self::TabletSplitting => 15,
180            Self::SchemaVersionMismatch => 16,
181            Self::ClusterVersionMismatch => 17,
182            Self::ResourceExhausted => 18,
183            Self::Unauthenticated => 19,
184            Self::PermissionDenied => 20,
185        }
186    }
187
188    /// Resolves a stable [`Self::code`] back to its category.
189    ///
190    /// Returns `None` for codes this build does not know (never emitted by
191    /// this build; possibly allocated by a newer one). Callers MUST treat an
192    /// unknown code as an unknown, non-retryable failure rather than
193    /// guessing.
194    pub const fn from_code(code: u32) -> Option<Self> {
195        match code {
196            1 => Some(Self::NotLeader),
197            2 => Some(Self::LeaderUnknown),
198            3 => Some(Self::StaleMetadata),
199            4 => Some(Self::ReplicaUnavailable),
200            5 => Some(Self::QuorumUnavailable),
201            6 => Some(Self::TransactionConflict),
202            7 => Some(Self::TransactionAborted),
203            8 => Some(Self::SerializationFailure),
204            9 => Some(Self::Deadlock),
205            10 => Some(Self::DeadlineExceeded),
206            11 => Some(Self::Cancelled),
207            12 => Some(Self::CommitOutcomeUnknown),
208            13 => Some(Self::CommitTooLate),
209            14 => Some(Self::TabletMoved),
210            15 => Some(Self::TabletSplitting),
211            16 => Some(Self::SchemaVersionMismatch),
212            17 => Some(Self::ClusterVersionMismatch),
213            18 => Some(Self::ResourceExhausted),
214            19 => Some(Self::Unauthenticated),
215            20 => Some(Self::PermissionDenied),
216            _ => None,
217        }
218    }
219
220    /// How a failure in this category may be retried (spec section 11.7).
221    pub const fn retry_class(self) -> RetryClass {
222        match self {
223            Self::NotLeader
224            | Self::LeaderUnknown
225            | Self::StaleMetadata
226            | Self::TabletMoved
227            | Self::TabletSplitting
228            | Self::SchemaVersionMismatch => RetryClass::AfterMetadataRefresh,
229            Self::ReplicaUnavailable | Self::QuorumUnavailable | Self::ResourceExhausted => {
230                RetryClass::AfterBackoff
231            }
232            Self::TransactionConflict
233            | Self::TransactionAborted
234            | Self::SerializationFailure
235            | Self::Deadlock
236            | Self::CommitTooLate => RetryClass::RetryTransaction,
237            Self::DeadlineExceeded
238            | Self::Cancelled
239            | Self::ClusterVersionMismatch
240            | Self::Unauthenticated
241            | Self::PermissionDenied => RetryClass::Never,
242            Self::CommitOutcomeUnknown => RetryClass::IdempotencyKeyRequired,
243        }
244    }
245
246    /// Whether a plain automatic retry — no fresh credentials and no durable
247    /// idempotency key — may succeed for this category.
248    ///
249    /// This is deliberately `false` for [`ErrorCategory::CommitOutcomeUnknown`]
250    /// even though a replay *with* a durable idempotency key is permitted
251    /// (spec section 11.7): "retryable" here means safe to retry blindly.
252    pub const fn is_retryable(self) -> bool {
253        matches!(
254            self.retry_class(),
255            RetryClass::AfterMetadataRefresh
256                | RetryClass::AfterBackoff
257                | RetryClass::RetryTransaction
258        )
259    }
260}
261
262/// The canonical structural error form of the taxonomy (spec section 9.7).
263///
264/// Every language binding — the Node addon, the C FFI, and the JNI binding,
265/// all built in later stages — maps exactly this structure: a stable
266/// [`ErrorCategory`] plus a human-readable message. The message is diagnostic
267/// text only; programmatic handling MUST key off `category` (or its stable
268/// [`CategoryError::code`]), never off the message.
269#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, thiserror::Error)]
270#[error("{category}: {message}")]
271pub struct CategoryError {
272    /// The stable category of the failure.
273    pub category: ErrorCategory,
274    /// Human-readable detail. Not part of the stable contract.
275    pub message: String,
276}
277
278impl CategoryError {
279    /// Builds the structural form for `category` with diagnostic `message`.
280    pub fn new(category: ErrorCategory, message: impl Into<String>) -> Self {
281        Self {
282            category,
283            message: message.into(),
284        }
285    }
286
287    /// The stable numeric code of [`Self::category`] (never reused, spec
288    /// section 4.10).
289    pub fn code(&self) -> u32 {
290        self.category.code()
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use serde::de::value::{Error as ValueError, StrDeserializer};
298    use serde::de::{
299        DeserializeSeed, EnumAccess, IntoDeserializer, MapAccess, VariantAccess, Visitor,
300    };
301    use serde::ser::{Impossible, SerializeStruct};
302    use serde::{Deserialize, Deserializer, Serialize, Serializer};
303
304    // Minimal serde data-model format used to round-trip the taxonomy in
305    // tests without pulling a serialization crate into this crate's
306    // (frozen) dependency set: values become a `Value` tree and the
307    // deserializer replays it.
308    #[derive(Debug, Clone, PartialEq, Eq)]
309    enum Value {
310        Str(String),
311        UnitVariant(&'static str),
312        Struct(Vec<(&'static str, Value)>),
313    }
314
315    struct ValueSerializer;
316
317    macro_rules! serialize_unsupported {
318        ($($method:ident ( $($param:ident : $ty:ty),* ) -> $ret:ty ;)*) => {
319            $(
320                fn $method(self, $($param: $ty),*) -> Result<$ret, ValueError> {
321                    Err(serde::ser::Error::custom(concat!(
322                        stringify!($method),
323                        " is not supported by the in-test serde format"
324                    )))
325                }
326            )*
327        };
328    }
329
330    impl Serializer for ValueSerializer {
331        type Ok = Value;
332        type Error = ValueError;
333        type SerializeSeq = Impossible<Value, ValueError>;
334        type SerializeTuple = Impossible<Value, ValueError>;
335        type SerializeTupleStruct = Impossible<Value, ValueError>;
336        type SerializeTupleVariant = Impossible<Value, ValueError>;
337        type SerializeMap = Impossible<Value, ValueError>;
338        type SerializeStruct = StructSerializer;
339        type SerializeStructVariant = Impossible<Value, ValueError>;
340
341        fn serialize_str(self, v: &str) -> Result<Value, ValueError> {
342            Ok(Value::Str(v.to_owned()))
343        }
344
345        fn serialize_unit_variant(
346            self,
347            _name: &'static str,
348            _variant_index: u32,
349            variant: &'static str,
350        ) -> Result<Value, ValueError> {
351            Ok(Value::UnitVariant(variant))
352        }
353
354        fn serialize_struct(
355            self,
356            _name: &'static str,
357            len: usize,
358        ) -> Result<StructSerializer, ValueError> {
359            Ok(StructSerializer {
360                fields: Vec::with_capacity(len),
361            })
362        }
363
364        fn serialize_none(self) -> Result<Value, ValueError> {
365            Err(serde::ser::Error::custom(
366                "serialize_none is not supported by the in-test serde format",
367            ))
368        }
369
370        fn serialize_some<T: ?Sized + Serialize>(self, _value: &T) -> Result<Value, ValueError> {
371            Err(serde::ser::Error::custom(
372                "serialize_some is not supported by the in-test serde format",
373            ))
374        }
375
376        fn serialize_newtype_struct<T: ?Sized + Serialize>(
377            self,
378            _name: &'static str,
379            _value: &T,
380        ) -> Result<Value, ValueError> {
381            Err(serde::ser::Error::custom(
382                "serialize_newtype_struct is not supported by the in-test serde format",
383            ))
384        }
385
386        fn serialize_newtype_variant<T: ?Sized + Serialize>(
387            self,
388            _name: &'static str,
389            _variant_index: u32,
390            _variant: &'static str,
391            _value: &T,
392        ) -> Result<Value, ValueError> {
393            Err(serde::ser::Error::custom(
394                "serialize_newtype_variant is not supported by the in-test serde format",
395            ))
396        }
397
398        serialize_unsupported! {
399            serialize_bool(_v: bool) -> Value;
400            serialize_i8(_v: i8) -> Value;
401            serialize_i16(_v: i16) -> Value;
402            serialize_i32(_v: i32) -> Value;
403            serialize_i64(_v: i64) -> Value;
404            serialize_u8(_v: u8) -> Value;
405            serialize_u16(_v: u16) -> Value;
406            serialize_u32(_v: u32) -> Value;
407            serialize_u64(_v: u64) -> Value;
408            serialize_f32(_v: f32) -> Value;
409            serialize_f64(_v: f64) -> Value;
410            serialize_char(_v: char) -> Value;
411            serialize_bytes(_v: &[u8]) -> Value;
412            serialize_unit() -> Value;
413            serialize_unit_struct(_name: &'static str) -> Value;
414            serialize_seq(_len: Option<usize>) -> Self::SerializeSeq;
415            serialize_tuple(_len: usize) -> Self::SerializeTuple;
416            serialize_tuple_struct(_name: &'static str, _len: usize) -> Self::SerializeTupleStruct;
417            serialize_tuple_variant(_name: &'static str, _variant_index: u32, _variant: &'static str, _len: usize) -> Self::SerializeTupleVariant;
418            serialize_map(_len: Option<usize>) -> Self::SerializeMap;
419            serialize_struct_variant(_name: &'static str, _variant_index: u32, _variant: &'static str, _len: usize) -> Self::SerializeStructVariant;
420        }
421    }
422
423    struct StructSerializer {
424        fields: Vec<(&'static str, Value)>,
425    }
426
427    impl SerializeStruct for StructSerializer {
428        type Ok = Value;
429        type Error = ValueError;
430
431        fn serialize_field<T: ?Sized + Serialize>(
432            &mut self,
433            key: &'static str,
434            value: &T,
435        ) -> Result<(), ValueError> {
436            self.fields.push((key, value.serialize(ValueSerializer)?));
437            Ok(())
438        }
439
440        fn end(self) -> Result<Value, ValueError> {
441            Ok(Value::Struct(self.fields))
442        }
443    }
444
445    struct ValueDeserializer(Value);
446
447    impl<'de> Deserializer<'de> for ValueDeserializer {
448        type Error = ValueError;
449
450        fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, ValueError> {
451            match self.0 {
452                Value::Str(text) => visitor.visit_string(text),
453                Value::UnitVariant(variant) => visitor.visit_enum(UnitVariantAccess { variant }),
454                Value::Struct(fields) => visitor.visit_map(StructAccess {
455                    iter: fields.into_iter(),
456                    value: None,
457                }),
458            }
459        }
460
461        fn deserialize_struct<V: Visitor<'de>>(
462            self,
463            _name: &'static str,
464            _fields: &'static [&'static str],
465            visitor: V,
466        ) -> Result<V::Value, ValueError> {
467            self.deserialize_any(visitor)
468        }
469
470        fn deserialize_enum<V: Visitor<'de>>(
471            self,
472            _name: &'static str,
473            _variants: &'static [&'static str],
474            visitor: V,
475        ) -> Result<V::Value, ValueError> {
476            self.deserialize_any(visitor)
477        }
478
479        fn deserialize_str<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, ValueError> {
480            self.deserialize_any(visitor)
481        }
482
483        fn deserialize_string<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, ValueError> {
484            self.deserialize_any(visitor)
485        }
486
487        serde::forward_to_deserialize_any! {
488            bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char bytes
489            byte_buf option unit unit_struct newtype_struct seq tuple
490            tuple_struct map identifier ignored_any
491        }
492    }
493
494    struct StructAccess {
495        iter: std::vec::IntoIter<(&'static str, Value)>,
496        value: Option<Value>,
497    }
498
499    impl<'de> MapAccess<'de> for StructAccess {
500        type Error = ValueError;
501
502        fn next_key_seed<K: DeserializeSeed<'de>>(
503            &mut self,
504            seed: K,
505        ) -> Result<Option<K::Value>, ValueError> {
506            match self.iter.next() {
507                Some((key, value)) => {
508                    self.value = Some(value);
509                    let deserializer: StrDeserializer<'de, ValueError> = key.into_deserializer();
510                    seed.deserialize(deserializer).map(Some)
511                }
512                None => Ok(None),
513            }
514        }
515
516        fn next_value_seed<V: DeserializeSeed<'de>>(
517            &mut self,
518            seed: V,
519        ) -> Result<V::Value, ValueError> {
520            let value = self
521                .value
522                .take()
523                .ok_or_else(|| serde::de::Error::custom("struct field value missing"))?;
524            seed.deserialize(ValueDeserializer(value))
525        }
526    }
527
528    struct UnitVariantAccess {
529        variant: &'static str,
530    }
531
532    impl<'de> EnumAccess<'de> for UnitVariantAccess {
533        type Error = ValueError;
534        type Variant = Self;
535
536        fn variant_seed<V: DeserializeSeed<'de>>(
537            self,
538            seed: V,
539        ) -> Result<(V::Value, Self), ValueError> {
540            let deserializer: StrDeserializer<'de, ValueError> = self.variant.into_deserializer();
541            let value = seed.deserialize(deserializer)?;
542            Ok((value, self))
543        }
544    }
545
546    impl<'de> VariantAccess<'de> for UnitVariantAccess {
547        type Error = ValueError;
548
549        fn unit_variant(self) -> Result<(), ValueError> {
550            Ok(())
551        }
552
553        fn newtype_variant_seed<T: DeserializeSeed<'de>>(
554            self,
555            _seed: T,
556        ) -> Result<T::Value, ValueError> {
557            Err(serde::de::Error::custom("expected a unit variant"))
558        }
559
560        fn tuple_variant<V: Visitor<'de>>(
561            self,
562            _len: usize,
563            _visitor: V,
564        ) -> Result<V::Value, ValueError> {
565            Err(serde::de::Error::custom("expected a unit variant"))
566        }
567
568        fn struct_variant<V: Visitor<'de>>(
569            self,
570            _fields: &'static [&'static str],
571            _visitor: V,
572        ) -> Result<V::Value, ValueError> {
573            Err(serde::de::Error::custom("expected a unit variant"))
574        }
575    }
576
577    fn to_value<T: Serialize>(value: &T) -> Value {
578        value
579            .serialize(ValueSerializer)
580            .expect("test serializer failed")
581    }
582
583    fn from_value<T: for<'de> Deserialize<'de>>(value: Value) -> T {
584        T::deserialize(ValueDeserializer(value)).expect("test deserializer failed")
585    }
586
587    #[test]
588    fn codes_are_stable_unique_and_round_trippable() {
589        assert_eq!(ErrorCategory::ALL.len(), 20);
590        let codes: Vec<u32> = ErrorCategory::ALL
591            .iter()
592            .map(|category| category.code())
593            .collect();
594        // Declaration order must stay exactly 1..=20: stable, ordered, unique.
595        assert_eq!(codes, (1..=20).collect::<Vec<u32>>());
596        for category in ErrorCategory::ALL {
597            assert_eq!(ErrorCategory::from_code(category.code()), Some(category));
598        }
599        assert_eq!(ErrorCategory::from_code(0), None);
600        assert_eq!(ErrorCategory::from_code(21), None);
601    }
602
603    #[test]
604    fn serde_round_trip_preserves_every_category() {
605        for category in ErrorCategory::ALL {
606            let value = to_value(&category);
607            // Externally tagged unit variants serialize as their bare variant
608            // name; the names are the cross-language wire contract.
609            match &value {
610                Value::UnitVariant(name) => assert_eq!(*name, format!("{category:?}").as_str()),
611                other => panic!("expected unit variant, got {other:?}"),
612            }
613            assert_eq!(from_value::<ErrorCategory>(value), category);
614        }
615    }
616
617    #[test]
618    fn category_error_serde_round_trip() {
619        let original = CategoryError::new(
620            ErrorCategory::CommitOutcomeUnknown,
621            "commit epoch 42 lost contact after propose",
622        );
623        let value = to_value(&original);
624        match &value {
625            Value::Struct(fields) => {
626                assert_eq!(fields.len(), 2);
627                assert_eq!(fields[0].0, "category");
628                assert_eq!(fields[1].0, "message");
629            }
630            other => panic!("expected struct, got {other:?}"),
631        }
632        assert_eq!(from_value::<CategoryError>(value), original);
633    }
634
635    #[test]
636    fn display_is_human_readable() {
637        assert_eq!(ErrorCategory::NotLeader.to_string(), "not leader");
638        assert_eq!(
639            ErrorCategory::CommitOutcomeUnknown.to_string(),
640            "commit outcome unknown"
641        );
642        assert_eq!(
643            ErrorCategory::PermissionDenied.to_string(),
644            "permission denied"
645        );
646        let error = CategoryError::new(
647            ErrorCategory::PermissionDenied,
648            "principal \"alice\" lacks Admin",
649        );
650        assert_eq!(
651            error.to_string(),
652            "permission denied: principal \"alice\" lacks Admin"
653        );
654        assert_eq!(error.code(), ErrorCategory::PermissionDenied.code());
655    }
656
657    #[test]
658    fn retry_classification_matches_spec_11_7() {
659        // Routing failures resolve through refreshed metadata (leader hints).
660        assert!(ErrorCategory::NotLeader.is_retryable());
661        assert_eq!(
662            ErrorCategory::NotLeader.retry_class(),
663            RetryClass::AfterMetadataRefresh
664        );
665        assert!(ErrorCategory::StaleMetadata.is_retryable());
666        assert_eq!(
667            ErrorCategory::StaleMetadata.retry_class(),
668            RetryClass::AfterMetadataRefresh
669        );
670        assert_eq!(
671            ErrorCategory::TabletMoved.retry_class(),
672            RetryClass::AfterMetadataRefresh
673        );
674        // Transient availability and resource pressure retry with backoff.
675        assert!(ErrorCategory::ReplicaUnavailable.is_retryable());
676        assert_eq!(
677            ErrorCategory::QuorumUnavailable.retry_class(),
678            RetryClass::AfterBackoff
679        );
680        assert_eq!(
681            ErrorCategory::ResourceExhausted.retry_class(),
682            RetryClass::AfterBackoff
683        );
684        // Transaction failures restart the whole transaction.
685        assert!(ErrorCategory::TransactionConflict.is_retryable());
686        assert_eq!(
687            ErrorCategory::Deadlock.retry_class(),
688            RetryClass::RetryTransaction
689        );
690        assert_eq!(
691            ErrorCategory::SerializationFailure.retry_class(),
692            RetryClass::RetryTransaction
693        );
694        // Section 11.7: never automatically replay an ambiguous write without
695        // a durable idempotency key.
696        assert!(!ErrorCategory::CommitOutcomeUnknown.is_retryable());
697        assert_eq!(
698            ErrorCategory::CommitOutcomeUnknown.retry_class(),
699            RetryClass::IdempotencyKeyRequired
700        );
701        // Terminal categories are never retried.
702        for category in [
703            ErrorCategory::DeadlineExceeded,
704            ErrorCategory::Cancelled,
705            ErrorCategory::ClusterVersionMismatch,
706            ErrorCategory::Unauthenticated,
707            ErrorCategory::PermissionDenied,
708        ] {
709            assert!(
710                !category.is_retryable(),
711                "{category:?} must not be retryable"
712            );
713            assert_eq!(category.retry_class(), RetryClass::Never);
714        }
715    }
716}