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}