Skip to main content

acyclic_fs/
simulation.rs

1//! Deterministic bounded fault simulator for authority and immutable-object backends.
2//!
3//! The simulator is qualification infrastructure over the two canonical storage
4//! primitives. It never becomes filesystem truth: healing or dropping it leaves
5//! only the wrapped memory authority log and immutable objects.
6
7use crate::async_storage::{AsyncAuthorityStore, AsyncObjectStore, GenerationForkSource};
8use crate::cancellation::CancellationToken;
9use crate::foundation::{
10    AuthorityId, DurableCommit, Epoch, Head, OperationId, ProposedCommit, Sequence,
11};
12use crate::performance::{WorkBudget, WorkCounters};
13use crate::storage::{
14    AppendOutcome, AuthorityFailure, AuthorityResult, AuthorityStoreError, CreateAuthorityOutcome,
15    ObjectFailure, ObjectId, ObjectRead, ObjectReadRequest, ObjectReadRetention, ObjectReceipt,
16    ObjectResult, ObjectStoreError, ReplayLimit, object_digest,
17};
18use bytes::Bytes;
19use std::collections::BTreeMap;
20use std::sync::{Arc, Mutex, MutexGuard};
21use thiserror::Error;
22
23type ReferenceAuthority = crate::distributed::StreamAuthorityStore<acyclic_stream::MemoryStream>;
24type ReferenceObjects = crate::distributed::ProviderObjectStore<acyclic_objects::MemoryObjects>;
25
26/// One exact simulator interception point.
27#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
28pub enum SimulationOperation {
29    /// Authority creation.
30    AuthorityCreate,
31    /// Linearizable authority-head read.
32    AuthorityHead,
33    /// Authority compare-and-append.
34    AuthorityAppend,
35    /// Bounded authority replay.
36    AuthorityReplay,
37    /// Authority epoch fence.
38    AuthorityFence,
39    /// Idempotent operation lookup.
40    AuthorityFindOperation,
41    /// Immutable-object admission.
42    ObjectPut,
43    /// One immutable-object read.
44    ObjectRead,
45    /// Ordered immutable-object batch read.
46    ObjectReadMany,
47    /// Immutable-object presence probe.
48    ObjectContains,
49}
50
51/// One exact injected backend behavior.
52#[derive(Clone, Copy, Debug, Eq, PartialEq)]
53pub enum SimulationFault {
54    /// Reject before touching the wrapped primitive.
55    RejectBefore,
56    /// Simulate an unavailable partition before touching the wrapped primitive.
57    PartitionBefore,
58    /// Acknowledge a valid object but delay its visibility until an explicit flush.
59    DelayObjectVisibility,
60    /// Return one digest-invalid byte from an otherwise successful object read.
61    CorruptObjectRead,
62    /// Return a strict prefix from an otherwise successful object read.
63    PartialObjectRead,
64    /// Return a caller-selected stale authority head.
65    StaleAuthorityHead(Head),
66    /// Duplicate the final record of an otherwise valid replay page.
67    DuplicateAuthorityReplay,
68    /// Durably append, then lose the acknowledgement as an indeterminate result.
69    AmbiguousAuthorityAppend,
70    /// Advance the epoch immediately before compare-and-append.
71    FenceBeforeAuthorityAppend,
72}
73
74impl SimulationFault {
75    const fn compatible(self, operation: SimulationOperation) -> bool {
76        match self {
77            Self::RejectBefore | Self::PartitionBefore => true,
78            Self::DelayObjectVisibility => matches!(operation, SimulationOperation::ObjectPut),
79            Self::CorruptObjectRead | Self::PartialObjectRead => matches!(
80                operation,
81                SimulationOperation::ObjectRead | SimulationOperation::ObjectReadMany
82            ),
83            Self::StaleAuthorityHead(_) => {
84                matches!(operation, SimulationOperation::AuthorityHead)
85            }
86            Self::DuplicateAuthorityReplay => {
87                matches!(operation, SimulationOperation::AuthorityReplay)
88            }
89            Self::AmbiguousAuthorityAppend | Self::FenceBeforeAuthorityAppend => {
90                matches!(operation, SimulationOperation::AuthorityAppend)
91            }
92        }
93    }
94}
95
96/// One exact operation occurrence and its injected behavior.
97#[derive(Clone, Copy, Debug, Eq, PartialEq)]
98pub struct ScheduledSimulationFault {
99    /// Intercepted operation family.
100    pub operation: SimulationOperation,
101    /// One-based occurrence within that operation family.
102    pub occurrence: u64,
103    /// Behavior injected at the exact occurrence.
104    pub fault: SimulationFault,
105}
106
107/// Hard simulator resource bounds.
108#[derive(Clone, Copy, Debug, Eq, PartialEq)]
109pub struct SimulationOptions {
110    /// Maximum scheduled fault points retained at once.
111    pub maximum_scheduled_faults: usize,
112    /// Maximum operation trace entries retained before fail-closed saturation.
113    pub maximum_trace_entries: usize,
114    /// Maximum delayed immutable objects retained before visibility flush.
115    pub maximum_pending_objects: usize,
116    /// Maximum cumulative delayed canonical object bytes.
117    pub maximum_pending_object_bytes: u64,
118}
119
120impl Default for SimulationOptions {
121    fn default() -> Self {
122        Self {
123            maximum_scheduled_faults: 4_096,
124            maximum_trace_entries: 65_536,
125            maximum_pending_objects: 4_096,
126            maximum_pending_object_bytes: 256 * 1024 * 1024,
127        }
128    }
129}
130
131/// One retained deterministic interception trace.
132#[derive(Clone, Copy, Debug, Eq, PartialEq)]
133pub struct SimulationTrace {
134    /// Intercepted operation family.
135    pub operation: SimulationOperation,
136    /// One-based occurrence within the operation family.
137    pub occurrence: u64,
138    /// Injected behavior, when this operation matched a schedule entry.
139    pub fault: Option<SimulationFault>,
140}
141
142/// Simulator configuration or state failure.
143#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
144pub enum SimulationError {
145    /// Every configured hard bound must be positive.
146    #[error("simulation bounds must be positive")]
147    ZeroBound,
148    /// Fault occurrence identities are one-based.
149    #[error("simulation fault occurrence must be positive")]
150    ZeroOccurrence,
151    /// The selected fault cannot apply to the selected operation family.
152    #[error("simulation fault is incompatible with its operation family")]
153    IncompatibleFault,
154    /// The exact operation occurrence already has a scheduled fault.
155    #[error("simulation operation occurrence already has a fault")]
156    DuplicateFault,
157    /// The selected operation occurrence has already completed.
158    #[error("simulation operation occurrence has already completed")]
159    PastOccurrence,
160    /// The configured fault schedule bound is exhausted.
161    #[error("simulation fault schedule is full")]
162    ScheduleFull,
163    /// A simulator lock was poisoned by a failed host thread.
164    #[error("simulation state is poisoned")]
165    Poisoned,
166}
167
168struct State {
169    options: SimulationOptions,
170    occurrences: BTreeMap<SimulationOperation, u64>,
171    faults: BTreeMap<(SimulationOperation, u64), SimulationFault>,
172    trace: Vec<SimulationTrace>,
173    pending_objects: BTreeMap<ObjectId, Bytes>,
174    pending_object_bytes: u64,
175}
176
177impl State {
178    fn intercept(
179        &mut self,
180        operation: SimulationOperation,
181    ) -> Result<(u64, Option<SimulationFault>), &'static str> {
182        if self.trace.len() == self.options.maximum_trace_entries {
183            return Err("simulation trace bound exhausted");
184        }
185        let occurrence = self
186            .occurrences
187            .get(&operation)
188            .copied()
189            .unwrap_or(0)
190            .checked_add(1)
191            .ok_or("simulation operation occurrence overflow")?;
192        self.occurrences.insert(operation, occurrence);
193        let fault = self.faults.remove(&(operation, occurrence));
194        self.trace
195            .try_reserve(1)
196            .map_err(|_| "simulation trace allocation failed")?;
197        self.trace.push(SimulationTrace {
198            operation,
199            occurrence,
200            fault,
201        });
202        Ok((occurrence, fault))
203    }
204}
205
206/// Shared deterministic simulator and its two backend handles.
207#[derive(Clone)]
208pub struct Simulation<
209    A = crate::distributed::StreamAuthorityStore<acyclic_stream::MemoryStream>,
210    O = crate::distributed::ProviderObjectStore<acyclic_objects::MemoryObjects>,
211> {
212    state: Arc<Mutex<State>>,
213    authority: Arc<A>,
214    objects: Arc<O>,
215}
216
217impl Simulation<ReferenceAuthority, ReferenceObjects> {
218    /// Creates a bounded simulator over fresh deterministic memory primitives.
219    ///
220    /// # Errors
221    ///
222    /// Rejects any zero hard bound before allocating simulator state.
223    pub fn new(options: SimulationOptions) -> Result<Self, SimulationError> {
224        let (authority, objects) = reference_backends();
225        Self::wrap(authority, objects, options)
226    }
227}
228
229impl<A, O> Simulation<A, O> {
230    /// Wraps explicit backends with one bounded deterministic schedule.
231    ///
232    /// This is the black-box qualification seam for future infrastructure
233    /// adapters. The simulator does not inspect or replace backend state.
234    ///
235    /// # Errors
236    ///
237    /// Rejects any zero hard bound before retaining the supplied backends.
238    pub fn wrap(
239        authority: A,
240        objects: O,
241        options: SimulationOptions,
242    ) -> Result<Self, SimulationError> {
243        if options.maximum_scheduled_faults == 0
244            || options.maximum_trace_entries == 0
245            || options.maximum_pending_objects == 0
246            || options.maximum_pending_object_bytes == 0
247        {
248            return Err(SimulationError::ZeroBound);
249        }
250        Ok(Self {
251            state: Arc::new(Mutex::new(State {
252                options,
253                occurrences: BTreeMap::new(),
254                faults: BTreeMap::new(),
255                trace: Vec::new(),
256                pending_objects: BTreeMap::new(),
257                pending_object_bytes: 0,
258            })),
259            authority: Arc::new(authority),
260            objects: Arc::new(objects),
261        })
262    }
263
264    /// Returns independently cloneable authority and object handles sharing one schedule.
265    #[must_use]
266    pub fn stores(&self) -> (SimulatedAuthorityStore<A>, SimulatedObjectStore<O>) {
267        (
268            SimulatedAuthorityStore {
269                state: self.state.clone(),
270                inner: self.authority.clone(),
271            },
272            SimulatedObjectStore {
273                state: self.state.clone(),
274                inner: self.objects.clone(),
275            },
276        )
277    }
278
279    /// Schedules one exact one-based interception point.
280    ///
281    /// # Errors
282    ///
283    /// Rejects zero occurrences, incompatible behavior, duplicate identities,
284    /// poisoned state, or a full bounded schedule.
285    pub fn schedule(&self, scheduled: ScheduledSimulationFault) -> Result<(), SimulationError> {
286        if scheduled.occurrence == 0 {
287            return Err(SimulationError::ZeroOccurrence);
288        }
289        if !scheduled.fault.compatible(scheduled.operation) {
290            return Err(SimulationError::IncompatibleFault);
291        }
292        let mut state = self.lock_state()?;
293        schedule_locked(&mut state, scheduled)
294    }
295
296    /// Atomically schedules the next occurrence of one operation family.
297    ///
298    /// This is useful for deterministic black-box runners after setup traffic.
299    /// Concurrent runners should retain the returned absolute occurrence in
300    /// their evidence rather than assume which request consumes it.
301    ///
302    /// # Errors
303    ///
304    /// Rejects incompatible behavior, occurrence exhaustion, poisoned state,
305    /// or a full bounded schedule.
306    pub fn schedule_next(
307        &self,
308        operation: SimulationOperation,
309        fault: SimulationFault,
310    ) -> Result<ScheduledSimulationFault, SimulationError> {
311        if !fault.compatible(operation) {
312            return Err(SimulationError::IncompatibleFault);
313        }
314        let mut state = self.lock_state()?;
315        let occurrence = state
316            .occurrences
317            .get(&operation)
318            .copied()
319            .unwrap_or(0)
320            .checked_add(1)
321            .ok_or(SimulationError::PastOccurrence)?;
322        let scheduled = ScheduledSimulationFault {
323            operation,
324            occurrence,
325            fault,
326        };
327        schedule_locked(&mut state, scheduled)?;
328        Ok(scheduled)
329    }
330
331    /// Returns the complete bounded trace collected so far.
332    ///
333    /// # Errors
334    ///
335    /// Returns a poisoned-state failure instead of silently discarding evidence.
336    pub fn trace(&self) -> Result<Vec<SimulationTrace>, SimulationError> {
337        Ok(self.lock_state()?.trace.clone())
338    }
339
340    fn lock_state(&self) -> Result<MutexGuard<'_, State>, SimulationError> {
341        self.state.lock().map_err(|_| SimulationError::Poisoned)
342    }
343}
344
345fn schedule_locked(
346    state: &mut State,
347    scheduled: ScheduledSimulationFault,
348) -> Result<(), SimulationError> {
349    if state
350        .faults
351        .contains_key(&(scheduled.operation, scheduled.occurrence))
352    {
353        return Err(SimulationError::DuplicateFault);
354    }
355    if state
356        .occurrences
357        .get(&scheduled.operation)
358        .is_some_and(|completed| *completed >= scheduled.occurrence)
359    {
360        return Err(SimulationError::PastOccurrence);
361    }
362    if state.faults.len() == state.options.maximum_scheduled_faults {
363        return Err(SimulationError::ScheduleFull);
364    }
365    state
366        .faults
367        .insert((scheduled.operation, scheduled.occurrence), scheduled.fault);
368    Ok(())
369}
370
371impl<A, O: AsyncObjectStore> Simulation<A, O> {
372    /// Publishes delayed immutable objects in canonical identity order.
373    ///
374    /// Only one pending entry is retained outside the simulator lock at a time,
375    /// so flushing does not allocate or copy the delayed object set. `Bytes`
376    /// cloning retains the same immutable allocation until backend admission.
377    ///
378    /// # Errors
379    ///
380    /// Returns exact immutable-store, cancellation, budget, or poisoned-state
381    /// failures. Successfully published entries are removed; the first failed
382    /// entry and every later entry remain pending for an idempotent retry.
383    pub async fn flush_object_visibility(
384        &self,
385        budget: WorkBudget,
386        cancellation: &CancellationToken,
387    ) -> ObjectResult<u64> {
388        let mut work = WorkCounters::default();
389        let mut published = 0_u64;
390        loop {
391            cancellation
392                .check()
393                .map_err(|_| ObjectFailure::new(ObjectStoreError::Cancelled, work))?;
394            let next = {
395                let state = self
396                    .state
397                    .lock()
398                    .map_err(|_| ObjectFailure::new(ObjectStoreError::Corrupt, work))?;
399                state
400                    .pending_objects
401                    .first_key_value()
402                    .map(|(object_id, bytes)| (*object_id, bytes.clone()))
403            };
404            let Some((object_id, bytes)) = next else {
405                return Ok(ObjectReceipt {
406                    value: published,
407                    work,
408                });
409            };
410            let remaining = work
411                .remaining(budget)
412                .map_err(|error| ObjectFailure::new(error.into(), work))?;
413            let receipt = self
414                .objects
415                .put(object_id, bytes, remaining, cancellation)
416                .await
417                .map_err(|failure| {
418                    let nested = *failure.work;
419                    match work.checked_add(nested) {
420                        Ok(combined) => ObjectFailure::new(failure.error, combined),
421                        Err(error) => ObjectFailure::new(error.into(), work),
422                    }
423                })?;
424            work = work
425                .checked_add(receipt.work)
426                .map_err(|error| ObjectFailure::new(error.into(), work))?;
427            let mut state = self
428                .state
429                .lock()
430                .map_err(|_| ObjectFailure::new(ObjectStoreError::Corrupt, work))?;
431            if let Some(removed) = state.pending_objects.remove(&object_id) {
432                state.pending_object_bytes = state
433                    .pending_object_bytes
434                    .checked_sub(u64::try_from(removed.len()).unwrap_or(u64::MAX))
435                    .ok_or_else(|| ObjectFailure::new(ObjectStoreError::Corrupt, work))?;
436            }
437            published = published.checked_add(1).ok_or_else(|| {
438                ObjectFailure::new(ObjectStoreError::Work(crate::WorkError::Overflow), work)
439            })?;
440        }
441    }
442}
443
444impl Default for Simulation {
445    fn default() -> Self {
446        let (authority, objects) = reference_backends();
447        Self {
448            state: Arc::new(Mutex::new(State {
449                options: SimulationOptions::default(),
450                occurrences: BTreeMap::new(),
451                faults: BTreeMap::new(),
452                trace: Vec::new(),
453                pending_objects: BTreeMap::new(),
454                pending_object_bytes: 0,
455            })),
456            authority: Arc::new(authority),
457            objects: Arc::new(objects),
458        }
459    }
460}
461
462/// Authority backend handle controlled by one [`Simulation`].
463#[derive(Clone)]
464pub struct SimulatedAuthorityStore<
465    A = crate::distributed::StreamAuthorityStore<acyclic_stream::MemoryStream>,
466> {
467    state: Arc<Mutex<State>>,
468    inner: Arc<A>,
469}
470
471/// Immutable-object backend handle controlled by one [`Simulation`].
472#[derive(Clone)]
473pub struct SimulatedObjectStore<
474    O = crate::distributed::ProviderObjectStore<acyclic_objects::MemoryObjects>,
475> {
476    state: Arc<Mutex<State>>,
477    inner: Arc<O>,
478}
479
480fn reference_backends() -> (ReferenceAuthority, ReferenceObjects) {
481    let streams = Arc::new(acyclic_stream::MemoryStream::default());
482    let (objects, bucket) = acyclic_objects::MemoryObjects::with_default_bucket();
483    (
484        crate::distributed::StreamAuthorityStore::new(streams),
485        crate::distributed::ProviderObjectStore::new(Arc::new(objects), bucket),
486    )
487}
488
489fn authority_fault(error: impl Into<String>) -> AuthorityFailure {
490    AuthorityFailure::before_work(AuthorityStoreError::Rejected(error.into()))
491}
492
493fn object_fault(error: impl Into<String>) -> ObjectFailure {
494    ObjectFailure::before_work(ObjectStoreError::Rejected(error.into()))
495}
496
497fn intercept_authority(
498    state: &Mutex<State>,
499    operation: SimulationOperation,
500) -> Result<Option<SimulationFault>, AuthorityFailure> {
501    let mut state = state.lock().map_err(|_| {
502        AuthorityFailure::before_work(AuthorityStoreError::Corrupt(
503            "simulation state is poisoned".to_owned(),
504        ))
505    })?;
506    state
507        .intercept(operation)
508        .map(|(_, fault)| fault)
509        .map_err(authority_fault)
510}
511
512fn intercept_object(
513    state: &Mutex<State>,
514    operation: SimulationOperation,
515) -> Result<Option<SimulationFault>, ObjectFailure> {
516    let mut state = state
517        .lock()
518        .map_err(|_| ObjectFailure::before_work(ObjectStoreError::Corrupt))?;
519    state
520        .intercept(operation)
521        .map(|(_, fault)| fault)
522        .map_err(object_fault)
523}
524
525fn reject_authority(fault: Option<SimulationFault>) -> Result<(), AuthorityFailure> {
526    match fault {
527        Some(SimulationFault::RejectBefore) => Err(authority_fault("scheduled rejection")),
528        Some(SimulationFault::PartitionBefore) => Err(authority_fault("scheduled partition")),
529        _ => Ok(()),
530    }
531}
532
533fn reject_object(fault: Option<SimulationFault>) -> Result<(), ObjectFailure> {
534    match fault {
535        Some(SimulationFault::RejectBefore) => Err(object_fault("scheduled rejection")),
536        Some(SimulationFault::PartitionBefore) => Err(object_fault("scheduled partition")),
537        _ => Ok(()),
538    }
539}
540
541impl<A: AsyncAuthorityStore> AsyncAuthorityStore for SimulatedAuthorityStore<A> {
542    async fn fork_generation_authority(
543        &self,
544        source: GenerationForkSource,
545        destination_authority: AuthorityId,
546        operation_id: OperationId,
547        budget: WorkBudget,
548        cancellation: &CancellationToken,
549    ) -> AuthorityResult<CreateAuthorityOutcome> {
550        cancellation
551            .check()
552            .map_err(|_| AuthorityFailure::before_work(AuthorityStoreError::Cancelled))?;
553        let fault = intercept_authority(&self.state, SimulationOperation::AuthorityCreate)?;
554        reject_authority(fault)?;
555        self.inner
556            .fork_generation_authority(
557                source,
558                destination_authority,
559                operation_id,
560                budget,
561                cancellation,
562            )
563            .await
564    }
565
566    async fn create_authority(
567        &self,
568        authority_id: AuthorityId,
569        genesis_epoch: Epoch,
570        budget: WorkBudget,
571        cancellation: &CancellationToken,
572    ) -> AuthorityResult<CreateAuthorityOutcome> {
573        cancellation
574            .check()
575            .map_err(|_| AuthorityFailure::before_work(AuthorityStoreError::Cancelled))?;
576        let fault = intercept_authority(&self.state, SimulationOperation::AuthorityCreate)?;
577        reject_authority(fault)?;
578        self.inner
579            .create_authority(authority_id, genesis_epoch, budget, cancellation)
580            .await
581    }
582
583    async fn head(
584        &self,
585        authority_id: AuthorityId,
586        budget: WorkBudget,
587        cancellation: &CancellationToken,
588    ) -> AuthorityResult<Head> {
589        cancellation
590            .check()
591            .map_err(|_| AuthorityFailure::before_work(AuthorityStoreError::Cancelled))?;
592        let fault = intercept_authority(&self.state, SimulationOperation::AuthorityHead)?;
593        reject_authority(fault)?;
594        if let Some(SimulationFault::StaleAuthorityHead(head)) = fault {
595            let work = WorkCounters {
596                authority_records_read: 1,
597                ..WorkCounters::default()
598            };
599            work.verify(budget)
600                .map_err(|error| AuthorityFailure::before_work(error.into()))?;
601            return Ok(crate::AuthorityReceipt { value: head, work });
602        }
603        self.inner.head(authority_id, budget, cancellation).await
604    }
605
606    async fn compare_and_append(
607        &self,
608        authority_id: AuthorityId,
609        epoch: Epoch,
610        expected: Head,
611        commit: ProposedCommit,
612        budget: WorkBudget,
613        cancellation: &CancellationToken,
614    ) -> AuthorityResult<AppendOutcome> {
615        cancellation
616            .check()
617            .map_err(|_| AuthorityFailure::before_work(AuthorityStoreError::Cancelled))?;
618        let fault = intercept_authority(&self.state, SimulationOperation::AuthorityAppend)?;
619        reject_authority(fault)?;
620        if matches!(fault, Some(SimulationFault::FenceBeforeAuthorityAppend)) {
621            let fenced = self
622                .inner
623                .fence(authority_id, expected, budget, cancellation)
624                .await?;
625            let remaining = fenced
626                .work
627                .remaining(budget)
628                .map_err(|error| AuthorityFailure::new(error.into(), fenced.work))?;
629            let appended = self
630                .inner
631                .compare_and_append(
632                    authority_id,
633                    epoch,
634                    expected,
635                    commit,
636                    remaining,
637                    cancellation,
638                )
639                .await
640                .map_err(|failure| {
641                    let nested = *failure.work;
642                    match fenced.work.checked_add(nested) {
643                        Ok(combined) => AuthorityFailure::new(failure.error, combined),
644                        Err(error) => AuthorityFailure::new(error.into(), fenced.work),
645                    }
646                })?;
647            let work = fenced
648                .work
649                .checked_add(appended.work)
650                .map_err(|error| AuthorityFailure::new(error.into(), fenced.work))?;
651            return Ok(crate::AuthorityReceipt {
652                value: appended.value,
653                work,
654            });
655        }
656        let receipt = self
657            .inner
658            .compare_and_append(authority_id, epoch, expected, commit, budget, cancellation)
659            .await?;
660        if matches!(fault, Some(SimulationFault::AmbiguousAuthorityAppend)) {
661            return Err(AuthorityFailure::new(
662                AuthorityStoreError::Indeterminate {
663                    operation: "compare-and-append",
664                    source: std::io::Error::other("scheduled acknowledgement loss"),
665                },
666                receipt.work,
667            ));
668        }
669        Ok(receipt)
670    }
671
672    async fn replay(
673        &self,
674        authority_id: AuthorityId,
675        after: Sequence,
676        limit: ReplayLimit,
677        budget: WorkBudget,
678        cancellation: &CancellationToken,
679    ) -> AuthorityResult<Vec<DurableCommit>> {
680        cancellation
681            .check()
682            .map_err(|_| AuthorityFailure::before_work(AuthorityStoreError::Cancelled))?;
683        let fault = intercept_authority(&self.state, SimulationOperation::AuthorityReplay)?;
684        reject_authority(fault)?;
685        let mut receipt = self
686            .inner
687            .replay(authority_id, after, limit, budget, cancellation)
688            .await?;
689        if matches!(fault, Some(SimulationFault::DuplicateAuthorityReplay))
690            && let Some(last) = receipt.value.last().cloned()
691        {
692            let copied = u64::try_from(last.payload.len()).unwrap_or(u64::MAX);
693            let extra = WorkCounters {
694                allocation_operations: 1,
695                bytes_copied: copied,
696                peak_allocation_bytes: copied,
697                items_returned: 1,
698                ..WorkCounters::default()
699            };
700            let work = receipt
701                .work
702                .checked_add(extra)
703                .map_err(|error| AuthorityFailure::new(error.into(), receipt.work))?;
704            work.verify(budget)
705                .map_err(|error| AuthorityFailure::new(error.into(), receipt.work))?;
706            receipt.value.push(last);
707            receipt.work = work;
708        }
709        Ok(receipt)
710    }
711
712    async fn fence(
713        &self,
714        authority_id: AuthorityId,
715        expected: Head,
716        budget: WorkBudget,
717        cancellation: &CancellationToken,
718    ) -> AuthorityResult<crate::storage::FenceOutcome> {
719        cancellation
720            .check()
721            .map_err(|_| AuthorityFailure::before_work(AuthorityStoreError::Cancelled))?;
722        let fault = intercept_authority(&self.state, SimulationOperation::AuthorityFence)?;
723        reject_authority(fault)?;
724        self.inner
725            .fence(authority_id, expected, budget, cancellation)
726            .await
727    }
728
729    async fn find_operation(
730        &self,
731        authority_id: AuthorityId,
732        operation_id: OperationId,
733        budget: WorkBudget,
734        cancellation: &CancellationToken,
735    ) -> AuthorityResult<Option<DurableCommit>> {
736        cancellation
737            .check()
738            .map_err(|_| AuthorityFailure::before_work(AuthorityStoreError::Cancelled))?;
739        let fault = intercept_authority(&self.state, SimulationOperation::AuthorityFindOperation)?;
740        reject_authority(fault)?;
741        self.inner
742            .find_operation(authority_id, operation_id, budget, cancellation)
743            .await
744    }
745}
746
747fn altered_read(
748    mut receipt: ObjectReceipt<ObjectRead>,
749    fault: Option<SimulationFault>,
750    budget: WorkBudget,
751) -> ObjectResult<ObjectRead> {
752    if !matches!(
753        fault,
754        Some(SimulationFault::CorruptObjectRead | SimulationFault::PartialObjectRead)
755    ) {
756        return Ok(receipt);
757    }
758    if receipt.value.bytes.is_empty() {
759        return Err(ObjectFailure::new(ObjectStoreError::Corrupt, receipt.work));
760    }
761    let original = u64::try_from(receipt.value.bytes.len()).unwrap_or(u64::MAX);
762    let retained = match fault {
763        Some(SimulationFault::PartialObjectRead) => original.saturating_sub(1),
764        _ => original,
765    };
766    let extra = WorkCounters {
767        allocation_operations: 1,
768        bytes_copied: retained,
769        peak_allocation_bytes: retained,
770        ..WorkCounters::default()
771    };
772    let work = receipt
773        .work
774        .checked_add(extra)
775        .map_err(|error| ObjectFailure::new(error.into(), receipt.work))?;
776    work.verify(budget)
777        .map_err(|error| ObjectFailure::new(error.into(), receipt.work))?;
778    let mut bytes = receipt.value.bytes.to_vec();
779    if matches!(fault, Some(SimulationFault::PartialObjectRead)) {
780        bytes.pop();
781    } else if let Some(first) = bytes.first_mut() {
782        *first ^= 0x80;
783    }
784    receipt.value = ObjectRead {
785        bytes: Bytes::from(bytes),
786        retention: ObjectReadRetention::Owned {
787            logical_bytes: retained,
788        },
789    };
790    receipt.work = work;
791    Ok(receipt)
792}
793
794impl<O: AsyncObjectStore> AsyncObjectStore for SimulatedObjectStore<O> {
795    async fn put(
796        &self,
797        object_id: ObjectId,
798        bytes: Bytes,
799        budget: WorkBudget,
800        cancellation: &CancellationToken,
801    ) -> ObjectResult<()> {
802        cancellation
803            .check()
804            .map_err(|_| ObjectFailure::before_work(ObjectStoreError::Cancelled))?;
805        let fault = intercept_object(&self.state, SimulationOperation::ObjectPut)?;
806        reject_object(fault)?;
807        if matches!(fault, Some(SimulationFault::DelayObjectVisibility)) {
808            let length = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
809            let work = WorkCounters {
810                object_probes: 1,
811                backend_write_operations: 1,
812                object_bytes_written: length,
813                bytes_hashed: length.saturating_add(crate::OBJECT_DIGEST_ENVELOPE_BYTES),
814                ..WorkCounters::default()
815            };
816            work.verify(budget)
817                .map_err(|error| ObjectFailure::before_work(error.into()))?;
818            if object_digest(object_id.kind, &bytes) != object_id.digest {
819                return Err(ObjectFailure::new(ObjectStoreError::DigestMismatch, work));
820            }
821            let mut state = self
822                .state
823                .lock()
824                .map_err(|_| ObjectFailure::new(ObjectStoreError::Corrupt, work))?;
825            if let Some(existing) = state.pending_objects.get(&object_id) {
826                if existing != &bytes {
827                    return Err(ObjectFailure::new(ObjectStoreError::Corrupt, work));
828                }
829                return Ok(ObjectReceipt { value: (), work });
830            }
831            let next_count = state.pending_objects.len().saturating_add(1);
832            let next_bytes = state
833                .pending_object_bytes
834                .checked_add(length)
835                .ok_or_else(|| {
836                    ObjectFailure::new(ObjectStoreError::Work(crate::WorkError::Overflow), work)
837                })?;
838            if next_count > state.options.maximum_pending_objects
839                || next_bytes > state.options.maximum_pending_object_bytes
840            {
841                return Err(ObjectFailure::new(
842                    ObjectStoreError::Rejected(
843                        "simulation delayed-object bound exhausted".to_owned(),
844                    ),
845                    work,
846                ));
847            }
848            state.pending_objects.insert(object_id, bytes);
849            state.pending_object_bytes = next_bytes;
850            return Ok(ObjectReceipt { value: (), work });
851        }
852        self.inner.put(object_id, bytes, budget, cancellation).await
853    }
854
855    async fn put_many(
856        &self,
857        writes: &[crate::storage::ObjectWrite],
858        budget: WorkBudget,
859        cancellation: &CancellationToken,
860    ) -> ObjectResult<()> {
861        cancellation
862            .check()
863            .map_err(|_| ObjectFailure::before_work(ObjectStoreError::Cancelled))?;
864        self.inner.put_many(writes, budget, cancellation).await
865    }
866
867    async fn read(
868        &self,
869        object_id: ObjectId,
870        maximum_bytes: u64,
871        budget: WorkBudget,
872        cancellation: &CancellationToken,
873    ) -> ObjectResult<ObjectRead> {
874        cancellation
875            .check()
876            .map_err(|_| ObjectFailure::before_work(ObjectStoreError::Cancelled))?;
877        let fault = intercept_object(&self.state, SimulationOperation::ObjectRead)?;
878        reject_object(fault)?;
879        altered_read(
880            self.inner
881                .read(object_id, maximum_bytes, budget, cancellation)
882                .await?,
883            fault,
884            budget,
885        )
886    }
887
888    async fn read_many(
889        &self,
890        requests: &[ObjectReadRequest],
891        budget: WorkBudget,
892        cancellation: &CancellationToken,
893    ) -> ObjectResult<Vec<ObjectRead>> {
894        cancellation
895            .check()
896            .map_err(|_| ObjectFailure::before_work(ObjectStoreError::Cancelled))?;
897        let fault = intercept_object(&self.state, SimulationOperation::ObjectReadMany)?;
898        reject_object(fault)?;
899        let mut receipt = self.inner.read_many(requests, budget, cancellation).await?;
900        if matches!(
901            fault,
902            Some(SimulationFault::CorruptObjectRead | SimulationFault::PartialObjectRead)
903        ) && let Some(first) = receipt.value.first_mut()
904        {
905            let altered = altered_read(
906                ObjectReceipt {
907                    value: first.clone(),
908                    work: WorkCounters::default(),
909                },
910                fault,
911                receipt
912                    .work
913                    .remaining(budget)
914                    .map_err(|error| ObjectFailure::new(error.into(), receipt.work))?,
915            )?;
916            receipt.work = receipt
917                .work
918                .checked_add(altered.work)
919                .map_err(|error| ObjectFailure::new(error.into(), receipt.work))?;
920            *first = altered.value;
921        }
922        Ok(receipt)
923    }
924
925    async fn contains(
926        &self,
927        object_id: ObjectId,
928        budget: WorkBudget,
929        cancellation: &CancellationToken,
930    ) -> ObjectResult<bool> {
931        cancellation
932            .check()
933            .map_err(|_| ObjectFailure::before_work(ObjectStoreError::Cancelled))?;
934        let fault = intercept_object(&self.state, SimulationOperation::ObjectContains)?;
935        reject_object(fault)?;
936        self.inner.contains(object_id, budget, cancellation).await
937    }
938}
939
940#[cfg(test)]
941#[path = "tests/simulation.rs"]
942mod tests;