Skip to main content

tsoracle_client/
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#[derive(Debug, thiserror::Error)]
25pub enum ClientError {
26    #[error("no reachable endpoints")]
27    NoReachableEndpoints,
28    #[error("transport: {0}")]
29    Transport(#[from] tonic::transport::Error),
30    /// A [`Self::Transport`] failure observed on one endpoint, fanned out to
31    /// the sibling waiters of a coalesced request chunk. `tonic::transport::Error`
32    /// is not `Clone`, so the originating value cannot be duplicated across
33    /// waiters; this variant carries its `Display` text instead. Distinct
34    /// from [`Self::NoReachableEndpoints`]: a single endpoint's transport
35    /// failed, *not* the whole cluster — collapsing the two would mislead
36    /// tracing/alerting on the receiving side.
37    #[error("transport (fanned out to coalesced waiter): {0}")]
38    TransportFanout(String),
39    #[error("rpc: {0}")]
40    Rpc(#[from] tonic::Status),
41    #[error("invalid endpoint: {0}")]
42    InvalidEndpoint(String),
43    #[error("invalid count: {0}")]
44    InvalidCount(u32),
45    #[error("custom channel connector failed: {0}")]
46    Connector(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
47    /// Local driver task has terminated (typically a panic in the user-supplied
48    /// RPC closure) and can no longer serve requests. Distinct from
49    /// [`Self::NoReachableEndpoints`]: the network is fine, the in-process
50    /// driver is dead. Operators should look for a panic in their `tracing`
51    /// logs, not a network issue.
52    #[error("client driver task is gone; subsequent requests cannot be served by this Client")]
53    DriverGone,
54    #[error("dense sequence outcome uncertain: the request may or may not have committed")]
55    SeqUncertain,
56    #[error("invalid sequence key")]
57    InvalidSeqKey,
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn connector_variant_renders_source_in_display() {
66        // Display must embed the boxed source's message so operators reading
67        // logs see the underlying cause, not just the wrapper.
68        let inner: Box<dyn std::error::Error + Send + Sync + 'static> = "boom".into();
69        let err = ClientError::Connector(inner);
70        assert_eq!(err.to_string(), "custom channel connector failed: boom");
71    }
72
73    #[test]
74    fn connector_variant_exposes_source() {
75        use std::error::Error;
76        let inner: Box<dyn std::error::Error + Send + Sync + 'static> =
77            std::io::Error::other("io-err").into();
78        let err = ClientError::Connector(inner);
79        assert!(err.source().is_some(), "Connector must propagate source()");
80    }
81}