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