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}
120
121impl Display for HawDBError {
122 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
123 match self {
124 HawDBError::Parse(message) => write!(f, "parse error: {message}"),
125 HawDBError::Semantic(message) => write!(f, "semantic error: {message}"),
126 HawDBError::Storage(message) => write!(f, "storage error: {message}"),
127 HawDBError::FileDescriptors(error) => Display::fmt(error, f),
128 HawDBError::StorageIntegrity(message) => {
129 write!(f, "storage integrity error: {message}")
130 }
131 HawDBError::Execution(message) => write!(f, "execution error: {message}"),
132 HawDBError::ReadBudgetExceeded(error) => write!(f, "execution error: {error}"),
133 HawDBError::GraphExpansionCandidateLimitExceeded { requested, limit } => write!(
134 f,
135 "graph expansion candidate_limit exceeded: requested {requested} rows, limit {limit} rows; configure ExecutionMemoryConfig.graph_expansion_budget.candidate_limit"
136 ),
137 HawDBError::GraphExpansionPayloadLimitExceeded { requested, limit } => write!(
138 f,
139 "graph expansion payload_byte_limit exceeded: requested {requested} bytes, limit {limit} bytes; configure ExecutionMemoryConfig.graph_expansion_budget.payload_byte_limit"
140 ),
141 HawDBError::TransactionConflict {
142 read_epoch,
143 committed_epoch,
144 key,
145 } => write!(
146 f,
147 "transaction conflict on {key}: transaction read epoch {read_epoch}, conflicting commit epoch {committed_epoch}"
148 ),
149 HawDBError::AppendSequenceExhausted {
150 table,
151 watermark,
152 requested,
153 } => write!(
154 f,
155 "append commit sequence exhausted for table {table}: watermark={watermark}, requested={requested}"
156 ),
157 HawDBError::CapabilityUnavailable { capability } => {
158 write!(f, "capability unavailable: {}", capability.as_str())
159 }
160 HawDBError::BranchCommandUnsupported { command, context } => {
161 write!(f, "{command} is unavailable in {context}")
162 }
163 HawDBError::BranchBusy { resource } => write!(f, "branch is busy: {resource}"),
164 }
165 }
166}
167
168impl std::error::Error for HawDBError {
169 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
170 match self {
171 Self::FileDescriptors(error) => Some(error),
172 Self::ReadBudgetExceeded(error) => Some(error),
173 _ => None,
174 }
175 }
176}
177
178impl HawDBError {
179 pub fn read_budget_exceeded(
180 resource: ReadBudgetResource,
181 limit: usize,
182 message: impl Into<String>,
183 ) -> Self {
184 Self::ReadBudgetExceeded(ReadBudgetError {
185 resource,
186 limit,
187 message: message.into(),
188 })
189 }
190
191 pub fn from_storage_error(error: impl std::error::Error + 'static) -> Self {
193 if let Some(error) = (&error as &dyn std::error::Error).downcast_ref::<Self>() {
194 return error.clone();
195 }
196 if let Some(resource) = file_descriptor_error(&error) {
197 Self::FileDescriptors(resource)
198 } else {
199 Self::Storage(error.to_string())
200 }
201 }
202
203 pub fn from_storage_error_with_context(
207 error: impl std::error::Error + 'static,
208 context: &str,
209 ) -> Self {
210 match Self::from_storage_error(error) {
211 Self::Storage(message) => Self::Storage(format!("{context}: {message}")),
212 error => error,
213 }
214 }
215
216 pub const fn is_retryable_transaction_conflict(&self) -> bool {
217 matches!(self, Self::TransactionConflict { .. })
218 }
219}
220
221pub type Result<T> = std::result::Result<T, HawDBError>;
222
223impl From<std::io::Error> for HawDBError {
224 fn from(error: std::io::Error) -> Self {
225 Self::from_storage_error(error)
226 }
227}
228
229pub fn file_descriptor_error(
230 error: &(dyn std::error::Error + 'static),
231) -> Option<FileDescriptorError> {
232 let mut current = Some(error);
233 while let Some(error) = current {
234 if let Some(resource) = error.downcast_ref::<FileDescriptorError>() {
235 return Some(resource.clone());
236 }
237 if let Some(HawDBError::FileDescriptors(resource)) = error.downcast_ref::<HawDBError>() {
238 return Some(resource.clone());
239 }
240 if let Some(error) = error.downcast_ref::<std::io::Error>() {
241 if let Some(code) = error.raw_os_error()
242 && descriptor_os_limit(code)
243 {
244 return Some(FileDescriptorError::OsLimit {
245 requested: 1,
246 os_code: Some(code),
247 soft: None,
248 hard: None,
249 });
250 }
251 if let Some(inner) = error.get_ref()
254 && let Some(resource) = file_descriptor_error(inner)
255 {
256 return Some(resource);
257 }
258 }
259 current = error.source();
260 }
261 None
262}
263
264fn descriptor_os_limit(code: i32) -> bool {
265 #[cfg(unix)]
266 {
267 matches!(code, 23 | 24)
268 }
269 #[cfg(windows)]
270 {
271 code == 4
272 }
273 #[cfg(not(any(unix, windows)))]
274 {
275 let _ = code;
276 false
277 }
278}