greentic_session/
inmemory.rs

1use crate::model::{Cas, Session, SessionKey};
2use crate::store::SessionStore;
3use dashmap::DashMap;
4use greentic_types::GResult;
5use parking_lot::Mutex;
6use time::{Duration, OffsetDateTime};
7
8struct Entry {
9    session: Session,
10    cas: Cas,
11    expires_at: Option<OffsetDateTime>,
12}
13
14impl Entry {
15    fn new(session: Session, cas: Cas) -> Self {
16        let expires_at = session.expires_at();
17        Self {
18            session,
19            cas,
20            expires_at,
21        }
22    }
23
24    fn is_expired(&self, now: OffsetDateTime) -> bool {
25        match self.expires_at {
26            Some(exp) => now >= exp,
27            None => false,
28        }
29    }
30}
31
32/// In-memory implementation backed by a concurrent hash map.
33pub struct InMemorySessionStore {
34    entries: DashMap<SessionKey, Entry>,
35    cleanup_hint: Mutex<OffsetDateTime>,
36}
37
38impl Default for InMemorySessionStore {
39    fn default() -> Self {
40        Self {
41            entries: DashMap::new(),
42            cleanup_hint: Mutex::new(OffsetDateTime::now_utc()),
43        }
44    }
45}
46
47impl InMemorySessionStore {
48    /// Constructs a store with no background maintenance. Expiration is handled lazily on access.
49    pub fn new() -> Self {
50        Self::default()
51    }
52
53    fn now() -> OffsetDateTime {
54        OffsetDateTime::now_utc()
55    }
56
57    fn sanitize_for_write(session: &mut Session, now: OffsetDateTime) {
58        session.updated_at = now;
59        session.normalize();
60    }
61
62    fn maybe_cleanup(&self, now: OffsetDateTime) {
63        let mut guard = self.cleanup_hint.lock();
64        if now - *guard < Duration::seconds(60) {
65            return;
66        }
67
68        let stale_keys: Vec<_> = self
69            .entries
70            .iter()
71            .filter_map(|entry| {
72                if entry.value().is_expired(now) {
73                    Some(entry.key().clone())
74                } else {
75                    None
76                }
77            })
78            .collect();
79
80        for key in stale_keys {
81            self.entries.remove(&key);
82        }
83
84        *guard = now;
85    }
86}
87
88impl SessionStore for InMemorySessionStore {
89    fn get(&self, key: &SessionKey) -> GResult<Option<(Session, Cas)>> {
90        let now = Self::now();
91        self.maybe_cleanup(now);
92        if let Some(entry) = self.entries.get(key) {
93            if entry.is_expired(now) {
94                drop(entry);
95                self.entries.remove(key);
96                return Ok(None);
97            }
98            return Ok(Some((entry.session.clone(), entry.cas)));
99        }
100        Ok(None)
101    }
102
103    fn put(&self, mut session: Session) -> GResult<Cas> {
104        let key = session.key.clone();
105        let now = Self::now();
106        self.maybe_cleanup(now);
107        if let Some(existing) = self.entries.get(&key) {
108            if existing.is_expired(now) {
109                drop(existing);
110                self.entries.remove(&key);
111            }
112        }
113
114        Self::sanitize_for_write(&mut session, now);
115
116        let mut cas = Cas::initial();
117        match self.entries.entry(key) {
118            dashmap::mapref::entry::Entry::Occupied(mut occ) => {
119                cas = occ.get().cas.next();
120                occ.insert(Entry::new(session, cas));
121            }
122            dashmap::mapref::entry::Entry::Vacant(vac) => {
123                vac.insert(Entry::new(session, cas));
124            }
125        }
126        Ok(cas)
127    }
128
129    fn update_cas(&self, mut session: Session, expected: Cas) -> GResult<Result<Cas, Cas>> {
130        let key = session.key.clone();
131        let now = Self::now();
132        self.maybe_cleanup(now);
133        Self::sanitize_for_write(&mut session, now);
134
135        if let Some(mut guard) = self.entries.get_mut(&key) {
136            if guard.is_expired(now) {
137                drop(guard);
138                self.entries.remove(&key);
139                return Ok(Err(Cas::none()));
140            }
141
142            let current = guard.cas;
143            if current != expected {
144                return Ok(Err(current));
145            }
146
147            let next = current.next();
148            guard.cas = next;
149            guard.session = session;
150            guard.expires_at = guard.session.expires_at();
151            return Ok(Ok(next));
152        }
153        Ok(Err(Cas::none()))
154    }
155
156    fn delete(&self, key: &SessionKey) -> GResult<bool> {
157        Ok(self.entries.remove(key).is_some())
158    }
159
160    fn touch(&self, key: &SessionKey, ttl_secs: Option<u32>) -> GResult<bool> {
161        let now = Self::now();
162        self.maybe_cleanup(now);
163        if let Some(mut guard) = self.entries.get_mut(key) {
164            if guard.is_expired(now) {
165                drop(guard);
166                self.entries.remove(key);
167                return Ok(false);
168            }
169
170            if let Some(ttl) = ttl_secs {
171                guard.session.ttl_secs = ttl;
172            }
173            guard.session.updated_at = now;
174            guard.session.normalize();
175            guard.expires_at = guard.session.expires_at();
176            return Ok(true);
177        }
178        Ok(false)
179    }
180}