Skip to main content

weavatrix_memory/store/
file.rs

1use super::{
2    EventStore, ExpectedVersion, InMemoryStore,
3    frame::{self, ScanOutcome},
4};
5use crate::{Codec, MemoryError, NewEvent, Result, StoredEvent, StreamId};
6use std::{
7    fs::{File, OpenOptions},
8    io::{Seek, SeekFrom, Write},
9    path::{Path, PathBuf},
10};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum RecoveryPolicy {
14    Strict,
15    TruncatePartialTail,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Durability {
20    Flush,
21    SyncData,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct FileStoreOptions {
26    pub recovery: RecoveryPolicy,
27    pub durability: Durability,
28    pub max_frame_bytes: usize,
29}
30
31impl Default for FileStoreOptions {
32    fn default() -> Self {
33        Self {
34            recovery: RecoveryPolicy::Strict,
35            durability: Durability::SyncData,
36            max_frame_bytes: 256 * 1024 * 1024,
37        }
38    }
39}
40
41pub struct FileEventStore<E, C> {
42    path: PathBuf,
43    file: File,
44    codec: C,
45    options: FileStoreOptions,
46    inner: InMemoryStore<E>,
47    durable_len: u64,
48}
49
50impl<E, C> FileEventStore<E, C>
51where
52    E: Clone,
53    C: Codec<StoredEvent<E>>,
54{
55    /// Opens or creates a framed append-only event journal.
56    ///
57    /// # Errors
58    ///
59    /// Rejects invalid headers, checksum corruption, invalid restored event
60    /// sequences, and partial tails in strict mode.
61    pub fn open(path: impl AsRef<Path>, codec: C, options: FileStoreOptions) -> Result<Self> {
62        if options.max_frame_bytes < 4 {
63            return Err(MemoryError::InvalidValue {
64                field: "max_frame_bytes",
65                reason: "must fit at least the event-count field",
66            });
67        }
68        let path = path.as_ref().to_path_buf();
69        let mut file = OpenOptions::new()
70            .read(true)
71            .write(true)
72            .create(true)
73            .truncate(false)
74            .open(&path)
75            .map_err(|error| io("open event log", error))?;
76        if file
77            .metadata()
78            .map_err(|error| io("read event log metadata", error))?
79            .len()
80            == 0
81        {
82            file.write_all(frame::FILE_HEADER)
83                .map_err(|error| io("write event log header", error))?;
84            sync(&file, options.durability)?;
85        }
86        let outcome = frame::scan(&mut file, &codec, options.max_frame_bytes)?;
87        let (events, durable_len, partial) = match outcome {
88            ScanOutcome::Complete {
89                events,
90                durable_len,
91            } => (events, durable_len, false),
92            ScanOutcome::PartialTail {
93                events,
94                durable_len,
95            } => (events, durable_len, true),
96        };
97        if partial && options.recovery == RecoveryPolicy::Strict {
98            return Err(MemoryError::CorruptLog {
99                offset: durable_len,
100                reason: "partial trailing batch".to_owned(),
101            });
102        }
103        if partial {
104            file.set_len(durable_len)
105                .map_err(|error| io("truncate partial event batch", error))?;
106            sync(&file, options.durability)?;
107        }
108        file.seek(SeekFrom::End(0))
109            .map_err(|error| io("seek event log end", error))?;
110        Ok(Self {
111            path,
112            file,
113            codec,
114            options,
115            inner: InMemoryStore::restore(events)?,
116            durable_len,
117        })
118    }
119
120    #[must_use]
121    pub fn path(&self) -> &Path {
122        &self.path
123    }
124
125    fn persist(&mut self, events: &[StoredEvent<E>]) -> Result<()> {
126        if events.is_empty() {
127            return Ok(());
128        }
129        let actual_len = self
130            .file
131            .metadata()
132            .map_err(|error| io("read event log metadata", error))?
133            .len();
134        if actual_len != self.durable_len {
135            return Err(MemoryError::ExternalModification);
136        }
137        let frame = frame::encode_batch(events, &self.codec, self.options.max_frame_bytes)?;
138        self.file
139            .seek(SeekFrom::Start(self.durable_len))
140            .map_err(|error| io("seek append position", error))?;
141        if let Err(error) = self.file.write_all(&frame) {
142            self.rollback_tail()?;
143            return Err(io("append event batch", error));
144        }
145        if let Err(error) = sync(&self.file, self.options.durability) {
146            self.rollback_tail()?;
147            return Err(error);
148        }
149        self.durable_len = self
150            .durable_len
151            .checked_add(u64::try_from(frame.len()).map_err(|_| MemoryError::CapacityOverflow)?)
152            .ok_or(MemoryError::CapacityOverflow)?;
153        Ok(())
154    }
155
156    fn rollback_tail(&mut self) -> Result<()> {
157        self.file
158            .set_len(self.durable_len)
159            .map_err(|error| io("rollback partial event batch", error))?;
160        self.file
161            .seek(SeekFrom::Start(self.durable_len))
162            .map_err(|error| io("seek after rollback", error))?;
163        sync(&self.file, self.options.durability)
164    }
165}
166
167impl<E, C> EventStore<E> for FileEventStore<E, C>
168where
169    E: Clone,
170    C: Codec<StoredEvent<E>>,
171{
172    fn append(
173        &mut self,
174        stream: &StreamId,
175        expected: ExpectedVersion,
176        events: &[NewEvent<E>],
177    ) -> Result<Vec<StoredEvent<E>>> {
178        let committed = self.inner.prepare_append(stream, expected, events)?;
179        self.persist(&committed)?;
180        self.inner.commit_prepared(&committed);
181        Ok(committed)
182    }
183
184    fn append_owned(
185        &mut self,
186        stream: &StreamId,
187        expected: ExpectedVersion,
188        events: Vec<NewEvent<E>>,
189    ) -> Result<Vec<StoredEvent<E>>> {
190        let committed = self.inner.prepare_append_owned(stream, expected, events)?;
191        self.persist(&committed)?;
192        self.inner.commit_prepared(&committed);
193        Ok(committed)
194    }
195
196    fn load_stream(&self, stream: &StreamId, after: Option<u64>) -> Vec<StoredEvent<E>> {
197        self.inner.load_stream(stream, after)
198    }
199
200    fn load_all(&self, after: Option<u64>, limit: usize) -> Vec<StoredEvent<E>> {
201        self.inner.load_all(after, limit)
202    }
203
204    fn stream_version(&self, stream: &StreamId) -> Option<u64> {
205        self.inner.stream_version(stream)
206    }
207
208    fn len(&self) -> usize {
209        self.inner.len()
210    }
211}
212
213fn sync(file: &File, durability: Durability) -> Result<()> {
214    match durability {
215        Durability::Flush => Ok(()),
216        Durability::SyncData => file
217            .sync_data()
218            .map_err(|error| io("sync event log", error)),
219    }
220}
221
222#[allow(clippy::needless_pass_by_value)]
223fn io(operation: &'static str, error: std::io::Error) -> MemoryError {
224    MemoryError::Io {
225        operation,
226        message: error.to_string(),
227    }
228}