d_engine_core/storage/lease.rs
1//! Lease (Time-based key expiration) trait for d-engine.
2//!
3//! Provides the interface for automatic key expiration through lease-based lifecycle management.
4//! This is a framework-level abstraction that allows custom lease implementations.
5//!
6//! # Design Philosophy
7//!
8//! d-engine provides lease-based key expiration management as a framework-level
9//! feature that all state machines (including custom implementations) can leverage.
10//!
11//! # Architecture
12//!
13//! - **Trait-based**: `Lease` trait defines the interface
14//! - **Implementation**: d-engine-server provides `DefaultLease` with high-performance dual-index
15//! design
16//! - **Zero overhead**: Completely disabled when not used
17//! - **Snapshot support**: Full persistence through Raft snapshots
18//!
19//! # Concurrency Model
20//!
21//! Implementations should follow these guidelines:
22//! - **Read path (hot)**: Lock-free, supports high concurrency
23//! - **Write path (cold)**: Single-threaded (CommitHandler), Mutex acceptable
24//! - **Read-write**: Concurrent safe, reads don't block on cleanup
25
26use std::time::SystemTime;
27
28use bytes::Bytes;
29
30use crate::Result;
31
32/// Lease management interface for key expiration.
33///
34/// Manages key lifecycles through time-based leases. d-engine provides a default
35/// implementation (`DefaultLease` in d-engine-server), but developers can implement
36/// custom lease management strategies.
37///
38/// # Thread Safety
39///
40/// All methods must be thread-safe as they will be called concurrently from:
41/// - Read path: Multiple concurrent client reads
42/// - Write path: Single-threaded apply operations
43///
44/// # Performance Requirements
45///
46/// - `is_expired()`: Must be O(1) and lock-free (hot path, called on every read)
47/// - `register()` / `unregister()`: Should be O(log N) or better
48/// - `get_expired_keys()`: Should be O(K log N) where K = expired keys
49///
50/// # Example Implementation
51///
52/// ```ignore
53/// use d_engine_core::storage::Lease;
54/// use bytes::Bytes;
55/// use std::time::SystemTime;
56///
57/// struct MyCustomLease {
58/// // Your data structures
59/// }
60///
61/// impl Lease for MyCustomLease {
62/// fn register(&self, key: Bytes, ttl_secs: u64) {
63/// // Your implementation
64/// }
65///
66/// fn is_expired(&self, key: &[u8]) -> bool {
67/// // Your implementation
68/// }
69///
70/// // ... other methods
71/// }
72/// ```
73pub trait Lease: Send + Sync + 'static {
74 /// Register a lease for a key.
75 ///
76 /// If the key already has a lease, updates to the new expiration time.
77 ///
78 /// # Arguments
79 /// * `key` - Key to set expiration for
80 /// * `ttl_secs` - Time-to-live in seconds from now
81 ///
82 /// # Performance
83 /// Should be O(log N) or better. Called on every insert with TTL.
84 fn register(
85 &self,
86 key: Bytes,
87 ttl_secs: u64,
88 );
89
90 /// Remove lease for a key (on update/delete).
91 ///
92 /// Called when a key is updated without TTL or explicitly deleted.
93 ///
94 /// # Performance
95 /// Should be O(log N) or better.
96 fn unregister(
97 &self,
98 key: &[u8],
99 );
100
101 /// Check if a key's lease has expired.
102 ///
103 /// # Returns
104 /// * `true` - Key has expired and should be treated as non-existent
105 /// * `false` - Key has not expired or has no lease
106 ///
107 /// # Performance
108 /// **CRITICAL**: Must be O(1) and lock-free. This is the hot path,
109 /// called on every read operation.
110 fn is_expired(
111 &self,
112 key: &[u8],
113 ) -> bool;
114
115 /// Get all keys with expired leases.
116 ///
117 /// Removes returned keys from internal lease indexes.
118 ///
119 /// # Arguments
120 /// * `now` - Current time to check expiration against
121 ///
122 /// # Returns
123 /// List of expired keys (keys are removed from lease tracking)
124 ///
125 /// # Performance
126 /// Should be O(K log N) where K = number of expired keys.
127 fn get_expired_keys(
128 &self,
129 now: SystemTime,
130 ) -> Vec<Bytes>;
131
132 /// Called on every apply operation (piggyback cleanup).
133 ///
134 /// The lease implementation decides internally whether to perform cleanup
135 /// based on its configuration (e.g., piggyback strategy with frequency control).
136 ///
137 /// # Returns
138 /// List of expired keys that were cleaned up (may be empty)
139 ///
140 /// # Performance
141 /// Should be O(1) most of the time (fast path when not cleaning).
142 fn on_apply(&self) -> Vec<Bytes>;
143
144 /// Check if any key has ever been registered with a lease.
145 ///
146 /// Used for fast-path optimization to skip lease logic entirely when
147 /// leases are not used.
148 ///
149 /// # Returns
150 /// * `true` - At least one key has been registered (even if expired)
151 /// * `false` - No keys have ever been registered
152 ///
153 /// # Performance
154 /// Must be O(1) - simple flag check.
155 fn has_lease_keys(&self) -> bool;
156
157 /// Fast check: returns true if there might be expired keys.
158 ///
159 /// This is an O(1) optimization to avoid full expired key scan.
160 ///
161 /// # Returns
162 /// * `false` - Definitely no expired keys (fast path)
163 /// * `true` - Maybe has expired keys (need full check)
164 ///
165 /// # Performance
166 /// Should be O(1) or O(log N) at most.
167 fn may_have_expired_keys(
168 &self,
169 now: SystemTime,
170 ) -> bool;
171
172 /// Get number of keys with active leases.
173 ///
174 /// # Performance
175 /// Should be O(1).
176 fn len(&self) -> usize;
177
178 /// Check if no keys have active leases.
179 fn is_empty(&self) -> bool {
180 self.len() == 0
181 }
182
183 /// Serialize lease state for snapshot.
184 ///
185 /// Used by Raft snapshot mechanism to persist lease metadata.
186 ///
187 /// # Returns
188 /// Serialized bytes (format is implementation-defined)
189 fn to_snapshot(&self) -> Vec<u8>;
190
191 /// Reload lease state from snapshot data.
192 ///
193 /// Used during snapshot restoration. Should filter out already-expired leases.
194 ///
195 /// # Arguments
196 /// * `data` - Serialized snapshot data
197 ///
198 /// # Errors
199 /// Returns error if deserialization fails.
200 fn reload(
201 &self,
202 data: &[u8],
203 ) -> Result<()>;
204}