Skip to main content

mj_controller/worker_client/
errors.rs

1use super::*;
2
3/// One bounded page in a catch-up whose upper frontier was fixed before any
4/// page was applied. The relay may return newer events on later `Attach`
5/// calls; those are deliberately left for the next catch-up.
6#[derive(Debug, Clone)]
7pub struct RelayEventPage {
8    pub events: Vec<RelayEvent>,
9    pub through_ordinal: u64,
10    pub through_digest: String,
11}
12
13#[derive(Debug, Clone)]
14pub struct RelayCatchUp {
15    pub state: RelayOperationalState,
16    pub frontier: RelayCursor,
17    pub first_page: RelayEventPage,
18}
19
20#[derive(Debug)]
21pub struct RelayRejected(pub RelayProtocolError);
22
23impl std::fmt::Display for RelayRejected {
24    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        write!(
26            formatter,
27            "relay rejected request ({:?}): {}",
28            self.0.code, self.0.message
29        )
30    }
31}
32
33impl std::error::Error for RelayRejected {}
34
35impl RelayRejected {
36    pub fn is_desynchronized(&self) -> bool {
37        self.0.code == RelayErrorCode::Desynchronized
38    }
39
40    /// Whether the relay itself said the same request could succeed later.
41    /// Validation rejections say no; transient internal failures say yes.
42    pub fn is_retryable(&self) -> bool {
43        self.0.retryable
44    }
45}
46
47/// A relay transport that can no longer carry requests: the proxy exited, one
48/// of its pipes failed, or the handshake never completed.
49///
50/// Every site that can prove this attaches the marker, and recovery decisions
51/// such as worker auto-restart downcast for it. Nothing reads the message text,
52/// so rewording a diagnostic can never silently disable recovery.
53#[derive(Debug)]
54pub struct RelayTransportDead {
55    pub(super) message: String,
56    pub(super) handshake_failed: bool,
57}
58
59impl std::fmt::Display for RelayTransportDead {
60    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        formatter.write_str(&self.message)
62    }
63}
64
65impl std::error::Error for RelayTransportDead {}
66
67impl RelayTransportDead {
68    pub fn new(message: impl Into<String>) -> Self {
69        Self {
70            message: message.into(),
71            handshake_failed: false,
72        }
73    }
74
75    /// Mark an I/O failure on the relay's pipes. The marker reports exactly
76    /// what the I/O error reported, so it adds a type without adding text.
77    pub(super) fn from_io(error: std::io::Error, kind: ExchangeKind) -> Self {
78        Self::during_exchange(error.to_string(), kind)
79    }
80
81    pub(super) fn during_exchange(message: impl Into<String>, kind: ExchangeKind) -> Self {
82        Self {
83            message: message.into(),
84            handshake_failed: kind == ExchangeKind::Handshake,
85        }
86    }
87
88    /// Whether this error, or any cause behind it, is a dead relay transport.
89    pub fn marks(error: &anyhow::Error) -> bool {
90        error.downcast_ref::<Self>().is_some()
91    }
92
93    /// Whether the worker was reachable enough to run its liveness probe but
94    /// the proxy then disconnected or failed I/O during a fresh handshake.
95    /// Timeouts are deliberately not marked: a live proxy can be waiting on a
96    /// loaded container runtime or filesystem, which restarting only worsens.
97    pub fn marks_failed_handshake(error: &anyhow::Error) -> bool {
98        error
99            .downcast_ref::<Self>()
100            .is_some_and(|failure| failure.handshake_failed)
101    }
102}
103
104/// Whether an exchange is the handshake that proves the transport carries
105/// traffic at all.
106///
107/// A disconnected handshake proves the new transport never became usable. A
108/// timeout does not: the proxy launcher or worker can still be alive and slow,
109/// so timeout classification is handled separately in [`RelayClient::exchange`].
110#[derive(Clone, Copy, PartialEq, Eq)]
111pub(super) enum ExchangeKind {
112    Handshake,
113    Call,
114}
115
116/// Why one relay proxy launch failed, and whether the SSH server refused the
117/// connection before authentication rather than the worker being unreachable.
118pub(super) struct ConnectFailure {
119    pub(super) error: anyhow::Error,
120    pub(super) transport_rejected: bool,
121}
122
123impl ConnectFailure {
124    pub(super) fn plain(error: anyhow::Error) -> Self {
125        Self {
126            error,
127            transport_rejected: false,
128        }
129    }
130}
131
132impl From<anyhow::Error> for ConnectFailure {
133    fn from(error: anyhow::Error) -> Self {
134        Self::plain(error)
135    }
136}