Skip to main content

hyphae_storage/
limits.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use std::{
4    io,
5    time::{Duration, Instant},
6};
7
8use thiserror::Error;
9
10use crate::snapshot::SnapshotReadLimits;
11
12/// Complete finite policy for opening and maintaining one data directory.
13#[derive(Clone, Debug, Default, Eq, PartialEq)]
14pub struct StorageLimits {
15    /// Limits shared by directory validation, log recovery, and index replay.
16    pub recovery: RecoveryLimits,
17    /// Limits shared by snapshot creation and online compaction.
18    pub maintenance: MaintenanceLimits,
19}
20
21impl StorageLimits {
22    pub(crate) fn compatibility() -> Self {
23        Self {
24            recovery: RecoveryLimits::compatibility(),
25            maintenance: MaintenanceLimits::compatibility(),
26        }
27    }
28
29    pub(crate) fn validate(&self) -> Result<(), StorageLimitError> {
30        self.recovery.validate()?;
31        self.maintenance.validate()
32    }
33}
34
35/// Finite policy for one complete open/recovery operation.
36#[derive(Clone, Debug, Eq, PartialEq)]
37pub struct RecoveryLimits {
38    /// Cooperative end-to-end open/recovery timeout.
39    pub timeout: Duration,
40    /// Maximum entries inspected in any owned metadata directory.
41    pub max_directory_entries: u64,
42    /// Maximum complete active log segment length.
43    pub max_log_file_bytes: u64,
44    /// Maximum complete frames inspected in the active segment.
45    pub max_log_frames: u64,
46    /// Maximum unique committed transactions retained for replay.
47    pub max_transactions: u64,
48    /// Maximum operation frames retained across committed transactions.
49    pub max_operations: u64,
50    /// Maximum aggregate decoded operation payload bytes retained for replay.
51    pub max_decoded_operation_bytes: u64,
52    /// Snapshot file, logical-record, and decoded-byte limits during restore.
53    pub snapshot: SnapshotReadLimits,
54    /// Maximum durable documents inspected while rebuilding one lexical index.
55    pub max_lexical_documents: u64,
56    /// Maximum normalized tokens retained while rebuilding one lexical index.
57    pub max_lexical_tokens: u64,
58}
59
60impl Default for RecoveryLimits {
61    fn default() -> Self {
62        Self {
63            timeout: Duration::from_secs(60),
64            max_directory_entries: 1_000_000,
65            max_log_file_bytes: 2 * 1024 * 1024 * 1024,
66            max_log_frames: 1_000_000,
67            max_transactions: 1_000_000,
68            max_operations: 1_000_000,
69            max_decoded_operation_bytes: 1024 * 1024 * 1024,
70            snapshot: SnapshotReadLimits::default(),
71            max_lexical_documents: 1_000_000,
72            max_lexical_tokens: 10_000_000,
73        }
74    }
75}
76
77impl RecoveryLimits {
78    pub(crate) fn compatibility() -> Self {
79        Self {
80            timeout: Duration::MAX,
81            max_directory_entries: u64::MAX,
82            max_log_file_bytes: u64::MAX,
83            max_log_frames: u64::MAX,
84            max_transactions: u64::MAX,
85            max_operations: u64::MAX,
86            max_decoded_operation_bytes: u64::MAX,
87            snapshot: SnapshotReadLimits {
88                file_bytes: u64::MAX,
89                entries: u64::MAX,
90                decoded_bytes: u64::MAX,
91            },
92            max_lexical_documents: u64::MAX,
93            max_lexical_tokens: u64::MAX,
94        }
95    }
96
97    pub(crate) fn validate(&self) -> Result<(), StorageLimitError> {
98        require_nonzero_duration(self.timeout, "recovery.timeout")?;
99        for (name, value) in [
100            ("recovery.max_directory_entries", self.max_directory_entries),
101            ("recovery.max_log_file_bytes", self.max_log_file_bytes),
102            ("recovery.max_log_frames", self.max_log_frames),
103            ("recovery.max_transactions", self.max_transactions),
104            ("recovery.max_operations", self.max_operations),
105            (
106                "recovery.max_decoded_operation_bytes",
107                self.max_decoded_operation_bytes,
108            ),
109            ("recovery.snapshot.file_bytes", self.snapshot.file_bytes),
110            ("recovery.snapshot.entries", self.snapshot.entries),
111            (
112                "recovery.snapshot.decoded_bytes",
113                self.snapshot.decoded_bytes,
114            ),
115            ("recovery.max_lexical_documents", self.max_lexical_documents),
116            ("recovery.max_lexical_tokens", self.max_lexical_tokens),
117        ] {
118            require_nonzero(value, name)?;
119        }
120        Ok(())
121    }
122}
123
124/// Finite policy for one snapshot or online compaction operation.
125#[derive(Clone, Debug, Eq, PartialEq)]
126pub struct MaintenanceLimits {
127    /// Cooperative end-to-end snapshot/compaction timeout.
128    pub timeout: Duration,
129    /// Maximum snapshot file, logical records, and decoded logical bytes.
130    pub snapshot: SnapshotReadLimits,
131}
132
133impl Default for MaintenanceLimits {
134    fn default() -> Self {
135        Self {
136            timeout: Duration::from_secs(60),
137            snapshot: SnapshotReadLimits::default(),
138        }
139    }
140}
141
142impl MaintenanceLimits {
143    fn compatibility() -> Self {
144        Self {
145            timeout: Duration::MAX,
146            snapshot: SnapshotReadLimits {
147                file_bytes: u64::MAX,
148                entries: u64::MAX,
149                decoded_bytes: u64::MAX,
150            },
151        }
152    }
153
154    pub(crate) fn validate(&self) -> Result<(), StorageLimitError> {
155        require_nonzero_duration(self.timeout, "maintenance.timeout")?;
156        for (name, value) in [
157            ("maintenance.snapshot.file_bytes", self.snapshot.file_bytes),
158            ("maintenance.snapshot.entries", self.snapshot.entries),
159            (
160                "maintenance.snapshot.decoded_bytes",
161                self.snapshot.decoded_bytes,
162            ),
163        ] {
164            require_nonzero(value, name)?;
165        }
166        Ok(())
167    }
168}
169
170/// Failure of a finite storage recovery or maintenance policy.
171#[derive(Clone, Debug, Error, Eq, PartialEq)]
172pub enum StorageLimitError {
173    /// A configured bound is zero.
174    #[error("storage limit must be positive: {name}")]
175    ZeroLimit {
176        /// Stable field name.
177        name: &'static str,
178    },
179    /// The shared cooperative deadline expired.
180    #[error("storage operation timed out")]
181    TimedOut,
182    /// An owned metadata directory contains too many entries.
183    #[error("storage directory entry limit exceeded: {maximum}")]
184    DirectoryEntriesExceeded {
185        /// Configured maximum.
186        maximum: u64,
187    },
188    /// The active log file exceeds policy before scan.
189    #[error("log file byte limit exceeded: {actual} > {maximum}")]
190    LogFileBytesExceeded {
191        /// Observed file length.
192        actual: u64,
193        /// Configured maximum.
194        maximum: u64,
195    },
196    /// The active log contains too many complete frames.
197    #[error("log frame limit exceeded: {maximum}")]
198    LogFramesExceeded {
199        /// Configured maximum.
200        maximum: u64,
201    },
202    /// Recovery retained too many unique transactions.
203    #[error("recovery transaction limit exceeded: {maximum}")]
204    TransactionsExceeded {
205        /// Configured maximum.
206        maximum: u64,
207    },
208    /// Recovery retained too many operation frames.
209    #[error("recovery operation limit exceeded: {maximum}")]
210    OperationsExceeded {
211        /// Configured maximum.
212        maximum: u64,
213    },
214    /// Recovery retained too many aggregate operation payload bytes.
215    #[error("recovery decoded operation byte limit exceeded: {maximum}")]
216    DecodedOperationBytesExceeded {
217        /// Configured maximum.
218        maximum: u64,
219    },
220    /// A lexical rebuild inspected too many durable documents.
221    #[error("lexical rebuild document limit exceeded: {maximum}")]
222    LexicalDocumentsExceeded {
223        /// Configured maximum.
224        maximum: u64,
225    },
226    /// A lexical rebuild retained too many normalized tokens.
227    #[error("lexical rebuild token limit exceeded: {maximum}")]
228    LexicalTokensExceeded {
229        /// Configured maximum.
230        maximum: u64,
231    },
232}
233
234#[derive(Clone, Debug)]
235pub(crate) struct OperationDeadline {
236    started: Instant,
237    timeout: Duration,
238}
239
240impl OperationDeadline {
241    pub(crate) fn new(timeout: Duration) -> Self {
242        Self {
243            started: Instant::now(),
244            timeout,
245        }
246    }
247
248    pub(crate) fn check(&self) -> Result<(), StorageLimitError> {
249        if self.started.elapsed() >= self.timeout {
250            Err(StorageLimitError::TimedOut)
251        } else {
252            Ok(())
253        }
254    }
255}
256
257pub(crate) fn limit_io_error(source: StorageLimitError) -> io::Error {
258    let kind = if matches!(source, StorageLimitError::TimedOut) {
259        io::ErrorKind::TimedOut
260    } else {
261        io::ErrorKind::Other
262    };
263    io::Error::new(kind, source)
264}
265
266/// Recovers a typed finite-policy failure carried through an existing I/O
267/// error variant.
268///
269/// Limited storage entry points preserve the exhaustive public error enums
270/// published in 0.2.0 by retaining [`StorageLimitError`] as the I/O error
271/// source. Callers that need typed policy handling can use this helper while
272/// legacy exhaustive matches remain source-compatible.
273pub fn storage_limit_from_io(source: &io::Error) -> Option<&StorageLimitError> {
274    source
275        .get_ref()
276        .and_then(|source| source.downcast_ref::<StorageLimitError>())
277}
278
279fn require_nonzero(value: u64, name: &'static str) -> Result<(), StorageLimitError> {
280    if value == 0 {
281        Err(StorageLimitError::ZeroLimit { name })
282    } else {
283        Ok(())
284    }
285}
286
287fn require_nonzero_duration(value: Duration, name: &'static str) -> Result<(), StorageLimitError> {
288    if value.is_zero() {
289        Err(StorageLimitError::ZeroLimit { name })
290    } else {
291        Ok(())
292    }
293}