Skip to main content

safemesh_crdt/
local.rs

1// Copyright (C) 2026 Ben Cassie
2// SPDX-License-Identifier: Apache-2.0
3//! Linux/local-filesystem packet-A adapter. All writers for a replica set must
4//! use the same directory and fixed configuration. Keep fence files in place.
5//! LocalReplica acknowledges in memory; DurableReplica commits before acknowledgement
6//! and offers checked ordinary restart of an existing committed store.
7use crate::{
8    ownership::*, Admission, Crdt, EventLog, GCounter, GCounterDelta, OrSet, OrSetDelta, Record,
9    RecordId, WireDecode, WireEncode, WireError, WireSchema,
10};
11use alloc::{format, string::String, vec::Vec};
12use std::{
13    fs::{self, File, OpenOptions, TryLockError},
14    io::{self, Read, Seek, SeekFrom, Write},
15    path::{Path, PathBuf},
16};
17
18/// Errors from local writer and durable store operations.
19///
20/// Downstream callers must allow for future variants:
21///
22/// ```compile_fail,E0004
23/// use safemesh_crdt::local::LocalError;
24/// fn classify(error: LocalError) -> &'static str {
25///     match error {
26///         LocalError::Refused => "refused",
27///         LocalError::Exhausted => "exhausted",
28///         LocalError::PeerWriterAhead => "writer ahead",
29///         LocalError::StoreExists => "store exists",
30///         LocalError::RecoveryRequired => "recovery",
31///         LocalError::UnsupportedFormat { .. } => "unsupported format",
32///         LocalError::Configuration => "configuration",
33///         LocalError::CounterWidth(_) => "counter width",
34///         LocalError::InvalidRecord(_) => "record",
35///         LocalError::History(_) => "history",
36///         LocalError::InvalidHistory => "invalid history",
37///         LocalError::Io(_) => "io",
38///         LocalError::AncestorSync { .. } => "ancestor sync",
39///         LocalError::RecordLimitExceeded { .. } => "record limit",
40///     }
41/// }
42/// ```
43#[derive(Debug)]
44#[non_exhaustive]
45pub enum LocalError {
46    Refused,
47    Exhausted,
48    /// A peer supplied an own-writer record above the committed high-water.
49    PeerWriterAhead,
50    /// A fresh writer store already exists at this root for this writer.
51    StoreExists,
52    /// Store metadata is damaged or disagrees with its durable history.
53    RecoveryRequired,
54    /// A stored wrapper has a version newer than this reader understands.
55    UnsupportedFormat {
56        file: &'static str,
57        version: u8,
58    },
59    Configuration,
60    /// The requested counter width exceeds the core constructor domain.
61    CounterWidth(crate::CoordinateError),
62    InvalidRecord(WireError),
63    History(WireError),
64    InvalidHistory,
65    Io(io::Error),
66    /// First creation could not open or sync an ancestor; no fence is written.
67    AncestorSync {
68        path: PathBuf,
69        source: io::Error,
70    },
71    /// A `_with_limits` restart found a committed history that declares more
72    /// records than [`DecodeLimits::max_records`](crate::DecodeLimits). Checked
73    /// from the frame header before any record is read; the store is unchanged.
74    RecordLimitExceeded {
75        max_records: usize,
76        default_limit: bool,
77    },
78}
79
80impl core::fmt::Display for LocalError {
81    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
82        match self {
83            Self::Refused => {
84                f.write_str("local writer operation refused by ownership or lease checks")
85            }
86            Self::Exhausted => {
87                f.write_str("local writer sequence, generation, or token allocation exhausted")
88            }
89            Self::PeerWriterAhead => f.write_str("a peer returned a record for this writer above its durable high-water; local writes on this open replica are stopped; reopening clears this stop and an old-identity restore is detected only when a peer returns such a record; open a restored store with a new writer identity"),
90            Self::StoreExists => f.write_str("local store already exists for this writer; restart it, or create with a new root or writer ID"),
91            Self::RecoveryRequired => f.write_str("local store requires recovery: its writer fence, journal header, rollback counter or migration files are damaged or do not agree"),
92            Self::UnsupportedFormat { file, version } => write!(f, "unsupported {file} version {version}; highest version this build reads is 1; use a newer SafeMesh"),
93            Self::CounterWidth(error) => error.fmt(f),
94            Self::Configuration => f.write_str("invalid or mismatched local writer configuration"),
95            Self::InvalidRecord(error) => error.fmt(f),
96            Self::History(error) => write!(f, "local history wire validation failed: {error}"),
97            Self::InvalidHistory => {
98                f.write_str("local history failed replay or sequence validation")
99            }
100            Self::AncestorSync { path, source } => write!(f, "AncestorSync: cannot sync store ancestor {}: {source}", path.display()),
101            Self::Io(error) => write!(f, "local store I/O failed: {error}"),
102            Self::RecordLimitExceeded { max_records, default_limit } => {
103                write!(f, "local history exceeds restart record budget: RecordLimitExceeded: {max_records}")?;
104                if *default_limit { f.write_str("; pass DecodeLimits::UNLIMITED to reopen") } else { Ok(()) }
105            }
106        }
107    }
108}
109
110impl core::error::Error for LocalError {
111    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
112        match self {
113            Self::InvalidRecord(error) | Self::History(error) => Some(error),
114            Self::Io(error) | Self::AncestorSync { source: error, .. } => Some(error),
115            Self::CounterWidth(error) => Some(error),
116            _ => None,
117        }
118    }
119}
120impl From<io::Error> for LocalError {
121    fn from(e: io::Error) -> Self {
122        Self::Io(e)
123    }
124}
125
126const FENCE_MAGIC: &[u8; 8] = b"SMFENCE1";
127const TRANSACTION_MAGIC: &[u8; 8] = b"SMTRANS1";
128
129fn fence_fields(bytes: &[u8]) -> Result<usize, LocalError> {
130    if bytes.len() == 24 {
131        return Ok(0);
132    }
133    if bytes.len() >= 9 && &bytes[..8] == FENCE_MAGIC {
134        if bytes[8] > 1 {
135            return Err(LocalError::UnsupportedFormat {
136                file: "fence",
137                version: bytes[8],
138            });
139        }
140        if bytes[8] == 1 && bytes.len() == 33 {
141            return Ok(9);
142        }
143    }
144    Err(LocalError::RecoveryRequired)
145}
146
147// A valid legacy transaction has an EventLog tag at byte 24. This
148// discriminator takes precedence even when its writers word equals the magic.
149fn transaction_fields(bytes: &[u8]) -> Result<usize, LocalError> {
150    if bytes.len() >= 25 && matches!(bytes[24], 0x02 | 0x03) {
151        return Ok(0);
152    }
153    if bytes.len() >= 9 && &bytes[..8] == TRANSACTION_MAGIC {
154        if bytes[8] > 1 {
155            return Err(LocalError::UnsupportedFormat {
156                file: "transaction",
157                version: bytes[8],
158            });
159        }
160        if bytes[8] == 1 && bytes.len() >= 34 {
161            return Ok(9);
162        }
163    }
164    if bytes.len() < 24 {
165        return Err(LocalError::RecoveryRequired);
166    }
167    Ok(0)
168}
169
170/// A generation captured by a caller. Renewal invalidates all older tickets.
171#[derive(Clone, Copy, Debug)]
172pub struct WriteTicket(u64);
173
174/// Owns the OS lock for its entire lifetime. No Clone or mutable state/log
175/// access is exposed. Contention yields a read-only instance whose writes fail.
176pub struct LocalReplica<C: Crdt> {
177    config: WriterConfig,
178    fence: File,
179    held: bool,
180    generation: u64,
181    state: C,
182    log: EventLog<C::Delta>,
183    last_sequence: u64,
184    peer_writer_ahead: bool,
185}
186
187// A private tentative insertion. Only version metadata (bounded by configured
188// writers), not historical payloads or the identity index, is copied. Restore on
189// errors and unwinding; persistence still sees the exact candidate EventLog.
190struct PendingInsertion<'a, D> {
191    log: &'a mut EventLog<D>,
192    id: RecordId,
193    len: usize,
194    replica_count: Option<usize>,
195    shape_bound: bool,
196    version: Option<crate::VersionVector>,
197}
198impl<'a, D> PendingInsertion<'a, D> {
199    fn new(log: &'a mut EventLog<D>, id: RecordId) -> Self {
200        Self {
201            id,
202            len: log.records.len(),
203            replica_count: log.replica_count,
204            shape_bound: log.shape_bound,
205            version: Some(log.version.clone()),
206            log,
207        }
208    }
209    fn accept(mut self) {
210        self.version = None;
211    }
212}
213impl<D> Drop for PendingInsertion<'_, D> {
214    fn drop(&mut self) {
215        if let Some(version) = self.version.take() {
216            self.log.seen.remove(&self.id);
217            self.log.records.truncate(self.len);
218            self.log.replica_count = self.replica_count;
219            self.log.shape_bound = self.shape_bound;
220            self.log.version = version;
221        }
222    }
223}
224
225impl<C: Crdt> Drop for LocalReplica<C> {
226    fn drop(&mut self) {
227        // Explicitly release our open-file-description lock before close. A
228        // concurrent fork/exec can briefly inherit an FD even with CLOEXEC.
229        let _ = self.fence.unlock();
230    }
231}
232
233// Without a fence, existing directories may be leftovers from interrupted mkdir.
234// No persistent provenance identifies a safe stopping ancestor, so sync the
235// resolved and traversed parent chains through / before creating the fence.
236fn prepare_durable_root(root: &Path, config: WriterConfig) -> Result<(), LocalError> {
237    if root
238        .join(format!("writer-{}.fence", config.writer))
239        .try_exists()?
240    {
241        return Ok(());
242    }
243    create_durable_root(root)
244}
245fn create_durable_root(root: &Path) -> Result<(), LocalError> {
246    create_durable_root_with(root, |parent| File::open(parent)?.sync_all())
247}
248
249fn create_durable_root_with(
250    root: &Path,
251    sync_parent: impl FnMut(&Path) -> io::Result<()>,
252) -> Result<(), LocalError> {
253    create_durable_root_using(root, |directory| fs::create_dir(directory), sync_parent)
254}
255
256fn create_durable_root_using(
257    root: &Path,
258    mut create: impl FnMut(&Path) -> io::Result<()>,
259    mut sync_parent: impl FnMut(&Path) -> io::Result<()>,
260) -> Result<(), LocalError> {
261    let root = std::path::absolute(root)?;
262    let mut missing = Vec::new();
263    let mut ancestor = root.as_path();
264    loop {
265        match fs::metadata(ancestor) {
266            Ok(metadata) if metadata.is_dir() => break,
267            Ok(_) => return Err(io::Error::from(io::ErrorKind::NotADirectory).into()),
268            Err(error) if error.kind() == io::ErrorKind::NotFound => {
269                missing.push(ancestor.to_path_buf());
270                ancestor = ancestor
271                    .parent()
272                    .ok_or_else(|| io::Error::other("no ancestor"))?;
273            }
274            Err(error) => return Err(error.into()),
275        }
276    }
277    for directory in missing.iter().rev() {
278        match create(directory) {
279            Ok(()) => {}
280            // A concurrent creator may have installed this directory after the walk.
281            Err(error)
282                if error.kind() == io::ErrorKind::AlreadyExists
283                    && fs::metadata(directory)?.is_dir() => {}
284            Err(error) => return Err(error.into()),
285        }
286    }
287    let resolved = root.canonicalize()?;
288    let mut parents: Vec<_> = resolved
289        .ancestors()
290        .skip(1)
291        .map(Path::to_path_buf)
292        .collect();
293    // Keep entries needed to traverse symlinks or cancelled `..` components
294    // durable too, even if an interrupted earlier call created them.
295    for parent in root.ancestors().skip(1) {
296        let parent = parent
297            .canonicalize()
298            .map_err(|source| LocalError::AncestorSync {
299                path: parent.to_path_buf(),
300                source,
301            })?;
302        if !parents.contains(&parent) {
303            parents.push(parent);
304        }
305    }
306    parents.sort_by_key(|parent| core::cmp::Reverse(parent.components().count()));
307    for parent in parents {
308        sync_parent(&parent).map_err(|source| LocalError::AncestorSync {
309            path: parent,
310            source,
311        })?;
312    }
313    Ok(())
314}
315
316impl<C: Crdt> LocalReplica<C>
317where
318    C::Delta: OwnedDelta + Clone + PartialEq,
319{
320    fn fresh(root: &Path, config: WriterConfig, state: C) -> Result<Self, LocalError> {
321        config.validate().map_err(|_| LocalError::Configuration)?;
322        prepare_durable_root(root, config)?;
323        let mut fence = OpenOptions::new()
324            .read(true)
325            .write(true)
326            .create(true)
327            .truncate(false)
328            .open(root.join(format!("writer-{}.fence", config.writer)))?;
329        let held = match fence.try_lock() {
330            Ok(()) => true,
331            Err(TryLockError::WouldBlock) => false,
332            Err(TryLockError::Error(e)) => return Err(e.into()),
333        };
334        let mut bytes = Vec::new();
335        fence.read_to_end(&mut bytes)?;
336        let generation = if bytes.is_empty() && held {
337            // Reserve the store even before the first edit. Resetting an old
338            // allocation is never an implicit recovery policy.
339            fence.write_all(&config.writers.to_le_bytes())?;
340            fence.write_all(&config.writer.to_le_bytes())?;
341            fence.write_all(&1u64.to_le_bytes())?;
342            fence.sync_all()?;
343            1
344        } else {
345            let at = fence_fields(&bytes)?;
346            let word = |i| u64::from_le_bytes(bytes[at + i..at + i + 8].try_into().unwrap());
347            if word(0) != config.writers || word(8) != config.writer {
348                return Err(LocalError::Configuration);
349            }
350            if held {
351                return Err(LocalError::StoreExists);
352            }
353            word(16)
354        };
355        Ok(Self {
356            config,
357            fence,
358            held,
359            generation,
360            log: EventLog::for_crdt(&state),
361            state,
362            last_sequence: 0,
363            peer_writer_ahead: false,
364        })
365    }
366
367    pub fn state(&self) -> &C {
368        &self.state
369    }
370    pub fn log(&self) -> &EventLog<C::Delta> {
371        &self.log
372    }
373    pub fn ticket(&self) -> WriteTicket {
374        WriteTicket(self.generation)
375    }
376    /// Ordinary allocation metadata, independent of the lock generation.
377    pub fn allocation_bytes(&self) -> Vec<u8> {
378        [
379            self.config.writers.to_le_bytes(),
380            self.config.writer.to_le_bytes(),
381            self.last_sequence.to_le_bytes(),
382        ]
383        .concat()
384    }
385    fn context(&self, ticket: WriteTicket, local: bool) -> WriteContext {
386        WriteContext {
387            config: self.config,
388            held: self.held,
389            generation: ticket.0,
390            current_generation: self.generation,
391            local,
392        }
393    }
394
395    /// Revoke outstanding tickets while retaining the same exclusive OS lock.
396    /// I/O uncertainty disables writes. This does not recover a replica.
397    pub fn renew(&mut self, ticket: WriteTicket) -> Result<WriteTicket, LocalError> {
398        if refuses(
399            self.context(ticket, true),
400            RecordId {
401                replica: self.config.writer,
402                sequence: 1,
403            },
404            OwnedPayload::Remove,
405        ) {
406            return Err(LocalError::Refused);
407        }
408        let next = next_sequence(self.generation).ok_or(LocalError::Exhausted)?;
409        self.held = false;
410        let mut bytes = Vec::new();
411        self.fence.seek(SeekFrom::Start(0))?;
412        self.fence.read_to_end(&mut bytes)?;
413        let at = fence_fields(&bytes)?;
414        self.fence.seek(SeekFrom::Start((at + 16) as u64))?;
415        self.fence.write_all(&next.to_le_bytes())?;
416        self.fence.sync_all()?;
417        self.generation = next;
418        self.held = true;
419        Ok(self.ticket())
420    }
421
422    /// Validate ownership and the local lease BEFORE calling M1 admission.
423    /// Remote redelivery and reordering do not require the receiver to own the
424    /// author's coordinate. They require the record to carry its author's space.
425    pub fn receive(
426        &mut self,
427        ticket: WriteTicket,
428        record: Record<C::Delta>,
429    ) -> Result<Admission, LocalError> {
430        self.admit(ticket, record, false)
431    }
432    fn admit(
433        &mut self,
434        ticket: WriteTicket,
435        record: Record<C::Delta>,
436        local: bool,
437    ) -> Result<Admission, LocalError> {
438        self.admit_committed(ticket, record, local, |_, _| Ok(()), |_| Ok(()))
439    }
440    fn admit_committed(
441        &mut self,
442        ticket: WriteTicket,
443        record: Record<C::Delta>,
444        local: bool,
445        commit: impl FnOnce(&EventLog<C::Delta>, u64) -> Result<(), LocalError>,
446        save_alarm: impl FnOnce(&EventLog<C::Delta>) -> Result<(), LocalError>,
447    ) -> Result<Admission, LocalError> {
448        if local && self.peer_writer_ahead {
449            return Err(LocalError::PeerWriterAhead);
450        }
451        // Preserve lease and ownership precedence, then surface the carrier cause
452        // for a received OR-Set sequence-zero add or remove.
453        if !local
454            && record.id.sequence == 0
455            && self.held
456            && ticket.0 == self.generation
457            && self.generation > 0
458            && record.id.replica < self.config.writers
459        {
460            if let Err(
461                error @ (WireError::ZeroSequenceAdd { .. } | WireError::ZeroSequenceRemove { .. }),
462            ) = self.state.validate_record(record.id, &record.delta)
463            {
464                return Err(LocalError::InvalidRecord(error));
465            }
466        }
467        if refuses(
468            self.context(ticket, local),
469            record.id,
470            record.delta.owned_payload(),
471        ) {
472            return Err(LocalError::Refused);
473        }
474        // Peer history cannot allocate IDs in this writer's space. A backup
475        // may have lost locally issued IDs, so stop this open writer as well.
476        if !local
477            && record.id.replica == self.config.writer
478            && record.id.sequence > self.last_sequence
479        {
480            self.peer_writer_ahead = true;
481            return Err(LocalError::PeerWriterAhead);
482        }
483        let outcome = self.log.admission(&self.state, &record);
484        if outcome != Admission::Accepted {
485            let prior = self.log.collisions.len();
486            self.log.raise_on_collision(outcome, record);
487            if self.log.collisions.len() != prior {
488                self.held = false;
489                save_alarm(&self.log)?;
490                self.held = true;
491            }
492            return Ok(outcome);
493        }
494        let candidate = PendingInsertion::new(&mut self.log, record.id);
495        let outcome = candidate.log.insert_record(&self.state, record.clone());
496        let sequence = if local {
497            record.id.sequence
498        } else {
499            self.last_sequence
500        };
501        // Keep the OS lock but revoke writes before any fallible persistence.
502        // An error (including an ambiguous rename/sync) cannot be renewed away.
503        self.held = false;
504        commit(candidate.log, sequence)?;
505        self.state.apply_delta(record.delta);
506        candidate.accept();
507        self.last_sequence = sequence;
508        self.held = true;
509        Ok(outcome)
510    }
511    // Only for the private restart candidate. Failure discards the entire
512    // candidate, so replay can admit in place without cloning a growing log.
513    fn replay_validated(&mut self, record: Record<C::Delta>) -> Result<(), LocalError> {
514        if refuses(
515            self.context(self.ticket(), false),
516            record.id,
517            record.delta.owned_payload(),
518        ) {
519            return Err(LocalError::Refused);
520        }
521        let id = record.id;
522        let outcome = self
523            .log
524            .admit_with(&mut self.state, record, |state, delta| {
525                state.apply_delta(delta.clone())
526            });
527        if outcome != Admission::Accepted {
528            return Err(LocalError::InvalidHistory);
529        }
530        if id.replica == self.config.writer {
531            self.last_sequence = self.last_sequence.max(id.sequence);
532        }
533        Ok(())
534    }
535
536    /// Append an existing delta in the configured writer's space. No allocation
537    /// or state mutation occurs on refusal, exhaustion, duplicate or collision.
538    pub fn append(
539        &mut self,
540        ticket: WriteTicket,
541        delta: C::Delta,
542    ) -> Result<Record<C::Delta>, LocalError> {
543        if self.peer_writer_ahead {
544            return Err(LocalError::PeerWriterAhead);
545        }
546        let sequence = next_sequence(self.last_sequence).ok_or(LocalError::Exhausted)?;
547        let record = Record {
548            id: RecordId {
549                replica: self.config.writer,
550                sequence,
551            },
552            delta,
553        };
554        match self.admit(ticket, record.clone(), true)? {
555            Admission::Accepted => Ok(record),
556            _ => Err(LocalError::Refused),
557        }
558    }
559}
560
561impl LocalReplica<GCounter> {
562    pub fn counter(root: &Path, config: WriterConfig) -> Result<Self, LocalError> {
563        config.validate().map_err(|_| LocalError::Configuration)?;
564        let n = usize::try_from(config.writers).map_err(|_| LocalError::Configuration)?;
565        Self::fresh(
566            root,
567            config,
568            GCounter::try_new(n).map_err(LocalError::CounterWidth)?,
569        )
570    }
571    pub fn bump(
572        &mut self,
573        ticket: WriteTicket,
574        tally: u64,
575    ) -> Result<Record<GCounterDelta>, LocalError> {
576        self.append(
577            ticket,
578            GCounterDelta {
579                replica: self.config.writer as usize,
580                tally,
581            },
582        )
583    }
584}
585impl LocalReplica<OrSet<String, u64>> {
586    pub fn utf8_set(root: &Path, config: WriterConfig) -> Result<Self, LocalError> {
587        Self::fresh(root, config, OrSet::new())
588    }
589    pub fn add(
590        &mut self,
591        ticket: WriteTicket,
592        element: String,
593    ) -> Result<Record<OrSetDelta<String, u64>>, LocalError> {
594        if self.peer_writer_ahead {
595            return Err(LocalError::PeerWriterAhead);
596        }
597        let sequence = next_sequence(self.last_sequence).ok_or(LocalError::Exhausted)?;
598        let token = allocate_token(self.config.writers, self.config.writer, sequence)
599            .ok_or(LocalError::Exhausted)?;
600        self.append(ticket, OrSetDelta::Add { element, token })
601    }
602    pub fn remove(
603        &mut self,
604        ticket: WriteTicket,
605        element: &String,
606    ) -> Result<Record<OrSetDelta<String, u64>>, LocalError> {
607        self.append(
608            ticket,
609            OrSetDelta::Remove {
610                tokens: self.state.observed_tokens(element).into_iter().collect(),
611            },
612        )
613    }
614}
615
616#[path = "persistence.rs"]
617mod persistence;
618
619#[path = "journal.rs"]
620mod journal;
621#[path = "rollback.rs"]
622mod rollback;
623
624/// One writer's committed history. The suffix is the existing, unchanged log
625/// wire encoding; the 24-byte prefix is LocalReplica::allocation_bytes(). This
626/// storage container is not a new transport encoding. Reading it grants no
627/// write lease and does not implement packet C's checked writable restart.
628/// For an append-log store, the journal's base frame and every complete record
629/// are reassembled into the frame a whole-history transaction would hold; a
630/// torn final record is excluded and the journal is not modified.
631#[derive(Debug, PartialEq, Eq)]
632pub struct CommittedTransaction {
633    pub config: WriterConfig,
634    pub last_sequence: u64,
635    pub log_bytes: Vec<u8>,
636}
637impl CommittedTransaction {
638    pub fn read(root: &Path, config: WriterConfig) -> Result<Self, LocalError> {
639        config.validate().map_err(|_| LocalError::Configuration)?;
640        if store_format(root, config.writer)? == Format::Journal {
641            let bytes = fs::read(journal::path(root, config.writer))?;
642            let parsed = journal::parse(&bytes, config)?;
643            let records: Vec<&[u8]> = parsed.entries.iter().map(|&(_, record)| record).collect();
644            return Ok(Self {
645                config,
646                last_sequence: parsed
647                    .entries
648                    .last()
649                    .map_or(parsed.base_sequence, |&(sequence, _)| sequence),
650                log_bytes: journal::assemble(parsed.base_frame, &records)
651                    .map_err(LocalError::History)?,
652            });
653        }
654        legacy_transaction(root, config)
655    }
656}
657fn transaction_path(root: &Path, config: WriterConfig) -> PathBuf {
658    root.join(format!("writer-{}.transaction", config.writer))
659}
660
661#[derive(Clone, Copy, Debug, PartialEq, Eq)]
662enum Format {
663    /// `writer-<id>.transaction`, written before the append log: every commit
664    /// replaces the whole file.
665    Transaction,
666    /// `writer-<id>.journal`: every commit appends one record and syncs it.
667    Journal,
668}
669
670fn exists(path: &Path) -> Result<bool, LocalError> {
671    match fs::metadata(path) {
672        Ok(_) => Ok(true),
673        Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
674        Err(e) => Err(e.into()),
675    }
676}
677
678// A writer has one committed store file. Both files mean an explicit migration
679// stopped after its journal was complete; only migration resolves that. With
680// neither, the transaction read reports NotFound exactly as before.
681fn store_format(root: &Path, writer: u64) -> Result<Format, LocalError> {
682    let transaction = exists(&root.join(format!("writer-{writer}.transaction")))?;
683    match (transaction, exists(&journal::path(root, writer))?) {
684        (true, true) => Err(LocalError::RecoveryRequired),
685        (false, true) => Ok(Format::Journal),
686        _ => Ok(Format::Transaction),
687    }
688}
689
690fn encode_error(error: WireError) -> LocalError {
691    LocalError::Io(io::Error::other(format!("{error:?}")))
692}
693
694// Read at most `len` leading bytes; a shorter file yields all of its bytes.
695fn read_prefix(path: &Path, len: u64) -> io::Result<Vec<u8>> {
696    let mut bytes = Vec::new();
697    File::open(path)?.take(len).read_to_end(&mut bytes)?;
698    Ok(bytes)
699}
700
701// The record count a committed transaction declares, from a bounded prefix: the
702// 24-byte allocation header, then the current EventLog frame's tag, length pair,
703// shape header and count. Neither the frame CRC nor any record is read, so a
704// corrupted count can be refused by budget before the full read would report it.
705// `None` means the prefix is not a current frame for this writer and delta
706// schema; the full checked read and decode then name the failure.
707fn declared_records<D: WireSchema>(
708    path: &Path,
709    config: WriterConfig,
710) -> Result<Option<usize>, LocalError> {
711    let bytes = read_prefix(path, (33 + frame_prefix::<D>()) as u64)?;
712    let at = transaction_fields(&bytes)?;
713    let mut cursor = crate::WireCursor::new(&bytes[at..]);
714    let mut parse = || -> Result<Option<usize>, WireError> {
715        if cursor.read_u64()? != config.writers || cursor.read_u64()? != config.writer {
716            return Ok(None);
717        }
718        cursor.read_u64()?;
719        declared_frame_records::<D>(&mut cursor)
720    };
721    Ok(parse().unwrap_or(None))
722}
723
724// Tag, length pair, shape header and record count of a current EventLog frame.
725fn frame_prefix<D: WireSchema>() -> usize {
726    1 + 8 + 4 + 4 + D::wire_schema().len() + 1 + 8 + 4
727}
728
729fn declared_frame_records<D: WireSchema>(
730    cursor: &mut crate::WireCursor<'_>,
731) -> Result<Option<usize>, WireError> {
732    if cursor.read_u8()? != crate::codec::TAG_EVENT_LOG {
733        return Ok(None);
734    }
735    let body = cursor.read_u32()?;
736    if cursor.read_u32()? != !body || cursor.read_u32()? != u32::MAX {
737        return Ok(None);
738    }
739    let schema_len = cursor.read_len()?;
740    if cursor.read_exact(schema_len)? != D::wire_schema().as_ref() {
741        return Ok(None);
742    }
743    match cursor.read_u8()? {
744        0 if !D::REQUIRES_ARITY => {}
745        1 => {
746            cursor.read_u64()?;
747        }
748        _ => return Ok(None),
749    }
750    cursor.read_len().map(Some)
751}
752
753// The same count for an append-log store: the base frame's declared records
754// plus complete journal entries, counted through a fixed buffer by their length
755// pairs. Stops once the budget is exceeded; no record is read or decoded.
756fn declared_journal_records<D: WireSchema>(
757    root: &Path,
758    config: WriterConfig,
759    max_records: usize,
760) -> Result<Option<usize>, LocalError> {
761    let file = File::open(journal::path(root, config.writer))?;
762    let file_len = file.metadata()?.len();
763    let mut reader = io::BufReader::new(file);
764    let mut header = [0u8; journal::HEADER];
765    if reader.read_exact(&mut header).is_err() {
766        return Ok(None);
767    }
768    let Ok((_, base_len)) = journal::check_header(&header, config) else {
769        return Ok(None);
770    };
771    let prefix_len = frame_prefix::<D>().min(base_len);
772    let mut prefix = alloc::vec![0u8; prefix_len];
773    if reader.read_exact(&mut prefix).is_err() {
774        return Ok(None);
775    }
776    let Ok(Some(base)) = declared_frame_records::<D>(&mut crate::WireCursor::new(&prefix)) else {
777        return Ok(None);
778    };
779    if base > max_records {
780        return Ok(Some(base));
781    }
782    reader.seek_relative((base_len - prefix_len) as i64)?;
783    let entries = journal::count_entries(
784        &mut reader,
785        (journal::HEADER + base_len) as u64,
786        file_len,
787        max_records - base + 1,
788    )?;
789    Ok(Some(base + entries))
790}
791
792/// Largest durable history, in records, that SafeMesh supports. Histories are
793/// retained forever and restart replays all of them, so restart time and memory
794/// grow with this size, as does the append cost of a store not yet migrated to
795/// the append log; `evidence/retention/results.md` records the benchmark that
796/// sets it. Ordinary restarts use this as their record cap; `_with_limits`
797/// accepts a caller budget or [`DecodeLimits::UNLIMITED`](crate::DecodeLimits).
798pub const SUPPORTED_MAX_RECORDS: usize = crate::DecodeLimits::DEFAULT_MAX_RECORDS;
799
800/// Additive durable API for Linux local filesystems. Fresh constructors create
801/// a missing root; the root must then remain in place. All writers use the same
802/// fixed root and configuration. Fresh stores keep an append-only
803/// `writer-<id>.journal`: each Accepted/Ok(record) follows appending that one
804/// record and syncing the file. Stores written before the append log keep
805/// `writer-<id>.transaction`, which restart still opens and each commit still
806/// replaces whole, with file + directory sync, until an explicit
807/// `migrate_*_to_append_log` call; restart never migrates. Any persistence
808/// error permanently disables this instance's writes, retaining its fence until
809/// drop. Use the explicit restart constructors for existing stores; fresh
810/// constructors never reset a store.
811pub struct DurableReplica<C: Crdt> {
812    inner: LocalReplica<C>,
813    store: Store,
814    root: PathBuf,
815    torn_tail: Option<TornTail>,
816    rollback: Option<rollback::Counter>,
817}
818
819enum Store {
820    Transaction(PathBuf),
821    /// The journal and the length of its complete entries, where the next
822    /// entry is written.
823    Journal {
824        file: File,
825        len: u64,
826    },
827}
828
829/// Bytes after the last complete record of an append-log store that restart
830/// discarded: an entry cut short, zero fill, or a final entry whose checksum
831/// fails. Such an append was interrupted before its file sync, so it was never
832/// acknowledged. Restart truncates the journal to `offset` and syncs it. A
833/// damaged entry followed by other data is not a torn tail; restart refuses it
834/// as `History(IntegrityMismatch)` and leaves the journal unchanged.
835#[derive(Clone, Copy, Debug, PartialEq, Eq)]
836#[non_exhaustive]
837pub struct TornTail {
838    /// Journal length after truncation: the end of the last complete record.
839    pub offset: u64,
840    /// Bytes removed from the end of the journal.
841    pub discarded_bytes: u64,
842}
843
844// A failed restart has no LocalReplica to unlock its fence. Release the lock
845// before closing the file: a concurrent fork may briefly retain a duplicate
846// of the open file description, even when the descriptor is close-on-exec.
847struct RestartFence(Option<File>);
848
849impl RestartFence {
850    fn file(&mut self) -> &mut File {
851        self.0.as_mut().unwrap()
852    }
853
854    fn into_file(mut self) -> File {
855        self.0.take().unwrap()
856    }
857}
858
859impl Drop for RestartFence {
860    fn drop(&mut self) {
861        if let Some(file) = &self.0 {
862            let _ = file.unlock();
863        }
864    }
865}
866
867#[cfg(test)]
868std::thread_local! {
869    static RETAIN_RESTART_FENCE: std::cell::RefCell<Option<Option<File>>> = const { std::cell::RefCell::new(None) };
870}
871
872impl<C: Crdt> DurableReplica<C>
873where
874    C::Delta: OwnedDelta + Clone + PartialEq + WireEncode + WireSchema,
875{
876    fn fresh(root: &Path, config: WriterConfig, state: C) -> Result<Self, LocalError> {
877        prepare_durable_root(root, config)?;
878        let root = root.canonicalize()?;
879        // Never overwrite a history whose fence is missing, in either format.
880        let path = journal::path(&root, config.writer);
881        if exists(&transaction_path(&root, config))? || exists(&path)? {
882            return Err(LocalError::StoreExists);
883        }
884        // The counter lives beside the store. Check it only after canonicalizing
885        // the root, and before creating this writer's fence or journal.
886        rollback::Counter::ensure_absent(&root, config)?;
887        let mut inner = LocalReplica::fresh(&root, config, state)?;
888        if !inner.held {
889            return Err(LocalError::Refused);
890        }
891        inner.held = false;
892        // The header and empty base frame appear whole or not at all.
893        let header = journal::header(config, 0, &inner.log.to_wire_bytes().map_err(encode_error)?)?;
894        persistence::replace(&path, &header)?;
895        let file = OpenOptions::new().read(true).write(true).open(&path)?;
896        let rollback = rollback::Counter::create(&root, config)?;
897        inner.held = true;
898        Ok(Self {
899            inner,
900            root,
901            store: Store::Journal {
902                file,
903                len: header.len() as u64,
904            },
905            torn_tail: None,
906            rollback: Some(rollback),
907        })
908    }
909    fn commit(
910        path: &Path,
911        config: WriterConfig,
912        log: &EventLog<C::Delta>,
913        sequence: u64,
914    ) -> Result<(), LocalError> {
915        let mut bytes = [
916            config.writers.to_le_bytes(),
917            config.writer.to_le_bytes(),
918            sequence.to_le_bytes(),
919        ]
920        .concat();
921        bytes.extend(log.to_wire_bytes().map_err(encode_error)?);
922        persistence::replace(path, &bytes)?;
923        Ok(())
924    }
925    // Append the candidate's new last record at the end of the complete entries,
926    // then sync. The entry becomes part of `len` only after the sync returns.
927    fn append_entry(
928        file: &File,
929        len: &mut u64,
930        log: &EventLog<C::Delta>,
931        sequence: u64,
932    ) -> Result<(), LocalError> {
933        use std::os::unix::fs::FileExt;
934        let record = log.records().last().ok_or(LocalError::InvalidHistory)?;
935        let bytes = journal::entry(sequence, &record.to_wire_bytes().map_err(encode_error)?)?;
936        persistence::checkpoint(11)?;
937        // Leave half an entry on disk, as an interrupted write would.
938        #[cfg(test)]
939        if persistence::FAULT.with(|fault| fault.get().0) == 12 {
940            file.write_all_at(&bytes[..bytes.len() / 2], *len)?;
941        }
942        persistence::checkpoint(12)?;
943        file.write_all_at(&bytes, *len)?;
944        persistence::checkpoint(13)?;
945        // fdatasync also persists the file length needed to read the entry back.
946        file.sync_data()?;
947        persistence::checkpoint(14)?;
948        *len += bytes.len() as u64;
949        Ok(())
950    }
951    /// The torn tail this instance's restart discarded from an append-log
952    /// store, if any. `None` for a fresh store, a transaction-format store and
953    /// a journal that ended on a complete record.
954    pub fn torn_tail(&self) -> Option<TornTail> {
955        self.torn_tail
956    }
957    pub fn state(&self) -> &C {
958        self.inner.state()
959    }
960    pub fn log(&self) -> &EventLog<C::Delta> {
961        self.inner.log()
962    }
963    pub fn allocation_bytes(&self) -> Vec<u8> {
964        self.inner.allocation_bytes()
965    }
966    pub fn ticket(&self) -> WriteTicket {
967        self.inner.ticket()
968    }
969    pub fn renew(&mut self, ticket: WriteTicket) -> Result<WriteTicket, LocalError> {
970        self.inner.renew(ticket)
971    }
972    fn admit(
973        &mut self,
974        ticket: WriteTicket,
975        record: Record<C::Delta>,
976        local: bool,
977    ) -> Result<Admission, LocalError> {
978        let store = &mut self.store;
979        let rollback = &mut self.rollback;
980        let config = self.inner.config;
981        let alarm_path = self.root.join(format!("writer-{}.alarms", config.writer));
982        let outcome = self.inner.admit_committed(
983            ticket,
984            record,
985            local,
986            |log, sequence| {
987                if local {
988                    if let Some(counter) = rollback {
989                        counter.advance(sequence)?;
990                    }
991                }
992                match store {
993                    Store::Transaction(path) => Self::commit(path, config, log, sequence),
994                    Store::Journal { file, len } => Self::append_entry(file, len, log, sequence),
995                }
996            },
997            |log| {
998                let bytes = log
999                    .collision_report_bytes()
1000                    .map_err(encode_error)?
1001                    .ok_or(LocalError::InvalidHistory)?;
1002                persistence::replace(&alarm_path, &bytes)?;
1003                Ok(())
1004            },
1005        )?;
1006        #[cfg(test)]
1007        persistence::checkpoint(7)?;
1008        Ok(outcome)
1009    }
1010    pub fn receive(
1011        &mut self,
1012        ticket: WriteTicket,
1013        record: Record<C::Delta>,
1014    ) -> Result<Admission, LocalError> {
1015        self.admit(ticket, record, false)
1016    }
1017    pub fn append(
1018        &mut self,
1019        ticket: WriteTicket,
1020        delta: C::Delta,
1021    ) -> Result<Record<C::Delta>, LocalError> {
1022        if self.inner.peer_writer_ahead {
1023            return Err(LocalError::PeerWriterAhead);
1024        }
1025        let sequence = next_sequence(self.inner.last_sequence).ok_or(LocalError::Exhausted)?;
1026        let record = Record {
1027            id: RecordId {
1028                replica: self.inner.config.writer,
1029                sequence,
1030            },
1031            delta,
1032        };
1033        match self.admit(ticket, record.clone(), true)? {
1034            Admission::Accepted => Ok(record),
1035            _ => Err(LocalError::Refused),
1036        }
1037    }
1038
1039    /// Commit local deltas as one durable batch. The counter records the final
1040    /// sequence before any journal entry is written. An interrupted batch can
1041    /// therefore require recovery, as an interrupted single append can.
1042    pub fn append_batch(
1043        &mut self,
1044        ticket: WriteTicket,
1045        deltas: impl IntoIterator<Item = C::Delta>,
1046    ) -> Result<Vec<Record<C::Delta>>, LocalError>
1047    where
1048        C: Clone,
1049    {
1050        let deltas: Vec<_> = deltas.into_iter().collect();
1051        if deltas.is_empty() {
1052            return Ok(Vec::new());
1053        }
1054        if self.inner.peer_writer_ahead {
1055            return Err(LocalError::PeerWriterAhead);
1056        }
1057        let count = u64::try_from(deltas.len()).map_err(|_| LocalError::Exhausted)?;
1058        let last = self
1059            .inner
1060            .last_sequence
1061            .checked_add(count)
1062            .ok_or(LocalError::Exhausted)?;
1063        let first = self.inner.last_sequence + 1;
1064        let mut state = self.inner.state.clone();
1065        let mut log = self.inner.log.clone();
1066        let mut records = Vec::with_capacity(deltas.len());
1067        let mut entries = Vec::with_capacity(deltas.len());
1068        for (index, delta) in deltas.into_iter().enumerate() {
1069            let sequence = first + index as u64;
1070            let record = Record {
1071                id: RecordId {
1072                    replica: self.inner.config.writer,
1073                    sequence,
1074                },
1075                delta,
1076            };
1077            if refuses(
1078                self.inner.context(ticket, true),
1079                record.id,
1080                record.delta.owned_payload(),
1081            ) {
1082                return Err(LocalError::Refused);
1083            }
1084            if log.insert_record(&state, record.clone()) != Admission::Accepted {
1085                return Err(LocalError::Refused);
1086            }
1087            state.apply_delta(record.delta.clone());
1088            if matches!(self.store, Store::Journal { .. }) {
1089                entries.push(journal::entry(
1090                    sequence,
1091                    &record.to_wire_bytes().map_err(encode_error)?,
1092                )?);
1093            }
1094            records.push(record);
1095        }
1096        // Keep the lock but disable writes before any uncertain persistence.
1097        self.inner.held = false;
1098        if let Some(counter) = &mut self.rollback {
1099            counter.advance_batch(first, last)?;
1100        }
1101        match &mut self.store {
1102            Store::Transaction(path) => Self::commit(path, self.inner.config, &log, last)?,
1103            Store::Journal { file, len } => {
1104                use std::os::unix::fs::FileExt;
1105                let mut offset = *len;
1106                for (index, bytes) in entries.iter().enumerate() {
1107                    file.write_all_at(bytes, offset)?;
1108                    offset += bytes.len() as u64;
1109                    if index == 0 {
1110                        persistence::checkpoint(21)?;
1111                    }
1112                }
1113                persistence::checkpoint(13)?;
1114                file.sync_data()?;
1115                persistence::checkpoint(14)?;
1116                *len = offset;
1117            }
1118        }
1119        self.inner.state = state;
1120        self.inner.log = log;
1121        self.inner.last_sequence = last;
1122        self.inner.held = true;
1123        Ok(records)
1124    }
1125}
1126impl<C: Crdt> DurableReplica<C>
1127where
1128    C::Delta: OwnedDelta + Clone + PartialEq + WireEncode + WireDecode + WireSchema,
1129{
1130    fn restart(root: &Path, config: WriterConfig, state: C) -> Result<Self, LocalError> {
1131        Self::restart_with_limit_origin(root, config, state, crate::DecodeLimits::default(), true)
1132    }
1133
1134    // `max_collection_elements: None` keeps the ordinary stored-byte budget below.
1135    fn restart_with_limits(
1136        root: &Path,
1137        config: WriterConfig,
1138        state: C,
1139        limits: crate::DecodeLimits,
1140    ) -> Result<Self, LocalError> {
1141        Self::restart_with_limit_origin(root, config, state, limits, false)
1142    }
1143
1144    fn restart_with_limit_origin(
1145        root: &Path,
1146        config: WriterConfig,
1147        state: C,
1148        limits: crate::DecodeLimits,
1149        default_limit: bool,
1150    ) -> Result<Self, LocalError> {
1151        config.validate().map_err(|_| LocalError::Configuration)?;
1152        let root = root.canonicalize()?;
1153        let fence = Self::lock_restart_fence(&root, config.writer)?;
1154        Self::restart_locked(&root, fence, config, state, limits, default_limit)
1155    }
1156
1157    fn lock_restart_fence(root: &Path, writer: u64) -> Result<RestartFence, LocalError> {
1158        // Opening without create is deliberate: missing ownership is not a new store.
1159        let fence = OpenOptions::new()
1160            .read(true)
1161            .write(true)
1162            .open(root.join(format!("writer-{writer}.fence")))?;
1163        match fence.try_lock() {
1164            Ok(()) => {}
1165            Err(TryLockError::WouldBlock) => return Err(LocalError::Refused),
1166            Err(TryLockError::Error(e)) => return Err(e.into()),
1167        }
1168        #[cfg(test)]
1169        RETAIN_RESTART_FENCE.with(|slot| {
1170            if let Some(duplicate) = slot.borrow_mut().as_mut() {
1171                *duplicate = Some(fence.try_clone()?);
1172            }
1173            Ok::<(), io::Error>(())
1174        })?;
1175        Ok(RestartFence(Some(fence)))
1176    }
1177
1178    fn restart_locked(
1179        root: &Path,
1180        mut fence: RestartFence,
1181        config: WriterConfig,
1182        state: C,
1183        limits: crate::DecodeLimits,
1184        default_limit: bool,
1185    ) -> Result<Self, LocalError> {
1186        let mut bytes = Vec::new();
1187        fence.file().read_to_end(&mut bytes)?;
1188        let at = fence_fields(&bytes)?;
1189        let word = |i| u64::from_le_bytes(bytes[at + i..at + i + 8].try_into().unwrap());
1190        if word(0) != config.writers || word(8) != config.writer {
1191            return Err(LocalError::Configuration);
1192        }
1193        let generation = word(16);
1194        if generation == 0 {
1195            return Err(LocalError::RecoveryRequired);
1196        }
1197        let max_records = limits.max_records;
1198        let format = store_format(root, config.writer)?;
1199        // Refuse an over-budget history from its declared count, before the
1200        // history is read in full or any record is decoded or replayed.
1201        if let Some(max_records) = max_records {
1202            let declared = match format {
1203                Format::Transaction => {
1204                    declared_records::<C::Delta>(&transaction_path(root, config), config)?
1205                }
1206                Format::Journal => declared_journal_records::<C::Delta>(root, config, max_records)?,
1207            };
1208            if declared.is_some_and(|records| records > max_records) {
1209                return Err(LocalError::RecordLimitExceeded {
1210                    max_records,
1211                    default_limit,
1212                });
1213            }
1214        }
1215        let decode_error = |error| match error {
1216            crate::DecodeError::Wire(error) => LocalError::History(error),
1217            crate::DecodeError::RecordLimitExceeded { max_records }
1218            | crate::DecodeError::DefaultRecordLimitExceeded { max_records } => {
1219                LocalError::RecordLimitExceeded {
1220                    max_records,
1221                    default_limit,
1222                }
1223            }
1224        };
1225        // The history's bytes are already in memory. Every wire collection
1226        // element occupies at least one byte, so their length bounds any count
1227        // without imposing a new writer-lifetime limit on stores created before
1228        // collection ceilings were introduced. Peer wire decoders retain their
1229        // independent 4,096-element default.
1230        let max_elements = |stored: usize| {
1231            limits.max_collection_elements.unwrap_or_else(|| {
1232                stored.max(crate::CollectionLimits::WIRE_DEFAULT.max_elements.unwrap())
1233            })
1234        };
1235        // The lock covers reading, checking and replaying the complete history.
1236        let (base, base_sequence, entries, store, torn_tail) = match format {
1237            Format::Transaction => {
1238                let transaction = CommittedTransaction::read(root, config)?;
1239                let log = EventLog::<C::Delta>::from_wire_bytes_for_with_limits(
1240                    &transaction.log_bytes,
1241                    &state,
1242                    crate::DecodeLimits {
1243                        max_records,
1244                        max_collection_elements: Some(max_elements(transaction.log_bytes.len())),
1245                    },
1246                )
1247                .map_err(decode_error)?;
1248                let store = Store::Transaction(transaction_path(root, config));
1249                (log, transaction.last_sequence, Vec::new(), store, None)
1250            }
1251            Format::Journal => {
1252                let mut file = OpenOptions::new()
1253                    .read(true)
1254                    .write(true)
1255                    .open(journal::path(root, config.writer))?;
1256                let mut bytes = Vec::new();
1257                file.read_to_end(&mut bytes)?;
1258                let parsed = journal::parse(&bytes, config)?;
1259                let max_elements = max_elements(bytes.len());
1260                let log = EventLog::<C::Delta>::from_wire_bytes_for_with_limits(
1261                    parsed.base_frame,
1262                    &state,
1263                    crate::DecodeLimits {
1264                        max_records,
1265                        max_collection_elements: Some(max_elements),
1266                    },
1267                )
1268                .map_err(decode_error)?;
1269                if let Some(max_records) = max_records {
1270                    if log.records().len() + parsed.entries.len() > max_records {
1271                        return Err(LocalError::RecordLimitExceeded {
1272                            max_records,
1273                            default_limit,
1274                        });
1275                    }
1276                }
1277                let entries = parsed
1278                    .entries
1279                    .iter()
1280                    .map(|&(sequence, bytes)| {
1281                        Record::<C::Delta>::from_wire_bytes_with_collection_limits(
1282                            bytes,
1283                            crate::CollectionLimits {
1284                                max_elements: Some(max_elements),
1285                            },
1286                        )
1287                        .map(|record| (sequence, record))
1288                        .map_err(LocalError::History)
1289                    })
1290                    .collect::<Result<Vec<_>, _>>()?;
1291                let torn_tail = parsed.tail.map(|tail| TornTail {
1292                    offset: tail.offset,
1293                    discarded_bytes: tail.bytes,
1294                });
1295                let store = Store::Journal {
1296                    file,
1297                    len: parsed.valid_len,
1298                };
1299                (log, parsed.base_sequence, entries, store, torn_tail)
1300            }
1301        };
1302        let mut inner = LocalReplica {
1303            config,
1304            fence: fence.into_file(),
1305            held: true,
1306            generation,
1307            log: EventLog::for_crdt(&state),
1308            state,
1309            last_sequence: 0,
1310            peer_writer_ahead: false,
1311        };
1312        // Compose packet A's corpus-bound ownedStep with M1 admission/replay.
1313        // This candidate is private until every record and allocation check passes.
1314        for record in base.records() {
1315            inner.replay_validated(record.clone())?;
1316        }
1317        if inner.last_sequence != base_sequence {
1318            return Err(LocalError::InvalidHistory);
1319        }
1320        drop(base);
1321        for (sequence, record) in entries {
1322            inner.replay_validated(record)?;
1323            if inner.last_sequence != sequence {
1324                return Err(LocalError::InvalidHistory);
1325            }
1326        }
1327        let rollback = rollback::Counter::open(root, config, inner.last_sequence)?;
1328        // The alarm file is a complete collision-report frame. A damaged
1329        // frame is refused; it is never treated as a journal torn tail.
1330        let alarm_path = root.join(format!("writer-{}.alarms", config.writer));
1331        match fs::read(alarm_path) {
1332            Ok(bytes) => {
1333                let report =
1334                    crate::CollisionReport::<C::Delta>::from_wire_bytes_with_limits(&bytes, limits)
1335                        .map_err(decode_error)?;
1336                for (id, verdict) in inner.log.merge_collision_report(report) {
1337                    if verdict != crate::CollisionVerdict::Recorded {
1338                        let _ = id;
1339                        return Err(LocalError::InvalidHistory);
1340                    }
1341                }
1342            }
1343            Err(error) if error.kind() == io::ErrorKind::NotFound => {}
1344            Err(error) => return Err(error.into()),
1345        }
1346        // Only a restart that validated every complete record drops the torn
1347        // tail, so the next entry is written where the last complete one ends.
1348        if let (Some(tail), Store::Journal { file, .. }) = (torn_tail, &store) {
1349            file.set_len(tail.offset)?;
1350            file.sync_all()?;
1351        }
1352        // A ticket from before restart must not authorize the newly acquired lease.
1353        inner.renew(inner.ticket())?;
1354        Ok(Self {
1355            inner,
1356            root: root.to_path_buf(),
1357            store,
1358            torn_tail,
1359            rollback,
1360        })
1361    }
1362
1363    // Explicit, one-way move of a transaction-format store to the append log.
1364    // Its complete history becomes the journal's base frame. A crash before the
1365    // journal is in place leaves the transaction store; after it, both files
1366    // exist, which ordinary restart refuses and a repeated migration completes.
1367    fn migrate(root: &Path, config: WriterConfig, state: C) -> Result<Self, LocalError> {
1368        config.validate().map_err(|_| LocalError::Configuration)?;
1369        let root = root.canonicalize()?;
1370        let fence = Self::lock_restart_fence(&root, config.writer)?;
1371        let transaction = transaction_path(&root, config);
1372        let path = journal::path(&root, config.writer);
1373        if exists(&transaction)? && exists(&path)? {
1374            // The journal must hold exactly the transaction's history with
1375            // nothing appended, as an interrupted migration leaves it.
1376            let committed = legacy_transaction(&root, config)?;
1377            let bytes = fs::read(&path)?;
1378            let parsed = journal::parse(&bytes, config)?;
1379            let limits = crate::DecodeLimits {
1380                // Recovery comparison must inspect both complete stored histories.
1381                max_records: crate::DecodeLimits::UNLIMITED.max_records,
1382                max_collection_elements: Some(bytes.len().max(4096)),
1383            };
1384            let decode = |frame| {
1385                EventLog::<C::Delta>::from_wire_bytes_for_with_limits(frame, &state, limits)
1386                    .map_err(|_| LocalError::RecoveryRequired)
1387            };
1388            if !parsed.entries.is_empty()
1389                || parsed.tail.is_some()
1390                || parsed.base_sequence != committed.last_sequence
1391                || decode(parsed.base_frame)? != decode(&committed.log_bytes)?
1392            {
1393                return Err(LocalError::RecoveryRequired);
1394            }
1395            fs::remove_file(&transaction)?;
1396            File::open(&root)?.sync_all()?;
1397        }
1398        let mut replica =
1399            Self::restart_locked(&root, fence, config, state, Default::default(), true)?;
1400        if let Store::Transaction(_) = replica.store {
1401            let header = journal::header(
1402                config,
1403                replica.inner.last_sequence,
1404                &replica.inner.log.to_wire_bytes().map_err(encode_error)?,
1405            )?;
1406            persistence::replace(&path, &header)?;
1407            persistence::checkpoint(15)?;
1408            fs::remove_file(&transaction)?;
1409            File::open(&root)?.sync_all()?;
1410            let file = OpenOptions::new().read(true).write(true).open(&path)?;
1411            replica.store = Store::Journal {
1412                file,
1413                len: header.len() as u64,
1414            };
1415        }
1416        Ok(replica)
1417    }
1418}
1419
1420fn legacy_transaction(
1421    root: &Path,
1422    config: WriterConfig,
1423) -> Result<CommittedTransaction, LocalError> {
1424    let bytes = fs::read(transaction_path(root, config))?;
1425    let at = transaction_fields(&bytes)?;
1426    if bytes.len() < at + 24 {
1427        return Err(LocalError::RecoveryRequired);
1428    }
1429    let word = |i| u64::from_le_bytes(bytes[at + i..at + i + 8].try_into().unwrap());
1430    if word(0) != config.writers || word(8) != config.writer {
1431        return Err(LocalError::Configuration);
1432    }
1433    Ok(CommittedTransaction {
1434        config,
1435        last_sequence: word(16),
1436        log_bytes: bytes[at + 24..].to_vec(),
1437    })
1438}
1439
1440impl DurableReplica<GCounter> {
1441    /// Reacquire ownership, validate the committed history and replay fresh state.
1442    /// Any error returns no replica and grants no write ticket.
1443    pub fn restart_counter(root: &Path, config: WriterConfig) -> Result<Self, LocalError> {
1444        config.validate().map_err(|_| LocalError::Configuration)?;
1445        let n = usize::try_from(config.writers).map_err(|_| LocalError::Configuration)?;
1446        Self::restart(
1447            root,
1448            config,
1449            GCounter::try_new(n).map_err(LocalError::CounterWidth)?,
1450        )
1451    }
1452    /// [`restart_counter`](Self::restart_counter) with a restart budget.
1453    /// `limits.max_records` refuses a history declaring more records with
1454    /// [`LocalError::RecordLimitExceeded`] before any record is read, leaving
1455    /// the store unchanged. `max_collection_elements: None` keeps the ordinary
1456    /// stored-byte collection budget rather than the 4,096 wire default.
1457    pub fn restart_counter_with_limits(
1458        root: &Path,
1459        config: WriterConfig,
1460        limits: crate::DecodeLimits,
1461    ) -> Result<Self, LocalError> {
1462        config.validate().map_err(|_| LocalError::Configuration)?;
1463        let n = usize::try_from(config.writers).map_err(|_| LocalError::Configuration)?;
1464        Self::restart_with_limits(
1465            root,
1466            config,
1467            GCounter::try_new(n).map_err(LocalError::CounterWidth)?,
1468            limits,
1469        )
1470    }
1471    /// Reacquire the writer, read its committed writer count, then run checked replay.
1472    /// The root must already contain a durable counter store for this writer.
1473    pub fn restart_counter_from_store(root: &Path, writer: u64) -> Result<Self, LocalError> {
1474        Self::restart_counter_from_store_with_origin(
1475            root,
1476            writer,
1477            crate::DecodeLimits::default(),
1478            true,
1479        )
1480    }
1481    /// [`restart_counter_from_store`](Self::restart_counter_from_store) with the
1482    /// restart budget of [`restart_counter_with_limits`](Self::restart_counter_with_limits).
1483    pub fn restart_counter_from_store_with_limits(
1484        root: &Path,
1485        writer: u64,
1486        limits: crate::DecodeLimits,
1487    ) -> Result<Self, LocalError> {
1488        Self::restart_counter_from_store_with_origin(root, writer, limits, false)
1489    }
1490
1491    fn restart_counter_from_store_with_origin(
1492        root: &Path,
1493        writer: u64,
1494        limits: crate::DecodeLimits,
1495        default_limit: bool,
1496    ) -> Result<Self, LocalError> {
1497        let root = root.canonicalize()?;
1498        let fence = Self::lock_restart_fence(&root, writer)?;
1499        // The writer lock covers the metadata read and the same checked replay
1500        // used by restart_counter. A missing or truncated transaction is not fresh.
1501        let (bytes, at) = match store_format(&root, writer)? {
1502            Format::Transaction => {
1503                let bytes = read_prefix(&root.join(format!("writer-{writer}.transaction")), 34)?;
1504                let at = transaction_fields(&bytes)?;
1505                (bytes, at)
1506            }
1507            Format::Journal => {
1508                let bytes = read_prefix(&journal::path(&root, writer), journal::HEADER as u64)?;
1509                journal::check_magic(&bytes)?;
1510                if bytes.len() < journal::HEADER {
1511                    return Err(LocalError::RecoveryRequired);
1512                }
1513                (bytes, 8)
1514            }
1515        };
1516        if bytes.len() < at + 24 {
1517            return Err(LocalError::RecoveryRequired);
1518        }
1519        let writers = u64::from_le_bytes(bytes[at..at + 8].try_into().unwrap());
1520        let stored_writer = u64::from_le_bytes(bytes[at + 8..at + 16].try_into().unwrap());
1521        WriterConfig {
1522            writers,
1523            writer: stored_writer,
1524        }
1525        .validate()
1526        .map_err(|_| LocalError::Configuration)?;
1527        let config = WriterConfig { writers, writer };
1528        if stored_writer != writer {
1529            return Err(LocalError::Configuration);
1530        }
1531        let n = usize::try_from(writers).map_err(|_| LocalError::Configuration)?;
1532        Self::restart_locked(
1533            &root,
1534            fence,
1535            config,
1536            GCounter::try_new(n).map_err(LocalError::CounterWidth)?,
1537            limits,
1538            default_limit,
1539        )
1540    }
1541    /// Explicitly move a counter store written before the append log
1542    /// (`writer-<id>.transaction`) to the append log (`writer-<id>.journal`),
1543    /// after the same checked replay as [`restart_counter`](Self::restart_counter),
1544    /// and return it ready to write. Every record is kept. Restart never
1545    /// migrates. If a crash leaves both files, restart refuses with
1546    /// `RecoveryRequired` and calling this again completes the move. An
1547    /// append-log store is restarted unchanged.
1548    pub fn migrate_counter_to_append_log(
1549        root: &Path,
1550        config: WriterConfig,
1551    ) -> Result<Self, LocalError> {
1552        config.validate().map_err(|_| LocalError::Configuration)?;
1553        let n = usize::try_from(config.writers).map_err(|_| LocalError::Configuration)?;
1554        Self::migrate(root, config, GCounter::new(n))
1555    }
1556    pub fn counter(root: &Path, config: WriterConfig) -> Result<Self, LocalError> {
1557        config.validate().map_err(|_| LocalError::Configuration)?;
1558        let n = usize::try_from(config.writers).map_err(|_| LocalError::Configuration)?;
1559        Self::fresh(
1560            root,
1561            config,
1562            GCounter::try_new(n).map_err(LocalError::CounterWidth)?,
1563        )
1564    }
1565    pub fn bump(
1566        &mut self,
1567        ticket: WriteTicket,
1568        tally: u64,
1569    ) -> Result<Record<GCounterDelta>, LocalError> {
1570        self.append(
1571            ticket,
1572            GCounterDelta {
1573                replica: self.inner.config.writer as usize,
1574                tally,
1575            },
1576        )
1577    }
1578    /// Append many increasing local tallies with one counter write and one
1579    /// journal sync. An empty batch has no effect.
1580    pub fn bump_batch(
1581        &mut self,
1582        ticket: WriteTicket,
1583        tallies: impl IntoIterator<Item = u64>,
1584    ) -> Result<Vec<Record<GCounterDelta>>, LocalError> {
1585        let replica = self.inner.config.writer as usize;
1586        self.append_batch(
1587            ticket,
1588            tallies
1589                .into_iter()
1590                .map(|tally| GCounterDelta { replica, tally }),
1591        )
1592    }
1593}
1594impl DurableReplica<OrSet<String, u64>> {
1595    /// Checked ordinary restart, including tombstones and token allocation.
1596    /// Budgets stored collection counts from the committed transaction length.
1597    pub fn restart_utf8_set(root: &Path, config: WriterConfig) -> Result<Self, LocalError> {
1598        Self::restart(root, config, OrSet::new())
1599    }
1600    /// [`restart_utf8_set`](Self::restart_utf8_set) with a restart budget.
1601    /// `limits.max_records` refuses a history declaring more records with
1602    /// [`LocalError::RecordLimitExceeded`] before any record is read, leaving
1603    /// the store unchanged. `max_collection_elements: None` keeps the ordinary
1604    /// stored-byte collection budget; `Some(n)` matches
1605    /// [`restart_utf8_set_with_max_collection_elements`](Self::restart_utf8_set_with_max_collection_elements).
1606    pub fn restart_utf8_set_with_limits(
1607        root: &Path,
1608        config: WriterConfig,
1609        limits: crate::DecodeLimits,
1610    ) -> Result<Self, LocalError> {
1611        Self::restart_with_limits(root, config, OrSet::new(), limits)
1612    }
1613    /// Restart a stored set with an explicit collection ceiling instead of the
1614    /// ordinary stored-byte budget.
1615    pub fn restart_utf8_set_with_max_collection_elements(
1616        root: &Path,
1617        config: WriterConfig,
1618        max_elements: usize,
1619    ) -> Result<Self, LocalError> {
1620        Self::restart_with_limits(
1621            root,
1622            config,
1623            OrSet::new(),
1624            crate::DecodeLimits {
1625                max_records: crate::DecodeLimits::default().max_records,
1626                max_collection_elements: Some(max_elements),
1627            },
1628        )
1629    }
1630    pub fn utf8_set(root: &Path, config: WriterConfig) -> Result<Self, LocalError> {
1631        Self::fresh(root, config, OrSet::new())
1632    }
1633    /// Explicitly move a UTF-8 OR-Set store written before the append log to
1634    /// the append log, as
1635    /// [`migrate_counter_to_append_log`](DurableReplica::migrate_counter_to_append_log)
1636    /// does for a counter, after the same checked replay as
1637    /// [`restart_utf8_set`](Self::restart_utf8_set).
1638    pub fn migrate_utf8_set_to_append_log(
1639        root: &Path,
1640        config: WriterConfig,
1641    ) -> Result<Self, LocalError> {
1642        Self::migrate(root, config, OrSet::new())
1643    }
1644    pub fn add(
1645        &mut self,
1646        ticket: WriteTicket,
1647        element: String,
1648    ) -> Result<Record<OrSetDelta<String, u64>>, LocalError> {
1649        if self.inner.peer_writer_ahead {
1650            return Err(LocalError::PeerWriterAhead);
1651        }
1652        let sequence = next_sequence(self.inner.last_sequence).ok_or(LocalError::Exhausted)?;
1653        let token = allocate_token(
1654            self.inner.config.writers,
1655            self.inner.config.writer,
1656            sequence,
1657        )
1658        .ok_or(LocalError::Exhausted)?;
1659        self.append(ticket, OrSetDelta::Add { element, token })
1660    }
1661    pub fn remove(
1662        &mut self,
1663        ticket: WriteTicket,
1664        element: &String,
1665    ) -> Result<Record<OrSetDelta<String, u64>>, LocalError> {
1666        self.append(
1667            ticket,
1668            OrSetDelta::Remove {
1669                tokens: self
1670                    .inner
1671                    .state
1672                    .observed_tokens(element)
1673                    .into_iter()
1674                    .collect(),
1675            },
1676        )
1677    }
1678}
1679
1680#[cfg(test)]
1681#[path = "durable_tests.rs"]
1682mod durable_tests;