Coverage Report

Created: 2026-07-31 00:13

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}