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::{
28 Arc,
29 atomic::{AtomicBool, Ordering},
30 },
31};
32use tokio::sync::Mutex;
34use super::{RecoveryEntry, RecoveryPage, RecoverySnapshot, StateBackend, StorageStateSettings};
36use crate::storage::{ReadLimits, RecoveryPosition, StorageBatch, StorageFuture, StoreHistory};
37use crate::{BackendFailure, Error, Result};
38use files::JournalOpener;
39use journal::StateLog;
40
41pub struct FileBackend {
50 root: PathBuf,
51 history: StoreHistory,
52 settings: StorageStateSettings,
53 opener: JournalOpener,
54 journal: Arc<Mutex<Option<StateLog>>>,
55}
56
57struct CancelOpen(Arc<AtomicBool>);
58
59impl Drop for CancelOpen {
66 fn drop(&mut self) {
67 self.0.store(true, Ordering::Release);
68 }
69}
70
71impl FileBackend {
72 pub fn new(root: PathBuf, history: StoreHistory, settings: StorageStateSettings) -> Self {
73 Self::with_journal_opener(root, history, settings, |path| {
74 OpenOptions::new().create(true).truncate(false).read(true).write(true).open(path)
75 })
76 }
77
78 pub fn with_journal_opener(
81 root: PathBuf, history: StoreHistory, settings: StorageStateSettings,
82 opener: impl Fn(&Path) -> std::io::Result<File> + Send + Sync + 'static,
83 ) -> Self {
84 let opener = Arc::new(opener);
85 let journal = Mutex::new(None);
86 let journal = Arc::new(journal);
87 Self { root, history, settings, opener, journal }
88 }
89
90 #[tracing::instrument(name = "storage.state.file.mod.call", level = "debug", skip_all)]
91 async fn call<T: Send + 'static>(&self, action: impl FnOnce(&mut StateLog) -> Result<T> + Send + 'static) -> Result<T> {
92 let mut owner = tracing::Instrument::instrument(self.journal.clone().lock_owned(), tracing::trace_span!("storage.lock.wait")).await;
94 {
95 let context = crate::execution::Context::current();
96 tokio::task::spawn_blocking(move || {
97 context.blocking(|| {
98 let journal = owner.as_mut().ok_or_else(|| format::invalid("state backend is closed"))?;
99 journal.check()?;
100 action(journal)
101 })
102 })
103 }
104 .await
105 .map_err(task)?
106 }
107}
108
109impl StateBackend for FileBackend {
110 fn history(&self) -> &StoreHistory {
111 &self.history
112 }
113
114 fn open(&self) -> StorageFuture<'_, RecoveryPosition> {
115 Box::pin(async move {
116 let context = crate::execution::Context::current();
117 let future = async move {
118 let span = tracing::debug_span!("storage.state.backend", method = "open", outcome = "cancelled");
119 let result = tracing::Instrument::instrument(
120 async move {
121 let mut owner =
122 tracing::Instrument::instrument(self.journal.clone().lock_owned(), tracing::trace_span!("storage.lock.wait"))
123 .await;
124 let root = self.root.clone();
125 let history = self.history.clone();
126 let settings = self.settings;
127 let opener = Arc::clone(&self.opener);
128 let cancelled = Arc::new(AtomicBool::new(false));
129 let _cancel = CancelOpen(Arc::clone(&cancelled));
130 {
131 let context = crate::execution::Context::current();
132 tokio::task::spawn_blocking(move || {
133 context.blocking(|| {
134 if owner.is_none() {
135 *owner = Some(StateLog::open(&root, history, settings, opener, &mut || {
136 if cancelled.load(Ordering::Acquire) {
137 Err(Error::Closing { domain: "cancelled state recovery" })
138 } else {
139 Ok(())
140 }
141 })?);
142 }
143 let journal = owner.as_ref().expect("opened journal");
144 journal.check()?;
145 Ok(journal.position())
146 })
147 })
148 }
149 .await
150 .map_err(task)?
151 },
152 span.clone(),
153 )
154 .await;
155 span.record("outcome", if result.is_ok() { "completed" } else { "failed" });
156 result
157 };
158 context.scope(future).await
159 })
160 }
161
162 fn read_snapshot(&self, maximum: NonZeroUsize) -> StorageFuture<'_, Option<RecoverySnapshot>> {
163 Box::pin(async move {
164 let context = crate::execution::Context::current();
165 let future = async move {
166 let span = tracing::debug_span!("storage.state.backend", method = "read_snapshot", outcome = "cancelled");
167 let result = tracing::Instrument::instrument(self.call(move |journal| journal.snapshot(maximum.get())), span.clone()).await;
168 span.record("outcome", if result.is_ok() { "completed" } else { "failed" });
169 result
170 };
171 context.scope(future).await
172 })
173 }
174
175 fn write_snapshot(&self, snapshot: StorageBatch<RecoverySnapshot>) -> StorageFuture<'_, ()> {
176 Box::pin(async move {
177 let context = crate::execution::Context::current();
178 let future = async move {
179 let span = tracing::debug_span!("storage.state.backend", method = "write_snapshot", outcome = "cancelled");
180 let result = tracing::Instrument::instrument(
181 async move {
182 if snapshot.len() != 1 {
183 return Err(format::invalid("snapshot write requires exactly one snapshot"));
184 }
185 self.history.check(&snapshot.as_slice()[0].through.history)?;
186 self.call(move |journal| journal.checkpoint(&snapshot.as_slice()[0])).await
187 },
188 span.clone(),
189 )
190 .await;
191 span.record("outcome", if result.is_ok() { "completed" } else { "failed" });
192 result
193 };
194 context.scope(future).await
195 })
196 }
197
198 fn read_entries<'a>(
199 &'a self, after: &'a RecoveryPosition, through: &'a RecoveryPosition, limits: ReadLimits,
200 ) -> StorageFuture<'a, RecoveryPage> {
201 Box::pin(async move {
202 let context = crate::execution::Context::current();
203 let future = async move {
204 let span = tracing::debug_span!("storage.state.backend", method = "read_entries", outcome = "cancelled");
205 let result = tracing::Instrument::instrument(
206 async move {
207 self.history.check(&after.history)?;
208 self.history.check(&through.history)?;
209 let after = after.clone();
210 let through = through.clone();
211 self.call(move |journal| journal.page(&after, &through, limits)).await
212 },
213 span.clone(),
214 )
215 .await;
216 span.record("outcome", if result.is_ok() { "completed" } else { "failed" });
217 result
218 };
219 context.scope(future).await
220 })
221 }
222
223 fn write_entries(&self, entries: StorageBatch<RecoveryEntry>) -> StorageFuture<'_, ()> {
224 Box::pin(async move {
225 let context = crate::execution::Context::current();
226 let future = async move {
227 let span = tracing::debug_span!("storage.state.backend", method = "write_entries", outcome = "cancelled");
228 let result = tracing::Instrument::instrument(
229 async move {
230 for entry in entries.as_slice() {
231 self.history.check(&entry.position.history)?;
232 }
233 self.call(move |journal| journal.append(entries.as_slice())).await
234 },
235 span.clone(),
236 )
237 .await;
238 span.record("outcome", if result.is_ok() { "completed" } else { "failed" });
239 result
240 };
241 context.scope(future).await
242 })
243 }
244
245 fn flush<'a>(&'a self, through: &'a RecoveryPosition) -> StorageFuture<'a, RecoveryPosition> {
246 Box::pin(async move {
247 let context = crate::execution::Context::current();
248 let future = async move {
249 let span = tracing::debug_span!("storage.state.backend", method = "flush", outcome = "cancelled");
250 let result = tracing::Instrument::instrument(
251 async move {
252 self.history.check(&through.history)?;
253 let through = through.clone();
254 self.call(move |journal| journal.flush(&through)).await
255 },
256 span.clone(),
257 )
258 .await;
259 span.record("outcome", if result.is_ok() { "completed" } else { "failed" });
260 result
261 };
262 context.scope(future).await
263 })
264 }
265
266 fn close(&self) -> StorageFuture<'_, ()> {
267 Box::pin(async move {
268 let context = crate::execution::Context::current();
269 let future = async move {
270 let span = tracing::debug_span!("storage.state.backend", method = "close", outcome = "cancelled");
271 let result = tracing::Instrument::instrument(
272 async move {
273 let mut owner =
274 tracing::Instrument::instrument(self.journal.clone().lock_owned(), tracing::trace_span!("storage.lock.wait"))
275 .await;
276 {
277 let context = crate::execution::Context::current();
278 tokio::task::spawn_blocking(move || {
279 context.blocking(|| {
280 let Some(mut journal) = owner.take() else {
281 return Ok(());
282 };
283 journal.check()?;
284 journal.flush(&journal.position())?;
285 Ok(())
286 })
287 })
288 }
289 .await
290 .map_err(task)?
291 },
292 span.clone(),
293 )
294 .await;
295 span.record("outcome", if result.is_ok() { "completed" } else { "failed" });
296 result
297 };
298 context.scope(future).await
299 })
300 }
301}
302
303fn task(error: tokio::task::JoinError) -> Error {
308 Error::backend("state file task", BackendFailure::Permanent, error)
309}
310
311