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}