1use crate::cache::ttl_millis;
4use crate::{Admission, AdmitMarker, Cache, CacheError};
5use async_trait::async_trait;
6use siderite_backends::redis::{MarkerAdmission, MarkerReply};
7use siderite_backends::redis::{RedisError, RedisStore};
8use std::time::Duration;
9
10const DEFAULT_PREFIX: &str = "siderite-cache:";
12
13#[derive(Clone)]
24pub struct RedisCache {
25 store: RedisStore,
26}
27
28impl std::fmt::Debug for RedisCache {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 f.debug_struct("RedisCache")
31 .field("store", &self.store)
32 .finish()
33 }
34}
35
36impl RedisCache {
37 #[must_use]
39 pub fn new(store: RedisStore) -> Self {
40 let store = if store.prefix().is_empty() {
41 store.with_prefix(DEFAULT_PREFIX)
42 } else {
43 store
44 };
45 Self { store }
46 }
47
48 pub async fn connect(url: &str) -> Result<Self, CacheError> {
54 let store = RedisStore::connect(url).await?;
55 Ok(Self::new(store))
56 }
57
58 #[must_use]
60 pub fn store(&self) -> &RedisStore {
61 &self.store
62 }
63}
64
65impl From<RedisError> for CacheError {
66 fn from(err: RedisError) -> Self {
67 Self::Backend(err.to_string())
68 }
69}
70
71fn encode_bytes(value: Vec<u8>) -> String {
73 value.into_iter().map(char::from).collect()
74}
75
76fn decode_bytes(key: &str, value: String) -> Result<Vec<u8>, CacheError> {
77 let mut out = Vec::with_capacity(value.len());
78 for ch in value.chars() {
79 match u8::try_from(u32::from(ch)) {
80 Ok(byte) => out.push(byte),
81 Err(_) => {
82 return Err(CacheError::Backend(format!(
83 "cache key `{key}` is not a Latin-1 Redis string"
84 )));
85 }
86 }
87 }
88 Ok(out)
89}
90
91fn is_incr_value_error(message: &str) -> bool {
92 let lower = message.to_ascii_lowercase();
93 lower.contains("not an integer")
94 || lower.contains("would overflow")
95 || lower.contains("out of range")
96}
97
98#[async_trait]
99impl Cache for RedisCache {
100 async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, CacheError> {
101 match self.store.get(key).await? {
102 None => Ok(None),
103 Some(value) => decode_bytes(key, value).map(Some),
104 }
105 }
106
107 async fn set(
108 &self,
109 key: &str,
110 value: Vec<u8>,
111 ttl: Option<Duration>,
112 ) -> Result<(), CacheError> {
113 self.store.set(key, &encode_bytes(value), ttl).await?;
114 Ok(())
115 }
116
117 async fn set_if_absent(&self, key: &str, value: Vec<u8>) -> Result<bool, CacheError> {
118 Ok(self.store.set_nx(key, &encode_bytes(value)).await?)
119 }
120
121 async fn set_if_absent_with_ttl(
122 &self,
123 key: &str,
124 value: Vec<u8>,
125 ttl: Duration,
126 ) -> Result<bool, CacheError> {
127 let ttl = Duration::from_millis(ttl_millis(ttl)?);
131 Ok(self.store.set_nx_px(key, &encode_bytes(value), ttl).await?)
132 }
133
134 async fn delete(&self, key: &str) -> Result<bool, CacheError> {
135 Ok(self.store.del(key).await? > 0)
136 }
137
138 async fn increment(&self, key: &str, by: i64) -> Result<i64, CacheError> {
139 match self.store.incr_by(key, by).await {
140 Ok(value) => Ok(value),
141 Err(RedisError::Command(message)) if is_incr_value_error(&message) => {
142 Err(CacheError::NotInteger {
143 key: key.to_owned(),
144 })
145 }
146 Err(err) => Err(err.into()),
147 }
148 }
149
150 async fn increment_with_expiry(
151 &self,
152 key: &str,
153 by: i64,
154 ttl: Duration,
155 ) -> Result<i64, CacheError> {
156 let ttl = Duration::from_millis(ttl_millis(ttl)?);
158 match self.store.incr_by_expire(key, by, ttl).await {
159 Ok(value) => Ok(value),
160 Err(RedisError::Command(message)) if is_incr_value_error(&message) => {
161 Err(CacheError::NotInteger {
162 key: key.to_owned(),
163 })
164 }
165 Err(err) => Err(err.into()),
166 }
167 }
168
169 async fn clear(&self) -> Result<(), CacheError> {
170 self.store.delete_namespace().await?;
171 Ok(())
172 }
173
174 async fn admit_marker(&self, req: &AdmitMarker<'_>) -> Result<Admission, CacheError> {
175 let token = encode_bytes(req.token.to_vec());
179 let epoch = encode_bytes(req.epoch.to_vec());
180 let call = MarkerAdmission {
181 marker: req.marker,
182 token: &token,
183 epoch_key: req.epoch_key,
184 epoch: &epoch,
185 budget_prefix: req.budget_prefix,
186 max_new: req.max_new,
187 window: req.window,
188 };
189 match self.store.admit_marker(call).await? {
190 MarkerReply::Admitted(value) => decode_bytes(req.marker, value).map(Admission::Marker),
191 MarkerReply::Stale => Ok(Admission::Stale),
192 MarkerReply::Exhausted(resets_in) => Ok(Admission::Exhausted { resets_in }),
193 }
194 }
195
196 async fn invalidate_marker(
197 &self,
198 marker: &str,
199 epoch_key: &str,
200 epoch: &[u8],
201 ttl: Duration,
202 ) -> Result<(), CacheError> {
203 let epoch = encode_bytes(epoch.to_vec());
206 let ttl = Duration::from_millis(ttl_millis(ttl)?);
207 Ok(self
208 .store
209 .rotate_marker(marker, epoch_key, &epoch, ttl)
210 .await?)
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn latin1_roundtrip_covers_every_byte() {
220 let original: Vec<u8> = (0..=255).collect();
221 let encoded = encode_bytes(original.clone());
222 assert_eq!(decode_bytes("k", encoded).unwrap(), original);
223 }
224
225 #[test]
226 fn decode_rejects_non_latin1() {
227 let err = decode_bytes("k", "€".into()).unwrap_err();
228 assert!(matches!(err, CacheError::Backend(_)));
229 }
230
231 #[test]
232 fn incr_error_classifier() {
233 assert!(is_incr_value_error(
234 "ERR value is not an integer or out of range"
235 ));
236 assert!(is_incr_value_error(
237 "ERR increment or decrement would overflow"
238 ));
239 assert!(!is_incr_value_error("WRONGTYPE Operation against a key"));
240 }
241}