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