harmos_storage/state/file/
mod.rs1mod files;
15mod format;
16mod journal;
17
18use std::{
24 fs::{File, OpenOptions},
25 num::NonZeroUsize,
26 path::{Path, PathBuf},
27 sync::Arc,
28};
29use tokio::sync::Mutex;
31use super::{RecoveryEntry, RecoveryPage, RecoverySnapshot, StateBackend, StorageStateSettings};
33use crate::storage::{ReadLimits, RecoveryPosition, StorageBatch, StorageFuture, StoreHistory};
34use crate::{BackendFailure, Error, Result};
35use files::JournalOpener;
36use journal::StateLog;
37
38pub struct FileBackend {
47 root: PathBuf,
48 history: StoreHistory,
49 settings: StorageStateSettings,
50 opener: JournalOpener,
51 journal: Arc<Mutex<Option<StateLog>>>,
52}
53
54impl FileBackend {
61 pub fn new(root: PathBuf, history: StoreHistory, settings: StorageStateSettings) -> Self {
62 Self::with_journal_opener(root, history, settings, |path| {
63 OpenOptions::new().create(true).truncate(false).read(true).write(true).open(path)
64 })
65 }
66
67 pub fn with_journal_opener(
70 root: PathBuf, history: StoreHistory, settings: StorageStateSettings,
71 opener: impl Fn(&Path) -> std::io::Result<File> + Send + Sync + 'static,
72 ) -> Self {
73 let opener = Arc::new(opener);
74 let journal = Mutex::new(None);
75 let journal = Arc::new(journal);
76 Self { root, history, settings, opener, journal }
77 }
78
79 #[tracing::instrument(name = "storage.state.file.mod.call", level = "debug", skip_all)]
80 async fn call<T: Send + 'static>(&self, action: impl FnOnce(&mut StateLog) -> Result<T> + Send + 'static) -> Result<T> {
81 let mut owner = tracing::Instrument::instrument(self.journal.clone().lock_owned(), tracing::trace_span!("storage.lock.wait")).await;
83 {
84 let context = crate::execution::Context::current();
85 tokio::task::spawn_blocking(move || {
86 context.blocking(|| {
87 let journal = owner.as_mut().ok_or_else(|| format::invalid("state backend is closed"))?;
88 journal.check()?;
89 action(journal)
90 })
91 })
92 }
93 .await
94 .map_err(task)?
95 }
96 async fn read<T: Send + 'static>(
97 &self, memory: crate::arrow::RetainedMemory, base: usize,
98 action: impl FnOnce(&mut StateLog, &crate::arrow::RetainedMemory) -> Result<T> + Send + 'static,
99 ) -> Result<T> {
100 memory.try_grow_to(base.saturating_add(8192))?;
101 let mut owner = tracing::Instrument::instrument(self.journal.clone().lock_owned(), tracing::trace_span!("storage.lock.wait")).await;
102 let (result, _memory) = read_task(memory, move |memory| {
103 let journal = owner.as_mut().ok_or_else(|| format::invalid("state backend is closed"))?;
104 journal.check()?;
105 action(journal, memory)
106 })
107 .await
108 .map_err(task)?;
109 result
110 }
111}
112
113impl StateBackend for FileBackend {
114 fn history(&self) -> &StoreHistory {
115 &self.history
116 }
117
118 fn open(&self) -> StorageFuture<'_, RecoveryPosition> {
119 Box::pin(async move {
120 let context = crate::execution::Context::current();
121 let future = async move {
122 let span = tracing::debug_span!("storage.state.backend", method = "open", outcome = "cancelled");
123 let result = tracing::Instrument::instrument(
124 async move {
125 let mut owner =
126 tracing::Instrument::instrument(self.journal.clone().lock_owned(), tracing::trace_span!("storage.lock.wait"))
127 .await;
128 let root = self.root.clone();
129 let history = self.history.clone();
130 let settings = self.settings;
131 let opener = Arc::clone(&self.opener);
132 {
133 let context = crate::execution::Context::current();
134 tokio::task::spawn_blocking(move || {
135 context.blocking(|| {
136 if owner.is_none() {
137 *owner = Some(StateLog::open(&root, history, settings, opener)?);
138 }
139 let journal = owner.as_ref().expect("opened journal");
140 journal.check()?;
141 Ok(journal.position())
142 })
143 })
144 }
145 .await
146 .map_err(task)?
147 },
148 span.clone(),
149 )
150 .await;
151 span.record("outcome", if result.is_ok() { "completed" } else { "failed" });
152 result
153 };
154 context.scope(future).await
155 })
156 }
157
158 fn read_snapshot(&self, maximum: NonZeroUsize, memory: crate::arrow::RetainedMemory) -> StorageFuture<'_, Option<RecoverySnapshot>> {
159 Box::pin(async move {
160 let context = crate::execution::Context::current();
161 let future = async move {
162 let span = tracing::debug_span!("storage.state.backend", method = "read_snapshot", outcome = "cancelled");
163 let result = tracing::Instrument::instrument(
164 self.read(memory, crate::manager::ENTRY_OVERHEAD, move |journal, memory| {
165 journal.snapshot_retained(maximum.get(), memory, crate::manager::ENTRY_OVERHEAD)
166 }),
167 span.clone(),
168 )
169 .await;
170 span.record("outcome", if result.is_ok() { "completed" } else { "failed" });
171 result
172 };
173 context.scope(future).await
174 })
175 }
176
177 fn write_snapshot(&self, snapshot: StorageBatch<RecoverySnapshot>) -> StorageFuture<'_, ()> {
178 Box::pin(async move {
179 let context = crate::execution::Context::current();
180 let future = async move {
181 let span = tracing::debug_span!("storage.state.backend", method = "write_snapshot", outcome = "cancelled");
182 let result = tracing::Instrument::instrument(
183 async move {
184 if snapshot.len() != 1 {
185 return Err(format::invalid("snapshot write requires exactly one snapshot"));
186 }
187 self.history.check(&snapshot.as_slice()[0].through.history)?;
188 self.call(move |journal| journal.checkpoint(&snapshot.as_slice()[0])).await
189 },
190 span.clone(),
191 )
192 .await;
193 span.record("outcome", if result.is_ok() { "completed" } else { "failed" });
194 result
195 };
196 context.scope(future).await
197 })
198 }
199
200 fn read_entries<'a>(
201 &'a self, after: &'a RecoveryPosition, through: &'a RecoveryPosition, limits: ReadLimits, memory: crate::arrow::RetainedMemory,
202 ) -> StorageFuture<'a, RecoveryPage> {
203 Box::pin(async move {
204 let context = crate::execution::Context::current();
205 let future = async move {
206 let span = tracing::debug_span!("storage.state.backend", method = "read_entries", outcome = "cancelled");
207 let result = tracing::Instrument::instrument(
208 async move {
209 self.history.check(&after.history)?;
210 self.history.check(&through.history)?;
211 let base = crate::manager::ENTRY_OVERHEAD
212 .saturating_add(crate::MemorySize::memory_size(&after.history))
213 .saturating_add(crate::MemorySize::memory_size(&through.history));
214 memory.try_grow_to(base)?;
215 let after = after.clone();
216 let through = through.clone();
217 self.read(memory, base, move |journal, memory| journal.page(&after, &through, limits, memory, base)).await
218 },
219 span.clone(),
220 )
221 .await;
222 span.record("outcome", if result.is_ok() { "completed" } else { "failed" });
223 result
224 };
225 context.scope(future).await
226 })
227 }
228
229 fn write_entries(&self, entries: StorageBatch<RecoveryEntry>) -> StorageFuture<'_, ()> {
230 Box::pin(async move {
231 let context = crate::execution::Context::current();
232 let future = async move {
233 let span = tracing::debug_span!("storage.state.backend", method = "write_entries", outcome = "cancelled");
234 let result = tracing::Instrument::instrument(
235 async move {
236 for entry in entries.as_slice() {
237 self.history.check(&entry.position.history)?;
238 }
239 self.call(move |journal| journal.append(entries.as_slice())).await
240 },
241 span.clone(),
242 )
243 .await;
244 span.record("outcome", if result.is_ok() { "completed" } else { "failed" });
245 result
246 };
247 context.scope(future).await
248 })
249 }
250
251 fn flush<'a>(&'a self, through: &'a RecoveryPosition) -> StorageFuture<'a, RecoveryPosition> {
252 Box::pin(async move {
253 let context = crate::execution::Context::current();
254 let future = async move {
255 let span = tracing::debug_span!("storage.state.backend", method = "flush", outcome = "cancelled");
256 let result = tracing::Instrument::instrument(
257 async move {
258 self.history.check(&through.history)?;
259 let through = through.clone();
260 self.call(move |journal| journal.flush(&through)).await
261 },
262 span.clone(),
263 )
264 .await;
265 span.record("outcome", if result.is_ok() { "completed" } else { "failed" });
266 result
267 };
268 context.scope(future).await
269 })
270 }
271
272 fn close(&self) -> StorageFuture<'_, ()> {
273 Box::pin(async move {
274 let context = crate::execution::Context::current();
275 let future = async move {
276 let span = tracing::debug_span!("storage.state.backend", method = "close", outcome = "cancelled");
277 let result = tracing::Instrument::instrument(
278 async move {
279 let mut owner =
280 tracing::Instrument::instrument(self.journal.clone().lock_owned(), tracing::trace_span!("storage.lock.wait"))
281 .await;
282 {
283 let context = crate::execution::Context::current();
284 tokio::task::spawn_blocking(move || {
285 context.blocking(|| {
286 let Some(mut journal) = owner.take() else {
287 return Ok(());
288 };
289 journal.check()?;
290 journal.flush(&journal.position())?;
291 Ok(())
292 })
293 })
294 }
295 .await
296 .map_err(task)?
297 },
298 span.clone(),
299 )
300 .await;
301 span.record("outcome", if result.is_ok() { "completed" } else { "failed" });
302 result
303 };
304 context.scope(future).await
305 })
306 }
307}
308
309fn read_task<T: Send + 'static>(
314 memory: crate::arrow::RetainedMemory, action: impl FnOnce(&crate::arrow::RetainedMemory) -> Result<T> + Send + 'static,
315) -> tokio::task::JoinHandle<(Result<T>, crate::arrow::RetainedMemory)> {
316 let context = crate::execution::Context::current();
317 tokio::task::spawn_blocking(move || {
318 let result = context.blocking(|| action(&memory));
319 (result, memory)
320 })
321}
322
323fn task(error: tokio::task::JoinError) -> Error {
324 Error::backend("state file task", BackendFailure::Permanent, error)
325}
326
327#[cfg(test)]
329mod tests {
330 use super::*;
331 use crate::test_backend::{Data, Recovery, history, storage_settings};
332
333 fn budget() -> crate::StorageManager {
334 crate::StorageManager::new(
335 crate::StorageBackends { primary: Arc::new(Data::<()>::new(history())), state: Arc::new(Recovery::new(history())) },
336 storage_settings(),
337 )
338 .unwrap()
339 }
340
341 #[tokio::test]
342 async fn detached_read_retained() {
343 let budget = budget();
344 let memory = budget.read_memory(NonZeroUsize::new(8192).unwrap()).await.unwrap();
345 let (entered, ready) = tokio::sync::oneshot::channel();
346 let (release, gate) = std::sync::mpsc::channel();
347 let task = read_task(memory, move |_| {
348 let bytes = vec![7u8; 4096];
349 entered.send(()).unwrap();
350 gate.recv().unwrap();
351 Ok(bytes)
352 });
353 ready.await.unwrap();
354 drop(task);
355 assert_eq!(budget.status().read_bytes, 8192);
356 let mut changed = budget.inner.changed.subscribe();
357 release.send(()).unwrap();
358 while budget.status().read_bytes != 0 {
359 changed.changed().await.unwrap();
360 }
361 }
362
363 #[tokio::test]
364 async fn completed_read_retained() {
365 let budget = budget();
366 let memory = budget.read_memory(NonZeroUsize::new(8192).unwrap()).await.unwrap();
367 let task = read_task(memory, |_| Ok(vec![7u8; 4096]));
368 while !task.is_finished() {
369 tokio::task::yield_now().await;
370 }
371 assert_eq!(budget.status().read_bytes, 8192);
372 drop(task);
373 assert_eq!(budget.status().read_bytes, 0);
374 }
375}