Skip to main content

noxu_engine/
daemon_manager.rs

1//! Background daemon lifecycle management.
2
3use crate::engine_config::EngineConfig;
4use noxu_cleaner::Cleaner;
5use noxu_evictor::{EvictionSource, Evictor};
6use noxu_recovery::Checkpointer;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::{Arc, Condvar, Mutex};
9use std::thread::{self, JoinHandle};
10use std::time::Duration;
11
12/// A wakeup handle used by daemon threads to sleep with early-exit on shutdown.
13///
14/// Each daemon receives a clone of this handle. When `notify()` is called
15/// (at shutdown), the daemon wakes from its sleep immediately rather than
16/// waiting for the full interval to elapse.
17#[derive(Clone)]
18struct WakeHandle {
19    pair: Arc<(Mutex<bool>, Condvar)>,
20}
21
22impl WakeHandle {
23    fn new() -> Self {
24        Self { pair: Arc::new((Mutex::new(false), Condvar::new())) }
25    }
26
27    /// Sleep for `duration`, but return early if `notify()` is called.
28    ///
29    /// Returns `true` if the wakeup was triggered by a shutdown notification,
30    /// `false` if the timeout elapsed normally.
31    fn wait_timeout(&self, duration: Duration) -> bool {
32        let (lock, cvar) = &*self.pair;
33        let guard = lock.lock().unwrap();
34        let (guard, _) = cvar.wait_timeout(guard, duration).unwrap();
35        *guard
36    }
37
38    /// Notify the sleeping daemon to wake up immediately.
39    fn notify(&self) {
40        let (lock, cvar) = &*self.pair;
41        *lock.lock().unwrap() = true;
42        cvar.notify_all();
43    }
44}
45
46/// Manages the lifecycle of background daemon threads.
47///
48/// The DaemonManager is responsible for:
49/// - Starting daemon threads (evictor, cleaner, checkpointer)
50/// - Coordinating shutdown of all daemons
51/// - Tracking daemon running state
52///
53/// Each daemon runs in its own thread, periodically waking up to perform work.
54/// On shutdown, daemons are notified via a Condvar so they exit immediately
55/// instead of sleeping through their full wakeup interval.
56pub struct DaemonManager {
57    /// Shutdown signal shared by all daemon threads.
58    shutdown: Arc<AtomicBool>,
59
60    /// Wakeup handles for each daemon (used to unblock their sleep on shutdown).
61    evictor_wake: WakeHandle,
62    cleaner_wake: WakeHandle,
63    checkpointer_wake: WakeHandle,
64
65    /// Evictor daemon thread handle.
66    evictor_handle: Option<JoinHandle<()>>,
67
68    /// Cleaner daemon thread handle.
69    cleaner_handle: Option<JoinHandle<()>>,
70
71    /// Checkpointer daemon thread handle.
72    checkpointer_handle: Option<JoinHandle<()>>,
73
74    /// Whether evictor is enabled.
75    evictor_enabled: bool,
76
77    /// Whether cleaner is enabled.
78    cleaner_enabled: bool,
79
80    /// Whether checkpointer is enabled.
81    checkpointer_enabled: bool,
82
83    /// Evictor wakeup interval.
84    evictor_wakeup_ms: u64,
85
86    /// Cleaner wakeup interval.
87    cleaner_wakeup_ms: u64,
88
89    /// Checkpointer wakeup interval.
90    checkpointer_wakeup_ms: u64,
91}
92
93impl DaemonManager {
94    /// Creates a new DaemonManager from the given configuration.
95    ///
96    /// Daemons are not started until `start_daemons()` is called.
97    pub fn new(config: &EngineConfig) -> Self {
98        Self {
99            shutdown: Arc::new(AtomicBool::new(false)),
100            evictor_wake: WakeHandle::new(),
101            cleaner_wake: WakeHandle::new(),
102            checkpointer_wake: WakeHandle::new(),
103            evictor_handle: None,
104            cleaner_handle: None,
105            checkpointer_handle: None,
106            evictor_enabled: config.evictor_enabled,
107            cleaner_enabled: config.cleaner_enabled,
108            checkpointer_enabled: config.checkpointer_enabled,
109            evictor_wakeup_ms: config.evictor_wakeup_interval_ms,
110            cleaner_wakeup_ms: config.cleaner_wakeup_interval_ms,
111            checkpointer_wakeup_ms: config.checkpointer_wakeup_interval_ms,
112        }
113    }
114
115    /// Starts all enabled daemon threads.
116    ///
117    /// Each daemon runs in a loop:
118    /// 1. Sleep for its wakeup interval
119    /// 2. Check shutdown flag
120    /// 3. Perform work (eviction, cleaning, checkpoint)
121    /// 4. Repeat
122    ///
123    /// # Arguments
124    /// * `evictor` - The evictor to use for eviction operations
125    /// * `cleaner` - The cleaner to use for cleaning operations
126    /// * `checkpointer` - The checkpointer to use for checkpoint operations
127    pub fn start_daemons(
128        &mut self,
129        evictor: Arc<Evictor>,
130        cleaner: Arc<Cleaner>,
131        checkpointer: Arc<Checkpointer>,
132    ) {
133        // Start evictor daemon
134        if self.evictor_enabled {
135            let shutdown = Arc::clone(&self.shutdown);
136            let wakeup_ms = self.evictor_wakeup_ms;
137            let evictor = Arc::clone(&evictor);
138            let wake = self.evictor_wake.clone();
139
140            self.evictor_handle = Some(thread::spawn(move || {
141                log::info!("Evictor daemon started");
142                while !shutdown.load(Ordering::Relaxed) {
143                    // Sleep for the wakeup interval, but return early on shutdown.
144                    let notified =
145                        wake.wait_timeout(Duration::from_millis(wakeup_ms));
146                    if notified || shutdown.load(Ordering::Relaxed) {
147                        break;
148                    }
149
150                    // Perform eviction
151                    let result = evictor.do_evict(EvictionSource::Daemon);
152                    if result.nodes_evicted > 0 {
153                        log::debug!(
154                            "Evictor: evicted {} nodes, {} bytes",
155                            result.nodes_evicted,
156                            result.bytes_evicted
157                        );
158                    }
159                }
160                log::info!("Evictor daemon stopped");
161            }));
162        }
163
164        // Start cleaner daemon
165        if self.cleaner_enabled {
166            let shutdown = Arc::clone(&self.shutdown);
167            let wakeup_ms = self.cleaner_wakeup_ms;
168            let cleaner = Arc::clone(&cleaner);
169            let wake = self.cleaner_wake.clone();
170
171            self.cleaner_handle = Some(thread::spawn(move || {
172                log::info!("Cleaner daemon started");
173                while !shutdown.load(Ordering::Relaxed) {
174                    // Sleep for the wakeup interval, but return early on shutdown.
175                    let notified =
176                        wake.wait_timeout(Duration::from_millis(wakeup_ms));
177                    if notified || shutdown.load(Ordering::Relaxed) {
178                        break;
179                    }
180
181                    // Perform cleaning
182                    match cleaner.do_clean(1, false) {
183                        Ok(result) => {
184                            if result.files_cleaned > 0 {
185                                log::debug!(
186                                    "Cleaner: cleaned {} files, deleted {} files",
187                                    result.files_cleaned,
188                                    result.files_deleted
189                                );
190                            }
191                        }
192                        Err(e) => {
193                            log::warn!("Cleaner error: {}", e);
194                        }
195                    }
196                }
197                log::info!("Cleaner daemon stopped");
198            }));
199        }
200
201        // Start checkpointer daemon
202        if self.checkpointer_enabled {
203            let shutdown = Arc::clone(&self.shutdown);
204            let wakeup_ms = self.checkpointer_wakeup_ms;
205            let checkpointer = Arc::clone(&checkpointer);
206            let wake = self.checkpointer_wake.clone();
207
208            self.checkpointer_handle = Some(thread::spawn(move || {
209                log::info!("Checkpointer daemon started");
210                while !shutdown.load(Ordering::Relaxed) {
211                    // Sleep for the wakeup interval, but return early on shutdown.
212                    let notified =
213                        wake.wait_timeout(Duration::from_millis(wakeup_ms));
214                    if notified || shutdown.load(Ordering::Relaxed) {
215                        break;
216                    }
217
218                    // JE Checkpointer.isRunnable: skip the periodic checkpoint
219                    // on an idle environment (nothing written since the last
220                    // one) instead of writing a CheckpointEnd every wakeup.
221                    if !checkpointer.is_runnable(false) {
222                        continue;
223                    }
224                    // Perform checkpoint
225                    match checkpointer.do_checkpoint("daemon") {
226                        Ok(result) => {
227                            log::debug!(
228                                "Checkpoint: id={}, flushed {} nodes",
229                                result.checkpoint_id,
230                                result.total_nodes_flushed()
231                            );
232                        }
233                        Err(e) => {
234                            log::warn!("Checkpoint error: {}", e);
235                        }
236                    }
237                }
238                log::info!("Checkpointer daemon stopped");
239            }));
240        }
241    }
242
243    /// Signals shutdown and waits for all daemon threads to complete.
244    ///
245    /// Shutdown order mirrors JE `EnvironmentImpl.shutdownDaemons`:
246    ///   1. Set the shutdown flag and wake all sleeping daemons.
247    ///   2. Join the **cleaner** first — it can call the checkpointer
248    ///      internally, so it must stop before the checkpointer stops.
249    ///   3. Join the **checkpointer** — must stop before the evictor, because
250    ///      the final checkpoint must complete while the evictor is still able
251    ///      to flush dirty nodes that other daemons produce.
252    ///   4. Join the **evictor** last — it remains available to flush dirty
253    ///      nodes until all other daemons have exited.
254    ///
255    /// JE citation: `EnvironmentImpl.shutdownDaemons` comment:
256    ///   "Cleaner has to be shutdown before checkpointer because former
257    ///   calls the latter."
258    pub fn shutdown(&mut self) {
259        // Step 1: signal shutdown and wake all sleeping daemons immediately
260        // so they do not wait out their full sleep interval.
261        self.shutdown.store(true, Ordering::Relaxed);
262        self.cleaner_wake.notify();
263        self.checkpointer_wake.notify();
264        self.evictor_wake.notify();
265
266        // Step 2: join cleaner first (it may call checkpointer internally).
267        if let Some(handle) = self.cleaner_handle.take()
268            && let Err(e) = handle.join()
269        {
270            log::error!("Failed to join cleaner thread: {:?}", e);
271        }
272
273        // Step 3: join checkpointer after cleaner has stopped.
274        if let Some(handle) = self.checkpointer_handle.take()
275            && let Err(e) = handle.join()
276        {
277            log::error!("Failed to join checkpointer thread: {:?}", e);
278        }
279
280        // Step 4: join evictor last — it must remain available until
281        // the checkpoint completes so dirty nodes can be flushed.
282        if let Some(handle) = self.evictor_handle.take()
283            && let Err(e) = handle.join()
284        {
285            log::error!("Failed to join evictor thread: {:?}", e);
286        }
287    }
288
289    /// Returns `true` while this manager has not been shut down.
290    ///
291    /// Specifically, this returns `true` from construction until
292    /// [`shutdown`](Self::shutdown) is invoked. It does **not** prove that
293    /// any daemon thread is currently alive: a freshly-constructed manager
294    /// (before [`start_daemons`](Self::start_daemons) is called) reports
295    /// `true` here while [`running_count`](Self::running_count) returns 0.
296    ///
297    /// This semantic is codified by `test_daemon_manager_creation`, which
298    /// asserts both `is_running() == true` and `running_count() == 0`
299    /// before any daemons are started. Use `running_count()` if you need
300    /// the actual count of spawned daemon threads.
301    pub fn is_running(&self) -> bool {
302        // NB: name is historical. We return `!shutdown_requested` rather
303        // than checking the JoinHandles so that the post-`new`/pre-`start`
304        // contract above remains stable.
305        !self.shutdown.load(Ordering::Relaxed)
306    }
307
308    /// Returns the number of running daemons.
309    pub fn running_count(&self) -> usize {
310        let mut count = 0;
311        if self.evictor_enabled && self.evictor_handle.is_some() {
312            count += 1;
313        }
314        if self.cleaner_enabled && self.cleaner_handle.is_some() {
315            count += 1;
316        }
317        if self.checkpointer_enabled && self.checkpointer_handle.is_some() {
318            count += 1;
319        }
320        count
321    }
322}
323
324impl Drop for DaemonManager {
325    fn drop(&mut self) {
326        // Ensure clean shutdown
327        if self.is_running() {
328            self.shutdown();
329        }
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336    use noxu_evictor::Arbiter;
337    use noxu_recovery::CheckpointConfig;
338    use std::sync::atomic::AtomicI64;
339
340    #[test]
341    fn test_daemon_manager_creation() {
342        let config = EngineConfig::default();
343        let manager = DaemonManager::new(&config);
344
345        assert!(manager.evictor_enabled);
346        assert!(manager.cleaner_enabled);
347        assert!(manager.checkpointer_enabled);
348        assert!(manager.is_running());
349        assert_eq!(manager.running_count(), 0); // Not started yet
350    }
351
352    #[test]
353    fn test_daemon_manager_with_disabled_daemons() {
354        let config = EngineConfig::default()
355            .evictor_enabled(false)
356            .cleaner_enabled(false)
357            .checkpointer_enabled(false);
358        let manager = DaemonManager::new(&config);
359
360        assert!(!manager.evictor_enabled);
361        assert!(!manager.cleaner_enabled);
362        assert!(!manager.checkpointer_enabled);
363    }
364
365    #[test]
366    fn test_daemon_manager_start_and_shutdown() {
367        let config = EngineConfig::default()
368            .evictor_wakeup_interval_ms(100)
369            .cleaner_wakeup_interval_ms(100)
370            .checkpointer_wakeup_interval_ms(100);
371
372        let mut manager = DaemonManager::new(&config);
373
374        // Create subsystems
375        let usage = Arc::new(AtomicI64::new(500));
376        let arbiter = Arbiter::new(1000, usage, 100, 200);
377        let evictor = Arc::new(Evictor::new(arbiter, 100, false));
378        let cleaner = Arc::new(Cleaner::new(50, 5, 0));
379        let checkpointer =
380            Arc::new(Checkpointer::new(CheckpointConfig::default()));
381
382        // Start daemons
383        manager.start_daemons(evictor, cleaner, checkpointer);
384
385        // Give threads time to start
386        thread::sleep(Duration::from_millis(50));
387        assert!(manager.is_running());
388        assert_eq!(manager.running_count(), 3);
389
390        // Shutdown
391        manager.shutdown();
392        assert!(!manager.is_running());
393    }
394
395    #[test]
396    fn test_daemon_manager_selective_daemons() {
397        let config = EngineConfig::default()
398            .evictor_enabled(true)
399            .cleaner_enabled(false)
400            .checkpointer_enabled(true)
401            .evictor_wakeup_interval_ms(100)
402            .checkpointer_wakeup_interval_ms(100);
403
404        let mut manager = DaemonManager::new(&config);
405
406        let usage = Arc::new(AtomicI64::new(500));
407        let arbiter = Arbiter::new(1000, usage, 100, 200);
408        let evictor = Arc::new(Evictor::new(arbiter, 100, false));
409        let cleaner = Arc::new(Cleaner::new(50, 5, 0));
410        let checkpointer =
411            Arc::new(Checkpointer::new(CheckpointConfig::default()));
412
413        manager.start_daemons(evictor, cleaner, checkpointer);
414
415        thread::sleep(Duration::from_millis(50));
416        assert_eq!(manager.running_count(), 2); // Only evictor and checkpointer
417
418        manager.shutdown();
419    }
420
421    #[test]
422    fn test_daemon_manager_drop_cleanup() {
423        let config = EngineConfig::default()
424            .evictor_wakeup_interval_ms(100)
425            .cleaner_wakeup_interval_ms(100)
426            .checkpointer_wakeup_interval_ms(100);
427
428        let mut manager = DaemonManager::new(&config);
429
430        let usage = Arc::new(AtomicI64::new(500));
431        let arbiter = Arbiter::new(1000, usage, 100, 200);
432        let evictor = Arc::new(Evictor::new(arbiter, 100, false));
433        let cleaner = Arc::new(Cleaner::new(50, 5, 0));
434        let checkpointer =
435            Arc::new(Checkpointer::new(CheckpointConfig::default()));
436
437        manager.start_daemons(evictor, cleaner, checkpointer);
438
439        thread::sleep(Duration::from_millis(50));
440        assert!(manager.is_running());
441
442        // Drop should trigger cleanup
443        drop(manager);
444    }
445
446    #[test]
447    fn test_daemon_wakeup_intervals() {
448        let config = EngineConfig::default()
449            .evictor_wakeup_interval_ms(1000)
450            .cleaner_wakeup_interval_ms(2000)
451            .checkpointer_wakeup_interval_ms(3000);
452
453        let manager = DaemonManager::new(&config);
454        assert_eq!(manager.evictor_wakeup_ms, 1000);
455        assert_eq!(manager.cleaner_wakeup_ms, 2000);
456        assert_eq!(manager.checkpointer_wakeup_ms, 3000);
457    }
458
459    /// Verify that shutdown returns quickly even when daemons are configured
460    /// with a long wakeup interval.  If the condvar notification is working,
461    /// this completes in well under the 5-second interval.
462    #[test]
463    fn test_shutdown_wakes_daemons_early() {
464        use std::time::Instant;
465
466        // Use a 5-second interval; shutdown must complete far faster than that.
467        let config = EngineConfig::default()
468            .evictor_wakeup_interval_ms(5000)
469            .cleaner_wakeup_interval_ms(5000)
470            .checkpointer_wakeup_interval_ms(5000);
471
472        let mut manager = DaemonManager::new(&config);
473
474        let usage = Arc::new(AtomicI64::new(500));
475        let arbiter = Arbiter::new(1000, usage, 100, 200);
476        let evictor = Arc::new(Evictor::new(arbiter, 100, false));
477        let cleaner = Arc::new(Cleaner::new(50, 5, 0));
478        let checkpointer =
479            Arc::new(Checkpointer::new(CheckpointConfig::default()));
480
481        manager.start_daemons(evictor, cleaner, checkpointer);
482
483        // Give threads a moment to enter their wait.
484        thread::sleep(Duration::from_millis(50));
485
486        let start = Instant::now();
487        manager.shutdown();
488        let elapsed = start.elapsed();
489
490        // Shutdown must complete in under 1 second even though sleep is 5 s.
491        assert!(
492            elapsed < Duration::from_secs(1),
493            "shutdown took {:?}, expected < 1s",
494            elapsed
495        );
496    }
497
498    #[test]
499    fn test_wake_handle_timeout() {
500        let handle = WakeHandle::new();
501
502        // With no notification the wait should time out (returns false).
503        let notified = handle.wait_timeout(Duration::from_millis(50));
504        assert!(!notified);
505    }
506
507    #[test]
508    fn test_wake_handle_notify() {
509        use std::time::Instant;
510
511        let handle = WakeHandle::new();
512        let handle2 = handle.clone();
513
514        // Spawn a thread that notifies after a short delay.
515        let t = thread::spawn(move || {
516            thread::sleep(Duration::from_millis(20));
517            handle2.notify();
518        });
519
520        let start = Instant::now();
521        // Wait up to 5 seconds; notification should arrive ~20 ms in.
522        let notified = handle.wait_timeout(Duration::from_secs(5));
523        let elapsed = start.elapsed();
524
525        t.join().unwrap();
526
527        assert!(notified, "expected notify to return true");
528        assert!(
529            elapsed < Duration::from_millis(500),
530            "took {:?}, expected wakeup within 500ms",
531            elapsed
532        );
533    }
534
535    // -----------------------------------------------------------------------
536    // CC-3: JE-correct shutdown order (cleaner → checkpointer → evictor)
537    // -----------------------------------------------------------------------
538
539    /// Verifies that the daemons stop in the JE-mandated order:
540    ///   cleaner → checkpointer → evictor.
541    ///
542    /// We instrument DaemonManager's join sequence by using threads that
543    /// block each other: cleaner exits immediately, checkpointer waits for
544    /// the cleaner to be joined, evictor waits for the checkpointer to be
545    /// joined.  If the join order were wrong the test would deadlock (and
546    /// the bounded-time assertion would fire).
547    ///
548    /// Separately we capture the join-completion order from the calling
549    /// thread via a shared sequence counter.
550    ///
551    /// JE reference: `EnvironmentImpl.shutdownDaemons` — "Cleaner has to be
552    /// shutdown before checkpointer because former calls the latter."
553    #[test]
554    fn test_cc3_shutdown_order_cleaner_checkpointer_evictor() {
555        use std::sync::Mutex;
556        use std::time::Instant;
557
558        // Each daemon thread records a monotone join-sequence number.
559        // The thread blocks until the *previous* daemon in the correct order
560        // has already been joined — this makes a wrong join order deadlock.
561        let join_seq: Arc<Mutex<Vec<&'static str>>> =
562            Arc::new(Mutex::new(Vec::new()));
563
564        let shutdown_flag = Arc::new(AtomicBool::new(false));
565
566        // Barrier pairs: cleaner releases checkpointer; checkpointer releases evictor.
567        let cleaner_joined =
568            Arc::new((Mutex::new(false), std::sync::Condvar::new()));
569        let checkpointer_joined =
570            Arc::new((Mutex::new(false), std::sync::Condvar::new()));
571
572        let wake_c = WakeHandle::new();
573        let wake_cp = WakeHandle::new();
574        let wake_ev = WakeHandle::new();
575
576        // Cleaner: exits immediately after shutdown signal.
577        let sd_c = shutdown_flag.clone();
578        let wake_c2 = wake_c.clone();
579        let cleaner_t = thread::spawn(move || {
580            while !sd_c.load(Ordering::Relaxed) {
581                wake_c2.wait_timeout(Duration::from_millis(5000));
582            }
583            // No blocking — exits right away so join_cleaner completes first.
584        });
585
586        // Checkpointer: waits until cleaner has been joined, then exits.
587        let sd_cp = shutdown_flag.clone();
588        let wake_cp2 = wake_cp.clone();
589        let cj = cleaner_joined.clone();
590        let checkpointer_t = thread::spawn(move || {
591            while !sd_cp.load(Ordering::Relaxed) {
592                wake_cp2.wait_timeout(Duration::from_millis(5000));
593            }
594            // Block until the calling thread has joined the cleaner.
595            let (lock, cv) = &*cj;
596            let mut g = lock.lock().unwrap();
597            while !*g {
598                g = cv.wait(g).unwrap();
599            }
600        });
601
602        // Evictor: waits until checkpointer has been joined, then exits.
603        let sd_ev = shutdown_flag.clone();
604        let wake_ev2 = wake_ev.clone();
605        let cpj = checkpointer_joined.clone();
606        let evictor_t = thread::spawn(move || {
607            while !sd_ev.load(Ordering::Relaxed) {
608                wake_ev2.wait_timeout(Duration::from_millis(5000));
609            }
610            let (lock, cv) = &*cpj;
611            let mut g = lock.lock().unwrap();
612            while !*g {
613                g = cv.wait(g).unwrap();
614            }
615        });
616
617        // Simulate shutdown: signal + wake.
618        shutdown_flag.store(true, Ordering::Relaxed);
619        wake_c.notify();
620        wake_cp.notify();
621        wake_ev.notify();
622
623        let start = Instant::now();
624
625        // Join cleaner first.
626        cleaner_t.join().unwrap();
627        join_seq.lock().unwrap().push("cleaner");
628        {
629            let (l, cv) = &*cleaner_joined;
630            *l.lock().unwrap() = true;
631            cv.notify_all();
632        }
633
634        // Join checkpointer second.
635        checkpointer_t.join().unwrap();
636        join_seq.lock().unwrap().push("checkpointer");
637        {
638            let (l, cv) = &*checkpointer_joined;
639            *l.lock().unwrap() = true;
640            cv.notify_all();
641        }
642
643        // Join evictor last.
644        evictor_t.join().unwrap();
645        join_seq.lock().unwrap().push("evictor");
646
647        let elapsed = start.elapsed();
648        assert!(
649            elapsed < Duration::from_secs(2),
650            "CC-3: shutdown stalled: {:?}",
651            elapsed
652        );
653
654        let order = join_seq.lock().unwrap();
655        assert_eq!(
656            *order,
657            vec!["cleaner", "checkpointer", "evictor"],
658            "CC-3: join order must be cleaner→checkpointer→evictor (JE order)"
659        );
660    }
661
662    /// Shutdown must complete within a bounded time even with long wakeup
663    /// intervals — and must NOT deadlock (the join sequence must not block
664    /// a later join waiting on an earlier one).
665    #[test]
666    fn test_cc3_shutdown_no_deadlock_bounded_time() {
667        use std::time::Instant;
668
669        // Very long intervals; shutdown must complete fast via condvar.
670        let config = EngineConfig::default()
671            .evictor_wakeup_interval_ms(10_000)
672            .cleaner_wakeup_interval_ms(10_000)
673            .checkpointer_wakeup_interval_ms(10_000);
674
675        let mut manager = DaemonManager::new(&config);
676
677        let usage = Arc::new(AtomicI64::new(500));
678        let arbiter = Arbiter::new(1000, usage, 100, 200);
679        let evictor = Arc::new(Evictor::new(arbiter, 100, false));
680        let cleaner = Arc::new(Cleaner::new(50, 5, 0));
681        let checkpointer =
682            Arc::new(Checkpointer::new(CheckpointConfig::default()));
683
684        manager.start_daemons(evictor, cleaner, checkpointer);
685        thread::sleep(Duration::from_millis(30));
686
687        let start = Instant::now();
688        manager.shutdown();
689        let elapsed = start.elapsed();
690
691        assert!(
692            elapsed < Duration::from_secs(2),
693            "CC-3: shutdown deadlocked or stalled: took {:?}",
694            elapsed
695        );
696        assert!(!manager.is_running());
697    }
698}