Skip to main content

hawdb_core/
error.rs

1// Copyright 2026 Nowledge
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::RuntimeCapability;
16use std::fmt::{Display, Formatter};
17
18/// Descriptor admission failures are distinct from corrupt storage or a busy
19/// branch. Project quota counts belong to one engine resource domain; OS-limit
20/// failures can additionally report the process allowance when it is known.
21#[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/// The result resource whose selected read limit rejected delivery.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum ReadBudgetResource {
62    Rows,
63    PayloadBytes,
64}
65
66/// Retains budget identity independently of the execution diagnostic text.
67#[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    /// Preserve a descriptor rejection through nested storage error wrappers.
192    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    /// `from_storage_error`, prefixing a generic `Storage` message with
204    /// `context` and leaving a preserved typed cause (e.g. `FileDescriptors`)
205    /// unprefixed and unchanged.
206    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            // io::Error::source can skip its boxed concrete error. Inspect it
252            // explicitly so typed capacity rejections survive each IO layer.
253            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}