zeppelin_embed/lifecycle/
close.rs1use std::sync::atomic::{AtomicU64, Ordering};
4use std::sync::{Arc, Condvar, Mutex, mpsc};
5use std::thread::JoinHandle;
6use std::time::Instant;
7
8use super::{Store, StoreError, StoreState};
9
10#[cfg(test)]
11pub(crate) struct TeardownProbe {
12 sequence: AtomicU64,
13 background_stopped: AtomicU64,
14 snapshot_released: AtomicU64,
15}
16
17#[cfg(test)]
18impl TeardownProbe {
19 pub(crate) const fn new() -> Self {
20 Self {
21 sequence: AtomicU64::new(0),
22 background_stopped: AtomicU64::new(0),
23 snapshot_released: AtomicU64::new(0),
24 }
25 }
26
27 fn record_background_stopped(&self) {
28 let event = self.sequence.fetch_add(1, Ordering::SeqCst) + 1;
29 let _ =
30 self.background_stopped
31 .compare_exchange(0, event, Ordering::SeqCst, Ordering::SeqCst);
32 }
33
34 pub(crate) fn record_snapshot_released(&self) {
35 let event = self.sequence.fetch_add(1, Ordering::SeqCst) + 1;
36 let _ =
37 self.snapshot_released
38 .compare_exchange(0, event, Ordering::SeqCst, Ordering::SeqCst);
39 }
40
41 pub(crate) fn events(&self) -> (u64, u64) {
42 (
43 self.background_stopped.load(Ordering::SeqCst),
44 self.snapshot_released.load(Ordering::SeqCst),
45 )
46 }
47}
48
49struct BackgroundControl {
50 stop: Mutex<bool>,
51 changed: Condvar,
52}
53
54pub(crate) struct BackgroundThread {
55 control: Arc<BackgroundControl>,
56 handle: Option<JoinHandle<()>>,
57 #[cfg(test)]
58 teardown_probe: Option<Arc<TeardownProbe>>,
59}
60
61impl BackgroundThread {
62 pub(crate) fn start() -> Result<Self, StoreError> {
63 static NEXT_THREAD: AtomicU64 = AtomicU64::new(1);
64
65 let control = Arc::new(BackgroundControl {
66 stop: Mutex::new(false),
67 changed: Condvar::new(),
68 });
69 let worker_control = Arc::clone(&control);
70 let (ready_tx, ready_rx) = mpsc::sync_channel(0);
71 let thread_id = NEXT_THREAD.fetch_add(1, Ordering::Relaxed);
72 let handle = std::thread::Builder::new()
73 .name(format!("ze-lifecycle-{thread_id}"))
74 .spawn(move || {
75 if ready_tx.send(()).is_err() {
76 return;
77 }
78 let Ok(mut stop) = worker_control.stop.lock() else {
79 return;
80 };
81 while !*stop {
82 let Ok(next) = worker_control.changed.wait(stop) else {
83 return;
84 };
85 stop = next;
86 }
87 })
88 .map_err(|source| StoreError::BackgroundStart { source })?;
89 if ready_rx.recv().is_err() {
90 let mut background = Self {
91 control,
92 handle: Some(handle),
93 #[cfg(test)]
94 teardown_probe: None,
95 };
96 background.stop_best_effort();
97 return Err(StoreError::BackgroundHandshake);
98 }
99 Ok(Self {
100 control,
101 handle: Some(handle),
102 #[cfg(test)]
103 teardown_probe: None,
104 })
105 }
106
107 #[cfg(test)]
108 pub(crate) fn set_teardown_probe(&mut self, probe: Arc<TeardownProbe>) {
109 self.teardown_probe = Some(probe);
110 }
111
112 fn stop_and_join(mut self) -> Result<(), StoreError> {
113 {
114 let mut stop = self
115 .control
116 .stop
117 .lock()
118 .map_err(|_| StoreError::Synchronization {
119 component: "lifecycle thread",
120 })?;
121 *stop = true;
122 self.control.changed.notify_all();
123 }
124 let Some(handle) = self.handle.take() else {
125 return Ok(());
126 };
127 let result = handle
128 .join()
129 .map_err(|_| StoreError::BackgroundThreadPanicked);
130 #[cfg(test)]
131 if result.is_ok()
132 && let Some(probe) = &self.teardown_probe
133 {
134 probe.record_background_stopped();
135 }
136 result
137 }
138
139 fn stop_best_effort(&mut self) {
140 if let Ok(mut stop) = self.control.stop.lock() {
141 *stop = true;
142 self.control.changed.notify_all();
143 }
144 if let Some(handle) = self.handle.take() {
145 let _ = handle.join();
146 }
147 #[cfg(test)]
148 if let Some(probe) = &self.teardown_probe {
149 probe.record_background_stopped();
150 }
151 }
152}
153
154impl Drop for BackgroundThread {
155 fn drop(&mut self) {
156 self.stop_best_effort();
157 }
158}
159
160impl Store {
161 pub fn close(&self) -> Result<(), StoreError> {
171 self.close_with_final_writer(|| {})
172 }
173
174 pub(crate) fn close_with_final_writer(
178 &self,
179 before_close: impl FnOnce(),
180 ) -> Result<(), StoreError> {
181 let mut state = self
182 .state
183 .lock()
184 .map_err(|_| StoreError::Synchronization { component: "state" })?;
185 loop {
186 match *state {
187 StoreState::Open => {
188 *state = StoreState::Closing;
189 break;
190 }
191 StoreState::Closing => {
192 state = self
193 .state_changed
194 .wait(state)
195 .map_err(|_| StoreError::Synchronization { component: "state" })?;
196 }
197 StoreState::Closed => return Ok(()),
198 }
199 }
200 drop(state);
201 let reader_deadline = Instant::now().checked_add(self.reader_drain_timeout);
202 before_close();
203
204 let mut background = self
205 .background
206 .lock()
207 .map_err(|_| StoreError::Synchronization {
208 component: "lifecycle thread",
209 })?;
210 let stopped_background = background.take();
211 drop(background);
212 let background_result = stopped_background.map_or(Ok(()), |thread| thread.stop_and_join());
213
214 #[cfg(feature = "graph-cypher")]
215 let native_graph_result = self
216 .native_graph
217 .drain_writer_for_close()
218 .and_then(|()| self.native_graph.drain_and_clear(reader_deadline));
219 #[cfg(not(feature = "graph-cypher"))]
220 let native_graph_result = Ok(());
221
222 let mut snapshot = self
223 .snapshot
224 .write()
225 .map_err(|_| StoreError::Synchronization {
226 component: "published snapshot",
227 })?;
228 let released_snapshot = snapshot.take();
229 drop(snapshot);
230 if let Some(snapshot) = released_snapshot.as_ref() {
231 let remaining = reader_deadline
232 .map(|deadline| deadline.saturating_duration_since(Instant::now()))
233 .unwrap_or_default();
234 snapshot.drain_readers(remaining)?;
235 }
236 let mut query_pool = self
237 .query_pool
238 .lock()
239 .map_err(|_| StoreError::Synchronization {
240 component: "query pool",
241 })?;
242 let stopped_query_pool = query_pool.take();
243 drop(query_pool);
244 let query_pool_result = stopped_query_pool.map_or(Ok(()), |pool| pool.stop_and_join());
245 let mut lexical_worker =
246 self.lexical_worker
247 .lock()
248 .map_err(|_| StoreError::Synchronization {
249 component: "pooled lexical worker",
250 })?;
251 let stopped_lexical_worker = lexical_worker.take();
252 drop(lexical_worker);
253 let lexical_worker_result =
254 stopped_lexical_worker.map_or(Ok(()), |worker| worker.stop_and_join());
255 let lexical_cache_result = self
258 .lexical_index_cache
259 .entry
260 .lock()
261 .map(|mut entry| {
262 drop(entry.take());
263 })
264 .map_err(|_| StoreError::Synchronization {
265 component: "lexical index cache",
266 });
267 drop(released_snapshot);
268
269 let mut active = self
270 .active
271 .lock()
272 .map_err(|_| StoreError::Synchronization {
273 component: "active segment",
274 })?;
275 let released_active = active.take();
276 drop(active);
277 drop(released_active);
278 self.snapshot_pin
279 .lock()
280 .map_err(|_| StoreError::Synchronization {
281 component: "snapshot pin",
282 })?
283 .take();
284
285 let mut wal_writer = self
286 .wal_writer
287 .lock()
288 .map_err(|_| StoreError::Synchronization {
289 component: "WAL writer",
290 })?;
291 let released_wal = wal_writer.take();
292 drop(wal_writer);
293 drop(released_wal);
294
295 let mut writer_lock = self
296 .writer_lock
297 .lock()
298 .map_err(|_| StoreError::Synchronization {
299 component: "writer lock",
300 })?;
301 let released = writer_lock.take();
302 drop(writer_lock);
303 drop(released);
304 drop(
305 self.logical_writer_lock
306 .lock()
307 .map_err(|_| StoreError::Synchronization {
308 component: "logical writer lock",
309 })?
310 .take(),
311 );
312
313 let mut state = self
314 .state
315 .lock()
316 .map_err(|_| StoreError::Synchronization { component: "state" })?;
317 self.reclamation_pin
318 .lock()
319 .map_err(|_| StoreError::Synchronization {
320 component: "reclamation pin",
321 })?
322 .take();
323 *state = StoreState::Closed;
324 self.state_changed.notify_all();
325 background_result
326 .and(native_graph_result)
327 .and(query_pool_result)
328 .and(lexical_worker_result)
329 .and(lexical_cache_result)
330 }
331
332 pub(crate) fn close_best_effort(&mut self) {
336 let state = match self.state.get_mut() {
337 Ok(state) => state,
338 Err(poisoned) => poisoned.into_inner(),
339 };
340 if *state == StoreState::Closed {
341 return;
342 }
343 *state = StoreState::Closing;
344
345 let background_slot = match self.background.get_mut() {
346 Ok(background) => background,
347 Err(poisoned) => poisoned.into_inner(),
348 };
349 if let Some(mut background) = background_slot.take() {
350 background.stop_best_effort();
351 }
352
353 #[cfg(feature = "graph-cypher")]
354 self.native_graph.cancel_and_clear_best_effort();
355
356 let snapshot_slot = match self.snapshot.get_mut() {
357 Ok(snapshot) => snapshot,
358 Err(poisoned) => poisoned.into_inner(),
359 };
360 let released_snapshot = snapshot_slot.take();
361 if let Some(snapshot) = released_snapshot.as_ref() {
362 snapshot.cancel_readers();
363 }
364
365 let query_pool_slot = match self.query_pool.get_mut() {
366 Ok(query_pool) => query_pool,
367 Err(poisoned) => poisoned.into_inner(),
368 };
369 if let Some(pool) = query_pool_slot.take() {
370 let _ = pool.stop_and_join();
371 }
372 let lexical_worker_slot = match self.lexical_worker.get_mut() {
373 Ok(worker) => worker,
374 Err(poisoned) => poisoned.into_inner(),
375 };
376 if let Some(worker) = lexical_worker_slot.take() {
377 worker.stop_best_effort();
378 }
379 let lexical_cache = match self.lexical_index_cache.entry.get_mut() {
380 Ok(cache) => cache,
381 Err(poisoned) => poisoned.into_inner(),
382 };
383 drop(lexical_cache.take());
384 drop(released_snapshot);
385
386 let active_slot = match self.active.get_mut() {
387 Ok(active) => active,
388 Err(poisoned) => poisoned.into_inner(),
389 };
390 drop(active_slot.take());
391
392 let wal_slot = match self.wal_writer.get_mut() {
393 Ok(wal) => wal,
394 Err(poisoned) => poisoned.into_inner(),
395 };
396 drop(wal_slot.take());
397
398 let writer_slot = match self.writer_lock.get_mut() {
399 Ok(writer_lock) => writer_lock,
400 Err(poisoned) => poisoned.into_inner(),
401 };
402 let released = writer_slot.take();
403 drop(released);
404 let logical = match self.logical_writer_lock.get_mut() {
405 Ok(slot) => slot,
406 Err(poisoned) => poisoned.into_inner(),
407 };
408 drop(logical.take());
409 let pin = match self.reclamation_pin.get_mut() {
410 Ok(slot) => slot,
411 Err(poisoned) => poisoned.into_inner(),
412 };
413 drop(pin.take());
414 *state = StoreState::Closed;
415 self.state_changed.notify_all();
416 }
417}
418
419#[cfg(test)]
420#[allow(clippy::expect_used)]
421mod tests {
422 use std::sync::Arc;
423
424 use tempfile::tempdir;
425
426 use crate::lifecycle::{OpenOptions, Store};
427
428 #[test]
429 fn background_thread_stops_before_snapshot_is_released_on_drop() {
430 let directory = tempdir().expect("store directory");
431 let store = Store::open(directory.path(), OpenOptions::default()).expect("open");
432 let probe = Arc::clone(&store.teardown_probe);
433
434 drop(store);
435
436 let (background_stopped, snapshot_released) = probe.events();
437 assert!(background_stopped > 0, "background stop was not observed");
438 assert!(snapshot_released > 0, "snapshot release was not observed");
439 assert!(
440 background_stopped < snapshot_released,
441 "snapshot released at event {snapshot_released} before background stopped at event {background_stopped}"
442 );
443 }
444}