Skip to main content

hawdb_core/error/
resource.rs

1// Copyright 2026 Nowledge
2// SPDX-License-Identifier: Apache-2.0
3
4use std::fmt::{Display, Formatter};
5
6/// Units of an executor resource reservation.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum ResourceBudgetKind {
9    MemoryBytes,
10    SpillBytes,
11    SpillRuns,
12    MetadataEntries,
13}
14
15impl ResourceBudgetKind {
16    pub const fn as_str(self) -> &'static str {
17        match self {
18            Self::MemoryBytes => "memory_bytes",
19            Self::SpillBytes => "spill_bytes",
20            Self::SpillRuns => "spill_runs",
21            Self::MetadataEntries => "metadata_entries",
22        }
23    }
24}
25
26/// The boundary that refused a reservation, rather than the caller's stage.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum ResourceBudgetScope {
29    Account,
30    Query,
31    Operator,
32    SharedPool,
33    FileSystem,
34}
35
36impl ResourceBudgetScope {
37    pub const fn as_str(self) -> &'static str {
38        match self {
39            Self::Account => "account",
40            Self::Query => "query",
41            Self::Operator => "operator",
42            Self::SharedPool => "shared_pool",
43            Self::FileSystem => "file_system",
44        }
45    }
46}
47
48/// A rejected resource reservation. Rejection does not charge the reservation.
49///
50/// `requested` is the additional charge, not the resulting total. For an
51/// ownership transfer, the query-level charge is the net increase. `used` and
52/// `budget` describe the checked boundary in the resource's units;
53/// `used + requested` need not fit in `u64`.
54///
55/// `budget_option` identifies the configuration option, runtime grant, or
56/// constructor argument defining the effective boundary. Raising a configured
57/// maximum does not widen a smaller runtime grant. A filesystem free-space
58/// reserve is a minimum to preserve, not a maximum to raise.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct ResourceBudgetError {
61    pub resource: ResourceBudgetKind,
62    pub scope: ResourceBudgetScope,
63    pub owner: String,
64    pub memory_class: Option<&'static str>,
65    pub requested: u64,
66    pub used: u64,
67    pub budget: u64,
68    pub budget_option: &'static str,
69    /// The existing execution diagnostic, without its outer error prefix.
70    pub message: String,
71}
72
73impl Display for ResourceBudgetError {
74    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
75        formatter.write_str(&self.message)
76    }
77}
78
79impl std::error::Error for ResourceBudgetError {}
80
81pub(super) fn find_resource_budget_error(
82    error: &(dyn std::error::Error + 'static),
83) -> Option<ResourceBudgetError> {
84    let mut current = Some(error);
85    while let Some(error) = current {
86        if let Some(resource) = error.downcast_ref::<ResourceBudgetError>() {
87            return Some(resource.clone());
88        }
89        // io::Error can hide its immediate boxed cause from Error::source.
90        if let Some(error) = error.downcast_ref::<std::io::Error>()
91            && let Some(inner) = error.get_ref()
92            && let Some(resource) = find_resource_budget_error(inner)
93        {
94            return Some(resource);
95        }
96        current = error.source();
97    }
98    None
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use crate::HawDBError;
105
106    #[test]
107    fn resource_metadata_survives_nested_io_and_storage_contexts() {
108        let cause = ResourceBudgetError {
109            resource: ResourceBudgetKind::MemoryBytes,
110            scope: ResourceBudgetScope::Account,
111            owner: "source".into(),
112            memory_class: Some("external_read"),
113            requested: 8,
114            used: 4,
115            budget: 10,
116            budget_option: "source grant",
117            message: "source budget refused".into(),
118        };
119        let expected = HawDBError::from(cause.clone());
120        for inner in [
121            std::io::Error::other(cause),
122            std::io::Error::other(expected.clone()),
123        ] {
124            let nested = std::io::Error::other(std::io::Error::other(inner));
125            assert_eq!(
126                HawDBError::from_storage_error_with_context(nested, "read node"),
127                expected
128            );
129        }
130        assert!(matches!(
131            HawDBError::from_storage_error(std::io::Error::other("ordinary I/O failure")),
132            HawDBError::Storage(_)
133        ));
134    }
135}