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