Skip to main content

luci/storage/
allocator.rs

1use crate::core::{LuciError, Result};
2
3use crate::storage::block::{BlockId, Extent};
4
5/// Block allocator with extent-based free-list tracking.
6///
7/// Manages space within a `.luci` file as fixed 256 KB blocks. The header
8/// occupies the first 4 KB of the file; blocks start immediately after.
9/// Allocation searches the free list first (first-fit), then extends the file
10/// if no suitable free extent exists. Deallocation returns extents to the free
11/// list and coalesces adjacent entries.
12///
13/// See [[architecture-storage-format#Block Allocator]].
14#[derive(Debug)]
15pub struct BlockAllocator {
16    /// Total number of data blocks the file currently spans.
17    /// The header is separate and not counted here.
18    total_blocks: u64,
19    /// Free extents, kept sorted by start block for efficient coalescing.
20    free_list: Vec<Extent>,
21}
22
23impl BlockAllocator {
24    /// Create an allocator for a new, empty file.
25    ///
26    /// A fresh file has only the 4 KB header — no data blocks yet.
27    /// All allocations extend the file.
28    pub fn new() -> Self {
29        Self {
30            total_blocks: 0,
31            free_list: Vec::new(),
32        }
33    }
34
35    /// Reconstruct an allocator from persisted state.
36    ///
37    /// Called during recovery: the metadata root block stores `total_blocks`
38    /// and the serialized free list.
39    pub fn from_state(total_blocks: u64, free_list: Vec<Extent>) -> Self {
40        let mut alloc = Self {
41            total_blocks,
42            free_list,
43        };
44        alloc.free_list.sort_by_key(|e| e.start);
45        alloc
46    }
47
48    /// Allocate `count` contiguous blocks.
49    ///
50    /// Strategy: first-fit scan of the free list. If no free extent is large
51    /// enough, extends the file by appending blocks at the end.
52    ///
53    /// # Errors
54    ///
55    /// Returns `LuciError::InvalidQuery` if `count` is zero.
56    pub fn allocate(&mut self, count: u32) -> Result<Extent> {
57        if count == 0 {
58            return Err(LuciError::InvalidQuery(
59                "cannot allocate zero blocks".into(),
60            ));
61        }
62
63        // First-fit scan of free list.
64        if let Some(idx) = self.free_list.iter().position(|e| e.count >= count) {
65            let extent = self.free_list[idx];
66            if extent.count == count {
67                // Exact fit — remove from free list.
68                self.free_list.remove(idx);
69            } else {
70                // Split: return the first `count` blocks, keep the remainder.
71                self.free_list[idx] =
72                    Extent::new(BlockId(extent.start.0 + count as u64), extent.count - count);
73            }
74            return Ok(Extent::new(extent.start, count));
75        }
76
77        // No suitable free extent — extend the file.
78        let start = BlockId(self.total_blocks);
79        self.total_blocks += count as u64;
80        Ok(Extent::new(start, count))
81    }
82
83    /// Return an extent to the free list.
84    ///
85    /// The freed blocks become available for future allocations. Adjacent
86    /// free extents are coalesced to reduce fragmentation.
87    ///
88    /// # Panics
89    ///
90    /// Debug-asserts that the extent falls within `[0, total_blocks)` and
91    /// does not overlap any existing free extent.
92    pub fn free(&mut self, extent: Extent) {
93        debug_assert!(
94            extent.end().0 <= self.total_blocks,
95            "extent {extent} exceeds file bounds (total_blocks = {})",
96            self.total_blocks
97        );
98
99        // Insert in sorted order.
100        let pos = self
101            .free_list
102            .binary_search_by_key(&extent.start, |e| e.start)
103            .unwrap_or_else(|i| i);
104
105        debug_assert!(
106            !self.overlaps_free_list(&extent),
107            "double-free: {extent} overlaps an existing free extent"
108        );
109
110        self.free_list.insert(pos, extent);
111        self.coalesce_at(pos);
112    }
113
114    /// Total number of data blocks the file spans (excludes the header).
115    pub fn total_blocks(&self) -> u64 {
116        self.total_blocks
117    }
118
119    /// Total number of free (reusable) blocks.
120    pub fn free_block_count(&self) -> u64 {
121        self.free_list.iter().map(|e| e.count as u64).sum()
122    }
123
124    /// Snapshot of the current free list (for persistence in metadata root).
125    pub fn free_list(&self) -> &[Extent] {
126        &self.free_list
127    }
128
129    // --- internals ---
130
131    /// Coalesce the extent at `pos` with its neighbours if they are adjacent.
132    fn coalesce_at(&mut self, pos: usize) {
133        // Merge with the next extent first (so `pos` stays valid for the
134        // backward merge).
135        if pos + 1 < self.free_list.len()
136            && self.free_list[pos].is_adjacent_to(&self.free_list[pos + 1])
137        {
138            self.free_list[pos] = Extent::new(
139                self.free_list[pos].start,
140                self.free_list[pos].count + self.free_list[pos + 1].count,
141            );
142            self.free_list.remove(pos + 1);
143        }
144
145        // Merge with the previous extent.
146        if pos > 0 && self.free_list[pos - 1].is_adjacent_to(&self.free_list[pos]) {
147            self.free_list[pos - 1] = Extent::new(
148                self.free_list[pos - 1].start,
149                self.free_list[pos - 1].count + self.free_list[pos].count,
150            );
151            self.free_list.remove(pos);
152        }
153    }
154
155    /// Check whether `extent` overlaps any entry in the free list.
156    fn overlaps_free_list(&self, extent: &Extent) -> bool {
157        self.free_list
158            .iter()
159            .any(|free| extent.start.0 < free.end().0 && free.start.0 < extent.end().0)
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn new_allocator_is_empty() {
169        let alloc = BlockAllocator::new();
170        assert_eq!(alloc.total_blocks(), 0);
171        assert_eq!(alloc.free_block_count(), 0);
172    }
173
174    #[test]
175    fn allocate_extends_file() {
176        let mut alloc = BlockAllocator::new();
177        let ext = alloc.allocate(4).unwrap();
178        assert_eq!(ext, Extent::new(BlockId(0), 4));
179        assert_eq!(alloc.total_blocks(), 4);
180    }
181
182    #[test]
183    fn sequential_allocations_are_contiguous() {
184        let mut alloc = BlockAllocator::new();
185        let a = alloc.allocate(2).unwrap();
186        let b = alloc.allocate(3).unwrap();
187        assert_eq!(a, Extent::new(BlockId(0), 2));
188        assert_eq!(b, Extent::new(BlockId(2), 3));
189        assert_eq!(alloc.total_blocks(), 5);
190    }
191
192    #[test]
193    fn allocate_zero_is_error() {
194        let mut alloc = BlockAllocator::new();
195        assert!(alloc.allocate(0).is_err());
196    }
197
198    #[test]
199    fn free_and_reuse() {
200        let mut alloc = BlockAllocator::new();
201        let a = alloc.allocate(4).unwrap();
202        let _b = alloc.allocate(2).unwrap();
203
204        // Free the first allocation.
205        alloc.free(a);
206        assert_eq!(alloc.free_block_count(), 4);
207
208        // Next allocation of the same size reuses freed blocks.
209        let c = alloc.allocate(4).unwrap();
210        assert_eq!(c, a);
211        assert_eq!(alloc.free_block_count(), 0);
212    }
213
214    #[test]
215    fn free_and_split() {
216        let mut alloc = BlockAllocator::new();
217        let a = alloc.allocate(8).unwrap();
218        alloc.free(a);
219
220        // Allocate fewer blocks than the free extent — should split.
221        let b = alloc.allocate(3).unwrap();
222        assert_eq!(b, Extent::new(BlockId(0), 3));
223        assert_eq!(alloc.free_block_count(), 5);
224
225        // The remainder is reusable.
226        let c = alloc.allocate(5).unwrap();
227        assert_eq!(c, Extent::new(BlockId(3), 5));
228        assert_eq!(alloc.free_block_count(), 0);
229    }
230
231    #[test]
232    fn coalesce_adjacent_frees() {
233        let mut alloc = BlockAllocator::new();
234        let a = alloc.allocate(3).unwrap(); // blocks [0..3)
235        let b = alloc.allocate(3).unwrap(); // blocks [3..6)
236        let c = alloc.allocate(3).unwrap(); // blocks [6..9)
237
238        // Free b then a — they should coalesce into [0..6).
239        alloc.free(b);
240        alloc.free(a);
241        assert_eq!(alloc.free_list().len(), 1);
242        assert_eq!(alloc.free_list()[0], Extent::new(BlockId(0), 6));
243
244        // Free c — coalesces into [0..9).
245        alloc.free(c);
246        assert_eq!(alloc.free_list().len(), 1);
247        assert_eq!(alloc.free_list()[0], Extent::new(BlockId(0), 9));
248    }
249
250    #[test]
251    fn non_adjacent_frees_stay_separate() {
252        let mut alloc = BlockAllocator::new();
253        let a = alloc.allocate(2).unwrap(); // [0..2)
254        let _b = alloc.allocate(2).unwrap(); // [2..4) — kept allocated
255        let c = alloc.allocate(2).unwrap(); // [4..6)
256
257        alloc.free(a);
258        alloc.free(c);
259        assert_eq!(alloc.free_list().len(), 2);
260        assert_eq!(alloc.free_list()[0], Extent::new(BlockId(0), 2));
261        assert_eq!(alloc.free_list()[1], Extent::new(BlockId(4), 2));
262    }
263
264    #[test]
265    fn first_fit_prefers_first_free_extent() {
266        let mut alloc = BlockAllocator::new();
267        let a = alloc.allocate(4).unwrap(); // [0..4)
268        let _b = alloc.allocate(2).unwrap(); // [4..6)
269        let c = alloc.allocate(6).unwrap(); // [6..12)
270
271        alloc.free(a);
272        alloc.free(c);
273
274        // Allocating 3 blocks should use the first free extent [0..4).
275        let d = alloc.allocate(3).unwrap();
276        assert_eq!(d, Extent::new(BlockId(0), 3));
277    }
278
279    #[test]
280    fn allocate_falls_through_to_file_extension_when_free_list_too_small() {
281        let mut alloc = BlockAllocator::new();
282        let a = alloc.allocate(2).unwrap();
283        alloc.free(a);
284
285        // Free list has 2 blocks, but we need 5 — must extend file.
286        let b = alloc.allocate(5).unwrap();
287        assert_eq!(b, Extent::new(BlockId(2), 5));
288        // Original free extent still available.
289        assert_eq!(alloc.free_block_count(), 2);
290    }
291
292    #[test]
293    fn from_state_reconstructs_correctly() {
294        let free_list = vec![Extent::new(BlockId(10), 3), Extent::new(BlockId(5), 2)];
295        let alloc = BlockAllocator::from_state(20, free_list);
296        assert_eq!(alloc.total_blocks(), 20);
297        assert_eq!(alloc.free_block_count(), 5);
298        // Should be sorted by start block.
299        assert_eq!(alloc.free_list()[0].start, BlockId(5));
300        assert_eq!(alloc.free_list()[1].start, BlockId(10));
301    }
302
303    #[test]
304    fn from_state_allocates_from_free_list() {
305        let free_list = vec![Extent::new(BlockId(5), 4)];
306        let mut alloc = BlockAllocator::from_state(20, free_list);
307
308        let ext = alloc.allocate(2).unwrap();
309        assert_eq!(ext, Extent::new(BlockId(5), 2));
310        assert_eq!(alloc.free_block_count(), 2);
311        // File size unchanged — reused free space.
312        assert_eq!(alloc.total_blocks(), 20);
313    }
314}