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;
6
7use super::{Store, StoreError, StoreState};
8
9#[cfg(test)]
10pub(crate) struct TeardownProbe {
11    sequence: AtomicU64,
12    background_stopped: AtomicU64,
13    snapshot_released: AtomicU64,
14}
15
16#[cfg(test)]
17impl TeardownProbe {
18    pub(crate) const fn new() -> Self {
19        Self {
20            sequence: AtomicU64::new(0),
21            background_stopped: AtomicU64::new(0),
22            snapshot_released: AtomicU64::new(0),
23        }
24    }
25
26    fn record_background_stopped(&self) {
27        let event = self.sequence.fetch_add(1, Ordering::SeqCst) + 1;
28        let _ =
29            self.background_stopped
30                .compare_exchange(0, event, Ordering::SeqCst, Ordering::SeqCst);
31    }
32
33    pub(crate) fn record_snapshot_released(&self) {
34        let event = self.sequence.fetch_add(1, Ordering::SeqCst) + 1;
35        let _ =
36            self.snapshot_released
37                .compare_exchange(0, event, Ordering::SeqCst, Ordering::SeqCst);
38    }
39
40    pub(crate) fn events(&self) -> (u64, u64) {
41        (
42            self.background_stopped.load(Ordering::SeqCst),
43            self.snapshot_released.load(Ordering::SeqCst),
44        )
45    }
46}
47
48struct BackgroundControl {
49    stop: Mutex<bool>,
50    changed: Condvar,
51}
52
53pub(crate) struct BackgroundThread {
54    control: Arc<BackgroundControl>,
55    handle: Option<JoinHandle<()>>,
56    #[cfg(test)]
57    teardown_probe: Option<Arc<TeardownProbe>>,
58}
59
60impl BackgroundThread {
61    pub(crate) fn start() -> Result<Self, StoreError> {
62        static NEXT_THREAD: AtomicU64 = AtomicU64::new(1);
63
64        let control = Arc::new(BackgroundControl {
65            stop: Mutex::new(false),
66            changed: Condvar::new(),
67        });
68        let worker_control = Arc::clone(&control);
69        let (ready_tx, ready_rx) = mpsc::sync_channel(0);
70        let thread_id = NEXT_THREAD.fetch_add(1, Ordering::Relaxed);
71        let handle = std::thread::Builder::new()
72            .name(format!("ze-lifecycle-{thread_id}"))
73            .spawn(move || {
74                if ready_tx.send(()).is_err() {
75                    return;
76                }
77                let Ok(mut stop) = worker_control.stop.lock() else {
78                    return;
79                };
80                while !*stop {
81                    let Ok(next) = worker_control.changed.wait(stop) else {
82                        return;
83                    };
84                    stop = next;
85                }
86            })
87            .map_err(|source| StoreError::BackgroundStart { source })?;
88        if ready_rx.recv().is_err() {
89            let mut background = Self {
90                control,
91                handle: Some(handle),
92                #[cfg(test)]
93                teardown_probe: None,
94            };
95            background.stop_best_effort();
96            return Err(StoreError::BackgroundHandshake);
97        }
98        Ok(Self {
99            control,
100            handle: Some(handle),
101            #[cfg(test)]
102            teardown_probe: None,
103        })
104    }
105
106    #[cfg(test)]
107    pub(crate) fn set_teardown_probe(&mut self, probe: Arc<TeardownProbe>) {
108        self.teardown_probe = Some(probe);
109    }
110
111    fn stop_and_join(mut self) -> Result<(), StoreError> {
112        {
113            let mut stop = self
114                .control
115                .stop
116                .lock()
117                .map_err(|_| StoreError::Synchronization {
118                    component: "lifecycle thread",
119                })?;
120            *stop = true;
121            self.control.changed.notify_all();
122        }
123        let Some(handle) = self.handle.take() else {
124            return Ok(());
125        };
126        let result = handle
127            .join()
128            .map_err(|_| StoreError::BackgroundThreadPanicked);
129        #[cfg(test)]
130        if result.is_ok()
131            && let Some(probe) = &self.teardown_probe
132        {
133            probe.record_background_stopped();
134        }
135        result
136    }
137
138    fn stop_best_effort(&mut self) {
139        if let Ok(mut stop) = self.control.stop.lock() {
140            *stop = true;
141            self.control.changed.notify_all();
142        }
143        if let Some(handle) = self.handle.take() {
144            let _ = handle.join();
145        }
146        #[cfg(test)]
147        if let Some(probe) = &self.teardown_probe {
148            probe.record_background_stopped();
149        }
150    }
151}
152
153impl Drop for BackgroundThread {
154    fn drop(&mut self) {
155        self.stop_best_effort();
156    }
157}
158
159impl Store {
160    /// Synchronously tears down this handle and releases writer ownership.
161    ///
162    /// Concurrent callers wait for the one `Open -> Closing -> Closed`
163    /// transition and observe the same completed close. Admitted reads may run
164    /// through the configured bounded drain grace period. At that deadline
165    /// their published snapshot is marked cancelled, and close waits for those
166    /// cooperative reads to observe cancellation and release their `Arc`
167    /// before it unmaps segments. Calling `close` after completion is
168    /// successful and has no further effect.
169    pub fn close(&self) -> Result<(), StoreError> {
170        let mut state = self
171            .state
172            .lock()
173            .map_err(|_| StoreError::Synchronization { component: "state" })?;
174        loop {
175            match *state {
176                StoreState::Open => {
177                    *state = StoreState::Closing;
178                    break;
179                }
180                StoreState::Closing => {
181                    state = self
182                        .state_changed
183                        .wait(state)
184                        .map_err(|_| StoreError::Synchronization { component: "state" })?;
185                }
186                StoreState::Closed => return Ok(()),
187            }
188        }
189        drop(state);
190
191        let mut background = self
192            .background
193            .lock()
194            .map_err(|_| StoreError::Synchronization {
195                component: "lifecycle thread",
196            })?;
197        let stopped_background = background.take();
198        drop(background);
199        let background_result = stopped_background.map_or(Ok(()), |thread| thread.stop_and_join());
200
201        let mut snapshot = self
202            .snapshot
203            .write()
204            .map_err(|_| StoreError::Synchronization {
205                component: "published snapshot",
206            })?;
207        let released_snapshot = snapshot.take();
208        drop(snapshot);
209        if let Some(snapshot) = released_snapshot.as_ref() {
210            snapshot.drain_readers(self.reader_drain_timeout)?;
211        }
212        let mut query_pool = self
213            .query_pool
214            .lock()
215            .map_err(|_| StoreError::Synchronization {
216                component: "query pool",
217            })?;
218        let stopped_query_pool = query_pool.take();
219        drop(query_pool);
220        let query_pool_result = stopped_query_pool.map_or(Ok(()), |pool| pool.stop_and_join());
221        let mut lexical_worker =
222            self.lexical_worker
223                .lock()
224                .map_err(|_| StoreError::Synchronization {
225                    component: "pooled lexical worker",
226                })?;
227        let stopped_lexical_worker = lexical_worker.take();
228        drop(lexical_worker);
229        let lexical_worker_result =
230            stopped_lexical_worker.map_or(Ok(()), |worker| worker.stop_and_join());
231        // Admission has stopped and readers/workers have drained. Drop only
232        // the cache owner; any retained query assembly keeps its own charge.
233        let lexical_cache_result = self
234            .lexical_index_cache
235            .entry
236            .lock()
237            .map(|mut entry| {
238                drop(entry.take());
239            })
240            .map_err(|_| StoreError::Synchronization {
241                component: "lexical index cache",
242            });
243        drop(released_snapshot);
244
245        let mut active = self
246            .active
247            .lock()
248            .map_err(|_| StoreError::Synchronization {
249                component: "active segment",
250            })?;
251        let released_active = active.take();
252        drop(active);
253        drop(released_active);
254
255        let mut wal_writer = self
256            .wal_writer
257            .lock()
258            .map_err(|_| StoreError::Synchronization {
259                component: "WAL writer",
260            })?;
261        let released_wal = wal_writer.take();
262        drop(wal_writer);
263        drop(released_wal);
264
265        let mut writer_lock = self
266            .writer_lock
267            .lock()
268            .map_err(|_| StoreError::Synchronization {
269                component: "writer lock",
270            })?;
271        let released = writer_lock.take();
272        drop(writer_lock);
273        drop(released);
274
275        let mut state = self
276            .state
277            .lock()
278            .map_err(|_| StoreError::Synchronization { component: "state" })?;
279        *state = StoreState::Closed;
280        self.state_changed.notify_all();
281        background_result
282            .and(query_pool_result)
283            .and(lexical_worker_result)
284            .and(lexical_cache_result)
285    }
286
287    /// Runs close ordering during `Drop` without propagating failures or
288    /// waiting on reader cooperation. Existing leases are cancelled and keep
289    /// their snapshot mapping alive only until the lease itself is dropped.
290    pub(crate) fn close_best_effort(&mut self) {
291        let state = match self.state.get_mut() {
292            Ok(state) => state,
293            Err(poisoned) => poisoned.into_inner(),
294        };
295        if *state == StoreState::Closed {
296            return;
297        }
298        *state = StoreState::Closing;
299
300        let background_slot = match self.background.get_mut() {
301            Ok(background) => background,
302            Err(poisoned) => poisoned.into_inner(),
303        };
304        if let Some(mut background) = background_slot.take() {
305            background.stop_best_effort();
306        }
307
308        let snapshot_slot = match self.snapshot.get_mut() {
309            Ok(snapshot) => snapshot,
310            Err(poisoned) => poisoned.into_inner(),
311        };
312        let released_snapshot = snapshot_slot.take();
313        if let Some(snapshot) = released_snapshot.as_ref() {
314            snapshot.cancel_readers();
315        }
316
317        let query_pool_slot = match self.query_pool.get_mut() {
318            Ok(query_pool) => query_pool,
319            Err(poisoned) => poisoned.into_inner(),
320        };
321        if let Some(pool) = query_pool_slot.take() {
322            let _ = pool.stop_and_join();
323        }
324        let lexical_worker_slot = match self.lexical_worker.get_mut() {
325            Ok(worker) => worker,
326            Err(poisoned) => poisoned.into_inner(),
327        };
328        if let Some(worker) = lexical_worker_slot.take() {
329            worker.stop_best_effort();
330        }
331        let lexical_cache = match self.lexical_index_cache.entry.get_mut() {
332            Ok(cache) => cache,
333            Err(poisoned) => poisoned.into_inner(),
334        };
335        drop(lexical_cache.take());
336        drop(released_snapshot);
337
338        let active_slot = match self.active.get_mut() {
339            Ok(active) => active,
340            Err(poisoned) => poisoned.into_inner(),
341        };
342        drop(active_slot.take());
343
344        let wal_slot = match self.wal_writer.get_mut() {
345            Ok(wal) => wal,
346            Err(poisoned) => poisoned.into_inner(),
347        };
348        drop(wal_slot.take());
349
350        let writer_slot = match self.writer_lock.get_mut() {
351            Ok(writer_lock) => writer_lock,
352            Err(poisoned) => poisoned.into_inner(),
353        };
354        let released = writer_slot.take();
355        drop(released);
356        *state = StoreState::Closed;
357        self.state_changed.notify_all();
358    }
359}
360
361#[cfg(test)]
362#[allow(clippy::expect_used)]
363mod tests {
364    use std::sync::Arc;
365
366    use tempfile::tempdir;
367
368    use crate::lifecycle::{OpenOptions, Store};
369
370    #[test]
371    fn background_thread_stops_before_snapshot_is_released_on_drop() {
372        let directory = tempdir().expect("store directory");
373        let store = Store::open(directory.path(), OpenOptions::default()).expect("open");
374        let probe = Arc::clone(&store.teardown_probe);
375
376        drop(store);
377
378        let (background_stopped, snapshot_released) = probe.events();
379        assert!(background_stopped > 0, "background stop was not observed");
380        assert!(snapshot_released > 0, "snapshot release was not observed");
381        assert!(
382            background_stopped < snapshot_released,
383            "snapshot released at event {snapshot_released} before background stopped at event {background_stopped}"
384        );
385    }
386}