Skip to main content

noxu_engine/
engine_config.rs

1//! Configuration for the Noxu DB engine.
2
3use std::path::PathBuf;
4
5/// Configuration for the Noxu DB engine.
6///
7/// Aggregates all configuration that affects environment behavior.
8/// This is the primary configuration structure for opening an environment.
9#[derive(Debug, Clone)]
10pub struct EngineConfig {
11    /// Environment home directory.
12    ///
13    /// All database files are stored in this directory.
14    pub home: PathBuf,
15
16    /// Whether to create the environment if it doesn't exist.
17    pub allow_create: bool,
18
19    /// Whether transactions are enabled.
20    ///
21    /// When true, the transaction manager is active and all database
22    /// operations can optionally be transactional.
23    pub transactional: bool,
24
25    /// Whether the environment is read-only.
26    ///
27    /// Read-only environments cannot modify the database or log files.
28    pub read_only: bool,
29
30    /// Maximum cache size in bytes.
31    ///
32    /// Controls the memory budget for the in-memory B-tree cache.
33    pub cache_size: u64,
34
35    /// Maximum number of lock tables (shards).
36    ///
37    /// Higher values reduce contention but increase memory overhead.
38    pub lock_table_count: u32,
39
40    /// Lock timeout in milliseconds (0 = no timeout).
41    ///
42    /// Maximum time to wait for a lock before timing out.
43    pub lock_timeout_ms: u64,
44
45    /// Transaction timeout in milliseconds (0 = no timeout).
46    ///
47    /// Maximum time a transaction can run before timing out.
48    pub txn_timeout_ms: u64,
49
50    /// Whether to run the evictor daemon.
51    ///
52    /// The evictor daemon runs in the background evicting nodes
53    /// from the cache when memory budget is exceeded.
54    pub evictor_enabled: bool,
55
56    /// Whether to run the cleaner daemon.
57    ///
58    /// The cleaner daemon runs in the background performing log
59    /// file garbage collection.
60    pub cleaner_enabled: bool,
61
62    /// Whether to run the checkpointer daemon.
63    ///
64    /// The checkpointer daemon runs in the background performing
65    /// periodic checkpoints to bound recovery time.
66    pub checkpointer_enabled: bool,
67
68    /// Checkpoint bytes interval.
69    ///
70    /// A checkpoint is performed after approximately this many bytes
71    /// have been written to the log (0 = disabled).
72    pub checkpoint_bytes_interval: u64,
73
74    /// Cleaner minimum utilization (0-100).
75    ///
76    /// Log files below this utilization percentage are candidates
77    /// for cleaning.
78    pub cleaner_min_utilization: u32,
79
80    /// Cleaner minimum file count.
81    ///
82    /// The cleaner won't run until at least this many log files exist.
83    pub cleaner_min_file_count: u32,
84
85    /// Evictor wakeup interval in milliseconds.
86    ///
87    /// How often the evictor daemon wakes up to check if eviction is needed.
88    pub evictor_wakeup_interval_ms: u64,
89
90    /// Cleaner wakeup interval in milliseconds.
91    ///
92    /// How often the cleaner daemon wakes up to check if cleaning is needed.
93    pub cleaner_wakeup_interval_ms: u64,
94
95    /// Checkpointer wakeup interval in milliseconds.
96    ///
97    /// How often the checkpointer daemon wakes up to check if checkpoint is needed.
98    pub checkpointer_wakeup_interval_ms: u64,
99
100    // -----------------------------------------------------------------------
101    // Log parameters (je.log.*)
102    // -----------------------------------------------------------------------
103    /// Maximum size of each log file in bytes (je.log.fileMax).
104    ///
105    /// Range: 1 MB – 1 GB. Default: 10 MB.
106    pub log_file_max: u64,
107
108    /// Whether the environment uses an in-memory log only (je.log.memOnly).
109    ///
110    /// When true, no files are written and the log lives entirely in memory.
111    pub log_mem_only: bool,
112
113    /// Whether to verify checksums when reading log entries (je.log.checksumRead).
114    pub log_checksum_read: bool,
115
116    /// Total bytes to use for log write buffers (je.log.totalBufferBytes).
117    ///
118    /// 0 means compute automatically from max_memory.
119    pub log_total_buffer_bytes: u64,
120
121    // -----------------------------------------------------------------------
122    // Evictor parameters (je.evictor.*)
123    // -----------------------------------------------------------------------
124    /// Number of bytes to evict per eviction pass (je.evictor.evictBytes).
125    ///
126    /// Default: 512 KB.
127    pub evictor_evict_bytes: u64,
128
129    /// Number of evictor core threads (je.evictor.coreThreads).
130    pub evictor_core_threads: u32,
131
132    /// Maximum number of evictor threads (je.evictor.maxThreads).
133    pub evictor_max_threads: u32,
134
135    /// Number of LRU lists for the evictor (je.evictor.nLRULists).
136    ///
137    /// More lists reduce contention. Range: 1–32. Default: 4.
138    pub evictor_n_lru_lists: u32,
139
140    // -----------------------------------------------------------------------
141    // Cleaner parameters (je.cleaner.*)
142    // -----------------------------------------------------------------------
143    /// Minimum per-file utilization (je.cleaner.minFileUtilization).
144    ///
145    /// Files below this percentage are cleaned regardless of overall utilization.
146    /// Range: 0–50. Default: 5.
147    pub cleaner_min_file_utilization: u32,
148
149    /// Number of cleaner threads (je.cleaner.threads).
150    ///
151    /// Default: 1.
152    pub cleaner_threads: u32,
153
154    /// Lock timeout for cleaner operations in milliseconds (je.cleaner.lockTimeout).
155    ///
156    /// Default: 500 ms.
157    pub cleaner_lock_timeout_ms: u64,
158
159    // -----------------------------------------------------------------------
160    // Transaction / lock parameters
161    // -----------------------------------------------------------------------
162    /// If true, all transactions use serializable isolation (je.txn.serializableIsolation).
163    pub txn_serializable_isolation: bool,
164
165    /// If true, deadlock detection is enabled (je.lock.deadlockDetect).
166    pub lock_deadlock_detect: bool,
167
168    // -----------------------------------------------------------------------
169    // Checkpointer parameters
170    // -----------------------------------------------------------------------
171    /// If true, the checkpointer runs at high priority (je.checkpointer.highPriority).
172    pub checkpointer_high_priority: bool,
173}
174
175impl EngineConfig {
176    /// Create a new EngineConfig with the given home directory.
177    pub fn new(home: impl Into<PathBuf>) -> Self {
178        Self { home: home.into(), ..Default::default() }
179    }
180
181    /// Set whether to create the environment if it doesn't exist.
182    pub fn allow_create(mut self, allow: bool) -> Self {
183        self.allow_create = allow;
184        self
185    }
186
187    /// Set whether transactions are enabled.
188    pub fn transactional(mut self, enabled: bool) -> Self {
189        self.transactional = enabled;
190        self
191    }
192
193    /// Set whether the environment is read-only.
194    pub fn read_only(mut self, read_only: bool) -> Self {
195        self.read_only = read_only;
196        self
197    }
198
199    /// Set the maximum cache size in bytes.
200    pub fn cache_size(mut self, size: u64) -> Self {
201        self.cache_size = size;
202        self
203    }
204
205    /// Set the number of lock table shards.
206    pub fn lock_table_count(mut self, count: u32) -> Self {
207        self.lock_table_count = count;
208        self
209    }
210
211    /// Set the lock timeout in milliseconds.
212    pub fn lock_timeout_ms(mut self, timeout: u64) -> Self {
213        self.lock_timeout_ms = timeout;
214        self
215    }
216
217    /// Set the transaction timeout in milliseconds.
218    pub fn txn_timeout_ms(mut self, timeout: u64) -> Self {
219        self.txn_timeout_ms = timeout;
220        self
221    }
222
223    /// Enable or disable the evictor daemon.
224    pub fn evictor_enabled(mut self, enabled: bool) -> Self {
225        self.evictor_enabled = enabled;
226        self
227    }
228
229    /// Enable or disable the cleaner daemon.
230    pub fn cleaner_enabled(mut self, enabled: bool) -> Self {
231        self.cleaner_enabled = enabled;
232        self
233    }
234
235    /// Enable or disable the checkpointer daemon.
236    pub fn checkpointer_enabled(mut self, enabled: bool) -> Self {
237        self.checkpointer_enabled = enabled;
238        self
239    }
240
241    /// Set the checkpoint bytes interval.
242    pub fn checkpoint_bytes_interval(mut self, bytes: u64) -> Self {
243        self.checkpoint_bytes_interval = bytes;
244        self
245    }
246
247    /// Set the cleaner minimum utilization percentage.
248    pub fn cleaner_min_utilization(mut self, percent: u32) -> Self {
249        self.cleaner_min_utilization = percent.min(100);
250        self
251    }
252
253    /// Set the evictor wakeup interval in milliseconds.
254    pub fn evictor_wakeup_interval_ms(mut self, ms: u64) -> Self {
255        self.evictor_wakeup_interval_ms = ms;
256        self
257    }
258
259    /// Set the cleaner wakeup interval in milliseconds.
260    pub fn cleaner_wakeup_interval_ms(mut self, ms: u64) -> Self {
261        self.cleaner_wakeup_interval_ms = ms;
262        self
263    }
264
265    /// Set the checkpointer wakeup interval in milliseconds.
266    pub fn checkpointer_wakeup_interval_ms(mut self, ms: u64) -> Self {
267        self.checkpointer_wakeup_interval_ms = ms;
268        self
269    }
270
271    // -----------------------------------------------------------------------
272    // Log builder methods
273    // -----------------------------------------------------------------------
274
275    /// Set the maximum log file size in bytes (je.log.fileMax).
276    pub fn log_file_max(mut self, bytes: u64) -> Self {
277        self.log_file_max = bytes;
278        self
279    }
280
281    /// Enable or disable in-memory-only log (je.log.memOnly).
282    pub fn log_mem_only(mut self, mem_only: bool) -> Self {
283        self.log_mem_only = mem_only;
284        self
285    }
286
287    /// Enable or disable log checksum verification on read (je.log.checksumRead).
288    pub fn log_checksum_read(mut self, enabled: bool) -> Self {
289        self.log_checksum_read = enabled;
290        self
291    }
292
293    /// Set total log buffer bytes (je.log.totalBufferBytes). 0 = auto.
294    pub fn log_total_buffer_bytes(mut self, bytes: u64) -> Self {
295        self.log_total_buffer_bytes = bytes;
296        self
297    }
298
299    // -----------------------------------------------------------------------
300    // Evictor builder methods
301    // -----------------------------------------------------------------------
302
303    /// Set the number of bytes to evict per pass (je.evictor.evictBytes).
304    pub fn evictor_evict_bytes(mut self, bytes: u64) -> Self {
305        self.evictor_evict_bytes = bytes;
306        self
307    }
308
309    /// Set the number of evictor core threads (je.evictor.coreThreads).
310    pub fn evictor_core_threads(mut self, n: u32) -> Self {
311        self.evictor_core_threads = n;
312        self
313    }
314
315    /// Set the maximum number of evictor threads (je.evictor.maxThreads).
316    pub fn evictor_max_threads(mut self, n: u32) -> Self {
317        self.evictor_max_threads = n;
318        self
319    }
320
321    /// Set the number of LRU lists for the evictor (je.evictor.nLRULists).
322    pub fn evictor_n_lru_lists(mut self, n: u32) -> Self {
323        self.evictor_n_lru_lists = n.clamp(1, 32);
324        self
325    }
326
327    // -----------------------------------------------------------------------
328    // Cleaner builder methods
329    // -----------------------------------------------------------------------
330
331    /// Set the per-file minimum utilization (je.cleaner.minFileUtilization).
332    pub fn cleaner_min_file_utilization(mut self, percent: u32) -> Self {
333        self.cleaner_min_file_utilization = percent.min(50);
334        self
335    }
336
337    /// Set the number of cleaner threads (je.cleaner.threads).
338    pub fn cleaner_threads(mut self, n: u32) -> Self {
339        self.cleaner_threads = n.max(1);
340        self
341    }
342
343    /// Set the cleaner lock timeout in milliseconds (je.cleaner.lockTimeout).
344    pub fn cleaner_lock_timeout_ms(mut self, ms: u64) -> Self {
345        self.cleaner_lock_timeout_ms = ms;
346        self
347    }
348
349    // -----------------------------------------------------------------------
350    // Transaction / lock builder methods
351    // -----------------------------------------------------------------------
352
353    /// Enable or disable serializable isolation for all transactions.
354    pub fn txn_serializable_isolation(mut self, enabled: bool) -> Self {
355        self.txn_serializable_isolation = enabled;
356        self
357    }
358
359    /// Enable or disable automatic deadlock detection.
360    pub fn lock_deadlock_detect(mut self, enabled: bool) -> Self {
361        self.lock_deadlock_detect = enabled;
362        self
363    }
364
365    // -----------------------------------------------------------------------
366    // Checkpointer builder methods
367    // -----------------------------------------------------------------------
368
369    /// Enable or disable high-priority checkpointing.
370    pub fn checkpointer_high_priority(mut self, enabled: bool) -> Self {
371        self.checkpointer_high_priority = enabled;
372        self
373    }
374
375    /// Validate the configuration.
376    ///
377    /// Returns an error if any configuration parameters are invalid.
378    pub fn validate(&self) -> Result<(), String> {
379        if self.cache_size < 1024 * 1024 {
380            return Err("cache_size must be at least 1 MB".to_string());
381        }
382
383        if self.lock_table_count == 0 {
384            return Err("lock_table_count must be at least 1".to_string());
385        }
386
387        if self.cleaner_min_utilization > 100 {
388            return Err("cleaner_min_utilization must be 0-100".to_string());
389        }
390
391        if self.cleaner_min_file_utilization > 50 {
392            return Err("cleaner_min_file_utilization must be 0-50".to_string());
393        }
394
395        if self.cleaner_threads == 0 {
396            return Err("cleaner_threads must be at least 1".to_string());
397        }
398
399        if !(1..=10_000_000).contains(&self.log_file_max) {
400            return Err(
401                "log_file_max must be between 1 MB and 1 GB".to_string()
402            );
403        }
404
405        if self.evictor_n_lru_lists == 0 || self.evictor_n_lru_lists > 32 {
406            return Err(
407                "evictor_n_lru_lists must be between 1 and 32".to_string()
408            );
409        }
410
411        if self.evictor_max_threads == 0 {
412            return Err("evictor_max_threads must be at least 1".to_string());
413        }
414
415        if self.read_only && (self.cleaner_enabled || self.checkpointer_enabled)
416        {
417            return Err(
418                "cleaner and checkpointer cannot be enabled in read-only mode"
419                    .to_string(),
420            );
421        }
422
423        Ok(())
424    }
425}
426
427impl Default for EngineConfig {
428    fn default() -> Self {
429        Self {
430            home: PathBuf::from("."),
431            allow_create: true,
432            transactional: true,
433            read_only: false,
434            cache_size: 64 * 1024 * 1024, // 64 MB
435            lock_table_count: 16,
436            lock_timeout_ms: 500, // 500 ms — matches default
437            txn_timeout_ms: 0,    // 0 = no timeout — matches default
438            evictor_enabled: true,
439            cleaner_enabled: true,
440            checkpointer_enabled: true,
441            checkpoint_bytes_interval: 20_000_000, // 20 MB — matches
442            cleaner_min_utilization: 50,           // 50% — matches
443            cleaner_min_file_count: 5,
444            evictor_wakeup_interval_ms: 5000, // 5 seconds
445            cleaner_wakeup_interval_ms: 10_000, // 10 s — matches
446            checkpointer_wakeup_interval_ms: 0, // 0 = bytes-based — matches
447            // Log defaults — match
448            log_file_max: 10_000_000, // 10 MB
449            log_mem_only: false,
450            log_checksum_read: true,
451            log_total_buffer_bytes: 0, // auto-computed
452            // Evictor defaults — match
453            evictor_evict_bytes: 524_288, // 512 KB
454            evictor_core_threads: 1,
455            evictor_max_threads: 10,
456            evictor_n_lru_lists: 4,
457            // Cleaner defaults — match
458            cleaner_min_file_utilization: 5, // 5%
459            cleaner_threads: 1,
460            cleaner_lock_timeout_ms: 500, // 500 ms
461            // Txn/lock defaults — match
462            txn_serializable_isolation: false,
463            lock_deadlock_detect: true,
464            // Checkpointer defaults — match
465            checkpointer_high_priority: false,
466        }
467    }
468}
469
470#[cfg(test)]
471#[expect(clippy::field_reassign_with_default)]
472mod tests {
473    use super::*;
474
475    #[test]
476    fn test_default_config() {
477        let config = EngineConfig::default();
478        assert_eq!(config.home, PathBuf::from("."));
479        assert!(config.allow_create);
480        assert!(config.transactional);
481        assert!(!config.read_only);
482        assert_eq!(config.cache_size, 64 * 1024 * 1024);
483        assert_eq!(config.lock_table_count, 16);
484        assert!(config.evictor_enabled);
485        assert!(config.cleaner_enabled);
486        assert!(config.checkpointer_enabled);
487        // Check -matched defaults
488        assert_eq!(config.lock_timeout_ms, 500);
489        assert_eq!(config.txn_timeout_ms, 0);
490        assert_eq!(config.cleaner_min_utilization, 50);
491        assert_eq!(config.cleaner_min_file_utilization, 5);
492        assert_eq!(config.cleaner_threads, 1);
493        assert_eq!(config.checkpoint_bytes_interval, 20_000_000);
494        assert_eq!(config.log_file_max, 10_000_000);
495        assert!(!config.log_mem_only);
496        assert!(config.log_checksum_read);
497        assert_eq!(config.log_total_buffer_bytes, 0);
498        assert_eq!(config.evictor_evict_bytes, 524_288);
499        assert_eq!(config.evictor_core_threads, 1);
500        assert_eq!(config.evictor_max_threads, 10);
501        assert_eq!(config.evictor_n_lru_lists, 4);
502        assert!(!config.txn_serializable_isolation);
503        assert!(config.lock_deadlock_detect);
504        assert!(!config.checkpointer_high_priority);
505    }
506
507    #[test]
508    fn test_new_config() {
509        let config = EngineConfig::new("/tmp/mydb");
510        assert_eq!(config.home, PathBuf::from("/tmp/mydb"));
511        // Other fields should be default
512        assert!(config.allow_create);
513        assert!(config.transactional);
514    }
515
516    #[test]
517    fn test_builder_pattern() {
518        let config = EngineConfig::new("/data/db")
519            .allow_create(false)
520            .transactional(true)
521            .read_only(false)
522            .cache_size(128 * 1024 * 1024)
523            .lock_table_count(32)
524            .lock_timeout_ms(10000)
525            .txn_timeout_ms(20000)
526            .evictor_enabled(true)
527            .cleaner_enabled(false)
528            .checkpointer_enabled(true)
529            .checkpoint_bytes_interval(50_000_000)
530            .cleaner_min_utilization(60)
531            .log_file_max(20_000_000)
532            .log_mem_only(false)
533            .log_checksum_read(true)
534            .evictor_evict_bytes(1_048_576)
535            .evictor_core_threads(2)
536            .evictor_max_threads(8)
537            .evictor_n_lru_lists(8)
538            .cleaner_min_file_utilization(10)
539            .cleaner_threads(2)
540            .cleaner_lock_timeout_ms(1000)
541            .txn_serializable_isolation(true)
542            .lock_deadlock_detect(true)
543            .checkpointer_high_priority(true);
544
545        assert_eq!(config.home, PathBuf::from("/data/db"));
546        assert!(!config.allow_create);
547        assert!(config.transactional);
548        assert!(!config.read_only);
549        assert_eq!(config.cache_size, 128 * 1024 * 1024);
550        assert_eq!(config.lock_table_count, 32);
551        assert_eq!(config.lock_timeout_ms, 10000);
552        assert_eq!(config.txn_timeout_ms, 20000);
553        assert!(config.evictor_enabled);
554        assert!(!config.cleaner_enabled);
555        assert!(config.checkpointer_enabled);
556        assert_eq!(config.checkpoint_bytes_interval, 50_000_000);
557        assert_eq!(config.cleaner_min_utilization, 60);
558        assert_eq!(config.log_file_max, 20_000_000);
559        assert!(!config.log_mem_only);
560        assert!(config.log_checksum_read);
561        assert_eq!(config.evictor_evict_bytes, 1_048_576);
562        assert_eq!(config.evictor_core_threads, 2);
563        assert_eq!(config.evictor_max_threads, 8);
564        assert_eq!(config.evictor_n_lru_lists, 8);
565        assert_eq!(config.cleaner_min_file_utilization, 10);
566        assert_eq!(config.cleaner_threads, 2);
567        assert_eq!(config.cleaner_lock_timeout_ms, 1000);
568        assert!(config.txn_serializable_isolation);
569        assert!(config.lock_deadlock_detect);
570        assert!(config.checkpointer_high_priority);
571    }
572
573    #[test]
574    fn test_validate_valid_config() {
575        let config = EngineConfig::default();
576        assert!(config.validate().is_ok());
577    }
578
579    #[test]
580    fn test_validate_cache_too_small() {
581        let config = EngineConfig::default().cache_size(1024);
582        let result = config.validate();
583        assert!(result.is_err());
584        assert!(result.unwrap_err().contains("cache_size"));
585    }
586
587    #[test]
588    fn test_validate_zero_lock_tables() {
589        let config = EngineConfig::default().lock_table_count(0);
590        let result = config.validate();
591        assert!(result.is_err());
592        assert!(result.unwrap_err().contains("lock_table_count"));
593    }
594
595    #[test]
596    fn test_validate_invalid_utilization() {
597        let mut config = EngineConfig::default();
598        config.cleaner_min_utilization = 150;
599        let result = config.validate();
600        assert!(result.is_err());
601        assert!(result.unwrap_err().contains("utilization"));
602    }
603
604    #[test]
605    fn test_validate_readonly_conflicts() {
606        let config =
607            EngineConfig::default().read_only(true).cleaner_enabled(true);
608        let result = config.validate();
609        assert!(result.is_err());
610        assert!(result.unwrap_err().contains("read-only"));
611
612        let config =
613            EngineConfig::default().read_only(true).checkpointer_enabled(true);
614        let result = config.validate();
615        assert!(result.is_err());
616        assert!(result.unwrap_err().contains("read-only"));
617    }
618
619    #[test]
620    fn test_cleaner_utilization_clamped() {
621        let config = EngineConfig::default().cleaner_min_utilization(150);
622        assert_eq!(config.cleaner_min_utilization, 100);
623
624        let config = EngineConfig::default().cleaner_min_utilization(50);
625        assert_eq!(config.cleaner_min_utilization, 50);
626    }
627
628    #[test]
629    fn test_readonly_config() {
630        let config = EngineConfig::new("/db")
631            .read_only(true)
632            .cleaner_enabled(false)
633            .checkpointer_enabled(false);
634        assert!(config.validate().is_ok());
635        assert!(config.read_only);
636        assert!(!config.cleaner_enabled);
637        assert!(!config.checkpointer_enabled);
638    }
639}