saysion 0.1.2

Async session support with pluggable stores
Documentation
use std::{collections::HashMap, sync::Arc};

use async_lock::RwLock;

use crate::{Result, Session, SessionStore, async_trait};

/// # in-memory session store
/// Because there is no external
/// persistance, this session store is ephemeral and will be cleared
/// on server restart.
///
/// # ***READ THIS BEFORE USING IN A PRODUCTION DEPLOYMENT***
///
/// Storing sessions only in memory brings the following problems:
///
/// 1. All sessions must fit in available memory (important for high load services)
/// 2. Sessions stored in memory are cleared only if a client calls [MemoryStore::destroy_session] or [MemoryStore::clear_store].
///    If sessions are not cleaned up properly it might result in OOM
/// 3. All sessions will be lost on shutdown
/// 4. If the service is clustered particular session will be stored only on a single instance.
///    This might be solved by using load balancers with sticky sessions.
///    Unfortunately, this solution brings additional complexity especially if the connection is
///    using secure transport since the load balancer has to perform SSL termination to understand
///    where should it forward packets to
///
/// Example crates providing alternative implementations:
/// - [async-sqlx-session](https://crates.io/crates/async-sqlx-session) postgres & sqlite
/// - [async-redis-session](https://crates.io/crates/async-redis-session)
/// - [async-mongodb-session](https://crates.io/crates/async-mongodb-session)
///
#[derive(Default, Debug, Clone)]
pub struct MemoryStore {
    inner: Arc<RwLock<HashMap<String, Session>>>,
}

#[async_trait]
impl SessionStore for MemoryStore {
    async fn load_session(&self, cookie_value: String) -> Result<Option<Session>> {
        let id = Session::id_from_cookie_value(&cookie_value)?;
        tracing::debug!("loading session by id `{}`", id);
        Ok(self
            .inner
            .read()
            .await
            .get(&id)
            .cloned()
            .and_then(Session::validate))
    }

    async fn store_session(&self, session: Session) -> Result<Option<String>> {
        tracing::debug!("storing session by id `{}`", session.id());
        self.inner
            .write()
            .await
            .insert(session.id().to_string(), session.clone());

        session.reset_data_changed();
        Ok(session.into_cookie_value())
    }

    async fn destroy_session(&self, session: Session) -> Result {
        tracing::debug!("destroying session by id `{}`", session.id());
        self.inner.write().await.remove(session.id());
        Ok(())
    }

    async fn clear_store(&self) -> Result {
        tracing::debug!("clearing memory store");
        self.inner.write().await.clear();
        Ok(())
    }
}

impl MemoryStore {
    /// Create a new instance of MemoryStore
    pub fn new() -> Self {
        Self::default()
    }

    /// Performs session cleanup. This should be run on an
    /// intermittent basis if this store is run for long enough that
    /// memory accumulation is a concern
    pub async fn cleanup(&self) -> Result {
        tracing::debug!("cleaning up memory store...");
        let ids_to_delete: Vec<_> = self
            .inner
            .read()
            .await
            .values()
            .filter_map(|session| {
                if session.is_expired() {
                    Some(session.id().to_owned())
                } else {
                    None
                }
            })
            .collect();

        tracing::debug!("found {} expired sessions", ids_to_delete.len());
        for id in ids_to_delete {
            self.inner.write().await.remove(&id);
        }
        Ok(())
    }

    /// returns the number of elements in the memory store
    /// # Example
    /// ```rust
    /// # use saysion::{MemoryStore, Session, SessionStore};
    /// # #[tokio::main]
    /// # async fn main() -> saysion::Result {
    /// let mut store = MemoryStore::new();
    /// assert_eq!(store.count().await, 0);
    /// store.store_session(Session::new()).await?;
    /// assert_eq!(store.count().await, 1);
    /// # Ok(()) }
    /// ```
    pub async fn count(&self) -> usize {
        let data = self.inner.read().await;
        data.len()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::time::Duration;

    #[tokio::test]
    async fn creating_a_new_session_with_no_expiry() -> Result {
        let store = MemoryStore::new();
        let mut session = Session::new();
        session.insert("key", "Hello")?;
        let cloned = session.clone();
        let cookie_value = store.store_session(session).await?.unwrap();
        let loaded_session = store.load_session(cookie_value).await?.unwrap();
        assert_eq!(cloned.id(), loaded_session.id());
        assert_eq!("Hello", &loaded_session.get::<String>("key").unwrap());
        assert!(!loaded_session.is_expired());
        assert!(loaded_session.validate().is_some());
        Ok(())
    }

    #[tokio::test]
    async fn updating_a_session() -> Result {
        let store = MemoryStore::new();
        let mut session = Session::new();

        session.insert("key", "value")?;
        let cookie_value = store.store_session(session).await?.unwrap();

        let mut session = store.load_session(cookie_value.clone()).await?.unwrap();
        session.insert("key", "other value")?;

        assert_eq!(store.store_session(session).await?, None);
        let session = store.load_session(cookie_value).await?.unwrap();
        assert_eq!(&session.get::<String>("key").unwrap(), "other value");

        Ok(())
    }

    #[tokio::test]
    async fn updating_a_session_extending_expiry() -> Result {
        let store = MemoryStore::new();
        let mut session = Session::new();
        session.expire_in(Duration::from_secs(1));
        let original_expires = session.expiry().unwrap().clone();
        let cookie_value = store.store_session(session).await?.unwrap();

        let mut session = store.load_session(cookie_value.clone()).await?.unwrap();

        assert_eq!(session.expiry().unwrap(), &original_expires);
        session.expire_in(Duration::from_secs(3));
        let new_expires = session.expiry().unwrap().clone();
        assert_eq!(None, store.store_session(session).await?);

        let session = store.load_session(cookie_value.clone()).await?.unwrap();
        assert_eq!(session.expiry().unwrap(), &new_expires);

        tokio::time::sleep(Duration::from_secs(3)).await;
        assert_eq!(None, store.load_session(cookie_value).await?);

        Ok(())
    }

    #[tokio::test]
    async fn creating_a_new_session_with_expiry() -> Result {
        let store = MemoryStore::new();
        let mut session = Session::new();
        session.expire_in(Duration::from_secs(3));
        session.insert("key", "value")?;
        let cloned = session.clone();

        let cookie_value = store.store_session(session).await?.unwrap();

        let loaded_session = store.load_session(cookie_value.clone()).await?.unwrap();
        assert_eq!(cloned.id(), loaded_session.id());
        assert_eq!("value", &*loaded_session.get::<String>("key").unwrap());

        assert!(!loaded_session.is_expired());

        tokio::time::sleep(Duration::from_secs(3)).await;
        assert_eq!(None, store.load_session(cookie_value).await?);

        Ok(())
    }

    #[tokio::test]
    async fn destroying_a_single_session() -> Result {
        let store = MemoryStore::new();
        for _ in 0..3i8 {
            store.store_session(Session::new()).await?;
        }

        let cookie = store.store_session(Session::new()).await?.unwrap();
        assert_eq!(4, store.count().await);
        let session = store.load_session(cookie.clone()).await?.unwrap();
        store.destroy_session(session.clone()).await?;
        assert_eq!(None, store.load_session(cookie).await?);
        assert_eq!(3, store.count().await);

        // attempting to destroy the session again is not an error
        assert!(store.destroy_session(session).await.is_ok());
        Ok(())
    }

    #[tokio::test]
    async fn clearing_the_whole_store() -> Result {
        let store = MemoryStore::new();
        for _ in 0..3i8 {
            store.store_session(Session::new()).await?;
        }

        assert_eq!(3, store.count().await);
        store.clear_store().await.unwrap();
        assert_eq!(0, store.count().await);

        Ok(())
    }
}