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