Skip to main content

mkit_server/sql/
capacity.rs

1//! The storage cap of a [`super::SqlKvStore`]: a hard size limit the engine
2//! enforces, and a soft limit below it where batches that add data stop.
3//!
4//! **Why a reserve.** Deleting from a `WITHOUT ROWID` b-tree can *grow* it:
5//! removing a cell from an interior page promotes a replacement divider,
6//! which may be longer (keys run from 1 to
7//! [`MAX_KEY_BYTES`](crate::store::MAX_KEY_BYTES) bytes) and split the page
8//! and its ancestors. A store filled to its engine limit
9//! (`SQLite` `max_page_count`, a Durable Object's 10 GB) can therefore
10//! fail a delete with `SQLITE_FULL`, which breaks normative rule 7 (a
11//! delete-only batch never returns `Full`, so pruning always runs). The
12//! store keeps a reserve free: a batch with a put returns
13//! [`StoreError::Full`](crate::StoreError::Full) once the bytes in use
14//! reach `cap - reserve`, and delete-only batches are never refused.
15//!
16//! **The reserve formula.** One batch holds at most [`MAX_BATCH_OPS`]
17//! operations. Each can split one page on every level of its path plus a
18//! new root: `depth + 1` pages. `SQLite` keeps at least four cells on an
19//! interior page (a longer cell overflows), so a depth of
20//! [`RESERVE_TREE_DEPTH`] = 20 covers `4^20` pages, beyond any database.
21//! A batch also writes at most [`MAX_BATCH_BYTES`] of payload; doubling it
22//! covers overflow-page and cell overhead. So one batch grows the database
23//! by at most [`batch_growth_bytes`]`(page) = (MAX_BATCH_OPS × 21 + 2 ×
24//! MAX_BATCH_BYTES / page) × page`, 10.2 MiB at 4 KiB pages. The reserve
25//! must hold two such batches: a put batch that starts just below the soft
26//! limit, then a delete-only batch. [`reserve_floor`] is that, 20.4 MiB at
27//! 4 KiB pages. The default reserve is the larger of that floor and 1/64
28//! of the cap (160 MiB of a Durable Object's 10 GB), which also absorbs a
29//! long run of prune batches and journal overhead the model ignores.
30//! Pages freed by deletes go to the free list, and later splits reuse them
31//! first, so pruning mostly consumes no new pages.
32
33use crate::store::{MAX_BATCH_BYTES, MAX_BATCH_OPS};
34
35/// The page size the default reserve assumes: `SQLite`'s default and a
36/// Durable Object's. A database with larger pages sets its reserve with
37/// [`Capacity::with_reserve`] and [`reserve_floor`].
38pub const DEFAULT_PAGE_SIZE: u64 = 4096;
39
40/// The b-tree depth the reserve plans for.
41pub const RESERVE_TREE_DEPTH: u64 = 20;
42
43/// The most one batch can grow a database with `page_size`-byte pages (see
44/// the module docs).
45#[must_use]
46pub const fn batch_growth_bytes(page_size: u64) -> u64 {
47    let split_pages = MAX_BATCH_OPS as u64 * (RESERVE_TREE_DEPTH + 1);
48    let payload_pages = 2 * MAX_BATCH_BYTES as u64 / page_size;
49    (split_pages + payload_pages) * page_size
50}
51
52/// The smallest safe reserve: two batches' growth (a put batch that starts
53/// just below the soft limit, then a delete-only batch).
54#[must_use]
55pub const fn reserve_floor(page_size: u64) -> u64 {
56    2 * batch_growth_bytes(page_size)
57}
58
59/// A store's size cap.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub struct Capacity {
62    cap_bytes: u64,
63    reserve_bytes: u64,
64}
65
66impl Capacity {
67    /// A hard cap of `cap_bytes` (the engine limit: natively applied as
68    /// `max_page_count`, on a Durable Object its fixed 10 GB) with the
69    /// default reserve, `max(reserve_floor(4096), cap_bytes / 64)`.
70    #[must_use]
71    pub const fn new(cap_bytes: u64) -> Self {
72        let floor = reserve_floor(DEFAULT_PAGE_SIZE);
73        let fraction = cap_bytes / 64;
74        Self {
75            cap_bytes,
76            reserve_bytes: if fraction > floor { fraction } else { floor },
77        }
78    }
79
80    /// Keep `reserve_bytes` free instead of the default. Below
81    /// [`reserve_floor`] a delete may fail at the engine limit.
82    #[must_use]
83    pub const fn with_reserve(mut self, reserve_bytes: u64) -> Self {
84        self.reserve_bytes = reserve_bytes;
85        self
86    }
87
88    /// The hard cap.
89    #[must_use]
90    pub const fn cap_bytes(&self) -> u64 {
91        self.cap_bytes
92    }
93
94    /// The reserve kept free below the hard cap.
95    #[must_use]
96    pub const fn reserve_bytes(&self) -> u64 {
97        self.reserve_bytes
98    }
99
100    /// Where batches with a put start returning `Full`: `cap - reserve`.
101    #[must_use]
102    pub const fn soft_limit(&self) -> u64 {
103        self.cap_bytes.saturating_sub(self.reserve_bytes)
104    }
105}