vv_agent/sessions/
redis_store.rs1use 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}