1use crate::daemon_manager::DaemonManager;
4use crate::engine_config::EngineConfig;
5use crate::env_stats::{
6 EnvironmentStats, EvictorStatsSnapshot, LockStatsSnapshot,
7 LogStatsSnapshot, TxnStatsSnapshot,
8};
9use crate::error::{EngineError, Result};
10use noxu_cleaner::{CleanResult, Cleaner};
11use noxu_dbi::EnvironmentImpl;
12use noxu_evictor::{Arbiter, EvictResult, EvictionSource, Evictor};
13use noxu_recovery::{
14 CheckpointConfig, CheckpointResult, Checkpointer, RecoveryManager,
15 log_scanner::InMemoryLogScanner,
16};
17use noxu_sync::Mutex;
18use std::sync::Arc;
19use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
20
21pub struct Engine {
33 config: EngineConfig,
35
36 env_impl: Arc<Mutex<EnvironmentImpl>>,
38
39 evictor: Arc<Evictor>,
41
42 cleaner: Arc<Cleaner>,
44
45 checkpointer: Arc<Checkpointer>,
47
48 daemon_manager: Mutex<DaemonManager>,
50
51 open: AtomicBool,
53
54 cache_usage: Arc<AtomicI64>,
56}
57
58impl Engine {
59 pub fn open(config: EngineConfig) -> Result<Self> {
77 config.validate().map_err(EngineError::InvalidConfig)?;
79
80 if config.allow_create && !config.home.exists() {
82 std::fs::create_dir_all(&config.home)?;
83 }
84
85 if !config.home.exists() {
87 return Err(EngineError::InvalidConfig(format!(
88 "environment directory does not exist: {}",
89 config.home.display()
90 )));
91 }
92
93 let env_impl = EnvironmentImpl::new(
95 &config.home,
96 config.read_only,
97 config.transactional,
98 )?;
99 let env_impl = Arc::new(Mutex::new(env_impl));
100
101 let cache_usage = Arc::new(AtomicI64::new(0));
103
104 let arbiter = Arbiter::new(
106 config.cache_size as i64,
107 Arc::clone(&cache_usage),
108 (config.cache_size / 10) as i64, (config.cache_size / 5) as i64, );
111
112 let evictor = Arc::new(Evictor::new(arbiter, 100, false));
114
115 let cleaner = Arc::new(Cleaner::new(
117 config.cleaner_min_utilization,
118 config.cleaner_min_file_count,
119 0, ));
121
122 let checkpoint_config = CheckpointConfig::default()
124 .bytes_interval(config.checkpoint_bytes_interval);
125 let checkpointer = Arc::new(Checkpointer::new(checkpoint_config));
126
127 let mut recovery_manager = RecoveryManager::new();
131 let mut scanner = InMemoryLogScanner::new();
132 log::info!("Running recovery...");
133 let recovery_info =
134 recovery_manager.recover(&mut scanner, None, true)?;
135 log::info!(
136 "Recovery completed: last_used_lsn={:?}, checkpoint_start_lsn={:?}",
137 recovery_info.last_used_lsn,
138 recovery_info.checkpoint_start_lsn
139 );
140
141 let mut daemon_manager = DaemonManager::new(&config);
143
144 daemon_manager.start_daemons(
146 Arc::clone(&evictor),
147 Arc::clone(&cleaner),
148 Arc::clone(&checkpointer),
149 );
150
151 let engine = Engine {
152 config,
153 env_impl,
154 evictor,
155 cleaner,
156 checkpointer,
157 daemon_manager: Mutex::new(daemon_manager),
158 open: AtomicBool::new(true),
159 cache_usage,
160 };
161
162 log::info!("Engine opened successfully");
163 Ok(engine)
164 }
165
166 pub fn close(&self) -> Result<()> {
179 if !self.is_open() {
180 return Err(EngineError::EnvironmentClosed);
181 }
182
183 log::info!("Closing engine...");
184
185 self.open.store(false, Ordering::Relaxed);
187
188 self.daemon_manager.lock().shutdown();
190
191 if !self.config.read_only
193 && self.config.checkpointer_enabled
194 && let Err(e) = self.checkpointer.do_checkpoint("close")
195 {
196 log::warn!("Final checkpoint failed: {}", e);
197 }
198
199 if let Err(e) = self.env_impl.lock().close() {
203 log::warn!("EnvironmentImpl close failed: {}", e);
204 }
205
206 log::info!("Engine closed successfully");
207 Ok(())
208 }
209
210 pub fn is_open(&self) -> bool {
212 self.open.load(Ordering::Relaxed)
213 }
214
215 pub fn get_env_impl(&self) -> &Arc<Mutex<EnvironmentImpl>> {
217 &self.env_impl
218 }
219
220 pub fn get_evictor(&self) -> &Arc<Evictor> {
222 &self.evictor
223 }
224
225 pub fn get_cleaner(&self) -> &Arc<Cleaner> {
227 &self.cleaner
228 }
229
230 pub fn get_checkpointer(&self) -> &Arc<Checkpointer> {
232 &self.checkpointer
233 }
234
235 pub fn get_config(&self) -> &EngineConfig {
237 &self.config
238 }
239
240 pub fn checkpoint(&self, invoker: &str) -> Result<CheckpointResult> {
254 if !self.is_open() {
255 return Err(EngineError::EnvironmentClosed);
256 }
257
258 if self.config.read_only {
259 return Err(EngineError::InvalidConfig(
260 "cannot checkpoint read-only environment".to_string(),
261 ));
262 }
263
264 let result = self.checkpointer.do_checkpoint(invoker)?;
265 Ok(result)
266 }
267
268 pub fn clean(&self, n_files: u32) -> Result<CleanResult> {
282 if !self.is_open() {
283 return Err(EngineError::EnvironmentClosed);
284 }
285
286 if self.config.read_only {
287 return Err(EngineError::InvalidConfig(
288 "cannot clean read-only environment".to_string(),
289 ));
290 }
291
292 let result = self
293 .cleaner
294 .do_clean(n_files, false)
295 .map_err(EngineError::DatabaseError)?;
296 Ok(result)
297 }
298
299 pub fn clean_adaptive(&self) -> Result<(CleanResult, u64)> {
313 if !self.is_open() {
314 return Err(EngineError::EnvironmentClosed);
315 }
316 if self.config.read_only {
317 return Err(EngineError::InvalidConfig(
318 "cannot clean read-only environment".to_string(),
319 ));
320 }
321
322 let bytes_written = {
324 let env_impl = self.env_impl.lock();
325 env_impl
326 .get_log_manager()
327 .map(|lm| lm.get_stats().n_sequential_write_bytes)
328 .unwrap_or(0)
329 };
330
331 let cleaning_needed =
333 self.cleaner.get_file_selector().lock().has_files_to_clean();
334
335 let (sleep_ms, n_files) =
336 self.cleaner.throttle.update(bytes_written, cleaning_needed);
337
338 let backlog =
341 self.cleaner.get_file_selector().lock().get_stats().to_be_cleaned;
342 self.cleaner.throttle.set_backlog(backlog as u64);
343
344 let result = self
345 .cleaner
346 .do_clean(n_files, false)
347 .map_err(EngineError::DatabaseError)?;
348
349 Ok((result, sleep_ms))
350 }
351
352 pub fn evict(&self) -> Result<EvictResult> {
360 if !self.is_open() {
361 return Err(EngineError::EnvironmentClosed);
362 }
363
364 let result = self.evictor.do_evict(EvictionSource::Manual);
365 Ok(result)
366 }
367
368 pub fn get_stats(&self) -> EnvironmentStats {
372 let evictor_stats = self.evictor.get_stats();
373 let cleaner_stats = self.cleaner.get_stats();
374 let checkpoint_stats = self.checkpointer.get_stats();
375
376 let env_impl = self.env_impl.lock();
377 let n_databases = env_impl.n_databases() as u32;
378 let log_stats = env_impl.get_log_manager().map(|lm| lm.get_stats());
379 let lock_stats = env_impl.get_lock_manager().get_stats();
380 let real_n_lock_tables =
381 env_impl.get_lock_manager().n_lock_tables() as u64;
382 let txn_stats = env_impl.get_txn_manager().get_stats();
383 let throughput = env_impl.get_throughput_snapshot();
384 drop(env_impl);
385
386 EnvironmentStats {
387 cache_size: self.config.cache_size,
388 cache_usage: self.cache_usage.load(Ordering::Relaxed) as u64,
389 n_databases,
390 evictor: EvictorStatsSnapshot::from(evictor_stats),
391 log: log_stats
392 .as_ref()
393 .map(LogStatsSnapshot::from)
394 .unwrap_or_default(),
395 lock: LockStatsSnapshot {
396 n_lock_tables: real_n_lock_tables,
400 ..LockStatsSnapshot::from(&lock_stats)
401 },
402 txn: TxnStatsSnapshot::from(&txn_stats),
403 cleaner: cleaner_stats.snapshot(),
404 checkpoint: checkpoint_stats.snapshot(),
405 throughput,
406 }
407 }
408
409 pub fn get_cache_usage(&self) -> u64 {
411 self.cache_usage.load(Ordering::Relaxed) as u64
412 }
413
414 pub fn get_cache_budget(&self) -> u64 {
416 self.config.cache_size
417 }
418
419 pub fn is_cache_over_budget(&self) -> bool {
421 self.get_cache_usage() > self.get_cache_budget()
422 }
423}
424
425impl Drop for Engine {
426 fn drop(&mut self) {
427 if self.is_open()
428 && let Err(e) = self.close()
429 {
430 log::error!("Error closing engine in drop: {}", e);
431 }
432 }
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438 use tempfile::TempDir;
439
440 fn temp_config() -> (TempDir, EngineConfig) {
441 let dir = TempDir::new().unwrap();
442 let config = EngineConfig::new(dir.path())
443 .allow_create(true)
444 .cache_size(10 * 1024 * 1024)
445 .evictor_wakeup_interval_ms(100)
446 .cleaner_wakeup_interval_ms(100)
447 .checkpointer_wakeup_interval_ms(100);
448 (dir, config)
449 }
450
451 #[test]
452 fn test_engine_open_and_close() {
453 let (_dir, config) = temp_config();
454 let engine = Engine::open(config).unwrap();
455 assert!(engine.is_open());
456 assert!(engine.get_env_impl().lock().is_open());
457
458 engine.close().unwrap();
459 assert!(!engine.is_open());
460 assert!(
463 !engine.get_env_impl().lock().is_open(),
464 "EnvironmentImpl must be closed after Engine::close"
465 );
466 }
467
468 #[test]
469 fn test_engine_open_creates_directory() {
470 let dir = TempDir::new().unwrap();
471 let home = dir.path().join("newdb");
472 let config = EngineConfig::new(&home).allow_create(true);
473
474 assert!(!home.exists());
475 let engine = Engine::open(config).unwrap();
476 assert!(home.exists());
477 assert!(engine.is_open());
478 }
479
480 #[test]
481 fn test_engine_open_fails_without_create() {
482 let dir = TempDir::new().unwrap();
483 let home = dir.path().join("nonexistent");
484 let config = EngineConfig::new(&home).allow_create(false);
485
486 let result = Engine::open(config);
487 assert!(result.is_err());
488 }
489
490 #[test]
491 fn test_engine_invalid_config() {
492 let dir = TempDir::new().unwrap();
493 let config = EngineConfig::new(dir.path()).cache_size(1024); let result = Engine::open(config);
496 assert!(result.is_err());
497 match result {
498 Err(EngineError::InvalidConfig(_)) => { }
499 _ => panic!("Expected InvalidConfig error"),
500 }
501 }
502
503 #[test]
504 fn test_engine_double_close() {
505 let (_dir, config) = temp_config();
506 let engine = Engine::open(config).unwrap();
507
508 engine.close().unwrap();
509 let result = engine.close();
510 assert!(result.is_err());
511 assert!(matches!(result.unwrap_err(), EngineError::EnvironmentClosed));
512 }
513
514 #[test]
515 fn test_engine_get_subsystems() {
516 let (_dir, config) = temp_config();
517 let engine = Engine::open(config).unwrap();
518
519 assert!(engine.get_env_impl().lock().is_open());
520 assert_eq!(
521 engine.get_evictor().get_lru_sizes().0
522 + engine.get_evictor().get_lru_sizes().1,
523 0
524 );
525 let _ = engine.get_cleaner();
527 let _ = engine.get_checkpointer();
528 }
529
530 #[test]
531 fn test_engine_checkpoint() {
532 let (_dir, config) = temp_config();
533 let engine = Engine::open(config).unwrap();
534
535 let result = engine.checkpoint("test");
536 assert!(result.is_ok());
537 }
538
539 #[test]
540 fn test_engine_checkpoint_readonly() {
541 let dir = TempDir::new().unwrap();
542 let config = EngineConfig::new(dir.path())
543 .allow_create(true)
544 .cache_size(10 * 1024 * 1024)
545 .read_only(true)
546 .cleaner_enabled(false)
547 .checkpointer_enabled(false)
548 .evictor_wakeup_interval_ms(100);
549 let engine = Engine::open(config).unwrap();
550
551 let result = engine.checkpoint("test");
552 assert!(result.is_err());
553 }
554
555 #[test]
556 fn test_engine_clean() {
557 let (_dir, config) = temp_config();
558 let engine = Engine::open(config).unwrap();
559
560 let result = engine.clean(5);
561 assert!(result.is_ok());
562 }
563
564 #[test]
565 fn test_engine_clean_readonly() {
566 let dir = TempDir::new().unwrap();
567 let config = EngineConfig::new(dir.path())
568 .allow_create(true)
569 .cache_size(10 * 1024 * 1024)
570 .read_only(true)
571 .cleaner_enabled(false)
572 .checkpointer_enabled(false)
573 .evictor_wakeup_interval_ms(100);
574 let engine = Engine::open(config).unwrap();
575
576 let result = engine.clean(5);
577 assert!(result.is_err());
578 }
579
580 #[test]
581 fn test_engine_evict() {
582 let (_dir, config) = temp_config();
583 let engine = Engine::open(config).unwrap();
584
585 let result = engine.evict();
586 assert!(result.is_ok());
587 let _evict_result = result.unwrap();
588 }
590
591 #[test]
592 fn test_engine_get_stats() {
593 let (_dir, config) = temp_config();
594 let engine = Engine::open(config).unwrap();
595
596 let stats = engine.get_stats();
597 assert_eq!(stats.cache_size, 10 * 1024 * 1024);
598 assert_eq!(stats.lock.n_lock_tables, 64);
601 }
602
603 #[test]
604 fn test_engine_cache_budget() {
605 let (_dir, config) = temp_config();
606 let engine = Engine::open(config).unwrap();
607
608 assert_eq!(engine.get_cache_budget(), 10 * 1024 * 1024);
609 assert_eq!(engine.get_cache_usage(), 0); assert!(!engine.is_cache_over_budget());
611 }
612
613 #[test]
614 fn test_engine_operations_after_close() {
615 let (_dir, config) = temp_config();
616 let engine = Engine::open(config).unwrap();
617 engine.close().unwrap();
618
619 assert!(engine.checkpoint("test").is_err());
620 assert!(engine.clean(5).is_err());
621 assert!(engine.evict().is_err());
622 }
623
624 #[test]
625 fn test_engine_drop_closes() {
626 let (_dir, config) = temp_config();
627 let engine = Engine::open(config).unwrap();
628 assert!(engine.is_open());
629 drop(engine);
630 }
632
633 #[test]
634 fn test_engine_readonly() {
635 let dir = TempDir::new().unwrap();
636 let config = EngineConfig::new(dir.path())
637 .allow_create(true)
638 .cache_size(10 * 1024 * 1024)
639 .read_only(true)
640 .cleaner_enabled(false)
641 .checkpointer_enabled(false)
642 .evictor_wakeup_interval_ms(100);
643 let engine = Engine::open(config).unwrap();
644
645 assert!(engine.is_open());
646 assert!(engine.get_config().read_only);
647
648 let stats = engine.get_stats();
650 assert_eq!(stats.cache_size, 10 * 1024 * 1024);
651
652 assert!(engine.checkpoint("test").is_err());
654 assert!(engine.clean(5).is_err());
655 }
656}