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