Skip to main content

reddb_engine/storage/
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    pub(crate) fn next_in_memory_page(&self) -> Option<u32> {
164        self.free_pages.last().copied()
165    }
166
167    /// Test whether a batch overlaps free pages already loaded into memory.
168    pub(crate) fn contains_any(&self, pages: &std::collections::HashSet<u32>) -> bool {
169        self.free_pages.iter().any(|id| pages.contains(id))
170    }
171
172    pub(crate) fn remove_existing_from(
173        &self,
174        requested: &std::collections::HashSet<u32>,
175        remaining: &mut std::collections::HashSet<u32>,
176    ) -> Result<(), FreeListError> {
177        for id in &self.free_pages {
178            if requested.contains(id) && !remaining.remove(id) {
179                return Err(FreeListError::Corrupted("duplicate recovery page".into()));
180            }
181        }
182        Ok(())
183    }
184
185    /// Load free pages from a trunk page
186    pub fn load_from_trunk(&mut self, trunk: &Page) -> Result<Option<u32>, FreeListError> {
187        // Verify page type
188        if trunk
189            .page_type()
190            .map_err(|_| FreeListError::InvalidTrunk(trunk.page_id()))?
191            != PageType::FreelistTrunk
192        {
193            return Err(FreeListError::InvalidTrunk(trunk.page_id()));
194        }
195
196        let data = trunk.as_bytes();
197
198        // Read next trunk pointer
199        let next_trunk = u32::from_le_bytes([
200            data[NEXT_TRUNK_OFFSET],
201            data[NEXT_TRUNK_OFFSET + 1],
202            data[NEXT_TRUNK_OFFSET + 2],
203            data[NEXT_TRUNK_OFFSET + 3],
204        ]);
205        if next_trunk == trunk.page_id() {
206            return Err(FreeListError::Corrupted("trunk points to itself".into()));
207        }
208
209        // Read count
210        let count = u32::from_le_bytes([
211            data[COUNT_OFFSET],
212            data[COUNT_OFFSET + 1],
213            data[COUNT_OFFSET + 2],
214            data[COUNT_OFFSET + 3],
215        ]) as usize;
216
217        if count > FREE_IDS_PER_TRUNK {
218            return Err(FreeListError::Corrupted(format!(
219                "Trunk has {} entries, max is {}",
220                count, FREE_IDS_PER_TRUNK
221            )));
222        }
223
224        self.free_pages.push(trunk.page_id());
225
226        // Read page IDs
227        for i in 0..count {
228            let offset = PAGE_IDS_OFFSET + i * 4;
229            let page_id = u32::from_le_bytes([
230                data[offset],
231                data[offset + 1],
232                data[offset + 2],
233                data[offset + 3],
234            ]);
235            self.free_pages.push(page_id);
236        }
237        // Spilling already counted these IDs. Only the consumed trunk becomes
238        // newly free. After reopen, learn any previously unknown resident IDs.
239        self.total_free = self
240            .total_free
241            .saturating_add(1)
242            .max(self.free_pages.len() as u32);
243        self.dirty = true;
244
245        // Update trunk head to next trunk
246        self.trunk_head = next_trunk;
247
248        Ok(if next_trunk != 0 {
249            Some(next_trunk)
250        } else {
251            None
252        })
253    }
254
255    /// Create a trunk page from current free pages
256    ///
257    /// Moves pages from in-memory list to a trunk page.
258    /// Returns the trunk page and remaining pages.
259    pub fn create_trunk(&mut self, trunk_page_id: u32, next_trunk: u32) -> Page {
260        let mut trunk = Page::new(PageType::FreelistTrunk, trunk_page_id);
261        let data = trunk.as_bytes_mut();
262
263        // Write next trunk pointer
264        data[NEXT_TRUNK_OFFSET..NEXT_TRUNK_OFFSET + 4].copy_from_slice(&next_trunk.to_le_bytes());
265
266        // Calculate how many pages to store
267        let count = self.free_pages.len().min(FREE_IDS_PER_TRUNK);
268
269        // Write count
270        data[COUNT_OFFSET..COUNT_OFFSET + 4].copy_from_slice(&(count as u32).to_le_bytes());
271
272        // Write page IDs (take from end to minimize moving)
273        for i in 0..count {
274            let page_id = self.free_pages.pop().unwrap();
275            let offset = PAGE_IDS_OFFSET + i * 4;
276            data[offset..offset + 4].copy_from_slice(&page_id.to_le_bytes());
277        }
278
279        trunk.update_checksum();
280        self.dirty = true;
281
282        trunk
283    }
284
285    /// Flush excess free pages to trunk pages
286    ///
287    /// If we have more than `threshold` pages in memory, create trunk pages.
288    /// Returns trunk pages that need to be written.
289    pub fn flush_to_trunks(
290        &mut self,
291        threshold: usize,
292        mut allocate_page: impl FnMut() -> u32,
293    ) -> Vec<Page> {
294        let mut trunks = Vec::new();
295
296        while self.free_pages.len() > threshold {
297            // A trunk is allocator metadata, not live data. Host it in an
298            // already-free page so repeated frees do not append forever.
299            let trunk_page_id = if let Some(id) = self.free_pages.pop() {
300                self.total_free = self.total_free.saturating_sub(1);
301                id
302            } else {
303                allocate_page()
304            };
305
306            // Create trunk with current head as next
307            let trunk = self.create_trunk(trunk_page_id, self.trunk_head);
308            self.trunk_head = trunk_page_id;
309
310            trunks.push(trunk);
311        }
312
313        trunks
314    }
315
316    /// Merge all trunk pages into memory
317    ///
318    /// Used during compaction to reclaim trunk pages.
319    pub fn merge_all_trunks(
320        &mut self,
321        load_page: impl Fn(u32) -> Option<Page>,
322    ) -> Result<Vec<u32>, FreeListError> {
323        let mut reclaimed_trunks = Vec::new();
324
325        while self.trunk_head != 0 {
326            let trunk_id = self.trunk_head;
327            let trunk = load_page(trunk_id).ok_or(FreeListError::InvalidTrunk(trunk_id))?;
328
329            self.load_from_trunk(&trunk)?;
330            reclaimed_trunks.push(trunk_id);
331        }
332
333        Ok(reclaimed_trunks)
334    }
335}
336
337impl Default for FreeList {
338    fn default() -> Self {
339        Self::new()
340    }
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346
347    #[test]
348    fn trunk_publication_does_not_extend_a_file_with_free_pages() {
349        let mut list = FreeList::new();
350        list.free_batch(&[100, 101, 102]);
351        let trunks = list.flush_to_trunks(0, || panic!("must reuse a free page"));
352        assert_eq!(trunks.len(), 1);
353        assert_eq!(trunks[0].page_id(), 102);
354        let mut loaded = FreeList::new();
355        loaded.load_from_trunk(&trunks[0]).expect("load trunk");
356        let mut pages = Vec::new();
357        while let Some(id) = loaded.allocate() {
358            pages.push(id);
359        }
360        pages.sort_unstable();
361        assert_eq!(pages, [100, 101, 102]);
362    }
363
364    #[test]
365    fn test_freelist_basic() {
366        let mut fl = FreeList::new();
367
368        assert!(fl.is_empty());
369        assert_eq!(fl.total_free(), 0);
370
371        // Free some pages
372        fl.free(10);
373        fl.free(20);
374        fl.free(30);
375
376        assert!(!fl.is_empty());
377        assert_eq!(fl.total_free(), 3);
378
379        // Allocate
380        assert_eq!(fl.allocate(), Some(30));
381        assert_eq!(fl.allocate(), Some(20));
382        assert_eq!(fl.allocate(), Some(10));
383        assert_eq!(fl.allocate(), None);
384
385        assert!(fl.is_empty());
386    }
387
388    #[test]
389    fn test_freelist_batch() {
390        let mut fl = FreeList::new();
391
392        fl.free_batch(&[1, 2, 3, 4, 5]);
393        assert_eq!(fl.total_free(), 5);
394        assert_eq!(fl.in_memory_count(), 5);
395    }
396
397    #[test]
398    fn test_freelist_dirty() {
399        let mut fl = FreeList::new();
400
401        assert!(!fl.is_dirty());
402
403        fl.free(1);
404        assert!(fl.is_dirty());
405
406        fl.mark_clean();
407        assert!(!fl.is_dirty());
408
409        fl.allocate();
410        assert!(fl.is_dirty());
411    }
412
413    #[test]
414    fn test_trunk_page_creation() {
415        let mut fl = FreeList::new();
416
417        // Add many pages
418        for i in 0..100 {
419            fl.free(i);
420        }
421
422        // Create a trunk
423        let trunk = fl.create_trunk(999, 0);
424
425        assert_eq!(trunk.page_type().unwrap(), PageType::FreelistTrunk);
426        assert_eq!(trunk.page_id(), 999);
427
428        // Some pages should have been moved to trunk
429        assert!(fl.in_memory_count() < 100);
430    }
431
432    #[test]
433    fn test_trunk_page_load() {
434        let mut fl = FreeList::new();
435
436        // Add pages and create trunk
437        for i in 0..50 {
438            fl.free(i);
439        }
440
441        let trunk = fl.create_trunk(999, 0);
442        let pages_in_trunk = 50 - fl.in_memory_count();
443
444        // Clear in-memory list
445        fl.free_pages.clear();
446
447        // Load from trunk
448        fl.load_from_trunk(&trunk).unwrap();
449
450        assert_eq!(fl.in_memory_count(), pages_in_trunk + 1);
451    }
452
453    #[test]
454    fn test_trunk_page_reuse() {
455        let mut original = FreeList::new();
456
457        for i in 0..8 {
458            original.free(i);
459        }
460
461        let trunk = original.create_trunk(999, 0);
462
463        let mut fl = FreeList::new();
464        fl.load_from_trunk(&trunk).unwrap();
465
466        let mut ids = Vec::new();
467        while let Some(id) = fl.allocate() {
468            ids.push(id);
469        }
470
471        assert!(ids.contains(&999));
472    }
473
474    #[test]
475    fn test_trunk_chain() {
476        let mut fl = FreeList::new();
477
478        // Add lots of pages (more than one trunk can hold)
479        for i in 0..2000 {
480            fl.free(i);
481        }
482
483        // Create trunk chain
484        let mut next_trunk_id = 10000u32;
485        let trunks = fl.flush_to_trunks(100, || {
486            let id = next_trunk_id;
487            next_trunk_id += 1;
488            id
489        });
490
491        assert!(!trunks.is_empty());
492        assert!(fl.in_memory_count() <= 100);
493    }
494
495    #[test]
496    fn test_free_ids_per_trunk() {
497        // Verify the constant is reasonable
498        // (16384 - 32 - 8) / 4 = 4086
499        assert_eq!(FREE_IDS_PER_TRUNK, 4086);
500        assert_eq!(FREE_IDS_PER_TRUNK, 4086);
501    }
502
503    #[test]
504    fn consuming_a_trunk_does_not_count_its_free_ids_twice() {
505        let mut list = FreeList::new();
506        list.free_batch(&[100, 101, 102, 103]);
507        let trunk = list.flush_to_trunks(0, || panic!("must reuse a free page"));
508        assert_eq!(trunk.len(), 1);
509        assert_eq!(list.total_free(), 3);
510        list.load_from_trunk(&trunk[0]).expect("load own trunk");
511        assert_eq!(
512            list.total_free(),
513            4,
514            "only the consumed trunk becomes newly free"
515        );
516        let ids: Vec<_> = std::iter::from_fn(|| list.allocate()).collect();
517        assert_eq!(ids.len(), 4);
518        assert_eq!(list.total_free(), 0);
519
520        let mut reopened = FreeList::from_header(trunk[0].page_id(), 0);
521        reopened
522            .load_from_trunk(&trunk[0])
523            .expect("learn reopened IDs");
524        assert_eq!(reopened.total_free(), 4);
525    }
526
527    #[test]
528    fn merging_trunks_counts_known_and_reopened_ids_once() {
529        for reopen in [false, true] {
530            let mut list = FreeList::new();
531            let ids: Vec<_> = (100..9100).collect();
532            list.free_batch(&ids);
533            let trunks = list.flush_to_trunks(0, || panic!("must reuse free pages"));
534            if reopen {
535                list = FreeList::from_header(list.trunk_head(), 0);
536            }
537            list.merge_all_trunks(|id| trunks.iter().find(|page| page.page_id() == id).cloned())
538                .expect("merge all trunks");
539            assert_eq!(list.total_free(), ids.len() as u32);
540            let allocated: std::collections::BTreeSet<_> =
541                std::iter::from_fn(|| list.allocate()).collect();
542            assert_eq!(allocated, ids.into_iter().collect());
543            assert_eq!(list.total_free(), 0);
544        }
545    }
546}