Skip to main content

commonware_runtime/storage/
faulty.rs

1//! A storage wrapper that injects deterministic faults for testing crash recovery.
2
3use crate::{
4    BlobVersion, Error, Handle, IoBufs, IoBufsMut, ReadOptions, WriteOptions,
5    deterministic::BoxDynRng,
6};
7use bytes::Buf;
8use commonware_utils::{
9    Probability, probability,
10    sync::{AsyncMutex, Mutex, RwLock},
11};
12use futures::{FutureExt as _, future::Shared};
13use rand::RngExt as _;
14use std::{
15    collections::{BTreeMap, HashSet},
16    io::Error as IoError,
17    sync::{
18        Arc, OnceLock, Weak,
19        atomic::{AtomicU64, Ordering},
20    },
21};
22
23/// Operation types for fault injection.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25enum Op {
26    Open,
27    Read,
28    Write,
29    Sync,
30    Resize,
31    Remove,
32    Scan,
33}
34
35/// Selects how submitted bytes are retained from a write.
36///
37/// Per the [crate::Blob] durability contract, both modes leave bytes outside the written
38/// range untouched.
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40pub enum PartialWriteMode {
41    /// Retain bytes from the beginning of the write until the first omitted byte.
42    Prefix,
43
44    /// Independently select each submitted byte for retention.
45    Subset,
46}
47
48/// Fault configuration for `write_at` operations and byte retention from failed writes or
49/// successful unsynchronized writes when a crash is simulated. A successful reopen reads only
50/// the blob's durable contents and retires its pending crash outcomes.
51#[derive(Clone, Copy, Debug, PartialEq)]
52pub struct WriteConfig {
53    /// Probability that `write_at` returns an injected failure.
54    pub failure_rate: Probability,
55
56    /// Probability used by the selected mode when retaining submitted bytes.
57    pub retention_rate: Probability,
58
59    /// Arrangement of bytes retained by the simulated storage device.
60    pub mode: PartialWriteMode,
61}
62
63#[cfg(feature = "arbitrary")]
64const WRITE_CONFIG_RATE_STEPS: u16 = 101;
65#[cfg(feature = "arbitrary")]
66const WRITE_CONFIG_RATE_PAIRS: u16 = WRITE_CONFIG_RATE_STEPS * WRITE_CONFIG_RATE_STEPS;
67#[cfg(feature = "arbitrary")]
68const WRITE_CONFIG_CELLS: u16 = WRITE_CONFIG_RATE_PAIRS * 2;
69
70#[cfg(feature = "arbitrary")]
71fn write_config_from_cell(cell: u16) -> WriteConfig {
72    let rates = cell % WRITE_CONFIG_RATE_PAIRS;
73    let failure = rates % WRITE_CONFIG_RATE_STEPS;
74    let retention = rates / WRITE_CONFIG_RATE_STEPS;
75    WriteConfig {
76        failure_rate: Probability::new(u64::from(failure), 100).unwrap(),
77        retention_rate: Probability::new(u64::from(retention), 100).unwrap(),
78        mode: if cell < WRITE_CONFIG_RATE_PAIRS {
79            PartialWriteMode::Prefix
80        } else {
81            PartialWriteMode::Subset
82        },
83    }
84}
85
86#[cfg(feature = "arbitrary")]
87impl<'a> arbitrary::Arbitrary<'a> for WriteConfig {
88    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
89        Ok(write_config_from_cell(
90            u.int_in_range(0..=WRITE_CONFIG_CELLS - 1)?,
91        ))
92    }
93
94    fn size_hint(_: usize) -> (usize, Option<usize>) {
95        (2, Some(2))
96    }
97}
98
99/// Fault configuration for `resize` operations and partial failure behavior.
100#[derive(Clone, Copy, Debug, PartialEq)]
101pub struct ResizeConfig {
102    /// Probability that `resize` returns an injected failure, also used independently as the
103    /// probability that a successful unsynchronized resize survives a simulated crash. A
104    /// successful reopen of the blob retires that outcome.
105    pub failure_rate: Probability,
106
107    /// Probability that an injected failure resizes to an intermediate size rather than leaving
108    /// the size unchanged.
109    pub partial_rate: Probability,
110}
111
112/// Configuration for deterministic storage fault injection.
113#[derive(Clone, Debug, Default)]
114pub struct Config {
115    /// Failure rate for `open_versioned` operations.
116    pub open_rate: Option<Probability>,
117
118    /// Failure rate for `read_at` operations.
119    pub read_rate: Option<Probability>,
120
121    /// Failure and byte-retention configuration for `write_at` operations.
122    pub write_rate: Option<WriteConfig>,
123
124    /// Failure rate for `sync` operations.
125    pub sync_rate: Option<Probability>,
126
127    /// Failure and partial-failure configuration for `resize` operations.
128    pub resize_rate: Option<ResizeConfig>,
129
130    /// Failure rate for `remove` operations.
131    pub remove_rate: Option<Probability>,
132
133    /// Failure rate for `scan` operations.
134    pub scan_rate: Option<Probability>,
135}
136
137impl Config {
138    /// Get the failure rate for an operation type.
139    fn rate_for(&self, op: Op) -> Probability {
140        match op {
141            Op::Open => self.open_rate,
142            Op::Read => self.read_rate,
143            Op::Write => self.write_rate.map(|config| config.failure_rate),
144            Op::Sync => self.sync_rate,
145            Op::Resize => self.resize_rate.map(|config| config.failure_rate),
146            Op::Remove => self.remove_rate,
147            Op::Scan => self.scan_rate,
148        }
149        .unwrap_or(probability!(0.0))
150    }
151
152    /// Set the open failure rate.
153    pub const fn open(mut self, rate: Probability) -> Self {
154        self.open_rate = Some(rate);
155        self
156    }
157
158    /// Set the read failure rate.
159    pub const fn read(mut self, rate: Probability) -> Self {
160        self.read_rate = Some(rate);
161        self
162    }
163
164    /// Set the write fault configuration.
165    pub const fn write(mut self, config: WriteConfig) -> Self {
166        self.write_rate = Some(config);
167        self
168    }
169
170    /// Set the sync failure rate.
171    pub const fn sync(mut self, rate: Probability) -> Self {
172        self.sync_rate = Some(rate);
173        self
174    }
175
176    /// Set the resize fault configuration.
177    pub const fn resize(mut self, config: ResizeConfig) -> Self {
178        self.resize_rate = Some(config);
179        self
180    }
181
182    /// Set the remove failure rate.
183    pub const fn remove(mut self, rate: Probability) -> Self {
184        self.remove_rate = Some(rate);
185        self
186    }
187
188    /// Set the scan failure rate.
189    pub const fn scan(mut self, rate: Probability) -> Self {
190        self.scan_rate = Some(rate);
191        self
192    }
193}
194
195/// Shared fault injection context.
196#[derive(Clone)]
197struct Oracle {
198    rng: Arc<Mutex<BoxDynRng>>,
199    config: Arc<RwLock<Config>>,
200}
201
202/// An issued mutation or durability cut whose crash outcome remains unresolved.
203enum PendingMutation<B> {
204    /// A write fragment whose `selection_offset` maps it into one issued write's shared byte
205    /// selection.
206    Write {
207        generation: Arc<FileGeneration>,
208        blob: Arc<B>,
209        offset: u64,
210        bufs: IoBufs,
211        retention: Arc<PendingWriteRetention>,
212        selection_offset: usize,
213    },
214    /// A successful resize already selected to survive a simulated crash.
215    Resize {
216        generation: Arc<FileGeneration>,
217        blob: Arc<B>,
218        len: u64,
219    },
220    /// A full-sync cut whose completion determines whether earlier mutations remain pending.
221    Sync { sync: Arc<PendingSync> },
222}
223
224/// One initiated full sync owns its durability cut and is observed by both its caller and crash
225/// replay bookkeeping.
226struct PendingSync {
227    generation: Arc<FileGeneration>,
228    completion: Shared<Handle<()>>,
229}
230
231impl PendingSync {
232    fn completed_successfully(&self) -> bool {
233        matches!(self.completion.clone().now_or_never(), Some(Ok(())))
234    }
235}
236
237/// Retention choices belong to the issued write and are shared by fragments created by later
238/// durability barriers.
239struct PendingWriteRetention {
240    policy: (PartialWriteMode, Probability),
241    len: usize,
242    selected: OnceLock<Vec<bool>>,
243}
244
245impl PendingWriteRetention {
246    const fn new(policy: (PartialWriteMode, Probability), len: usize) -> Self {
247        Self {
248            policy,
249            len,
250            selected: OnceLock::new(),
251        }
252    }
253}
254
255impl<B> PendingMutation<B> {
256    fn generation(&self) -> &Arc<FileGeneration> {
257        match self {
258            Self::Write { generation, .. } | Self::Resize { generation, .. } => generation,
259            Self::Sync { sync } => &sync.generation,
260        }
261    }
262}
263
264/// Unresolved entries in issue order within each file generation.
265type PendingMutations<B> = Arc<Mutex<Vec<PendingMutation<B>>>>;
266
267/// Evidence detached from the fault model. Callers release namespace locks before dropping it.
268pub(crate) struct Retired<B> {
269    _mutations: Vec<PendingMutation<B>>,
270}
271
272/// Identifies a file by partition and name.
273type FileKey = (String, Vec<u8>);
274
275/// Identifies one live file generation and serializes its mutations across handles.
276struct FileGeneration {
277    mutation: AsyncMutex<()>,
278}
279
280impl FileGeneration {
281    fn new() -> Self {
282        Self {
283            mutation: AsyncMutex::new(()),
284        }
285    }
286}
287
288/// Tracks the generation shared by existing handles and unresolved mutations for each file.
289type FileGenerations = Arc<Mutex<BTreeMap<FileKey, Weak<FileGeneration>>>>;
290
291fn clear_pending<B>(
292    pending: &PendingMutations<B>,
293    generations: &[Arc<FileGeneration>],
294) -> Retired<B> {
295    let mut pending = pending.lock();
296    let retired = pending
297        .extract_if(.., |mutation| {
298            generations
299                .iter()
300                .any(|generation| Arc::ptr_eq(mutation.generation(), generation))
301        })
302        .collect();
303    Retired {
304        _mutations: retired,
305    }
306}
307
308/// A successful full sync retires mutations issued before it while preserving later crash debt.
309fn resolve_pending_sync<B>(
310    pending: &PendingMutations<B>,
311    sync: &Arc<PendingSync>,
312    succeeded: bool,
313) {
314    let mut pending = pending.lock();
315    let Some(cut) = pending.iter().position(
316        |mutation| matches!(mutation, PendingMutation::Sync { sync: candidate, .. } if Arc::ptr_eq(candidate, sync)),
317    ) else {
318        return;
319    };
320    let mut index = 0;
321    let retired: Vec<_> = pending.extract_if(.., |mutation| {
322        let is_target = matches!(mutation, PendingMutation::Sync { sync: candidate, .. } if Arc::ptr_eq(candidate, sync));
323        let retire = is_target
324            || (succeeded
325                && index < cut
326                && Arc::ptr_eq(mutation.generation(), &sync.generation));
327        index += 1;
328        retire
329    }).collect();
330    drop(pending);
331    drop(retired);
332}
333
334impl Oracle {
335    /// Check if a fault should be injected for the given operation.
336    fn should_fail(&self, op: Op) -> bool {
337        self.roll(self.config.read().rate_for(op))
338    }
339
340    /// Check if a write fault should be injected.
341    /// Reads config once to avoid nested lock acquisition.
342    fn check_write_fault(&self) -> (bool, Option<(PartialWriteMode, Probability)>) {
343        let config = self.config.read();
344        let fail = self.roll(config.rate_for(Op::Write));
345        let retention = config
346            .write_rate
347            .map(|config| (config.mode, config.retention_rate))
348            .filter(|(_, retention_rate)| !retention_rate.is_zero());
349        (fail, retention)
350    }
351
352    /// Check if a resize fault should be injected and snapshot its crash outcome.
353    /// Reads config once to avoid nested lock acquisition.
354    fn check_resize_fault(&self) -> (bool, Probability, bool) {
355        let config = self.config.read();
356        let Some(resize_config) = config.resize_rate else {
357            return (false, probability!(0.0), false);
358        };
359        let failure_rate = config.rate_for(Op::Resize);
360        let fail = self.roll(failure_rate);
361        let retain = !fail && self.roll(failure_rate);
362        (fail, resize_config.partial_rate, retain)
363    }
364
365    /// Check if an event should occur based on a probability rate.
366    fn roll(&self, rate: Probability) -> bool {
367        rate.sample(&mut **self.rng.lock())
368    }
369
370    /// Generate a random value strictly between `from` and `to`, or None if not possible.
371    fn random_between(&self, from: u64, to: u64) -> Option<u64> {
372        if from == to {
373            return None;
374        }
375        let (min, max) = if from < to { (from, to) } else { (to, from) };
376        if max - min <= 1 {
377            return None;
378        }
379        Some(self.rng.lock().random_range(min + 1..max))
380    }
381
382    /// Select retained byte positions according to a snapshotted write policy.
383    fn retained_bytes(
384        &self,
385        len: usize,
386        (mode, retention_rate): (PartialWriteMode, Probability),
387    ) -> Vec<bool> {
388        let mut rng = self.rng.lock();
389        match mode {
390            PartialWriteMode::Prefix => {
391                let mut positions = vec![false; len];
392                let retained = (0..len)
393                    .take_while(|_| retention_rate.sample(&mut **rng))
394                    .count();
395                positions[..retained].fill(true);
396                positions
397            }
398            PartialWriteMode::Subset => (0..len)
399                .map(|_| retention_rate.sample(&mut **rng))
400                .collect(),
401        }
402    }
403
404    /// Try to generate a partial operation target. Returns Some if both the rate
405    /// check passes and an intermediate value exists between `from` and `to`.
406    fn try_partial(&self, rate: Probability, from: u64, to: u64) -> Option<u64> {
407        if self.roll(rate) {
408            self.random_between(from, to)
409        } else {
410            None
411        }
412    }
413}
414
415/// A storage wrapper that injects deterministic faults based on configuration.
416///
417/// Uses a shared RNG for determinism.
418#[derive(Clone)]
419pub struct Storage<S: crate::Storage> {
420    inner: S,
421    ctx: Oracle,
422    pending: PendingMutations<S::Blob>,
423    generations: FileGenerations,
424}
425
426impl<S: crate::Storage> Storage<S> {
427    /// Create a new faulty storage wrapper.
428    pub fn new(inner: S, rng: Arc<Mutex<BoxDynRng>>, config: Arc<RwLock<Config>>) -> Self {
429        Self {
430            inner,
431            ctx: Oracle { rng, config },
432            pending: Arc::new(Mutex::new(Vec::new())),
433            generations: Arc::new(Mutex::new(BTreeMap::new())),
434        }
435    }
436
437    /// Get a reference to the inner storage.
438    pub const fn inner(&self) -> &S {
439        &self.inner
440    }
441
442    /// Get access to the fault configuration for dynamic modification.
443    pub fn config(&self) -> Arc<RwLock<Config>> {
444        self.ctx.config.clone()
445    }
446
447    /// Associates an open blob with the current generation for its file.
448    fn wrap_blob(&self, partition: &str, name: &[u8], inner: S::Blob, size: u64) -> Blob<S::Blob> {
449        let key = (partition.to_string(), name.to_vec());
450        let generation = {
451            let mut generations = self.generations.lock();
452            generations
453                .get(&key)
454                .and_then(Weak::upgrade)
455                .unwrap_or_else(|| {
456                    let generation = Arc::new(FileGeneration::new());
457                    generations.insert(key.clone(), Arc::downgrade(&generation));
458                    generation
459                })
460        };
461        Blob::new(
462            self.ctx.clone(),
463            self.pending.clone(),
464            generation,
465            inner,
466            size,
467        )
468    }
469
470    /// Retires generations and pending mutations for one file or an entire partition.
471    fn retire_names(&self, partition: &str, name: Option<&[u8]>) -> Retired<S::Blob> {
472        let retired = {
473            let mut generations = self.generations.lock();
474            match name {
475                Some(name) => generations
476                    .remove(&(partition.to_string(), name.to_vec()))
477                    .and_then(|generation| generation.upgrade())
478                    .into_iter()
479                    .collect::<Vec<_>>(),
480                None => generations
481                    .extract_if(.., |(candidate, _), _| candidate == partition)
482                    .filter_map(|(_, generation)| generation.upgrade())
483                    .collect(),
484            }
485        };
486        clear_pending(&self.pending, &retired)
487    }
488
489    /// Retire the removed file's crash evidence without destroying its byte owners.
490    pub(crate) async fn remove_retired(
491        &self,
492        partition: &str,
493        name: Option<&[u8]>,
494    ) -> Result<Retired<S::Blob>, Error> {
495        if self.ctx.should_fail(Op::Remove) {
496            return Err(injected_io_error().into());
497        }
498        self.inner.remove(partition, name).await?;
499        Ok(self.retire_names(partition, name))
500    }
501}
502
503impl Storage<crate::storage::memory::Storage> {
504    /// Retire crash evidence excluded from the durable snapshot admitted by a successful open.
505    /// The caller holds the logical namespace guard through admission and retirement.
506    pub(crate) fn admit(
507        &self,
508        partition: &str,
509        name: &[u8],
510    ) -> Retired<crate::storage::memory::Blob> {
511        let generation = self
512            .generations
513            .lock()
514            .get(&(partition.to_owned(), name.to_vec()))
515            .and_then(Weak::upgrade)
516            .expect("an admitted blob retains its file generation");
517        clear_pending(&self.pending, std::slice::from_ref(&generation))
518    }
519
520    /// Replay selected crash outcomes in issue order.
521    pub(crate) fn crash(&self) -> Result<(), Error> {
522        let pending = std::mem::take(&mut *self.pending.lock());
523        let mut synced = HashSet::new();
524        let mut replay = Vec::with_capacity(pending.len());
525        for mutation in pending.into_iter().rev() {
526            match mutation {
527                PendingMutation::Sync { sync } => {
528                    if sync.completed_successfully() {
529                        synced.insert(Arc::as_ptr(&sync.generation));
530                    }
531                }
532                mutation => {
533                    if !synced.contains(&Arc::as_ptr(mutation.generation())) {
534                        replay.push(mutation);
535                    }
536                }
537            }
538        }
539        for mutation in replay.into_iter().rev() {
540            match mutation {
541                PendingMutation::Write {
542                    blob,
543                    offset,
544                    bufs,
545                    retention,
546                    selection_offset,
547                    ..
548                } => {
549                    let selected = retention
550                        .selected
551                        .get_or_init(|| self.ctx.retained_bytes(retention.len, retention.policy));
552                    let selection_end = selection_offset
553                        .checked_add(bufs.remaining())
554                        .expect("a pending-write fragment stays within its selection");
555                    let mut retained = selected[selection_offset..selection_end].iter().copied();
556                    blob.retain_crash_write(offset, bufs, || {
557                        retained
558                            .next()
559                            .expect("the retention policy covers every submitted byte")
560                    })?;
561                }
562                PendingMutation::Resize { blob, len, .. } => {
563                    blob.retain_crash_resize(len)?;
564                }
565                PendingMutation::Sync { .. } => unreachable!("sync markers are not replayed"),
566            }
567        }
568        Ok(())
569    }
570}
571
572/// Create an IoError for injected faults.
573fn injected_io_error() -> IoError {
574    IoError::other("injected storage fault")
575}
576
577impl<S: crate::Storage> crate::Storage for Storage<S> {
578    type Blob = Blob<S::Blob>;
579
580    async fn open_versioned(
581        &self,
582        partition: &str,
583        name: &[u8],
584        versions: std::ops::RangeInclusive<BlobVersion>,
585    ) -> Result<(Self::Blob, u64, BlobVersion), Error> {
586        if self.ctx.should_fail(Op::Open) {
587            return Err(injected_io_error().into());
588        }
589        let (blob, len, blob_version) =
590            self.inner.open_versioned(partition, name, versions).await?;
591        Ok((
592            self.wrap_blob(partition, name, blob, len),
593            len,
594            blob_version,
595        ))
596    }
597
598    async fn remove(&self, partition: &str, name: Option<&[u8]>) -> Result<(), Error> {
599        drop(self.remove_retired(partition, name).await?);
600        Ok(())
601    }
602
603    async fn scan(&self, partition: &str) -> Result<Vec<Vec<u8>>, Error> {
604        if self.ctx.should_fail(Op::Scan) {
605            return Err(injected_io_error().into());
606        }
607        self.inner.scan(partition).await
608    }
609}
610
611/// A blob wrapper that injects deterministic faults based on configuration.
612pub struct Blob<B: crate::Blob> {
613    inner: Arc<B>,
614    ctx: Oracle,
615    pending: PendingMutations<B>,
616    generation: Arc<FileGeneration>,
617    /// Tracked size for partial resize support.
618    size: AtomicU64,
619}
620
621impl<B: crate::Blob> Blob<B> {
622    fn new(
623        ctx: Oracle,
624        pending: PendingMutations<B>,
625        generation: Arc<FileGeneration>,
626        inner: B,
627        size: u64,
628    ) -> Self {
629        Self {
630            inner: Arc::new(inner),
631            ctx,
632            pending,
633            generation,
634            size: AtomicU64::new(size),
635        }
636    }
637
638    fn record_pending(
639        &self,
640        offset: u64,
641        bufs: IoBufs,
642        retention: (PartialWriteMode, Probability),
643    ) {
644        if bufs.is_empty() {
645            return;
646        }
647        let retention = Arc::new(PendingWriteRetention::new(retention, bufs.remaining()));
648        self.pending.lock().push(PendingMutation::Write {
649            generation: self.generation.clone(),
650            blob: self.inner.clone(),
651            offset,
652            bufs,
653            retention,
654            selection_offset: 0,
655        });
656    }
657
658    fn record_pending_resize(&self, len: u64) {
659        self.pending.lock().push(PendingMutation::Resize {
660            generation: self.generation.clone(),
661            blob: self.inner.clone(),
662            len,
663        });
664    }
665
666    /// Retire covered write debt and replay the durable range after any earlier resize debt.
667    fn record_durable_range(&self, offset: u64, durable: IoBufs) {
668        let len = durable.remaining() as u64;
669        if len == 0 {
670            return;
671        }
672        let end = offset
673            .checked_add(len)
674            .expect("submitted write range was validated before mutation");
675        let mut pending = self.pending.lock();
676        let mutations = std::mem::take(&mut *pending);
677        let mut retained = Vec::with_capacity(mutations.len() + 1);
678        let mut retired = Vec::new();
679        let mut follows_resize = false;
680        for mutation in mutations {
681            if !Arc::ptr_eq(mutation.generation(), &self.generation) {
682                retained.push(mutation);
683                continue;
684            }
685            let (write_generation, write_blob, write_offset, mut bufs, retention, selection) =
686                match mutation {
687                    PendingMutation::Write {
688                        generation,
689                        blob,
690                        offset,
691                        bufs,
692                        retention,
693                        selection_offset,
694                    } => (generation, blob, offset, bufs, retention, selection_offset),
695                    resize @ PendingMutation::Resize { .. } => {
696                        follows_resize = true;
697                        retained.push(resize);
698                        continue;
699                    }
700                    sync @ PendingMutation::Sync { .. } => {
701                        retained.push(sync);
702                        continue;
703                    }
704                };
705            let write_len = bufs.remaining() as u64;
706            let write_end = write_offset
707                .checked_add(write_len)
708                .expect("pending write ranges are validated before recording");
709            let overlap_start = write_offset.max(offset);
710            let overlap_end = write_end.min(end);
711            if overlap_start >= overlap_end {
712                retained.push(PendingMutation::Write {
713                    generation: write_generation,
714                    blob: write_blob,
715                    offset: write_offset,
716                    bufs,
717                    retention,
718                    selection_offset: selection,
719                });
720                continue;
721            }
722
723            let prefix_len = usize::try_from(overlap_start - write_offset)
724                .expect("a pending-write subrange fits its source buffer");
725            let prefix = bufs.split_to(prefix_len);
726            let overlap_len = usize::try_from(overlap_end - overlap_start)
727                .expect("an overlapping subrange fits its source buffer");
728            retired.push(bufs.split_to(overlap_len));
729            if prefix_len != 0 {
730                retained.push(PendingMutation::Write {
731                    generation: write_generation.clone(),
732                    blob: write_blob.clone(),
733                    offset: write_offset,
734                    bufs: prefix,
735                    retention: retention.clone(),
736                    selection_offset: selection,
737                });
738            }
739            if overlap_end < write_end {
740                let suffix_start = usize::try_from(overlap_end - write_offset)
741                    .expect("a pending-write subrange fits its source buffer");
742                retained.push(PendingMutation::Write {
743                    generation: write_generation,
744                    blob: write_blob,
745                    offset: overlap_end,
746                    bufs,
747                    retention,
748                    selection_offset: selection
749                        .checked_add(suffix_start)
750                        .expect("a pending-write fragment stays within its selection"),
751                });
752            }
753        }
754        if follows_resize {
755            let retention = Arc::new(PendingWriteRetention::new(
756                (PartialWriteMode::Prefix, probability!(1.0)),
757                durable.remaining(),
758            ));
759            retained.push(PendingMutation::Write {
760                generation: self.generation.clone(),
761                blob: self.inner.clone(),
762                offset,
763                bufs: durable,
764                retention,
765                selection_offset: 0,
766            });
767        }
768        *pending = retained;
769        drop(pending);
770        drop(retired);
771    }
772}
773
774impl<B: crate::Blob> crate::Blob for Blob<B> {
775    async fn read_at(
776        &self,
777        offset: u64,
778        len: usize,
779        options: ReadOptions,
780    ) -> Result<IoBufsMut, Error> {
781        if self.ctx.should_fail(Op::Read) {
782            return Err(injected_io_error().into());
783        }
784        self.inner.read_at(offset, len, options).await
785    }
786
787    async fn read_at_buf(
788        &self,
789        offset: u64,
790        len: usize,
791        bufs: impl Into<IoBufsMut> + Send,
792        options: ReadOptions,
793    ) -> Result<IoBufsMut, Error> {
794        if self.ctx.should_fail(Op::Read) {
795            return Err(injected_io_error().into());
796        }
797        self.inner
798            .read_at_buf(offset, len, bufs.into(), options)
799            .await
800    }
801
802    async fn write_at(
803        &self,
804        offset: u64,
805        bufs: impl Into<IoBufs> + Send,
806        options: WriteOptions,
807    ) -> Result<(), Error> {
808        let bufs = bufs.into();
809        let total_bytes = bufs.remaining() as u64;
810        let sync = options.contains(WriteOptions::SYNC);
811        if sync && total_bytes == 0 {
812            return Ok(());
813        }
814        offset
815            .checked_add(total_bytes)
816            .ok_or(Error::OffsetOverflow)?;
817        let (should_fail, write_retention) = self.ctx.check_write_fault();
818        let _mutation = self.generation.mutation.lock().await;
819        if should_fail {
820            if let Some(retention) = write_retention {
821                let len = bufs.remaining();
822                let retained = self.ctx.retained_bytes(len, retention);
823                let bufs = bufs.coalesce();
824                let mut position = 0;
825                while position < len {
826                    if !retained[position] {
827                        position += 1;
828                        continue;
829                    }
830
831                    let start = position;
832                    while position < len && retained[position] {
833                        position += 1;
834                    }
835                    let end = position;
836                    let run_offset = offset
837                        .checked_add(start as u64)
838                        .ok_or(Error::OffsetOverflow)?;
839                    let run = bufs.slice(start..end);
840                    let durable = run.clone().into();
841                    self.inner
842                        .write_at(run_offset, run, options | WriteOptions::SYNC)
843                        .await?;
844                    self.record_durable_range(run_offset, durable);
845                    self.size.fetch_max(
846                        run_offset.saturating_add((end - start) as u64),
847                        Ordering::Relaxed,
848                    );
849                }
850            }
851            return Err(injected_io_error().into());
852        }
853
854        if sync && self.ctx.should_fail(Op::Sync) {
855            let pending = write_retention.map(|retention| (bufs.clone(), retention));
856            self.inner
857                .write_at(offset, bufs, options.without(WriteOptions::SYNC))
858                .await?;
859            self.size
860                .fetch_max(offset.saturating_add(total_bytes), Ordering::Relaxed);
861            if let Some((bufs, retention)) = pending {
862                self.record_pending(offset, bufs, retention);
863            }
864            return Err(injected_io_error().into());
865        }
866
867        let pending = match (sync, write_retention) {
868            (false, Some(retention)) => Some((bufs.clone(), retention)),
869            _ => None,
870        };
871        let durable = sync.then(|| bufs.clone());
872        self.inner.write_at(offset, bufs, options).await?;
873        self.size
874            .fetch_max(offset.saturating_add(total_bytes), Ordering::Relaxed);
875        if let Some(durable) = durable {
876            self.record_durable_range(offset, durable);
877        } else if let Some((bufs, retention)) = pending {
878            self.record_pending(offset, bufs, retention);
879        }
880        Ok(())
881    }
882
883    async fn resize(&self, len: u64) -> Result<(), Error> {
884        let (should_fail, partial_rate, retain) = self.ctx.check_resize_fault();
885        let _mutation = self.generation.mutation.lock().await;
886        if should_fail {
887            let current = self.size.load(Ordering::Relaxed);
888            if let Some(len) = self.ctx.try_partial(partial_rate, current, len) {
889                self.inner.resize(len).await?;
890                self.record_pending_resize(len);
891                self.size.store(len, Ordering::Relaxed);
892                return Err(injected_io_error().into());
893            }
894            return Err(injected_io_error().into());
895        }
896        self.inner.resize(len).await?;
897        if retain {
898            self.record_pending_resize(len);
899        }
900        self.size.store(len, Ordering::Relaxed);
901        Ok(())
902    }
903
904    async fn sync(&self) -> Result<(), Error> {
905        if self.ctx.should_fail(Op::Sync) {
906            return Err(injected_io_error().into());
907        }
908        let _mutation = self.generation.mutation.lock().await;
909        self.inner.sync().await?;
910        clear_pending(&self.pending, std::slice::from_ref(&self.generation));
911        Ok(())
912    }
913
914    async fn start_sync(&self) -> Handle<()> {
915        if self.ctx.should_fail(Op::Sync) {
916            return Handle::ready(Err(injected_io_error().into()));
917        }
918        let _mutation = self.generation.mutation.lock().await;
919        let sync = Arc::new(PendingSync {
920            generation: self.generation.clone(),
921            completion: self.inner.start_sync().await.shared(),
922        });
923        self.pending
924            .lock()
925            .push(PendingMutation::Sync { sync: sync.clone() });
926
927        let pending = self.pending.clone();
928        let completion = sync.completion.clone();
929        Handle::from_future(async move {
930            let result = completion.await;
931            resolve_pending_sync(&pending, &sync, result.is_ok());
932            result
933        })
934    }
935}
936
937#[cfg(test)]
938mod tests {
939    use super::*;
940    use crate::{
941        Blob as _, IoBufMut, Runner, Spawner, Storage as _,
942        mocks::RecordingContext,
943        storage::{memory::Storage as MemStorage, tests::run_storage_tests},
944    };
945    use commonware_utils::ScriptedRng;
946    use futures::task::noop_waker;
947    use rand::{SeedableRng, rngs::StdRng};
948    use rstest::rstest;
949    use std::{
950        future::Future,
951        pin::Pin,
952        sync::atomic::AtomicBool,
953        task::{Context, Poll},
954    };
955
956    #[cfg(feature = "arbitrary")]
957    #[test]
958    fn test_write_config_arbitrary_is_compact_and_covers_grid() {
959        use arbitrary::{Arbitrary as _, Unstructured};
960
961        assert_eq!(WriteConfig::size_hint(0), (2, Some(2)));
962        let mut input = Unstructured::new(&[0, 0, 0]);
963        WriteConfig::arbitrary(&mut input).unwrap();
964        assert_eq!(input.len(), 1);
965
966        for cell in 0..WRITE_CONFIG_CELLS {
967            let config = write_config_from_cell(cell);
968            let rates = cell % WRITE_CONFIG_RATE_PAIRS;
969            assert_eq!(
970                config.failure_rate,
971                Probability::new(u64::from(rates % WRITE_CONFIG_RATE_STEPS), 100).unwrap()
972            );
973            assert_eq!(
974                config.retention_rate,
975                Probability::new(u64::from(rates / WRITE_CONFIG_RATE_STEPS), 100).unwrap()
976            );
977            assert_eq!(
978                config.mode,
979                if cell < WRITE_CONFIG_RATE_PAIRS {
980                    PartialWriteMode::Prefix
981                } else {
982                    PartialWriteMode::Subset
983                }
984            );
985        }
986    }
987
988    struct OperationGate<B, C> {
989        inner: Arc<B>,
990        context: Arc<C>,
991        pause_after_write: bool,
992        armed: Arc<AtomicBool>,
993        started: Arc<tokio::sync::Notify>,
994        release: Arc<tokio::sync::Notify>,
995        completed: Arc<tokio::sync::Notify>,
996    }
997
998    impl<B, C> Clone for OperationGate<B, C> {
999        fn clone(&self) -> Self {
1000            Self {
1001                inner: self.inner.clone(),
1002                context: self.context.clone(),
1003                pause_after_write: self.pause_after_write,
1004                armed: self.armed.clone(),
1005                started: self.started.clone(),
1006                release: self.release.clone(),
1007                completed: self.completed.clone(),
1008            }
1009        }
1010    }
1011
1012    impl<B, C> OperationGate<B, C> {
1013        async fn pause(&self, after_write: bool) {
1014            if self.pause_after_write == after_write && self.armed.swap(false, Ordering::Relaxed) {
1015                self.started.notify_one();
1016                self.release.notified().await;
1017            }
1018        }
1019    }
1020
1021    impl<B: crate::Blob, C: Spawner> crate::Blob for OperationGate<B, C> {
1022        async fn read_at_buf(
1023            &self,
1024            offset: u64,
1025            len: usize,
1026            bufs: impl Into<IoBufsMut> + Send,
1027            options: ReadOptions,
1028        ) -> Result<IoBufsMut, Error> {
1029            self.inner.read_at_buf(offset, len, bufs, options).await
1030        }
1031
1032        async fn read_at(
1033            &self,
1034            offset: u64,
1035            len: usize,
1036            options: ReadOptions,
1037        ) -> Result<IoBufsMut, Error> {
1038            self.inner.read_at(offset, len, options).await
1039        }
1040
1041        async fn write_at(
1042            &self,
1043            offset: u64,
1044            bufs: impl Into<IoBufs> + Send,
1045            options: WriteOptions,
1046        ) -> Result<(), Error> {
1047            self.inner.write_at(offset, bufs, options).await?;
1048            self.pause(true).await;
1049            Ok(())
1050        }
1051
1052        async fn resize(&self, len: u64) -> Result<(), Error> {
1053            self.inner.resize(len).await
1054        }
1055
1056        async fn sync(&self) -> Result<(), Error> {
1057            self.inner.sync().await?;
1058            self.pause(false).await;
1059            Ok(())
1060        }
1061
1062        async fn start_sync(&self) -> Handle<()> {
1063            let gate = self.clone();
1064            let (sender, receiver) = tokio::sync::oneshot::channel();
1065            drop(self.context.child("sync").spawn(move |_| async move {
1066                let _ = sender.send(gate.sync().await);
1067                gate.completed.notify_one();
1068            }));
1069            Handle::from_receiver(receiver)
1070        }
1071    }
1072
1073    fn poll_once<F: Future>(future: Pin<&mut F>) -> Poll<F::Output> {
1074        let waker = noop_waker();
1075        future.poll(&mut Context::from_waker(&waker))
1076    }
1077
1078    /// Test harness with faulty storage wrapping memory storage.
1079    struct Harness {
1080        inner: MemStorage,
1081        storage: Storage<MemStorage>,
1082        config: Arc<RwLock<Config>>,
1083    }
1084
1085    impl Harness {
1086        fn new(config: Config) -> Self {
1087            Self::with_seed(42, config)
1088        }
1089
1090        fn with_seed(seed: u64, config: Config) -> Self {
1091            Self::with_rng(Box::new(StdRng::seed_from_u64(seed)), config)
1092        }
1093
1094        fn with_rng(rng: BoxDynRng, config: Config) -> Self {
1095            let inner = MemStorage::new();
1096            let rng = Arc::new(Mutex::new(rng));
1097            let config = Arc::new(RwLock::new(config));
1098            let storage = Storage::new(inner.clone(), rng, config.clone());
1099            Self {
1100                inner,
1101                storage,
1102                config,
1103            }
1104        }
1105    }
1106
1107    #[rstest]
1108    #[case::tokio(crate::tokio::Runner::default())]
1109    #[cfg_attr(target_os = "linux", case::iouring(crate::iouring::Runner::default()))]
1110    fn test_start_sync_returns_before_backing_completion<R: Runner>(#[case] runner: R)
1111    where
1112        R::Context: Spawner,
1113    {
1114        runner.start(|context| async move {
1115            let h = Harness::new(Config::default().write(WriteConfig {
1116                failure_rate: probability!(0.0),
1117                retention_rate: probability!(1.0),
1118                mode: PartialWriteMode::Prefix,
1119            }));
1120            let (inner, _) = h.inner.open("partition", b"start-sync").await.unwrap();
1121            inner
1122                .write_at(0, b"data", WriteOptions::SYNC)
1123                .await
1124                .unwrap();
1125
1126            let started = Arc::new(tokio::sync::Notify::new());
1127            let release = Arc::new(tokio::sync::Notify::new());
1128            let gated = OperationGate {
1129                inner: Arc::new(inner),
1130                context: Arc::new(context),
1131                pause_after_write: false,
1132                armed: Arc::new(AtomicBool::new(true)),
1133                started: started.clone(),
1134                release: release.clone(),
1135                completed: Arc::new(tokio::sync::Notify::new()),
1136            };
1137            let pending = Arc::new(Mutex::new(Vec::new()));
1138            let blob = Blob::new(
1139                h.storage.ctx.clone(),
1140                pending,
1141                Arc::new(FileGeneration::new()),
1142                gated,
1143                4,
1144            );
1145
1146            let mut start = Box::pin(blob.start_sync());
1147            let Poll::Ready(mut completion) = poll_once(start.as_mut()) else {
1148                panic!("start_sync waited for backing durability");
1149            };
1150            started.notified().await;
1151            assert!(poll_once(Pin::new(&mut completion)).is_pending());
1152            release.notify_one();
1153            completion.await.unwrap();
1154        });
1155    }
1156
1157    async fn run_overlapping_barrier(context: impl Spawner, start: bool) {
1158        let h = Harness::new(Config::default().write(WriteConfig {
1159            failure_rate: probability!(0.0),
1160            retention_rate: probability!(1.0),
1161            mode: PartialWriteMode::Prefix,
1162        }));
1163        let (inner, _) = h.inner.open("partition", b"overlap").await.unwrap();
1164        inner
1165            .write_at(0, b"base", WriteOptions::SYNC)
1166            .await
1167            .unwrap();
1168
1169        let started = Arc::new(tokio::sync::Notify::new());
1170        let release = Arc::new(tokio::sync::Notify::new());
1171        let completed = Arc::new(tokio::sync::Notify::new());
1172        let gated = OperationGate {
1173            inner: Arc::new(inner),
1174            context: Arc::new(context.child("gate")),
1175            pause_after_write: false,
1176            armed: Arc::new(AtomicBool::new(true)),
1177            started: started.clone(),
1178            release: release.clone(),
1179            completed: completed.clone(),
1180        };
1181        let pending = Arc::new(Mutex::new(Vec::new()));
1182        let blob = Blob::new(
1183            h.storage.ctx.clone(),
1184            pending.clone(),
1185            Arc::new(FileGeneration::new()),
1186            gated,
1187            4,
1188        );
1189        let blob = Arc::new(blob);
1190
1191        let barrier_blob = blob.clone();
1192        let barrier = context.child("barrier").spawn(move |_| async move {
1193            if start {
1194                drop(barrier_blob.start_sync().await);
1195            } else {
1196                barrier_blob.sync().await.unwrap();
1197            }
1198        });
1199        started.notified().await;
1200
1201        let mut late = Box::pin(blob.write_at(0, b"late", WriteOptions::default()));
1202        if start {
1203            let Poll::Ready(result) = poll_once(late.as_mut()) else {
1204                panic!("a started sync blocked a later write");
1205            };
1206            result.unwrap();
1207            release.notify_one();
1208            barrier.await.unwrap();
1209            // Wait for backing durability after dropping its completion handle.
1210            completed.notified().await;
1211        } else {
1212            assert!(poll_once(late.as_mut()).is_pending());
1213            release.notify_one();
1214            barrier.await.unwrap();
1215            late.await.unwrap();
1216        }
1217
1218        for mutation in std::mem::take(&mut *pending.lock()) {
1219            match mutation {
1220                PendingMutation::Write {
1221                    blob,
1222                    offset,
1223                    bufs,
1224                    retention,
1225                    ..
1226                } => {
1227                    assert_eq!(
1228                        retention.policy,
1229                        (PartialWriteMode::Prefix, probability!(1.0))
1230                    );
1231                    blob.inner
1232                        .retain_crash_write(offset, bufs, || true)
1233                        .unwrap();
1234                }
1235                PendingMutation::Sync { .. } => {}
1236                PendingMutation::Resize { .. } => panic!("write test recorded a resize"),
1237            }
1238        }
1239        let (durable, len) = h.inner.open("partition", b"overlap").await.unwrap();
1240        assert_eq!(len, 4);
1241        assert_eq!(
1242            durable
1243                .read_at(0, 4, ReadOptions::default())
1244                .await
1245                .unwrap()
1246                .coalesce(),
1247            b"late"
1248        );
1249    }
1250
1251    #[test]
1252    fn test_completed_backing_write_cannot_record_after_later_full_sync() {
1253        crate::tokio::Runner::default().start(|context| async move {
1254            let h = Harness::new(Config::default().write(WriteConfig {
1255                failure_rate: probability!(0.0),
1256                retention_rate: probability!(1.0),
1257                mode: PartialWriteMode::Prefix,
1258            }));
1259            let (inner, _) = h.inner.open("partition", b"late-record").await.unwrap();
1260            inner
1261                .write_at(0, b"base!", WriteOptions::SYNC)
1262                .await
1263                .unwrap();
1264
1265            let started = Arc::new(tokio::sync::Notify::new());
1266            let release = Arc::new(tokio::sync::Notify::new());
1267            let gated = OperationGate {
1268                inner: Arc::new(inner),
1269                context: Arc::new(context),
1270                pause_after_write: true,
1271                armed: Arc::new(AtomicBool::new(true)),
1272                started: started.clone(),
1273                release: release.clone(),
1274                completed: Arc::new(tokio::sync::Notify::new()),
1275            };
1276            let pending = Arc::new(Mutex::new(Vec::new()));
1277            let blob = Blob::new(
1278                h.storage.ctx.clone(),
1279                pending.clone(),
1280                Arc::new(FileGeneration::new()),
1281                gated,
1282                5,
1283            );
1284            let blob = Arc::new(blob);
1285
1286            let mut stale = Box::pin(blob.write_at(0, b"stale", WriteOptions::default()));
1287            assert!(poll_once(stale.as_mut()).is_pending());
1288            started.notified().await;
1289
1290            *h.config.write() = Config::default();
1291            let fresh_blob = blob.clone();
1292            let mut fresh = Box::pin(async move {
1293                fresh_blob
1294                    .write_at(0, b"fresh", WriteOptions::default())
1295                    .await?;
1296                fresh_blob.sync().await
1297            });
1298            assert!(poll_once(fresh.as_mut()).is_pending());
1299
1300            release.notify_one();
1301            stale.await.unwrap();
1302            fresh.await.unwrap();
1303
1304            for mutation in std::mem::take(&mut *pending.lock()) {
1305                let PendingMutation::Write {
1306                    blob,
1307                    offset,
1308                    bufs,
1309                    retention,
1310                    ..
1311                } = mutation
1312                else {
1313                    panic!("write test recorded a resize");
1314                };
1315                assert_eq!(
1316                    retention.policy,
1317                    (PartialWriteMode::Prefix, probability!(1.0))
1318                );
1319                blob.inner
1320                    .retain_crash_write(offset, bufs, || true)
1321                    .unwrap();
1322            }
1323            let (durable, len) = h.inner.open("partition", b"late-record").await.unwrap();
1324            assert_eq!(len, 5);
1325            assert_eq!(
1326                durable
1327                    .read_at(0, 5, ReadOptions::default())
1328                    .await
1329                    .unwrap()
1330                    .coalesce(),
1331                b"fresh"
1332            );
1333        });
1334    }
1335
1336    async fn run_subset_overwrite(seed: u64, original: &[u8], replacement: &[u8]) -> Vec<u8> {
1337        assert_eq!(original.len(), replacement.len());
1338
1339        let h = Harness::with_seed(
1340            seed,
1341            Config::default().write(WriteConfig {
1342                failure_rate: probability!(0.0),
1343                retention_rate: probability!(0.5),
1344                mode: PartialWriteMode::Subset,
1345            }),
1346        );
1347        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
1348        blob.write_at(0, original.to_vec(), WriteOptions::default())
1349            .await
1350            .unwrap();
1351        blob.sync().await.unwrap();
1352
1353        {
1354            let mut config = h.config.write();
1355            config.write_rate = Some(WriteConfig {
1356                failure_rate: probability!(1.0),
1357                retention_rate: probability!(0.5),
1358                mode: PartialWriteMode::Subset,
1359            });
1360        }
1361
1362        let result = blob
1363            .write_at(0, replacement.to_vec(), WriteOptions::default())
1364            .await;
1365        assert!(matches!(result, Err(Error::Io(_))));
1366
1367        let (inner_blob, size) = h.inner.open("partition", b"test").await.unwrap();
1368        assert_eq!(size, original.len() as u64);
1369        inner_blob
1370            .read_at(0, original.len(), ReadOptions::default())
1371            .await
1372            .unwrap()
1373            .coalesce()
1374            .as_ref()
1375            .to_vec()
1376    }
1377
1378    async fn run_random_crash(seed: u64, len: usize) -> Vec<u8> {
1379        let h = Harness::with_seed(
1380            seed,
1381            Config::default().write(WriteConfig {
1382                failure_rate: probability!(0.0),
1383                retention_rate: probability!(0.5),
1384                mode: PartialWriteMode::Subset,
1385            }),
1386        );
1387        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
1388        blob.write_at(0, vec![0; len], WriteOptions::SYNC)
1389            .await
1390            .unwrap();
1391        blob.write_at(0, vec![1; len], WriteOptions::default())
1392            .await
1393            .unwrap();
1394
1395        // The retention policy is part of the completed write, not a global crash-time choice.
1396        h.config.write().write_rate = None;
1397        h.storage.crash().unwrap();
1398
1399        let (blob, durable_len) = h.inner.open("partition", b"test").await.unwrap();
1400        assert_eq!(durable_len, len as u64);
1401        blob.read_at(0, len, ReadOptions::default())
1402            .await
1403            .unwrap()
1404            .coalesce()
1405            .as_ref()
1406            .to_vec()
1407    }
1408
1409    #[tokio::test]
1410    async fn test_reopened_sync_clears_prior_handle_crash_writes() {
1411        let h = Harness::new(Config::default().write(WriteConfig {
1412            failure_rate: probability!(0.0),
1413            retention_rate: probability!(1.0),
1414            mode: PartialWriteMode::Prefix,
1415        }));
1416        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
1417        blob.write_at(0, b"stale", WriteOptions::default())
1418            .await
1419            .unwrap();
1420        drop(blob);
1421
1422        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
1423        blob.write_at(0, b"fresh", WriteOptions::default())
1424            .await
1425            .unwrap();
1426        blob.sync().await.unwrap();
1427        drop(blob);
1428
1429        h.storage.crash().unwrap();
1430        let (blob, len) = h.inner.open("partition", b"test").await.unwrap();
1431        assert_eq!(len, 5);
1432        assert_eq!(
1433            blob.read_at(0, 5, ReadOptions::default())
1434                .await
1435                .unwrap()
1436                .coalesce()
1437                .as_ref(),
1438            b"fresh"
1439        );
1440    }
1441
1442    #[tokio::test]
1443    async fn test_dropped_completed_start_sync_clears_the_crash_epoch() {
1444        let h = Harness::new(Config::default().write(WriteConfig {
1445            failure_rate: probability!(0.0),
1446            retention_rate: probability!(1.0),
1447            mode: PartialWriteMode::Prefix,
1448        }));
1449        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
1450        blob.write_at(0, b"stale", WriteOptions::default())
1451            .await
1452            .unwrap();
1453        h.config.write().write_rate = None;
1454        blob.write_at(0, b"fresh", WriteOptions::default())
1455            .await
1456            .unwrap();
1457
1458        let completion = blob.start_sync().await;
1459        drop(completion);
1460
1461        h.config.write().write_rate = Some(WriteConfig {
1462            failure_rate: probability!(0.0),
1463            retention_rate: probability!(1.0),
1464            mode: PartialWriteMode::Prefix,
1465        });
1466        blob.write_at(5, b"later", WriteOptions::default())
1467            .await
1468            .unwrap();
1469        h.storage.crash().unwrap();
1470
1471        let (blob, len) = h.inner.open("partition", b"test").await.unwrap();
1472        assert_eq!(len, 10);
1473        assert_eq!(
1474            blob.read_at(0, 10, ReadOptions::default())
1475                .await
1476                .unwrap()
1477                .coalesce()
1478                .as_ref(),
1479            b"freshlater"
1480        );
1481    }
1482
1483    #[tokio::test]
1484    async fn test_sync_write_does_not_barrier_disjoint_pending_write() {
1485        let h = Harness::new(Config::default().write(WriteConfig {
1486            failure_rate: probability!(0.0),
1487            retention_rate: probability!(1.0),
1488            mode: PartialWriteMode::Prefix,
1489        }));
1490        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
1491        blob.write_at(0, b"........", WriteOptions::SYNC)
1492            .await
1493            .unwrap();
1494
1495        blob.write_at(0, b"A", WriteOptions::default())
1496            .await
1497            .unwrap();
1498        blob.write_at(7, b"Z", WriteOptions::SYNC).await.unwrap();
1499        h.storage.crash().unwrap();
1500
1501        let (durable, len) = h.inner.open("partition", b"test").await.unwrap();
1502        assert_eq!(len, 8);
1503        assert_eq!(
1504            durable
1505                .read_at(0, 8, ReadOptions::default())
1506                .await
1507                .unwrap()
1508                .coalesce(),
1509            b"A......Z"
1510        );
1511    }
1512
1513    #[tokio::test]
1514    async fn test_sync_write_retires_only_overlapping_pending_bytes() {
1515        let h = Harness::new(Config::default().write(WriteConfig {
1516            failure_rate: probability!(0.0),
1517            retention_rate: probability!(1.0),
1518            mode: PartialWriteMode::Prefix,
1519        }));
1520        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
1521        let (other, _) = h.storage.open("partition", b"other").await.unwrap();
1522        for candidate in [&blob, &other] {
1523            candidate
1524                .write_at(0, b"........", WriteOptions::SYNC)
1525                .await
1526                .unwrap();
1527        }
1528
1529        blob.write_at(0, b"ABCDEFGH", WriteOptions::default())
1530            .await
1531            .unwrap();
1532        other
1533            .write_at(0, b"12345678", WriteOptions::default())
1534            .await
1535            .unwrap();
1536        blob.write_at(3, b"xy", WriteOptions::SYNC).await.unwrap();
1537        h.storage.crash().unwrap();
1538
1539        let (durable, _) = h.inner.open("partition", b"test").await.unwrap();
1540        assert_eq!(
1541            durable
1542                .read_at(0, 8, ReadOptions::default())
1543                .await
1544                .unwrap()
1545                .coalesce(),
1546            b"ABCxyFGH"
1547        );
1548        let (durable, _) = h.inner.open("partition", b"other").await.unwrap();
1549        assert_eq!(
1550            durable
1551                .read_at(0, 8, ReadOptions::default())
1552                .await
1553                .unwrap()
1554                .coalesce(),
1555            b"12345678"
1556        );
1557    }
1558
1559    #[tokio::test]
1560    async fn test_range_sync_preserves_prefix_retention_across_pending_fragments() {
1561        let mut exercised = false;
1562        for seed in 0..64 {
1563            let h = Harness::with_seed(
1564                seed,
1565                Config::default().write(WriteConfig {
1566                    failure_rate: probability!(0.0),
1567                    retention_rate: probability!(0.5),
1568                    mode: PartialWriteMode::Prefix,
1569                }),
1570            );
1571            let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
1572            blob.write_at(0, b"........", WriteOptions::SYNC)
1573                .await
1574                .unwrap();
1575            blob.write_at(0, b"ABCDEFGH", WriteOptions::default())
1576                .await
1577                .unwrap();
1578            blob.write_at(3, b"xy", WriteOptions::SYNC).await.unwrap();
1579
1580            h.storage.crash().unwrap();
1581            let (durable, _) = h.inner.open("partition", b"test").await.unwrap();
1582            let durable = durable
1583                .read_at(0, 8, ReadOptions::default())
1584                .await
1585                .unwrap()
1586                .coalesce();
1587            if durable.as_ref()[..3].contains(&b'.') {
1588                exercised = true;
1589                assert_eq!(&durable.as_ref()[5..], b"...");
1590            }
1591        }
1592        assert!(exercised, "seed sweep must exercise a partial prefix");
1593    }
1594
1595    #[tokio::test]
1596    async fn test_crash_replays_writes_and_resizes_in_issue_order() {
1597        let retained_resizes = [u64::MAX, 0].repeat(3);
1598        let h = Harness::with_rng(
1599            Box::new(ScriptedRng::new(retained_resizes)),
1600            Config::default()
1601                .write(WriteConfig {
1602                    failure_rate: probability!(0.0),
1603                    retention_rate: probability!(1.0),
1604                    mode: PartialWriteMode::Prefix,
1605                })
1606                .resize(ResizeConfig {
1607                    failure_rate: probability!(0.5),
1608                    partial_rate: probability!(0.0),
1609                }),
1610        );
1611        let (write_then_resize, _) = h.storage.open("partition", b"first").await.unwrap();
1612        write_then_resize
1613            .write_at(0, b"abcdef", WriteOptions::default())
1614            .await
1615            .unwrap();
1616        write_then_resize.resize(3).await.unwrap();
1617        write_then_resize.resize(5).await.unwrap();
1618
1619        let (resize_then_write, _) = h.storage.open("partition", b"second").await.unwrap();
1620        resize_then_write
1621            .write_at(0, b"abcdef", WriteOptions::SYNC)
1622            .await
1623            .unwrap();
1624        resize_then_write.resize(3).await.unwrap();
1625        resize_then_write
1626            .write_at(5, b"X", WriteOptions::default())
1627            .await
1628            .unwrap();
1629
1630        h.storage.crash().unwrap();
1631
1632        let (first, len) = h.inner.open("partition", b"first").await.unwrap();
1633        assert_eq!(len, 5);
1634        assert_eq!(
1635            first
1636                .read_at(0, 5, ReadOptions::default())
1637                .await
1638                .unwrap()
1639                .coalesce(),
1640            b"abc\0\0"
1641        );
1642        let (second, len) = h.inner.open("partition", b"second").await.unwrap();
1643        assert_eq!(len, 6);
1644        assert_eq!(
1645            second
1646                .read_at(0, 6, ReadOptions::default())
1647                .await
1648                .unwrap()
1649                .coalesce(),
1650            b"abc\0\0X"
1651        );
1652    }
1653
1654    #[tokio::test]
1655    async fn test_partial_sync_write_replays_after_retained_resize() {
1656        let retained_resize = [u64::MAX, 0];
1657        let retained_write_bytes = [0, u64::MAX, 0, u64::MAX];
1658        let h = Harness::with_rng(
1659            Box::new(ScriptedRng::new(
1660                retained_resize.into_iter().chain(retained_write_bytes),
1661            )),
1662            Config::default().resize(ResizeConfig {
1663                failure_rate: probability!(0.5),
1664                partial_rate: probability!(0.0),
1665            }),
1666        );
1667        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
1668        blob.write_at(0, b"abcdefghij", WriteOptions::SYNC)
1669            .await
1670            .unwrap();
1671        blob.resize(3).await.unwrap();
1672        {
1673            let mut config = h.config.write();
1674            config.write_rate = Some(WriteConfig {
1675                failure_rate: probability!(1.0),
1676                retention_rate: probability!(0.5),
1677                mode: PartialWriteMode::Subset,
1678            });
1679        }
1680
1681        assert!(blob.write_at(6, b"WXYZ", WriteOptions::SYNC).await.is_err());
1682        let (before, _) = h.inner.open("partition", b"test").await.unwrap();
1683        let before = before
1684            .read_at(0, 10, ReadOptions::default())
1685            .await
1686            .unwrap()
1687            .coalesce();
1688        let mut expected = b"abc".to_vec();
1689        for (index, replacement) in b"WXYZ".iter().copied().enumerate() {
1690            let index = index + 6;
1691            if before.as_ref()[index] != replacement {
1692                continue;
1693            }
1694            expected.resize(index + 1, 0);
1695            expected[index] = replacement;
1696        }
1697        assert!(expected.len() > 3);
1698
1699        h.storage.crash().unwrap();
1700
1701        let (durable, len) = h.inner.open("partition", b"test").await.unwrap();
1702        assert_eq!(len, expected.len() as u64);
1703        assert_eq!(
1704            durable
1705                .read_at(0, expected.len(), ReadOptions::default())
1706                .await
1707                .unwrap()
1708                .coalesce()
1709                .as_ref(),
1710            expected
1711        );
1712    }
1713
1714    #[tokio::test]
1715    async fn test_preissued_sync_write_survives_failed_partial_resize() {
1716        let h = Harness::new(Config::default().resize(ResizeConfig {
1717            failure_rate: probability!(1.0),
1718            partial_rate: probability!(1.0),
1719        }));
1720        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
1721        blob.write_at(0, b"abcdefghij", WriteOptions::SYNC)
1722            .await
1723            .unwrap();
1724
1725        let (resize, write) = tokio::join!(biased;
1726            blob.resize(0),
1727            blob.write_at(10, b"X", WriteOptions::SYNC),
1728        );
1729        assert!(resize.is_err());
1730        write.unwrap();
1731
1732        let retained_len = h
1733            .storage
1734            .pending
1735            .lock()
1736            .iter()
1737            .find_map(|mutation| match mutation {
1738                PendingMutation::Resize { len, .. } => Some(*len),
1739                PendingMutation::Write { .. } | PendingMutation::Sync { .. } => None,
1740            })
1741            .unwrap();
1742        h.storage.crash().unwrap();
1743
1744        let (durable, len) = h.inner.open("partition", b"test").await.unwrap();
1745        assert_eq!(len, 11);
1746        let mut expected = b"abcdefghij".to_vec();
1747        expected.truncate(retained_len as usize);
1748        expected.resize(10, 0);
1749        expected.push(b'X');
1750        assert_eq!(
1751            durable
1752                .read_at(0, 11, ReadOptions::default())
1753                .await
1754                .unwrap()
1755                .coalesce()
1756                .as_ref(),
1757            expected
1758        );
1759    }
1760
1761    #[tokio::test]
1762    async fn test_partial_sync_write_retires_each_persisted_range() {
1763        for partial_write_mode in [PartialWriteMode::Prefix, PartialWriteMode::Subset] {
1764            let h = Harness::new(Config::default().write(WriteConfig {
1765                failure_rate: probability!(0.0),
1766                retention_rate: probability!(1.0),
1767                mode: PartialWriteMode::Prefix,
1768            }));
1769            let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
1770            blob.write_at(0, b"........", WriteOptions::SYNC)
1771                .await
1772                .unwrap();
1773            blob.write_at(0, b"ABCDEFGH", WriteOptions::default())
1774                .await
1775                .unwrap();
1776            {
1777                let mut config = h.config.write();
1778                config.write_rate = Some(WriteConfig {
1779                    failure_rate: probability!(1.0),
1780                    retention_rate: probability!(0.5),
1781                    mode: partial_write_mode,
1782                });
1783            }
1784
1785            assert!(blob.write_at(2, b"wxyz", WriteOptions::SYNC).await.is_err());
1786            let (before, _) = h.inner.open("partition", b"test").await.unwrap();
1787            let before = before
1788                .read_at(0, 8, ReadOptions::default())
1789                .await
1790                .unwrap()
1791                .coalesce();
1792            h.storage.crash().unwrap();
1793            let (after, _) = h.inner.open("partition", b"test").await.unwrap();
1794            let after = after
1795                .read_at(0, 8, ReadOptions::default())
1796                .await
1797                .unwrap()
1798                .coalesce();
1799            for (index, (&before, &after)) in before
1800                .as_ref()
1801                .iter()
1802                .zip(after.as_ref().iter())
1803                .enumerate()
1804            {
1805                let expected = if before == b'.' {
1806                    b'A' + index as u8
1807                } else {
1808                    before
1809                };
1810                assert_eq!(
1811                    after, expected,
1812                    "mode={partial_write_mode:?}, index={index}"
1813                );
1814            }
1815        }
1816    }
1817
1818    #[rstest]
1819    #[case::tokio(crate::tokio::Runner::default())]
1820    #[cfg_attr(target_os = "linux", case::iouring(crate::iouring::Runner::default()))]
1821    fn test_full_sync_epoch_is_linearized_with_overlapping_write<R: Runner>(#[case] runner: R)
1822    where
1823        R::Context: Spawner,
1824    {
1825        runner.start(|context| async move {
1826            run_overlapping_barrier(context, false).await;
1827        });
1828    }
1829
1830    #[rstest]
1831    #[case::tokio(crate::tokio::Runner::default())]
1832    #[cfg_attr(target_os = "linux", case::iouring(crate::iouring::Runner::default()))]
1833    fn test_dropped_start_sync_epoch_is_linearized_with_overlapping_write<R: Runner>(
1834        #[case] runner: R,
1835    ) where
1836        R::Context: Spawner,
1837    {
1838        runner.start(|context| async move {
1839            run_overlapping_barrier(context, true).await;
1840        });
1841    }
1842
1843    #[rstest]
1844    #[case::tokio(crate::tokio::Runner::default())]
1845    #[cfg_attr(target_os = "linux", case::iouring(crate::iouring::Runner::default()))]
1846    fn test_faulty_storage_no_faults<R: Runner>(#[case] runner: R)
1847    where
1848        R::Context: Spawner,
1849    {
1850        runner.start(|context| async move {
1851            let h = Harness::new(Config::default());
1852            run_storage_tests(context, h.storage).await;
1853        });
1854    }
1855
1856    #[test]
1857    fn test_probability_endpoints_do_not_consume_randomness() {
1858        let expected = Harness::with_seed(0, Config::default());
1859        let expected = expected.storage.ctx.rng.lock().random::<u64>();
1860
1861        let h = Harness::with_seed(0, Config::default());
1862        for (probability, outcome) in [(probability!(0.0), false), (probability!(1.0), true)] {
1863            assert_eq!(h.storage.ctx.roll(probability), outcome);
1864            for mode in [PartialWriteMode::Prefix, PartialWriteMode::Subset] {
1865                assert_eq!(
1866                    h.storage.ctx.retained_bytes(4, (mode, probability)),
1867                    [outcome; 4]
1868                );
1869            }
1870        }
1871
1872        assert_eq!(h.storage.ctx.rng.lock().random::<u64>(), expected);
1873    }
1874
1875    #[test]
1876    fn test_prefix_retention_rate_selects_inclusive_prefix() {
1877        let h = Harness::new(Config::default());
1878        let mut observed = [false; 5];
1879        for seed in 0..512 {
1880            let h = Harness::with_seed(seed, Config::default());
1881            let retained = h
1882                .storage
1883                .ctx
1884                .retained_bytes(4, (PartialWriteMode::Prefix, probability!(0.5)));
1885            let prefix_len = retained.iter().take_while(|&&keep| keep).count();
1886            assert!(retained[prefix_len..].iter().all(|&keep| !keep));
1887            observed[prefix_len] = true;
1888        }
1889        assert!(observed.iter().all(|&seen| seen));
1890
1891        assert!(
1892            h.storage
1893                .ctx
1894                .retained_bytes(0, (PartialWriteMode::Prefix, probability!(0.5)))
1895                .is_empty()
1896        );
1897    }
1898
1899    #[tokio::test]
1900    async fn test_write_rejects_offset_overflow_before_retention() {
1901        let h = Harness::new(Config::default().write(WriteConfig {
1902            failure_rate: probability!(1.0),
1903            retention_rate: probability!(0.5),
1904            mode: PartialWriteMode::Subset,
1905        }));
1906        let (blob, _) = h.storage.open("partition", b"blob").await.unwrap();
1907        assert!(matches!(
1908            blob.write_at(u64::MAX, vec![1, 2], WriteOptions::default())
1909                .await,
1910            Err(Error::OffsetOverflow)
1911        ));
1912        let (_, len) = h.inner.open("partition", b"blob").await.unwrap();
1913        assert_eq!(len, 0);
1914    }
1915
1916    #[tokio::test]
1917    async fn test_failed_write_can_retain_every_byte() {
1918        let h = Harness::new(Config::default().write(WriteConfig {
1919            failure_rate: probability!(1.0),
1920            retention_rate: probability!(1.0),
1921            mode: PartialWriteMode::Prefix,
1922        }));
1923        let (blob, _) = h.storage.open("partition", b"blob").await.unwrap();
1924
1925        assert!(matches!(
1926            blob.write_at(0, b"x", WriteOptions::default()).await,
1927            Err(Error::Io(_))
1928        ));
1929        let (durable, len) = h.inner.open("partition", b"blob").await.unwrap();
1930        assert_eq!(len, 1);
1931        assert_eq!(
1932            durable
1933                .read_at(0, 1, ReadOptions::default())
1934                .await
1935                .unwrap()
1936                .coalesce(),
1937            b"x"
1938        );
1939    }
1940
1941    #[tokio::test]
1942    async fn test_faulty_blob_forwards_read_options_without_faults() {
1943        let (inner, recordings) = RecordingContext::new(MemStorage::new());
1944        let rng = Arc::new(Mutex::new(Box::new(StdRng::seed_from_u64(42)) as BoxDynRng));
1945        let storage = Storage::new(inner, rng, Arc::new(RwLock::new(Config::default())));
1946        let (blob, _) = storage.open("partition", b"blob").await.unwrap();
1947        blob.write_at(0, b"data", WriteOptions::default())
1948            .await
1949            .unwrap();
1950        recordings.clear();
1951
1952        // With fault rates disabled, both read entry points must preserve DONT_CACHE.
1953        let read = blob.read_at(0, 4, ReadOptions::DONT_CACHE).await.unwrap();
1954        assert_eq!(read.coalesce(), b"data");
1955        let read = blob
1956            .read_at_buf(0, 4, IoBufMut::with_capacity(4), ReadOptions::DONT_CACHE)
1957            .await
1958            .unwrap();
1959        assert_eq!(read.coalesce(), b"data");
1960
1961        assert_eq!(
1962            recordings.snapshot().reads,
1963            vec![ReadOptions::DONT_CACHE, ReadOptions::DONT_CACHE]
1964        );
1965    }
1966
1967    #[tokio::test]
1968    async fn test_faulty_storage_sync_always_fails() {
1969        let h = Harness::new(Config::default().sync(probability!(1.0)));
1970
1971        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
1972        blob.write_at(0, b"data".to_vec(), WriteOptions::default())
1973            .await
1974            .unwrap();
1975
1976        assert!(matches!(blob.sync().await, Err(Error::Io(_))));
1977    }
1978
1979    #[tokio::test]
1980    async fn test_faulty_storage_start_sync_always_fails() {
1981        let h = Harness::new(
1982            Config::default()
1983                .write(WriteConfig {
1984                    failure_rate: probability!(0.0),
1985                    retention_rate: probability!(1.0),
1986                    mode: PartialWriteMode::Prefix,
1987                })
1988                .sync(probability!(1.0)),
1989        );
1990
1991        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
1992        blob.write_at(0, b"data".to_vec(), WriteOptions::default())
1993            .await
1994            .unwrap();
1995
1996        let result = blob.start_sync().await.await;
1997        assert!(matches!(result, Err(Error::Io(_))));
1998        assert_eq!(h.storage.pending.lock().len(), 1);
1999    }
2000
2001    #[tokio::test]
2002    async fn test_faulty_storage_write_always_fails() {
2003        let h = Harness::new(Config::default().write(WriteConfig {
2004            failure_rate: probability!(1.0),
2005            retention_rate: probability!(0.0),
2006            mode: PartialWriteMode::Prefix,
2007        }));
2008
2009        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
2010
2011        assert!(matches!(
2012            blob.write_at(0, b"data".to_vec(), WriteOptions::default())
2013                .await,
2014            Err(Error::Io(_))
2015        ));
2016    }
2017
2018    #[tokio::test]
2019    async fn test_faulty_storage_write_at_sync_write_always_fails() {
2020        let h = Harness::new(Config::default().write(WriteConfig {
2021            failure_rate: probability!(1.0),
2022            retention_rate: probability!(0.0),
2023            mode: PartialWriteMode::Prefix,
2024        }));
2025
2026        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
2027
2028        assert!(matches!(
2029            blob.write_at(0, b"data".to_vec(), WriteOptions::SYNC).await,
2030            Err(Error::Io(_))
2031        ));
2032    }
2033
2034    #[tokio::test]
2035    async fn test_faulty_storage_write_at_sync_failure_is_not_durable() {
2036        let h = Harness::new(Config::default().sync(probability!(1.0)));
2037
2038        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
2039
2040        assert!(matches!(
2041            blob.write_at(0, b"data".to_vec(), WriteOptions::SYNC).await,
2042            Err(Error::Io(_))
2043        ));
2044
2045        let (_reopened, size) = h.inner.open("partition", b"test").await.unwrap();
2046        assert_eq!(size, 0);
2047    }
2048
2049    #[tokio::test]
2050    async fn test_faulty_storage_empty_write_at_sync_does_not_sync_prior_write() {
2051        let h = Harness::new(Config::default().sync(probability!(1.0)));
2052
2053        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
2054        blob.write_at(0, b"data".to_vec(), WriteOptions::default())
2055            .await
2056            .unwrap();
2057
2058        blob.write_at(4, Vec::<u8>::new(), WriteOptions::SYNC)
2059            .await
2060            .unwrap();
2061
2062        let (_reopened, size) = h.inner.open("partition", b"test").await.unwrap();
2063        assert_eq!(size, 0);
2064    }
2065
2066    #[tokio::test]
2067    async fn test_empty_unsynced_write_does_not_create_crash_debt() {
2068        let h = Harness::new(Config::default().write(WriteConfig {
2069            failure_rate: probability!(0.0),
2070            retention_rate: probability!(1.0),
2071            mode: PartialWriteMode::Prefix,
2072        }));
2073        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
2074        blob.write_at(0, Vec::<u8>::new(), WriteOptions::default())
2075            .await
2076            .unwrap();
2077        assert!(h.storage.pending.lock().is_empty());
2078    }
2079
2080    #[tokio::test]
2081    async fn test_faulty_storage_read_always_fails() {
2082        let h = Harness::new(Config::default());
2083
2084        // Write some data first (no faults)
2085        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
2086        blob.write_at(0, b"data".to_vec(), WriteOptions::default())
2087            .await
2088            .unwrap();
2089        blob.sync().await.unwrap();
2090
2091        // Enable read faults
2092        h.config.write().read_rate = Some(probability!(1.0));
2093
2094        assert!(matches!(
2095            blob.read_at(0, 4, ReadOptions::default()).await,
2096            Err(Error::Io(_))
2097        ));
2098    }
2099
2100    #[tokio::test]
2101    async fn test_faulty_storage_open_always_fails() {
2102        let h = Harness::new(Config::default().open(probability!(1.0)));
2103
2104        assert!(matches!(
2105            h.storage.open("partition", b"test").await,
2106            Err(Error::Io(_))
2107        ));
2108    }
2109
2110    #[tokio::test]
2111    async fn test_partition_removal_preserves_unrelated_crash_writes() {
2112        // Retain every unsynced write so crash replay exposes evidence lost during removal.
2113        let h = Harness::new(Config::default().write(WriteConfig {
2114            failure_rate: probability!(0.0),
2115            retention_rate: probability!(1.0),
2116            mode: PartialWriteMode::Prefix,
2117        }));
2118        let (a, _) = h.storage.open("removed", b"a").await.unwrap();
2119        let (b, _) = h.storage.open("removed", b"b").await.unwrap();
2120        let (kept, _) = h.storage.open("kept", b"blob").await.unwrap();
2121
2122        // Interleave both removed blobs with two distinct retained ranges.
2123        for (blob, offset, bytes) in [
2124            (&a, 0, b"one"),
2125            (&kept, 0, b"old"),
2126            (&b, 0, b"two"),
2127            (&kept, 3, b"new"),
2128        ] {
2129            blob.write_at(offset, bytes.as_slice(), WriteOptions::default())
2130                .await
2131                .unwrap();
2132        }
2133        drop((a, b, kept));
2134
2135        // Retirement must remove both names' evidence and leave exactly the kept writes.
2136        h.storage.remove("removed", None).await.unwrap();
2137        assert_eq!(h.storage.pending.lock().len(), 2);
2138        h.storage.crash().unwrap();
2139
2140        // Replay preserves both ranges in the surviving partition and cannot revive removed names.
2141        assert!(matches!(
2142            h.inner.scan("removed").await,
2143            Err(Error::PartitionMissing(_))
2144        ));
2145        let (kept, len) = h.inner.open("kept", b"blob").await.unwrap();
2146        assert_eq!(len, 6);
2147        assert_eq!(
2148            kept.read_at(0, 6, ReadOptions::default())
2149                .await
2150                .unwrap()
2151                .coalesce()
2152                .as_ref(),
2153            b"oldnew"
2154        );
2155    }
2156
2157    #[tokio::test]
2158    async fn test_faulty_storage_remove_always_fails() {
2159        let h = Harness::new(Config::default());
2160
2161        // Create a blob first
2162        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
2163        blob.write_at(0, b"data".to_vec(), WriteOptions::default())
2164            .await
2165            .unwrap();
2166        blob.sync().await.unwrap();
2167        drop(blob);
2168
2169        // Enable remove faults
2170        h.config.write().remove_rate = Some(probability!(1.0));
2171
2172        assert!(matches!(
2173            h.storage.remove("partition", Some(b"test")).await,
2174            Err(Error::Io(_))
2175        ));
2176    }
2177
2178    #[tokio::test]
2179    async fn test_faulty_storage_scan_always_fails() {
2180        let h = Harness::new(Config::default());
2181
2182        // Create some blobs first
2183        for i in 0..3 {
2184            let name = format!("blob{i}");
2185            let (blob, _) = h.storage.open("partition", name.as_bytes()).await.unwrap();
2186            blob.write_at(0, b"data".to_vec(), WriteOptions::default())
2187                .await
2188                .unwrap();
2189            blob.sync().await.unwrap();
2190        }
2191
2192        // Enable scan faults
2193        h.config.write().scan_rate = Some(probability!(1.0));
2194
2195        assert!(matches!(
2196            h.storage.scan("partition").await,
2197            Err(Error::Io(_))
2198        ));
2199    }
2200
2201    #[tokio::test]
2202    async fn test_faulty_storage_determinism() {
2203        async fn run_ops(seed: u64, rate: Probability) -> Vec<bool> {
2204            let h = Harness::with_seed(seed, Config::default().open(rate));
2205            let mut results = Vec::new();
2206            for i in 0..20 {
2207                let name = format!("blob{i}");
2208                results.push(h.storage.open("partition", name.as_bytes()).await.is_ok());
2209            }
2210            results
2211        }
2212
2213        let results1 = run_ops(42, probability!(0.5)).await;
2214        let results2 = run_ops(42, probability!(0.5)).await;
2215        assert_eq!(results1, results2, "Same seed should produce same results");
2216
2217        let results3 = run_ops(999, probability!(0.5)).await;
2218        assert_ne!(
2219            results1, results3,
2220            "Different seeds should produce different results"
2221        );
2222    }
2223
2224    #[tokio::test]
2225    async fn test_faulty_storage_rate_for() {
2226        let config = Config::default()
2227            .open(probability!(0.1))
2228            .write(WriteConfig {
2229                failure_rate: probability!(0.3),
2230                retention_rate: probability!(0.4),
2231                mode: PartialWriteMode::Subset,
2232            })
2233            .resize(ResizeConfig {
2234                failure_rate: probability!(0.7),
2235                partial_rate: probability!(0.8),
2236            })
2237            .sync(probability!(0.9));
2238
2239        assert_eq!(config.rate_for(Op::Open), probability!(0.1));
2240        assert_eq!(config.rate_for(Op::Write), probability!(0.3));
2241        assert_eq!(config.rate_for(Op::Resize), probability!(0.7));
2242        assert_eq!(config.rate_for(Op::Sync), probability!(0.9));
2243    }
2244
2245    #[tokio::test]
2246    async fn test_faulty_storage_dynamic_config() {
2247        let h = Harness::new(Config::default());
2248
2249        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
2250        blob.sync().await.unwrap();
2251
2252        h.config.write().sync_rate = Some(probability!(1.0));
2253        assert!(matches!(blob.sync().await, Err(Error::Io(_))));
2254
2255        h.config.write().sync_rate = Some(probability!(0.0));
2256        blob.sync().await.unwrap();
2257    }
2258
2259    #[tokio::test]
2260    async fn test_write_retention_is_snapshotted_and_replayed_in_order() {
2261        let h = Harness::new(Config::default().write(WriteConfig {
2262            failure_rate: probability!(0.0),
2263            retention_rate: probability!(1.0),
2264            mode: PartialWriteMode::Prefix,
2265        }));
2266        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
2267        blob.write_at(0, b"........", WriteOptions::SYNC)
2268            .await
2269            .unwrap();
2270
2271        blob.write_at(0, b"AB", WriteOptions::default())
2272            .await
2273            .unwrap();
2274        h.config.write().write_rate = None;
2275        blob.write_at(2, b"CD", WriteOptions::default())
2276            .await
2277            .unwrap();
2278        h.config.write().write_rate = Some(WriteConfig {
2279            failure_rate: probability!(0.0),
2280            retention_rate: probability!(1.0),
2281            mode: PartialWriteMode::Prefix,
2282        });
2283        blob.write_at(1, b"XY", WriteOptions::default())
2284            .await
2285            .unwrap();
2286        h.config.write().write_rate = None;
2287
2288        h.storage.crash().unwrap();
2289        let (durable, _) = h.inner.open("partition", b"test").await.unwrap();
2290        assert_eq!(
2291            durable
2292                .read_at(0, 8, ReadOptions::default())
2293                .await
2294                .unwrap()
2295                .coalesce(),
2296            b"AXY....."
2297        );
2298
2299        durable.write_at(0, b"Q", WriteOptions::SYNC).await.unwrap();
2300        h.storage.crash().unwrap();
2301        let (durable, _) = h.inner.open("partition", b"test").await.unwrap();
2302        assert_eq!(
2303            durable
2304                .read_at(0, 8, ReadOptions::default())
2305                .await
2306                .unwrap()
2307                .coalesce(),
2308            b"QXY....."
2309        );
2310    }
2311
2312    #[tokio::test]
2313    async fn test_random_crash_write_is_deterministic_and_inclusive() {
2314        let first = run_random_crash(12345, 64).await;
2315        let second = run_random_crash(12345, 64).await;
2316        let different = run_random_crash(54321, 64).await;
2317        assert_eq!(first, second);
2318        assert_ne!(first, different);
2319        assert!(first.contains(&0));
2320        assert!(first.contains(&1));
2321
2322        let mut saw_empty = false;
2323        let mut saw_full = false;
2324        for seed in 0..64 {
2325            match run_random_crash(seed, 1).await.as_slice() {
2326                [0] => saw_empty = true,
2327                [1] => saw_full = true,
2328                other => panic!("unexpected one-byte crash result: {other:?}"),
2329            }
2330        }
2331        assert!(saw_empty && saw_full);
2332    }
2333
2334    #[tokio::test]
2335    async fn failed_partial_write_does_not_barrier_prior_crash_writes() {
2336        for partial_write_mode in [PartialWriteMode::Prefix, PartialWriteMode::Subset] {
2337            let mut saw_old = false;
2338            let mut saw_new = false;
2339            for seed in 0..64 {
2340                let h = Harness::with_seed(
2341                    seed,
2342                    Config::default().write(WriteConfig {
2343                        failure_rate: probability!(0.0),
2344                        retention_rate: probability!(0.5),
2345                        mode: PartialWriteMode::Subset,
2346                    }),
2347                );
2348                let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
2349                blob.write_at(0, b"......", WriteOptions::SYNC)
2350                    .await
2351                    .unwrap();
2352                blob.write_at(0, b"AAAA", WriteOptions::default())
2353                    .await
2354                    .unwrap();
2355                {
2356                    let mut config = h.config.write();
2357                    config.write_rate = Some(WriteConfig {
2358                        failure_rate: probability!(1.0),
2359                        retention_rate: probability!(0.5),
2360                        mode: partial_write_mode,
2361                    });
2362                }
2363
2364                assert!(
2365                    blob.write_at(4, b"XY", WriteOptions::default())
2366                        .await
2367                        .is_err()
2368                );
2369                h.storage.crash().unwrap();
2370
2371                let (durable, _) = h.inner.open("partition", b"test").await.unwrap();
2372                let durable = durable
2373                    .read_at(0, 4, ReadOptions::default())
2374                    .await
2375                    .unwrap()
2376                    .coalesce();
2377                saw_old |= durable.as_ref().contains(&b'.');
2378                saw_new |= durable.as_ref().contains(&b'A');
2379            }
2380            assert!(saw_old && saw_new);
2381        }
2382    }
2383
2384    #[tokio::test]
2385    async fn failed_partial_resize_does_not_barrier_prior_crash_writes() {
2386        let mut saw_old = false;
2387        let mut saw_new = false;
2388        for seed in 0..64 {
2389            let h = Harness::with_seed(
2390                seed,
2391                Config::default().write(WriteConfig {
2392                    failure_rate: probability!(0.0),
2393                    retention_rate: probability!(0.5),
2394                    mode: PartialWriteMode::Subset,
2395                }),
2396            );
2397            let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
2398            blob.write_at(0, b"......", WriteOptions::SYNC)
2399                .await
2400                .unwrap();
2401            blob.write_at(0, b"AAAA", WriteOptions::default())
2402                .await
2403                .unwrap();
2404            {
2405                let mut config = h.config.write();
2406                config.resize_rate = Some(ResizeConfig {
2407                    failure_rate: probability!(1.0),
2408                    partial_rate: probability!(1.0),
2409                });
2410            }
2411
2412            assert!(blob.resize(8).await.is_err());
2413            h.storage.crash().unwrap();
2414
2415            let (durable, len) = h.inner.open("partition", b"test").await.unwrap();
2416            assert_eq!(len, 7);
2417            let durable = durable
2418                .read_at(0, 4, ReadOptions::default())
2419                .await
2420                .unwrap()
2421                .coalesce();
2422            saw_old |= durable.as_ref().contains(&b'.');
2423            saw_new |= durable.as_ref().contains(&b'A');
2424        }
2425        assert!(saw_old && saw_new);
2426    }
2427
2428    #[tokio::test]
2429    async fn test_crash_journal_clears_only_after_completed_durability() {
2430        let h = Harness::new(Config::default().write(WriteConfig {
2431            failure_rate: probability!(0.0),
2432            retention_rate: probability!(1.0),
2433            mode: PartialWriteMode::Prefix,
2434        }));
2435        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
2436
2437        blob.write_at(0, b"a", WriteOptions::default())
2438            .await
2439            .unwrap();
2440        assert_eq!(h.storage.pending.lock().len(), 1);
2441        h.config.write().sync_rate = Some(probability!(1.0));
2442        assert!(matches!(blob.sync().await, Err(Error::Io(_))));
2443        assert_eq!(h.storage.pending.lock().len(), 1);
2444
2445        h.config.write().sync_rate = None;
2446        blob.sync().await.unwrap();
2447        assert!(h.storage.pending.lock().is_empty());
2448
2449        let (other, _) = h.storage.open("partition", b"other").await.unwrap();
2450        blob.write_at(0, b"b", WriteOptions::default())
2451            .await
2452            .unwrap();
2453        other
2454            .write_at(0, b"x", WriteOptions::default())
2455            .await
2456            .unwrap();
2457        assert_eq!(h.storage.pending.lock().len(), 2);
2458        blob.sync().await.unwrap();
2459        assert_eq!(h.storage.pending.lock().len(), 1);
2460        other.sync().await.unwrap();
2461        assert!(h.storage.pending.lock().is_empty());
2462
2463        blob.write_at(0, b"b", WriteOptions::default())
2464            .await
2465            .unwrap();
2466        assert_eq!(h.storage.pending.lock().len(), 1);
2467        blob.start_sync().await.await.unwrap();
2468        assert!(h.storage.pending.lock().is_empty());
2469
2470        blob.write_at(0, b"c", WriteOptions::default())
2471            .await
2472            .unwrap();
2473        assert_eq!(h.storage.pending.lock().len(), 1);
2474        blob.write_at(0, b"d", WriteOptions::SYNC).await.unwrap();
2475        assert!(h.storage.pending.lock().is_empty());
2476    }
2477
2478    #[tokio::test]
2479    async fn test_sync_failure_journals_the_successful_inner_write() {
2480        let h = Harness::new(
2481            Config::default()
2482                .write(WriteConfig {
2483                    failure_rate: probability!(0.0),
2484                    retention_rate: probability!(1.0),
2485                    mode: PartialWriteMode::Prefix,
2486                })
2487                .sync(probability!(1.0)),
2488        );
2489        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
2490        assert!(matches!(
2491            blob.write_at(0, b"data", WriteOptions::SYNC).await,
2492            Err(Error::Io(_))
2493        ));
2494        assert_eq!(h.storage.pending.lock().len(), 1);
2495
2496        h.storage.crash().unwrap();
2497        let (durable, len) = h.inner.open("partition", b"test").await.unwrap();
2498        assert_eq!(len, 4);
2499        assert_eq!(
2500            durable
2501                .read_at(0, 4, ReadOptions::default())
2502                .await
2503                .unwrap()
2504                .coalesce(),
2505            b"data"
2506        );
2507    }
2508
2509    #[tokio::test]
2510    async fn test_faulty_storage_zero_write_retention_preserves_nothing() {
2511        let h = Harness::new(Config::default().write(WriteConfig {
2512            failure_rate: probability!(1.0),
2513            retention_rate: probability!(0.0),
2514            mode: PartialWriteMode::Prefix,
2515        }));
2516
2517        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
2518        let data = b"hello world".to_vec();
2519        let result = blob
2520            .write_at(0, data.clone(), WriteOptions::default())
2521            .await;
2522
2523        assert!(matches!(result, Err(Error::Io(_))));
2524
2525        let (_, size) = h.inner.open("partition", b"test").await.unwrap();
2526        assert_eq!(size, 0);
2527    }
2528
2529    #[tokio::test]
2530    async fn test_faulty_storage_partial_write_subset_can_be_non_prefix() {
2531        let original = b"abcdefghijklmnop";
2532        let replacement = b"ABCDEFGHIJKLMNOP";
2533        let observed = run_subset_overwrite(42, original, replacement).await;
2534
2535        let retained: Vec<_> = observed
2536            .iter()
2537            .zip(original)
2538            .zip(replacement)
2539            .map(|((&actual, &old), &new)| {
2540                assert!(actual == old || actual == new);
2541                actual == new
2542            })
2543            .collect();
2544        assert!(retained.iter().any(|&keep| keep));
2545        assert!(retained.iter().any(|&keep| !keep));
2546
2547        let mut omitted = false;
2548        let non_prefix = retained.into_iter().any(|keep| {
2549            if keep {
2550                omitted
2551            } else {
2552                omitted = true;
2553                false
2554            }
2555        });
2556        assert!(non_prefix, "expected a retained byte after an omitted byte");
2557    }
2558
2559    #[tokio::test]
2560    async fn test_faulty_storage_write_retention_rate_endpoints() {
2561        for (retention_rate, expected) in [
2562            (probability!(0.0), b"old".as_slice()),
2563            (probability!(1.0), b"new".as_slice()),
2564        ] {
2565            for mode in [PartialWriteMode::Prefix, PartialWriteMode::Subset] {
2566                let failed = Harness::new(Config::default().write(WriteConfig {
2567                    failure_rate: probability!(1.0),
2568                    retention_rate,
2569                    mode,
2570                }));
2571                let (blob, _) = failed.storage.open("partition", b"failed").await.unwrap();
2572                blob.write_at(0, b"new", WriteOptions::SYNC)
2573                    .await
2574                    .unwrap_err();
2575                let (durable, len) = failed.inner.open("partition", b"failed").await.unwrap();
2576                if retention_rate.is_one() {
2577                    assert_eq!(len, 3);
2578                    assert_eq!(
2579                        durable
2580                            .read_at(0, 3, ReadOptions::default())
2581                            .await
2582                            .unwrap()
2583                            .coalesce(),
2584                        expected
2585                    );
2586                } else {
2587                    assert_eq!(len, 0);
2588                }
2589
2590                let crashed = Harness::new(Config::default().write(WriteConfig {
2591                    failure_rate: probability!(0.0),
2592                    retention_rate,
2593                    mode,
2594                }));
2595                let (blob, _) = crashed.storage.open("partition", b"crashed").await.unwrap();
2596                blob.write_at(0, b"old", WriteOptions::SYNC).await.unwrap();
2597                blob.write_at(0, b"new", WriteOptions::default())
2598                    .await
2599                    .unwrap();
2600                crashed.storage.crash().unwrap();
2601                let (durable, len) = crashed.inner.open("partition", b"crashed").await.unwrap();
2602                assert_eq!(len, 3);
2603                assert_eq!(
2604                    durable
2605                        .read_at(0, 3, ReadOptions::default())
2606                        .await
2607                        .unwrap()
2608                        .coalesce(),
2609                    expected
2610                );
2611            }
2612        }
2613    }
2614
2615    #[tokio::test]
2616    async fn test_faulty_storage_partial_write_subset_same_seed_is_deterministic() {
2617        let original = vec![0x11; 64];
2618        let replacement = vec![0xAA; 64];
2619
2620        let first = run_subset_overwrite(12345, &original, &replacement).await;
2621        let second = run_subset_overwrite(12345, &original, &replacement).await;
2622
2623        assert_eq!(first, second);
2624        assert!(first.contains(&0x11));
2625        assert!(first.contains(&0xAA));
2626    }
2627
2628    #[tokio::test]
2629    async fn test_faulty_storage_partial_resize_grow() {
2630        let h = Harness::new(Config::default().resize(ResizeConfig {
2631            failure_rate: probability!(1.0),
2632            partial_rate: probability!(1.0),
2633        }));
2634
2635        let (blob, initial_size) = h.storage.open("partition", b"test").await.unwrap();
2636        assert_eq!(initial_size, 0);
2637
2638        let target_size = 100u64;
2639        let result = blob.resize(target_size).await;
2640
2641        assert!(matches!(result, Err(Error::Io(_))));
2642        h.storage.crash().unwrap();
2643
2644        let (_, actual_size) = h.inner.open("partition", b"test").await.unwrap();
2645        assert!(
2646            actual_size > 0 && actual_size < target_size,
2647            "Expected partial resize: size {actual_size} should be between 0 and {target_size}"
2648        );
2649    }
2650
2651    #[tokio::test]
2652    async fn test_faulty_storage_partial_resize_shrink() {
2653        let h = Harness::new(Config::default());
2654
2655        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
2656        blob.resize(100).await.unwrap();
2657        blob.sync().await.unwrap();
2658
2659        {
2660            let mut cfg = h.config.write();
2661            cfg.resize_rate = Some(ResizeConfig {
2662                failure_rate: probability!(1.0),
2663                partial_rate: probability!(1.0),
2664            });
2665        }
2666
2667        let target_size = 10u64;
2668        let result = blob.resize(target_size).await;
2669
2670        assert!(matches!(result, Err(Error::Io(_))));
2671        h.storage.crash().unwrap();
2672
2673        let (_, actual_size) = h.inner.open("partition", b"test").await.unwrap();
2674        assert!(
2675            actual_size > target_size && actual_size < 100,
2676            "Expected partial shrink: size {actual_size} should be between {target_size} and 100"
2677        );
2678    }
2679
2680    #[tokio::test]
2681    async fn test_faulty_storage_partial_resize_disabled() {
2682        let h = Harness::new(Config::default().resize(ResizeConfig {
2683            failure_rate: probability!(1.0),
2684            partial_rate: probability!(0.0),
2685        }));
2686
2687        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
2688        let result = blob.resize(100).await;
2689
2690        assert!(matches!(result, Err(Error::Io(_))));
2691
2692        let (_, size) = h.inner.open("partition", b"test").await.unwrap();
2693        assert_eq!(size, 0, "Expected no resize when partial rate is 0");
2694    }
2695
2696    #[tokio::test]
2697    async fn test_faulty_storage_partial_resize_same_size() {
2698        let h = Harness::new(Config::default().resize(ResizeConfig {
2699            failure_rate: probability!(1.0),
2700            partial_rate: probability!(1.0),
2701        }));
2702
2703        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
2704        let result = blob.resize(0).await;
2705
2706        assert!(matches!(result, Err(Error::Io(_))));
2707
2708        let (_, size) = h.inner.open("partition", b"test").await.unwrap();
2709        assert_eq!(size, 0);
2710    }
2711
2712    #[tokio::test]
2713    async fn test_faulty_storage_partial_resize_after_write_extends() {
2714        let h = Harness::new(Config::default());
2715
2716        let (blob, initial_size) = h.storage.open("partition", b"test").await.unwrap();
2717        assert_eq!(initial_size, 0);
2718
2719        blob.write_at(0, vec![0xABu8; 50], WriteOptions::default())
2720            .await
2721            .unwrap();
2722        blob.sync().await.unwrap();
2723
2724        let (_, size_after_write) = h.inner.open("partition", b"test").await.unwrap();
2725        assert_eq!(size_after_write, 50);
2726
2727        {
2728            let mut cfg = h.config.write();
2729            cfg.resize_rate = Some(ResizeConfig {
2730                failure_rate: probability!(1.0),
2731                partial_rate: probability!(1.0),
2732            });
2733        }
2734
2735        let target_size = 10u64;
2736        let result = blob.resize(target_size).await;
2737
2738        assert!(matches!(result, Err(Error::Io(_))));
2739        h.storage.crash().unwrap();
2740
2741        let (_, actual_size) = h.inner.open("partition", b"test").await.unwrap();
2742        assert!(
2743            actual_size > target_size && actual_size < 50,
2744            "Expected partial shrink from 50: size {actual_size} should be between {target_size} and 50"
2745        );
2746    }
2747
2748    #[tokio::test]
2749    async fn test_faulty_storage_partial_resize_one_byte_difference() {
2750        let h = Harness::new(Config::default().resize(ResizeConfig {
2751            failure_rate: probability!(1.0),
2752            partial_rate: probability!(1.0),
2753        }));
2754
2755        let (blob, _) = h.storage.open("partition", b"test").await.unwrap();
2756        let result = blob.resize(1).await;
2757
2758        assert!(matches!(result, Err(Error::Io(_))));
2759
2760        let (_, size) = h.inner.open("partition", b"test").await.unwrap();
2761        assert_eq!(size, 0);
2762    }
2763}