forked from
slices.network/slices
Highly ambitious ATProtocol AppView service and sdks
1//! In-memory cache implementation with TTL support.
2//!
3//! Provides a thread-safe, in-memory cache with automatic expiration.
4//! Used as a fallback when Redis is unavailable or for local development.
5
6use super::Cache;
7use anyhow::Result;
8use async_trait::async_trait;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::sync::Arc;
12use std::time::{Duration, Instant};
13use tokio::sync::RwLock;
14use tracing::{debug, warn};
15
16/// Cache entry: (serialized_value, optional_expiry)
17type CacheEntry = (String, Option<Instant>);
18
19/// In-memory cache implementation with TTL support.
20///
21/// Uses Arc<RwLock<HashMap>> for thread-safe concurrent access.
22/// Expired entries are checked on read but not automatically cleaned up.
23pub struct InMemoryCache {
24 data: Arc<RwLock<HashMap<String, CacheEntry>>>,
25 default_ttl_seconds: u64,
26}
27
28impl InMemoryCache {
29 /// Create a new in-memory cache with optional default TTL.
30 ///
31 /// # Arguments
32 /// * `default_ttl_seconds` - Default expiration time in seconds (default: 3600)
33 pub fn new(default_ttl_seconds: Option<u64>) -> Self {
34 Self {
35 data: Arc::new(RwLock::new(HashMap::new())),
36 default_ttl_seconds: default_ttl_seconds.unwrap_or(3600),
37 }
38 }
39}
40
41#[async_trait]
42impl Cache for InMemoryCache {
43 async fn get<T>(&mut self, key: &str) -> Result<Option<T>>
44 where
45 T: for<'de> Deserialize<'de> + Send,
46 {
47 let data = self.data.read().await;
48
49 if let Some((serialized, expiry)) = data.get(key) {
50 if let Some(exp) = expiry
51 && *exp <= Instant::now()
52 {
53 debug!(cache_key = %key, "Cache entry expired");
54 return Ok(None);
55 }
56
57 match serde_json::from_str::<T>(serialized) {
58 Ok(value) => Ok(Some(value)),
59 Err(e) => {
60 warn!(
61 error = ?e,
62 cache_key = %key,
63 "Failed to deserialize cached value"
64 );
65 Ok(None)
66 }
67 }
68 } else {
69 Ok(None)
70 }
71 }
72
73 async fn set<T>(&mut self, key: &str, value: &T, ttl_seconds: Option<u64>) -> Result<()>
74 where
75 T: Serialize + Send + Sync,
76 {
77 let ttl = ttl_seconds.unwrap_or(self.default_ttl_seconds);
78
79 match serde_json::to_string(value) {
80 Ok(serialized) => {
81 let expiry = if ttl > 0 {
82 Some(Instant::now() + Duration::from_secs(ttl))
83 } else {
84 None
85 };
86
87 let mut data = self.data.write().await;
88 data.insert(key.to_string(), (serialized, expiry));
89
90 debug!(
91 cache_key = %key,
92 ttl_seconds = ttl,
93 "Cached value in memory"
94 );
95 Ok(())
96 }
97 Err(e) => {
98 warn!(
99 error = ?e,
100 cache_key = %key,
101 "Failed to serialize value for caching"
102 );
103 Ok(())
104 }
105 }
106 }
107
108 async fn delete(&mut self, key: &str) -> Result<()> {
109 let mut data = self.data.write().await;
110 data.remove(key);
111 debug!(cache_key = %key, "Deleted key from in-memory cache");
112 Ok(())
113 }
114
115 async fn set_multiple<T>(&mut self, items: Vec<(&str, &T, Option<u64>)>) -> Result<()>
116 where
117 T: Serialize + Send + Sync,
118 {
119 if items.is_empty() {
120 return Ok(());
121 }
122
123 let mut data = self.data.write().await;
124 let mut success_count = 0;
125
126 for (key, value, ttl) in &items {
127 match serde_json::to_string(value) {
128 Ok(serialized) => {
129 let ttl_to_use = ttl.unwrap_or(self.default_ttl_seconds);
130 let expiry = if ttl_to_use > 0 {
131 Some(Instant::now() + Duration::from_secs(ttl_to_use))
132 } else {
133 None
134 };
135
136 data.insert(key.to_string(), (serialized, expiry));
137 success_count += 1;
138 }
139 Err(e) => {
140 warn!(
141 error = ?e,
142 cache_key = %key,
143 "Failed to serialize value for bulk caching"
144 );
145 }
146 }
147 }
148
149 debug!(
150 items_count = success_count,
151 total_items = items.len(),
152 "Successfully bulk cached items in memory"
153 );
154 Ok(())
155 }
156
157 async fn ping(&mut self) -> Result<bool> {
158 Ok(true)
159 }
160
161 async fn get_info(&mut self) -> Result<String> {
162 let data = self.data.read().await;
163 let now = Instant::now();
164
165 let mut total_entries = 0;
166 let mut expired_entries = 0;
167
168 for (_, expiry) in data.values() {
169 total_entries += 1;
170 if let Some(exp) = expiry
171 && *exp <= now
172 {
173 expired_entries += 1;
174 }
175 }
176
177 Ok(format!(
178 "InMemoryCache: {} total entries, {} expired, {} active",
179 total_entries,
180 expired_entries,
181 total_entries - expired_entries
182 ))
183 }
184}