Skip to main content

vv_agent/sessions/
redis_store.rs

1use redis::{Commands, Connection as RedisConnection};
2
3use super::*;
4
5const REDIS_SESSION_KEY_PREFIX: &str = "vv-agent-session";
6
7#[derive(Clone)]
8pub struct RedisSessionStore {
9    connection: Arc<Mutex<RedisConnection>>,
10    key_prefix: Arc<String>,
11}
12
13impl RedisSessionStore {
14    pub fn new(redis_url: impl AsRef<str>) -> Result<Self, String> {
15        Self::with_key_prefix(redis_url, REDIS_SESSION_KEY_PREFIX)
16    }
17
18    pub fn with_key_prefix(
19        redis_url: impl AsRef<str>,
20        key_prefix: impl Into<String>,
21    ) -> Result<Self, String> {
22        let client = redis::Client::open(redis_url.as_ref()).map_err(redis_error)?;
23        let connection = client.get_connection().map_err(redis_error)?;
24        Ok(Self {
25            connection: Arc::new(Mutex::new(connection)),
26            key_prefix: Arc::new(key_prefix.into()),
27        })
28    }
29
30    pub fn session(&self, session_id: &str) -> Arc<dyn Session> {
31        <Self as SessionStore>::session(self, session_id)
32    }
33}
34
35impl SessionStore for RedisSessionStore {
36    fn session(&self, session_id: &str) -> Arc<dyn Session> {
37        Arc::new(RedisSession {
38            session_id: Arc::new(session_id.to_string()),
39            connection: self.connection.clone(),
40            key_prefix: self.key_prefix.clone(),
41        })
42    }
43}
44
45#[derive(Clone)]
46struct RedisSession {
47    session_id: Arc<String>,
48    connection: Arc<Mutex<RedisConnection>>,
49    key_prefix: Arc<String>,
50}
51
52impl RedisSession {
53    fn key(&self) -> String {
54        format!("{}:{}", self.key_prefix, self.session_id)
55    }
56
57    fn commit_key(&self) -> String {
58        format!("{}:commits", self.key())
59    }
60}
61
62impl Session for RedisSession {
63    fn session_id(&self) -> &str {
64        self.session_id.as_str()
65    }
66
67    fn get_items(&self, limit: Option<usize>) -> SessionFuture<Vec<SessionItem>> {
68        let connection = self.connection.clone();
69        let key = self.key();
70        Box::pin(async move {
71            let raw_items: Vec<String> = {
72                let mut connection = connection
73                    .lock()
74                    .map_err(|_| "redis session store lock poisoned".to_string())?;
75                match limit {
76                    Some(0) => return Ok(Vec::new()),
77                    Some(limit) => {
78                        let limit = isize::try_from(limit).unwrap_or(isize::MAX);
79                        connection.lrange(&key, -limit, -1).map_err(redis_error)?
80                    }
81                    None => connection.lrange(&key, 0, -1).map_err(redis_error)?,
82                }
83            };
84            raw_items
85                .into_iter()
86                .map(|item_json| serde_json::from_str(&item_json).map_err(json_error))
87                .collect()
88        })
89    }
90
91    fn add_items(&self, items: Vec<SessionItem>) -> SessionFuture<()> {
92        let connection = self.connection.clone();
93        let key = self.key();
94        Box::pin(async move {
95            if items.is_empty() {
96                return Ok(());
97            }
98            let payloads = items
99                .iter()
100                .map(serde_json::to_string)
101                .collect::<Result<Vec<_>, _>>()
102                .map_err(json_error)?;
103            connection
104                .lock()
105                .map_err(|_| "redis session store lock poisoned".to_string())?
106                .rpush::<_, _, usize>(key, payloads)
107                .map_err(redis_error)?;
108            Ok(())
109        })
110    }
111
112    fn supports_add_items_once(&self) -> bool {
113        true
114    }
115
116    fn add_items_once(
117        &self,
118        commit_id: String,
119        payload_digest: String,
120        items: Vec<SessionItem>,
121    ) -> SessionFuture<SessionAppendOutcome> {
122        let connection = self.connection.clone();
123        let items_key = self.key();
124        let commits_key = self.commit_key();
125        Box::pin(async move {
126            validate_session_commit(&commit_id, &payload_digest, &items)?;
127            let payloads = items
128                .iter()
129                .map(serde_json::to_string)
130                .collect::<Result<Vec<_>, _>>()
131                .map_err(json_error)?;
132            let script = redis::Script::new(
133                r#"
134                local existing = redis.call('HGET', KEYS[2], ARGV[1])
135                if existing then
136                    if existing == ARGV[2] then
137                        return 0
138                    end
139                    return -1
140                end
141                for index = 3, #ARGV do
142                    redis.call('RPUSH', KEYS[1], ARGV[index])
143                end
144                redis.call('HSET', KEYS[2], ARGV[1], ARGV[2])
145                return 1
146                "#,
147            );
148            let mut invocation = script.prepare_invoke();
149            invocation.key(items_key).key(commits_key);
150            invocation.arg(commit_id).arg(payload_digest);
151            for payload in payloads {
152                invocation.arg(payload);
153            }
154            let outcome: i64 = invocation
155                .invoke(
156                    &mut *connection
157                        .lock()
158                        .map_err(|_| "redis session store lock poisoned".to_string())?,
159                )
160                .map_err(redis_error)?;
161            match outcome {
162                1 => Ok(SessionAppendOutcome::Committed),
163                0 => Ok(SessionAppendOutcome::Replayed),
164                -1 => Err(
165                    "session_commit_identity_conflict: commit_id has a different payload"
166                        .to_string(),
167                ),
168                _ => Err("redis session append-once returned an invalid outcome".to_string()),
169            }
170        })
171    }
172
173    fn pop_item(&self) -> SessionFuture<Option<SessionItem>> {
174        let connection = self.connection.clone();
175        let key = self.key();
176        Box::pin(async move {
177            let raw: Option<String> = connection
178                .lock()
179                .map_err(|_| "redis session store lock poisoned".to_string())?
180                .rpop(key, None)
181                .map_err(redis_error)?;
182            raw.map(|item_json| serde_json::from_str(&item_json).map_err(json_error))
183                .transpose()
184        })
185    }
186
187    fn clear(&self) -> SessionFuture<()> {
188        let connection = self.connection.clone();
189        let key = self.key();
190        let commit_key = self.commit_key();
191        Box::pin(async move {
192            redis::cmd("DEL")
193                .arg(key)
194                .arg(commit_key)
195                .query::<usize>(
196                    &mut *connection
197                        .lock()
198                        .map_err(|_| "redis session store lock poisoned".to_string())?,
199                )
200                .map_err(redis_error)?;
201            Ok(())
202        })
203    }
204}
205
206fn redis_error(error: redis::RedisError) -> String {
207    error.to_string()
208}