Skip to main content

alopex_server/
session.rs

1use std::sync::Arc;
2use std::time::{Duration, SystemTime};
3
4use alopex_cluster::{AuthenticatedSubject, TableLifecycleEffect};
5use alopex_core::async_runtime::{BoxFuture, BoxStream};
6use alopex_sql::catalog::TableMetadata;
7use alopex_sql::executor::{ExecutionResult, ExecutorError, Row};
8use alopex_sql::planner::PlannedStatement;
9use alopex_sql::storage::erased::ErasedAsyncSqlTransaction;
10use dashmap::DashMap;
11use futures::StreamExt;
12use tokio::sync::mpsc;
13use tokio_stream::wrappers::ReceiverStream;
14use uuid::Uuid;
15
16use crate::error::{Result, ServerError};
17
18/// Session identifier.
19#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Serialize, serde::Deserialize)]
20pub struct SessionId(Uuid);
21
22impl SessionId {
23    pub fn new() -> Self {
24        Self(Uuid::new_v4())
25    }
26}
27
28impl Default for SessionId {
29    fn default() -> Self {
30        Self::new()
31    }
32}
33
34impl std::fmt::Display for SessionId {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        write!(f, "{}", self.0)
37    }
38}
39
40impl std::str::FromStr for SessionId {
41    type Err = uuid::Error;
42
43    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
44        Ok(Self(Uuid::parse_str(s)?))
45    }
46}
47
48/// Session lifecycle state.
49#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
50pub enum SessionState {
51    Idle,
52    InTransaction,
53    Committing,
54    RollingBack,
55}
56
57/// Snapshot of a session for safe sharing.
58#[derive(Clone, Debug, serde::Serialize)]
59pub struct SessionSnapshot {
60    pub id: SessionId,
61    /// Authenticated subject bound when the session was created for remote work.
62    pub authenticated_subject: Option<AuthenticatedSubject>,
63    pub has_transaction: bool,
64    pub created_at: SystemTime,
65    pub last_active: SystemTime,
66    pub expires_at: SystemTime,
67    pub state: SessionState,
68}
69
70/// Transaction handle for a session.
71#[derive(Clone)]
72pub struct TxnHandle {
73    inner: Arc<TxnHandleInner>,
74}
75
76#[derive(Clone)]
77pub enum CatalogRollbackEffect {
78    DropTable { table_name: String },
79    CreateTable { table: Box<TableMetadata> },
80}
81
82struct TxnHandleInner {
83    txn: tokio::sync::Mutex<Option<Box<dyn ErasedAsyncSqlTransaction>>>,
84    pending_table_lifecycle_effects: tokio::sync::Mutex<Vec<TableLifecycleEffect>>,
85    pending_catalog_rollback_effects: tokio::sync::Mutex<Vec<CatalogRollbackEffect>>,
86    created_at: SystemTime,
87}
88
89impl TxnHandle {
90    pub fn new(txn: Box<dyn ErasedAsyncSqlTransaction>) -> Self {
91        Self {
92            inner: Arc::new(TxnHandleInner {
93                txn: tokio::sync::Mutex::new(Some(txn)),
94                pending_table_lifecycle_effects: tokio::sync::Mutex::new(Vec::new()),
95                pending_catalog_rollback_effects: tokio::sync::Mutex::new(Vec::new()),
96                created_at: SystemTime::now(),
97            }),
98        }
99    }
100
101    pub fn created_at(&self) -> SystemTime {
102        self.inner.created_at
103    }
104
105    pub fn execute<'a>(
106        &'a self,
107        sql: &'a str,
108    ) -> BoxFuture<'a, alopex_sql::executor::Result<ExecutionResult>> {
109        Box::pin(async move {
110            let mut guard = self.inner.txn.lock().await;
111            let txn = guard
112                .as_mut()
113                .ok_or_else(|| ExecutorError::InvalidOperation {
114                    operation: "execute".into(),
115                    reason: "transaction is closed".into(),
116                })?;
117            txn.execute(sql).await
118        })
119    }
120
121    pub fn execute_multi<'a>(
122        &'a self,
123        sql: &'a str,
124    ) -> BoxFuture<'a, alopex_sql::executor::Result<Vec<ExecutionResult>>> {
125        Box::pin(async move {
126            let mut guard = self.inner.txn.lock().await;
127            let txn = guard
128                .as_mut()
129                .ok_or_else(|| ExecutorError::InvalidOperation {
130                    operation: "execute_multi".into(),
131                    reason: "transaction is closed".into(),
132                })?;
133            txn.execute_multi(sql).await
134        })
135    }
136
137    pub fn query<'a>(&'a self, sql: &'a str) -> BoxStream<'a, alopex_sql::executor::Result<Row>> {
138        let (sender, receiver) = mpsc::channel(32);
139        let sql = sql.to_string();
140        let inner = Arc::clone(&self.inner);
141
142        tokio::spawn(async move {
143            let guard = inner.txn.lock().await;
144            let Some(txn) = guard.as_ref() else {
145                let _ = sender
146                    .send(Err(ExecutorError::InvalidOperation {
147                        operation: "query".into(),
148                        reason: "transaction is closed".into(),
149                    }))
150                    .await;
151                return;
152            };
153            let mut stream = txn.query(&sql);
154            while let Some(item) = stream.next().await {
155                if sender.send(item).await.is_err() {
156                    break;
157                }
158            }
159        });
160
161        Box::pin(ReceiverStream::new(receiver))
162    }
163
164    pub fn plan_for_routing<'a>(
165        &'a self,
166        sql: &'a str,
167    ) -> BoxFuture<'a, alopex_sql::executor::Result<Vec<PlannedStatement>>> {
168        Box::pin(async move {
169            let guard = self.inner.txn.lock().await;
170            let txn = guard
171                .as_ref()
172                .ok_or_else(|| ExecutorError::InvalidOperation {
173                    operation: "plan_for_routing".into(),
174                    reason: "transaction is closed".into(),
175                })?;
176            txn.plan_for_routing(sql).await
177        })
178    }
179
180    pub async fn buffer_table_lifecycle_effects(&self, effects: Vec<TableLifecycleEffect>) {
181        if effects.is_empty() {
182            return;
183        }
184        self.inner
185            .pending_table_lifecycle_effects
186            .lock()
187            .await
188            .extend(effects);
189    }
190
191    pub async fn buffer_catalog_rollback_effects(&self, effects: Vec<CatalogRollbackEffect>) {
192        if effects.is_empty() {
193            return;
194        }
195        self.inner
196            .pending_catalog_rollback_effects
197            .lock()
198            .await
199            .extend(effects);
200    }
201
202    pub async fn commit(self) -> alopex_sql::executor::Result<Vec<TableLifecycleEffect>> {
203        let mut guard = self.inner.txn.lock().await;
204        let txn = guard
205            .take()
206            .ok_or_else(|| ExecutorError::InvalidOperation {
207                operation: "commit".into(),
208                reason: "transaction is closed".into(),
209            })?;
210        txn.commit_boxed().await?;
211        let mut effects = self.inner.pending_table_lifecycle_effects.lock().await;
212        Ok(std::mem::take(&mut *effects))
213    }
214
215    pub async fn rollback(self) -> alopex_sql::executor::Result<Vec<CatalogRollbackEffect>> {
216        let mut guard = self.inner.txn.lock().await;
217        let txn = guard
218            .take()
219            .ok_or_else(|| ExecutorError::InvalidOperation {
220                operation: "rollback".into(),
221                reason: "transaction is closed".into(),
222            })?;
223        let result = txn.rollback_boxed().await;
224        self.inner
225            .pending_table_lifecycle_effects
226            .lock()
227            .await
228            .clear();
229        result?;
230        let mut effects = self.inner.pending_catalog_rollback_effects.lock().await;
231        Ok(std::mem::take(&mut *effects))
232    }
233}
234
235/// Session configuration.
236#[derive(Clone, Copy, Debug)]
237pub struct SessionConfig {
238    pub ttl: Duration,
239}
240
241/// Transaction factory for session manager.
242pub type TransactionFactory =
243    Arc<dyn Fn() -> BoxFuture<'static, Result<Box<dyn ErasedAsyncSqlTransaction>>> + Send + Sync>;
244
245/// Session manager for server.
246pub struct SessionManager {
247    sessions: DashMap<SessionId, Session>,
248    config: SessionConfig,
249    txn_factory: TransactionFactory,
250}
251
252struct Session {
253    id: SessionId,
254    authenticated_subject: Option<AuthenticatedSubject>,
255    txn_handle: Option<TxnHandle>,
256    created_at: SystemTime,
257    last_active: SystemTime,
258    expires_at: SystemTime,
259    state: SessionState,
260}
261
262impl SessionManager {
263    pub fn new(config: SessionConfig, txn_factory: TransactionFactory) -> Self {
264        Self {
265            sessions: DashMap::new(),
266            config,
267            txn_factory,
268        }
269    }
270
271    pub async fn create_session(&self) -> Result<SessionId> {
272        self.create_session_with_subject(None).await
273    }
274
275    /// Create a session whose authority is permanently bound to a validated
276    /// remote-read delegation subject.
277    pub async fn create_authenticated_session(
278        &self,
279        subject: AuthenticatedSubject,
280    ) -> Result<SessionId> {
281        self.create_session_with_subject(Some(subject)).await
282    }
283
284    async fn create_session_with_subject(
285        &self,
286        authenticated_subject: Option<AuthenticatedSubject>,
287    ) -> Result<SessionId> {
288        let now = SystemTime::now();
289        let id = SessionId::new();
290        let session = Session {
291            id: id.clone(),
292            authenticated_subject,
293            txn_handle: None,
294            created_at: now,
295            last_active: now,
296            expires_at: now + self.config.ttl,
297            state: SessionState::Idle,
298        };
299        self.sessions.insert(id.clone(), session);
300        Ok(id)
301    }
302
303    /// Return the subject bound to a remote-read session.
304    ///
305    /// Ordinary legacy sessions intentionally have no subject and cannot be
306    /// repurposed as a remote worker session.
307    pub async fn authenticated_subject(&self, id: &SessionId) -> Result<AuthenticatedSubject> {
308        let snapshot = self.get_session(id).await?;
309        snapshot.authenticated_subject.ok_or_else(|| {
310            ServerError::Unauthorized("remote read requires a subject-bound session".into())
311        })
312    }
313
314    pub async fn get_session(&self, id: &SessionId) -> Result<SessionSnapshot> {
315        let entry = self
316            .sessions
317            .get(id)
318            .ok_or_else(|| ServerError::NotFound("session not found".into()))?;
319        if entry.expires_at <= SystemTime::now() {
320            drop(entry);
321            self.sessions.remove(id);
322            return Err(ServerError::SessionExpired("session expired".into()));
323        }
324        Ok(SessionSnapshot {
325            id: entry.id.clone(),
326            authenticated_subject: entry.authenticated_subject.clone(),
327            has_transaction: entry.txn_handle.is_some(),
328            created_at: entry.created_at,
329            last_active: entry.last_active,
330            expires_at: entry.expires_at,
331            state: entry.state,
332        })
333    }
334
335    pub async fn begin_transaction(&self, id: &SessionId) -> Result<TxnHandle> {
336        let mut entry = self
337            .sessions
338            .get_mut(id)
339            .ok_or_else(|| ServerError::NotFound("session not found".into()))?;
340        if entry.expires_at <= SystemTime::now() {
341            drop(entry);
342            self.sessions.remove(id);
343            return Err(ServerError::SessionExpired("session expired".into()));
344        }
345        if entry.txn_handle.is_some() {
346            return Err(ServerError::Conflict("transaction already active".into()));
347        }
348        let txn = (self.txn_factory)().await?;
349        let handle = TxnHandle::new(txn);
350        entry.txn_handle = Some(handle.clone());
351        entry.last_active = SystemTime::now();
352        entry.state = SessionState::InTransaction;
353        Ok(handle)
354    }
355
356    pub async fn get_transaction(&self, id: &SessionId) -> Result<TxnHandle> {
357        let mut entry = self
358            .sessions
359            .get_mut(id)
360            .ok_or_else(|| ServerError::NotFound("session not found".into()))?;
361        if entry.expires_at <= SystemTime::now() {
362            drop(entry);
363            self.sessions.remove(id);
364            return Err(ServerError::SessionExpired("session expired".into()));
365        }
366        let handle = entry
367            .txn_handle
368            .clone()
369            .ok_or_else(|| ServerError::BadRequest("transaction not started".into()))?;
370        entry.last_active = SystemTime::now();
371        entry.state = SessionState::InTransaction;
372        Ok(handle)
373    }
374
375    pub async fn execute_in_session(&self, id: &SessionId, sql: &str) -> Result<ExecutionResult> {
376        let handle = {
377            let mut entry = self
378                .sessions
379                .get_mut(id)
380                .ok_or_else(|| ServerError::NotFound("session not found".into()))?;
381            if entry.expires_at <= SystemTime::now() {
382                drop(entry);
383                self.sessions.remove(id);
384                return Err(ServerError::SessionExpired("session expired".into()));
385            }
386            let handle = entry
387                .txn_handle
388                .clone()
389                .ok_or_else(|| ServerError::BadRequest("transaction not started".into()))?;
390            entry.last_active = SystemTime::now();
391            handle
392        };
393
394        handle
395            .execute(sql)
396            .await
397            .map_err(|err| ServerError::Sql(err.into()))
398    }
399
400    pub async fn commit(&self, id: &SessionId) -> Result<Vec<TableLifecycleEffect>> {
401        let handle = self.take_handle(id, SessionState::Committing)?;
402        let effects = handle
403            .commit()
404            .await
405            .map_err(|err| ServerError::Sql(err.into()))?;
406        Ok(effects)
407    }
408
409    pub async fn rollback(&self, id: &SessionId) -> Result<Vec<CatalogRollbackEffect>> {
410        let handle = self.take_handle(id, SessionState::RollingBack)?;
411        let effects = handle
412            .rollback()
413            .await
414            .map_err(|err| ServerError::Sql(err.into()))?;
415        Ok(effects)
416    }
417
418    pub fn cleanup_expired(&self) {
419        let now = SystemTime::now();
420        let expired: Vec<SessionId> = self
421            .sessions
422            .iter()
423            .filter(|entry| entry.expires_at <= now)
424            .map(|entry| entry.id.clone())
425            .collect();
426        for id in expired {
427            self.sessions.remove(&id);
428        }
429    }
430
431    #[cfg(test)]
432    pub(crate) fn active_session_count(&self) -> usize {
433        self.sessions.len()
434    }
435
436    fn take_handle(&self, id: &SessionId, state: SessionState) -> Result<TxnHandle> {
437        let mut entry = self
438            .sessions
439            .get_mut(id)
440            .ok_or_else(|| ServerError::NotFound("session not found".into()))?;
441        if entry.expires_at <= SystemTime::now() {
442            drop(entry);
443            self.sessions.remove(id);
444            return Err(ServerError::SessionExpired("session expired".into()));
445        }
446        let handle = entry
447            .txn_handle
448            .take()
449            .ok_or_else(|| ServerError::BadRequest("transaction not started".into()))?;
450        entry.state = state;
451        entry.last_active = SystemTime::now();
452        Ok(handle)
453    }
454}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459
460    fn manager() -> SessionManager {
461        SessionManager::new(
462            SessionConfig {
463                ttl: Duration::from_secs(60),
464            },
465            Arc::new(|| {
466                Box::pin(async {
467                    Err(ServerError::Internal(
468                        "test factory must not create a transaction".into(),
469                    ))
470                })
471            }),
472        )
473    }
474
475    #[tokio::test]
476    async fn only_authenticated_sessions_expose_a_remote_read_subject() {
477        let manager = manager();
478        let anonymous = manager.create_session().await.unwrap();
479        assert!(matches!(
480            manager.authenticated_subject(&anonymous).await,
481            Err(ServerError::Unauthorized(_))
482        ));
483
484        let subject = AuthenticatedSubject::new("user-a");
485        let bound = manager
486            .create_authenticated_session(subject.clone())
487            .await
488            .unwrap();
489        assert_eq!(
490            manager.authenticated_subject(&bound).await.unwrap(),
491            subject
492        );
493    }
494}