1use std::time::Duration;
4
5use crate::compression::CompressionType;
6use linkrs_core::error::storage::StorageErrorKind;
7use linkrs_core::types::{AutoCompactConfig, Timestamp};
8use linkrs_core::StorageError;
9
10pub use crate::engine::freeze_decision::{FreezeDecisionEngine, FreezeDecisionInput};
11
12pub fn default_vertex_table_shards() -> usize {
18 std::thread::available_parallelism()
19 .map(|n| n.get())
20 .unwrap_or(1)
21 .clamp(1, 256)
22 .next_power_of_two()
23}
24
25#[derive(Debug, Clone)]
27pub struct FlushConfig {
28 pub flush_threshold: usize,
29 pub flush_interval: Duration,
30 pub compression: CompressionType,
31}
32
33impl Default for FlushConfig {
34 fn default() -> Self {
35 Self {
36 flush_threshold: 50_000,
37 flush_interval: Duration::from_secs(30),
38 compression: CompressionType::Zstd { level: 3 },
39 }
40 }
41}
42
43#[derive(Debug, Clone)]
45pub struct ResourceConfig {
46 pub max_memory_bytes: u64,
48 pub index_memory_bytes: u64,
50 pub memory_soft_ratio: f64,
52 pub memory_hard_ratio: f64,
54 pub max_active_snapshots: usize,
56 pub max_snapshot_age: Duration,
58 pub max_tombstones: usize,
60 pub max_tombstone_bytes: u64,
62 pub index_gc_batch: usize,
64 pub operation_timeout: Duration,
66 pub dirty_flush_operations: u64,
68 pub dirty_flush_bytes: u64,
70 pub cache_ttl: Option<Duration>,
72 pub cache_tti: Option<Duration>,
74 pub spill_threshold_ratio: f64,
77 pub cache_eviction_sync: bool,
80 pub index_pool_capacity_bytes: u64,
82 pub index_eviction_enabled: bool,
84 pub index_eviction_high_ratio: f64,
86 pub index_eviction_low_ratio: f64,
88}
89
90impl Default for ResourceConfig {
91 fn default() -> Self {
92 Self {
93 max_memory_bytes: 512 * 1024 * 1024,
94 index_memory_bytes: 128 * 1024 * 1024,
95 memory_soft_ratio: 0.80,
96 memory_hard_ratio: 0.95,
97 max_active_snapshots: 1_000,
98 max_snapshot_age: Duration::from_secs(300),
99 max_tombstones: 1_000_000,
100 max_tombstone_bytes: 256 * 1024 * 1024,
101 index_gc_batch: 10_000,
102 operation_timeout: Duration::from_secs(30),
103 dirty_flush_operations: 50_000,
104 dirty_flush_bytes: 64 * 1024 * 1024,
105 cache_ttl: Some(Duration::from_secs(60)),
106 cache_tti: Some(Duration::from_secs(300)),
107 spill_threshold_ratio: 0.90,
108 cache_eviction_sync: true,
109 index_pool_capacity_bytes: 128 * 1024 * 1024,
110 index_eviction_enabled: true,
111 index_eviction_high_ratio: 0.85,
112 index_eviction_low_ratio: 0.65,
113 }
114 }
115}
116
117impl ResourceConfig {
118 pub fn validate(&self) -> Result<(), StorageError> {
119 crate::engine::resource_budget::MemoryBudget::new(
120 self.max_memory_bytes,
121 self.index_memory_bytes,
122 self.memory_soft_ratio,
123 self.memory_hard_ratio,
124 )?;
125 if self.max_active_snapshots == 0 {
126 return Err(StorageError::new(
127 StorageErrorKind::InvalidInput,
128 "max_active_snapshots must be greater than 0",
129 ));
130 }
131 if self.max_snapshot_age.is_zero() {
132 return Err(StorageError::new(
133 StorageErrorKind::InvalidInput,
134 "max_snapshot_age must be greater than 0",
135 ));
136 }
137 if self.max_tombstones == 0 || self.max_tombstone_bytes == 0 {
138 return Err(StorageError::new(
139 StorageErrorKind::InvalidInput,
140 "tombstone limits must be greater than 0",
141 ));
142 }
143 if self.index_gc_batch == 0 {
144 return Err(StorageError::new(
145 StorageErrorKind::InvalidInput,
146 "index_gc_batch must be greater than 0",
147 ));
148 }
149 if self.operation_timeout.is_zero() {
150 return Err(StorageError::new(
151 StorageErrorKind::InvalidInput,
152 "operation_timeout must be greater than 0",
153 ));
154 }
155 if self.dirty_flush_operations == 0 || self.dirty_flush_bytes == 0 {
156 return Err(StorageError::new(
157 StorageErrorKind::InvalidInput,
158 "dirty flush limits must be greater than 0",
159 ));
160 }
161 for (name, duration) in [("cache_ttl", self.cache_ttl), ("cache_tti", self.cache_tti)] {
162 if duration.is_some_and(|value| value.is_zero()) {
163 return Err(StorageError::new(
164 StorageErrorKind::InvalidInput,
165 format!("{name} must be greater than 0 when configured"),
166 ));
167 }
168 }
169 if !self.spill_threshold_ratio.is_finite()
170 || self.spill_threshold_ratio <= self.memory_soft_ratio
171 || self.spill_threshold_ratio > 1.0
172 {
173 return Err(StorageError::new(
174 StorageErrorKind::InvalidInput,
175 format!(
176 "spill_threshold_ratio ({}) must satisfy soft_ratio < spill_ratio <= 1.0",
177 self.spill_threshold_ratio
178 ),
179 ));
180 }
181 Ok(())
182 }
183}
184
185#[derive(Debug, Clone)]
191pub struct FreezeConfig {
192 pub delta_edge_threshold: u64,
195 pub delta_memory_threshold_bytes: u64,
197 pub deletion_threshold: f64,
201}
202
203impl FreezeConfig {
204 pub fn development() -> Self {
210 Self {
211 delta_edge_threshold: 50_000,
212 delta_memory_threshold_bytes: 128 * 1024 * 1024, deletion_threshold: 0.5,
214 }
215 }
216
217 pub fn production_small() -> Self {
223 Self {
224 delta_edge_threshold: 100_000,
225 delta_memory_threshold_bytes: 256 * 1024 * 1024, deletion_threshold: 0.2,
227 }
228 }
229
230 pub fn production_large() -> Self {
236 Self {
237 delta_edge_threshold: 500_000,
238 delta_memory_threshold_bytes: 1_000_000_000, deletion_threshold: 0.3,
240 }
241 }
242
243 pub fn validate(&self) -> Result<(), StorageError> {
250 if self.delta_edge_threshold == 0 {
251 return Err(StorageError::new(
252 StorageErrorKind::InvalidInput,
253 "delta_edge_threshold must be > 0",
254 ));
255 }
256
257 if self.delta_memory_threshold_bytes == 0 {
258 return Err(StorageError::new(
259 StorageErrorKind::InvalidInput,
260 "delta_memory_threshold_bytes must be > 0",
261 ));
262 }
263
264 if !(0.0..=1.0).contains(&self.deletion_threshold) {
265 return Err(StorageError::new(
266 StorageErrorKind::InvalidInput,
267 format!(
268 "deletion_threshold must be in [0.0, 1.0], got {}",
269 self.deletion_threshold
270 ),
271 ));
272 }
273
274 Ok(())
275 }
276}
277
278impl Default for FreezeConfig {
279 fn default() -> Self {
280 Self::production_small()
281 }
282}
283
284#[derive(Debug, Clone)]
285pub struct PropertyGraphConfig {
286 pub enable_cache: bool,
287 pub cache_memory: usize,
288 pub flush_config: FlushConfig,
289 pub resources: ResourceConfig,
290 pub freeze: FreezeConfig,
291 pub auto_compact: AutoCompactConfig,
293 pub vertex_table_shards: usize,
295 pub gc_safety_margin: Timestamp,
298}
299
300impl Default for PropertyGraphConfig {
301 fn default() -> Self {
302 Self {
303 enable_cache: true,
304 cache_memory: 128 * 1024 * 1024,
305 flush_config: FlushConfig::default(),
306 resources: ResourceConfig::default(),
307 freeze: FreezeConfig::default(),
308 auto_compact: AutoCompactConfig::default(),
309 vertex_table_shards: default_vertex_table_shards(),
310 gc_safety_margin: 1,
311 }
312 }
313}
314
315impl PropertyGraphConfig {
316 pub fn development() -> Self {
318 let freeze = FreezeConfig::development();
319 let _ = freeze.validate();
321 Self {
322 enable_cache: true,
323 cache_memory: 64 * 1024 * 1024, flush_config: FlushConfig::default(),
325 resources: ResourceConfig {
326 max_memory_bytes: 256 * 1024 * 1024,
327 index_memory_bytes: 64 * 1024 * 1024,
328 ..Default::default()
329 },
330 freeze: freeze.clone(),
331 auto_compact: AutoCompactConfig::default(),
332 vertex_table_shards: default_vertex_table_shards(),
333 gc_safety_margin: 1,
334 }
335 }
336
337 pub fn production_small() -> Self {
339 let freeze = FreezeConfig::production_small();
340 let _ = freeze.validate();
342 Self {
343 enable_cache: true,
344 cache_memory: 128 * 1024 * 1024,
345 flush_config: FlushConfig::default(),
346 resources: ResourceConfig::default(),
347 freeze: freeze.clone(),
348 auto_compact: AutoCompactConfig::default(),
349 vertex_table_shards: default_vertex_table_shards(),
350 gc_safety_margin: 1,
351 }
352 }
353
354 pub fn production_large() -> Self {
356 let freeze = FreezeConfig::production_large();
357 let _ = freeze.validate();
359 Self {
360 enable_cache: true,
361 cache_memory: 256 * 1024 * 1024,
362 flush_config: FlushConfig::default(),
363 resources: ResourceConfig {
364 max_memory_bytes: 1024 * 1024 * 1024,
365 index_memory_bytes: 256 * 1024 * 1024,
366 ..Default::default()
367 },
368 freeze: freeze.clone(),
369 auto_compact: AutoCompactConfig::default(),
370 vertex_table_shards: default_vertex_table_shards(),
371 gc_safety_margin: 1,
372 }
373 }
374
375 pub fn test() -> Self {
380 Self {
381 enable_cache: true,
382 cache_memory: 8 * 1024 * 1024,
383 flush_config: FlushConfig {
384 flush_threshold: 100000,
385 flush_interval: Duration::from_secs(3600),
386 ..Default::default()
387 },
388 resources: ResourceConfig {
389 max_memory_bytes: 64 * 1024 * 1024,
390 index_memory_bytes: 16 * 1024 * 1024,
391 ..Default::default()
392 },
393 freeze: FreezeConfig {
394 delta_edge_threshold: 5000,
395 delta_memory_threshold_bytes: 16 * 1024 * 1024,
396 deletion_threshold: 0.5,
397 },
398 auto_compact: AutoCompactConfig::default(),
399 vertex_table_shards: default_vertex_table_shards(),
400 gc_safety_margin: 1,
401 }
402 }
403
404 pub fn validate(&self) -> Result<(), StorageError> {
406 if self.enable_cache && self.cache_memory == 0 {
407 return Err(StorageError::new(
408 StorageErrorKind::InvalidInput,
409 "cache_memory must be > 0 when cache is enabled",
410 ));
411 }
412 if self.enable_cache && self.cache_memory as u64 > self.resources.max_memory_bytes {
413 return Err(StorageError::new(
414 StorageErrorKind::InvalidInput,
415 "cache_memory cannot exceed max_memory_bytes",
416 ));
417 }
418 self.resources.validate()?;
419 if self.flush_config.flush_threshold == 0 {
420 return Err(StorageError::new(
421 StorageErrorKind::InvalidInput,
422 "flush_threshold must be > 0",
423 ));
424 }
425 if self.flush_config.flush_interval.is_zero() {
426 return Err(StorageError::new(
427 StorageErrorKind::InvalidInput,
428 "flush_interval must be > 0",
429 ));
430 }
431 self.freeze.validate()?;
432 if !(0.0..=1.0).contains(&self.auto_compact.min_hole_ratio) {
433 return Err(StorageError::new(
434 StorageErrorKind::InvalidInput,
435 "auto_compact min_hole_ratio must be in [0.0, 1.0]",
436 ));
437 }
438 if !(1..=256).contains(&self.vertex_table_shards)
439 || !self.vertex_table_shards.is_power_of_two()
440 {
441 return Err(StorageError::new(
442 StorageErrorKind::InvalidInput,
443 "vertex_table_shards must be a power of two in [1, 256]",
444 ));
445 }
446 Ok(())
447 }
448}
449
450#[cfg(test)]
451mod tests {
452 use super::*;
453
454 #[test]
455 fn test_freeze_config_development() {
456 let config = FreezeConfig::development();
457 assert_eq!(config.delta_edge_threshold, 50_000);
458 assert_eq!(config.delta_memory_threshold_bytes, 128 * 1024 * 1024);
459 assert_eq!(config.deletion_threshold, 0.5);
460 }
461
462 #[test]
463 fn test_freeze_config_production_small() {
464 let config = FreezeConfig::production_small();
465 assert_eq!(config.delta_edge_threshold, 100_000);
466 assert_eq!(config.delta_memory_threshold_bytes, 256 * 1024 * 1024);
467 assert_eq!(config.deletion_threshold, 0.2);
468 }
469
470 #[test]
471 fn test_freeze_config_production_large() {
472 let config = FreezeConfig::production_large();
473 assert_eq!(config.delta_edge_threshold, 500_000);
474 assert_eq!(config.delta_memory_threshold_bytes, 1_000_000_000);
475 assert_eq!(config.deletion_threshold, 0.3);
476 }
477
478 #[test]
479 fn test_freeze_config_validate_success() {
480 let config = FreezeConfig::default();
481 assert!(config.validate().is_ok());
482 }
483
484 #[test]
485 fn test_freeze_config_validate_zero_edge_threshold() {
486 let config = FreezeConfig {
487 delta_edge_threshold: 0,
488 ..Default::default()
489 };
490 assert!(config.validate().is_err());
491 }
492
493 #[test]
494 fn test_freeze_config_validate_zero_memory_threshold() {
495 let config = FreezeConfig {
496 delta_memory_threshold_bytes: 0,
497 ..Default::default()
498 };
499 assert!(config.validate().is_err());
500 }
501
502 #[test]
503 fn test_freeze_config_validate_invalid_deletion_threshold() {
504 let config = FreezeConfig {
505 deletion_threshold: 1.5,
506 ..Default::default()
507 };
508 assert!(config.validate().is_err());
509
510 let config = FreezeConfig {
511 deletion_threshold: -0.1,
512 ..Default::default()
513 };
514 assert!(config.validate().is_err());
515 }
516
517 #[test]
518 fn test_property_graph_config_development() {
519 let config = PropertyGraphConfig::development();
520 assert!(config.freeze.validate().is_ok());
521 }
522
523 #[test]
524 fn test_property_graph_config_production_small() {
525 let config = PropertyGraphConfig::production_small();
526 assert!(config.freeze.validate().is_ok());
527 }
528
529 #[test]
530 fn test_property_graph_config_production_large() {
531 let config = PropertyGraphConfig::production_large();
532 assert!(config.freeze.validate().is_ok());
533 }
534
535 #[test]
536 fn test_property_graph_config_validate() {
537 let config = PropertyGraphConfig::default();
538 assert!(config.validate().is_ok());
539 }
540
541 #[test]
542 fn test_resource_config_rejects_invalid_budget_relationships() {
543 let config = PropertyGraphConfig {
544 resources: ResourceConfig {
545 index_memory_bytes: 1024,
546 max_memory_bytes: 512,
547 ..Default::default()
548 },
549 ..PropertyGraphConfig::default()
550 };
551 assert!(config.validate().is_err());
552 }
553}