Skip to main content

nanocodex_service/
service_error.rs

1use 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}