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        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    /// Runs close ordering during `Drop` without propagating failures or
333    /// waiting on reader cooperation. Existing leases are cancelled and keep
334    /// their snapshot mapping alive only until the lease itself is dropped.
335    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}