mj_controller/database/
writer.rs1use super::*;
2
3pub(super) const DATABASE_WRITE_QUEUE_CAPACITY: usize = 256;
4
5pub(super) type DatabaseWriteJob = Box<dyn FnOnce(Result<&mut Connection>) + Send + 'static>;
9
10pub(super) enum DatabaseWriterMessage {
11 Run {
12 label: &'static str,
13 job: DatabaseWriteJob,
14 },
15 Shutdown,
16}
17
18#[derive(Clone)]
24pub struct DatabaseWriter {
25 pub(super) id: u64,
26 pub(super) sender: SyncSender<DatabaseWriterMessage>,
27 path: Arc<PathBuf>,
28 committed: tokio::sync::watch::Sender<std::result::Result<CommittedState, Arc<str>>>,
29}
30
31impl std::fmt::Debug for DatabaseWriter {
32 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 formatter
34 .debug_struct("DatabaseWriter")
35 .field("id", &self.id)
36 .finish_non_exhaustive()
37 }
38}
39
40impl DatabaseWriter {
41 pub fn committed_state(&self) -> Result<CommittedState> {
42 self.committed
43 .borrow()
44 .clone()
45 .map_err(|error| anyhow::anyhow!("{error}"))
46 }
47
48 pub fn committed_changes(
49 &self,
50 ) -> tokio::sync::watch::Receiver<std::result::Result<CommittedState, Arc<str>>> {
51 self.committed.subscribe()
52 }
53
54 pub(super) fn execute<T, F>(&self, label: &'static str, operation: F) -> Result<T>
55 where
56 T: Send + 'static,
57 F: FnOnce(&mut Connection) -> Result<T> + Send + 'static,
58 {
59 let (reply_tx, reply_rx) = sync_channel(1);
60 let publication = self.clone();
61 self.sender
62 .send(DatabaseWriterMessage::Run {
63 label,
64 job: Box::new(move |connection| {
65 let reply = match connection {
66 Ok(connection) => (|| {
67 let previous = publication.committed_state()?;
68 committed::begin_operation(&publication.path);
69 let result = operation(connection);
70 match committed::finish_operation(connection, &previous) {
71 Ok(Some(next)) => { drop(publication.committed.send_replace(Ok(next))); }
72 Ok(None) => {}
73 Err(error) => {
74 let detail: Arc<str> = format!(
78 "database publication failed after {label}; a commit may have occurred, do not replay this mutation: {error:#}"
79 ).into();
80 drop(publication.committed.send_replace(Err(detail.clone())));
81 bail!("{detail}");
82 }
83 }
84 result
85 })(),
86 Err(error) => Err(error),
90 };
91 let _ = reply_tx.send(reply);
92 }),
93 })
94 .map_err(|_| {
95 anyhow::anyhow!("submit database writer operation {label}: writer stopped")
96 })?;
97 reply_rx
98 .recv()
99 .with_context(|| format!("database writer stopped during {label}"))?
100 }
101}
102
103pub struct DatabaseWriterOwner {
109 pub(super) writer: DatabaseWriter,
110 pub(super) thread: Option<JoinHandle<()>>,
111 pub(super) stopped: Receiver<Result<()>>,
112}
113
114impl std::fmt::Debug for DatabaseWriterOwner {
115 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 formatter
117 .debug_struct("DatabaseWriterOwner")
118 .field("writer", &self.writer)
119 .finish_non_exhaustive()
120 }
121}
122
123impl DatabaseWriterOwner {
124 pub fn shutdown(mut self) -> Result<()> {
125 self.shutdown_inner()
126 }
127
128 pub(super) fn shutdown_inner(&mut self) -> Result<()> {
129 if self.thread.is_none() {
130 return Ok(());
131 }
132 clear_database_writer(self.writer.id);
133 let send_result = self.writer.sender.send(DatabaseWriterMessage::Shutdown);
134 let worker_result = self
135 .stopped
136 .recv()
137 .context("database writer stopped without reporting its result")?;
138 let join_result = self
139 .thread
140 .take()
141 .expect("database writer thread checked above")
142 .join();
143 if let Err(panic) = join_result {
144 std::panic::resume_unwind(panic);
145 }
146 match (send_result, worker_result) {
147 (_, Err(error)) => Err(error),
148 (Err(_), Ok(())) => bail!("request database writer shutdown: writer stopped"),
149 (Ok(()), Ok(())) => Ok(()),
150 }
151 }
152}
153
154impl Drop for DatabaseWriterOwner {
155 fn drop(&mut self) {
156 if let Err(error) = self.shutdown_inner() {
157 tracing::error!(%error, "database writer did not shut down cleanly");
158 }
159 }
160}
161
162pub(super) fn database_writer_slot() -> &'static Mutex<Option<DatabaseWriter>> {
163 static WRITER: OnceLock<Mutex<Option<DatabaseWriter>>> = OnceLock::new();
164 WRITER.get_or_init(|| Mutex::new(None))
165}
166
167pub(crate) fn database_writer_installed() -> bool {
171 database_writer_slot()
172 .lock()
173 .unwrap_or_else(PoisonError::into_inner)
174 .is_some()
175}
176
177pub fn committed_state() -> Result<Option<CommittedState>> {
180 let writer = database_writer_slot()
181 .lock()
182 .unwrap_or_else(PoisonError::into_inner)
183 .clone();
184 writer.map(|writer| writer.committed_state()).transpose()
185}
186
187pub fn subscribe_committed_state()
188-> Result<tokio::sync::watch::Receiver<std::result::Result<CommittedState, Arc<str>>>> {
189 let writer = database_writer_slot()
190 .lock()
191 .unwrap_or_else(PoisonError::into_inner)
192 .clone()
193 .context("database writer is not installed")?;
194 Ok(writer.committed_changes())
195}
196
197pub(super) fn clear_database_writer(id: u64) {
198 let mut installed = database_writer_slot()
199 .lock()
200 .unwrap_or_else(PoisonError::into_inner);
201 if installed.as_ref().is_some_and(|writer| writer.id == id) {
202 *installed = None;
203 }
204}
205
206#[doc(hidden)]
222#[must_use = "the writer stops when this owner is dropped"]
223pub fn install_isolated_test_writer() -> DatabaseWriterOwner {
224 start_database_writer().expect("install the writer for an isolated test child")
225}
226
227pub fn start_database_writer() -> Result<DatabaseWriterOwner> {
228 start_database_writer_at(&database_path(), true)
229}
230
231pub(super) fn start_database_writer_at(
232 path: &Path,
233 install_globally: bool,
234) -> Result<DatabaseWriterOwner> {
235 static NEXT_WRITER_ID: AtomicU64 = AtomicU64::new(1);
236
237 let mut connection = schema::open_writer(path)?;
238 let mut observed_revision = schema::read_schema_state(&connection)?.revision;
239 let initial = CommittedState::bootstrap(&mut connection)?;
240 let (committed, _) = tokio::sync::watch::channel(Ok(initial));
241 let path = path.to_owned();
242 let (sender, receiver) = sync_channel(DATABASE_WRITE_QUEUE_CAPACITY);
243 let (stopped_tx, stopped) = sync_channel(1);
244 let id = NEXT_WRITER_ID.fetch_add(1, Ordering::Relaxed);
245 let writer = DatabaseWriter {
246 id,
247 sender,
248 path: Arc::new(path.clone()),
249 committed,
250 };
251 if install_globally {
252 let mut installed = database_writer_slot()
253 .lock()
254 .unwrap_or_else(PoisonError::into_inner);
255 ensure!(installed.is_none(), "database writer is already running");
256 *installed = Some(writer.clone());
257 }
258 let publication = writer.clone();
259 let thread = match thread::Builder::new()
260 .name("hel-database-writer".to_owned())
261 .spawn(move || {
262 let mut connection = connection;
263 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
264 loop {
265 match receiver.recv() {
266 Ok(DatabaseWriterMessage::Run { label, job }) => {
267 tracing::trace!(operation = label, "running database writer operation");
268 match writer_schema_state(
271 &path,
272 &connection,
273 label,
274 &mut observed_revision,
275 ) {
276 Ok(()) => job(Ok(&mut connection)),
277 Err(error) => job(Err(error)),
278 }
279 if let Err(error) = publication.committed_state() {
280 break Err(error);
281 }
282 }
283 Ok(DatabaseWriterMessage::Shutdown) => break Ok(()),
284 Err(error) => {
285 break Err(error).context("database writer queue disconnected");
286 }
287 }
288 }
289 }))
290 .unwrap_or_else(|panic| {
291 let detail = panic
292 .downcast_ref::<&str>()
293 .copied()
294 .or_else(|| panic.downcast_ref::<String>().map(String::as_str))
295 .unwrap_or("unknown panic payload");
296 Err(anyhow::anyhow!("database writer thread panicked: {detail}"))
297 });
298 if let Err(error) = &result {
299 drop(
300 publication
301 .committed
302 .send_replace(Err(format!("database writer stopped: {error:#}").into())),
303 );
304 }
305 if result.is_ok() {
306 clear_database_writer(id);
307 }
308 let _ = stopped_tx.send(result);
309 }) {
310 Ok(thread) => thread,
311 Err(error) => {
312 if install_globally {
313 clear_database_writer(id);
314 }
315 return Err(error).context("spawn database writer thread");
316 }
317 };
318 Ok(DatabaseWriterOwner {
319 writer,
320 thread: Some(thread),
321 stopped,
322 })
323}
324
325pub(super) fn writer_schema_state(
327 path: &Path,
328 connection: &Connection,
329 label: &'static str,
330 observed_revision: &mut i64,
331) -> Result<()> {
332 let result: Result<()> = (|| {
333 let state = schema::read_schema_state(connection)?;
334 if state.revision < *observed_revision {
335 return Err(StoreSchemaMismatch {
336 found: state.revision,
337 supported: SCHEMA_VERSION,
338 reason: StoreSchemaMismatchReason::Rollback {
339 previous: *observed_revision,
340 },
341 }
342 .into());
343 }
344 *observed_revision = state.revision;
345 state.ensure_supported()
346 })();
347 if let Err(error) = &result {
348 tracing::error!(
349 operation = label,
350 path = %path.display(),
351 error = %error,
352 "could not establish store compatibility; refusing the operation"
353 );
354 }
355 result.with_context(|| {
356 format!(
357 "check database compatibility before {label} at {}",
358 path.display()
359 )
360 })
361}
362
363pub(super) fn submit_database_write<T, F>(label: &'static str, operation: F) -> Result<T>
364where
365 T: Send + 'static,
366 F: FnOnce(&mut Connection) -> Result<T> + Send + 'static,
367{
368 let writer = database_writer_slot()
369 .lock()
370 .unwrap_or_else(PoisonError::into_inner)
371 .clone();
372 if let Some(writer) = writer {
373 writer.execute(label, operation)
374 } else {
375 bail!("database writer is not available for operation {label}")
383 }
384}