Skip to main content

reddb_server/storage/engine/
freelist.rs

1//! Free Page List Management
2//!
3//! Tracks free pages available for allocation. Uses a linked list of
4//! trunk pages, each containing a list of free page IDs.
5//!
6//! # Structure
7//!
8//! The freelist is stored as:
9//! 1. Header page (page 0) contains pointer to first trunk page
10//! 2. Trunk pages contain:
11//!    - Next trunk page ID (0 if last)
12//!    - Count of free page IDs in this trunk
13//!    - Array of free page IDs
14//!
15//! # Trunk Page Layout (4096 bytes)
16//!
17//! ```text
18//! ┌────────────────────────────────────────────────────────────┐
19//! │ PageHeader (32 bytes)                                      │
20//! ├────────────────────────────────────────────────────────────┤
21//! │ next_trunk: u32 (4 bytes) - Next trunk page ID            │
22//! │ count: u32 (4 bytes) - Number of free page IDs            │
23//! ├────────────────────────────────────────────────────────────┤
24//! │ page_ids: [u32; N] - Free page IDs                        │
25//! │ (N = (4096 - 32 - 8) / 4 = 1014 entries per trunk)        │
26//! └────────────────────────────────────────────────────────────┘
27//! ```
28
29use super::page::{Page, PageType, HEADER_SIZE, PAGE_SIZE};
30
31/// Maximum number of free page IDs per trunk page
32pub const FREE_IDS_PER_TRUNK: usize = (PAGE_SIZE - HEADER_SIZE - 8) / 4;
33
34/// Offset of next_trunk field in trunk page content
35const NEXT_TRUNK_OFFSET: usize = HEADER_SIZE;
36
37/// Offset of count field in trunk page content
38const COUNT_OFFSET: usize = HEADER_SIZE + 4;
39
40/// Offset of page_ids array in trunk page content
41const PAGE_IDS_OFFSET: usize = HEADER_SIZE + 8;
42
43/// Free list error types
44#[derive(Debug, Clone)]
45pub enum FreeListError {
46    /// Freelist is empty
47    Empty,
48    /// Freelist is corrupted
49    Corrupted(String),
50    /// Invalid trunk page
51    InvalidTrunk(u32),
52}
53
54impl std::fmt::Display for FreeListError {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        match self {
57            Self::Empty => write!(f, "Freelist is empty"),
58            Self::Corrupted(msg) => write!(f, "Freelist corrupted: {}", msg),
59            Self::InvalidTrunk(id) => write!(f, "Invalid trunk page: {}", id),
60        }
61    }
62}
63
64impl std::error::Error for FreeListError {}
65
66/// In-memory freelist tracking
67///
68/// Maintains a fast in-memory list of free pages, with persistence
69/// through trunk pages.
70#[derive(Debug)]
71pub struct FreeList {
72    /// First trunk page ID (0 = no trunk pages)
73    trunk_head: u32,
74    /// In-memory list of free page IDs (fast allocation)
75    free_pages: Vec<u32>,
76    /// Total count of free pages (including those in trunk pages)
77    total_free: u32,
78    /// Whether the freelist has been modified
79    dirty: bool,
80}
81
82impl FreeList {
83    /// Create an empty freelist
84    pub fn new() -> Self {
85        Self {
86            trunk_head: 0,
87            free_pages: Vec::new(),
88            total_free: 0,
89            dirty: false,
90        }
91    }
92
93    /// Create freelist from header page info
94    pub fn from_header(trunk_head: u32, total_free: u32) -> Self {
95        Self {
96            trunk_head,
97            free_pages: Vec::new(),
98            total_free,
99            dirty: false,
100        }
101    }
102
103    /// Get trunk head page ID
104    pub fn trunk_head(&self) -> u32 {
105        self.trunk_head
106    }
107
108    /// Get total count of free pages
109    pub fn total_free(&self) -> u32 {
110        self.total_free
111    }
112
113    /// Check if freelist is empty
114    pub fn is_empty(&self) -> bool {
115        self.free_pages.is_empty() && self.trunk_head == 0
116    }
117
118    /// Check if freelist has been modified
119    pub fn is_dirty(&self) -> bool {
120        self.dirty
121    }
122
123    /// Mark freelist as clean
124    pub fn mark_clean(&mut self) {
125        self.dirty = false;
126    }
127
128    /// Allocate a free page
129    ///
130    /// Returns None if no free pages available.
131    pub fn allocate(&mut self) -> Option<u32> {
132        // Try in-memory list first
133        if let Some(page_id) = self.free_pages.pop() {
134            self.total_free = self.total_free.saturating_sub(1);
135            self.dirty = true;
136            return Some(page_id);
137        }
138
139        // Would need to load from trunk page
140        // This is handled by the pager which has access to disk
141        None
142    }
143
144    /// Return a page to the freelist
145    pub fn free(&mut self, page_id: u32) {
146        self.free_pages.push(page_id);
147        self.total_free += 1;
148        self.dirty = true;
149    }
150
151    /// Add multiple pages to freelist
152    pub fn free_batch(&mut self, page_ids: &[u32]) {
153        self.free_pages.extend_from_slice(page_ids);
154        self.total_free += page_ids.len() as u32;
155        self.dirty = true;
156    }
157
158    /// Get count of pages in memory (not including trunk pages)
159    pub fn in_memory_count(&self) -> usize {
160        self.free_pages.len()
161    }
162
163    /// Load free pages from a trunk page
164    pub fn load_from_trunk(&mut self, trunk: &Page) -> Result<Option<u32>, FreeListError> {
165        // Verify page type
166        if trunk
167            .page_type()
168            .map_err(|_| FreeListError::InvalidTrunk(trunk.page_id()))?
169            != PageType::FreelistTrunk
170        {
171            return Err(FreeListError::InvalidTrunk(trunk.page_id()));
172        }
173
174        let data = trunk.as_bytes();
175
176        // Read next trunk pointer
177        let next_trunk = u32::from_le_bytes([
178            data[NEXT_TRUNK_OFFSET],
179            data[NEXT_TRUNK_OFFSET + 1],
180            data[NEXT_TRUNK_OFFSET + 2],
181            data[NEXT_TRUNK_OFFSET + 3],
182        ]);
183
184        // Read count
185        let count = u32::from_le_bytes([
186            data[COUNT_OFFSET],
187            data[COUNT_OFFSET + 1],
188            data[COUNT_OFFSET + 2],
189            data[COUNT_OFFSET + 3],
190        ]) as usize;
191
192        if count > FREE_IDS_PER_TRUNK {
193            return Err(FreeListError::Corrupted(format!(
194                "Trunk has {} entries, max is {}",
195                count, FREE_IDS_PER_TRUNK
196            )));
197        }
198
199        self.free_pages.push(trunk.page_id());
200
201        // Read page IDs
202        for i in 0..count {
203            let offset = PAGE_IDS_OFFSET + i * 4;
204            let page_id = u32::from_le_bytes([
205                data[offset],
206                data[offset + 1],
207                data[offset + 2],
208                data[offset + 3],
209            ]);
210            self.free_pages.push(page_id);
211        }
212        self.total_free = self.total_free.saturating_add(count as u32 + 1);
213        self.dirty = true;
214
215        // Update trunk head to next trunk
216        self.trunk_head = next_trunk;
217
218        Ok(if next_trunk != 0 {
219            Some(next_trunk)
220        } else {
221            None
222        })
223    }
224
225    /// Create a trunk page from current free pages
226    ///
227    /// Moves pages from in-memory list to a trunk page.
228    /// Returns the trunk page and remaining pages.
229    pub fn create_trunk(&mut self, trunk_page_id: u32, next_trunk: u32) -> Page {
230        let mut trunk = Page::new(PageType::FreelistTrunk, trunk_page_id);
231        let data = trunk.as_bytes_mut();
232
233        // Write next trunk pointer
234        data[NEXT_TRUNK_OFFSET..NEXT_TRUNK_OFFSET + 4].copy_from_slice(&next_trunk.to_le_bytes());
235
236        // Calculate how many pages to store
237        let count = self.free_pages.len().min(FREE_IDS_PER_TRUNK);
238
239        // Write count
240        data[COUNT_OFFSET..COUNT_OFFSET + 4].copy_from_slice(&(count as u32).to_le_bytes());
241
242        // Write page IDs (take from end to minimize moving)
243        for i in 0..count {
244            let page_id = self.free_pages.pop().unwrap();
245            let offset = PAGE_IDS_OFFSET + i * 4;
246            data[offset..offset + 4].copy_from_slice(&page_id.to_le_bytes());
247        }
248
249        trunk.update_checksum();
250        self.dirty = true;
251
252        trunk
253    }
254
255    /// Flush excess free pages to trunk pages
256    ///
257    /// If we have more than `threshold` pages in memory, create trunk pages.
258    /// Returns trunk pages that need to be written.
259    pub fn flush_to_trunks(
260        &mut self,
261        threshold: usize,
262        mut allocate_page: impl FnMut() -> u32,
263    ) -> Vec<Page> {
264        let mut trunks = Vec::new();
265
266        while self.free_pages.len() > threshold {
267            // Allocate a page for the trunk
268            let trunk_page_id = allocate_page();
269
270            // Create trunk with current head as next
271            let trunk = self.create_trunk(trunk_page_id, self.trunk_head);
272            self.trunk_head = trunk_page_id;
273
274            trunks.push(trunk);
275        }
276
277        trunks
278    }
279
280    /// Merge all trunk pages into memory
281    ///
282    /// Used during compaction to reclaim trunk pages.
283    pub fn merge_all_trunks(
284        &mut self,
285        load_page: impl Fn(u32) -> Option<Page>,
286    ) -> Result<Vec<u32>, FreeListError> {
287        let mut reclaimed_trunks = Vec::new();
288
289        while self.trunk_head != 0 {
290            let trunk_id = self.trunk_head;
291            let trunk = load_page(trunk_id).ok_or(FreeListError::InvalidTrunk(trunk_id))?;
292
293            self.load_from_trunk(&trunk)?;
294            reclaimed_trunks.push(trunk_id);
295        }
296
297        Ok(reclaimed_trunks)
298    }
299}
300
301impl Default for FreeList {
302    fn default() -> Self {
303        Self::new()
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310
311    #[test]
312    fn test_freelist_basic() {
313        let mut fl = FreeList::new();
314
315        assert!(fl.is_empty());
316        assert_eq!(fl.total_free(), 0);
317
318        // Free some pages
319        fl.free(10);
320        fl.free(20);
321        fl.free(30);
322
323        assert!(!fl.is_empty());
324        assert_eq!(fl.total_free(), 3);
325
326        // Allocate
327        assert_eq!(fl.allocate(), Some(30));
328        assert_eq!(fl.allocate(), Some(20));
329        assert_eq!(fl.allocate(), Some(10));
330        assert_eq!(fl.allocate(), None);
331
332        assert!(fl.is_empty());
333    }
334
335    #[test]
336    fn test_freelist_batch() {
337        let mut fl = FreeList::new();
338
339        fl.free_batch(&[1, 2, 3, 4, 5]);
340        assert_eq!(fl.total_free(), 5);
341        assert_eq!(fl.in_memory_count(), 5);
342    }
343
344    #[test]
345    fn test_freelist_dirty() {
346        let mut fl = FreeList::new();
347
348        assert!(!fl.is_dirty());
349
350        fl.free(1);
351        assert!(fl.is_dirty());
352
353        fl.mark_clean();
354        assert!(!fl.is_dirty());
355
356        fl.allocate();
357        assert!(fl.is_dirty());
358    }
359
360    #[test]
361    fn test_trunk_page_creation() {
362        let mut fl = FreeList::new();
363
364        // Add many pages
365        for i in 0..100 {
366            fl.free(i);
367        }
368
369        // Create a trunk
370        let trunk = fl.create_trunk(999, 0);
371
372        assert_eq!(trunk.page_type().unwrap(), PageType::FreelistTrunk);
373        assert_eq!(trunk.page_id(), 999);
374
375        // Some pages should have been moved to trunk
376        assert!(fl.in_memory_count() < 100);
377    }
378
379    #[test]
380    fn test_trunk_page_load() {
381        let mut fl = FreeList::new();
382
383        // Add pages and create trunk
384        for i in 0..50 {
385            fl.free(i);
386        }
387
388        let trunk = fl.create_trunk(999, 0);
389        let pages_in_trunk = 50 - fl.in_memory_count();
390
391        // Clear in-memory list
392        fl.free_pages.clear();
393
394        // Load from trunk
395        fl.load_from_trunk(&trunk).unwrap();
396
397        assert_eq!(fl.in_memory_count(), pages_in_trunk + 1);
398    }
399
400    #[test]
401    fn test_trunk_page_reuse() {
402        let mut original = FreeList::new();
403
404        for i in 0..8 {
405            original.free(i);
406        }
407
408        let trunk = original.create_trunk(999, 0);
409
410        let mut fl = FreeList::new();
411        fl.load_from_trunk(&trunk).unwrap();
412
413        let mut ids = Vec::new();
414        while let Some(id) = fl.allocate() {
415            ids.push(id);
416        }
417
418        assert!(ids.contains(&999));
419    }
420
421    #[test]
422    fn test_trunk_chain() {
423        let mut fl = FreeList::new();
424
425        // Add lots of pages (more than one trunk can hold)
426        for i in 0..2000 {
427            fl.free(i);
428        }
429
430        // Create trunk chain
431        let mut next_trunk_id = 10000u32;
432        let trunks = fl.flush_to_trunks(100, || {
433            let id = next_trunk_id;
434            next_trunk_id += 1;
435            id
436        });
437
438        assert!(!trunks.is_empty());
439        assert!(fl.in_memory_count() <= 100);
440    }
441
442    #[test]
443    fn test_free_ids_per_trunk() {
444        // Verify the constant is reasonable
445        // (4096 - 32 - 8) / 4 = 1014
446        assert_eq!(FREE_IDS_PER_TRUNK, 1014);
447        assert_eq!(FREE_IDS_PER_TRUNK, 1014);
448    }
449}