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