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#[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#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
50pub enum SessionState {
51 Idle,
52 InTransaction,
53 Committing,
54 RollingBack,
55}
56
57#[derive(Clone, Debug, serde::Serialize)]
59pub struct SessionSnapshot {
60 pub id: SessionId,
61 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#[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#[derive(Clone, Copy, Debug)]
237pub struct SessionConfig {
238 pub ttl: Duration,
239}
240
241pub type TransactionFactory =
243 Arc<dyn Fn() -> BoxFuture<'static, Result<Box<dyn ErasedAsyncSqlTransaction>>> + Send + Sync>;
244
245pub 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 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 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}