Skip to main content

acyclic_fs/
storage.rs

1//! Pluggable authority and immutable object storage interfaces.
2
3use crate::foundation::{
4    AuthorityId, Digest, DurableCommit, Epoch, Head, OperationId, ProposedCommit, Sequence,
5};
6use crate::performance::{MeasuredResult, OperationFailure, WorkBudget, WorkCounters};
7use bytes::Bytes;
8use serde::{Deserialize, Serialize};
9use std::sync::Arc;
10use thiserror::Error;
11
12/// Canonical bytes hashed in addition to one object's body.
13pub const OBJECT_DIGEST_ENVELOPE_BYTES: u64 = b"acyclic-fs-object-v1\0".len() as u64 + 1 + 8;
14
15/// Bounds one replay request before storage allocates or returns bytes.
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub struct ReplayLimit {
18    /// Maximum records returned.
19    pub records: u32,
20    /// Maximum cumulative payload bytes returned.
21    pub payload_bytes: u64,
22}
23
24/// Idempotent authority creation result.
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub enum CreateAuthorityOutcome {
27    /// A new empty authority was durably created.
28    Created(Head),
29    /// The authority already existed and was not changed.
30    Existing(Head),
31}
32
33/// Successful or rejected compare-and-append result.
34#[derive(Clone, Debug, Eq, PartialEq)]
35pub enum AppendOutcome {
36    /// The proposed operation became durable at this commit.
37    Committed(DurableCommit),
38    /// The same operation and fingerprint was already durable.
39    AlreadyCommitted(DurableCommit),
40    /// The expected head was stale; no bytes were appended.
41    Conflict {
42        /// Linearizable current head.
43        actual: Head,
44    },
45    /// The caller epoch was stale; no bytes were appended.
46    Fenced {
47        /// Active authority epoch.
48        actual_epoch: Epoch,
49    },
50    /// The operation identity exists with a different fingerprint.
51    IdempotencyConflict {
52        /// Fingerprint already bound to the operation identity.
53        committed_fingerprint: Digest,
54    },
55}
56
57/// Optional authority-side admission condition for one publication.
58///
59/// Ordinary SDK callers use [`PublicationPermit::Unrestricted`]. Managed
60/// operation windows issue lease permits whose gate is checked in the same
61/// storage transaction as the generation append.
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub enum PublicationPermit {
64    /// No operation-window admission is required.
65    Unrestricted,
66    /// The exact durable tool lease must still be active.
67    Lease {
68        /// Authority derived from the admitted workspace identity.
69        authority_id: [u8; 16],
70        /// Workspace whose mount admitted the operation.
71        workspace_id: [u8; 16],
72        /// Unique durable lease identity.
73        lease_id: [u8; 16],
74        /// Provider-evaluated exclusive publication deadline.
75        expires_at_millis: u64,
76    },
77    /// The exact durable multi-authority publication reservation is active.
78    Reservation {
79        /// Stable logical publication identity.
80        operation_id: [u8; 16],
81        /// Tail produced when the authority gate was reserved.
82        gate_tail: u64,
83        /// Exact authority head authenticated when the gate was acquired.
84        expected: Head,
85    },
86}
87
88/// Durable exclusive authority reservation used by multi-root publication.
89#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
90pub struct PublicationReservation {
91    /// Authority protected by the reservation.
92    pub authority_id: AuthorityId,
93    /// Stable logical publication identity.
94    pub operation_id: OperationId,
95    /// Exact authority head validated while acquiring the gate.
96    pub expected: Head,
97    /// Gate tail that authenticates the reservation holder.
98    pub gate_tail: u64,
99}
100
101/// Result of atomically reserving an authority at an exact head.
102#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103pub enum ReservationOutcome {
104    /// This operation owns the exclusive gate.
105    Reserved(PublicationReservation),
106    /// The identical reservation was already acquired.
107    AlreadyReserved(PublicationReservation),
108    /// The authority head or gate changed before acquisition.
109    Conflict {
110        /// Linearizable head observed while acquisition failed.
111        actual: Head,
112    },
113}
114
115/// Complete atomic authority append request, including any admission permit.
116#[derive(Clone, Debug, Eq, PartialEq)]
117pub struct GuardedAppend {
118    /// Authority receiving the operation.
119    pub authority_id: AuthorityId,
120    /// Writer epoch expected by the caller.
121    pub epoch: Epoch,
122    /// Complete expected authority head.
123    pub expected: Head,
124    /// Canonical durable operation.
125    pub commit: ProposedCommit,
126    /// Optional operation-window admission requirement.
127    pub permit: PublicationPermit,
128}
129
130/// Successful or rejected compare-and-fence result.
131#[derive(Clone, Copy, Debug, Eq, PartialEq)]
132pub enum FenceOutcome {
133    /// The exact expected head was atomically advanced to a fresh writer epoch.
134    Advanced(Head),
135    /// The authority changed before ownership could be acquired.
136    Conflict {
137        /// Linearizable current head; no fence was advanced.
138        actual: Head,
139    },
140}
141
142/// Storage-only authority interface.
143///
144/// Implementations own ordering, fencing, idempotency, durable acknowledgement,
145/// and bounded replay. They do not interpret commit payloads.
146pub trait AuthorityStore: Send + Sync {
147    /// Creates one authority at the supplied non-zero genesis epoch.
148    ///
149    /// # Errors
150    ///
151    /// Returns a typed backend error if creation cannot be made durable.
152    fn create_authority(
153        &self,
154        authority_id: AuthorityId,
155        genesis_epoch: Epoch,
156        budget: WorkBudget,
157    ) -> AuthorityResult<CreateAuthorityOutcome>;
158
159    /// Returns the linearizable current head.
160    ///
161    /// # Errors
162    ///
163    /// Returns a typed backend error when the authority is missing, corrupt,
164    /// unavailable, or cannot be read durably.
165    fn head(&self, authority_id: AuthorityId, budget: WorkBudget) -> AuthorityResult<Head>;
166
167    /// Atomically checks epoch/head/idempotency and durably appends one operation.
168    ///
169    /// # Errors
170    ///
171    /// Returns a typed backend error when durable evaluation cannot complete.
172    /// Semantic conflicts are returned as [`AppendOutcome`] variants.
173    fn compare_and_append(
174        &self,
175        authority_id: AuthorityId,
176        epoch: Epoch,
177        expected: Head,
178        commit: ProposedCommit,
179        budget: WorkBudget,
180    ) -> AuthorityResult<AppendOutcome>;
181
182    /// Replays a contiguous bounded page after `sequence`.
183    ///
184    /// # Errors
185    ///
186    /// Returns a typed backend error for invalid limits, missing/corrupt
187    /// authority state, or storage failure.
188    fn replay(
189        &self,
190        authority_id: AuthorityId,
191        after: Sequence,
192        limit: ReplayLimit,
193        budget: WorkBudget,
194    ) -> AuthorityResult<Vec<DurableCommit>>;
195
196    /// Atomically compares the complete head, advances its epoch, and
197    /// permanently fences prior writers.
198    ///
199    /// # Errors
200    ///
201    /// A stale expected head is a semantic [`FenceOutcome::Conflict`], while a
202    /// backend failure means the fence could not be durably resolved.
203    fn fence(
204        &self,
205        authority_id: AuthorityId,
206        expected: Head,
207        budget: WorkBudget,
208    ) -> AuthorityResult<FenceOutcome>;
209
210    /// Looks up a durable operation for exact retry resolution.
211    ///
212    /// # Errors
213    ///
214    /// Returns a typed backend error when the authority cannot be read safely.
215    fn find_operation(
216        &self,
217        authority_id: AuthorityId,
218        operation_id: OperationId,
219        budget: WorkBudget,
220    ) -> AuthorityResult<Option<DurableCommit>>;
221}
222
223impl<T: AuthorityStore + ?Sized> AuthorityStore for Arc<T> {
224    fn create_authority(
225        &self,
226        authority_id: AuthorityId,
227        genesis_epoch: Epoch,
228        budget: WorkBudget,
229    ) -> AuthorityResult<CreateAuthorityOutcome> {
230        (**self).create_authority(authority_id, genesis_epoch, budget)
231    }
232
233    fn head(&self, authority_id: AuthorityId, budget: WorkBudget) -> AuthorityResult<Head> {
234        (**self).head(authority_id, budget)
235    }
236
237    fn compare_and_append(
238        &self,
239        authority_id: AuthorityId,
240        epoch: Epoch,
241        expected: Head,
242        commit: ProposedCommit,
243        budget: WorkBudget,
244    ) -> AuthorityResult<AppendOutcome> {
245        (**self).compare_and_append(authority_id, epoch, expected, commit, budget)
246    }
247
248    fn replay(
249        &self,
250        authority_id: AuthorityId,
251        after: Sequence,
252        limit: ReplayLimit,
253        budget: WorkBudget,
254    ) -> AuthorityResult<Vec<DurableCommit>> {
255        (**self).replay(authority_id, after, limit, budget)
256    }
257
258    fn fence(
259        &self,
260        authority_id: AuthorityId,
261        expected: Head,
262        budget: WorkBudget,
263    ) -> AuthorityResult<FenceOutcome> {
264        (**self).fence(authority_id, expected, budget)
265    }
266
267    fn find_operation(
268        &self,
269        authority_id: AuthorityId,
270        operation_id: OperationId,
271        budget: WorkBudget,
272    ) -> AuthorityResult<Option<DurableCommit>> {
273        (**self).find_operation(authority_id, operation_id, budget)
274    }
275}
276
277/// One successful authority operation and its exact backend work.
278#[derive(Clone, Debug, Eq, PartialEq)]
279pub struct AuthorityReceipt<T> {
280    /// Operation value.
281    pub value: T,
282    /// Exact backend work.
283    pub work: WorkCounters,
284}
285
286/// One failed authority operation retaining exact spent work.
287pub type AuthorityFailure = OperationFailure<AuthorityStoreError>;
288
289/// Receipt-bearing authority result.
290pub type AuthorityResult<T> = MeasuredResult<AuthorityReceipt<T>, AuthorityStoreError>;
291
292/// Typed authority storage failures.
293#[derive(Debug, Error)]
294pub enum AuthorityStoreError {
295    /// Operation was cancelled before durable authority work began.
296    #[error("authority operation was cancelled")]
297    Cancelled,
298    /// The authority does not exist.
299    #[error("authority does not exist")]
300    Missing,
301    /// A configured replay bound is invalid.
302    #[error("replay limit must have non-zero record and payload bounds")]
303    InvalidReplayLimit,
304    /// One authority record cannot fit into the requested replay page.
305    #[error("next authority record has {observed} payload bytes; replay maximum is {maximum}")]
306    ReplayRecordTooLarge {
307        /// Next record payload bytes.
308        observed: u64,
309        /// Requested page payload bound.
310        maximum: u64,
311    },
312    /// One submitted authority payload exceeds the backend admission bound.
313    #[error("authority payload has {observed} bytes; maximum is {maximum}")]
314    PayloadTooLarge {
315        /// Submitted payload bytes.
316        observed: u64,
317        /// Configured maximum.
318        maximum: u64,
319    },
320    /// Epoch arithmetic cannot represent another fence.
321    #[error("authority epoch exhausted")]
322    EpochExhausted,
323    /// Sequence arithmetic cannot represent another authority fact.
324    #[error("authority sequence exhausted")]
325    SequenceExhausted,
326    /// Persistent state is corrupt and cannot be safely interpreted.
327    #[error("authority storage is corrupt: {0}")]
328    Corrupt(String),
329    /// Persistent I/O failed.
330    #[error("authority storage I/O failed: {0}")]
331    Io(#[from] std::io::Error),
332    /// Durable state may have changed, so the caller must reopen and resolve by identity.
333    #[error("authority {operation} outcome is indeterminate: {source}")]
334    Indeterminate {
335        /// Durable operation whose acknowledgement was lost.
336        operation: &'static str,
337        /// Underlying storage failure after mutation may have begun.
338        #[source]
339        source: std::io::Error,
340    },
341    /// Backend rejected an operation for a stable implementation-specific reason.
342    #[error("authority backend rejected operation: {0}")]
343    Rejected(String),
344    /// Backend work could not be admitted within the caller's hard budget.
345    #[error(transparent)]
346    Work(#[from] crate::performance::WorkError),
347}
348
349/// Immutable object class. Hashes are scoped by both kind and canonical bytes.
350#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
351pub enum ObjectKind {
352    /// Authenticated blob-index page.
353    Blob,
354    /// Fixed-size immutable content chunk addressed by its complete bytes.
355    BlobChunk,
356    /// Authenticated namespace page.
357    TreePage,
358    /// Authenticated file-extent page.
359    ExtentPage,
360    /// Authenticated file-table page.
361    FileTablePage,
362    /// Composite immutable generation root.
363    GenerationRoot,
364    /// Canonical metadata object.
365    Metadata,
366    /// Authenticated named extended-attribute/stream page.
367    AttributePage,
368}
369
370impl ObjectKind {
371    /// Returns the stable canonical object-class tag.
372    #[must_use]
373    pub const fn canonical_tag(self) -> u8 {
374        match self {
375            Self::Blob => 1,
376            Self::BlobChunk => 2,
377            Self::TreePage => 3,
378            Self::ExtentPage => 4,
379            Self::FileTablePage => 5,
380            Self::GenerationRoot => 6,
381            Self::Metadata => 7,
382            Self::AttributePage => 8,
383        }
384    }
385
386    /// Parses one stable canonical object-class tag.
387    ///
388    /// # Errors
389    ///
390    /// Rejects unknown tags instead of guessing a future object class.
391    pub const fn from_canonical_tag(tag: u8) -> Result<Self, ObjectKindError> {
392        match tag {
393            1 => Ok(Self::Blob),
394            2 => Ok(Self::BlobChunk),
395            3 => Ok(Self::TreePage),
396            4 => Ok(Self::ExtentPage),
397            5 => Ok(Self::FileTablePage),
398            6 => Ok(Self::GenerationRoot),
399            7 => Ok(Self::Metadata),
400            8 => Ok(Self::AttributePage),
401            value => Err(ObjectKindError::Unknown(value)),
402        }
403    }
404}
405
406/// Canonical object-kind parsing failures.
407#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
408pub enum ObjectKindError {
409    /// A newer or corrupt object used an unknown kind tag.
410    #[error("unknown object kind tag {0}")]
411    Unknown(u8),
412}
413
414/// Typed object identity.
415#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
416pub struct ObjectId {
417    /// Object class.
418    pub kind: ObjectKind,
419    /// Digest over object class and canonical bytes.
420    pub digest: Digest,
421}
422
423/// Computes the canonical digest for one typed immutable object.
424#[must_use]
425pub fn object_digest(kind: ObjectKind, bytes: &[u8]) -> Digest {
426    let mut hasher = blake3::Hasher::new();
427    hasher.update(b"acyclic-fs-object-v1\0");
428    hasher.update(&[kind.canonical_tag()]);
429    hasher.update(&u64::try_from(bytes.len()).unwrap_or(u64::MAX).to_le_bytes());
430    hasher.update(bytes);
431    Digest::from_bytes(*hasher.finalize().as_bytes())
432}
433
434/// Exact bounded logical byte range.
435#[derive(Clone, Copy, Debug, Eq, PartialEq)]
436pub struct ByteRange {
437    /// Inclusive start offset.
438    pub offset: u64,
439    /// Exact requested byte count.
440    pub length: u64,
441}
442
443/// One successful immutable-object operation and its exact physical work.
444#[derive(Clone, Debug, Eq, PartialEq)]
445pub struct ObjectReceipt<T> {
446    /// Operation value.
447    pub value: T,
448    /// Exact backend work, including zero-copy versus copied-byte behavior.
449    pub work: WorkCounters,
450}
451
452/// How the bytes returned by one immutable-object read remain resident.
453#[derive(Clone, Copy, Debug, Eq, PartialEq)]
454pub enum ObjectReadRetention {
455    /// The returned view shares backend-owned immutable storage and adds no
456    /// operation-owned byte buffer.
457    Shared,
458    /// The backend allocated this exact logical byte capacity and transfers
459    /// ownership to the caller until the value is dropped.
460    Owned {
461        /// Exact requested capacity retained by the returned value.
462        logical_bytes: u64,
463    },
464}
465
466/// Immutable bytes plus explicit retention evidence for zero-copy and peak
467/// allocation accounting.
468#[derive(Clone, Debug, Eq, PartialEq)]
469pub struct ObjectRead {
470    /// Complete canonical object bytes.
471    pub bytes: Bytes,
472    /// Whether those bytes are a shared view or an owned returned buffer.
473    pub retention: ObjectReadRetention,
474}
475
476/// One complete immutable object requested as part of an ordered batch.
477#[derive(Clone, Copy, Debug, Eq, PartialEq)]
478pub struct ObjectReadRequest {
479    /// Exact typed immutable identity.
480    pub object_id: ObjectId,
481    /// Maximum canonical bytes admitted for this object.
482    pub maximum_bytes: u64,
483}
484
485/// One canonical immutable object submitted as part of an ordered write batch.
486#[derive(Clone, Debug, Eq, PartialEq)]
487pub struct ObjectWrite {
488    /// Exact typed immutable identity.
489    pub object_id: ObjectId,
490    /// Canonical bytes authenticated by `object_id`.
491    pub bytes: Bytes,
492}
493
494impl std::ops::Deref for ObjectRead {
495    type Target = [u8];
496
497    fn deref(&self) -> &Self::Target {
498        &self.bytes
499    }
500}
501
502/// One failed immutable-object operation and all work completed before failure.
503pub type ObjectFailure = OperationFailure<ObjectStoreError>;
504
505/// Receipt-bearing immutable-object result. Failure never erases spent work.
506pub type ObjectResult<T> = MeasuredResult<ObjectReceipt<T>, ObjectStoreError>;
507
508/// Immutable digest-addressed object storage interface.
509///
510/// Objects are deliberately read whole. Pages and blob chunks have hard size
511/// limits; logical range reads are built over authenticated chunk indexes. This
512/// prevents a backend from hiding a whole-object scan behind a tiny range read.
513pub trait ObjectStore: Send + Sync {
514    /// Admits canonical bytes under their verified identity.
515    ///
516    /// # Errors
517    ///
518    /// Returns a typed error for digest mismatch or storage failure.
519    fn put(&self, object_id: ObjectId, bytes: Bytes, budget: WorkBudget) -> ObjectResult<()>;
520
521    /// Admits an ordered bounded group through one backend batch operation.
522    ///
523    /// # Errors
524    ///
525    /// Returns the first typed failure and all work completed before it.
526    fn put_many(&self, writes: &[ObjectWrite], budget: WorkBudget) -> ObjectResult<()>;
527
528    /// Reads exactly the requested range or returns a typed error.
529    ///
530    /// # Errors
531    ///
532    /// Returns a typed error when the object is absent, corrupt, unavailable,
533    /// or the range is invalid.
534    fn read(
535        &self,
536        object_id: ObjectId,
537        maximum_bytes: u64,
538        budget: WorkBudget,
539    ) -> ObjectResult<ObjectRead>;
540
541    /// Reads an ordered batch of complete bounded objects.
542    ///
543    /// The result position always matches the request position. Implementations
544    /// should coalesce physical transactions or I/O submission where their
545    /// backend supports it. The default preserves exact semantics and work
546    /// accounting without claiming fewer backend operations.
547    ///
548    /// # Errors
549    ///
550    /// Returns the first typed object failure and all work completed before it.
551    fn read_many(
552        &self,
553        requests: &[ObjectReadRequest],
554        budget: WorkBudget,
555    ) -> ObjectResult<Vec<ObjectRead>>;
556
557    /// Returns whether the exact typed object is present and verified.
558    ///
559    /// # Errors
560    ///
561    /// Returns a typed error when presence cannot be checked safely.
562    fn contains(&self, object_id: ObjectId, budget: WorkBudget) -> ObjectResult<bool>;
563}
564
565impl<T: ObjectStore + ?Sized> ObjectStore for Arc<T> {
566    fn put(&self, object_id: ObjectId, bytes: Bytes, budget: WorkBudget) -> ObjectResult<()> {
567        (**self).put(object_id, bytes, budget)
568    }
569
570    fn put_many(&self, writes: &[ObjectWrite], budget: WorkBudget) -> ObjectResult<()> {
571        (**self).put_many(writes, budget)
572    }
573
574    fn read(
575        &self,
576        object_id: ObjectId,
577        maximum_bytes: u64,
578        budget: WorkBudget,
579    ) -> ObjectResult<ObjectRead> {
580        (**self).read(object_id, maximum_bytes, budget)
581    }
582
583    fn read_many(
584        &self,
585        requests: &[ObjectReadRequest],
586        budget: WorkBudget,
587    ) -> ObjectResult<Vec<ObjectRead>> {
588        (**self).read_many(requests, budget)
589    }
590
591    fn contains(&self, object_id: ObjectId, budget: WorkBudget) -> ObjectResult<bool> {
592        (**self).contains(object_id, budget)
593    }
594}
595
596/// Typed immutable-object failures.
597#[derive(Debug, Error)]
598pub enum ObjectStoreError {
599    /// Operation was cancelled before immutable-object work began.
600    #[error("object operation was cancelled")]
601    Cancelled,
602    /// Exact typed object is absent.
603    #[error("object is missing")]
604    Missing,
605    /// Supplied bytes do not match the typed object digest.
606    #[error("object digest does not match canonical bytes")]
607    DigestMismatch,
608    /// Object exceeds the backend's declared bound.
609    #[error("object is {observed} bytes; maximum is {maximum}")]
610    TooLarge {
611        /// Observed canonical byte length.
612        observed: u64,
613        /// Configured maximum canonical byte length.
614        maximum: u64,
615    },
616    /// Requested range is invalid or outside the object.
617    #[error("object range is outside the admitted object")]
618    InvalidRange,
619    /// Stored bytes fail verification.
620    #[error("stored object is corrupt")]
621    Corrupt,
622    /// Corrupt bytes could not be isolated from the live object namespace.
623    #[error("corrupt object quarantine failed: {0}")]
624    QuarantineFailed(#[source] std::io::Error),
625    /// Storage I/O failed.
626    #[error("object storage I/O failed: {0}")]
627    Io(#[from] std::io::Error),
628    /// Backend rejected an operation for a stable implementation-specific reason.
629    #[error("object backend rejected operation: {0}")]
630    Rejected(String),
631    /// Backend work could not be admitted without exceeding the caller budget.
632    #[error(transparent)]
633    Work(#[from] crate::performance::WorkError),
634}
635
636#[cfg(test)]
637#[path = "tests/storage.rs"]
638mod tests;