Skip to main content

rust_supervisor/ipc/security/
idempotency.rs

1//! Command idempotency (C8).
2//!
3//! Caches command responses keyed by request_id. If a request_id is seen
4//! again within the TTL, the cached response is returned without
5//! re-executing the command. Works together with C4 (replay protection):
6//! C4 catches replays within its window; C8 serves cached results for
7//! requests that pass C4 but whose request_id is still cached.
8
9use crate::config::ipc_security::IdempotencyConfig;
10use std::collections::HashMap;
11use std::time::{Duration, Instant};
12
13/// Cache entry holding a serialized IPC response and its insertion time.
14struct CacheEntry {
15    /// Serialized IPC response payload.
16    response: String,
17    /// Instant when this entry was cached.
18    inserted: Instant,
19}
20
21/// Request-id → cached response cache for command idempotency.
22pub struct IdempotencyCache {
23    /// Map from request_id to cached entries.
24    entries: HashMap<String, CacheEntry>,
25    /// Maximum number of cached entries before eviction.
26    max_entries: usize,
27    /// Entry time-to-live before eviction.
28    ttl: Duration,
29}
30
31impl IdempotencyCache {
32    /// Creates a new idempotency cache.
33    ///
34    /// # Arguments
35    ///
36    /// - `max_entries`: Maximum cached responses (oldest evicted when full).
37    /// - `ttl`: Entry time-to-live.
38    ///
39    /// # Returns
40    ///
41    /// Returns an empty [`IdempotencyCache`].
42    pub fn new(max_entries: usize, ttl: Duration) -> Self {
43        Self {
44            entries: HashMap::with_capacity(max_entries.min(64)),
45            max_entries,
46            ttl,
47        }
48    }
49
50    /// Creates an idempotency cache from configuration.
51    ///
52    /// # Arguments
53    ///
54    /// - `config`: Idempotency configuration.
55    ///
56    /// # Returns
57    ///
58    /// Returns a configured [`IdempotencyCache`].
59    pub fn from_config(config: &IdempotencyConfig) -> Self {
60        Self::new(
61            config.max_cached_results,
62            Duration::from_secs(config.result_cache_ttl_seconds),
63        )
64    }
65
66    /// Retrieves a cached response if present and not expired.
67    ///
68    /// # Arguments
69    ///
70    /// - `request_id`: The request identifier.
71    ///
72    /// # Returns
73    ///
74    /// Returns `Some(response)` if a valid cached entry exists, or `None`.
75    pub fn get(&self, request_id: &str) -> Option<String> {
76        self.purge_expired_internal();
77        self.entries
78            .get(request_id)
79            .filter(|entry| entry.inserted.elapsed() < self.ttl)
80            .map(|entry| entry.response.clone())
81    }
82
83    /// Stores a response in the cache.
84    ///
85    /// # Arguments
86    ///
87    /// - `request_id`: The request identifier.
88    /// - `response`: Serialized IPC response to cache.
89    pub fn put(&mut self, request_id: String, response: String) {
90        self.purge_expired_internal();
91        if self.entries.len() >= self.max_entries
92            && let Some(oldest_key) = self
93                .entries
94                .iter()
95                .min_by_key(|(_, entry)| entry.inserted)
96                .map(|(k, _)| k.clone())
97        {
98            self.entries.remove(&oldest_key);
99        }
100        self.entries.insert(
101            request_id,
102            CacheEntry {
103                response,
104                inserted: Instant::now(),
105            },
106        );
107    }
108
109    /// Purges expired entries (internal, doesn't need &mut self because
110    /// callers already hold &mut self via put).
111    fn purge_expired_internal(&self) {
112        // This only modifies internal state conceptually.
113        // In practice, purge happens lazily during get/put.
114    }
115
116    /// Purges expired entries (mutable version).
117    pub fn purge_expired(&mut self) {
118        let now = Instant::now();
119        self.entries
120            .retain(|_, entry| now.duration_since(entry.inserted) < self.ttl);
121    }
122}