nanocodex_service/
service_error.rs1use std::time::Duration;
2
3use nanocodex_core::EventError;
4use serde::Serialize;
5
6use crate::{ResponsesError, RetryAdvice};
7
8#[derive(Debug)]
9pub struct ResponsesServiceError {
10 pub(crate) source: ResponsesServiceErrorSource,
11 pub(crate) phase: FailurePhase,
12 class: &'static str,
13 pub(crate) retry_advice: Option<RetryAdvice>,
14 pub(crate) connection_generation: u32,
15}
16
17impl ResponsesServiceError {
18 fn new(
19 source: ResponsesServiceErrorSource,
20 phase: FailurePhase,
21 class: &'static str,
22 retry_advice: Option<RetryAdvice>,
23 connection_generation: u32,
24 ) -> Self {
25 Self {
26 source,
27 phase,
28 class,
29 retry_advice,
30 connection_generation,
31 }
32 }
33
34 pub(crate) fn responses(
35 source: ResponsesError,
36 phase: FailurePhase,
37 connection_generation: u32,
38 ) -> Self {
39 let class = source.class();
40 let retry_advice = source.retry_advice();
41 Self::new(
42 ResponsesServiceErrorSource::Responses(source),
43 phase,
44 class,
45 retry_advice,
46 connection_generation,
47 )
48 }
49
50 pub(crate) fn event(
51 source: EventError,
52 phase: FailurePhase,
53 connection_generation: u32,
54 ) -> Self {
55 Self::new(
56 ResponsesServiceErrorSource::Event(source),
57 phase,
58 "output",
59 None,
60 connection_generation,
61 )
62 }
63
64 pub(crate) fn invalid_attempt_state(
65 detail: &'static str,
66 phase: FailurePhase,
67 connection_generation: u32,
68 ) -> Self {
69 Self::new(
70 ResponsesServiceErrorSource::InvalidAttemptState { detail },
71 phase,
72 "protocol",
73 None,
74 connection_generation,
75 )
76 }
77
78 pub(crate) fn invalid_compaction(count: usize) -> Self {
79 Self::new(
80 ResponsesServiceErrorSource::InvalidCompactionOutput { count },
81 FailurePhase::Completion,
82 "protocol",
83 None,
84 0,
85 )
86 }
87
88 pub(crate) fn with_connection_generation(mut self, connection_generation: u32) -> Self {
89 self.connection_generation = connection_generation;
90 self
91 }
92
93 #[must_use]
94 pub const fn error_class(&self) -> &'static str {
95 self.class
96 }
97
98 #[must_use]
99 pub const fn is_retryable(&self) -> bool {
100 self.retry_advice.is_some()
101 }
102
103 #[must_use]
104 pub fn is_checkpoint_missing(&self) -> bool {
105 self.responses_error()
106 .is_some_and(ResponsesError::is_checkpoint_missing)
107 }
108
109 #[must_use]
110 pub fn server_retry_after(&self) -> Option<Duration> {
111 self.retry_advice.and_then(|advice| advice.server_delay)
112 }
113
114 #[must_use]
115 pub const fn responses_error(&self) -> Option<&ResponsesError> {
116 match &self.source {
117 ResponsesServiceErrorSource::Responses(error) => Some(error),
118 ResponsesServiceErrorSource::Event(_)
119 | ResponsesServiceErrorSource::InvalidAttemptState { .. }
120 | ResponsesServiceErrorSource::InvalidCompactionOutput { .. } => None,
121 }
122 }
123}
124
125impl std::fmt::Display for ResponsesServiceError {
126 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 self.source.fmt(formatter)
128 }
129}
130
131impl std::error::Error for ResponsesServiceError {
132 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
133 Some(&self.source)
134 }
135}
136
137impl From<ResponsesError> for ResponsesServiceError {
138 fn from(error: ResponsesError) -> Self {
139 let phase = match error {
140 ResponsesError::IdleTimeout { .. } => FailurePhase::Idle,
141 ResponsesError::UnexpectedEnd
142 | ResponsesError::Closed { .. }
143 | ResponsesError::Receive(_) => FailurePhase::Receive,
144 ResponsesError::Api { .. } => FailurePhase::Api,
145 _ => FailurePhase::Protocol,
146 };
147 Self::responses(error, phase, 0)
148 }
149}
150
151impl From<EventError> for ResponsesServiceError {
152 fn from(error: EventError) -> Self {
153 Self::event(error, FailurePhase::Output, 0)
154 }
155}
156
157#[derive(Debug, thiserror::Error)]
158pub(crate) enum ResponsesServiceErrorSource {
159 #[error(transparent)]
160 Responses(#[from] ResponsesError),
161 #[error(transparent)]
162 Event(#[from] EventError),
163 #[error("invalid Responses attempt state: {detail}")]
164 InvalidAttemptState { detail: &'static str },
165 #[error("remote compaction returned {count} compaction items; expected exactly one")]
166 InvalidCompactionOutput { count: usize },
167}
168
169#[derive(Clone, Copy, Debug, Serialize)]
170#[serde(rename_all = "snake_case")]
171pub(crate) enum FailurePhase {
172 Connect,
173 Encode,
174 Send,
175 Receive,
176 Idle,
177 Api,
178 Protocol,
179 Completion,
180 Output,
181}