1use std::sync::Arc;
5use std::time::Duration;
6
7use chrono::Duration as ChronoDuration;
8use rainier_cache::{Cache, CacheExt};
9use rainier_support::{BoxFuture, Result};
10
11use crate::session::SessionData;
12use crate::store::SessionStore;
13
14const PREFIX: &str = "session:";
16
17pub struct CacheSessionStore {
49 cache: Arc<dyn Cache>,
50 lifetime: Duration,
51}
52
53impl CacheSessionStore {
54 pub fn new(cache: Arc<dyn Cache>) -> Self {
56 Self { cache, lifetime: Duration::from_secs(2 * 60 * 60) }
57 }
58
59 pub fn with_lifetime(mut self, lifetime: ChronoDuration) -> Self {
61 if let Ok(lifetime) = lifetime.to_std() {
62 self.lifetime = lifetime;
63 }
64 self
65 }
66
67 pub fn cache(&self) -> &Arc<dyn Cache> {
69 &self.cache
70 }
71
72 pub fn lifetime(&self) -> Duration {
74 self.lifetime
75 }
76
77 fn key(id: &str) -> String {
78 format!("{PREFIX}{id}")
79 }
80}
81
82impl SessionStore for CacheSessionStore {
83 fn name(&self) -> &str {
84 self.cache.name()
87 }
88
89 fn read<'a>(&'a self, id: &'a str) -> BoxFuture<'a, Result<Option<SessionData>>> {
90 Box::pin(async move {
91 self.cache.get_json(&Self::key(id)).await
96 })
97 }
98
99 fn write<'a>(&'a self, id: &'a str, data: &'a SessionData) -> BoxFuture<'a, Result<()>> {
100 Box::pin(async move {
101 self.cache.put_json(&Self::key(id), data, Some(self.lifetime)).await
105 })
106 }
107
108 fn destroy<'a>(&'a self, id: &'a str) -> BoxFuture<'a, Result<()>> {
109 Box::pin(async move {
110 self.cache.forget(&Self::key(id)).await?;
111 Ok(())
112 })
113 }
114
115 fn gc(&self) -> BoxFuture<'_, Result<u64>> {
121 Box::pin(async { Ok(0) })
122 }
123}
124
125impl std::fmt::Debug for CacheSessionStore {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 f.debug_struct("CacheSessionStore")
128 .field("cache", &self.cache.name())
129 .field("lifetime", &self.lifetime)
130 .finish()
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use rainier_cache::MemoryCache;
138
139 fn store() -> (CacheSessionStore, Arc<MemoryCache>) {
140 let cache = Arc::new(MemoryCache::new());
141 (CacheSessionStore::new(Arc::clone(&cache) as Arc<dyn Cache>), cache)
142 }
143
144 fn data(user: u64) -> SessionData {
145 let mut values = serde_json::Map::new();
146 values.insert("user_id".into(), user.into());
147 SessionData { values, flash: Vec::new() }
148 }
149
150 #[tokio::test]
151 async fn a_written_session_reads_back() {
152 let (store, _) = store();
153 store.write("abc", &data(42)).await.unwrap();
154
155 assert_eq!(store.read("abc").await.unwrap().unwrap().values["user_id"], 42);
156 }
157
158 #[tokio::test]
159 async fn an_unknown_session_reads_as_none() {
160 let (store, _) = store();
161 assert!(store.read("nope").await.unwrap().is_none());
162 }
163
164 #[tokio::test]
165 async fn destroying_removes_it() {
166 let (store, _) = store();
167 store.write("abc", &data(42)).await.unwrap();
168
169 store.destroy("abc").await.unwrap();
170 assert!(store.read("abc").await.unwrap().is_none());
171 }
172
173 #[tokio::test]
174 async fn keys_are_prefixed_so_they_do_not_collide_with_cached_values() {
175 let (store, cache) = store();
176 store.write("abc", &data(42)).await.unwrap();
177
178 assert!(cache.has("session:abc").await.unwrap());
179 assert!(!cache.has("abc").await.unwrap(), "the bare key must be free for other uses");
180 }
181
182 #[tokio::test]
183 async fn a_ttl_is_set_on_every_write() {
184 let brief = CacheSessionStore::new(Arc::new(MemoryCache::new()))
188 .with_lifetime(ChronoDuration::nanoseconds(1));
189
190 brief.write("brief", &data(1)).await.unwrap();
191 assert!(brief.read("brief").await.unwrap().is_none());
192 }
193
194 #[tokio::test]
195 async fn an_unreadable_session_reads_as_a_fresh_one() {
196 let (store, cache) = store();
197 cache.put("session:abc", b"not json", None).await.unwrap();
198
199 assert!(store.read("abc").await.unwrap().is_none());
200 }
201
202 #[tokio::test]
203 async fn gc_does_not_flush_the_cache() {
204 let (store, cache) = store();
206 cache.put("something-else", b"v", None).await.unwrap();
207
208 assert_eq!(store.gc().await.unwrap(), 0);
209 assert!(cache.has("something-else").await.unwrap());
210 }
211
212 #[tokio::test]
213 async fn the_name_reports_the_driver_underneath() {
214 let (store, _) = store();
215 assert_eq!(store.name(), "memory");
216 }
217
218 #[test]
219 fn the_lifetime_is_configurable_and_ignores_a_negative_one() {
220 let (store, _) = store();
221 let store = store.with_lifetime(ChronoDuration::days(14));
222 assert_eq!(store.lifetime(), Duration::from_secs(14 * 24 * 60 * 60));
223
224 let store = store.with_lifetime(ChronoDuration::seconds(-1));
227 assert_eq!(store.lifetime(), Duration::from_secs(14 * 24 * 60 * 60));
228 }
229
230 #[tokio::test]
231 async fn it_is_not_client_side() {
232 let (store, _) = store();
233 assert!(!store.is_client_side());
234 assert!(store.decode("chosen-by-the-client").is_err());
235 }
236}