segment_buffer/lib.rs
1//! Durable bounded queue backed by zstd-compressed CBOR segment files.
2//!
3//! Items are accumulated in memory, flushed as zstd-compressed CBOR batches
4//! to `seg_{start:012}_{end:012}.zst` files, and deleted once the consumer
5//! acknowledges receipt via [`SegmentBuffer::delete_acked`].
6//!
7//! The buffer is generic over any `T: Serialize + DeserializeOwned + Clone + Send + 'static`.
8//! Crash recovery is filename-based: scanning the directory rebuilds `head_seq`
9//! and `next_seq` without any WAL or metadata database.
10//!
11//! # Example
12//!
13//! ```no_run
14//! use segment_buffer::{SegmentBuffer, SegmentConfig};
15//! use serde::{Serialize, Deserialize};
16//!
17//! #[derive(Serialize, Deserialize, Clone)]
18//! struct MyItem { id: u64 }
19//!
20//! let buffer = SegmentBuffer::<MyItem>::open("/tmp/my-queue", SegmentConfig::default())?;
21//! let seq = buffer.append(MyItem { id: 1 })?;
22//! let items = buffer.read_from(0, 100)?;
23//! # Ok::<(), Box<dyn std::error::Error>>(())
24//! ```
25
26#![warn(missing_docs)]
27
28mod cipher;
29mod error;
30mod segment;
31
32#[cfg(feature = "encryption")]
33pub use cipher::AesGcmCipher;
34pub use cipher::{CipherError, SegmentCipher};
35pub use error::{Result, SegmentError};
36
37use std::fs;
38use std::path::PathBuf;
39use std::time::Instant;
40
41use parking_lot::Mutex;
42use serde::de::DeserializeOwned;
43use serde::Serialize;
44use tracing::{debug, info};
45
46use segment::SegmentRange;
47
48/// Configuration knobs for [`SegmentBuffer`].
49///
50/// This struct is `#[non_exhaustive]`: new fields may be added in any release
51/// without breaking semver. Construct via [`SegmentConfig::default()`] and then
52/// mutate the public fields you care about:
53///
54/// ```
55/// use segment_buffer::SegmentConfig;
56///
57/// let mut config = SegmentConfig::default();
58/// config.max_batch_events = 64;
59/// config.flush_interval_secs = 1;
60/// ```
61#[non_exhaustive]
62pub struct SegmentConfig {
63 /// Max events accumulated in RAM before auto-flush (default: 256).
64 pub max_batch_events: usize,
65 /// Max seconds between flushes. An append after this interval triggers a
66 /// flush even if the batch threshold hasn't been reached (default: 5s).
67 pub flush_interval_secs: u64,
68 /// Max total disk usage before the buffer reports overload pressure (default: 10 GB).
69 pub max_size_bytes: u64,
70 /// zstd compression level (1-22; 3 is fast with a good ratio).
71 pub compression_level: i32,
72 /// Optional cipher for encrypting segment files at rest. When `None`,
73 /// segments are written as plaintext zstd+CBOR.
74 pub cipher: Option<Box<dyn SegmentCipher>>,
75}
76
77impl std::fmt::Debug for SegmentConfig {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 f.debug_struct("SegmentConfig")
80 .field("max_batch_events", &self.max_batch_events)
81 .field("flush_interval_secs", &self.flush_interval_secs)
82 .field("max_size_bytes", &self.max_size_bytes)
83 .field("compression_level", &self.compression_level)
84 .field("cipher", &self.cipher.as_ref().map(|_| "[set]"))
85 .finish()
86 }
87}
88
89impl Default for SegmentConfig {
90 fn default() -> Self {
91 Self {
92 max_batch_events: 256,
93 flush_interval_secs: 5,
94 max_size_bytes: 10 * 1024 * 1024 * 1024,
95 compression_level: 3,
96 cipher: None,
97 }
98 }
99}
100
101/// Point-in-time snapshot of buffer state, captured atomically under a single
102/// lock acquisition so all fields are mutually consistent.
103///
104/// Returned by [`SegmentBuffer::stats`]. Useful for metrics endpoints or
105/// dashboards that need to observe multiple values without paying for several
106/// lock/unlock round-trips (and risking a torn read between calls).
107///
108/// This struct is `#[non_exhaustive]`: new fields may be added in any release
109/// without breaking semver. It is constructed internally by [`SegmentBuffer::stats`];
110/// callers read fields via dot-syntax or pattern-match with `..` only.
111#[derive(Debug, Clone)]
112#[non_exhaustive]
113pub struct BufferStats {
114 /// Items waiting in the buffer (on-disk + in-memory pending).
115 /// Same value as [`SegmentBuffer::pending_count`].
116 pub pending_count: u64,
117 /// Highest sequence number assigned (or `0` if the buffer is empty).
118 /// Same value as [`SegmentBuffer::latest_sequence`].
119 pub latest_sequence: u64,
120 /// Oldest unacknowledged sequence number (`head_seq`).
121 pub head_sequence: u64,
122 /// Next sequence number that will be assigned by the next successful
123 /// [`SegmentBuffer::append`] (`next_seq`).
124 pub next_sequence: u64,
125 /// Approximate total bytes used by segment files on disk. Decreases when
126 /// [`SegmentBuffer::delete_acked`] removes files.
127 pub approx_disk_bytes: u64,
128 /// Configured ceiling on disk usage (`max_size_bytes`). `0` disables the
129 /// limit; in that case [`store_pressure`](Self::store_pressure) is `0.0`.
130 pub max_size_bytes: u64,
131 /// `approx_disk_bytes / max_size_bytes`, clamped to `[0.0, 1.0]`.
132 /// `0.0` when no limit is configured.
133 pub store_pressure: f32,
134}
135
136struct BufferInner<T> {
137 /// Items buffered in memory, not yet written to a segment file. Drained by
138 /// [`SegmentBuffer::flush`] and rebuilt empty on crash recovery (unflushed
139 /// items do not survive a crash by design).
140 unflushed: Vec<T>,
141 next_seq: u64,
142 head_seq: u64,
143 last_flush: Instant,
144 approx_disk_bytes: u64,
145}
146
147/// Durable bounded queue of `T` backed by compressed segment files.
148///
149/// Thread-safe via `parking_lot::Mutex`. All file I/O is synchronous. The mutex
150/// is never held across an async boundary because there are no await points.
151///
152/// Create with [`SegmentBuffer::open`], supplying the directory and config.
153pub struct SegmentBuffer<T> {
154 dir: PathBuf,
155 config: SegmentConfig,
156 inner: Mutex<BufferInner<T>>,
157}
158
159/// `Debug` mirrors the field set of [`BufferStats`] plus the directory path.
160/// It does NOT print the in-memory `unflushed` items (which could be large or
161/// sensitive), so `T` itself is not required to be `Debug`.
162impl<T> std::fmt::Debug for SegmentBuffer<T>
163where
164 T: Serialize + DeserializeOwned + Clone + Send + 'static,
165{
166 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167 let stats = self.stats();
168 f.debug_struct("SegmentBuffer")
169 .field("dir", &self.dir)
170 .field("pending_count", &stats.pending_count)
171 .field("latest_sequence", &stats.latest_sequence)
172 .field("head_sequence", &stats.head_sequence)
173 .field("next_sequence", &stats.next_sequence)
174 .field("approx_disk_bytes", &stats.approx_disk_bytes)
175 .field("max_size_bytes", &stats.max_size_bytes)
176 .field("store_pressure", &stats.store_pressure)
177 .finish()
178 }
179}
180
181impl<T> SegmentBuffer<T>
182where
183 T: Serialize + DeserializeOwned + Clone + Send + 'static,
184{
185 /// Open (or create) a buffer at `dir`, recovering from any existing
186 /// segment files.
187 ///
188 /// Recovery is **filename-based**: it scans the directory to rebuild
189 /// `head_seq` / `next_seq` and deletes leftover `.tmp` debris. Segment
190 /// *contents* are not read until [`read_from`](Self::read_from), so a
191 /// corrupted segment does not fail here — it fails when read.
192 ///
193 /// # Example
194 ///
195 /// ```
196 /// use segment_buffer::{SegmentBuffer, SegmentConfig};
197 /// use tempfile::tempdir;
198 ///
199 /// let dir = tempdir()?;
200 /// let buf: SegmentBuffer<u64> =
201 /// SegmentBuffer::open(dir.path(), SegmentConfig::default())?;
202 /// # Ok::<(), Box<dyn std::error::Error>>(())
203 /// ```
204 ///
205 /// # Errors
206 ///
207 /// Returns [`SegmentError::Io`] if the directory cannot be created or read.
208 pub fn open(dir: impl Into<PathBuf>, config: SegmentConfig) -> Result<Self> {
209 let dir = dir.into();
210 fs::create_dir_all(&dir)?;
211
212 let buffer = Self {
213 dir,
214 config,
215 inner: Mutex::new(BufferInner {
216 unflushed: Vec::new(),
217 next_seq: 0,
218 head_seq: 0,
219 last_flush: Instant::now(),
220 approx_disk_bytes: 0,
221 }),
222 };
223
224 buffer.recover()?;
225 Ok(buffer)
226 }
227
228 // -----------------------------------------------------------------------
229 // Public API
230 // -----------------------------------------------------------------------
231
232 /// Append an item to the buffer. Assigns the next sequence number and
233 /// auto-flushes if the batch threshold or interval is reached.
234 ///
235 /// Returns the assigned sequence number. The first append returns `0`,
236 /// and the number increments by 1 for each subsequent append.
237 ///
238 /// # Example
239 ///
240 /// ```
241 /// use segment_buffer::{SegmentBuffer, SegmentConfig};
242 /// use tempfile::tempdir;
243 ///
244 /// let dir = tempdir()?;
245 /// let buf: SegmentBuffer<u64> =
246 /// SegmentBuffer::open(dir.path(), SegmentConfig::default())?;
247 ///
248 /// assert_eq!(buf.append(1)?, 0);
249 /// assert_eq!(buf.append(2)?, 1);
250 /// assert_eq!(buf.append(3)?, 2);
251 /// # Ok::<(), Box<dyn std::error::Error>>(())
252 /// ```
253 pub fn append(&self, event: T) -> Result<u64> {
254 let (should_flush, seq) = {
255 let mut inner = self.inner.lock();
256 inner.unflushed.push(event);
257 inner.next_seq += 1;
258 let seq = inner.next_seq - 1;
259
260 let batch_full = inner.unflushed.len() >= self.config.max_batch_events;
261 let interval_elapsed =
262 inner.last_flush.elapsed().as_secs() >= self.config.flush_interval_secs;
263 (batch_full || interval_elapsed, seq)
264 };
265
266 if should_flush {
267 self.flush()?;
268 }
269
270 Ok(seq)
271 }
272
273 /// Flush buffered items to a segment file. No-op if nothing is buffered.
274 ///
275 /// Flushing is also triggered automatically by [`append`](Self::append)
276 /// when the batch threshold (`max_batch_events`) or interval
277 /// (`flush_interval_secs`) is reached. Call this explicitly when you need
278 /// durability before a known threshold.
279 ///
280 /// # Example
281 ///
282 /// ```
283 /// use segment_buffer::{SegmentBuffer, SegmentConfig};
284 /// use tempfile::tempdir;
285 ///
286 /// let dir = tempdir()?;
287 /// let buf: SegmentBuffer<u64> =
288 /// SegmentBuffer::open(dir.path(), SegmentConfig::default())?;
289 /// buf.append(1)?;
290 /// buf.append(2)?;
291 ///
292 /// buf.flush()?; // items now durable on disk
293 /// assert_eq!(buf.pending_count(), 2);
294 /// # Ok::<(), Box<dyn std::error::Error>>(())
295 /// ```
296 pub fn flush(&self) -> Result<()> {
297 let (events, start_seq, end_seq) = {
298 let mut inner = self.inner.lock();
299 inner.last_flush = Instant::now();
300 if inner.unflushed.is_empty() {
301 return Ok(());
302 }
303 let events = std::mem::take(&mut inner.unflushed);
304 let count = events.len() as u64;
305 let end_seq = inner.next_seq - 1;
306 let start_seq = end_seq + 1 - count;
307 (events, start_seq, end_seq)
308 };
309
310 let compressed_len = self.write_segment(start_seq, end_seq, &events)?;
311
312 {
313 let mut inner = self.inner.lock();
314 inner.approx_disk_bytes += compressed_len;
315 }
316
317 debug!(start_seq, end_seq, count = events.len(), "Flushed segment");
318 Ok(())
319 }
320
321 /// Read up to `limit` items starting from `start_seq` (inclusive).
322 ///
323 /// Reads from both on-disk segment files and in-memory pending items.
324 /// Items are returned in ascending sequence order.
325 ///
326 /// Passing `limit = 0` returns an empty `Vec` without scanning.
327 ///
328 /// # Example
329 ///
330 /// ```
331 /// use segment_buffer::{SegmentBuffer, SegmentConfig};
332 /// use tempfile::tempdir;
333 ///
334 /// let dir = tempdir()?;
335 /// let buf: SegmentBuffer<u64> =
336 /// SegmentBuffer::open(dir.path(), SegmentConfig::default())?;
337 /// buf.append(10)?;
338 /// buf.append(20)?;
339 /// buf.append(30)?;
340 /// buf.flush()?;
341 ///
342 /// let items = buf.read_from(0, 100)?;
343 /// assert_eq!(items, vec![10, 20, 30]);
344 ///
345 /// // start_seq skips already-read items:
346 /// let tail = buf.read_from(2, 100)?;
347 /// assert_eq!(tail, vec![30]);
348 /// # Ok::<(), Box<dyn std::error::Error>>(())
349 /// ```
350 pub fn read_from(&self, start_seq: u64, limit: usize) -> Result<Vec<T>> {
351 if limit == 0 {
352 return Ok(Vec::new());
353 }
354
355 let mut result: Vec<T> = Vec::with_capacity(limit.min(1024));
356
357 // Phase 1: read from on-disk segments.
358 let segments = self.scan_segments()?;
359 for seg in &segments {
360 if result.len() >= limit {
361 break;
362 }
363 if seg.end < start_seq {
364 continue;
365 }
366
367 let events = self.read_segment(*seg)?;
368 let skip = if seg.start < start_seq {
369 (start_seq - seg.start) as usize
370 } else {
371 0
372 };
373
374 for event in events.into_iter().skip(skip) {
375 if result.len() >= limit {
376 break;
377 }
378 result.push(event);
379 }
380 }
381
382 // Phase 2: read from in-memory pending events.
383 if result.len() < limit {
384 let inner = self.inner.lock();
385 let pending_start = inner.next_seq.saturating_sub(inner.unflushed.len() as u64);
386 for (i, event) in inner.unflushed.iter().enumerate() {
387 let seq = pending_start + i as u64;
388 if seq < start_seq {
389 continue;
390 }
391 if result.len() >= limit {
392 break;
393 }
394 result.push(event.clone());
395 }
396 }
397
398 Ok(result)
399 }
400
401 /// Delete all on-disk segment files whose items are fully covered by
402 /// `acked_seq`.
403 ///
404 /// A segment is deleted when its `end_seq <= acked_seq`. Returns the number
405 /// of segment files removed.
406 ///
407 /// # Example
408 ///
409 /// ```
410 /// use segment_buffer::{SegmentBuffer, SegmentConfig};
411 /// use tempfile::tempdir;
412 ///
413 /// let dir = tempdir()?;
414 /// let buf: SegmentBuffer<u64> =
415 /// SegmentBuffer::open(dir.path(), SegmentConfig::default())?;
416 /// for i in 0..5u64 {
417 /// buf.append(i)?;
418 /// }
419 /// buf.flush()?;
420 ///
421 /// // Consumer has processed sequence 0..=4; acknowledge them:
422 /// let removed = buf.delete_acked(4)?;
423 /// assert_eq!(removed, 1); // one segment file deleted
424 /// assert_eq!(buf.pending_count(), 0);
425 /// # Ok::<(), Box<dyn std::error::Error>>(())
426 /// ```
427 ///
428 /// # Limitation
429 ///
430 /// Acknowledgement only removes **flushed** segment files. Items still held
431 /// in the in-memory pending batch have no segment file to delete, so they
432 /// remain readable (and counted by [`SegmentBuffer::pending_count`]) until
433 /// they are flushed and acknowledged in a later call. `head_seq` is clamped
434 /// so it never advances past the pending window, keeping the backlog count
435 /// honest.
436 pub fn delete_acked(&self, acked_seq: u64) -> Result<usize> {
437 let segments = self.scan_segments()?;
438 let mut deleted = 0;
439 let mut freed_bytes: u64 = 0;
440 let mut new_head = None;
441
442 for seg in &segments {
443 if seg.end <= acked_seq {
444 let path = self.segment_path(seg.start, seg.end);
445 if let Ok(meta) = fs::metadata(&path) {
446 freed_bytes += meta.len();
447 }
448 match fs::remove_file(&path) {
449 Ok(()) => {
450 deleted += 1;
451 debug!(start = seg.start, end = seg.end, "Deleted acked segment");
452 }
453 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
454 Err(e) => return Err(e.into()),
455 }
456 } else if new_head.is_none() {
457 new_head = Some(seg.start);
458 }
459 }
460
461 {
462 let mut inner = self.inner.lock();
463 inner.approx_disk_bytes = inner.approx_disk_bytes.saturating_sub(freed_bytes);
464 // `head_seq` tracks the oldest unacked sequence. Clamp it to the
465 // start of the in-memory pending window: items still waiting to be
466 // flushed cannot be acknowledged (there is no segment file to
467 // delete), so head_seq must not advance past them. Without this
468 // clamp, acknowledging past a buffer that still holds unflushed
469 // items would make `pending_count` under-report the real backlog.
470 let pending_start = inner.next_seq.saturating_sub(inner.unflushed.len() as u64);
471 inner.head_seq = new_head.unwrap_or(inner.next_seq).min(pending_start);
472 }
473
474 if deleted > 0 {
475 info!(deleted, freed_bytes, acked_seq, "Deleted acked segments");
476 }
477
478 Ok(deleted)
479 }
480
481 /// The highest sequence number assigned (or 0 if buffer is empty).
482 ///
483 /// # Example
484 ///
485 /// ```
486 /// use segment_buffer::{SegmentBuffer, SegmentConfig};
487 /// use tempfile::tempdir;
488 ///
489 /// let dir = tempdir()?;
490 /// let buf: SegmentBuffer<u64> =
491 /// SegmentBuffer::open(dir.path(), SegmentConfig::default())?;
492 ///
493 /// assert_eq!(buf.latest_sequence(), 0);
494 /// buf.append(7)?;
495 /// assert_eq!(buf.latest_sequence(), 0);
496 /// buf.append(8)?;
497 /// assert_eq!(buf.latest_sequence(), 1);
498 /// # Ok::<(), Box<dyn std::error::Error>>(())
499 /// ```
500 #[must_use = "the sequence number is meaningless if discarded"]
501 pub fn latest_sequence(&self) -> u64 {
502 let inner = self.inner.lock();
503 if inner.next_seq == 0 {
504 0
505 } else {
506 inner.next_seq - 1
507 }
508 }
509
510 /// Total items waiting in the buffer (on-disk + in-memory pending).
511 ///
512 /// Equivalent to `latest_sequence() - head_seq + 1` when non-empty, 0 when
513 /// empty. Decreases as [`delete_acked`](Self::delete_acked) removes files.
514 ///
515 /// # Example
516 ///
517 /// ```
518 /// use segment_buffer::{SegmentBuffer, SegmentConfig};
519 /// use tempfile::tempdir;
520 ///
521 /// let dir = tempdir()?;
522 /// let buf: SegmentBuffer<u64> =
523 /// SegmentBuffer::open(dir.path(), SegmentConfig::default())?;
524 ///
525 /// assert_eq!(buf.pending_count(), 0);
526 /// buf.append(1)?;
527 /// buf.append(2)?;
528 /// assert_eq!(buf.pending_count(), 2);
529 /// buf.flush()?;
530 /// assert_eq!(buf.pending_count(), 2); // still pending until acked
531 /// buf.delete_acked(1)?;
532 /// assert_eq!(buf.pending_count(), 0);
533 /// # Ok::<(), Box<dyn std::error::Error>>(())
534 /// ```
535 #[must_use = "the backlog size is meaningless if discarded"]
536 pub fn pending_count(&self) -> u64 {
537 let inner = self.inner.lock();
538 inner.next_seq.saturating_sub(inner.head_seq)
539 }
540
541 /// Standard [`len`](#method.len) alias for [`pending_count`](Self::pending_count).
542 ///
543 /// Provided so `SegmentBuffer` reads like a normal collection at the call
544 /// site (`buf.len()`, `buf.is_empty()`). Same value as `pending_count()`,
545 /// kept as `u64` because the buffer is proven beyond `usize::MAX` on
546 /// 32-bit targets (597M+ events in monitor365).
547 ///
548 /// # Example
549 ///
550 /// ```
551 /// use segment_buffer::{SegmentBuffer, SegmentConfig};
552 /// use tempfile::tempdir;
553 ///
554 /// let dir = tempdir()?;
555 /// let buf: SegmentBuffer<u64> =
556 /// SegmentBuffer::open(dir.path(), SegmentConfig::default())?;
557 /// assert!(buf.is_empty());
558 /// buf.append(7)?;
559 /// assert_eq!(buf.len(), 1);
560 /// assert!(!buf.is_empty());
561 /// # Ok::<(), Box<dyn std::error::Error>>(())
562 /// ```
563 #[must_use = "the backlog size is meaningless if discarded"]
564 pub fn len(&self) -> u64 {
565 self.pending_count()
566 }
567
568 /// `true` when there are no items waiting in the buffer (on-disk or
569 /// in-memory). Equivalent to `pending_count() == 0`.
570 ///
571 /// # Example
572 ///
573 /// ```
574 /// use segment_buffer::{SegmentBuffer, SegmentConfig};
575 /// use tempfile::tempdir;
576 ///
577 /// let dir = tempdir()?;
578 /// let buf: SegmentBuffer<u64> =
579 /// SegmentBuffer::open(dir.path(), SegmentConfig::default())?;
580 /// assert!(buf.is_empty());
581 /// # Ok::<(), Box<dyn std::error::Error>>(())
582 /// ```
583 #[must_use = "the emptiness flag is meaningless if discarded"]
584 pub fn is_empty(&self) -> bool {
585 self.pending_count() == 0
586 }
587
588 /// Disk usage pressure as a value between 0.0 and 1.0.
589 ///
590 /// Use this to implement your own admission/backpressure policy (e.g.
591 /// reject low-priority items above 0.90, reject standard items above 0.95).
592 /// Returns 0.0 when `max_size_bytes == 0` (limit disabled).
593 ///
594 /// # Example
595 ///
596 /// ```
597 /// use segment_buffer::{SegmentBuffer, SegmentConfig};
598 /// use tempfile::tempdir;
599 ///
600 /// let dir = tempdir()?;
601 /// let mut cfg = SegmentConfig::default();
602 /// cfg.max_size_bytes = 1000; // tiny limit so pressure is observable
603 /// let buf: SegmentBuffer<u64> = SegmentBuffer::open(dir.path(), cfg)?;
604 ///
605 /// assert!(buf.store_pressure() < 0.1);
606 /// # Ok::<(), Box<dyn std::error::Error>>(())
607 /// ```
608 #[must_use = "the pressure value is meaningless if discarded"]
609 pub fn store_pressure(&self) -> f32 {
610 let inner = self.inner.lock();
611 if self.config.max_size_bytes == 0 {
612 return 0.0;
613 }
614 (inner.approx_disk_bytes as f32 / self.config.max_size_bytes as f32).min(1.0)
615 }
616
617 /// True when disk usage exceeds 90% of the configured limit.
618 ///
619 /// Convenience wrapper around `store_pressure() > 0.9`.
620 ///
621 /// # Example
622 ///
623 /// ```
624 /// use segment_buffer::{SegmentBuffer, SegmentConfig};
625 /// use tempfile::tempdir;
626 ///
627 /// let dir = tempdir()?;
628 /// let buf: SegmentBuffer<u64> =
629 /// SegmentBuffer::open(dir.path(), SegmentConfig::default())?;
630 ///
631 /// assert!(!buf.is_overloaded());
632 /// # Ok::<(), Box<dyn std::error::Error>>(())
633 /// ```
634 #[must_use = "the overload flag is meaningless if discarded"]
635 pub fn is_overloaded(&self) -> bool {
636 self.store_pressure() > 0.9
637 }
638
639 /// Capture a consistent snapshot of buffer state under a single lock.
640 ///
641 /// Cheaper and more consistent than calling
642 /// [`pending_count`](Self::pending_count),
643 /// [`latest_sequence`](Self::latest_sequence),
644 /// [`store_pressure`](Self::store_pressure) etc. individually (which each
645 /// take the mutex and could observe a flush/delete between calls).
646 ///
647 /// # Performance
648 ///
649 /// Micro-benchmarked in `benches/bench_stats.rs` (run with
650 /// `cargo bench --bench bench_stats --features encryption`):
651 ///
652 /// | Operation | Measured time (median, typical run) |
653 /// |--------------------------------------------|--------------------------------------|
654 /// | `stats()` (single lock, 7-field snapshot) | ~12 ns |
655 /// | 3 individual accessors (`pending_count` + `latest_sequence` + `store_pressure`) | ~31 ns |
656 ///
657 /// So `stats()` is roughly **2.5× cheaper than 3 individual accessors**
658 /// while also being atomic — torn reads between calls are impossible.
659 /// Numbers are from the benchmark machine and fluctuate with hardware;
660 /// the relative ratio is the durable claim.
661 ///
662 /// # Example
663 ///
664 /// ```
665 /// use segment_buffer::{SegmentBuffer, SegmentConfig};
666 /// use tempfile::tempdir;
667 ///
668 /// let dir = tempdir()?;
669 /// let buf: SegmentBuffer<u64> =
670 /// SegmentBuffer::open(dir.path(), SegmentConfig::default())?;
671 /// buf.append(1)?;
672 /// buf.append(2)?;
673 ///
674 /// let snapshot = buf.stats();
675 /// assert_eq!(snapshot.pending_count, 2);
676 /// assert_eq!(snapshot.next_sequence, 2);
677 /// assert!(snapshot.store_pressure < 0.01);
678 /// # Ok::<(), Box<dyn std::error::Error>>(())
679 /// ```
680 #[must_use = "the snapshot is meaningless if discarded"]
681 pub fn stats(&self) -> BufferStats {
682 let inner = self.inner.lock();
683 let pending_count = inner.next_seq.saturating_sub(inner.head_seq);
684 let latest_sequence = if inner.next_seq == 0 {
685 0
686 } else {
687 inner.next_seq - 1
688 };
689 let store_pressure = if self.config.max_size_bytes == 0 {
690 0.0
691 } else {
692 (inner.approx_disk_bytes as f32 / self.config.max_size_bytes as f32).min(1.0)
693 };
694 BufferStats {
695 pending_count,
696 latest_sequence,
697 head_sequence: inner.head_seq,
698 next_sequence: inner.next_seq,
699 approx_disk_bytes: inner.approx_disk_bytes,
700 max_size_bytes: self.config.max_size_bytes,
701 store_pressure,
702 }
703 }
704
705 // -----------------------------------------------------------------------
706 // Internal helpers
707 // -----------------------------------------------------------------------
708
709 fn recover(&self) -> Result<()> {
710 segment::clean_tmp(&self.dir)?;
711
712 let segments = self.scan_segments()?;
713
714 // All filesystem access (stat'ing each segment for its size) happens
715 // BEFORE the mutex is taken. The lock is held only long enough to
716 // publish the rebuilt in-memory state, honouring the invariant that
717 // the mutex is never held across file I/O.
718 let total_bytes: u64 = segments
719 .iter()
720 .filter_map(|s| fs::metadata(self.segment_path(s.start, s.end)).ok())
721 .map(|m| m.len())
722 .sum();
723
724 let (head_seq, next_seq) = match (segments.first(), segments.last()) {
725 (Some(first), Some(last)) => (first.start, last.end + 1),
726 _ => (0, 0),
727 };
728
729 let segment_count = segments.len();
730 {
731 let mut inner = self.inner.lock();
732 inner.head_seq = head_seq;
733 inner.next_seq = next_seq;
734 inner.approx_disk_bytes = total_bytes;
735 }
736
737 info!(
738 segments = segment_count,
739 head_seq,
740 next_seq,
741 disk_bytes = total_bytes,
742 "Segment buffer recovered"
743 );
744
745 Ok(())
746 }
747
748 fn write_segment(&self, start: u64, end: u64, events: &[T]) -> Result<u64> {
749 segment::write(
750 &self.dir,
751 self.config.cipher.as_deref(),
752 self.config.compression_level,
753 SegmentRange::new(start, end),
754 events,
755 )
756 }
757
758 fn read_segment(&self, seg: SegmentRange) -> Result<Vec<T>> {
759 segment::read(&self.dir, self.config.cipher.as_deref(), seg)
760 }
761
762 fn scan_segments(&self) -> Result<Vec<SegmentRange>> {
763 segment::scan(&self.dir)
764 }
765
766 fn segment_path(&self, start: u64, end: u64) -> PathBuf {
767 self.dir.join(segment::filename(start, end))
768 }
769}
770
771// ---------------------------------------------------------------------------
772// Static thread-safety assertion
773// ---------------------------------------------------------------------------
774
775// `SegmentBuffer<T>` is documented as MPMC-safe via `parking_lot::Mutex`. This
776// fails to compile if anyone ever introduces a non-`Send`/`Sync` field on
777// `SegmentBuffer` or `BufferInner` (e.g. an `Rc`), turning the documented
778// thread-safety guarantee into a compile-time contract instead of a comment.
779const _: () = {
780 const fn assert_send_sync<T: Send + Sync>() {}
781 assert_send_sync::<SegmentBuffer<()>>();
782};
783
784#[cfg(test)]
785mod tests;
786
787#[cfg(test)]
788mod property_tests;