reddb_server/storage/cache/strategy.rs
1//! Buffer access strategies for the page cache.
2//!
3//! Mirrors PostgreSQL's `BufferAccessStrategy` (src/backend/storage/buffer/freelist.c)
4//! — a hint passed by callers to tell the cache that a particular access
5//! pattern (sequential scan, bulk read, bulk write) should NOT pollute
6//! the main hot pool. Strategy-tagged accesses go through a small
7//! dedicated ring instead.
8//!
9//! The `Normal` strategy is the default and uses the main SIEVE pool
10//! exactly as before. Sequential scans, full table scans, vector batch
11//! scans, timeseries chunk iteration, and backup/export should pass
12//! `SequentialScan` / `BulkRead` / `BulkWrite` to spare the main pool.
13//!
14//! See `src/storage/cache/README.md` § Invariants 4 for the rules.
15
16/// How a caller intends to access the page cache.
17///
18/// `Normal` is the default — pages flow through the main SIEVE pool.
19/// Other variants route through small dedicated rings sized to keep
20/// scan workloads out of the main pool's working set.
21#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
22pub enum BufferAccessStrategy {
23 /// Use the main SIEVE pool. Default for OLTP-style point access.
24 #[default]
25 Normal,
26 /// Sequential scan over a known-large relation. 16-page ring.
27 SequentialScan,
28 /// Bulk read (vector batch, timeseries chunk iter, backup export).
29 /// 32-page ring.
30 BulkRead,
31 /// Bulk write (initial load, restore). 32-page ring; dirty pages
32 /// flushed through the pager on eviction.
33 BulkWrite,
34}
35
36impl BufferAccessStrategy {
37 /// Ring capacity for non-`Normal` strategies, or `None` when the
38 /// caller should use the main pool directly.
39 pub fn ring_size(self) -> Option<usize> {
40 match self {
41 Self::Normal => None,
42 Self::SequentialScan => Some(16),
43 Self::BulkRead | Self::BulkWrite => Some(32),
44 }
45 }
46
47 /// True iff the strategy uses a ring buffer (i.e. is non-`Normal`).
48 pub fn is_ring(self) -> bool {
49 self.ring_size().is_some()
50 }
51
52 /// True iff the strategy expects writes that must flush dirty
53 /// pages on eviction. Currently only `BulkWrite`.
54 pub fn is_write(self) -> bool {
55 matches!(self, Self::BulkWrite)
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62
63 #[test]
64 fn default_is_normal() {
65 assert_eq!(
66 BufferAccessStrategy::default(),
67 BufferAccessStrategy::Normal
68 );
69 }
70
71 #[test]
72 fn ring_size_matches_strategy() {
73 assert_eq!(BufferAccessStrategy::Normal.ring_size(), None);
74 assert_eq!(BufferAccessStrategy::SequentialScan.ring_size(), Some(16));
75 assert_eq!(BufferAccessStrategy::BulkRead.ring_size(), Some(32));
76 assert_eq!(BufferAccessStrategy::BulkWrite.ring_size(), Some(32));
77 }
78
79 #[test]
80 fn is_ring_true_for_non_normal() {
81 assert!(!BufferAccessStrategy::Normal.is_ring());
82 assert!(BufferAccessStrategy::SequentialScan.is_ring());
83 assert!(BufferAccessStrategy::BulkRead.is_ring());
84 assert!(BufferAccessStrategy::BulkWrite.is_ring());
85 }
86
87 #[test]
88 fn is_write_only_for_bulk_write() {
89 assert!(!BufferAccessStrategy::Normal.is_write());
90 assert!(!BufferAccessStrategy::SequentialScan.is_write());
91 assert!(!BufferAccessStrategy::BulkRead.is_write());
92 assert!(BufferAccessStrategy::BulkWrite.is_write());
93 }
94}