Skip to main content

nova_boot_resilience_store/
lib.rs

1//! Resilience store abstraction and optional Redis-backed implementation.
2//!
3//! The `ResilienceStore` trait exposes a minimal set of operations used by
4//! the framework to implement distributed circuit breakers and rate limiters.
5//!
6//! The optional `redis-store` feature provides `RedisStore`, a small
7//! adapter that converts Redis return values to `LuaValue` variants.
8
9use std::fmt;
10
11/// Error type returned by resilience store implementations.
12#[derive(Debug, Clone)]
13pub struct ResilienceError {
14    message: String,
15}
16
17impl ResilienceError {
18    /// Create a new `ResilienceError` with a message.
19    pub fn new(message: impl Into<String>) -> Self {
20        Self {
21            message: message.into(),
22        }
23    }
24}
25
26impl fmt::Display for ResilienceError {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        write!(f, "{}", self.message)
29    }
30}
31
32impl std::error::Error for ResilienceError {}
33
34/// Lua script return values supported by resilience stores.
35///
36/// This enum models the limited set of return types we expect from
37/// `EVAL` / script invocations used by rate limiter and circuit breaker
38/// helpers.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum LuaValue {
41    Nil,
42    Integer(i64),
43    Bytes(Vec<u8>),
44    Array(Vec<LuaValue>),
45    Status(String),
46    Ok,
47}
48
49impl LuaValue {
50    /// Return an `i64` if the value is an integer.
51    pub fn as_i64(&self) -> Option<i64> {
52        match self {
53            Self::Integer(value) => Some(*value),
54            _ => None,
55        }
56    }
57
58    /// Return bytes slice when the variant holds raw bytes.
59    pub fn as_bytes(&self) -> Option<&[u8]> {
60        match self {
61            Self::Bytes(value) => Some(value.as_slice()),
62            _ => None,
63        }
64    }
65
66    /// Return text representation when the value holds UTF-8 bytes.
67    pub fn as_str(&self) -> Option<&str> {
68        self.as_bytes()
69            .and_then(|value| std::str::from_utf8(value).ok())
70    }
71}
72
73/// Minimal distributed key-value operations used by resilience backends.
74///
75/// Implement this trait to allow the framework to store counters, flags
76/// and execute Lua scripts atomically in a backing store (e.g., Redis).
77#[async_trait::async_trait]
78pub trait ResilienceStore: Send + Sync + 'static {
79    async fn incr(&self, key: &str) -> Result<i64, ResilienceError>;
80    async fn get_i64(&self, key: &str) -> Result<Option<i64>, ResilienceError>;
81    async fn set_ex(&self, key: &str, val: i64, ttl_seconds: usize) -> Result<(), ResilienceError>;
82    async fn del(&self, key: &str) -> Result<(), ResilienceError>;
83    async fn eval_lua(
84        &self,
85        script: &str,
86        keys: &[&str],
87        args: &[&str],
88    ) -> Result<LuaValue, ResilienceError>;
89}
90
91#[cfg(feature = "redis-store")]
92pub mod redis_store {
93    //! Small Redis adapter implementing `ResilienceStore`.
94    //!
95    //! This adapter converts Redis library return types into `LuaValue`
96    //! variants and provides async implementations of the trait methods.
97
98    use super::{LuaValue, ResilienceError, ResilienceStore};
99    use redis::AsyncCommands;
100    use redis::Client;
101
102    /// Redis-backed resilience store.
103    #[derive(Clone)]
104    pub struct RedisStore {
105        client: Client,
106    }
107
108    impl RedisStore {
109        /// Create a new `RedisStore` from a Redis connection URL.
110        pub fn new(url: &str) -> Result<Self, ResilienceError> {
111            let client = Client::open(url).map_err(|e| ResilienceError::new(e.to_string()))?;
112            Ok(Self { client })
113        }
114    }
115
116    impl From<redis::Value> for LuaValue {
117        fn from(value: redis::Value) -> Self {
118            match value {
119                redis::Value::Nil => Self::Nil,
120                redis::Value::Int(value) => Self::Integer(value),
121                redis::Value::Data(value) => Self::Bytes(value),
122                redis::Value::Bulk(values) => {
123                    Self::Array(values.into_iter().map(Self::from).collect())
124                }
125                redis::Value::Status(value) => Self::Status(value),
126                redis::Value::Okay => Self::Ok,
127            }
128        }
129    }
130
131    #[async_trait::async_trait]
132    impl ResilienceStore for RedisStore {
133        async fn incr(&self, key: &str) -> Result<i64, ResilienceError> {
134            let mut conn = self
135                .client
136                .get_tokio_connection()
137                .await
138                .map_err(|e| ResilienceError::new(e.to_string()))?;
139            let v: i64 = conn
140                .incr(key, 1)
141                .await
142                .map_err(|e| ResilienceError::new(e.to_string()))?;
143            Ok(v)
144        }
145
146        async fn get_i64(&self, key: &str) -> Result<Option<i64>, ResilienceError> {
147            let mut conn = self
148                .client
149                .get_tokio_connection()
150                .await
151                .map_err(|e| ResilienceError::new(e.to_string()))?;
152            let v: Option<i64> = conn
153                .get(key)
154                .await
155                .map_err(|e| ResilienceError::new(e.to_string()))?;
156            Ok(v)
157        }
158
159        async fn set_ex(
160            &self,
161            key: &str,
162            val: i64,
163            ttl_seconds: usize,
164        ) -> Result<(), ResilienceError> {
165            let mut conn = self
166                .client
167                .get_tokio_connection()
168                .await
169                .map_err(|e| ResilienceError::new(e.to_string()))?;
170            let () = conn
171                .set_ex(key, val, ttl_seconds)
172                .await
173                .map_err(|e| ResilienceError::new(e.to_string()))?;
174            Ok(())
175        }
176
177        async fn del(&self, key: &str) -> Result<(), ResilienceError> {
178            let mut conn = self
179                .client
180                .get_tokio_connection()
181                .await
182                .map_err(|e| ResilienceError::new(e.to_string()))?;
183            let () = conn
184                .del(key)
185                .await
186                .map_err(|e| ResilienceError::new(e.to_string()))?;
187            Ok(())
188        }
189
190        async fn eval_lua(
191            &self,
192            script: &str,
193            keys: &[&str],
194            args: &[&str],
195        ) -> Result<LuaValue, ResilienceError> {
196            let mut conn = self
197                .client
198                .get_tokio_connection()
199                .await
200                .map_err(|e| ResilienceError::new(e.to_string()))?;
201
202            let script = redis::Script::new(script);
203            let mut invocation = script.prepare_invoke();
204            for key in keys {
205                invocation.key(*key);
206            }
207            for arg in args {
208                invocation.arg(*arg);
209            }
210
211            let value: redis::Value = invocation
212                .invoke_async(&mut conn)
213                .await
214                .map_err(|e| ResilienceError::new(e.to_string()))?;
215            Ok(value.into())
216        }
217    }
218}