gsm_session/
lib.rs

1use anyhow::{Context, Result};
2pub use greentic_session::{SessionData, SessionKey};
3use greentic_session::{
4    SessionStore, inmemory::InMemorySessionStore, redis_store::RedisSessionStore,
5};
6use greentic_types::{TenantCtx, UserId};
7use redis::Client;
8use std::sync::Arc;
9use tokio::task;
10
11const DEFAULT_NAMESPACE: &str = "greentic:session";
12const SESSION_NAMESPACE_ENV: &str = "SESSION_NAMESPACE";
13const SESSION_REDIS_URL_ENV: &str = "SESSION_REDIS_URL";
14
15/// Shared session store handle that wraps the greentic-session backends.
16#[derive(Clone)]
17pub struct SharedSessionStore {
18    inner: Arc<SessionBackend>,
19}
20
21enum SessionBackend {
22    InMemory(Arc<InMemorySessionStore>),
23    Redis(Arc<RedisSessionStore>),
24}
25
26/// Builds a session store from environment configuration.
27pub async fn store_from_env() -> Result<SharedSessionStore> {
28    match std::env::var(SESSION_REDIS_URL_ENV) {
29        Ok(url) => {
30            let namespace =
31                std::env::var(SESSION_NAMESPACE_ENV).unwrap_or_else(|_| DEFAULT_NAMESPACE.into());
32            build_redis_store(&url, &namespace)
33        }
34        Err(_) => Ok(shared_memory_store()),
35    }
36}
37
38/// Returns an in-memory session store.
39pub fn shared_memory_store() -> SharedSessionStore {
40    SharedSessionStore {
41        inner: Arc::new(SessionBackend::InMemory(Arc::new(
42            InMemorySessionStore::new(),
43        ))),
44    }
45}
46
47fn build_redis_store(url: &str, namespace: &str) -> Result<SharedSessionStore> {
48    let client = Client::open(url).context("invalid SESSION_REDIS_URL")?;
49    let store = RedisSessionStore::with_namespace(client, namespace);
50    Ok(SharedSessionStore {
51        inner: Arc::new(SessionBackend::Redis(Arc::new(store))),
52    })
53}
54
55impl SharedSessionStore {
56    /// Looks up the active session bound to the provided tenant + user combination.
57    pub async fn find_by_user(
58        &self,
59        ctx: &TenantCtx,
60        user: &UserId,
61    ) -> Result<Option<(SessionKey, SessionData)>> {
62        match self.inner.as_ref() {
63            SessionBackend::InMemory(store) => store.find_by_user(ctx, user).map_err(Into::into),
64            SessionBackend::Redis(store) => {
65                let store = Arc::clone(store);
66                let ctx = ctx.clone();
67                let user = user.clone();
68                blocking_call(move || store.find_by_user(&ctx, &user)).await
69            }
70        }
71    }
72
73    /// Creates a new session and returns its key.
74    pub async fn create_session(&self, ctx: &TenantCtx, data: SessionData) -> Result<SessionKey> {
75        match self.inner.as_ref() {
76            SessionBackend::InMemory(store) => store.create_session(ctx, data).map_err(Into::into),
77            SessionBackend::Redis(store) => {
78                let store = Arc::clone(store);
79                let ctx = ctx.clone();
80                blocking_call(move || store.create_session(&ctx, data)).await
81            }
82        }
83    }
84
85    /// Updates an existing session with the supplied snapshot.
86    pub async fn update_session(&self, key: &SessionKey, data: SessionData) -> Result<()> {
87        match self.inner.as_ref() {
88            SessionBackend::InMemory(store) => store.update_session(key, data).map_err(Into::into),
89            SessionBackend::Redis(store) => {
90                let store = Arc::clone(store);
91                let key = key.clone();
92                blocking_call(move || store.update_session(&key, data)).await
93            }
94        }
95    }
96}
97
98async fn blocking_call<T, F>(f: F) -> Result<T>
99where
100    T: Send + 'static,
101    F: FnOnce() -> greentic_types::GResult<T> + Send + 'static,
102{
103    task::spawn_blocking(move || f().map_err(anyhow::Error::from))
104        .await
105        .context("session store operation failed")?
106}