Skip to main content

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}