diff --git a/moka-store/src/lib.rs b/moka-store/src/lib.rs index 6fd3258..fa9ae83 100644 --- a/moka-store/src/lib.rs +++ b/moka-store/src/lib.rs @@ -1,5 +1,7 @@ +use std::time::{Duration as StdDuration, Instant as StdInstant}; + use async_trait::async_trait; -use moka::future::Cache; +use moka::{future::Cache, Expiry}; use time::OffsetDateTime; use tower_sessions_core::{ session::{Id, Record}, @@ -7,6 +9,10 @@ use tower_sessions_core::{ }; /// A session store that uses Moka, a fast and concurrent caching library. +/// +/// This store uses Moka's built-in time-based per-entry expiration policy +/// according to the session's expiry date. Therefore, expired sessions +/// are automatically removed from the cache. #[derive(Debug, Clone)] pub struct MokaStore { cache: Cache, @@ -28,7 +34,8 @@ impl MokaStore { let cache_builder = match max_capacity { Some(capacity) => Cache::builder().max_capacity(capacity), None => Cache::builder(), - }; + } + .expire_after(SessionExpiry); Self { cache: cache_builder.build(), @@ -52,11 +59,9 @@ impl SessionStore for MokaStore { } async fn load(&self, session_id: &Id) -> session_store::Result> { - Ok(self - .cache - .get(session_id) - .await - .filter(|Record { expiry_date, .. }| is_active(*expiry_date))) + // expired sessions are automatically removed from the cache, + // so it's safe to just call get + Ok(self.cache.get(session_id).await) } async fn delete(&self, session_id: &Id) -> session_store::Result<()> { @@ -65,9 +70,48 @@ impl SessionStore for MokaStore { } } -// TODO: Moka supports expiry natively, but that interface is being overhauled -// such that it's more accessible. When that work is done, we should replace -// this with actual expiry. -fn is_active(expiry_date: OffsetDateTime) -> bool { - expiry_date > OffsetDateTime::now_utc() +/// Moka per-entry expiration policy for session records. +struct SessionExpiry; + +impl SessionExpiry { + /// Calculates the expiry duration of a record + /// by comparing it to the current time. + /// + /// If the expiry date of the record is in the past, + /// returns an empty duration. + fn expiry_date_to_duration(record: &Record) -> StdDuration { + // we use this to calculate the current time + // because it is not possible to convert + // StdInstant to OffsetDateTime + let now = OffsetDateTime::now_utc(); + let expiry_date = record.expiry_date; + + if expiry_date > now { + (expiry_date - now).unsigned_abs() + } else { + StdDuration::default() + } + } +} + +impl Expiry for SessionExpiry { + fn expire_after_create( + &self, + _id: &Id, + record: &Record, + _created_at: StdInstant, + ) -> Option { + Some(Self::expiry_date_to_duration(record)) + } + + fn expire_after_update( + &self, + _id: &Id, + record: &Record, + _updated_at: StdInstant, + _duration_until_expiry: Option, + ) -> Option { + // expiry_date could change, so we calculate it again + Some(Self::expiry_date_to_duration(record)) + } }