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 #[cfg(feature = "graph-cypher")]
305 drop(
306 self.reader_store_lock
307 .lock()
308 .map_err(|_| StoreError::Synchronization {
309 component: "reader store lock",
310 })?
311 .take(),
312 );
313 drop(
314 self.logical_writer_lock
315 .lock()
316 .map_err(|_| StoreError::Synchronization {
317 component: "logical writer lock",
318 })?
319 .take(),
320 );
321
322 let mut state = self
323 .state
324 .lock()
325 .map_err(|_| StoreError::Synchronization { component: "state" })?;
326 self.reclamation_pin
327 .lock()
328 .map_err(|_| StoreError::Synchronization {
329 component: "reclamation pin",
330 })?
331 .take();
332 *state = StoreState::Closed;
333 self.state_changed.notify_all();
334 background_result
335 .and(native_graph_result)
336 .and(query_pool_result)
337 .and(lexical_worker_result)
338 .and(lexical_cache_result)
339 }
340
341 pub(crate) fn close_best_effort(&mut self) {
345 let state = match self.state.get_mut() {
346 Ok(state) => state,
347 Err(poisoned) => poisoned.into_inner(),
348 };
349 if *state == StoreState::Closed {
350 return;
351 }
352 *state = StoreState::Closing;
353
354 let background_slot = match self.background.get_mut() {
355 Ok(background) => background,
356 Err(poisoned) => poisoned.into_inner(),
357 };
358 if let Some(mut background) = background_slot.take() {
359 background.stop_best_effort();
360 }
361
362 #[cfg(feature = "graph-cypher")]
363 self.native_graph.cancel_and_clear_best_effort();
364
365 let snapshot_slot = match self.snapshot.get_mut() {
366 Ok(snapshot) => snapshot,
367 Err(poisoned) => poisoned.into_inner(),
368 };
369 let released_snapshot = snapshot_slot.take();
370 if let Some(snapshot) = released_snapshot.as_ref() {
371 snapshot.cancel_readers();
372 }
373
374 let query_pool_slot = match self.query_pool.get_mut() {
375 Ok(query_pool) => query_pool,
376 Err(poisoned) => poisoned.into_inner(),
377 };
378 if let Some(pool) = query_pool_slot.take() {
379 let _ = pool.stop_and_join();
380 }
381 let lexical_worker_slot = match self.lexical_worker.get_mut() {
382 Ok(worker) => worker,
383 Err(poisoned) => poisoned.into_inner(),
384 };
385 if let Some(worker) = lexical_worker_slot.take() {
386 worker.stop_best_effort();
387 }
388 let lexical_cache = match self.lexical_index_cache.entry.get_mut() {
389 Ok(cache) => cache,
390 Err(poisoned) => poisoned.into_inner(),
391 };
392 drop(lexical_cache.take());
393 drop(released_snapshot);
394
395 let active_slot = match self.active.get_mut() {
396 Ok(active) => active,
397 Err(poisoned) => poisoned.into_inner(),
398 };
399 drop(active_slot.take());
400
401 let wal_slot = match self.wal_writer.get_mut() {
402 Ok(wal) => wal,
403 Err(poisoned) => poisoned.into_inner(),
404 };
405 drop(wal_slot.take());
406
407 let writer_slot = match self.writer_lock.get_mut() {
408 Ok(writer_lock) => writer_lock,
409 Err(poisoned) => poisoned.into_inner(),
410 };
411 let released = writer_slot.take();
412 drop(released);
413 #[cfg(feature = "graph-cypher")]
414 {
415 let reader = match self.reader_store_lock.get_mut() {
416 Ok(slot) => slot,
417 Err(poisoned) => poisoned.into_inner(),
418 };
419 drop(reader.take());
420 }
421 let logical = match self.logical_writer_lock.get_mut() {
422 Ok(slot) => slot,
423 Err(poisoned) => poisoned.into_inner(),
424 };
425 drop(logical.take());
426 let pin = match self.reclamation_pin.get_mut() {
427 Ok(slot) => slot,
428 Err(poisoned) => poisoned.into_inner(),
429 };
430 drop(pin.take());
431 *state = StoreState::Closed;
432 self.state_changed.notify_all();
433 }
434}
435
436#[cfg(test)]
437#[allow(clippy::expect_used)]
438mod tests {
439 use std::sync::Arc;
440
441 use tempfile::tempdir;
442
443 use crate::lifecycle::{OpenOptions, Store};
444
445 #[test]
446 fn background_thread_stops_before_snapshot_is_released_on_drop() {
447 let directory = tempdir().expect("store directory");
448 let store = Store::open(directory.path(), OpenOptions::default()).expect("open");
449 let probe = Arc::clone(&store.teardown_probe);
450
451 drop(store);
452
453 let (background_stopped, snapshot_released) = probe.events();
454 assert!(background_stopped > 0, "background stop was not observed");
455 assert!(snapshot_released > 0, "snapshot release was not observed");
456 assert!(
457 background_stopped < snapshot_released,
458 "snapshot released at event {snapshot_released} before background stopped at event {background_stopped}"
459 );
460 }
461}