Skip to main content

kyu_storage/
frame.rs

1use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
2
3use crate::latch::RwLatch;
4use crate::page_id::{PAGE_SIZE, PageId};
5
6/// A buffer frame: a PAGE_SIZE block of memory with metadata for buffer management.
7///
8/// # Safety
9///
10/// The `data` pointer is allocated via `std::alloc` and must be freed via `Frame::drop`.
11/// Access to the data must be coordinated through:
12/// - `pin_count > 0` (frame is in use, cannot be evicted)
13/// - `latch` (read/write access to the page data)
14pub struct Frame {
15    /// Raw pointer to PAGE_SIZE bytes of aligned memory.
16    data: *mut u8,
17    /// Which page is currently loaded in this frame (encoded as u64).
18    page_id: AtomicU64,
19    /// Number of threads currently using this frame. >0 prevents eviction.
20    pin_count: AtomicU32,
21    /// Whether the frame has been modified since loading.
22    dirty: AtomicBool,
23    /// Whether the frame was recently accessed (for clock eviction).
24    recently_used: AtomicBool,
25    /// Reader-writer latch protecting the page data.
26    latch: RwLatch,
27}
28
29// Frame is safe to share across threads: all mutable state is atomic or latch-protected.
30unsafe impl Send for Frame {}
31unsafe impl Sync for Frame {}
32
33impl Frame {
34    /// Allocate a new frame with zeroed PAGE_SIZE memory.
35    pub fn new() -> Self {
36        let layout =
37            std::alloc::Layout::from_size_align(PAGE_SIZE, PAGE_SIZE).expect("invalid page layout");
38        // SAFETY: Layout is valid (non-zero, power-of-two alignment).
39        let data = unsafe { std::alloc::alloc_zeroed(layout) };
40        if data.is_null() {
41            std::alloc::handle_alloc_error(layout);
42        }
43        Self {
44            data,
45            page_id: AtomicU64::new(PageId::INVALID.to_u64()),
46            pin_count: AtomicU32::new(0),
47            dirty: AtomicBool::new(false),
48            recently_used: AtomicBool::new(false),
49            latch: RwLatch::new(),
50        }
51    }
52
53    /// Get a shared (read-only) reference to the page data.
54    ///
55    /// # Safety
56    ///
57    /// Caller must hold a shared latch or pin and ensure no writer exists.
58    pub unsafe fn data(&self) -> &[u8] {
59        // SAFETY: data was allocated with PAGE_SIZE bytes and is valid for the lifetime of Frame.
60        unsafe { std::slice::from_raw_parts(self.data, PAGE_SIZE) }
61    }
62
63    /// Get an exclusive (mutable) reference to the page data.
64    ///
65    /// # Safety
66    ///
67    /// Caller must hold an exclusive latch. The `&self` signature is
68    /// intentional: `Frame` uses interior mutability via a raw pointer,
69    /// coordinated by pin_count and latch.
70    #[allow(clippy::mut_from_ref)]
71    pub unsafe fn data_mut(&self) -> &mut [u8] {
72        // SAFETY: data was allocated with PAGE_SIZE bytes. Exclusive access is ensured by the caller.
73        unsafe { std::slice::from_raw_parts_mut(self.data, PAGE_SIZE) }
74    }
75
76    /// Get the page ID currently loaded in this frame.
77    pub fn page_id(&self) -> PageId {
78        PageId::from_u64(self.page_id.load(Ordering::Acquire))
79    }
80
81    /// Set the page ID for this frame.
82    pub fn set_page_id(&self, pid: PageId) {
83        self.page_id.store(pid.to_u64(), Ordering::Release);
84    }
85
86    /// Get the current pin count.
87    pub fn pin_count(&self) -> u32 {
88        self.pin_count.load(Ordering::Acquire)
89    }
90
91    /// Increment the pin count.
92    pub fn pin(&self) -> u32 {
93        self.pin_count.fetch_add(1, Ordering::AcqRel) + 1
94    }
95
96    /// Decrement the pin count.
97    pub fn unpin(&self) -> u32 {
98        let prev = self.pin_count.fetch_sub(1, Ordering::AcqRel);
99        debug_assert!(prev > 0, "unpin called on unpinned frame");
100        prev - 1
101    }
102
103    /// Check if the frame is pinned.
104    pub fn is_pinned(&self) -> bool {
105        self.pin_count() > 0
106    }
107
108    /// Check if the frame is dirty.
109    pub fn is_dirty(&self) -> bool {
110        self.dirty.load(Ordering::Acquire)
111    }
112
113    /// Mark the frame as dirty.
114    pub fn set_dirty(&self) {
115        self.dirty.store(true, Ordering::Release);
116    }
117
118    /// Clear the dirty flag.
119    pub fn clear_dirty(&self) {
120        self.dirty.store(false, Ordering::Release);
121    }
122
123    /// Check if the frame was recently used.
124    pub fn is_recently_used(&self) -> bool {
125        self.recently_used.load(Ordering::Relaxed)
126    }
127
128    /// Mark the frame as recently used.
129    pub fn set_recently_used(&self) {
130        self.recently_used.store(true, Ordering::Relaxed);
131    }
132
133    /// Clear the recently-used flag (second-chance eviction).
134    pub fn clear_recently_used(&self) {
135        self.recently_used.store(false, Ordering::Relaxed);
136    }
137
138    /// Get a reference to the latch.
139    pub fn latch(&self) -> &RwLatch {
140        &self.latch
141    }
142
143    /// Reset the frame to an empty state (for after eviction).
144    pub fn reset(&self) {
145        self.page_id
146            .store(PageId::INVALID.to_u64(), Ordering::Release);
147        self.dirty.store(false, Ordering::Release);
148        self.recently_used.store(false, Ordering::Relaxed);
149        // pin_count should already be 0 when resetting
150        debug_assert_eq!(self.pin_count(), 0);
151    }
152
153    /// Check if this frame holds a valid page.
154    pub fn has_valid_page(&self) -> bool {
155        self.page_id().is_valid()
156    }
157}
158
159impl Drop for Frame {
160    fn drop(&mut self) {
161        let layout =
162            std::alloc::Layout::from_size_align(PAGE_SIZE, PAGE_SIZE).expect("invalid page layout");
163        // SAFETY: data was allocated with this exact layout in Frame::new().
164        unsafe {
165            std::alloc::dealloc(self.data, layout);
166        }
167    }
168}
169
170impl Default for Frame {
171    fn default() -> Self {
172        Self::new()
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use crate::page_id::FileId;
180
181    #[test]
182    fn new_frame() {
183        let frame = Frame::new();
184        assert!(!frame.has_valid_page());
185        assert_eq!(frame.pin_count(), 0);
186        assert!(!frame.is_dirty());
187        assert!(!frame.is_recently_used());
188        assert!(!frame.is_pinned());
189    }
190
191    #[test]
192    fn data_is_zeroed() {
193        let frame = Frame::new();
194        // SAFETY: No concurrent access in this test.
195        let data = unsafe { frame.data() };
196        assert!(data.iter().all(|&b| b == 0));
197        assert_eq!(data.len(), PAGE_SIZE);
198    }
199
200    #[test]
201    fn write_and_read_data() {
202        let frame = Frame::new();
203        // SAFETY: No concurrent access in this test.
204        unsafe {
205            let data = frame.data_mut();
206            data[0] = 0xDE;
207            data[1] = 0xAD;
208            data[PAGE_SIZE - 1] = 0xFF;
209        }
210        let data = unsafe { frame.data() };
211        assert_eq!(data[0], 0xDE);
212        assert_eq!(data[1], 0xAD);
213        assert_eq!(data[PAGE_SIZE - 1], 0xFF);
214    }
215
216    #[test]
217    fn set_page_id() {
218        let frame = Frame::new();
219        let pid = PageId::new(FileId(1), 42);
220        frame.set_page_id(pid);
221        assert_eq!(frame.page_id(), pid);
222        assert!(frame.has_valid_page());
223    }
224
225    #[test]
226    fn pin_and_unpin() {
227        let frame = Frame::new();
228        assert_eq!(frame.pin(), 1);
229        assert_eq!(frame.pin(), 2);
230        assert!(frame.is_pinned());
231        assert_eq!(frame.unpin(), 1);
232        assert_eq!(frame.unpin(), 0);
233        assert!(!frame.is_pinned());
234    }
235
236    #[test]
237    fn dirty_flag() {
238        let frame = Frame::new();
239        assert!(!frame.is_dirty());
240        frame.set_dirty();
241        assert!(frame.is_dirty());
242        frame.clear_dirty();
243        assert!(!frame.is_dirty());
244    }
245
246    #[test]
247    fn recently_used_flag() {
248        let frame = Frame::new();
249        assert!(!frame.is_recently_used());
250        frame.set_recently_used();
251        assert!(frame.is_recently_used());
252        frame.clear_recently_used();
253        assert!(!frame.is_recently_used());
254    }
255
256    #[test]
257    fn reset() {
258        let frame = Frame::new();
259        frame.set_page_id(PageId::new(FileId(1), 0));
260        frame.set_dirty();
261        frame.set_recently_used();
262        frame.reset();
263        assert!(!frame.has_valid_page());
264        assert!(!frame.is_dirty());
265        assert!(!frame.is_recently_used());
266    }
267
268    #[test]
269    fn latch_access() {
270        let frame = Frame::new();
271        frame.latch().lock_shared();
272        assert!(!frame.latch().is_unlocked());
273        frame.latch().unlock_shared();
274        assert!(frame.latch().is_unlocked());
275    }
276}