Skip to main content

ipfrs_storage/
chunk_manager.rs

1//! Storage chunk manager for splitting large objects into fixed-size chunks.
2//!
3//! Manages chunk metadata, state transitions, and reassembly ordering
4//! for streaming reads of large data objects.
5
6use std::collections::HashMap;
7
8/// State of an individual chunk within a chunked object.
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum ChunkState {
11    /// Chunk has not yet been written.
12    Pending,
13    /// Chunk was successfully stored.
14    Written,
15    /// Chunk checksum has been confirmed.
16    Verified,
17    /// Chunk write failed.
18    Failed,
19}
20
21/// Metadata for a single chunk within a larger object.
22#[derive(Clone, Debug)]
23pub struct Chunk {
24    /// Unique identifier for this chunk.
25    pub chunk_id: u64,
26    /// Parent object this chunk belongs to.
27    pub object_id: u64,
28    /// 0-based position within the object.
29    pub chunk_index: u32,
30    /// Byte offset within the original data.
31    pub offset: u64,
32    /// Size of this chunk in bytes.
33    pub size_bytes: u64,
34    /// Current state of the chunk.
35    pub state: ChunkState,
36    /// FNV-1a checksum of (object_id XOR chunk_index as u64).
37    pub checksum: u64,
38}
39
40/// A large object that has been split into fixed-size chunks.
41#[derive(Clone, Debug)]
42pub struct ChunkedObject {
43    /// Unique identifier for this object.
44    pub object_id: u64,
45    /// Total size of the original data in bytes.
46    pub total_size_bytes: u64,
47    /// Configured chunk size in bytes.
48    pub chunk_size_bytes: u64,
49    /// Ordered list of chunks by chunk_index.
50    pub chunks: Vec<Chunk>,
51}
52
53impl ChunkedObject {
54    /// Returns the total number of chunks in this object.
55    pub fn total_chunks(&self) -> usize {
56        self.chunks.len()
57    }
58
59    /// Returns the count of chunks in Written or Verified state.
60    pub fn written_chunks(&self) -> usize {
61        self.chunks
62            .iter()
63            .filter(|c| matches!(c.state, ChunkState::Written | ChunkState::Verified))
64            .count()
65    }
66
67    /// Returns true when all chunks are in Written or Verified state.
68    pub fn is_complete(&self) -> bool {
69        !self.chunks.is_empty()
70            && self
71                .chunks
72                .iter()
73                .all(|c| matches!(c.state, ChunkState::Written | ChunkState::Verified))
74    }
75
76    /// Returns written_chunks / total_chunks * 100.0; 0.0 if empty.
77    pub fn completion_pct(&self) -> f64 {
78        let total = self.total_chunks();
79        if total == 0 {
80            return 0.0;
81        }
82        (self.written_chunks() as f64 / total as f64) * 100.0
83    }
84}
85
86/// Aggregate statistics across all managed objects.
87#[derive(Clone, Debug, Default)]
88pub struct ChunkManagerStats {
89    /// Total number of objects being tracked.
90    pub total_objects: usize,
91    /// Total number of chunks across all objects.
92    pub total_chunks: usize,
93    /// Number of chunks in Written or Verified state.
94    pub written_chunks: usize,
95    /// Number of chunks in Failed state.
96    pub failed_chunks: usize,
97    /// Total bytes across all objects.
98    pub total_bytes: u64,
99}
100
101/// Computes FNV-1a hash of the 8-byte little-endian representation of `value`.
102pub fn fnv1a_u64(value: u64) -> u64 {
103    const FNV_OFFSET_BASIS: u64 = 14_695_981_039_346_656_037;
104    const FNV_PRIME: u64 = 1_099_511_628_211;
105
106    let bytes = value.to_le_bytes();
107    let mut hash = FNV_OFFSET_BASIS;
108    for &byte in &bytes {
109        hash ^= byte as u64;
110        hash = hash.wrapping_mul(FNV_PRIME);
111    }
112    hash
113}
114
115/// Manages splitting large data objects into fixed-size chunks and tracking
116/// their metadata for streaming reads and writes.
117pub struct StorageChunkManager {
118    /// Map from object_id to its chunked representation.
119    pub objects: HashMap<u64, ChunkedObject>,
120    /// Counter for assigning monotonically increasing object IDs.
121    pub next_object_id: u64,
122    /// Default chunk size in bytes (1 MB).
123    pub chunk_size_bytes: u64,
124}
125
126impl StorageChunkManager {
127    /// Creates a new `StorageChunkManager` with the given chunk size.
128    pub fn new(chunk_size_bytes: u64) -> Self {
129        Self {
130            objects: HashMap::new(),
131            next_object_id: 1,
132            chunk_size_bytes,
133        }
134    }
135
136    /// Splits a data object of `total_size_bytes` into chunks and registers it.
137    ///
138    /// Returns the assigned object_id.
139    pub fn create_object(&mut self, total_size_bytes: u64) -> u64 {
140        let object_id = self.next_object_id;
141        self.next_object_id += 1;
142
143        let chunk_size = self.chunk_size_bytes;
144        let num_chunks = total_size_bytes.div_ceil(chunk_size).max(1);
145
146        let mut chunks = Vec::with_capacity(num_chunks as usize);
147        let mut remaining = total_size_bytes;
148
149        for chunk_index in 0..num_chunks {
150            let offset = chunk_index * chunk_size;
151            let size = remaining.min(chunk_size);
152            remaining = remaining.saturating_sub(size);
153
154            let checksum_input = object_id ^ chunk_index;
155            let checksum = fnv1a_u64(checksum_input);
156
157            // chunk_id encodes object and index for uniqueness
158            let chunk_id = object_id.wrapping_shl(32) | (chunk_index & 0xFFFF_FFFF);
159
160            chunks.push(Chunk {
161                chunk_id,
162                object_id,
163                chunk_index: chunk_index as u32,
164                offset,
165                size_bytes: size,
166                state: ChunkState::Pending,
167                checksum,
168            });
169        }
170
171        self.objects.insert(
172            object_id,
173            ChunkedObject {
174                object_id,
175                total_size_bytes,
176                chunk_size_bytes: chunk_size,
177                chunks,
178            },
179        );
180
181        object_id
182    }
183
184    /// Sets the specified chunk to `Written`. Returns false if not found.
185    pub fn mark_written(&mut self, object_id: u64, chunk_index: u32) -> bool {
186        self.set_chunk_state(object_id, chunk_index, ChunkState::Written)
187    }
188
189    /// Sets the specified chunk to `Verified`. Returns false if not found.
190    pub fn mark_verified(&mut self, object_id: u64, chunk_index: u32) -> bool {
191        self.set_chunk_state(object_id, chunk_index, ChunkState::Verified)
192    }
193
194    /// Sets the specified chunk to `Failed`. Returns false if not found.
195    pub fn mark_failed(&mut self, object_id: u64, chunk_index: u32) -> bool {
196        self.set_chunk_state(object_id, chunk_index, ChunkState::Failed)
197    }
198
199    /// Returns a reference to the `ChunkedObject` if it exists.
200    pub fn get_object(&self, object_id: u64) -> Option<&ChunkedObject> {
201        self.objects.get(&object_id)
202    }
203
204    /// Returns all `Pending` chunks for the given object, sorted by chunk_index.
205    pub fn pending_chunks(&self, object_id: u64) -> Vec<&Chunk> {
206        match self.objects.get(&object_id) {
207            None => Vec::new(),
208            Some(obj) => {
209                let mut pending: Vec<&Chunk> = obj
210                    .chunks
211                    .iter()
212                    .filter(|c| c.state == ChunkState::Pending)
213                    .collect();
214                pending.sort_by_key(|c| c.chunk_index);
215                pending
216            }
217        }
218    }
219
220    /// Removes the object from the manager. Returns true if it existed.
221    pub fn delete_object(&mut self, object_id: u64) -> bool {
222        self.objects.remove(&object_id).is_some()
223    }
224
225    /// Returns aggregate statistics over all tracked objects.
226    pub fn stats(&self) -> ChunkManagerStats {
227        let mut stats = ChunkManagerStats {
228            total_objects: self.objects.len(),
229            ..Default::default()
230        };
231
232        for obj in self.objects.values() {
233            stats.total_chunks += obj.total_chunks();
234            stats.written_chunks += obj
235                .chunks
236                .iter()
237                .filter(|c| matches!(c.state, ChunkState::Written | ChunkState::Verified))
238                .count();
239            stats.failed_chunks += obj
240                .chunks
241                .iter()
242                .filter(|c| c.state == ChunkState::Failed)
243                .count();
244            stats.total_bytes += obj.total_size_bytes;
245        }
246
247        stats
248    }
249
250    // --- Internal helpers ---
251
252    fn set_chunk_state(&mut self, object_id: u64, chunk_index: u32, state: ChunkState) -> bool {
253        match self.objects.get_mut(&object_id) {
254            None => false,
255            Some(obj) => match obj.chunks.iter_mut().find(|c| c.chunk_index == chunk_index) {
256                None => false,
257                Some(chunk) => {
258                    chunk.state = state;
259                    true
260                }
261            },
262        }
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269
270    // --- Construction ---
271
272    #[test]
273    fn test_new_with_chunk_size() {
274        let mgr = StorageChunkManager::new(512);
275        assert_eq!(mgr.chunk_size_bytes, 512);
276        assert!(mgr.objects.is_empty());
277    }
278
279    #[test]
280    fn test_new_default_chunk_size_constant() {
281        let mgr = StorageChunkManager::new(1_048_576);
282        assert_eq!(mgr.chunk_size_bytes, 1_048_576);
283    }
284
285    // --- create_object: monotonic IDs ---
286
287    #[test]
288    fn test_create_object_returns_monotonic_ids() {
289        let mut mgr = StorageChunkManager::new(1024);
290        let id1 = mgr.create_object(1024);
291        let id2 = mgr.create_object(1024);
292        let id3 = mgr.create_object(1024);
293        assert!(id1 < id2);
294        assert!(id2 < id3);
295    }
296
297    // --- create_object: chunk count exact fit ---
298
299    #[test]
300    fn test_create_object_exact_chunk_count() {
301        let chunk_size = 1024_u64;
302        let mut mgr = StorageChunkManager::new(chunk_size);
303        let id = mgr.create_object(chunk_size * 4);
304        let obj = mgr.get_object(id).expect("object should exist");
305        assert_eq!(obj.total_chunks(), 4);
306    }
307
308    // --- create_object: chunk count partial last chunk ---
309
310    #[test]
311    fn test_create_object_partial_last_chunk_count() {
312        let chunk_size = 1024_u64;
313        let mut mgr = StorageChunkManager::new(chunk_size);
314        let id = mgr.create_object(chunk_size * 3 + 1);
315        let obj = mgr.get_object(id).expect("object should exist");
316        assert_eq!(obj.total_chunks(), 4);
317    }
318
319    // --- last chunk smaller size ---
320
321    #[test]
322    fn test_last_chunk_has_correct_smaller_size() {
323        let chunk_size = 1024_u64;
324        let remainder = 300_u64;
325        let mut mgr = StorageChunkManager::new(chunk_size);
326        let id = mgr.create_object(chunk_size * 2 + remainder);
327        let obj = mgr.get_object(id).expect("object should exist");
328        assert_eq!(obj.total_chunks(), 3);
329        let last = obj.chunks.last().expect("must have last chunk");
330        assert_eq!(last.size_bytes, remainder);
331    }
332
333    // --- chunk offsets sequential ---
334
335    #[test]
336    fn test_chunk_offsets_are_sequential() {
337        let chunk_size = 512_u64;
338        let mut mgr = StorageChunkManager::new(chunk_size);
339        let id = mgr.create_object(chunk_size * 5);
340        let obj = mgr.get_object(id).expect("object should exist");
341        for (i, chunk) in obj.chunks.iter().enumerate() {
342            assert_eq!(chunk.offset, i as u64 * chunk_size);
343        }
344    }
345
346    // --- checksum computed ---
347
348    #[test]
349    fn test_chunk_checksums_computed() {
350        let mut mgr = StorageChunkManager::new(256);
351        let id = mgr.create_object(256 * 3);
352        let obj = mgr.get_object(id).expect("object should exist");
353        for chunk in &obj.chunks {
354            let expected = fnv1a_u64(chunk.object_id ^ (chunk.chunk_index as u64));
355            assert_eq!(chunk.checksum, expected);
356        }
357    }
358
359    // --- mark_written ---
360
361    #[test]
362    fn test_mark_written_sets_written_state() {
363        let mut mgr = StorageChunkManager::new(1024);
364        let id = mgr.create_object(2048);
365        assert!(mgr.mark_written(id, 0));
366        let obj = mgr.get_object(id).expect("object should exist");
367        assert_eq!(obj.chunks[0].state, ChunkState::Written);
368    }
369
370    #[test]
371    fn test_mark_written_returns_false_for_unknown_object() {
372        let mut mgr = StorageChunkManager::new(1024);
373        assert!(!mgr.mark_written(999, 0));
374    }
375
376    #[test]
377    fn test_mark_written_returns_false_for_unknown_chunk_index() {
378        let mut mgr = StorageChunkManager::new(1024);
379        let id = mgr.create_object(1024);
380        assert!(!mgr.mark_written(id, 99));
381    }
382
383    // --- mark_verified ---
384
385    #[test]
386    fn test_mark_verified_sets_verified_state() {
387        let mut mgr = StorageChunkManager::new(1024);
388        let id = mgr.create_object(2048);
389        assert!(mgr.mark_verified(id, 1));
390        let obj = mgr.get_object(id).expect("object should exist");
391        assert_eq!(obj.chunks[1].state, ChunkState::Verified);
392    }
393
394    // --- mark_failed ---
395
396    #[test]
397    fn test_mark_failed_sets_failed_state() {
398        let mut mgr = StorageChunkManager::new(1024);
399        let id = mgr.create_object(2048);
400        assert!(mgr.mark_failed(id, 0));
401        let obj = mgr.get_object(id).expect("object should exist");
402        assert_eq!(obj.chunks[0].state, ChunkState::Failed);
403    }
404
405    // --- is_complete ---
406
407    #[test]
408    fn test_is_complete_true_when_all_written() {
409        let mut mgr = StorageChunkManager::new(1024);
410        let id = mgr.create_object(2048);
411        mgr.mark_written(id, 0);
412        mgr.mark_written(id, 1);
413        let obj = mgr.get_object(id).expect("object should exist");
414        assert!(obj.is_complete());
415    }
416
417    #[test]
418    fn test_is_complete_true_when_all_verified() {
419        let mut mgr = StorageChunkManager::new(1024);
420        let id = mgr.create_object(2048);
421        mgr.mark_verified(id, 0);
422        mgr.mark_verified(id, 1);
423        let obj = mgr.get_object(id).expect("object should exist");
424        assert!(obj.is_complete());
425    }
426
427    #[test]
428    fn test_is_complete_true_mixed_written_verified() {
429        let mut mgr = StorageChunkManager::new(1024);
430        let id = mgr.create_object(2048);
431        mgr.mark_written(id, 0);
432        mgr.mark_verified(id, 1);
433        let obj = mgr.get_object(id).expect("object should exist");
434        assert!(obj.is_complete());
435    }
436
437    #[test]
438    fn test_is_complete_false_when_pending_remain() {
439        let mut mgr = StorageChunkManager::new(1024);
440        let id = mgr.create_object(2048);
441        mgr.mark_written(id, 0);
442        // chunk 1 still Pending
443        let obj = mgr.get_object(id).expect("object should exist");
444        assert!(!obj.is_complete());
445    }
446
447    // --- completion_pct ---
448
449    #[test]
450    fn test_completion_pct_correct() {
451        let mut mgr = StorageChunkManager::new(1024);
452        let id = mgr.create_object(4096); // 4 chunks
453        mgr.mark_written(id, 0);
454        mgr.mark_verified(id, 1);
455        let obj = mgr.get_object(id).expect("object should exist");
456        let pct = obj.completion_pct();
457        assert!((pct - 50.0).abs() < f64::EPSILON);
458    }
459
460    #[test]
461    fn test_completion_pct_zero_when_empty_chunks() {
462        let obj = ChunkedObject {
463            object_id: 1,
464            total_size_bytes: 0,
465            chunk_size_bytes: 1024,
466            chunks: Vec::new(),
467        };
468        assert_eq!(obj.completion_pct(), 0.0);
469    }
470
471    // --- written_chunks counts Written + Verified ---
472
473    #[test]
474    fn test_written_chunks_counts_written_and_verified() {
475        let mut mgr = StorageChunkManager::new(512);
476        let id = mgr.create_object(512 * 4);
477        mgr.mark_written(id, 0);
478        mgr.mark_verified(id, 2);
479        mgr.mark_failed(id, 3);
480        let obj = mgr.get_object(id).expect("object should exist");
481        assert_eq!(obj.written_chunks(), 2);
482    }
483
484    // --- pending_chunks ---
485
486    #[test]
487    fn test_pending_chunks_returns_only_pending_sorted() {
488        let mut mgr = StorageChunkManager::new(512);
489        let id = mgr.create_object(512 * 4);
490        mgr.mark_written(id, 1);
491        mgr.mark_verified(id, 3);
492        let pending = mgr.pending_chunks(id);
493        assert_eq!(pending.len(), 2);
494        assert_eq!(pending[0].chunk_index, 0);
495        assert_eq!(pending[1].chunk_index, 2);
496    }
497
498    #[test]
499    fn test_pending_chunks_empty_for_unknown_object() {
500        let mgr = StorageChunkManager::new(512);
501        let pending = mgr.pending_chunks(999);
502        assert!(pending.is_empty());
503    }
504
505    // --- get_object ---
506
507    #[test]
508    fn test_get_object_returns_some_for_existing() {
509        let mut mgr = StorageChunkManager::new(1024);
510        let id = mgr.create_object(1024);
511        assert!(mgr.get_object(id).is_some());
512    }
513
514    #[test]
515    fn test_get_object_returns_none_for_missing() {
516        let mgr = StorageChunkManager::new(1024);
517        assert!(mgr.get_object(42).is_none());
518    }
519
520    // --- delete_object ---
521
522    #[test]
523    fn test_delete_object_returns_true_for_existing() {
524        let mut mgr = StorageChunkManager::new(1024);
525        let id = mgr.create_object(1024);
526        assert!(mgr.delete_object(id));
527        assert!(mgr.get_object(id).is_none());
528    }
529
530    #[test]
531    fn test_delete_object_returns_false_for_missing() {
532        let mut mgr = StorageChunkManager::new(1024);
533        assert!(!mgr.delete_object(999));
534    }
535
536    // --- stats ---
537
538    #[test]
539    fn test_stats_total_objects_and_chunks() {
540        let mut mgr = StorageChunkManager::new(1024);
541        mgr.create_object(2048); // 2 chunks
542        mgr.create_object(4096); // 4 chunks
543        let s = mgr.stats();
544        assert_eq!(s.total_objects, 2);
545        assert_eq!(s.total_chunks, 6);
546    }
547
548    #[test]
549    fn test_stats_written_and_failed_chunks() {
550        let mut mgr = StorageChunkManager::new(1024);
551        let id = mgr.create_object(4096); // 4 chunks
552        mgr.mark_written(id, 0);
553        mgr.mark_verified(id, 1);
554        mgr.mark_failed(id, 2);
555        let s = mgr.stats();
556        assert_eq!(s.written_chunks, 2);
557        assert_eq!(s.failed_chunks, 1);
558    }
559
560    #[test]
561    fn test_stats_total_bytes() {
562        let mut mgr = StorageChunkManager::new(1024);
563        mgr.create_object(2000);
564        mgr.create_object(3000);
565        let s = mgr.stats();
566        assert_eq!(s.total_bytes, 5000);
567    }
568
569    // --- fnv1a helper ---
570
571    #[test]
572    fn test_fnv1a_u64_deterministic() {
573        let h1 = fnv1a_u64(42);
574        let h2 = fnv1a_u64(42);
575        assert_eq!(h1, h2);
576    }
577
578    #[test]
579    fn test_fnv1a_u64_different_inputs_produce_different_hashes() {
580        let h1 = fnv1a_u64(0);
581        let h2 = fnv1a_u64(1);
582        assert_ne!(h1, h2);
583    }
584}