peat_mesh/storage/ttl.rs
1//! TTL and Data Lifecycle Management
2//!
3//! Provides helpers for managing document lifecycle:
4//! - **Soft-delete pattern**: Avoids husking on high-churn data (beacons, positions)
5//! - **EVICT helpers**: Local storage cleanup for edge devices
6//! - **Tombstone TTL config**: Configure tombstone reaping interval
7//! - **Offline retention**: Connectivity-aware eviction policies
8//!
9//! ## Deletion Model
10//!
11//! Two deletion mechanisms:
12//!
13//! 1. **DELETE/EVICT**: Creates tombstones that sync mesh-wide and auto-reap after `TOMBSTONE_TTL_HOURS`
14//! 2. **EVICT (local)**: Removes documents from local storage only (no tombstone, may re-sync)
15//!
16//! ## Architectural Challenges
17//!
18//! ### Delete-Then-Disconnect Problem
19//!
20//! When a node deletes a document then goes offline, other nodes that were offline
21//! during the delete may resurrect the document when they come back online with stale data.
22//!
23//! **Solution**: Tombstones persist for `TOMBSTONE_TTL_HOURS`, ensuring deletions propagate
24//! even to nodes offline during the delete operation.
25//!
26//! ### Concurrent Delete-Update (Husking)
27//!
28//! When one node updates a field while another deletes the document, CRDT merge can
29//! create "husked documents" where updated fields exist but all others are null.
30//!
31//! **Solution**: Use soft-delete pattern for high-churn data (update `_deleted` flag instead
32//! of deleting).
33//!
34//! ## Usage
35//!
36//! ```ignore
37//! use peat_mesh::storage::ttl::{TtlConfig, EvictionStrategy};
38//! use std::time::Duration;
39//!
40//! // Configure for tactical operations
41//! let config = TtlConfig::tactical();
42//!
43//! // Or custom configuration
44//! let config = TtlConfig::new()
45//! .with_beacon_ttl(Duration::from_secs(300)) // 5 min soft-delete
46//! .with_eviction(EvictionStrategy::OldestFirst);
47//! ```
48
49use std::time::Duration;
50
51/// TTL configuration for data lifecycle management
52///
53/// Provides collection-specific TTLs and eviction strategies that coordinate
54/// with tombstone TTL-based reaping.
55#[derive(Debug, Clone)]
56pub struct TtlConfig {
57 /// Tombstone TTL (hours)
58 ///
59 /// Controls how long tombstones persist before being reaped.
60 pub tombstone_ttl_hours: u32,
61
62 /// Enable automatic tombstone reaping
63 pub tombstone_reaping_enabled: bool,
64
65 /// Days between tombstone reaping scans (default: 1)
66 pub days_between_reaping: u32,
67
68 /// Beacon soft-delete TTL (avoids husking)
69 ///
70 /// Beacons are high-churn data. Use soft-delete pattern to mark as deleted
71 /// rather than creating tombstones.
72 pub beacon_ttl: Duration,
73
74 /// Node position soft-delete TTL (avoids husking)
75 pub position_ttl: Duration,
76
77 /// Capability hard-delete TTL
78 ///
79 /// Capabilities are low-churn, coordinated updates. Safe to use hard delete (EVICT).
80 pub capability_ttl: Duration,
81
82 /// EVICT strategy for edge devices with storage constraints
83 pub evict_strategy: EvictionStrategy,
84
85 /// Offline retention policy (optional)
86 pub offline_policy: Option<OfflineRetentionPolicy>,
87}
88
89/// Eviction strategy when storage limits are reached or node is offline
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91#[allow(clippy::upper_case_acronyms)]
92pub enum EvictionStrategy {
93 /// Evict oldest documents first (by last_updated_at)
94 OldestFirst,
95
96 /// Evict based on storage pressure threshold
97 StoragePressure {
98 /// Percentage threshold (0-100) to trigger eviction
99 threshold_pct: u8,
100 },
101
102 /// Keep only last N documents per collection
103 KeepLastN(usize),
104
105 /// No automatic eviction
106 None,
107}
108
109/// Offline retention policy for connectivity-aware lifecycle
110///
111/// When a node is offline, it cannot contribute to the mesh anyway.
112/// More aggressive eviction saves storage and battery while preserving
113/// the ability to re-sync latest state from peers when reconnected.
114#[derive(Debug, Clone)]
115pub struct OfflineRetentionPolicy {
116 /// TTL when node is online (normal retention)
117 pub online_ttl: Duration,
118
119 /// TTL when node is offline (aggressive eviction)
120 ///
121 /// Typically 10x shorter than online_ttl to free resources
122 pub offline_ttl: Duration,
123
124 /// Always keep last N items per collection, even when offline
125 pub keep_last_n: usize,
126}
127
128impl TtlConfig {
129 /// Create a new TTL configuration with conservative defaults
130 pub fn new() -> Self {
131 Self {
132 tombstone_ttl_hours: 168, // 7 days (Edge SDK default)
133 tombstone_reaping_enabled: true,
134 days_between_reaping: 1,
135 beacon_ttl: Duration::from_secs(600), // 10 min
136 position_ttl: Duration::from_secs(600), // 10 min
137 capability_ttl: Duration::from_secs(7200), // 2 hours
138 evict_strategy: EvictionStrategy::None,
139 offline_policy: None,
140 }
141 }
142
143 /// Tactical operations preset (high-frequency updates, short TTLs)
144 ///
145 /// Optimized for:
146 /// - Rapid beacon discovery (5 min TTL)
147 /// - Frequent position updates (10 min TTL)
148 /// - Short-lived capabilities (2 hour TTL)
149 /// - Offline nodes keep only last 10 items
150 pub fn tactical() -> Self {
151 Self {
152 tombstone_ttl_hours: 168, // 7 days
153 tombstone_reaping_enabled: true,
154 days_between_reaping: 1,
155 beacon_ttl: Duration::from_secs(300), // 5 min soft-delete
156 position_ttl: Duration::from_secs(600), // 10 min soft-delete
157 capability_ttl: Duration::from_secs(7200), // 2 hours
158 evict_strategy: EvictionStrategy::OldestFirst,
159 offline_policy: Some(OfflineRetentionPolicy {
160 online_ttl: Duration::from_secs(600), // 10 min
161 offline_ttl: Duration::from_secs(60), // 1 min when offline
162 keep_last_n: 10,
163 }),
164 }
165 }
166
167 /// Long-duration operations preset (ISR, surveillance)
168 ///
169 /// Optimized for:
170 /// - Longer beacon retention (10 min)
171 /// - Extended position history (1 hour)
172 /// - Long-lived capabilities (48 hours)
173 pub fn long_duration() -> Self {
174 Self {
175 tombstone_ttl_hours: 168, // 7 days
176 tombstone_reaping_enabled: true,
177 days_between_reaping: 1,
178 beacon_ttl: Duration::from_secs(600), // 10 min
179 position_ttl: Duration::from_secs(3600), // 1 hour
180 capability_ttl: Duration::from_secs(172800), // 48 hours
181 evict_strategy: EvictionStrategy::StoragePressure { threshold_pct: 80 },
182 offline_policy: None, // No special offline handling
183 }
184 }
185
186 /// Offline/storage-constrained node preset
187 ///
188 /// Optimized for:
189 /// - Minimal storage footprint
190 /// - Aggressive local eviction (EVICT, not DELETE)
191 /// - Short tombstone TTL (3 days instead of 7)
192 pub fn offline_node() -> Self {
193 Self {
194 tombstone_ttl_hours: 72, // 3 days (shorter for storage-constrained edge)
195 tombstone_reaping_enabled: true,
196 days_between_reaping: 1,
197 beacon_ttl: Duration::from_secs(30), // 30 sec EVICT
198 position_ttl: Duration::from_secs(60), // 1 min EVICT
199 capability_ttl: Duration::from_secs(300), // 5 min EVICT
200 evict_strategy: EvictionStrategy::KeepLastN(10),
201 offline_policy: Some(OfflineRetentionPolicy {
202 online_ttl: Duration::from_secs(300), // 5 min
203 offline_ttl: Duration::from_secs(30), // 30 sec when offline
204 keep_last_n: 5, // Minimal retention
205 }),
206 }
207 }
208
209 /// Set beacon soft-delete TTL
210 pub fn with_beacon_ttl(mut self, ttl: Duration) -> Self {
211 self.beacon_ttl = ttl;
212 self
213 }
214
215 /// Set position soft-delete TTL
216 pub fn with_position_ttl(mut self, ttl: Duration) -> Self {
217 self.position_ttl = ttl;
218 self
219 }
220
221 /// Set capability hard-delete TTL
222 pub fn with_capability_ttl(mut self, ttl: Duration) -> Self {
223 self.capability_ttl = ttl;
224 self
225 }
226
227 /// Set eviction strategy
228 pub fn with_eviction(mut self, strategy: EvictionStrategy) -> Self {
229 self.evict_strategy = strategy;
230 self
231 }
232
233 /// Set offline retention policy
234 pub fn with_offline_policy(mut self, policy: OfflineRetentionPolicy) -> Self {
235 self.offline_policy = Some(policy);
236 self
237 }
238
239 /// Set tombstone TTL (hours)
240 pub fn with_tombstone_ttl(mut self, hours: u32) -> Self {
241 self.tombstone_ttl_hours = hours;
242 self
243 }
244
245 /// Get TTL for a specific collection
246 ///
247 /// Returns the appropriate TTL based on collection name and current configuration.
248 pub fn get_collection_ttl(&self, collection: &str) -> Option<Duration> {
249 match collection {
250 "beacons" => Some(self.beacon_ttl),
251 "node_positions" => Some(self.position_ttl),
252 "capabilities" => Some(self.capability_ttl),
253 "hierarchical_commands" => None, // Manual expiration via TimeoutManager
254 "cells" => Some(Duration::from_secs(3600)), // 1 hour
255 _ => None,
256 }
257 }
258}
259
260impl Default for TtlConfig {
261 fn default() -> Self {
262 Self::new()
263 }
264}
265
266impl OfflineRetentionPolicy {
267 /// Create a minimal retention policy for storage-constrained devices
268 pub fn minimal() -> Self {
269 Self {
270 online_ttl: Duration::from_secs(300), // 5 min
271 offline_ttl: Duration::from_secs(30), // 30 sec
272 keep_last_n: 5,
273 }
274 }
275
276 /// Create a conservative retention policy
277 pub fn conservative() -> Self {
278 Self {
279 online_ttl: Duration::from_secs(3600), // 1 hour
280 offline_ttl: Duration::from_secs(300), // 5 min
281 keep_last_n: 20,
282 }
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn test_tactical_preset() {
292 let config = TtlConfig::tactical();
293
294 assert_eq!(config.tombstone_ttl_hours, 168); // 7 days
295 assert_eq!(config.beacon_ttl, Duration::from_secs(300)); // 5 min
296 assert_eq!(config.position_ttl, Duration::from_secs(600)); // 10 min
297 assert!(config.offline_policy.is_some());
298
299 let offline = config.offline_policy.unwrap();
300 assert_eq!(offline.keep_last_n, 10);
301 }
302
303 #[test]
304 fn test_long_duration_preset() {
305 let config = TtlConfig::long_duration();
306
307 assert_eq!(config.beacon_ttl, Duration::from_secs(600)); // 10 min
308 assert_eq!(config.position_ttl, Duration::from_secs(3600)); // 1 hour
309 assert_eq!(config.capability_ttl, Duration::from_secs(172800)); // 48 hours
310 assert!(config.offline_policy.is_none());
311 }
312
313 #[test]
314 fn test_offline_node_preset() {
315 let config = TtlConfig::offline_node();
316
317 assert_eq!(config.tombstone_ttl_hours, 72); // 3 days (shorter)
318 assert_eq!(config.beacon_ttl, Duration::from_secs(30)); // 30 sec
319 assert_eq!(config.position_ttl, Duration::from_secs(60)); // 1 min
320
321 match config.evict_strategy {
322 EvictionStrategy::KeepLastN(n) => assert_eq!(n, 10),
323 _ => panic!("Expected KeepLastN strategy"),
324 }
325 }
326
327 #[test]
328 fn test_collection_ttl_lookup() {
329 let config = TtlConfig::tactical();
330
331 assert_eq!(
332 config.get_collection_ttl("beacons"),
333 Some(Duration::from_secs(300))
334 );
335 assert_eq!(
336 config.get_collection_ttl("node_positions"),
337 Some(Duration::from_secs(600))
338 );
339 assert_eq!(config.get_collection_ttl("hierarchical_commands"), None);
340 }
341
342 #[test]
343 fn test_builder_pattern() {
344 let config = TtlConfig::new()
345 .with_beacon_ttl(Duration::from_secs(120))
346 .with_eviction(EvictionStrategy::OldestFirst)
347 .with_tombstone_ttl(96); // 4 days
348
349 assert_eq!(config.beacon_ttl, Duration::from_secs(120));
350 assert_eq!(config.tombstone_ttl_hours, 96);
351 assert!(matches!(
352 config.evict_strategy,
353 EvictionStrategy::OldestFirst
354 ));
355 }
356
357 #[test]
358 fn test_offline_retention_presets() {
359 let minimal = OfflineRetentionPolicy::minimal();
360 assert_eq!(minimal.offline_ttl, Duration::from_secs(30));
361 assert_eq!(minimal.keep_last_n, 5);
362
363 let conservative = OfflineRetentionPolicy::conservative();
364 assert_eq!(conservative.online_ttl, Duration::from_secs(3600));
365 assert_eq!(conservative.keep_last_n, 20);
366 }
367
368 #[test]
369 fn test_eviction_strategy_variants() {
370 let oldest = EvictionStrategy::OldestFirst;
371 let pressure = EvictionStrategy::StoragePressure { threshold_pct: 80 };
372 let keep_n = EvictionStrategy::KeepLastN(100);
373 let none = EvictionStrategy::None;
374
375 assert!(matches!(oldest, EvictionStrategy::OldestFirst));
376 assert!(matches!(
377 pressure,
378 EvictionStrategy::StoragePressure { threshold_pct: 80 }
379 ));
380 assert!(matches!(keep_n, EvictionStrategy::KeepLastN(100)));
381 assert!(matches!(none, EvictionStrategy::None));
382 }
383
384 #[test]
385 fn test_tombstone_ttl_edge_constraint() {
386 // Edge SDK should never exceed server TTL
387 let edge_config = TtlConfig::tactical();
388 assert!(edge_config.tombstone_ttl_hours <= 720); // 30 days server default
389
390 // Offline node uses shorter TTL for storage constraints
391 let offline_config = TtlConfig::offline_node();
392 assert!(offline_config.tombstone_ttl_hours < edge_config.tombstone_ttl_hours);
393 }
394}