refactor(cache): refresh crate across hybrid, redis and metrics
zhenyi committed
Modified lib/cache/redis/mod.rs lib/cache/redis/mod.rs +317 −187 View lib/cache/redis/mod.rs
@@ -3,6 +3,7 @@
33pub use conn::RedisConnClone;
44
55use crate::config::redis::{AppRedisMode, AppRedisSettings};
6use crate::metrics;
67use conn::RedisConn;
78use redis::{
89 AsyncCommands, ConnectionAddr, IntoConnectionInfo, RedisError as RedisDriverError,
@@ -138,27 +139,39 @@
138139 }
139140
140141 pub async fn get_bytes(&self, key: &str) -> Result<Option<Vec<u8>>, RedisError> {
141 let full_key = self.prefixed(key);
142 let mut conn = self.spawn_conn();
143 Ok(conn.get(&*full_key).await?)
142 let op = "get_bytes";
143 let result: Result<Option<Vec<u8>>, RedisError> = async {
144 let full_key = self.prefixed(key);
145 let mut conn = self.spawn_conn();
146 Ok(conn.get(&*full_key).await?)
147 }
148 .await;
149 metrics::record_redis_op(op, result.is_ok());
150 result
144151 }
145152
146153 pub(crate) async fn get_bytes_with_ttl(
147154 &self,
148155 key: &str,
149156 ) -> Result<Option<(Vec<u8>, Option<u64>)>, RedisError> {
150 let full_key = self.prefixed(key);
151 let mut conn = self.spawn_conn();
152 let Some(value) = conn.get(&*full_key).await? else {
153 return Ok(None);
154 };
155 let ttl_millis: i64 = conn.pttl(&*full_key).await?;
156 let ttl_millis = match ttl_millis {
157 -1 => None,
158 value if value >= 0 => Some(value as u64),
159 _ => Some(0),
160 };
161 Ok(Some((value, ttl_millis)))
157 let op = "get_bytes_with_ttl";
158 let result = async {
159 let full_key = self.prefixed(key);
160 let mut conn = self.spawn_conn();
161 let Some(value) = conn.get(&*full_key).await? else {
162 return Ok(None);
163 };
164 let ttl_millis: i64 = conn.pttl(&*full_key).await?;
165 let ttl_millis = match ttl_millis {
166 -1 => None,
167 value if value >= 0 => Some(value as u64),
168 _ => Some(0),
169 };
170 Ok(Some((value, ttl_millis)))
171 }
172 .await;
173 metrics::record_redis_op(op, result.is_ok());
174 result
162175 }
163176
164177 pub async fn set_bytes(
@@ -167,14 +180,20 @@
167180 value: Vec<u8>,
168181 ttl_seconds: u64,
169182 ) -> Result<(), RedisError> {
170 let full_key = self.prefixed(key);
171 let mut conn = self.spawn_conn();
172 if ttl_seconds > 0 {
173 let _: () = conn.set_ex(&*full_key, value, ttl_seconds).await?;
174 } else {
175 let _: () = conn.set(&*full_key, value).await?;
183 let op = "set_bytes";
184 let result: Result<(), RedisError> = async {
185 let full_key = self.prefixed(key);
186 let mut conn = self.spawn_conn();
187 if ttl_seconds > 0 {
188 let _: () = conn.set_ex(&*full_key, value, ttl_seconds).await?;
189 } else {
190 let _: () = conn.set(&*full_key, value).await?;
191 }
192 Ok(())
176193 }
177 Ok(())
194 .await;
195 metrics::record_redis_op(op, result.is_ok());
196 result
178197 }
179198
180199 pub async fn compare_and_set_bytes(
@@ -184,152 +203,215 @@
184203 value: Vec<u8>,
185204 ttl_seconds: u64,
186205 ) -> Result<bool, RedisError> {
187 let full_key = self.prefixed(key);
188 let mut conn = self.spawn_conn();
189 let changed: i64 = redis::Script::new(COMPARE_AND_SET_BYTES_SCRIPT)
190 .key(full_key)
191 .arg(expected)
192 .arg(value)
193 .arg(ttl_seconds)
194 .invoke_async(&mut conn)
195 .await?;
196 Ok(changed == 1)
206 let op = "compare_and_set_bytes";
207 let result: Result<bool, RedisError> = async {
208 let full_key = self.prefixed(key);
209 let mut conn = self.spawn_conn();
210 let changed: i64 = redis::Script::new(COMPARE_AND_SET_BYTES_SCRIPT)
211 .key(full_key)
212 .arg(expected)
213 .arg(value)
214 .arg(ttl_seconds)
215 .invoke_async(&mut conn)
216 .await?;
217 Ok(changed == 1)
218 }
219 .await;
220 metrics::record_redis_op(op, result.is_ok());
221 result
197222 }
198223
199224 pub async fn delete(&self, key: &str) -> Result<(), RedisError> {
200 let full_key = self.prefixed(key);
201 let mut conn = self.spawn_conn();
202 let _: () = conn.del(&*full_key).await?;
203 Ok(())
225 let op = "delete";
226 let result: Result<(), RedisError> = async {
227 let full_key = self.prefixed(key);
228 let mut conn = self.spawn_conn();
229 let _: () = conn.del(&*full_key).await?;
230 Ok(())
231 }
232 .await;
233 metrics::record_redis_op(op, result.is_ok());
234 result
204235 }
205236
206 /// Atomically set a key only if it does not already exist (`SET NX EX`).
207 /// Used for distributed locks. Returns `true` when the lock was acquired.
208 pub async fn set_nx(
237 pub async fn set_nx(
209238 &self,
210239 key: &str,
211240 value: Vec<u8>,
212241 ttl_seconds: u64,
213242 ) -> Result<bool, RedisError> {
214 let full_key = self.prefixed(key);
215 let mut conn = self.spawn_conn();
216 let set: Option<()> = redis::cmd("SET")
217 .arg(&*full_key)
218 .arg(value)
219 .arg("NX")
220 .arg("EX")
221 .arg(ttl_seconds as i64)
222 .query_async(&mut conn)
223 .await?;
224 Ok(set.is_some())
243 let op = "set_nx";
244 let result: Result<bool, RedisError> = async {
245 let full_key = self.prefixed(key);
246 let mut conn = self.spawn_conn();
247 let set: Option<()> = redis::cmd("SET")
248 .arg(&*full_key)
249 .arg(value)
250 .arg("NX")
251 .arg("EX")
252 .arg(ttl_seconds as i64)
253 .query_async(&mut conn)
254 .await?;
255 Ok(set.is_some())
256 }
257 .await;
258 metrics::record_redis_op(op, result.is_ok());
259 result
225260 }
226261
227 /// Atomically delete a key only if it currently holds the expected value
228 /// (Lua GET + DEL). Returns `true` when a value was deleted.
229 pub async fn compare_and_delete(&self, key: &str, expected: &[u8]) -> Result<bool, RedisError> {
230 let full_key = self.prefixed(key);
231 let mut conn = self.spawn_conn();
232 let deleted: i64 = redis::Script::new(COMPARE_AND_DELETE_SCRIPT)
233 .key(full_key)
234 .arg(expected)
235 .invoke_async(&mut conn)
236 .await?;
237 Ok(deleted == 1)
262 pub async fn compare_and_delete(&self, key: &str, expected: &[u8]) -> Result<bool, RedisError> {
263 let op = "compare_and_delete";
264 let result: Result<bool, RedisError> = async {
265 let full_key = self.prefixed(key);
266 let mut conn = self.spawn_conn();
267 let deleted: i64 = redis::Script::new(COMPARE_AND_DELETE_SCRIPT)
268 .key(full_key)
269 .arg(expected)
270 .invoke_async(&mut conn)
271 .await?;
272 Ok(deleted == 1)
273 }
274 .await;
275 metrics::record_redis_op(op, result.is_ok());
276 result
238277 }
239278
240 /// Atomically extend the TTL of a distributed lock only if the key still
241 /// holds the expected value (Lua GET + EXPIRE). Returns `true` when the
242 /// lock was refreshed.
243 pub async fn refresh_lock(
279 pub async fn refresh_lock(
244280 &self,
245281 key: &str,
246282 value: &[u8],
247283 ttl_seconds: u64,
248284 ) -> Result<bool, RedisError> {
249 let full_key = self.prefixed(key);
250 let mut conn = self.spawn_conn();
251 let refreshed: i64 = redis::Script::new(REFRESH_LOCK_SCRIPT)
252 .key(full_key)
253 .arg(value)
254 .arg(ttl_seconds as i64)
255 .invoke_async(&mut conn)
256 .await?;
257 Ok(refreshed == 1)
285 let op = "refresh_lock";
286 let result: Result<bool, RedisError> = async {
287 let full_key = self.prefixed(key);
288 let mut conn = self.spawn_conn();
289 let refreshed: i64 = redis::Script::new(REFRESH_LOCK_SCRIPT)
290 .key(full_key)
291 .arg(value)
292 .arg(ttl_seconds as i64)
293 .invoke_async(&mut conn)
294 .await?;
295 Ok(refreshed == 1)
296 }
297 .await;
298 metrics::record_redis_op(op, result.is_ok());
299 result
258300 }
259301
260 /// Push a value onto the tail of a list (`RPUSH`). Returns the new list length.
261 pub async fn rpush(&self, key: &str, value: Vec<u8>) -> Result<u64, RedisError> {
262 let full_key = self.prefixed(key);
263 let mut conn = self.spawn_conn();
264 let len: u64 = conn.rpush(&*full_key, value).await?;
265 Ok(len)
302 pub async fn rpush(&self, key: &str, value: Vec<u8>) -> Result<u64, RedisError> {
303 let op = "rpush";
304 let result: Result<u64, RedisError> = async {
305 let full_key = self.prefixed(key);
306 let mut conn = self.spawn_conn();
307 let len: u64 = conn.rpush(&*full_key, value).await?;
308 Ok(len)
309 }
310 .await;
311 metrics::record_redis_op(op, result.is_ok());
312 result
266313 }
267314
268 /// Push a value onto the head of a list (`LPUSH`). Returns the new list length.
269 pub async fn lpush(&self, key: &str, value: Vec<u8>) -> Result<u64, RedisError> {
270 let full_key = self.prefixed(key);
271 let mut conn = self.spawn_conn();
272 let len: u64 = conn.lpush(&*full_key, value).await?;
273 Ok(len)
315 pub async fn lpush(&self, key: &str, value: Vec<u8>) -> Result<u64, RedisError> {
316 let op = "lpush";
317 let result: Result<u64, RedisError> = async {
318 let full_key = self.prefixed(key);
319 let mut conn = self.spawn_conn();
320 let len: u64 = conn.lpush(&*full_key, value).await?;
321 Ok(len)
322 }
323 .await;
324 metrics::record_redis_op(op, result.is_ok());
325 result
274326 }
275327
276 /// Pop and return the head of a list (`LPOP`), if any.
277 pub async fn lpop(&self, key: &str) -> Result<Option<Vec<u8>>, RedisError> {
278 let full_key = self.prefixed(key);
279 let mut conn = self.spawn_conn();
280 Ok(redis::cmd("LPOP")
281 .arg(&*full_key)
282 .query_async(&mut conn)
283 .await?)
328 pub async fn lpop(&self, key: &str) -> Result<Option<Vec<u8>>, RedisError> {
329 let op = "lpop";
330 let result: Result<Option<Vec<u8>>, RedisError> = async {
331 let full_key = self.prefixed(key);
332 let mut conn = self.spawn_conn();
333 Ok(redis::cmd("LPOP")
334 .arg(&*full_key)
335 .query_async(&mut conn)
336 .await?)
337 }
338 .await;
339 metrics::record_redis_op(op, result.is_ok());
340 result
284341 }
285342
286 /// Remove up to `count` occurrences of `member` from a list (`LREM`).
287 /// Returns the number of removed elements.
288 pub async fn lrem(&self, key: &str, count: i64, member: &[u8]) -> Result<u64, RedisError> {
289 let full_key = self.prefixed(key);
290 let mut conn = self.spawn_conn();
291 let removed: u64 = conn.lrem(&*full_key, count as isize, member).await?;
292 Ok(removed)
343 pub async fn lrem(&self, key: &str, count: i64, member: &[u8]) -> Result<u64, RedisError> {
344 let op = "lrem";
345 let result: Result<u64, RedisError> = async {
346 let full_key = self.prefixed(key);
347 let mut conn = self.spawn_conn();
348 let removed: u64 = conn.lrem(&*full_key, count as isize, member).await?;
349 Ok(removed)
350 }
351 .await;
352 metrics::record_redis_op(op, result.is_ok());
353 result
293354 }
294355
295 /// Return the length of a list (`LLEN`).
296 pub async fn llen(&self, key: &str) -> Result<u64, RedisError> {
297 let full_key = self.prefixed(key);
298 let mut conn = self.spawn_conn();
299 let len: u64 = conn.llen(&*full_key).await?;
300 Ok(len)
356 pub async fn llen(&self, key: &str) -> Result<u64, RedisError> {
357 let op = "llen";
358 let result: Result<u64, RedisError> = async {
359 let full_key = self.prefixed(key);
360 let mut conn = self.spawn_conn();
361 let len: u64 = conn.llen(&*full_key).await?;
362 Ok(len)
363 }
364 .await;
365 metrics::record_redis_op(op, result.is_ok());
366 result
301367 }
302368
303 /// Atomically fetch-and-delete a key (GETDEL). Returns the previous value
304 /// if present, enabling single-use consumption of one-time tokens.
305 pub async fn getdel_bytes(&self, key: &str) -> Result<Option<Vec<u8>>, RedisError> {
306 let full_key = self.prefixed(key);
307 let mut conn = self.spawn_conn();
308 let value: Option<Vec<u8>> = redis::cmd("GETDEL")
309 .arg(&*full_key)
310 .query_async(&mut conn)
311 .await?;
312 Ok(value)
369 pub async fn getdel_bytes(&self, key: &str) -> Result<Option<Vec<u8>>, RedisError> {
370 let op = "getdel_bytes";
371 let result: Result<Option<Vec<u8>>, RedisError> = async {
372 let full_key = self.prefixed(key);
373 let mut conn = self.spawn_conn();
374 let value: Option<Vec<u8>> = redis::cmd("GETDEL")
375 .arg(&*full_key)
376 .query_async(&mut conn)
377 .await?;
378 Ok(value)
379 }
380 .await;
381 metrics::record_redis_op(op, result.is_ok());
382 result
313383 }
314384
315385 pub async fn incr_with_ttl(&self, key: &str, ttl_seconds: u64) -> Result<u64, RedisError> {
316 let full_key = self.prefixed(key);
317 let mut conn = self.spawn_conn();
318 let (count,): (u64,) = redis::pipe()
319 .atomic()
320 .incr(&*full_key, 1u64)
321 .expire(&*full_key, ttl_seconds as i64)
322 .ignore()
323 .query_async(&mut conn)
324 .await?;
325 Ok(count)
386 let op = "incr_with_ttl";
387 let result: Result<u64, RedisError> = async {
388 let full_key = self.prefixed(key);
389 let mut conn = self.spawn_conn();
390 let (count,): (u64,) = redis::pipe()
391 .atomic()
392 .incr(&*full_key, 1u64)
393 .expire(&*full_key, ttl_seconds as i64)
394 .ignore()
395 .query_async(&mut conn)
396 .await?;
397 Ok(count)
398 }
399 .await;
400 metrics::record_redis_op(op, result.is_ok());
401 result
326402 }
327403
328404 pub async fn get_count(&self, key: &str) -> Result<u64, RedisError> {
329 let full_key = self.prefixed(key);
330 let mut conn = self.spawn_conn();
331 let value: Option<u64> = conn.get(&*full_key).await?;
332 Ok(value.unwrap_or(0))
405 let op = "get_count";
406 let result: Result<u64, RedisError> = async {
407 let full_key = self.prefixed(key);
408 let mut conn = self.spawn_conn();
409 let value: Option<u64> = conn.get(&*full_key).await?;
410 Ok(value.unwrap_or(0))
411 }
412 .await;
413 metrics::record_redis_op(op, result.is_ok());
414 result
333415 }
334416
335417 pub async fn add_to_set_with_ttl(
@@ -338,37 +420,61 @@
338420 member: &str,
339421 ttl_seconds: u64,
340422 ) -> Result<(), RedisError> {
341 let full_key = self.prefixed(key);
342 let mut conn = self.spawn_conn();
343 let _: () = redis::pipe()
344 .atomic()
345 .sadd(&*full_key, member)
346 .expire(&*full_key, ttl_seconds as i64)
347 .ignore()
348 .query_async(&mut conn)
349 .await?;
350 Ok(())
423 let op = "add_to_set_with_ttl";
424 let result: Result<(), RedisError> = async {
425 let full_key = self.prefixed(key);
426 let mut conn = self.spawn_conn();
427 let _: () = redis::pipe()
428 .atomic()
429 .sadd(&*full_key, member)
430 .expire(&*full_key, ttl_seconds as i64)
431 .ignore()
432 .query_async(&mut conn)
433 .await?;
434 Ok(())
435 }
436 .await;
437 metrics::record_redis_op(op, result.is_ok());
438 result
351439 }
352440
353441 pub async fn remove_from_set(&self, key: &str, member: &str) -> Result<(), RedisError> {
354 let full_key = self.prefixed(key);
355 let mut conn = self.spawn_conn();
356 let _: () = conn.srem(&*full_key, member).await?;
357 Ok(())
442 let op = "remove_from_set";
443 let result: Result<(), RedisError> = async {
444 let full_key = self.prefixed(key);
445 let mut conn = self.spawn_conn();
446 let _: () = conn.srem(&*full_key, member).await?;
447 Ok(())
448 }
449 .await;
450 metrics::record_redis_op(op, result.is_ok());
451 result
358452 }
359453
360454 pub async fn set_members(&self, key: &str) -> Result<Vec<String>, RedisError> {
361 let full_key = self.prefixed(key);
362 let mut conn = self.spawn_conn();
363 let members: Vec<String> = conn.smembers(&*full_key).await?;
364 Ok(members)
455 let op = "set_members";
456 let result: Result<Vec<String>, RedisError> = async {
457 let full_key = self.prefixed(key);
458 let mut conn = self.spawn_conn();
459 let members: Vec<String> = conn.smembers(&*full_key).await?;
460 Ok(members)
461 }
462 .await;
463 metrics::record_redis_op(op, result.is_ok());
464 result
365465 }
366466
367467 pub async fn delete_set_members(&self, key: &str) -> Result<(), RedisError> {
368 let full_key = self.prefixed(key);
369 let mut conn = self.spawn_conn();
370 let _: () = conn.del(&*full_key).await?;
371 Ok(())
468 let op = "delete_set_members";
469 let result: Result<(), RedisError> = async {
470 let full_key = self.prefixed(key);
471 let mut conn = self.spawn_conn();
472 let _: () = conn.del(&*full_key).await?;
473 Ok(())
474 }
475 .await;
476 metrics::record_redis_op(op, result.is_ok());
477 result
372478 }
373479
374480 pub async fn zadd(
@@ -378,16 +484,22 @@
378484 score: f64,
379485 ttl_seconds: u64,
380486 ) -> Result<(), RedisError> {
381 let full_key = self.prefixed(key);
382 let mut conn = self.spawn_conn();
383 let _: () = redis::pipe()
384 .atomic()
385 .zadd(&full_key, member, score)
386 .expire(&full_key, ttl_seconds as i64)
387 .ignore()
388 .query_async(&mut conn)
389 .await?;
390 Ok(())
487 let op = "zadd";
488 let result: Result<(), RedisError> = async {
489 let full_key = self.prefixed(key);
490 let mut conn = self.spawn_conn();
491 let _: () = redis::pipe()
492 .atomic()
493 .zadd(&full_key, member, score)
494 .expire(&full_key, ttl_seconds as i64)
495 .ignore()
496 .query_async(&mut conn)
497 .await?;
498 Ok(())
499 }
500 .await;
501 metrics::record_redis_op(op, result.is_ok());
502 result
391503 }
392504
393505 pub async fn zremrange_by_score_lt(
@@ -395,38 +507,56 @@
395507 key: &str,
396508 max_score: f64,
397509 ) -> Result<u64, RedisError> {
398 let full_key = self.prefixed(key);
399 let mut conn = self.spawn_conn();
400 let removed: u64 = conn.zrembyscore(&*full_key, "-inf", max_score).await?;
401 Ok(removed)
510 let op = "zremrange_by_score_lt";
511 let result: Result<u64, RedisError> = async {
512 let full_key = self.prefixed(key);
513 let mut conn = self.spawn_conn();
514 let removed: u64 = conn.zrembyscore(&*full_key, "-inf", max_score).await?;
515 Ok(removed)
516 }
517 .await;
518 metrics::record_redis_op(op, result.is_ok());
519 result
402520 }
403521
404522 pub async fn zcard(&self, key: &str) -> Result<u64, RedisError> {
405 let full_key = self.prefixed(key);
406 let mut conn = self.spawn_conn();
407 let count: u64 = conn.zcard(&*full_key).await?;
408 Ok(count)
523 let op = "zcard";
524 let result: Result<u64, RedisError> = async {
525 let full_key = self.prefixed(key);
526 let mut conn = self.spawn_conn();
527 let count: u64 = conn.zcard(&*full_key).await?;
528 Ok(count)
529 }
530 .await;
531 metrics::record_redis_op(op, result.is_ok());
532 result
409533 }
410534
411535 pub async fn clear(&self) -> Result<(), RedisError> {
412 let pattern = format!("{}*", self.key_prefix);
413 let mut scan_conn = self.spawn_conn();
414 let mut delete_conn = self.spawn_conn();
415 let mut keys: Vec<String> = Vec::with_capacity(CLEAR_BATCH_SIZE);
416 {
417 let mut iter = scan_conn.scan_match::<_, String>(&*pattern).await?;
418 while let Some(key) = iter.next_item().await {
419 keys.push(key?);
420 if keys.len() == CLEAR_BATCH_SIZE {
421 let _: usize = delete_conn.unlink(&keys).await?;
422 keys.clear();
536 let op = "clear";
537 let result: Result<(), RedisError> = async {
538 let pattern = format!("{}*", self.key_prefix);
539 let mut scan_conn = self.spawn_conn();
540 let mut delete_conn = self.spawn_conn();
541 let mut keys: Vec<String> = Vec::with_capacity(CLEAR_BATCH_SIZE);
542 {
543 let mut iter = scan_conn.scan_match::<_, String>(&*pattern).await?;
544 while let Some(key) = iter.next_item().await {
545 keys.push(key?);
546 if keys.len() == CLEAR_BATCH_SIZE {
547 let _: usize = delete_conn.unlink(&keys).await?;
548 keys.clear();
549 }
423550 }
424551 }
425 }
426 if !keys.is_empty() {
427 let _: usize = delete_conn.unlink(&keys).await?;
552 if !keys.is_empty() {
553 let _: usize = delete_conn.unlink(&keys).await?;
554 }
555 Ok(())
428556 }
429 Ok(())
557 .await;
558 metrics::record_redis_op(op, result.is_ok());
559 result
430560 }
431561
432562 pub fn connection(&self) -> RedisConnClone {
@@ -444,4 +574,4 @@
444574 RedisConn::Sentinel(c) => RedisConnClone::Sentinel(c.clone()),
445575 }
446576 }
447}
577}
No newline at end of file
Modified lib/cache/hybrid.rs lib/cache/hybrid.rs +6 −0 View lib/cache/hybrid.rs
@@ -2,6 +2,7 @@
22use bincache::{Cache as Bincache, Noop};
33
44use crate::config::cache::AppCacheSettings;
5use crate::metrics;
56use std::collections::HashMap;
67use std::path::Path;
78use std::sync::Arc;
@@ -65,6 +66,7 @@
6566 let hybrid = self.hybrid.read().await;
6667 if let Ok(val) = hybrid.get(key.to_string()).await {
6768 self.stats.hybrid_hits.fetch_add(1, Ordering::Relaxed);
69 metrics::record_lookup(metrics::CacheLookupOutcome::HybridHit);
6870 debug!(key, tier = "hybrid", "cache hit");
6971 return Some(val.into_owned());
7072 }
@@ -74,6 +76,7 @@
7476 match l3.get_bytes_with_ttl(key).await {
7577 Ok(Some((val, remaining_ttl_millis))) => {
7678 self.stats.l3_hits.fetch_add(1, Ordering::Relaxed);
79 metrics::record_lookup(metrics::CacheLookupOutcome::L3Hit);
7780 debug!(key, tier = 3, "cache hit");
7881 if remaining_ttl_millis != Some(0) {
7982 let mut expirations = self.expires_at.write().await;
@@ -97,12 +100,14 @@
97100 Ok(None) => {}
98101 Err(e) => {
99102 self.stats.errors.fetch_add(1, Ordering::Relaxed);
103 metrics::record_error();
100104 debug!(key, error = %e, "L3 cache error");
101105 }
102106 }
103107 }
104108
105109 self.stats.misses.fetch_add(1, Ordering::Relaxed);
110 metrics::record_lookup(metrics::CacheLookupOutcome::Miss);
106111 debug!(key, "cache miss");
107112 None
108113 }
@@ -135,6 +140,7 @@
135140 && let Err(e) = l3.set_bytes(key, value, ttl).await
136141 {
137142 self.stats.errors.fetch_add(1, Ordering::Relaxed);
143 metrics::record_error();
138144 debug!(key, error = %e, "L3 cache set error");
139145 }
140146 }
Modified lib/cache/lib.rs lib/cache/lib.rs +1 −0 View lib/cache/lib.rs
@@ -1,5 +1,6 @@
11pub mod config;
22pub mod hybrid;
3pub mod metrics;
34pub mod redis;
45pub mod typed;
56
Added lib/cache/metrics.rs lib/cache/metrics.rs +57 −0 View lib/cache/metrics.rs
@@ -0,0 +1,57 @@
1use std::sync::Once;
2
3use metrics::{counter, describe_counter};
4
5pub enum CacheLookupOutcome {
6 HybridHit,
7 L3Hit,
8 Miss,
9}
10
11impl CacheLookupOutcome {
12 fn as_label(&self) -> &'static str {
13 match self {
14 CacheLookupOutcome::HybridHit => "hit_hybrid",
15 CacheLookupOutcome::L3Hit => "hit_l3",
16 CacheLookupOutcome::Miss => "miss",
17 }
18 }
19}
20
21static DESCRIBE_ONCE: Once = Once::new();
22
23pub fn ensure_described() {
24 DESCRIBE_ONCE.call_once(describe_all);
25}
26
27fn describe_all() {
28 describe_counter!(
29 "cache_lookup_total",
30 "Cache lookups partitioned by outcome (hit_hybrid, hit_l3, miss). Hit rate = (hit_hybrid + hit_l3) / lookups."
31 );
32 describe_counter!("cache_errors_total", "Cache errors at the hybrid or L3 layer.");
33 describe_counter!(
34 "cache_redis_ops_total",
35 "Raw Redis operations partitioned by op and outcome (success, error)."
36 );
37}
38
39pub fn record_lookup(outcome: CacheLookupOutcome) {
40 ensure_described();
41 counter!("cache_lookup_total", "outcome" => outcome.as_label()).increment(1);
42}
43
44pub fn record_error() {
45 ensure_described();
46 counter!("cache_errors_total").increment(1);
47}
48
49pub fn record_redis_op(op: &'static str, ok: bool) {
50 ensure_described();
51 counter!(
52 "cache_redis_ops_total",
53 "op" => op,
54 "outcome" => if ok { "success" } else { "error" },
55 )
56 .increment(1);
57}
No newline at end of file