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#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum HawDBError {
61    Parse(String),
62    Semantic(String),
63    Storage(String),
64    FileDescriptors(FileDescriptorError),
65    StorageIntegrity(String),
66    Execution(String),
67    TransactionConflict {
68        read_epoch: u64,
69        committed_epoch: u64,
70        key: String,
71    },
72    AppendSequenceExhausted {
73        table: String,
74        watermark: i64,
75        requested: usize,
76    },
77    CapabilityUnavailable {
78        capability: RuntimeCapability,
79    },
80    BranchCommandUnsupported {
81        command: &'static str,
82        context: &'static str,
83    },
84    BranchBusy {
85        resource: &'static str,
86    },
87}
88
89impl Display for HawDBError {
90    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
91        match self {
92            HawDBError::Parse(message) => write!(f, "parse error: {message}"),
93            HawDBError::Semantic(message) => write!(f, "semantic error: {message}"),
94            HawDBError::Storage(message) => write!(f, "storage error: {message}"),
95            HawDBError::FileDescriptors(error) => Display::fmt(error, f),
96            HawDBError::StorageIntegrity(message) => {
97                write!(f, "storage integrity error: {message}")
98            }
99            HawDBError::Execution(message) => write!(f, "execution error: {message}"),
100            HawDBError::TransactionConflict {
101                read_epoch,
102                committed_epoch,
103                key,
104            } => write!(
105                f,
106                "transaction conflict on {key}: transaction read epoch {read_epoch}, conflicting commit epoch {committed_epoch}"
107            ),
108            HawDBError::AppendSequenceExhausted {
109                table,
110                watermark,
111                requested,
112            } => write!(
113                f,
114                "append commit sequence exhausted for table {table}: watermark={watermark}, requested={requested}"
115            ),
116            HawDBError::CapabilityUnavailable { capability } => {
117                write!(f, "capability unavailable: {}", capability.as_str())
118            }
119            HawDBError::BranchCommandUnsupported { command, context } => {
120                write!(f, "{command} is unavailable in {context}")
121            }
122            HawDBError::BranchBusy { resource } => write!(f, "branch is busy: {resource}"),
123        }
124    }
125}
126
127impl std::error::Error for HawDBError {
128    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
129        match self {
130            Self::FileDescriptors(error) => Some(error),
131            _ => None,
132        }
133    }
134}
135
136impl HawDBError {
137    /// Preserve a descriptor rejection through nested storage error wrappers.
138    pub fn from_storage_error(error: impl std::error::Error + 'static) -> Self {
139        if let Some(error) = (&error as &dyn std::error::Error).downcast_ref::<Self>() {
140            return error.clone();
141        }
142        if let Some(resource) = file_descriptor_error(&error) {
143            Self::FileDescriptors(resource)
144        } else {
145            Self::Storage(error.to_string())
146        }
147    }
148
149    /// `from_storage_error`, prefixing a generic `Storage` message with
150    /// `context` and leaving a preserved typed cause (e.g. `FileDescriptors`)
151    /// unprefixed and unchanged.
152    pub fn from_storage_error_with_context(
153        error: impl std::error::Error + 'static,
154        context: &str,
155    ) -> Self {
156        match Self::from_storage_error(error) {
157            Self::Storage(message) => Self::Storage(format!("{context}: {message}")),
158            error => error,
159        }
160    }
161
162    pub const fn is_retryable_transaction_conflict(&self) -> bool {
163        matches!(self, Self::TransactionConflict { .. })
164    }
165}
166
167pub type Result<T> = std::result::Result<T, HawDBError>;
168
169impl From<std::io::Error> for HawDBError {
170    fn from(error: std::io::Error) -> Self {
171        Self::from_storage_error(error)
172    }
173}
174
175pub fn file_descriptor_error(
176    error: &(dyn std::error::Error + 'static),
177) -> Option<FileDescriptorError> {
178    let mut current = Some(error);
179    while let Some(error) = current {
180        if let Some(resource) = error.downcast_ref::<FileDescriptorError>() {
181            return Some(resource.clone());
182        }
183        if let Some(HawDBError::FileDescriptors(resource)) = error.downcast_ref::<HawDBError>() {
184            return Some(resource.clone());
185        }
186        if let Some(error) = error.downcast_ref::<std::io::Error>() {
187            if let Some(code) = error.raw_os_error()
188                && descriptor_os_limit(code)
189            {
190                return Some(FileDescriptorError::OsLimit {
191                    requested: 1,
192                    os_code: Some(code),
193                    soft: None,
194                    hard: None,
195                });
196            }
197            // io::Error::source can skip its boxed concrete error. Inspect it
198            // explicitly so typed capacity rejections survive each IO layer.
199            if let Some(inner) = error.get_ref()
200                && let Some(resource) = file_descriptor_error(inner)
201            {
202                return Some(resource);
203            }
204        }
205        current = error.source();
206    }
207    None
208}
209
210fn descriptor_os_limit(code: i32) -> bool {
211    #[cfg(unix)]
212    {
213        matches!(code, 23 | 24)
214    }
215    #[cfg(windows)]
216    {
217        code == 4
218    }
219    #[cfg(not(any(unix, windows)))]
220    {
221        let _ = code;
222        false
223    }
224}