Skip to main content

zeppelin_embed/lifecycle/
close.rs

1//! Ordered store teardown.
2
3use 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    /// Synchronously tears down this handle and releases writer ownership.
162    ///
163    /// Concurrent callers wait for the one `Open -> Closing -> Closed`
164    /// transition and observe the same completed close. Admitted reads may run
165    /// through the configured bounded drain grace period. At that deadline
166    /// their published snapshot is marked cancelled, and close waits for those
167    /// cooperative reads to observe cancellation and release their `Arc`
168    /// before it unmaps segments. Calling `close` after completion is
169    /// successful and has no further effect.
170    pub fn close(&self) -> Result<(), StoreError> {
171        self.close_with_final_writer(|| {})
172    }
173
174    /// Run one facade-owned final write after admission stops, before releasing
175    /// writer ownership. The facade retains any typed failure; teardown always
176    /// runs, and repeated/concurrent close never repeats the callback.
177    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        // Admission has stopped and readers/workers have drained. Drop only
256        // the cache owner; any retained query assembly keeps its own charge.
257        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    /// Runs close ordering during `Drop` without propagating failures or
342    /// waiting on reader cooperation. Existing leases are cancelled and keep
343    /// their snapshot mapping alive only until the lease itself is dropped.
344    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}