Skip to main content

tsoracle_consensus/
error.rs

1//
2//  ░▀█▀░█▀▀░█▀█░█▀▄░█▀█░█▀▀░█░░░█▀▀
3//  ░░█░░▀▀█░█░█░█▀▄░█▀█░█░░░█░░░█▀▀
4//  ░░▀░░▀▀▀░▀▀▀░▀░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀
5//
6//  tsoracle — Distributed Timestamp Oracle
7//  https://www.tsoracle.rs
8//
9//  Copyright (c) 2026 Prisma Risk
10//
11//  Licensed under the Apache License, Version 2.0 (the "License");
12//  you may not use this file except in compliance with the License.
13//  You may obtain a copy of the License at
14//
15//      https://www.apache.org/licenses/LICENSE-2.0
16//
17//  Unless required by applicable law or agreed to in writing, software
18//  distributed under the License is distributed on an "AS IS" BASIS,
19//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20//  See the License for the specific language governing permissions and
21//  limitations under the License.
22//
23
24//! Errors returned by [`ConsensusDriver`](crate::ConsensusDriver) operations.
25
26use tsoracle_core::Epoch;
27
28/// Errors returned by `ConsensusDriver` operations.
29///
30/// Driver implementations classify their internal failures into one of
31/// `TransientDriver` or `PermanentDriver`. The server uses that classification
32/// directly to pick a gRPC status code:
33///
34/// | Variant              | gRPC code            | Client expectation         |
35/// |----------------------|----------------------|----------------------------|
36/// | `NotLeader`          | `FAILED_PRECONDITION` + `LeaderHint` | Retry against the new leader |
37/// | `Fenced`             | `FAILED_PRECONDITION` + `LeaderHint` | Retry against the new leader |
38/// | `TransientDriver`    | `UNAVAILABLE`        | Safe to retry              |
39/// | `PermanentDriver`    | `INTERNAL`           | Do NOT silently retry      |
40/// | `AdvanceOutOfRange`  | `INTERNAL`           | Do NOT silently retry      |
41#[derive(Debug, thiserror::Error)]
42pub enum ConsensusError {
43    #[error("not leader (current epoch: {observed:?})")]
44    NotLeader { observed: Option<Epoch> },
45    #[error("epoch fenced: expected {expected:?}, current {current:?}")]
46    Fenced { expected: Epoch, current: Epoch },
47    /// A driver-level failure the caller MAY retry. Use for errors that are
48    /// reasonably expected to clear on their own: storage I/O hiccup, peer
49    /// transport flap, transient quorum loss.
50    #[error("transient driver error: {0}")]
51    TransientDriver(#[source] Box<dyn std::error::Error + Send + Sync>),
52    /// A driver-level failure the caller MUST NOT silently retry. Use for
53    /// persistent local fault: read-only filesystem, corruption, gone
54    /// storage device, bad driver implementation, invariant violation.
55    #[error("permanent driver error: {0}")]
56    PermanentDriver(#[source] Box<dyn std::error::Error + Send + Sync>),
57    /// A high-water advance whose `physical_ms` value exceeds the 46-bit
58    /// timestamp layout's cap. Carries the offending value structurally so
59    /// the server can format and route it without downcasting through
60    /// `Box<dyn Error>`. Semantically permanent (a saturated or misconfigured
61    /// clock), so the server surfaces it as `INTERNAL` rather than retrying.
62    /// See [`reject_out_of_range_advance`] for the propose-time guard.
63    ///
64    /// [`reject_out_of_range_advance`]: crate::reject_out_of_range_advance
65    #[error("high-water advance {0} exceeds the 46-bit physical_ms maximum")]
66    AdvanceOutOfRange(u64),
67    #[error("dense sequences are not supported by this driver")]
68    DenseUnsupported,
69    #[error("dense key-cardinality cap {cap} reached")]
70    SeqKeyCardinalityExceeded { cap: u64 },
71    #[error("dense counter overflow for the requested key")]
72    SeqOverflow,
73    #[error(
74        "dense sequences require write version {required} but the cluster is at {active}; activate the format first"
75    )]
76    DenseNotActivated { required: u8, active: u8 },
77    #[error(
78        "dense batch sequences require write version {required} but the cluster is at {active}; activate the format first"
79    )]
80    DenseBatchNotActivated { required: u8, active: u8 },
81    #[error("leases are not supported by this driver")]
82    LeasesUnsupported,
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn consensus_error_display_text() {
91        let not_leader = ConsensusError::NotLeader {
92            observed: Some(Epoch(3)),
93        };
94        assert!(not_leader.to_string().contains("not leader"));
95
96        let fenced = ConsensusError::Fenced {
97            expected: Epoch(2),
98            current: Epoch(5),
99        };
100        let fenced_text = fenced.to_string();
101        assert!(fenced_text.contains("fenced"));
102        assert!(fenced_text.contains('2'));
103        assert!(fenced_text.contains('5'));
104
105        let transient = ConsensusError::TransientDriver(Box::new(std::io::Error::other("flap")));
106        assert!(transient.to_string().contains("transient"));
107        assert!(transient.to_string().contains("flap"));
108
109        let permanent = ConsensusError::PermanentDriver(Box::new(std::io::Error::other("corrupt")));
110        assert!(permanent.to_string().contains("permanent"));
111        assert!(permanent.to_string().contains("corrupt"));
112
113        let out_of_range = ConsensusError::AdvanceOutOfRange(tsoracle_core::PHYSICAL_MS_MAX + 1);
114        let oor_text = out_of_range.to_string();
115        assert!(oor_text.contains("high-water advance"));
116        assert!(oor_text.contains("46-bit"));
117        assert!(oor_text.contains(&(tsoracle_core::PHYSICAL_MS_MAX + 1).to_string()));
118    }
119}