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