/home/runner/work/feoxdb/feoxdb/src/core/store/ttl.rs
Line | Count | Source |
1 | | use bytes::Bytes; |
2 | | use std::sync::atomic::Ordering; |
3 | | use std::sync::Arc; |
4 | | use std::time::{SystemTime, UNIX_EPOCH}; |
5 | | |
6 | | use crate::core::record::Record; |
7 | | use crate::core::ttl_sweep::TtlConfig; |
8 | | use crate::error::{FeoxError, Result}; |
9 | | |
10 | | use super::FeoxStore; |
11 | | |
12 | | enum TtlReplacementValue { |
13 | | Resident(Bytes), |
14 | | Deferred(Arc<Record>), |
15 | | } |
16 | | |
17 | | impl FeoxStore { |
18 | | /// Insert or update a key-value pair with TTL (Time-To-Live). |
19 | | /// |
20 | | /// # Arguments |
21 | | /// |
22 | | /// * `key` - The key to insert |
23 | | /// * `value` - The value to store |
24 | | /// * `ttl_seconds` - Time-to-live in seconds |
25 | | /// |
26 | | /// # Returns |
27 | | /// |
28 | | /// Returns `Ok(())` if successful. |
29 | | /// |
30 | | /// # Example |
31 | | /// |
32 | | /// ```rust |
33 | | /// # use feoxdb::FeoxStore; |
34 | | /// # fn main() -> feoxdb::Result<()> { |
35 | | /// # let store = FeoxStore::builder().enable_ttl(true).build()?; |
36 | | /// // Key expires after 60 seconds |
37 | | /// store.insert_with_ttl(b"session:123", b"data", 60)?; |
38 | | /// # Ok(()) |
39 | | /// # } |
40 | | /// ``` |
41 | | /// |
42 | | /// # Performance |
43 | | /// |
44 | | /// * Memory mode: ~800ns |
45 | | /// * Persistent mode: ~1µs (buffered write) |
46 | 45 | pub fn insert_with_ttl(&self, key: &[u8], value: &[u8], ttl_seconds: u64) -> Result<bool> { |
47 | 45 | if !self.enable_ttl { |
48 | 2 | return Err(FeoxError::TtlNotEnabled); |
49 | 43 | } |
50 | 43 | self.insert_with_ttl_and_timestamp(key, value, ttl_seconds, None) |
51 | 45 | } |
52 | | |
53 | | /// Insert or update a key-value pair with TTL and explicit timestamp. |
54 | | /// |
55 | | /// # Arguments |
56 | | /// |
57 | | /// * `key` - The key to insert |
58 | | /// * `value` - The value to store |
59 | | /// * `ttl_seconds` - Time-to-live in seconds |
60 | | /// * `timestamp` - Optional timestamp for conflict resolution. If `None`, uses current time. |
61 | | /// |
62 | | /// # Returns |
63 | | /// |
64 | | /// Returns `Ok(())` if successful. |
65 | 51 | pub fn insert_with_ttl_and_timestamp( |
66 | 51 | &self, |
67 | 51 | key: &[u8], |
68 | 51 | value: &[u8], |
69 | 51 | ttl_seconds: u64, |
70 | 51 | timestamp: Option<u64>, |
71 | 51 | ) -> Result<bool> { |
72 | 51 | if !self.enable_ttl { |
73 | 1 | return Err(FeoxError::TtlNotEnabled); |
74 | 50 | } |
75 | 50 | self.ensure_ttl_write_supported()?1 ; |
76 | 49 | self.insert_with_timestamp_and_ttl_internal(key, value, timestamp, ttl_seconds) |
77 | 51 | } |
78 | | |
79 | | /// Insert or update a key-value pair with TTL using zero-copy Bytes. |
80 | | /// |
81 | | /// This method avoids copying the value data by directly using the Bytes type, |
82 | | /// which provides reference-counted zero-copy semantics. |
83 | | /// |
84 | | /// # Arguments |
85 | | /// |
86 | | /// * `key` - The key to insert |
87 | | /// * `value` - The value to store as Bytes |
88 | | /// * `ttl_seconds` - Time-to-live in seconds |
89 | | /// |
90 | | /// # Returns |
91 | | /// |
92 | | /// Returns `Ok(())` if successful. |
93 | | /// |
94 | | /// # Example |
95 | | /// |
96 | | /// ```rust |
97 | | /// # use feoxdb::FeoxStore; |
98 | | /// # use bytes::Bytes; |
99 | | /// # fn main() -> feoxdb::Result<()> { |
100 | | /// # let store = FeoxStore::builder().enable_ttl(true).build()?; |
101 | | /// let data = Bytes::from_static(b"session_data"); |
102 | | /// // Key expires after 60 seconds |
103 | | /// store.insert_bytes_with_ttl(b"session:123", data, 60)?; |
104 | | /// # Ok(()) |
105 | | /// # } |
106 | | /// ``` |
107 | | /// |
108 | | /// # Performance |
109 | | /// |
110 | | /// * Memory mode: ~800ns (avoids value copy) |
111 | | /// * Persistent mode: ~1µs (buffered write, avoids value copy) |
112 | 3 | pub fn insert_bytes_with_ttl( |
113 | 3 | &self, |
114 | 3 | key: &[u8], |
115 | 3 | value: Bytes, |
116 | 3 | ttl_seconds: u64, |
117 | 3 | ) -> Result<bool> { |
118 | 3 | if !self.enable_ttl { |
119 | 1 | return Err(FeoxError::TtlNotEnabled); |
120 | 2 | } |
121 | 2 | self.insert_bytes_with_ttl_and_timestamp(key, value, ttl_seconds, None) |
122 | 3 | } |
123 | | |
124 | | /// Insert or update a key-value pair with TTL and explicit timestamp using zero-copy Bytes. |
125 | | /// |
126 | | /// # Arguments |
127 | | /// |
128 | | /// * `key` - The key to insert |
129 | | /// * `value` - The value to store as Bytes |
130 | | /// * `ttl_seconds` - Time-to-live in seconds |
131 | | /// * `timestamp` - Optional timestamp for conflict resolution. If `None`, uses current time. |
132 | | /// |
133 | | /// # Returns |
134 | | /// |
135 | | /// Returns `Ok(())` if successful. |
136 | 5 | pub fn insert_bytes_with_ttl_and_timestamp( |
137 | 5 | &self, |
138 | 5 | key: &[u8], |
139 | 5 | value: Bytes, |
140 | 5 | ttl_seconds: u64, |
141 | 5 | timestamp: Option<u64>, |
142 | 5 | ) -> Result<bool> { |
143 | 5 | if !self.enable_ttl { |
144 | 0 | return Err(FeoxError::TtlNotEnabled); |
145 | 5 | } |
146 | 5 | self.ensure_ttl_write_supported()?0 ; |
147 | 5 | self.insert_bytes_with_timestamp_and_ttl_internal(key, value, timestamp, ttl_seconds) |
148 | 5 | } |
149 | | |
150 | | /// Get the remaining TTL (Time-To-Live) for a key in seconds. |
151 | | /// |
152 | | /// # Arguments |
153 | | /// |
154 | | /// * `key` - The key to check |
155 | | /// |
156 | | /// # Returns |
157 | | /// |
158 | | /// Returns `Some(seconds)` if the key has TTL set, `None` if no TTL or key not found. |
159 | | /// |
160 | | /// # Example |
161 | | /// |
162 | | /// ```rust |
163 | | /// # use feoxdb::FeoxStore; |
164 | | /// # fn main() -> feoxdb::Result<()> { |
165 | | /// # let store = FeoxStore::builder().enable_ttl(true).build()?; |
166 | | /// store.insert_with_ttl(b"session", b"data", 3600)?; |
167 | | /// |
168 | | /// // Check remaining TTL |
169 | | /// if let Ok(Some(ttl)) = store.get_ttl(b"session") { |
170 | | /// println!("Session expires in {} seconds", ttl); |
171 | | /// } |
172 | | /// # Ok(()) |
173 | | /// # } |
174 | | /// ``` |
175 | 13 | pub fn get_ttl(&self, key: &[u8]) -> Result<Option<u64>> { |
176 | 13 | if !self.enable_ttl { |
177 | 1 | return Err(FeoxError::TtlNotEnabled); |
178 | 12 | } |
179 | 12 | self.validate_key(key)?0 ; |
180 | | |
181 | 12 | let record = self |
182 | 12 | .hash_table |
183 | 12 | .read(key, |_, v| v.clone()) |
184 | 12 | .ok_or(FeoxError::KeyNotFound)?0 ; |
185 | 12 | let ttl_expiry = record.ttl_expiry.load(Ordering::Acquire); |
186 | | |
187 | 12 | if ttl_expiry == 0 { |
188 | 6 | return Ok(None); // No TTL set |
189 | 6 | } |
190 | | |
191 | 6 | let now = self.get_timestamp_pub(); |
192 | 6 | if now >= ttl_expiry { |
193 | 0 | return Ok(Some(0)); // Already expired |
194 | 6 | } |
195 | | |
196 | | // Return remaining seconds |
197 | 6 | Ok(Some((ttl_expiry - now) / 1_000_000_000)) |
198 | 13 | } |
199 | | |
200 | | /// Update the TTL for an existing key. |
201 | | /// |
202 | | /// # Arguments |
203 | | /// |
204 | | /// * `key` - The key to update |
205 | | /// * `ttl_seconds` - New TTL in seconds (0 to remove TTL) |
206 | | /// |
207 | | /// # Returns |
208 | | /// |
209 | | /// Returns `Ok(())` if successful. |
210 | | /// |
211 | | /// # Errors |
212 | | /// |
213 | | /// * `KeyNotFound` - Key does not exist |
214 | | /// |
215 | | /// # Example |
216 | | /// |
217 | | /// ```rust |
218 | | /// # use feoxdb::FeoxStore; |
219 | | /// # fn main() -> feoxdb::Result<()> { |
220 | | /// # let store = FeoxStore::builder().enable_ttl(true).build()?; |
221 | | /// # store.insert(b"key", b"value")?; |
222 | | /// // Extend TTL to 1 hour |
223 | | /// store.update_ttl(b"key", 3600)?; |
224 | | /// # Ok(()) |
225 | | /// # } |
226 | | /// ``` |
227 | 17 | pub fn update_ttl(&self, key: &[u8], ttl_seconds: u64) -> Result<()> { |
228 | 17 | if !self.enable_ttl { |
229 | 1 | return Err(FeoxError::TtlNotEnabled); |
230 | 16 | } |
231 | 16 | self.ensure_ttl_write_supported()?2 ; |
232 | 14 | self.validate_key(key)?0 ; |
233 | | |
234 | 14 | let (new_record11 , old_record11 , cache_guarded11 ) = self |
235 | 14 | .hash_table |
236 | 14 | .update(key, |stored_key, current| { |
237 | 14 | let old_record = Arc::clone(current); |
238 | 14 | let old_expiry = old_record.ttl_expiry.load(Ordering::Acquire); |
239 | 14 | let now = self.get_timestamp_pub(); |
240 | 14 | if old_expiry > 0 && now > old_expiry10 { |
241 | 3 | return Err(FeoxError::KeyNotFound); |
242 | 11 | } |
243 | 11 | let timestamp = self.version_clock.next(stored_key, now).max( |
244 | 11 | old_record |
245 | 11 | .timestamp |
246 | 11 | .checked_add(1) |
247 | 11 | .ok_or(FeoxError::OlderTimestamp)?0 , |
248 | | ); |
249 | 11 | let expiry = ttl_expiry(now, ttl_seconds); |
250 | 11 | let resident = old_record.get_value(); |
251 | 11 | let cache_entry = resident.is_none().then(|| {6 |
252 | 6 | self.cache |
253 | 6 | .as_ref() |
254 | 6 | .map(|cache| cache4 .record_entry4 (stored_key4 , &old_record4 )) |
255 | 6 | }); |
256 | 11 | let cache_entry = cache_entry.flatten(); |
257 | 11 | let cache_guarded = cache_entry.is_some(); |
258 | 11 | let value = Self::ttl_replacement_value( |
259 | 11 | &old_record, |
260 | 11 | resident.or_else(|| cache_entry6 .as_ref6 ().and_then6 (|entry| entry4 .value4 ())), |
261 | | ); |
262 | 11 | let new_record = match value5 { |
263 | 5 | TtlReplacementValue::Resident(value1 ) if expiry == 01 => { |
264 | 1 | Arc::new(Record::new_from_bytes(stored_key.clone(), value, timestamp)) |
265 | | } |
266 | 4 | TtlReplacementValue::Resident(value) => { |
267 | 4 | Arc::new(Record::new_from_bytes_with_ttl( |
268 | 4 | stored_key.clone(), |
269 | 4 | value, |
270 | 4 | timestamp, |
271 | 4 | expiry, |
272 | | )) |
273 | | } |
274 | 6 | TtlReplacementValue::Deferred(predecessor) => Arc::new( |
275 | 6 | Record::new_deferred_with_ttl(&predecessor, timestamp, expiry), |
276 | | ), |
277 | | }; |
278 | | |
279 | 11 | old_record.link_successor(&new_record); |
280 | 11 | old_record.refcount.store(0, Ordering::Release); |
281 | 11 | *current = Arc::clone(&new_record); |
282 | 11 | if let Some(entry4 ) = cache_entry { |
283 | 4 | entry.remove(); |
284 | 7 | } |
285 | 11 | self.publish_to_tree(stored_key, Arc::clone(&new_record)); |
286 | 11 | self.note_ttl_transition(old_expiry, expiry); |
287 | | |
288 | 11 | Ok((new_record, old_record, cache_guarded)) |
289 | 14 | }) |
290 | 14 | .ok_or(FeoxError::KeyNotFound)?0 ?3 ; |
291 | | |
292 | 11 | if !cache_guarded { |
293 | 7 | self.remove_cached(key, &old_record); |
294 | 7 | }4 |
295 | | |
296 | 11 | if let Some(write_buffer7 ) = self.write_buffer.as_ref() { |
297 | 7 | write_buffer.add_replacement(new_record, old_record)?0 ; |
298 | 4 | } |
299 | | |
300 | 11 | Ok(()) |
301 | 17 | } |
302 | | |
303 | 11 | fn ttl_replacement_value(record: &Arc<Record>, value: Option<Bytes>) -> TtlReplacementValue { |
304 | 11 | if let Some(value5 ) = value { |
305 | 5 | return TtlReplacementValue::Resident(value); |
306 | 6 | } |
307 | | |
308 | 6 | let predecessor = Arc::clone(record); |
309 | | #[cfg(test)] |
310 | 6 | crate::test_hooks::pause_at(crate::test_hooks::AFTER_TTL_DEFERRED_SOURCE); |
311 | 6 | TtlReplacementValue::Deferred(predecessor) |
312 | 11 | } |
313 | | |
314 | 33.6k | pub(super) fn note_ttl_transition(&self, previous: u64, current: u64) { |
315 | 33.6k | match (previous > 0, current > 0) { |
316 | 319 | (false, true) => { |
317 | 319 | self.stats.keys_with_ttl.fetch_add(1, Ordering::Relaxed); |
318 | 319 | } |
319 | | (true, false) => { |
320 | 310 | let _ = self.stats.keys_with_ttl.fetch_update( |
321 | 310 | Ordering::Relaxed, |
322 | 310 | Ordering::Relaxed, |
323 | 310 | |count| Some(count.saturating_sub(1)), |
324 | | ); |
325 | | } |
326 | 33.0k | _ => {} |
327 | | } |
328 | 33.6k | } |
329 | | |
330 | 76 | pub(super) fn ensure_ttl_write_supported(&self) -> Result<()> { |
331 | 76 | if !self.memory_only && self.format_version == 112 { |
332 | 3 | return Err(FeoxError::Unsupported); |
333 | 73 | } |
334 | 73 | Ok(()) |
335 | 76 | } |
336 | | |
337 | | /// Remove TTL from a key, making it persistent. |
338 | | /// |
339 | | /// # Arguments |
340 | | /// |
341 | | /// * `key` - The key to persist |
342 | | /// |
343 | | /// # Returns |
344 | | /// |
345 | | /// Returns `Ok(())` if successful. |
346 | | /// |
347 | | /// # Errors |
348 | | /// |
349 | | /// * `KeyNotFound` - Key does not exist |
350 | | /// |
351 | | /// # Example |
352 | | /// |
353 | | /// ```rust |
354 | | /// # use feoxdb::FeoxStore; |
355 | | /// # fn main() -> feoxdb::Result<()> { |
356 | | /// # let store = FeoxStore::builder().enable_ttl(true).build()?; |
357 | | /// # store.insert_with_ttl(b"temp", b"data", 60)?; |
358 | | /// // Remove TTL, make permanent |
359 | | /// store.persist(b"temp")?; |
360 | | /// # Ok(()) |
361 | | /// # } |
362 | | /// ``` |
363 | 5 | pub fn persist(&self, key: &[u8]) -> Result<()> { |
364 | 5 | if !self.enable_ttl { |
365 | 1 | return Err(FeoxError::TtlNotEnabled); |
366 | 4 | } |
367 | 4 | self.update_ttl(key, 0) |
368 | 5 | } |
369 | | |
370 | | /// Start the TTL sweeper if configured |
371 | | /// This must be called with an `Arc<Self>` after construction |
372 | 0 | pub fn start_ttl_sweeper(self: &Arc<Self>, config: Option<TtlConfig>) { |
373 | | // Only start TTL sweeper if TTL is enabled |
374 | 0 | if !self.enable_ttl { |
375 | 0 | return; |
376 | 0 | } |
377 | | |
378 | 0 | let ttl_config = config.unwrap_or_else(|| { |
379 | 0 | if self.memory_only { |
380 | 0 | TtlConfig::default_memory() |
381 | | } else { |
382 | 0 | TtlConfig::default_persistent() |
383 | | } |
384 | 0 | }); |
385 | | |
386 | 0 | if ttl_config.enabled { |
387 | 0 | let weak_store = Arc::downgrade(self); |
388 | 0 | let mut sweeper = crate::core::ttl_sweep::TtlSweeper::new(weak_store, ttl_config); |
389 | 0 | sweeper.start(); |
390 | 0 |
|
391 | 0 | // Store the sweeper |
392 | 0 | *self.ttl_sweeper.write() = Some(sweeper); |
393 | 0 | } |
394 | 0 | } |
395 | | |
396 | | /// Get current timestamp (public for TTL cleaner) |
397 | 33.7k | pub fn get_timestamp_pub(&self) -> u64 { |
398 | 33.7k | SystemTime::now() |
399 | 33.7k | .duration_since(UNIX_EPOCH) |
400 | 33.7k | .unwrap() |
401 | 33.7k | .as_nanos() as u64 |
402 | 33.7k | } |
403 | | } |
404 | | |
405 | | #[inline] |
406 | 11 | fn ttl_expiry(timestamp: u64, ttl_seconds: u64) -> u64 { |
407 | 11 | if ttl_seconds == 0 { |
408 | 2 | 0 |
409 | | } else { |
410 | 9 | timestamp.saturating_add(ttl_seconds.saturating_mul(1_000_000_000)) |
411 | | } |
412 | 11 | } |