Skip to main content

commonware_storage/journal/segmented/
glob.rs

1//! Simple section-based blob storage for values.
2//!
3//! This module provides a minimal blob storage optimized for storing values where
4//! the size is tracked externally (in an index entry). Unlike the segmented variable
5//! journal, this format does not include a size prefix since the caller already
6//! knows the size.
7//!
8//! # Format
9//!
10//! Each entry is stored as:
11//!
12//! ```text
13//! +---+---+---+---+---+---+---+---+---+---+---+---+
14//! |     Compressed Data (variable)    |   CRC32   |
15//! +---+---+---+---+---+---+---+---+---+---+---+---+
16//! ```
17//!
18//! - **Compressed Data**: zstd compressed (if enabled) or raw codec output
19//! - **CRC32**: 4-byte checksum of the compressed data
20//!
21//! # Read Flow
22//!
23//! 1. Get `(offset, size)` from index entry
24//! 2. Read `size` bytes directly from blob at byte offset
25//! 3. Last 4 bytes are CRC32, verify it
26//! 4. Decompress remaining bytes if compression enabled
27//! 5. Decode value
28
29use super::manager::{Config as ManagerConfig, Manager, WriteFactory};
30use crate::{
31    Context,
32    journal::{Error, frame},
33};
34use bytes::Bytes;
35use commonware_codec::{Codec, CodecShared, FixedSize};
36use commonware_cryptography::{Crc32, crc32};
37#[cfg(any(test, feature = "test-utils"))]
38use commonware_runtime::{Blob as _, ReadOptions, Storage, WriteOptions};
39use commonware_runtime::{BufMut, Error as RError, Handle};
40use std::{collections::BTreeMap, num::NonZeroUsize};
41use zstd::zstd_safe::compress_bound;
42
43/// Physical overhead appended to every frame: the CRC32 of the frame's data.
44pub(crate) const CHECKSUM_SIZE: usize = crc32::Digest::SIZE;
45
46/// Configuration for blob storage.
47#[derive(Clone)]
48pub struct Config<C> {
49    /// The partition to use for storing blobs.
50    pub partition: String,
51
52    /// Optional zstd compression level for stored values.
53    ///
54    /// Keep the choice between `None` and `Some(_)` fixed while stored values are retained.
55    /// Only the compression level may change between initializations when compression is enabled.
56    pub compression: Option<u8>,
57
58    /// The codec configuration to use for encoding and decoding items.
59    pub codec_config: C,
60
61    /// The size of the write buffer to use for each blob.
62    pub write_buffer: NonZeroUsize,
63}
64
65/// The glob's state, boxed so the public [Glob] handle stays pointer-sized.
66struct Inner<E: Context, V: Codec> {
67    manager: Manager<E, WriteFactory>,
68
69    /// Compression level (if enabled).
70    compression: Option<u8>,
71
72    /// Codec configuration.
73    codec_config: V::Cfg,
74}
75
76impl<E: Context, V: CodecShared> Inner<E, V> {
77    /// See [Glob::init].
78    async fn init(context: E, cfg: Config<V::Cfg>, ceiling: u64) -> Result<Self, Error> {
79        let manager_cfg = ManagerConfig {
80            partition: cfg.partition,
81            factory: WriteFactory {
82                capacity: cfg.write_buffer,
83            },
84        };
85        let manager = Manager::init_bounded(context, manager_cfg, ceiling).await?;
86
87        Ok(Self {
88            manager,
89            compression: cfg.compression,
90            codec_config: cfg.codec_config,
91        })
92    }
93
94    /// See [Glob::append].
95    async fn append(&mut self, section: u64, value: &V) -> Result<(u64, u32), Error> {
96        // Encode and optionally compress, then append checksum
97        let buf = if let Some(level) = self.compression {
98            // Compressed: encode first, then compress, then append checksum
99            let encoded = value.encode();
100            let mut compressed = Vec::with_capacity(compress_bound(encoded.len()) + CHECKSUM_SIZE);
101            frame::compress_into(level, &encoded, &mut compressed)?;
102            let checksum = Crc32::checksum(&compressed);
103            compressed.put_u32(checksum);
104            compressed
105        } else {
106            // Uncompressed: pre-allocate exact size to avoid copying
107            let entry_size = value.encode_size() + CHECKSUM_SIZE;
108            let mut buf = Vec::with_capacity(entry_size);
109            value.write(&mut buf);
110            let checksum = Crc32::checksum(&buf);
111            buf.put_u32(checksum);
112            buf
113        };
114
115        // Write to blob, taking the writer only when the entry does not fit in its buffer
116        let entry_size = u32::try_from(buf.len()).map_err(|_| Error::ValueTooLarge)?;
117        let writer = self.manager.get_or_create(section).await?;
118        let offset = writer.size();
119        if !writer.try_write_at(offset, &buf) {
120            // Return the writer to the manager only after the owned write succeeds.
121            let writer = self.manager.take(section).await?;
122            let writer = writer.write_at(offset, buf).await.map_err(Error::Runtime)?;
123            self.manager.put(section, writer);
124        }
125
126        Ok((offset, entry_size))
127    }
128
129    /// See [Glob::get].
130    async fn get(&self, section: u64, offset: u64, size: u32) -> Result<V, Error> {
131        let writer = self
132            .manager
133            .get(section)?
134            .ok_or(Error::SectionOutOfRange(section))?;
135
136        // Read via buffered writer (handles read-through for buffered data)
137        let buf = writer.read_at(offset, size as usize).await?.coalesce();
138
139        // Entry format: [compressed_data] [crc32 (4 bytes)]
140        if buf.len() < CHECKSUM_SIZE {
141            return Err(Error::Runtime(RError::BlobInsufficientLength));
142        }
143
144        let data_len = buf.len() - CHECKSUM_SIZE;
145        let compressed_data = &buf.as_ref()[..data_len];
146        let stored_checksum = u32::from_be_bytes(
147            buf.as_ref()[data_len..]
148                .try_into()
149                .expect("checksum is 4 bytes"),
150        );
151
152        // Verify checksum
153        let checksum = Crc32::checksum(compressed_data);
154        if checksum != stored_checksum {
155            return Err(Error::ChecksumMismatch(stored_checksum, checksum));
156        }
157
158        // Decompress if needed and decode
159        let value = if self.compression.is_some() {
160            let decompressed = frame::decompress(compressed_data)?;
161            V::decode_cfg(decompressed, &self.codec_config).map_err(Error::Codec)?
162        } else {
163            // Byte fields decoded from the frame share one `Bytes` owner.
164            V::decode_cfg(Bytes::from(buf.slice(..data_len)), &self.codec_config)
165                .map_err(Error::Codec)?
166        };
167
168        Ok(value)
169    }
170
171    /// See [Recovery::verify].
172    async fn verify(&self, section: u64, offset: u64, size: u32) -> Result<bool, Error> {
173        // A frame is at least its checksum trailer.
174        if (size as usize) < CHECKSUM_SIZE {
175            return Ok(false);
176        }
177        let Some(writer) = self.manager.get(section)? else {
178            return Ok(false);
179        };
180
181        let buf = match writer.read_at(offset, size as usize).await {
182            Ok(buf) => buf.coalesce(),
183            Err(RError::BlobInsufficientLength | RError::OffsetOverflow) => return Ok(false),
184            Err(err) => return Err(Error::Runtime(err)),
185        };
186        let data_len = buf.len() - CHECKSUM_SIZE;
187        let stored_checksum = u32::from_be_bytes(
188            buf.as_ref()[data_len..]
189                .try_into()
190                .expect("checksum is 4 bytes"),
191        );
192        Ok(Crc32::checksum(&buf.as_ref()[..data_len]) == stored_checksum)
193    }
194
195    /// See [Glob::inject].
196    #[cfg(test)]
197    async fn inject(&mut self, section: u64, offset: u64, buf: Vec<u8>) -> Result<(), Error> {
198        let writer = self.manager.take(section).await?;
199        let writer = writer.write_at(offset, buf).await.map_err(Error::Runtime)?;
200        self.manager.put(section, writer);
201        Ok(())
202    }
203
204    /// See [Glob::sync].
205    async fn sync(mut self: Box<Self>, sections: impl crate::Sections) -> Result<Box<Self>, Error> {
206        self.manager = self.manager.sync(sections).await?;
207        Ok(self)
208    }
209
210    /// See [Glob::start_sync].
211    async fn start_sync(
212        mut self: Box<Self>,
213        sections: impl crate::Sections,
214    ) -> Result<(Box<Self>, Handle<()>), Error> {
215        let (manager, handle) = self.manager.start_sync(sections).await?;
216        self.manager = manager;
217        Ok((self, handle))
218    }
219
220    /// See [Glob::sync_all].
221    async fn sync_all(mut self: Box<Self>) -> Result<Box<Self>, Error> {
222        self.manager = self.manager.sync_all().await?;
223        Ok(self)
224    }
225
226    /// See [Glob::size].
227    fn size(&self, section: u64) -> Result<u64, Error> {
228        self.manager.size(section)
229    }
230
231    /// Truncate an initialization-owned suffix.
232    async fn truncate_pending(
233        mut self: Box<Self>,
234        section: u64,
235        size: u64,
236    ) -> Result<Box<Self>, Error> {
237        self.manager = self.manager.truncate_pending(section, size).await?;
238        Ok(self)
239    }
240
241    /// See [Glob::prune].
242    async fn prune(mut self: Box<Self>, min: u64) -> Result<(Box<Self>, bool), Error> {
243        let (manager, pruned) = self.manager.prune(min).await?;
244        self.manager = manager;
245        Ok((self, pruned))
246    }
247
248    /// See [Glob::pruned].
249    const fn pruned(&self, section: u64) -> bool {
250        self.manager.pruned(section)
251    }
252
253    /// See [Glob::oldest_section].
254    fn oldest_section(&self) -> Option<u64> {
255        self.manager.oldest_section()
256    }
257
258    /// See [Glob::newest_section].
259    fn newest_section(&self) -> Option<u64> {
260        self.manager.newest_section()
261    }
262
263    /// See [Glob::sections].
264    fn sections(&self) -> impl Iterator<Item = u64> + '_ {
265        self.manager.sections()
266    }
267
268    /// See [Glob::remove_section].
269    async fn remove_section(mut self: Box<Self>, section: u64) -> Result<(Box<Self>, bool), Error> {
270        let (manager, removed) = self.manager.remove_section(section).await?;
271        self.manager = manager;
272        Ok((self, removed))
273    }
274
275    /// See [Glob::destroy].
276    async fn destroy(self) -> Result<(), Error> {
277        self.manager.destroy().await
278    }
279}
280
281/// Simple section-based blob storage for values.
282///
283/// Uses [`buffer::Write`](commonware_runtime::buffer::Write) for batching writes.
284/// Reads go directly to blobs without any caching (ideal for large values that
285/// shouldn't pollute a page cache).
286///
287/// Mutating functions consume the glob and return it only on success: an error (or a dropped
288/// future) destroys the handle. Mutations on pruned sections fail with
289/// [Error::AlreadyPrunedToSection] without mutating. Check [Glob::pruned] first to keep the
290/// handle.
291pub struct Glob<E: Context, V: Codec>(Box<Inner<E, V>>);
292
293impl<E: Context, V: CodecShared> std::fmt::Debug for Glob<E, V> {
294    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295        f.debug_struct("Glob")
296            .field("oldest_section", &self.oldest_section())
297            .field("newest_section", &self.newest_section())
298            .finish_non_exhaustive()
299    }
300}
301
302impl<E: Context, V: CodecShared> Glob<E, V> {
303    /// Initialize blob storage, opening existing section blobs.
304    pub async fn init(context: E, cfg: Config<V::Cfg>) -> Result<Self, Error> {
305        Ok(Recovery::init(context, cfg, u64::MAX).await?.into())
306    }
307
308    /// Append value to section.
309    ///
310    /// The returned offset is the byte offset where the entry was written.
311    /// The returned size is the total bytes written (compressed_data + crc32).
312    /// Both should be stored in the index entry for later retrieval.
313    pub async fn append(mut self, section: u64, value: &V) -> Result<(Self, u64, u32), Error> {
314        let (offset, size) = self.0.append(section, value).await?;
315        Ok((self, offset, size))
316    }
317
318    /// Read value at offset with known size (from index entry).
319    ///
320    /// The offset should be the byte offset returned by `append()`.
321    /// Reads directly from blob without any caching.
322    pub async fn get(&self, section: u64, offset: u64, size: u32) -> Result<V, Error> {
323        self.0.get(section, offset, size).await
324    }
325
326    /// Inject arbitrary bytes at `offset` in `section`, bypassing entry framing.
327    #[cfg(test)]
328    pub(super) async fn inject(
329        &mut self,
330        section: u64,
331        offset: u64,
332        buf: Vec<u8>,
333    ) -> Result<(), Error> {
334        self.0.inject(section, offset, buf).await
335    }
336
337    /// Sync the given `sections` to disk (flushes write buffers).
338    pub async fn sync(mut self, sections: impl crate::Sections) -> Result<Self, Error> {
339        self.0 = self.0.sync(sections).await?;
340        Ok(self)
341    }
342
343    /// Start syncing the given `sections` to disk.
344    ///
345    /// An error reported by the returned [Handle] is fatal to the glob: the caller
346    /// must stop using the returned glob.
347    pub async fn start_sync(
348        mut self,
349        sections: impl crate::Sections,
350    ) -> Result<(Self, Handle<()>), Error> {
351        let (inner, handle) = self.0.start_sync(sections).await?;
352        self.0 = inner;
353        Ok((self, handle))
354    }
355
356    /// Sync all sections to disk.
357    pub async fn sync_all(mut self) -> Result<Self, Error> {
358        self.0 = self.0.sync_all().await?;
359        Ok(self)
360    }
361
362    /// Get the current size of a section (including buffered data).
363    pub fn size(&self, section: u64) -> Result<u64, Error> {
364        self.0.size(section)
365    }
366
367    /// Prune sections before min.
368    pub async fn prune(mut self, min: u64) -> Result<(Self, bool), Error> {
369        let (inner, pruned) = self.0.prune(min).await?;
370        self.0 = inner;
371        Ok((self, pruned))
372    }
373
374    /// Returns true when `section` is below the prune floor.
375    ///
376    /// The floor only tracks prunes from the current execution and resets at init, so a
377    /// section pruned in a previous execution reports false.
378    pub fn pruned(&self, section: u64) -> bool {
379        self.0.pruned(section)
380    }
381
382    /// Returns the number of the oldest section.
383    pub fn oldest_section(&self) -> Option<u64> {
384        self.0.oldest_section()
385    }
386
387    /// Returns the number of the newest section.
388    pub fn newest_section(&self) -> Option<u64> {
389        self.0.newest_section()
390    }
391
392    /// Returns an iterator over all section numbers.
393    pub fn sections(&self) -> impl Iterator<Item = u64> + '_ {
394        self.0.sections()
395    }
396
397    /// Remove a specific section. Returns true if the section existed and was removed.
398    pub async fn remove_section(mut self, section: u64) -> Result<(Self, bool), Error> {
399        let (inner, removed) = self.0.remove_section(section).await?;
400        self.0 = inner;
401        Ok((self, removed))
402    }
403
404    /// Destroy all blobs.
405    pub async fn destroy(self) -> Result<(), Error> {
406        self.0.destroy().await
407    }
408}
409
410/// Owns value sections until paired initialization has finished.
411pub(crate) struct Recovery<E: Context, V: Codec>(Box<Inner<E, V>>);
412
413impl<E: Context, V: CodecShared> From<Recovery<E, V>> for Glob<E, V> {
414    /// Publish every value section after paired recovery.
415    fn from(recovery: Recovery<E, V>) -> Self {
416        Self(recovery.0)
417    }
418}
419
420impl<E: Context, V: CodecShared> Recovery<E, V> {
421    /// Open value sections through `ceiling` under paired initialization ownership. Later sections
422    /// stay closed until paired recovery removes them.
423    pub(crate) async fn init(context: E, cfg: Config<V::Cfg>, ceiling: u64) -> Result<Self, Error> {
424        Ok(Self(Box::new(Inner::init(context, cfg, ceiling).await?)))
425    }
426
427    /// Check whether the entry at `(offset, size)` in `section` has a valid trailing checksum.
428    ///
429    /// Returns `Ok(false)` if the frame is smaller than its checksum trailer, the section
430    /// does not exist, the range is not fully covered by the section, or the checksum does
431    /// not match. Other read failures are propagated.
432    pub(crate) async fn verify(&self, section: u64, offset: u64, size: u32) -> Result<bool, Error> {
433        self.0.verify(section, offset, size).await
434    }
435
436    /// Truncate to a specific section and size.
437    ///
438    /// Truncates the section to the given size and removes all sections after it. A shorter
439    /// length is durable when this returns.
440    pub(crate) async fn truncate(mut self, section: u64, size: u64) -> Result<Self, Error> {
441        self.0 = self.0.truncate_pending(section, size).await?;
442        Ok(self)
443    }
444
445    /// Truncate only the given section to a specific size.
446    ///
447    /// Other sections are unaffected. A shorter length is durable when this returns.
448    pub(crate) async fn truncate_section(mut self, section: u64, size: u64) -> Result<Self, Error> {
449        self.0.manager = self
450            .0
451            .manager
452            .truncate_pending_section(section, size)
453            .await?;
454        Ok(self)
455    }
456
457    /// Durably truncate the selected independent value sections.
458    pub(crate) async fn truncate_sections(
459        mut self,
460        sizes: &BTreeMap<u64, u64>,
461    ) -> Result<Self, Error> {
462        self.0.manager = self.0.manager.truncate_pending_sections(sizes).await?;
463        Ok(self)
464    }
465
466    /// Return the size of a section during recovery.
467    pub(crate) fn size(&self, section: u64) -> Result<u64, Error> {
468        self.0.size(section)
469    }
470
471    /// Return the retained section numbers.
472    pub(crate) fn sections(&self) -> impl Iterator<Item = u64> + '_ {
473        self.0.sections()
474    }
475
476    /// Make repaired value sections durable.
477    pub(crate) async fn sync(mut self, sections: impl crate::Sections) -> Result<Self, Error> {
478        self.0 = self.0.sync(sections).await?;
479        Ok(self)
480    }
481
482    /// Remove an orphaned value section.
483    pub(crate) async fn remove_section(mut self, section: u64) -> Result<Self, Error> {
484        let (inner, _) = self.0.remove_section(section).await?;
485        self.0 = inner;
486        Ok(self)
487    }
488}
489
490/// Flip one byte inside value frame `frame` of the blob at `name`, breaking that frame's CRC
491/// while leaving every other frame valid. Models a value torn by a crash after its index entry
492/// became durable. Addresses uncompressed fixed-size frames: `frame_size` is the encoded value
493/// size plus its CRC32.
494#[cfg(any(test, feature = "test-utils"))]
495pub async fn corrupt_frame(
496    storage: &impl Storage,
497    partition: &str,
498    name: &[u8],
499    frame: u64,
500    frame_size: u64,
501) {
502    let offset = frame * frame_size;
503    let (blob, size) = storage.open(partition, name).await.unwrap();
504    assert!(offset < size, "corruption target must be inside the blob");
505    let byte = blob
506        .read_at(offset, 1, ReadOptions::default())
507        .await
508        .unwrap()
509        .coalesce();
510    blob.write_at(offset, vec![byte.as_ref()[0] ^ 0xFF], WriteOptions::SYNC)
511        .await
512        .unwrap();
513}
514
515#[cfg(test)]
516mod tests {
517    use super::*;
518    use commonware_codec::Encode as _;
519    use commonware_macros::test_traced;
520    use commonware_runtime::{Runner, Supervisor as _, deterministic};
521    use commonware_utils::{NZUsize, probability};
522    use rand::Rng as _;
523
524    impl<E: crate::Context, V: CodecShared> Glob<E, V> {
525        pub(in super::super) fn test_configuration(&self) -> (E, Config<V::Cfg>) {
526            let (context, partition, factory) = self.0.manager.test_configuration();
527            (
528                context,
529                Config {
530                    partition,
531                    write_buffer: factory.capacity,
532                    compression: self.0.compression,
533                    codec_config: self.0.codec_config.clone(),
534                },
535            )
536        }
537
538        async fn test_reopen_section(self, section: u64, end: u64) -> Result<Self, Error> {
539            let (context, partition, factory) = self.0.manager.test_configuration();
540            let cfg = Config {
541                partition,
542                write_buffer: factory.capacity,
543                compression: self.0.compression,
544                codec_config: self.0.codec_config.clone(),
545            };
546            _ = self.sync_all().await?;
547            let pending = Recovery::init(context, cfg, u64::MAX).await?;
548            let pending = pending.truncate_section(section, end).await?;
549            Ok(pending.into())
550        }
551    }
552
553    fn test_cfg() -> Config<()> {
554        Config {
555            partition: "test-partition".into(),
556            compression: None,
557            codec_config: (),
558            write_buffer: NZUsize!(1024),
559        }
560    }
561
562    #[test_traced]
563    fn test_glob_append_and_get() {
564        let executor = deterministic::Runner::default();
565        executor.start(|context| async move {
566            let glob: Glob<_, i32> = Glob::init(context.child("storage"), test_cfg())
567                .await
568                .expect("Failed to init glob");
569
570            // Append a value
571            let value: i32 = 42;
572            let (glob, offset, size) = glob.append(1, &value).await.expect("Failed to append");
573            assert_eq!(offset, 0);
574
575            // Get the value back
576            let retrieved = glob.get(1, offset, size).await.expect("Failed to get");
577            assert_eq!(retrieved, value);
578
579            // Sync and verify
580            let glob = glob.sync(1).await.expect("Failed to sync");
581            let retrieved = glob.get(1, offset, size).await.expect("Failed to get");
582            assert_eq!(retrieved, value);
583
584            glob.destroy().await.expect("Failed to destroy");
585        });
586    }
587
588    #[test_traced]
589    fn test_glob_get_view() {
590        let executor = deterministic::Runner::default();
591        executor.start(|context| async move {
592            let cfg = Config {
593                partition: "test-partition".into(),
594                compression: None,
595                codec_config: (..).into(),
596                write_buffer: NZUsize!(1024),
597            };
598            let glob: Glob<_, Bytes> = Glob::init(context.child("storage"), cfg)
599                .await
600                .expect("Failed to init glob");
601
602            // Append a value that stays in the buffered tip
603            let value = Bytes::from(vec![7u8; 32]);
604            let (glob, offset, size) = glob.append(1, &value).await.expect("Failed to append");
605
606            // Two live reads decode views of the same tip buffer
607            let a = glob.get(1, offset, size).await.expect("Failed to get");
608            let b = glob.get(1, offset, size).await.expect("Failed to get");
609            assert_eq!(a, value);
610            assert_eq!(b, value);
611            assert_eq!(a.as_ptr(), b.as_ptr());
612
613            glob.destroy().await.expect("Failed to destroy");
614        });
615    }
616
617    #[test_traced]
618    fn test_glob_multiple_values() {
619        let executor = deterministic::Runner::default();
620        executor.start(|context| async move {
621            let mut glob: Glob<_, i32> = Glob::init(context.child("storage"), test_cfg())
622                .await
623                .expect("Failed to init glob");
624
625            // Append multiple values
626            let values: Vec<i32> = vec![1, 2, 3, 4, 5];
627            let mut locations = Vec::new();
628
629            for value in &values {
630                let offset;
631                let size;
632                (glob, offset, size) = glob.append(1, value).await.expect("Failed to append");
633                locations.push((offset, size));
634            }
635
636            // Get all values back
637            for (i, (offset, size)) in locations.iter().enumerate() {
638                let retrieved = glob.get(1, *offset, *size).await.expect("Failed to get");
639                assert_eq!(retrieved, values[i]);
640            }
641
642            glob.destroy().await.expect("Failed to destroy");
643        });
644    }
645
646    #[test_traced]
647    fn test_glob_with_compression() {
648        let executor = deterministic::Runner::default();
649        executor.start(|context| async move {
650            let cfg = Config {
651                partition: "test-partition".into(),
652                compression: Some(3), // zstd level 3
653                codec_config: (),
654                write_buffer: NZUsize!(1024),
655            };
656            let glob: Glob<_, [u8; 100]> = Glob::init(context.child("storage"), cfg)
657                .await
658                .expect("Failed to init glob");
659
660            // Append a value
661            let value: [u8; 100] = [0u8; 100]; // Compressible data
662            let (glob, offset, size) = glob.append(1, &value).await.expect("Failed to append");
663
664            // Size should be smaller due to compression
665            assert!(size < 100 + 4);
666
667            // Get the value back
668            let retrieved = glob.get(1, offset, size).await.expect("Failed to get");
669            assert_eq!(retrieved, value);
670
671            glob.destroy().await.expect("Failed to destroy");
672        });
673    }
674
675    #[test_traced]
676    fn test_glob_compressed_entries_match_reference_format() {
677        let executor = deterministic::Runner::default();
678        executor.start(|mut context| async move {
679            let cfg = Config {
680                partition: "test-partition".into(),
681                compression: Some(19),
682                codec_config: ((..).into(), ()),
683                write_buffer: NZUsize!(1024),
684            };
685            let mut glob: Glob<_, Vec<u8>> = Glob::init(context.child("first"), cfg.clone())
686                .await
687                .expect("Failed to init glob");
688
689            // Random bytes stay larger than the write buffer after compression, so that entry
690            // is written through to the blob.
691            let mut values: Vec<Vec<u8>> = [0usize, 1, 127, 4096, 70_000]
692                .into_iter()
693                .map(|len| (0..len).map(|i| (i % 7) as u8).collect())
694                .collect();
695            let mut random = vec![0; 4096];
696            context.fill_bytes(&mut random);
697            values.push(random);
698
699            let mut entries = Vec::new();
700            for value in values {
701                let offset;
702                let size;
703                (glob, offset, size) = glob.append(1, &value).await.expect("Failed to append");
704
705                let mut expected = zstd::bulk::compress(&value.encode(), 19).unwrap();
706                let checksum = Crc32::checksum(&expected);
707                expected.put_u32(checksum);
708                let writer = glob.0.manager.get(1).unwrap().unwrap();
709                let stored = writer.read_at(offset, size as usize).await.unwrap();
710                assert_eq!(stored.coalesce().as_ref(), expected.as_slice());
711                assert_eq!(glob.get(1, offset, size).await.unwrap(), value);
712                entries.push((offset, size, expected, value));
713            }
714            assert!(entries.last().unwrap().1 > 1024);
715            let glob = glob.sync(1).await.expect("Failed to sync");
716            drop(glob);
717
718            // Persisted entries keep the same bytes and values.
719            let glob: Glob<_, Vec<u8>> = Glob::init(context.child("second"), cfg)
720                .await
721                .expect("Failed to reinit glob");
722            let writer = glob.0.manager.get(1).unwrap().unwrap();
723            for (offset, size, expected, value) in &entries {
724                let stored = writer.read_at(*offset, *size as usize).await.unwrap();
725                assert_eq!(stored.coalesce().as_ref(), expected.as_slice());
726                assert_eq!(glob.get(1, *offset, *size).await.unwrap(), *value);
727            }
728
729            glob.destroy().await.expect("Failed to destroy");
730        });
731    }
732
733    #[test_traced]
734    fn test_glob_prune() {
735        let executor = deterministic::Runner::default();
736        executor.start(|context| async move {
737            let mut glob: Glob<_, i32> = Glob::init(context.child("storage"), test_cfg())
738                .await
739                .expect("Failed to init glob");
740
741            // Append to multiple sections
742            for section in 1..=5 {
743                (glob, _, _) = glob
744                    .append(section, &(section as i32))
745                    .await
746                    .expect("Failed to append");
747                glob = glob.sync(section).await.expect("Failed to sync");
748            }
749
750            // Prune sections < 3
751            let (glob, _) = glob.prune(3).await.expect("Failed to prune");
752
753            // The public accessor mirrors the guard
754            assert!(glob.pruned(1));
755            assert!(glob.pruned(2));
756            assert!(!glob.pruned(3));
757
758            // Sections 1 and 2 should be gone
759            assert!(glob.get(1, 0, 8).await.is_err());
760            assert!(glob.get(2, 0, 8).await.is_err());
761
762            // Sections 3-5 should still exist
763            assert!(glob.0.manager.blobs.contains_key(&3));
764            assert!(glob.0.manager.blobs.contains_key(&4));
765            assert!(glob.0.manager.blobs.contains_key(&5));
766
767            glob.destroy().await.expect("Failed to destroy");
768        });
769    }
770
771    #[test_traced]
772    fn test_glob_checksum_mismatch() {
773        let executor = deterministic::Runner::default();
774        executor.start(|context| async move {
775            let glob: Glob<_, i32> = Glob::init(context.child("storage"), test_cfg())
776                .await
777                .expect("Failed to init glob");
778
779            // Append a value
780            let value: i32 = 42;
781            let (glob, offset, size) = glob.append(1, &value).await.expect("Failed to append");
782            let mut glob = glob.sync(1).await.expect("Failed to sync");
783
784            // Corrupt the data by writing directly to the underlying blob
785            let writer = glob.0.manager.take(1).await.unwrap();
786            let writer = writer
787                .write_at(offset, vec![0xFF, 0xFF, 0xFF, 0xFF])
788                .await
789                .expect("Failed to corrupt");
790            let writer = writer.sync().await.expect("Failed to sync");
791            glob.0.manager.put(1, writer);
792
793            // Get should fail with checksum mismatch
794            let result = glob.get(1, offset, size).await;
795            assert!(matches!(result, Err(Error::ChecksumMismatch(_, _))));
796
797            glob.destroy().await.expect("Failed to destroy");
798        });
799    }
800
801    #[test_traced]
802    fn test_glob_truncate() {
803        let executor = deterministic::Runner::default();
804        executor.start(|context| async move {
805            let mut glob: Glob<_, i32> = Glob::init(context.child("storage"), test_cfg())
806                .await
807                .expect("Failed to init glob");
808
809            // Append multiple values and track sizes
810            let values: Vec<i32> = vec![1, 2, 3, 4, 5];
811            let mut locations = Vec::new();
812
813            for value in &values {
814                let offset;
815                let size;
816                (glob, offset, size) = glob.append(1, value).await.expect("Failed to append");
817                locations.push((offset, size));
818            }
819            glob = glob.sync(1).await.expect("Failed to sync");
820
821            // Truncate to after the third value
822            let (third_offset, third_size) = locations[2];
823            let truncate_size = third_offset + u64::from(third_size);
824            let glob = glob
825                .test_reopen_section(1, truncate_size)
826                .await
827                .expect("Failed to truncate");
828
829            // First three values should still be readable
830            for (i, (offset, size)) in locations.iter().take(3).enumerate() {
831                let retrieved = glob.get(1, *offset, *size).await.expect("Failed to get");
832                assert_eq!(retrieved, values[i]);
833            }
834
835            // Fourth and fifth values should fail (reading past end of blob)
836            let (fourth_offset, fourth_size) = locations[3];
837            let result = glob.get(1, fourth_offset, fourth_size).await;
838            assert!(result.is_err());
839
840            glob.destroy().await.expect("Failed to destroy");
841        });
842    }
843
844    /// Reopen a section at a shorter size, append over the freed bytes without a sync, then
845    /// crash with the append retained or lost and any unsynced resize lost. Recovery must not
846    /// stitch the discarded frames behind the new value.
847    #[test_traced]
848    fn test_glob_truncate_survives_crash() {
849        // A buffer smaller than one 8-byte frame writes every value straight to the blob.
850        let cfg = || Config {
851            write_buffer: NZUsize!(4),
852            ..test_cfg()
853        };
854        for retained in [true, false] {
855            // Seed durable history, reopen after its first frame, and leave a replacement unsynced.
856            let executor = deterministic::Runner::default();
857            let ((kept, offset, size), checkpoint) =
858                executor.start_and_recover(move |context| async move {
859                    let mut glob: Glob<_, i32> =
860                        Glob::init(context.child("first"), cfg()).await.unwrap();
861                    let mut kept = 0;
862                    for value in 1..=3 {
863                        let (offset, size);
864                        (glob, offset, size) = glob.append(1, &value).await.unwrap();
865                        if value == 1 {
866                            kept = offset + u64::from(size);
867                        }
868                    }
869                    let glob = glob.sync(1).await.unwrap();
870
871                    // Keep or lose unsynced writes and lose unsynced resizes at the crash.
872                    *context.storage_fault_config().write() = if retained {
873                        deterministic::FaultConfig {
874                            write_rate: Some(deterministic::WriteConfig {
875                                failure_rate: probability!(0.0),
876                                retention_rate: probability!(1.0),
877                                mode: deterministic::PartialWriteMode::Prefix,
878                            }),
879                            resize_rate: Some(deterministic::ResizeConfig {
880                                failure_rate: probability!(0.0),
881                                partial_rate: probability!(0.0),
882                            }),
883                            ..Default::default()
884                        }
885                    } else {
886                        deterministic::FaultConfig::default()
887                    };
888                    let glob = glob.test_reopen_section(1, kept).await.unwrap();
889                    assert_eq!(glob.size(1).unwrap(), kept);
890                    let (glob, offset, size) = glob.append(1, &4).await.unwrap();
891                    assert_eq!(offset, kept);
892                    drop(glob);
893                    (kept, offset, size)
894                });
895
896            // Recovery may retain or lose the replacement, but cannot restore the discarded suffix.
897            deterministic::Runner::from(checkpoint).start(move |context| async move {
898                *context.storage_fault_config().write() = deterministic::FaultConfig::default();
899                let glob: Glob<_, i32> = Glob::init(context.child("second"), cfg()).await.unwrap();
900                let end = if retained {
901                    offset + u64::from(size)
902                } else {
903                    kept
904                };
905                let recovered = glob.size(1).unwrap();
906                assert_eq!(
907                    recovered, end,
908                    "recovered {recovered} bytes, not the {end} byte prefix of the new history"
909                );
910                assert_eq!(glob.get(1, 0, kept as u32).await.unwrap(), 1);
911                if retained {
912                    assert_eq!(glob.get(1, offset, size).await.unwrap(), 4);
913                }
914                glob.destroy().await.unwrap();
915            });
916        }
917    }
918
919    #[test_traced]
920    fn test_glob_persistence() {
921        let executor = deterministic::Runner::default();
922        executor.start(|context| async move {
923            let cfg = test_cfg();
924
925            // Create and populate glob
926            let glob: Glob<_, i32> = Glob::init(context.child("first"), cfg.clone())
927                .await
928                .expect("Failed to init glob");
929
930            let value: i32 = 42;
931            let (glob, offset, size) = glob.append(1, &value).await.expect("Failed to append");
932            let glob = glob.sync(1).await.expect("Failed to sync");
933            drop(glob);
934
935            // Reopen and verify
936            let glob: Glob<_, i32> = Glob::init(context.child("second"), cfg)
937                .await
938                .expect("Failed to reinit glob");
939
940            let retrieved = glob.get(1, offset, size).await.expect("Failed to get");
941            assert_eq!(retrieved, value);
942
943            glob.destroy().await.expect("Failed to destroy");
944        });
945    }
946
947    #[test_traced]
948    fn test_glob_get_invalid_size() {
949        let executor = deterministic::Runner::default();
950        executor.start(|context| async move {
951            let glob: Glob<_, i32> = Glob::init(context.child("storage"), test_cfg())
952                .await
953                .expect("Failed to init glob");
954
955            let (glob, offset, _size) = glob.append(1, &42).await.expect("Failed to append");
956            let glob = glob.sync(1).await.expect("Failed to sync");
957
958            // Size 0 - should fail
959            assert!(glob.get(1, offset, 0).await.is_err());
960
961            // Size < CRC_SIZE (1, 2, 3 bytes) - should fail with BlobInsufficientLength
962            for size in 1..4u32 {
963                let result = glob.get(1, offset, size).await;
964                assert!(matches!(
965                    result,
966                    Err(Error::Runtime(RError::BlobInsufficientLength))
967                ));
968            }
969
970            glob.destroy().await.expect("Failed to destroy");
971        });
972    }
973
974    #[test_traced]
975    fn test_glob_get_wrong_size() {
976        let executor = deterministic::Runner::default();
977        executor.start(|context| async move {
978            let glob: Glob<_, i32> = Glob::init(context.child("storage"), test_cfg())
979                .await
980                .expect("Failed to init glob");
981
982            let (glob, offset, correct_size) = glob.append(1, &42).await.expect("Failed to append");
983            let glob = glob.sync(1).await.expect("Failed to sync");
984
985            // Size too small (but >= CRC_SIZE) - checksum mismatch
986            let result = glob.get(1, offset, correct_size - 1).await;
987            assert!(matches!(result, Err(Error::ChecksumMismatch(_, _))));
988
989            glob.destroy().await.expect("Failed to destroy");
990        });
991    }
992}