1use super::page::{Page, PageType, HEADER_SIZE, PAGE_SIZE};
30
31pub const FREE_IDS_PER_TRUNK: usize = (PAGE_SIZE - HEADER_SIZE - 8) / 4;
33
34const NEXT_TRUNK_OFFSET: usize = HEADER_SIZE;
36
37const COUNT_OFFSET: usize = HEADER_SIZE + 4;
39
40const PAGE_IDS_OFFSET: usize = HEADER_SIZE + 8;
42
43#[derive(Debug, Clone)]
45pub enum FreeListError {
46 Empty,
48 Corrupted(String),
50 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#[derive(Debug)]
71pub struct FreeList {
72 trunk_head: u32,
74 free_pages: Vec<u32>,
76 total_free: u32,
78 dirty: bool,
80}
81
82impl FreeList {
83 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 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 pub fn trunk_head(&self) -> u32 {
105 self.trunk_head
106 }
107
108 pub fn total_free(&self) -> u32 {
110 self.total_free
111 }
112
113 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 pub fn is_empty(&self) -> bool {
120 self.free_pages.is_empty() && self.trunk_head == 0
121 }
122
123 pub fn is_dirty(&self) -> bool {
125 self.dirty
126 }
127
128 pub fn mark_clean(&mut self) {
130 self.dirty = false;
131 }
132
133 pub fn allocate(&mut self) -> Option<u32> {
137 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 None
147 }
148
149 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 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 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 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 pub fn load_from_trunk(&mut self, trunk: &Page) -> Result<Option<u32>, FreeListError> {
192 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 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 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 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 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 self.trunk_head = next_trunk;
252
253 Ok(if next_trunk != 0 {
254 Some(next_trunk)
255 } else {
256 None
257 })
258 }
259
260 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 data[NEXT_TRUNK_OFFSET..NEXT_TRUNK_OFFSET + 4].copy_from_slice(&next_trunk.to_le_bytes());
270
271 let count = self.free_pages.len().min(FREE_IDS_PER_TRUNK);
273
274 data[COUNT_OFFSET..COUNT_OFFSET + 4].copy_from_slice(&(count as u32).to_le_bytes());
276
277 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 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 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 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 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 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 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 for i in 0..100 {
424 fl.free(i);
425 }
426
427 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 assert!(fl.in_memory_count() < 100);
435 }
436
437 #[test]
438 fn test_trunk_page_load() {
439 let mut fl = FreeList::new();
440
441 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 fl.free_pages.clear();
451
452 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 for i in 0..2000 {
485 fl.free(i);
486 }
487
488 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 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}