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            // A trunk is allocator metadata, not live data. Host it in an
268            // already-free page so repeated frees do not append forever.
269            let trunk_page_id = if let Some(id) = self.free_pages.pop() {
270                self.total_free = self.total_free.saturating_sub(1);
271                id
272            } else {
273                allocate_page()
274            };
275
276            // Create trunk with current head as next
277            let trunk = self.create_trunk(trunk_page_id, self.trunk_head);
278            self.trunk_head = trunk_page_id;
279
280            trunks.push(trunk);
281        }
282
283        trunks
284    }
285
286    /// Merge all trunk pages into memory
287    ///
288    /// Used during compaction to reclaim trunk pages.
289    pub fn merge_all_trunks(
290        &mut self,
291        load_page: impl Fn(u32) -> Option<Page>,
292    ) -> Result<Vec<u32>, FreeListError> {
293        let mut reclaimed_trunks = Vec::new();
294
295        while self.trunk_head != 0 {
296            let trunk_id = self.trunk_head;
297            let trunk = load_page(trunk_id).ok_or(FreeListError::InvalidTrunk(trunk_id))?;
298
299            self.load_from_trunk(&trunk)?;
300            reclaimed_trunks.push(trunk_id);
301        }
302
303        Ok(reclaimed_trunks)
304    }
305}
306
307impl Default for FreeList {
308    fn default() -> Self {
309        Self::new()
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316
317    #[test]
318    fn trunk_publication_does_not_extend_a_file_with_free_pages() {
319        let mut list = FreeList::new();
320        list.free_batch(&[100, 101, 102]);
321        let trunks = list.flush_to_trunks(0, || panic!("must reuse a free page"));
322        assert_eq!(trunks.len(), 1);
323        assert_eq!(trunks[0].page_id(), 102);
324        let mut loaded = FreeList::new();
325        loaded.load_from_trunk(&trunks[0]).expect("load trunk");
326        let mut pages = Vec::new();
327        while let Some(id) = loaded.allocate() {
328            pages.push(id);
329        }
330        pages.sort_unstable();
331        assert_eq!(pages, [100, 101, 102]);
332    }
333
334    #[test]
335    fn test_freelist_basic() {
336        let mut fl = FreeList::new();
337
338        assert!(fl.is_empty());
339        assert_eq!(fl.total_free(), 0);
340
341        // Free some pages
342        fl.free(10);
343        fl.free(20);
344        fl.free(30);
345
346        assert!(!fl.is_empty());
347        assert_eq!(fl.total_free(), 3);
348
349        // Allocate
350        assert_eq!(fl.allocate(), Some(30));
351        assert_eq!(fl.allocate(), Some(20));
352        assert_eq!(fl.allocate(), Some(10));
353        assert_eq!(fl.allocate(), None);
354
355        assert!(fl.is_empty());
356    }
357
358    #[test]
359    fn test_freelist_batch() {
360        let mut fl = FreeList::new();
361
362        fl.free_batch(&[1, 2, 3, 4, 5]);
363        assert_eq!(fl.total_free(), 5);
364        assert_eq!(fl.in_memory_count(), 5);
365    }
366
367    #[test]
368    fn test_freelist_dirty() {
369        let mut fl = FreeList::new();
370
371        assert!(!fl.is_dirty());
372
373        fl.free(1);
374        assert!(fl.is_dirty());
375
376        fl.mark_clean();
377        assert!(!fl.is_dirty());
378
379        fl.allocate();
380        assert!(fl.is_dirty());
381    }
382
383    #[test]
384    fn test_trunk_page_creation() {
385        let mut fl = FreeList::new();
386
387        // Add many pages
388        for i in 0..100 {
389            fl.free(i);
390        }
391
392        // Create a trunk
393        let trunk = fl.create_trunk(999, 0);
394
395        assert_eq!(trunk.page_type().unwrap(), PageType::FreelistTrunk);
396        assert_eq!(trunk.page_id(), 999);
397
398        // Some pages should have been moved to trunk
399        assert!(fl.in_memory_count() < 100);
400    }
401
402    #[test]
403    fn test_trunk_page_load() {
404        let mut fl = FreeList::new();
405
406        // Add pages and create trunk
407        for i in 0..50 {
408            fl.free(i);
409        }
410
411        let trunk = fl.create_trunk(999, 0);
412        let pages_in_trunk = 50 - fl.in_memory_count();
413
414        // Clear in-memory list
415        fl.free_pages.clear();
416
417        // Load from trunk
418        fl.load_from_trunk(&trunk).unwrap();
419
420        assert_eq!(fl.in_memory_count(), pages_in_trunk + 1);
421    }
422
423    #[test]
424    fn test_trunk_page_reuse() {
425        let mut original = FreeList::new();
426
427        for i in 0..8 {
428            original.free(i);
429        }
430
431        let trunk = original.create_trunk(999, 0);
432
433        let mut fl = FreeList::new();
434        fl.load_from_trunk(&trunk).unwrap();
435
436        let mut ids = Vec::new();
437        while let Some(id) = fl.allocate() {
438            ids.push(id);
439        }
440
441        assert!(ids.contains(&999));
442    }
443
444    #[test]
445    fn test_trunk_chain() {
446        let mut fl = FreeList::new();
447
448        // Add lots of pages (more than one trunk can hold)
449        for i in 0..2000 {
450            fl.free(i);
451        }
452
453        // Create trunk chain
454        let mut next_trunk_id = 10000u32;
455        let trunks = fl.flush_to_trunks(100, || {
456            let id = next_trunk_id;
457            next_trunk_id += 1;
458            id
459        });
460
461        assert!(!trunks.is_empty());
462        assert!(fl.in_memory_count() <= 100);
463    }
464
465    #[test]
466    fn test_free_ids_per_trunk() {
467        // Verify the constant is reasonable
468        // (16384 - 32 - 8) / 4 = 4086
469        assert_eq!(FREE_IDS_PER_TRUNK, 4086);
470        assert_eq!(FREE_IDS_PER_TRUNK, 4086);
471    }
472}