tower_sessions_ext_core/
session_store.rs1use std::fmt::Debug;
86
87use async_trait::async_trait;
88
89use crate::session::{Id, Record};
90
91#[derive(thiserror::Error, Debug)]
93pub enum Error {
94 #[error("Encoding failed with: {0}")]
95 Encode(String),
96
97 #[error("Decoding failed with: {0}")]
98 Decode(String),
99
100 #[error("{0}")]
101 Backend(String),
102}
103
104pub type Result<T> = std::result::Result<T, Error>;
105
106#[async_trait]
110pub trait SessionStore: Debug + Send + Sync + 'static {
111 async fn create(&self, session_record: &mut Record) -> Result<()> {
119 default_create(self, session_record).await
120 }
121
122 async fn save(&self, session_record: &Record) -> Result<()>;
126
127 async fn load(&self, session_id: &Id) -> Result<Option<Record>>;
133
134 async fn delete(&self, session_id: &Id) -> Result<()>;
138}
139
140async fn default_create<S: SessionStore + ?Sized>(
141 store: &S,
142 session_record: &mut Record,
143) -> Result<()> {
144 tracing::warn!(
145 "The default implementation of `SessionStore::create` is being used, which relies on \
146 `SessionStore::save`. To properly handle potential ID collisions, it is recommended that \
147 stores implement their own version of `SessionStore::create`."
148 );
149 store.save(session_record).await?;
150 Ok(())
151}
152
153#[derive(Debug, Clone)]
177pub struct CachingSessionStore<Cache: SessionStore, Store: SessionStore> {
178 cache: Cache,
179 store: Store,
180}
181
182impl<Cache: SessionStore, Store: SessionStore> CachingSessionStore<Cache, Store> {
183 pub fn new(cache: Cache, store: Store) -> Self {
185 Self { cache, store }
186 }
187}
188
189#[async_trait]
190impl<Cache, Store> SessionStore for CachingSessionStore<Cache, Store>
191where
192 Cache: SessionStore,
193 Store: SessionStore,
194{
195 async fn create(&self, record: &mut Record) -> Result<()> {
196 self.store.create(record).await?;
197 self.cache.create(record).await?;
198 Ok(())
199 }
200
201 async fn save(&self, record: &Record) -> Result<()> {
202 let store_save_fut = self.store.save(record);
203 let cache_save_fut = self.cache.save(record);
204
205 futures::try_join!(store_save_fut, cache_save_fut)?;
206
207 Ok(())
208 }
209
210 async fn load(&self, session_id: &Id) -> Result<Option<Record>> {
211 match self.cache.load(session_id).await {
212 Ok(Some(session_record)) => Ok(Some(session_record)),
214
215 Ok(None) => {
219 let session_record = self.store.load(session_id).await?;
220
221 if let Some(ref session_record) = session_record {
222 self.cache.save(session_record).await?;
223 }
224
225 Ok(session_record)
226 }
227
228 Err(err) => Err(err),
230 }
231 }
232
233 async fn delete(&self, session_id: &Id) -> Result<()> {
234 let store_delete_fut = self.store.delete(session_id);
235 let cache_delete_fut = self.cache.delete(session_id);
236
237 futures::try_join!(store_delete_fut, cache_delete_fut)?;
238
239 Ok(())
240 }
241}
242
243#[async_trait]
245pub trait ExpiredDeletion: SessionStore
246where
247 Self: Sized,
248{
249 async fn delete_expired(&self) -> Result<Vec<Record>>;
251
252 #[cfg(feature = "deletion-task")]
285 #[cfg_attr(docsrs, doc(cfg(feature = "deletion-task")))]
286 async fn continuously_delete_expired(
287 self,
288 period: tokio::time::Duration,
289 on_deleted: impl Fn(Vec<Record>) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>> + Send + Sync + 'static,
290 ) -> Result<()> {
291 let mut interval = tokio::time::interval(period);
292 interval.tick().await; loop {
294 interval.tick().await;
295 let deleted = self.delete_expired().await?;
296 if !deleted.is_empty() {
297 on_deleted(deleted).await;
298 }
299 }
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use mockall::{
306 mock,
307 predicate::{self, *},
308 };
309 use time::{Duration, OffsetDateTime};
310
311 use super::*;
312
313 mock! {
314 #[derive(Debug)]
315 pub Cache {}
316
317 #[async_trait]
318 impl SessionStore for Cache {
319 async fn create(&self, record: &mut Record) -> Result<()>;
320 async fn save(&self, record: &Record) -> Result<()>;
321 async fn load(&self, session_id: &Id) -> Result<Option<Record>>;
322 async fn delete(&self, session_id: &Id) -> Result<()>;
323 }
324 }
325
326 mock! {
327 #[derive(Debug)]
328 pub Store {}
329
330 #[async_trait]
331 impl SessionStore for Store {
332 async fn create(&self, record: &mut Record) -> Result<()>;
333 async fn save(&self, record: &Record) -> Result<()>;
334 async fn load(&self, session_id: &Id) -> Result<Option<Record>>;
335 async fn delete(&self, session_id: &Id) -> Result<()>;
336 }
337 }
338
339 mock! {
340 #[derive(Debug)]
341 pub CollidingStore {}
342
343 #[async_trait]
344 impl SessionStore for CollidingStore {
345 async fn save(&self, record: &Record) -> Result<()>;
346 async fn load(&self, session_id: &Id) -> Result<Option<Record>>;
347 async fn delete(&self, session_id: &Id) -> Result<()>;
348 }
349 }
350
351 #[tokio::test]
352 async fn test_create() {
353 let mut store = MockCollidingStore::new();
354 let mut record = Record {
355 id: Default::default(),
356 data: Default::default(),
357 expiry_date: OffsetDateTime::now_utc() + Duration::minutes(30),
358 };
359
360 store
361 .expect_save()
362 .with(predicate::eq(record.clone()))
363 .times(1)
364 .returning(|_| Ok(()));
365 let result = store.create(&mut record).await;
366 assert!(result.is_ok());
367 }
368
369 #[tokio::test]
370 async fn test_save() {
371 let mut store = MockStore::new();
372 let record = Record {
373 id: Default::default(),
374 data: Default::default(),
375 expiry_date: OffsetDateTime::now_utc() + Duration::minutes(30),
376 };
377 store
378 .expect_save()
379 .with(predicate::eq(record.clone()))
380 .times(1)
381 .returning(|_| Ok(()));
382
383 let result = store.save(&record).await;
384 assert!(result.is_ok());
385 }
386
387 #[tokio::test]
388 async fn test_load() {
389 let mut store = MockStore::new();
390 let session_id = Id::default();
391 let record = Record {
392 id: Default::default(),
393 data: Default::default(),
394 expiry_date: OffsetDateTime::now_utc() + Duration::minutes(30),
395 };
396 let expected_record = record.clone();
397
398 store
399 .expect_load()
400 .with(predicate::eq(session_id))
401 .times(1)
402 .returning(move |_| Ok(Some(record.clone())));
403
404 let result = store.load(&session_id).await;
405 assert!(result.is_ok());
406 assert_eq!(result.unwrap(), Some(expected_record));
407 }
408
409 #[tokio::test]
410 async fn test_delete() {
411 let mut store = MockStore::new();
412 let session_id = Id::default();
413
414 store
415 .expect_delete()
416 .with(predicate::eq(session_id))
417 .times(1)
418 .returning(|_| Ok(()));
419
420 let result = store.delete(&session_id).await;
421 assert!(result.is_ok());
422 }
423
424 #[tokio::test]
425 async fn test_caching_store_create() {
426 let mut cache = MockCache::new();
427 let mut store = MockStore::new();
428 let mut record = Record {
429 id: Default::default(),
430 data: Default::default(),
431 expiry_date: OffsetDateTime::now_utc() + Duration::minutes(30),
432 };
433
434 cache.expect_create().times(1).returning(|_| Ok(()));
435 store.expect_create().times(1).returning(|_| Ok(()));
436
437 let caching_store = CachingSessionStore::new(cache, store);
438 let result = caching_store.create(&mut record).await;
439 assert!(result.is_ok());
440 }
441
442 #[tokio::test]
443 async fn test_caching_store_save() {
444 let mut cache = MockCache::new();
445 let mut store = MockStore::new();
446 let record = Record {
447 id: Default::default(),
448 data: Default::default(),
449 expiry_date: OffsetDateTime::now_utc() + Duration::minutes(30),
450 };
451
452 cache
453 .expect_save()
454 .with(predicate::eq(record.clone()))
455 .times(1)
456 .returning(|_| Ok(()));
457 store
458 .expect_save()
459 .with(predicate::eq(record.clone()))
460 .times(1)
461 .returning(|_| Ok(()));
462
463 let caching_store = CachingSessionStore::new(cache, store);
464 let result = caching_store.save(&record).await;
465 assert!(result.is_ok());
466 }
467
468 #[tokio::test]
469 async fn test_caching_store_load() {
470 let mut cache = MockCache::new();
471 let mut store = MockStore::new();
472 let session_id = Id::default();
473 let record = Record {
474 id: Default::default(),
475 data: Default::default(),
476 expiry_date: OffsetDateTime::now_utc() + Duration::minutes(30),
477 };
478 let expected_record = record.clone();
479
480 cache
481 .expect_load()
482 .with(predicate::eq(session_id))
483 .times(1)
484 .returning(move |_| Ok(Some(record.clone())));
485 store.expect_load().times(0);
487
488 let caching_store = CachingSessionStore::new(cache, store);
489 let result = caching_store.load(&session_id).await;
490 assert!(result.is_ok());
491 assert_eq!(result.unwrap(), Some(expected_record));
492 }
493
494 #[tokio::test]
495 async fn test_caching_store_delete() {
496 let mut cache = MockCache::new();
497 let mut store = MockStore::new();
498 let session_id = Id::default();
499
500 cache
501 .expect_delete()
502 .with(predicate::eq(session_id))
503 .times(1)
504 .returning(|_| Ok(()));
505 store
506 .expect_delete()
507 .with(predicate::eq(session_id))
508 .times(1)
509 .returning(|_| Ok(()));
510
511 let caching_store = CachingSessionStore::new(cache, store);
512 let result = caching_store.delete(&session_id).await;
513 assert!(result.is_ok());
514 }
515}