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