Skip to main content

reddb_server/runtime/
snapshot_reuse.rs

1//! Snapshot reuse coordinator — Phase 5 / PLAN.md backlog 3.4.
2//!
3//! Caches the current MVCC snapshot per session and lets
4//! successive read-only queries reuse it without round-tripping
5//! through the transaction manager. Invalidates on any write
6//! visible to the session.
7//!
8//! Mirrors PG's `xact_completion` counter pattern:
9//!
10//! - Every commit / abort bumps a global atomic counter.
11//! - Each session caches `(snapshot, last_seen_counter)`.
12//! - On the next read-only query, compare `last_seen_counter`
13//!   to the global counter. If unchanged, reuse the cached
14//!   snapshot. If incremented, invalidate and refetch.
15//!
16//! ## Why this matters
17//!
18//! reddb's snapshot fetch goes through the dormant
19//! `MvccCoordinator` which serialises behind the global
20//! transaction lock. For OLTP-style workloads with many
21//! independent read-only queries, the lock is the bottleneck.
22//! Snapshot reuse short-circuits the lock when nothing has
23//! changed since the last fetch.
24//!
25//! ## Wiring
26//!
27//! Not yet called by `runtime/impl_core.rs::execute_query`.
28//! Phase 5 wiring adds:
29//! 1. A `SnapshotCache` field on `RuntimeSession` (or
30//!    equivalent per-session struct).
31//! 2. The dispatch loop checks `cache.try_reuse(global_counter)`
32//!    before the snapshot fetch and falls back to the slow
33//!    path when the counter has advanced.
34//! 3. Every commit/abort path bumps `global_counter` via
35//!    `bump_completion_counter()`.
36
37use std::sync::atomic::{AtomicU64, Ordering};
38
39/// Process-wide xact_completion counter. Bumped on every
40/// commit or abort. Sessions compare against their last-seen
41/// snapshot to decide whether to reuse.
42pub static GLOBAL_COMPLETION_COUNTER: AtomicU64 = AtomicU64::new(0);
43
44/// Increment the global counter. Called from the transaction
45/// manager's commit / abort paths.
46///
47/// Returns the new counter value so callers can stash it for
48/// observability / debugging.
49pub fn bump_completion_counter() -> u64 {
50    GLOBAL_COMPLETION_COUNTER.fetch_add(1, Ordering::Release) + 1
51}
52
53/// Snapshot identifier — opaque from this module's perspective.
54/// reddb's actual snapshot type lives in
55/// `storage::transaction::coordinator::Snapshot`; we keep this
56/// generic so the cache doesn't bring the whole MVCC graph
57/// into its dependency footprint.
58#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
59pub struct SnapshotId(pub u64);
60
61/// Per-session snapshot cache. Owned by the session struct;
62/// not thread-shared (one session = one user, one cache).
63#[derive(Debug, Default)]
64pub struct SnapshotCache {
65    cached: Option<SnapshotId>,
66    last_seen_counter: u64,
67}
68
69impl SnapshotCache {
70    /// New empty cache. Initialises to "no cached snapshot,
71    /// last seen counter is 0".
72    pub fn new() -> Self {
73        Self::default()
74    }
75
76    /// Try to reuse the cached snapshot. Returns `Some(id)`
77    /// when (a) we have a cached snapshot AND (b) the global
78    /// counter hasn't advanced since we cached it. Returns
79    /// `None` otherwise — the caller must fetch a fresh one.
80    pub fn try_reuse(&self) -> Option<SnapshotId> {
81        let global = GLOBAL_COMPLETION_COUNTER.load(Ordering::Acquire);
82        if global == self.last_seen_counter {
83            self.cached
84        } else {
85            None
86        }
87    }
88
89    /// Stash a freshly-fetched snapshot. Records the current
90    /// global counter so a later `try_reuse` can validate it
91    /// hasn't been invalidated by an intervening commit.
92    pub fn cache(&mut self, snapshot: SnapshotId) {
93        self.cached = Some(snapshot);
94        self.last_seen_counter = GLOBAL_COMPLETION_COUNTER.load(Ordering::Acquire);
95    }
96
97    /// Force-invalidate the cache. Used when the session knows
98    /// it just performed a write — even if `try_reuse` would
99    /// otherwise return the cached snapshot, the write makes
100    /// it stale for self-visibility.
101    pub fn invalidate(&mut self) {
102        self.cached = None;
103        self.last_seen_counter = 0;
104    }
105
106    /// Diagnostic: how many bumps have we missed? Difference
107    /// between the global counter and our last-seen value.
108    /// Used by EXPLAIN / metrics to track reuse effectiveness.
109    pub fn staleness(&self) -> u64 {
110        let global = GLOBAL_COMPLETION_COUNTER.load(Ordering::Acquire);
111        global.saturating_sub(self.last_seen_counter)
112    }
113}