Skip to main content

ipfrs_storage/
write_buffer.rs

1//! Storage Write-Ahead Buffer — buffers incoming writes in memory before
2//! flushing to the underlying storage backend.
3//!
4//! Provides durability semantics via monotonic sequence numbers so that any
5//! unflushed entries can be replayed after a crash.
6
7// ---------------------------------------------------------------------------
8// WriteOp
9// ---------------------------------------------------------------------------
10
11/// An individual write operation staged in the buffer.
12#[derive(Clone, Debug, PartialEq)]
13pub enum WriteOp {
14    /// Store a new block identified by `cid`.
15    Put {
16        /// Content identifier.
17        cid: String,
18        /// FNV / SHA hash of the block data.
19        data_hash: u64,
20        /// Size of the block payload in bytes.
21        size_bytes: u64,
22    },
23    /// Remove the block identified by `cid`.
24    Delete {
25        /// Content identifier of the block to remove.
26        cid: String,
27    },
28    /// Update the stored hash for an existing block.
29    Update {
30        /// Content identifier of the block to update.
31        cid: String,
32        /// Replacement hash value.
33        new_hash: u64,
34    },
35}
36
37// ---------------------------------------------------------------------------
38// BufferedEntry
39// ---------------------------------------------------------------------------
40
41/// A single entry held in the [`StorageWriteAheadBuffer`].
42#[derive(Clone, Debug)]
43pub struct BufferedEntry {
44    /// Monotonically increasing sequence number assigned at write time.
45    pub seq: u64,
46    /// The operation being buffered.
47    pub op: WriteOp,
48    /// Wall-clock seconds at which the entry was buffered (caller-supplied).
49    pub buffered_at_secs: u64,
50    /// `true` once the entry has been included in a [`flush`](StorageWriteAheadBuffer::flush) call.
51    pub flushed: bool,
52}
53
54// ---------------------------------------------------------------------------
55// FlushResult
56// ---------------------------------------------------------------------------
57
58/// Summary returned by [`StorageWriteAheadBuffer::flush`].
59#[derive(Clone, Debug, PartialEq)]
60pub struct FlushResult {
61    /// Number of entries that were marked flushed in this batch.
62    pub flushed_count: usize,
63    /// Lowest sequence number in the flush batch (`u64::MAX` when batch is empty).
64    pub from_seq: u64,
65    /// Highest sequence number in the flush batch (`0` when batch is empty).
66    pub to_seq: u64,
67    /// Sum of `size_bytes` for every `Put` operation in the flush batch.
68    pub total_bytes: u64,
69}
70
71// ---------------------------------------------------------------------------
72// BufferConfig
73// ---------------------------------------------------------------------------
74
75/// Configuration knobs for [`StorageWriteAheadBuffer`].
76#[derive(Clone, Debug)]
77pub struct BufferConfig {
78    /// Auto-flush when the number of buffered (unflushed) entries reaches this limit.
79    pub max_buffered_entries: usize,
80    /// Auto-flush when the total payload bytes of buffered `Put` ops reaches this limit.
81    pub max_buffered_bytes: u64,
82    /// Auto-flush when the oldest unflushed entry is at least this many seconds old.
83    pub flush_interval_secs: u64,
84}
85
86impl Default for BufferConfig {
87    fn default() -> Self {
88        Self {
89            max_buffered_entries: 1_000,
90            max_buffered_bytes: 64 * 1024 * 1024, // 64 MiB
91            flush_interval_secs: 5,
92        }
93    }
94}
95
96// ---------------------------------------------------------------------------
97// StorageWriteAheadBuffer
98// ---------------------------------------------------------------------------
99
100/// In-memory write-ahead buffer with sequence-numbered entries.
101///
102/// Incoming [`WriteOp`]s are appended with monotonically increasing sequence
103/// numbers.  The buffer tracks total payload bytes and the age of the oldest
104/// unflushed entry so that callers can decide when to flush.
105///
106/// After a flush the entries remain in `entries` (marked `flushed = true`)
107/// until [`trim_flushed`](Self::trim_flushed) is called, enabling crash-recovery
108/// replay via [`replay_from`](Self::replay_from).
109pub struct StorageWriteAheadBuffer {
110    /// All entries in arrival order.
111    pub entries: Vec<BufferedEntry>,
112    /// Configuration controlling flush thresholds.
113    pub config: BufferConfig,
114    /// Next sequence number to assign.
115    pub next_seq: u64,
116    /// Running total of `size_bytes` for unflushed `Put` ops.
117    pub total_buffered_bytes: u64,
118}
119
120impl StorageWriteAheadBuffer {
121    // -----------------------------------------------------------------------
122    // Construction
123    // -----------------------------------------------------------------------
124
125    /// Create a new, empty buffer with the supplied configuration.
126    pub fn new(config: BufferConfig) -> Self {
127        Self {
128            entries: Vec::new(),
129            config,
130            next_seq: 0,
131            total_buffered_bytes: 0,
132        }
133    }
134
135    // -----------------------------------------------------------------------
136    // Write
137    // -----------------------------------------------------------------------
138
139    /// Stage `op` in the buffer and return the assigned sequence number.
140    ///
141    /// * `Put` ops increment [`total_buffered_bytes`](Self::total_buffered_bytes)
142    ///   by their `size_bytes`.
143    /// * `Delete` and `Update` ops do not affect the byte counter.
144    pub fn write(&mut self, op: WriteOp, now_secs: u64) -> u64 {
145        let seq = self.next_seq;
146        self.next_seq += 1;
147
148        if let WriteOp::Put { size_bytes, .. } = &op {
149            self.total_buffered_bytes = self.total_buffered_bytes.saturating_add(*size_bytes);
150        }
151
152        self.entries.push(BufferedEntry {
153            seq,
154            op,
155            buffered_at_secs: now_secs,
156            flushed: false,
157        });
158
159        seq
160    }
161
162    // -----------------------------------------------------------------------
163    // Flush decision
164    // -----------------------------------------------------------------------
165
166    /// Returns `true` if any flush threshold has been reached.
167    ///
168    /// Triggers when:
169    /// * the number of unflushed entries ≥ [`max_buffered_entries`](BufferConfig::max_buffered_entries), **or**
170    /// * [`total_buffered_bytes`](Self::total_buffered_bytes) ≥ [`max_buffered_bytes`](BufferConfig::max_buffered_bytes), **or**
171    /// * the oldest unflushed entry's age ≥ [`flush_interval_secs`](BufferConfig::flush_interval_secs).
172    pub fn should_flush(&self, now_secs: u64) -> bool {
173        let pending: Vec<&BufferedEntry> = self.pending_entries();
174
175        if pending.len() >= self.config.max_buffered_entries {
176            return true;
177        }
178
179        if self.total_buffered_bytes >= self.config.max_buffered_bytes {
180            return true;
181        }
182
183        if let Some(oldest) = pending.first() {
184            let age = now_secs.saturating_sub(oldest.buffered_at_secs);
185            if age >= self.config.flush_interval_secs {
186                return true;
187            }
188        }
189
190        false
191    }
192
193    // -----------------------------------------------------------------------
194    // Flush
195    // -----------------------------------------------------------------------
196
197    /// Mark every unflushed entry as flushed and return a [`FlushResult`].
198    ///
199    /// After this call [`total_buffered_bytes`](Self::total_buffered_bytes) is
200    /// reset to `0`.  Flushed entries are retained in [`entries`](Self::entries)
201    /// until [`trim_flushed`](Self::trim_flushed) is called.
202    pub fn flush(&mut self, _now_secs: u64) -> FlushResult {
203        let mut flushed_count: usize = 0;
204        let mut from_seq: u64 = u64::MAX;
205        let mut to_seq: u64 = 0;
206        let mut total_bytes: u64 = 0;
207
208        for entry in self.entries.iter_mut().filter(|e| !e.flushed) {
209            entry.flushed = true;
210            flushed_count += 1;
211
212            if entry.seq < from_seq {
213                from_seq = entry.seq;
214            }
215            if entry.seq > to_seq {
216                to_seq = entry.seq;
217            }
218
219            if let WriteOp::Put { size_bytes, .. } = &entry.op {
220                total_bytes = total_bytes.saturating_add(*size_bytes);
221            }
222        }
223
224        self.total_buffered_bytes = 0;
225
226        FlushResult {
227            flushed_count,
228            from_seq,
229            to_seq,
230            total_bytes,
231        }
232    }
233
234    // -----------------------------------------------------------------------
235    // Queries
236    // -----------------------------------------------------------------------
237
238    /// Returns references to all entries that have **not** yet been flushed,
239    /// in arrival order.
240    pub fn pending_entries(&self) -> Vec<&BufferedEntry> {
241        self.entries.iter().filter(|e| !e.flushed).collect()
242    }
243
244    /// Returns references to all entries whose sequence number is ≥ `seq`,
245    /// regardless of flushed state.  Useful for crash-recovery replay.
246    pub fn replay_from(&self, seq: u64) -> Vec<&BufferedEntry> {
247        self.entries.iter().filter(|e| e.seq >= seq).collect()
248    }
249
250    // -----------------------------------------------------------------------
251    // Maintenance
252    // -----------------------------------------------------------------------
253
254    /// Remove all entries that have been flushed, freeing memory.
255    ///
256    /// After this call, [`replay_from`](Self::replay_from) will only see
257    /// entries that have not yet been flushed.
258    pub fn trim_flushed(&mut self) {
259        self.entries.retain(|e| !e.flushed);
260    }
261
262    /// Returns `(total_entry_count, unflushed_entry_count)`.
263    pub fn stats(&self) -> (usize, u64) {
264        let total = self.entries.len();
265        let unflushed = self.entries.iter().filter(|e| !e.flushed).count() as u64;
266        (total, unflushed)
267    }
268}
269
270// ---------------------------------------------------------------------------
271// Tests
272// ---------------------------------------------------------------------------
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    fn default_buf() -> StorageWriteAheadBuffer {
279        StorageWriteAheadBuffer::new(BufferConfig::default())
280    }
281
282    fn put_op(cid: &str, size: u64) -> WriteOp {
283        WriteOp::Put {
284            cid: cid.to_string(),
285            data_hash: 0xDEAD_BEEF,
286            size_bytes: size,
287        }
288    }
289
290    fn delete_op(cid: &str) -> WriteOp {
291        WriteOp::Delete {
292            cid: cid.to_string(),
293        }
294    }
295
296    fn update_op(cid: &str, hash: u64) -> WriteOp {
297        WriteOp::Update {
298            cid: cid.to_string(),
299            new_hash: hash,
300        }
301    }
302
303    // 1. Sequential sequence numbers
304    #[test]
305    fn test_write_assigns_sequential_seqs() {
306        let mut buf = default_buf();
307        let s0 = buf.write(put_op("cid0", 100), 0);
308        let s1 = buf.write(put_op("cid1", 200), 0);
309        let s2 = buf.write(delete_op("cid0"), 0);
310        assert_eq!(s0, 0);
311        assert_eq!(s1, 1);
312        assert_eq!(s2, 2);
313    }
314
315    // 2. next_seq advances correctly
316    #[test]
317    fn test_next_seq_increments() {
318        let mut buf = default_buf();
319        assert_eq!(buf.next_seq, 0);
320        buf.write(put_op("a", 10), 0);
321        assert_eq!(buf.next_seq, 1);
322        buf.write(put_op("b", 20), 0);
323        assert_eq!(buf.next_seq, 2);
324    }
325
326    // 3. Put increments total_buffered_bytes
327    #[test]
328    fn test_put_increments_buffered_bytes() {
329        let mut buf = default_buf();
330        buf.write(put_op("a", 512), 0);
331        assert_eq!(buf.total_buffered_bytes, 512);
332        buf.write(put_op("b", 1024), 0);
333        assert_eq!(buf.total_buffered_bytes, 1536);
334    }
335
336    // 4. Delete does NOT increment total_buffered_bytes
337    #[test]
338    fn test_delete_does_not_increment_bytes() {
339        let mut buf = default_buf();
340        buf.write(delete_op("a"), 0);
341        assert_eq!(buf.total_buffered_bytes, 0);
342    }
343
344    // 5. Update does NOT increment total_buffered_bytes
345    #[test]
346    fn test_update_does_not_increment_bytes() {
347        let mut buf = default_buf();
348        buf.write(update_op("a", 42), 0);
349        assert_eq!(buf.total_buffered_bytes, 0);
350    }
351
352    // 6. should_flush triggers on count threshold
353    #[test]
354    fn test_should_flush_on_count() {
355        let cfg = BufferConfig {
356            max_buffered_entries: 3,
357            max_buffered_bytes: u64::MAX,
358            flush_interval_secs: u64::MAX,
359        };
360        let mut buf = StorageWriteAheadBuffer::new(cfg);
361        buf.write(put_op("a", 1), 0);
362        buf.write(put_op("b", 1), 0);
363        assert!(!buf.should_flush(0));
364        buf.write(put_op("c", 1), 0);
365        assert!(buf.should_flush(0));
366    }
367
368    // 7. should_flush triggers on byte threshold
369    #[test]
370    fn test_should_flush_on_bytes() {
371        let cfg = BufferConfig {
372            max_buffered_entries: usize::MAX,
373            max_buffered_bytes: 100,
374            flush_interval_secs: u64::MAX,
375        };
376        let mut buf = StorageWriteAheadBuffer::new(cfg);
377        buf.write(put_op("a", 60), 0);
378        assert!(!buf.should_flush(0));
379        buf.write(put_op("b", 40), 0);
380        assert!(buf.should_flush(0));
381    }
382
383    // 8. should_flush triggers on age threshold
384    #[test]
385    fn test_should_flush_on_age() {
386        let cfg = BufferConfig {
387            max_buffered_entries: usize::MAX,
388            max_buffered_bytes: u64::MAX,
389            flush_interval_secs: 5,
390        };
391        let mut buf = StorageWriteAheadBuffer::new(cfg);
392        buf.write(put_op("a", 1), 100); // buffered at t=100
393        assert!(!buf.should_flush(104)); // age = 4 < 5
394        assert!(buf.should_flush(105)); // age = 5 >= 5
395    }
396
397    // 9. Empty buffer never triggers should_flush (age path skipped)
398    #[test]
399    fn test_should_flush_empty() {
400        let buf = default_buf();
401        assert!(!buf.should_flush(9999));
402    }
403
404    // 10. flush marks all unflushed entries as flushed
405    #[test]
406    fn test_flush_marks_entries_flushed() {
407        let mut buf = default_buf();
408        buf.write(put_op("a", 10), 0);
409        buf.write(delete_op("b"), 0);
410        buf.flush(1);
411        assert!(buf.entries.iter().all(|e| e.flushed));
412    }
413
414    // 11. FlushResult flushed_count
415    #[test]
416    fn test_flush_result_count() {
417        let mut buf = default_buf();
418        buf.write(put_op("a", 10), 0);
419        buf.write(put_op("b", 20), 0);
420        buf.write(delete_op("c"), 0);
421        let result = buf.flush(1);
422        assert_eq!(result.flushed_count, 3);
423    }
424
425    // 12. FlushResult from_seq / to_seq
426    #[test]
427    fn test_flush_result_seq_range() {
428        let mut buf = default_buf();
429        buf.write(put_op("a", 10), 0); // seq 0
430        buf.write(put_op("b", 20), 0); // seq 1
431        buf.write(delete_op("c"), 0); // seq 2
432        let result = buf.flush(1);
433        assert_eq!(result.from_seq, 0);
434        assert_eq!(result.to_seq, 2);
435    }
436
437    // 13. FlushResult total_bytes sums only Put ops
438    #[test]
439    fn test_flush_result_total_bytes() {
440        let mut buf = default_buf();
441        buf.write(put_op("a", 100), 0);
442        buf.write(delete_op("b"), 0);
443        buf.write(put_op("c", 200), 0);
444        buf.write(update_op("d", 7), 0);
445        let result = buf.flush(1);
446        assert_eq!(result.total_bytes, 300);
447    }
448
449    // 14. flush resets total_buffered_bytes to 0
450    #[test]
451    fn test_flush_resets_buffered_bytes() {
452        let mut buf = default_buf();
453        buf.write(put_op("a", 512), 0);
454        assert_eq!(buf.total_buffered_bytes, 512);
455        buf.flush(1);
456        assert_eq!(buf.total_buffered_bytes, 0);
457    }
458
459    // 15. pending_entries returns 0 after flush
460    #[test]
461    fn test_pending_entries_after_flush_is_zero() {
462        let mut buf = default_buf();
463        buf.write(put_op("a", 10), 0);
464        buf.write(delete_op("b"), 0);
465        buf.flush(1);
466        assert_eq!(buf.pending_entries().len(), 0);
467    }
468
469    // 16. pending_entries only returns unflushed
470    #[test]
471    fn test_pending_entries_partial_flush() {
472        let mut buf = default_buf();
473        buf.write(put_op("a", 10), 0); // seq 0
474        buf.write(put_op("b", 20), 0); // seq 1
475        buf.flush(1); // both flushed
476        buf.write(put_op("c", 30), 0); // seq 2 — new, unflushed
477        let pending = buf.pending_entries();
478        assert_eq!(pending.len(), 1);
479        assert_eq!(pending[0].seq, 2);
480    }
481
482    // 17. replay_from returns entries with seq >= given value
483    #[test]
484    fn test_replay_from() {
485        let mut buf = default_buf();
486        buf.write(put_op("a", 10), 0); // seq 0
487        buf.write(put_op("b", 20), 0); // seq 1
488        buf.write(delete_op("c"), 0); // seq 2
489        buf.flush(1);
490
491        let replayed = buf.replay_from(1);
492        assert_eq!(replayed.len(), 2);
493        assert_eq!(replayed[0].seq, 1);
494        assert_eq!(replayed[1].seq, 2);
495    }
496
497    // 18. replay_from(0) returns all entries
498    #[test]
499    fn test_replay_from_zero_returns_all() {
500        let mut buf = default_buf();
501        buf.write(put_op("a", 10), 0);
502        buf.write(put_op("b", 20), 0);
503        buf.flush(1);
504        assert_eq!(buf.replay_from(0).len(), 2);
505    }
506
507    // 19. trim_flushed removes flushed entries
508    #[test]
509    fn test_trim_flushed_removes_flushed() {
510        let mut buf = default_buf();
511        buf.write(put_op("a", 10), 0); // seq 0
512        buf.write(put_op("b", 20), 0); // seq 1
513        buf.flush(1);
514        buf.write(put_op("c", 30), 0); // seq 2 — unflushed
515        buf.trim_flushed();
516        assert_eq!(buf.entries.len(), 1);
517        assert_eq!(buf.entries[0].seq, 2);
518    }
519
520    // 20. trim_flushed on all-unflushed buffer is a no-op
521    #[test]
522    fn test_trim_flushed_noop_when_nothing_flushed() {
523        let mut buf = default_buf();
524        buf.write(put_op("a", 10), 0);
525        buf.write(put_op("b", 20), 0);
526        buf.trim_flushed();
527        assert_eq!(buf.entries.len(), 2);
528    }
529
530    // 21. stats returns correct total and unflushed counts
531    #[test]
532    fn test_stats_unflushed_count() {
533        let mut buf = default_buf();
534        buf.write(put_op("a", 10), 0); // seq 0
535        buf.write(put_op("b", 20), 0); // seq 1
536        buf.write(delete_op("c"), 0); // seq 2
537        let (total, unflushed) = buf.stats();
538        assert_eq!(total, 3);
539        assert_eq!(unflushed, 3);
540
541        buf.flush(1);
542        let (total2, unflushed2) = buf.stats();
543        assert_eq!(total2, 3);
544        assert_eq!(unflushed2, 0);
545    }
546
547    // 22. stats after trim
548    #[test]
549    fn test_stats_after_trim() {
550        let mut buf = default_buf();
551        buf.write(put_op("a", 10), 0);
552        buf.write(put_op("b", 20), 0);
553        buf.flush(1);
554        buf.write(put_op("c", 30), 0);
555        buf.trim_flushed();
556        let (total, unflushed) = buf.stats();
557        assert_eq!(total, 1);
558        assert_eq!(unflushed, 1);
559    }
560
561    // 23. Empty flush result sentinel values
562    #[test]
563    fn test_flush_empty_buffer_sentinels() {
564        let mut buf = default_buf();
565        let result = buf.flush(0);
566        assert_eq!(result.flushed_count, 0);
567        assert_eq!(result.from_seq, u64::MAX);
568        assert_eq!(result.to_seq, 0);
569        assert_eq!(result.total_bytes, 0);
570    }
571
572    // 24. Second flush (all already flushed) returns zero count
573    #[test]
574    fn test_second_flush_is_empty() {
575        let mut buf = default_buf();
576        buf.write(put_op("a", 10), 0);
577        buf.flush(1);
578        let result2 = buf.flush(2);
579        assert_eq!(result2.flushed_count, 0);
580    }
581
582    // 25. WriteOp derives Clone/Debug/PartialEq
583    #[test]
584    fn test_write_op_derives() {
585        let op1 = WriteOp::Put {
586            cid: "c1".to_string(),
587            data_hash: 1,
588            size_bytes: 64,
589        };
590        let op2 = op1.clone();
591        assert_eq!(op1, op2);
592        let _ = format!("{op1:?}");
593    }
594}