1use crate::RuntimeCapability;
16use std::fmt::{Display, Formatter};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum FileDescriptorError {
23 InvalidBudget {
24 limit: usize,
25 },
26 ConfigurationConflict {
27 configured: usize,
28 requested: usize,
29 },
30 BudgetExceeded {
31 requested: usize,
32 available: usize,
33 limit: usize,
34 },
35 OsLimit {
36 requested: usize,
37 os_code: Option<i32>,
38 soft: Option<u64>,
39 hard: Option<u64>,
40 },
41}
42
43impl Display for FileDescriptorError {
44 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
45 match self {
46 Self::InvalidBudget { limit } => write!(formatter, "invalid file descriptor budget: {limit}"),
47 Self::ConfigurationConflict { configured, requested } => write!(formatter,
48 "project file descriptor budget conflict: configured {configured}, requested {requested}"),
49 Self::BudgetExceeded { requested, available, limit } => write!(formatter,
50 "project file descriptor budget exceeded: requested {requested}, available {available}, limit {limit}"),
51 Self::OsLimit { requested, os_code, soft, hard } => write!(formatter,
52 "operating system file descriptor limit: requested {requested}, soft {soft:?}, hard {hard:?}, OS code {os_code:?}"),
53 }
54 }
55}
56
57impl std::error::Error for FileDescriptorError {}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum ReadBudgetResource {
62 Rows,
63 PayloadBytes,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct ReadBudgetError {
69 pub resource: ReadBudgetResource,
70 pub limit: usize,
71 pub message: String,
72}
73
74impl Display for ReadBudgetError {
75 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
76 formatter.write_str(&self.message)
77 }
78}
79
80impl std::error::Error for ReadBudgetError {}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum HawDBError {
84 Parse(String),
85 Semantic(String),
86 Storage(String),
87 FileDescriptors(FileDescriptorError),
88 StorageIntegrity(String),
89 Execution(String),
90 ReadBudgetExceeded(ReadBudgetError),
91 GraphExpansionCandidateLimitExceeded {
92 requested: usize,
93 limit: usize,
94 },
95 GraphExpansionPayloadLimitExceeded {
96 requested: usize,
97 limit: usize,
98 },
99 TransactionConflict {
100 read_epoch: u64,
101 committed_epoch: u64,
102 key: String,
103 },
104 AppendSequenceExhausted {
105 table: String,
106 watermark: i64,
107 requested: usize,
108 },
109 CapabilityUnavailable {
110 capability: RuntimeCapability,
111 },
112 BranchCommandUnsupported {
113 command: &'static str,
114 context: &'static str,
115 },
116 BranchBusy {
117 resource: &'static str,
118 },
119 BranchReclamationBlocked {
122 branch_id: crate::Uuid,
123 catalog_revision: u64,
124 metadata_revision: u64,
125 cause: Box<HawDBError>,
126 },
127}
128
129impl Display for HawDBError {
130 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
131 match self {
132 HawDBError::Parse(message) => write!(f, "parse error: {message}"),
133 HawDBError::Semantic(message) => write!(f, "semantic error: {message}"),
134 HawDBError::Storage(message) => write!(f, "storage error: {message}"),
135 HawDBError::FileDescriptors(error) => Display::fmt(error, f),
136 HawDBError::StorageIntegrity(message) => {
137 write!(f, "storage integrity error: {message}")
138 }
139 HawDBError::Execution(message) => write!(f, "execution error: {message}"),
140 HawDBError::ReadBudgetExceeded(error) => write!(f, "execution error: {error}"),
141 HawDBError::GraphExpansionCandidateLimitExceeded { requested, limit } => write!(
142 f,
143 "graph expansion candidate_limit exceeded: requested {requested} rows, limit {limit} rows; configure ExecutionMemoryConfig.graph_expansion_budget.candidate_limit"
144 ),
145 HawDBError::GraphExpansionPayloadLimitExceeded { requested, limit } => write!(
146 f,
147 "graph expansion payload_byte_limit exceeded: requested {requested} bytes, limit {limit} bytes; configure ExecutionMemoryConfig.graph_expansion_budget.payload_byte_limit"
148 ),
149 HawDBError::TransactionConflict {
150 read_epoch,
151 committed_epoch,
152 key,
153 } => write!(
154 f,
155 "transaction conflict on {key}: transaction read epoch {read_epoch}, conflicting commit epoch {committed_epoch}"
156 ),
157 HawDBError::AppendSequenceExhausted {
158 table,
159 watermark,
160 requested,
161 } => write!(
162 f,
163 "append commit sequence exhausted for table {table}: watermark={watermark}, requested={requested}"
164 ),
165 HawDBError::CapabilityUnavailable { capability } => {
166 write!(f, "capability unavailable: {}", capability.as_str())
167 }
168 HawDBError::BranchCommandUnsupported { command, context } => {
169 write!(f, "{command} is unavailable in {context}")
170 }
171 HawDBError::BranchBusy { resource } => write!(f, "branch is busy: {resource}"),
172 HawDBError::BranchReclamationBlocked {
173 branch_id,
174 catalog_revision,
175 metadata_revision,
176 cause,
177 } => write!(
178 f,
179 "pending branch {branch_id} blocks reclamation at catalog revision {catalog_revision}, branch revision {metadata_revision}: {cause}"
180 ),
181 }
182 }
183}
184
185impl std::error::Error for HawDBError {
186 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
187 match self {
188 Self::FileDescriptors(error) => Some(error),
189 Self::ReadBudgetExceeded(error) => Some(error),
190 Self::BranchReclamationBlocked { cause, .. } => Some(cause.as_ref()),
191 _ => None,
192 }
193 }
194}
195
196impl HawDBError {
197 pub fn read_budget_exceeded(
198 resource: ReadBudgetResource,
199 limit: usize,
200 message: impl Into<String>,
201 ) -> Self {
202 Self::ReadBudgetExceeded(ReadBudgetError {
203 resource,
204 limit,
205 message: message.into(),
206 })
207 }
208
209 pub fn from_storage_error(error: impl std::error::Error + 'static) -> Self {
211 if let Some(error) = (&error as &dyn std::error::Error).downcast_ref::<Self>() {
212 return error.clone();
213 }
214 if let Some(resource) = file_descriptor_error(&error) {
215 Self::FileDescriptors(resource)
216 } else {
217 Self::Storage(error.to_string())
218 }
219 }
220
221 pub fn from_storage_error_with_context(
225 error: impl std::error::Error + 'static,
226 context: &str,
227 ) -> Self {
228 match Self::from_storage_error(error) {
229 Self::Storage(message) => Self::Storage(format!("{context}: {message}")),
230 error => error,
231 }
232 }
233
234 pub const fn is_retryable_transaction_conflict(&self) -> bool {
235 matches!(self, Self::TransactionConflict { .. })
236 }
237}
238
239pub type Result<T> = std::result::Result<T, HawDBError>;
240
241impl From<std::io::Error> for HawDBError {
242 fn from(error: std::io::Error) -> Self {
243 Self::from_storage_error(error)
244 }
245}
246
247pub fn file_descriptor_error(
248 error: &(dyn std::error::Error + 'static),
249) -> Option<FileDescriptorError> {
250 let mut current = Some(error);
251 while let Some(error) = current {
252 if let Some(resource) = error.downcast_ref::<FileDescriptorError>() {
253 return Some(resource.clone());
254 }
255 if let Some(HawDBError::FileDescriptors(resource)) = error.downcast_ref::<HawDBError>() {
256 return Some(resource.clone());
257 }
258 if let Some(error) = error.downcast_ref::<std::io::Error>() {
259 if let Some(code) = error.raw_os_error()
260 && descriptor_os_limit(code)
261 {
262 return Some(FileDescriptorError::OsLimit {
263 requested: 1,
264 os_code: Some(code),
265 soft: None,
266 hard: None,
267 });
268 }
269 if let Some(inner) = error.get_ref()
272 && let Some(resource) = file_descriptor_error(inner)
273 {
274 return Some(resource);
275 }
276 }
277 current = error.source();
278 }
279 None
280}
281
282fn descriptor_os_limit(code: i32) -> bool {
283 #[cfg(unix)]
284 {
285 matches!(code, 23 | 24)
286 }
287 #[cfg(windows)]
288 {
289 code == 4
290 }
291 #[cfg(not(any(unix, windows)))]
292 {
293 let _ = code;
294 false
295 }
296}