Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions config/base/raft.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,8 @@ allow_client_override = true
state_machine_sync_timeout_ms = 10

[raft.state_machine.lease]
# Enable TTL (time-to-live) key expiration feature.
# When disabled (default), no lease components are initialized — zero overhead.
# When enabled, a background worker periodically removes expired keys.
enabled = false
# TTL key expiration — always active, no enable flag needed.
# A background worker periodically removes expired keys.

# How often the background cleanup worker runs (milliseconds).
# Controls MEMORY EFFICIENCY only — expired keys are always rejected at read time
Expand Down
53 changes: 10 additions & 43 deletions d-engine-core/src/config/lease.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,16 @@
//!
//! # Design Philosophy
//!
//! - **Simple**: Single `enabled` flag - no strategy choices
//! - **Always On**: TTL is part of the public API — no feature flag needed
//! - **Best Practice**: Fixed background cleanup (industry standard)
//! - **Zero Overhead**: When disabled, no lease components are initialized
//! - **Graceful Degradation**: TTL requests are rejected when disabled
//! - **Low Overhead**: When no TTL keys exist, cleanup cycle is a single atomic load (~10ns)
//!
//! # Usage
//!
//! ```toml
//! # Enable TTL feature (default: disabled)
//! # Optional: tune cleanup behaviour (TTL is always active)
//! [raft.state_machine.lease]
//! enabled = true
//! cleanup_interval_ms = 1000 # Optional, default 1 second
//! cleanup_interval_ms = 1000 # default 1 second
//! ```

use config::ConfigError;
Expand All @@ -26,52 +24,27 @@ use serde::Serialize;
use crate::errors::Error;
use crate::errors::Result;

/// Lease configuration for TTL-based key expiration
/// Lease configuration for TTL-based key expiration.
///
/// When `enabled = true`:
/// - Server initializes lease manager at startup
/// - Background worker periodically cleans expired keys
/// - Client TTL requests (ttl_secs > 0) are accepted
///
/// When `enabled = false` (default):
/// - No lease components initialized (zero overhead)
/// - Client TTL requests (ttl_secs > 0) are rejected with error
/// - All keys are permanent (ttl_secs = 0 is always allowed)
/// TTL is always active — no enable flag. Background cleanup runs automatically.
///
/// # Performance
///
/// Background cleanup overhead: ~0.001% CPU
/// - Wakes up every `cleanup_interval_ms` (default 1000ms)
/// - Scans expired keys (limited by `max_cleanup_duration_ms`)
/// - Deletes expired entries from storage
/// - No-op when no TTL keys exist (~10ns atomic load)
/// - Deletes expired entries when keys are present
///
/// # Examples
///
/// ```rust
/// use d_engine_core::config::LeaseConfig;
///
/// // Default: disabled
/// let config = LeaseConfig::default();
/// assert!(!config.enabled);
///
/// // Enable with defaults
/// let config = LeaseConfig {
/// enabled: true,
/// ..Default::default()
/// };
/// assert_eq!(config.cleanup_interval_ms, 1000);
/// ```
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct LeaseConfig {
/// Enable TTL feature
///
/// - `true`: Initialize lease manager + start background cleanup worker
/// - `false`: No TTL support, reject requests with ttl_secs > 0
///
/// Default: false (zero overhead when TTL not needed)
#[serde(default)]
pub enabled: bool,

/// How often the background worker wakes up to scan and delete expired keys (milliseconds).
///
/// This setting controls MEMORY EFFICIENCY only, not TTL correctness.
Expand All @@ -90,7 +63,7 @@ pub struct LeaseConfig {
/// - 100-500ms: High-throughput short-TTL workloads (e.g., rate limiting, sessions)
/// - 5000-60000ms: Low-frequency TTL usage where memory pressure is not a concern
///
/// Only used when `enabled = true`
/// Always active — no enable flag needed.
#[serde(default = "default_cleanup_interval_ms")]
pub cleanup_interval_ms: u64,

Expand All @@ -107,7 +80,7 @@ pub struct LeaseConfig {
/// - 5-10ms: Aggressive cleanup when backlog exists
/// - >10ms: Not recommended (can impact foreground operations)
///
/// Only used when `enabled = true`
/// Always active — no enable flag needed.
#[serde(default = "default_max_cleanup_duration_ms")]
pub max_cleanup_duration_ms: u64,
}
Expand All @@ -123,7 +96,6 @@ fn default_max_cleanup_duration_ms() -> u64 {
impl Default for LeaseConfig {
fn default() -> Self {
Self {
enabled: false, // Default: TTL disabled (zero overhead)
cleanup_interval_ms: default_cleanup_interval_ms(),
max_cleanup_duration_ms: default_max_cleanup_duration_ms(),
}
Expand All @@ -137,11 +109,6 @@ impl LeaseConfig {
/// - `cleanup_interval_ms` is out of range (100-60000)
/// - `max_cleanup_duration_ms` is out of range (1-100)
pub fn validate(&self) -> Result<()> {
// Skip validation if disabled
if !self.enabled {
return Ok(());
}

// Validate cleanup_interval_ms
// Range: 100ms to 60s
// - Lower bound (100ms): Prevents excessive wakeups (CPU waste)
Expand Down
136 changes: 83 additions & 53 deletions d-engine-core/src/config/lease_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,91 +3,123 @@ use super::lease::LeaseConfig;
#[test]
fn test_default_config() {
let config = LeaseConfig::default();
assert!(!config.enabled); // Default: disabled
assert_eq!(config.cleanup_interval_ms, 1000);
assert_eq!(config.max_cleanup_duration_ms, 1);
assert!(config.validate().is_ok());
}

#[test]
fn test_enabled_config() {
fn test_explicit_config() {
let config = LeaseConfig {
enabled: true,
cleanup_interval_ms: 5000,
max_cleanup_duration_ms: 5,
};
assert!(config.enabled);
assert_eq!(config.cleanup_interval_ms, 5000);
assert_eq!(config.max_cleanup_duration_ms, 5);
assert!(config.validate().is_ok());
}

#[test]
fn test_disabled_config_skips_validation() {
let config = LeaseConfig {
enabled: false,
cleanup_interval_ms: 50, // Invalid (too small), but ignored when disabled
max_cleanup_duration_ms: 0, // Invalid, but ignored when disabled
};
assert!(config.validate().is_ok());
}

#[test]
fn test_validation_cleanup_interval_ms() {
let mut config = LeaseConfig {
enabled: true,
..Default::default()
};
let base = LeaseConfig::default();

// Too small
config.cleanup_interval_ms = 99;
assert!(config.validate().is_err());

assert!(
LeaseConfig {
cleanup_interval_ms: 99,
..base
}
.validate()
.is_err()
);
// Too large
config.cleanup_interval_ms = 60_001;
assert!(config.validate().is_err());

assert!(
LeaseConfig {
cleanup_interval_ms: 60_001,
..base
}
.validate()
.is_err()
);
// Valid range
config.cleanup_interval_ms = 100;
assert!(config.validate().is_ok());

config.cleanup_interval_ms = 1000;
assert!(config.validate().is_ok());

config.cleanup_interval_ms = 60_000;
assert!(config.validate().is_ok());
assert!(
LeaseConfig {
cleanup_interval_ms: 100,
..base
}
.validate()
.is_ok()
);
assert!(
LeaseConfig {
cleanup_interval_ms: 1000,
..base
}
.validate()
.is_ok()
);
assert!(
LeaseConfig {
cleanup_interval_ms: 60_000,
..base
}
.validate()
.is_ok()
);
}

#[test]
fn test_validation_max_cleanup_duration() {
let mut config = LeaseConfig {
enabled: true,
..Default::default()
};
let base = LeaseConfig::default();

// Too small
config.max_cleanup_duration_ms = 0;
assert!(config.validate().is_err());

assert!(
LeaseConfig {
max_cleanup_duration_ms: 0,
..base
}
.validate()
.is_err()
);
// Too large
config.max_cleanup_duration_ms = 101;
assert!(config.validate().is_err());

assert!(
LeaseConfig {
max_cleanup_duration_ms: 101,
..base
}
.validate()
.is_err()
);
// Valid range
config.max_cleanup_duration_ms = 1;
assert!(config.validate().is_ok());

config.max_cleanup_duration_ms = 50;
assert!(config.validate().is_ok());

config.max_cleanup_duration_ms = 100;
assert!(config.validate().is_ok());
assert!(
LeaseConfig {
max_cleanup_duration_ms: 1,
..base
}
.validate()
.is_ok()
);
assert!(
LeaseConfig {
max_cleanup_duration_ms: 50,
..base
}
.validate()
.is_ok()
);
assert!(
LeaseConfig {
max_cleanup_duration_ms: 100,
..base
}
.validate()
.is_ok()
);
}

#[test]
fn test_typical_production_config() {
let config = LeaseConfig {
enabled: true,
cleanup_interval_ms: 1000,
max_cleanup_duration_ms: 1,
};
Expand All @@ -97,7 +129,6 @@ fn test_typical_production_config() {
#[test]
fn test_aggressive_cleanup_config() {
let config = LeaseConfig {
enabled: true,
cleanup_interval_ms: 100,
max_cleanup_duration_ms: 10,
};
Expand All @@ -107,7 +138,6 @@ fn test_aggressive_cleanup_config() {
#[test]
fn test_relaxed_cleanup_config() {
let config = LeaseConfig {
enabled: true,
cleanup_interval_ms: 60_000,
max_cleanup_duration_ms: 5,
};
Expand Down
8 changes: 0 additions & 8 deletions d-engine-core/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,14 +247,6 @@ pub enum StorageError {
#[error(transparent)]
IdAllocation(#[from] IdAllocationError),

/// Feature not enabled in configuration
///
/// Returned when client requests a feature (e.g., TTL) that is not
/// enabled in the server configuration. This prevents silent failures
/// and ensures explicit feature activation.
#[error("Feature not enabled: {0}")]
FeatureNotEnabled(String),

/// State machine not serving requests
///
/// Returned when read operations are attempted during critical operations
Expand Down
1 change: 0 additions & 1 deletion d-engine-server/benches/lease_performance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ use tempfile::TempDir;
async fn create_sm_background() -> (FileStateMachine, TempDir) {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let lease_config = d_engine_core::config::LeaseConfig {
enabled: true,
cleanup_interval_ms: 1000,
max_cleanup_duration_ms: 1,
};
Expand Down
1 change: 0 additions & 1 deletion d-engine-server/benches/state_machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ async fn create_test_state_machine() -> (FileStateMachine, TempDir) {

// Enable TTL for benchmarks that need it
let lease_config = d_engine_core::config::LeaseConfig {
enabled: true,
cleanup_interval_ms: 1000,
max_cleanup_duration_ms: 1,
};
Expand Down
3 changes: 0 additions & 3 deletions d-engine-server/benches/ttl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ async fn create_test_state_machine() -> (FileStateMachine, TempDir) {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
// For TTL benchmarks, we need lease enabled
let lease_config = d_engine_core::config::LeaseConfig {
enabled: true,
cleanup_interval_ms: 1000,
max_cleanup_duration_ms: 1,
};
Expand Down Expand Up @@ -88,7 +87,6 @@ fn bench_piggyback_cleanup(c: &mut Criterion) {
// Test with different numbers of expired keys
for expired_count in [10, 50, 100, 500].iter() {
let lease_config = d_engine_core::config::LeaseConfig {
enabled: true,
cleanup_interval_ms: 1000,
max_cleanup_duration_ms: 1,
};
Expand Down Expand Up @@ -126,7 +124,6 @@ fn bench_ttl_registration(c: &mut Criterion) {
use d_engine_server::storage::DefaultLease;

let lease_config = d_engine_core::config::LeaseConfig {
enabled: true,
cleanup_interval_ms: 1000,
max_cleanup_duration_ms: 1,
};
Expand Down
Loading
Loading