1use std::sync::Arc;
10
11use redis::aio::ConnectionManager;
12use redis::{AsyncCommands, Client};
13use serde::{de::DeserializeOwned, Serialize};
14use uuid::Uuid;
15
16use crate::error::{DbError, Result};
17
18pub mod keys {
20 pub const USER_SESSION: &str = "session:";
22 pub const USER_PROFILE: &str = "user:";
24 pub const TOKEN_PRICE: &str = "price:";
26 pub const TOKEN_META: &str = "token:";
28 pub const USER_BALANCE: &str = "balance:";
30 pub const RATE_LIMIT: &str = "ratelimit:";
32 pub const LOCK: &str = "lock:";
34}
35
36#[derive(Debug, Clone)]
38pub struct CacheConfig {
39 pub redis_url: String,
41 pub default_ttl_secs: u64,
43 pub session_ttl_secs: u64,
45 pub price_ttl_secs: u64,
47 pub profile_ttl_secs: u64,
49 pub balance_ttl_secs: u64,
51 pub key_prefix: String,
53}
54
55impl Default for CacheConfig {
56 fn default() -> Self {
57 Self {
58 redis_url: "redis://127.0.0.1:6379".to_string(),
59 default_ttl_secs: 3600, session_ttl_secs: 86400, price_ttl_secs: 5, profile_ttl_secs: 300, balance_ttl_secs: 30, key_prefix: "kaccy:".to_string(),
65 }
66 }
67}
68
69impl CacheConfig {
70 pub fn from_env() -> Self {
72 let redis_url =
73 std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
74
75 Self {
76 redis_url,
77 ..Default::default()
78 }
79 }
80
81 pub fn with_ttls(
83 mut self,
84 default: u64,
85 session: u64,
86 price: u64,
87 profile: u64,
88 balance: u64,
89 ) -> Self {
90 self.default_ttl_secs = default;
91 self.session_ttl_secs = session;
92 self.price_ttl_secs = price;
93 self.profile_ttl_secs = profile;
94 self.balance_ttl_secs = balance;
95 self
96 }
97}
98
99#[derive(Clone)]
101pub struct RedisCache {
102 conn: ConnectionManager,
103 config: CacheConfig,
104}
105
106impl RedisCache {
107 pub async fn new(config: CacheConfig) -> Result<Self> {
109 let client = Client::open(config.redis_url.as_str())
110 .map_err(|e| DbError::Connection(format!("Redis client error: {}", e)))?;
111
112 let conn = ConnectionManager::new(client)
113 .await
114 .map_err(|e| DbError::Connection(format!("Redis connection error: {}", e)))?;
115
116 tracing::info!("Redis cache connected to {}", config.redis_url);
117
118 Ok(Self { conn, config })
119 }
120
121 fn full_key(&self, key: &str) -> String {
123 format!("{}{}", self.config.key_prefix, key)
124 }
125
126 pub async fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
128 let full_key = self.full_key(key);
129 let mut conn = self.conn.clone();
130
131 let value: Option<String> = conn
132 .get(&full_key)
133 .await
134 .map_err(|e| DbError::Cache(format!("Redis GET error: {}", e)))?;
135
136 match value {
137 Some(json) => {
138 let parsed: T = serde_json::from_str(&json)
139 .map_err(|e| DbError::Cache(format!("Deserialization error: {}", e)))?;
140 Ok(Some(parsed))
141 }
142 None => Ok(None),
143 }
144 }
145
146 pub async fn set<T: Serialize>(&self, key: &str, value: &T, ttl_secs: u64) -> Result<()> {
148 let full_key = self.full_key(key);
149 let json = serde_json::to_string(value)
150 .map_err(|e| DbError::Cache(format!("Serialization error: {}", e)))?;
151
152 let mut conn = self.conn.clone();
153 let _: () = conn
154 .set_ex(&full_key, json, ttl_secs)
155 .await
156 .map_err(|e| DbError::Cache(format!("Redis SET error: {}", e)))?;
157
158 Ok(())
159 }
160
161 pub async fn set_default<T: Serialize>(&self, key: &str, value: &T) -> Result<()> {
163 self.set(key, value, self.config.default_ttl_secs).await
164 }
165
166 pub async fn delete(&self, key: &str) -> Result<bool> {
168 let full_key = self.full_key(key);
169 let mut conn = self.conn.clone();
170
171 let deleted: i64 = conn
172 .del(&full_key)
173 .await
174 .map_err(|e| DbError::Cache(format!("Redis DEL error: {}", e)))?;
175
176 Ok(deleted > 0)
177 }
178
179 pub async fn delete_pattern(&self, pattern: &str) -> Result<u64> {
181 let full_pattern = self.full_key(pattern);
182 let mut conn = self.conn.clone();
183
184 let keys: Vec<String> = conn
185 .keys(&full_pattern)
186 .await
187 .map_err(|e| DbError::Cache(format!("Redis KEYS error: {}", e)))?;
188
189 if keys.is_empty() {
190 return Ok(0);
191 }
192
193 let deleted: i64 = conn
194 .del(&keys)
195 .await
196 .map_err(|e| DbError::Cache(format!("Redis DEL error: {}", e)))?;
197
198 Ok(deleted as u64)
199 }
200
201 pub async fn exists(&self, key: &str) -> Result<bool> {
203 let full_key = self.full_key(key);
204 let mut conn = self.conn.clone();
205
206 let exists: bool = conn
207 .exists(&full_key)
208 .await
209 .map_err(|e| DbError::Cache(format!("Redis EXISTS error: {}", e)))?;
210
211 Ok(exists)
212 }
213
214 pub async fn incr(&self, key: &str) -> Result<i64> {
216 let full_key = self.full_key(key);
217 let mut conn = self.conn.clone();
218
219 let value: i64 = conn
220 .incr(&full_key, 1)
221 .await
222 .map_err(|e| DbError::Cache(format!("Redis INCR error: {}", e)))?;
223
224 Ok(value)
225 }
226
227 pub async fn incr_with_expiry(&self, key: &str, ttl_secs: u64) -> Result<i64> {
229 let full_key = self.full_key(key);
230 let mut conn = self.conn.clone();
231
232 let (value,): (i64,) = redis::pipe()
234 .atomic()
235 .incr(&full_key, 1)
236 .expire(&full_key, ttl_secs as i64)
237 .ignore()
238 .query_async(&mut conn)
239 .await
240 .map_err(|e| DbError::Cache(format!("Redis INCR/EXPIRE error: {}", e)))?;
241
242 Ok(value)
243 }
244
245 pub async fn expire(&self, key: &str, ttl_secs: u64) -> Result<bool> {
247 let full_key = self.full_key(key);
248 let mut conn = self.conn.clone();
249
250 let set: bool = conn
251 .expire(&full_key, ttl_secs as i64)
252 .await
253 .map_err(|e| DbError::Cache(format!("Redis EXPIRE error: {}", e)))?;
254
255 Ok(set)
256 }
257
258 pub async fn ttl(&self, key: &str) -> Result<i64> {
260 let full_key = self.full_key(key);
261 let mut conn = self.conn.clone();
262
263 let ttl: i64 = conn
264 .ttl(&full_key)
265 .await
266 .map_err(|e| DbError::Cache(format!("Redis TTL error: {}", e)))?;
267
268 Ok(ttl)
269 }
270
271 pub async fn try_lock(&self, resource: &str, ttl_secs: u64) -> Result<Option<String>> {
273 let key = format!("{}{}:{}", keys::LOCK, resource, Uuid::new_v4());
274 let full_key = self.full_key(&key);
275 let lock_id = Uuid::new_v4().to_string();
276 let mut conn = self.conn.clone();
277
278 let set: bool = conn
279 .set_nx(&full_key, &lock_id)
280 .await
281 .map_err(|e| DbError::Cache(format!("Redis SETNX error: {}", e)))?;
282
283 if set {
284 let _: () = conn
285 .expire(&full_key, ttl_secs as i64)
286 .await
287 .map_err(|e| DbError::Cache(format!("Redis EXPIRE error: {}", e)))?;
288 Ok(Some(lock_id))
289 } else {
290 Ok(None)
291 }
292 }
293
294 pub async fn release_lock(&self, resource: &str, lock_id: &str) -> Result<bool> {
296 let key = format!("{}{}:{}", keys::LOCK, resource, lock_id);
297 self.delete(&key).await
298 }
299
300 pub async fn health_check(&self) -> Result<bool> {
302 let mut conn = self.conn.clone();
303 let pong: String = redis::cmd("PING")
304 .query_async(&mut conn)
305 .await
306 .map_err(|e| DbError::Cache(format!("Redis PING error: {}", e)))?;
307
308 Ok(pong == "PONG")
309 }
310}
311
312impl RedisCache {
316 pub async fn set_session(&self, session_id: &str, user_id: Uuid) -> Result<()> {
318 let key = format!("{}{}", keys::USER_SESSION, session_id);
319 self.set(&key, &user_id, self.config.session_ttl_secs).await
320 }
321
322 pub async fn get_session(&self, session_id: &str) -> Result<Option<Uuid>> {
324 let key = format!("{}{}", keys::USER_SESSION, session_id);
325 self.get(&key).await
326 }
327
328 pub async fn invalidate_session(&self, session_id: &str) -> Result<bool> {
330 let key = format!("{}{}", keys::USER_SESSION, session_id);
331 self.delete(&key).await
332 }
333
334 #[allow(dead_code)]
336 pub async fn invalidate_user_sessions(&self, user_id: Uuid) -> Result<u64> {
337 tracing::warn!(
341 "invalidate_user_sessions: scanning all sessions for user {}",
342 user_id
343 );
344 Ok(0) }
346}
347
348impl RedisCache {
350 pub async fn set_user_profile<T: Serialize>(&self, user_id: Uuid, profile: &T) -> Result<()> {
352 let key = format!("{}{}", keys::USER_PROFILE, user_id);
353 self.set(&key, profile, self.config.profile_ttl_secs).await
354 }
355
356 pub async fn get_user_profile<T: DeserializeOwned>(&self, user_id: Uuid) -> Result<Option<T>> {
358 let key = format!("{}{}", keys::USER_PROFILE, user_id);
359 self.get(&key).await
360 }
361
362 pub async fn invalidate_user_profile(&self, user_id: Uuid) -> Result<bool> {
364 let key = format!("{}{}", keys::USER_PROFILE, user_id);
365 self.delete(&key).await
366 }
367}
368
369impl RedisCache {
371 pub async fn set_token_price(&self, token_id: Uuid, price_btc: f64) -> Result<()> {
373 let key = format!("{}{}", keys::TOKEN_PRICE, token_id);
374 self.set(&key, &price_btc, self.config.price_ttl_secs).await
375 }
376
377 pub async fn get_token_price(&self, token_id: Uuid) -> Result<Option<f64>> {
379 let key = format!("{}{}", keys::TOKEN_PRICE, token_id);
380 self.get(&key).await
381 }
382
383 pub async fn set_token_prices(&self, prices: &[(Uuid, f64)]) -> Result<()> {
385 for (token_id, price) in prices {
386 self.set_token_price(*token_id, *price).await?;
387 }
388 Ok(())
389 }
390}
391
392impl RedisCache {
394 pub async fn set_token_meta<T: Serialize>(&self, token_id: Uuid, meta: &T) -> Result<()> {
396 let key = format!("{}{}", keys::TOKEN_META, token_id);
397 self.set(&key, meta, self.config.default_ttl_secs).await
398 }
399
400 pub async fn get_token_meta<T: DeserializeOwned>(&self, token_id: Uuid) -> Result<Option<T>> {
402 let key = format!("{}{}", keys::TOKEN_META, token_id);
403 self.get(&key).await
404 }
405
406 pub async fn invalidate_token_meta(&self, token_id: Uuid) -> Result<bool> {
408 let key = format!("{}{}", keys::TOKEN_META, token_id);
409 self.delete(&key).await
410 }
411}
412
413impl RedisCache {
415 pub async fn set_balance(&self, user_id: Uuid, token_id: Uuid, balance: f64) -> Result<()> {
417 let key = format!("{}{}:{}", keys::USER_BALANCE, user_id, token_id);
418 self.set(&key, &balance, self.config.balance_ttl_secs).await
419 }
420
421 pub async fn get_balance(&self, user_id: Uuid, token_id: Uuid) -> Result<Option<f64>> {
423 let key = format!("{}{}:{}", keys::USER_BALANCE, user_id, token_id);
424 self.get(&key).await
425 }
426
427 pub async fn invalidate_user_balances(&self, user_id: Uuid) -> Result<u64> {
429 let pattern = format!("{}{}:*", keys::USER_BALANCE, user_id);
430 self.delete_pattern(&pattern).await
431 }
432
433 pub async fn invalidate_token_balances(&self, token_id: Uuid) -> Result<u64> {
435 let pattern = format!("{}*:{}", keys::USER_BALANCE, token_id);
436 self.delete_pattern(&pattern).await
437 }
438}
439
440impl RedisCache {
442 pub async fn check_rate_limit(
445 &self,
446 identifier: &str,
447 limit: u64,
448 window_secs: u64,
449 ) -> Result<(u64, bool)> {
450 let key = format!("{}{}", keys::RATE_LIMIT, identifier);
451 let count = self.incr_with_expiry(&key, window_secs).await? as u64;
452 Ok((count, count <= limit))
453 }
454
455 pub async fn get_rate_limit_count(&self, identifier: &str) -> Result<u64> {
457 let key = format!("{}{}", keys::RATE_LIMIT, identifier);
458 let count: Option<u64> = self.get(&key).await?;
459 Ok(count.unwrap_or(0))
460 }
461
462 pub async fn reset_rate_limit(&self, identifier: &str) -> Result<bool> {
464 let key = format!("{}{}", keys::RATE_LIMIT, identifier);
465 self.delete(&key).await
466 }
467}
468
469pub struct CachedRepository<R> {
471 cache: Arc<RedisCache>,
472 repo: R,
473}
474
475impl<R> CachedRepository<R> {
476 pub fn new(cache: Arc<RedisCache>, repo: R) -> Self {
478 Self { cache, repo }
479 }
480
481 pub fn repo(&self) -> &R {
483 &self.repo
484 }
485
486 pub fn cache(&self) -> &RedisCache {
488 &self.cache
489 }
490}
491
492#[derive(Debug, Clone, Default, serde::Serialize)]
494pub struct CacheStats {
495 pub hits: u64,
497 pub misses: u64,
499 pub sets: u64,
501 pub deletes: u64,
503}
504
505impl CacheStats {
506 pub fn hit_rate(&self) -> f64 {
508 let total = self.hits + self.misses;
509 if total == 0 {
510 0.0
511 } else {
512 self.hits as f64 / total as f64
513 }
514 }
515}