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 fn is_empty(&self) -> bool {
115 self.free_pages.is_empty() && self.trunk_head == 0
116 }
117
118 pub fn is_dirty(&self) -> bool {
120 self.dirty
121 }
122
123 pub fn mark_clean(&mut self) {
125 self.dirty = false;
126 }
127
128 pub fn allocate(&mut self) -> Option<u32> {
132 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 None
142 }
143
144 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 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 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 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 pub fn load_from_trunk(&mut self, trunk: &Page) -> Result<Option<u32>, FreeListError> {
187 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 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 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 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 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 self.trunk_head = next_trunk;
247
248 Ok(if next_trunk != 0 {
249 Some(next_trunk)
250 } else {
251 None
252 })
253 }
254
255 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 data[NEXT_TRUNK_OFFSET..NEXT_TRUNK_OFFSET + 4].copy_from_slice(&next_trunk.to_le_bytes());
265
266 let count = self.free_pages.len().min(FREE_IDS_PER_TRUNK);
268
269 data[COUNT_OFFSET..COUNT_OFFSET + 4].copy_from_slice(&(count as u32).to_le_bytes());
271
272 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 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 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 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 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 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 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 for i in 0..100 {
419 fl.free(i);
420 }
421
422 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 assert!(fl.in_memory_count() < 100);
430 }
431
432 #[test]
433 fn test_trunk_page_load() {
434 let mut fl = FreeList::new();
435
436 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 fl.free_pages.clear();
446
447 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 for i in 0..2000 {
480 fl.free(i);
481 }
482
483 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 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}