1use std::sync::Arc;
2use std::time::{Duration, SystemTime};
3
4use alopex_cluster::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 has_transaction: bool,
62 pub created_at: SystemTime,
63 pub last_active: SystemTime,
64 pub expires_at: SystemTime,
65 pub state: SessionState,
66}
67
68#[derive(Clone)]
70pub struct TxnHandle {
71 inner: Arc<TxnHandleInner>,
72}
73
74#[derive(Clone)]
75pub enum CatalogRollbackEffect {
76 DropTable { table_name: String },
77 CreateTable { table: Box<TableMetadata> },
78}
79
80struct TxnHandleInner {
81 txn: tokio::sync::Mutex<Option<Box<dyn ErasedAsyncSqlTransaction>>>,
82 pending_table_lifecycle_effects: tokio::sync::Mutex<Vec<TableLifecycleEffect>>,
83 pending_catalog_rollback_effects: tokio::sync::Mutex<Vec<CatalogRollbackEffect>>,
84 created_at: SystemTime,
85}
86
87impl TxnHandle {
88 pub fn new(txn: Box<dyn ErasedAsyncSqlTransaction>) -> Self {
89 Self {
90 inner: Arc::new(TxnHandleInner {
91 txn: tokio::sync::Mutex::new(Some(txn)),
92 pending_table_lifecycle_effects: tokio::sync::Mutex::new(Vec::new()),
93 pending_catalog_rollback_effects: tokio::sync::Mutex::new(Vec::new()),
94 created_at: SystemTime::now(),
95 }),
96 }
97 }
98
99 pub fn created_at(&self) -> SystemTime {
100 self.inner.created_at
101 }
102
103 pub fn execute<'a>(
104 &'a self,
105 sql: &'a str,
106 ) -> BoxFuture<'a, alopex_sql::executor::Result<ExecutionResult>> {
107 Box::pin(async move {
108 let mut guard = self.inner.txn.lock().await;
109 let txn = guard
110 .as_mut()
111 .ok_or_else(|| ExecutorError::InvalidOperation {
112 operation: "execute".into(),
113 reason: "transaction is closed".into(),
114 })?;
115 txn.execute(sql).await
116 })
117 }
118
119 pub fn query<'a>(&'a self, sql: &'a str) -> BoxStream<'a, alopex_sql::executor::Result<Row>> {
120 let (sender, receiver) = mpsc::channel(32);
121 let sql = sql.to_string();
122 let inner = Arc::clone(&self.inner);
123
124 tokio::spawn(async move {
125 let guard = inner.txn.lock().await;
126 let Some(txn) = guard.as_ref() else {
127 let _ = sender
128 .send(Err(ExecutorError::InvalidOperation {
129 operation: "query".into(),
130 reason: "transaction is closed".into(),
131 }))
132 .await;
133 return;
134 };
135 let mut stream = txn.query(&sql);
136 while let Some(item) = stream.next().await {
137 if sender.send(item).await.is_err() {
138 break;
139 }
140 }
141 });
142
143 Box::pin(ReceiverStream::new(receiver))
144 }
145
146 pub fn plan_for_routing<'a>(
147 &'a self,
148 sql: &'a str,
149 ) -> BoxFuture<'a, alopex_sql::executor::Result<Vec<PlannedStatement>>> {
150 Box::pin(async move {
151 let guard = self.inner.txn.lock().await;
152 let txn = guard
153 .as_ref()
154 .ok_or_else(|| ExecutorError::InvalidOperation {
155 operation: "plan_for_routing".into(),
156 reason: "transaction is closed".into(),
157 })?;
158 txn.plan_for_routing(sql).await
159 })
160 }
161
162 pub async fn buffer_table_lifecycle_effects(&self, effects: Vec<TableLifecycleEffect>) {
163 if effects.is_empty() {
164 return;
165 }
166 self.inner
167 .pending_table_lifecycle_effects
168 .lock()
169 .await
170 .extend(effects);
171 }
172
173 pub async fn buffer_catalog_rollback_effects(&self, effects: Vec<CatalogRollbackEffect>) {
174 if effects.is_empty() {
175 return;
176 }
177 self.inner
178 .pending_catalog_rollback_effects
179 .lock()
180 .await
181 .extend(effects);
182 }
183
184 pub async fn commit(self) -> alopex_sql::executor::Result<Vec<TableLifecycleEffect>> {
185 let mut guard = self.inner.txn.lock().await;
186 let txn = guard
187 .take()
188 .ok_or_else(|| ExecutorError::InvalidOperation {
189 operation: "commit".into(),
190 reason: "transaction is closed".into(),
191 })?;
192 txn.commit_boxed().await?;
193 let mut effects = self.inner.pending_table_lifecycle_effects.lock().await;
194 Ok(std::mem::take(&mut *effects))
195 }
196
197 pub async fn rollback(self) -> alopex_sql::executor::Result<Vec<CatalogRollbackEffect>> {
198 let mut guard = self.inner.txn.lock().await;
199 let txn = guard
200 .take()
201 .ok_or_else(|| ExecutorError::InvalidOperation {
202 operation: "rollback".into(),
203 reason: "transaction is closed".into(),
204 })?;
205 let result = txn.rollback_boxed().await;
206 self.inner
207 .pending_table_lifecycle_effects
208 .lock()
209 .await
210 .clear();
211 result?;
212 let mut effects = self.inner.pending_catalog_rollback_effects.lock().await;
213 Ok(std::mem::take(&mut *effects))
214 }
215}
216
217#[derive(Clone, Copy, Debug)]
219pub struct SessionConfig {
220 pub ttl: Duration,
221}
222
223pub type TransactionFactory =
225 Arc<dyn Fn() -> BoxFuture<'static, Result<Box<dyn ErasedAsyncSqlTransaction>>> + Send + Sync>;
226
227pub struct SessionManager {
229 sessions: DashMap<SessionId, Session>,
230 config: SessionConfig,
231 txn_factory: TransactionFactory,
232}
233
234struct Session {
235 id: SessionId,
236 txn_handle: Option<TxnHandle>,
237 created_at: SystemTime,
238 last_active: SystemTime,
239 expires_at: SystemTime,
240 state: SessionState,
241}
242
243impl SessionManager {
244 pub fn new(config: SessionConfig, txn_factory: TransactionFactory) -> Self {
245 Self {
246 sessions: DashMap::new(),
247 config,
248 txn_factory,
249 }
250 }
251
252 pub async fn create_session(&self) -> Result<SessionId> {
253 let now = SystemTime::now();
254 let id = SessionId::new();
255 let session = Session {
256 id: id.clone(),
257 txn_handle: None,
258 created_at: now,
259 last_active: now,
260 expires_at: now + self.config.ttl,
261 state: SessionState::Idle,
262 };
263 self.sessions.insert(id.clone(), session);
264 Ok(id)
265 }
266
267 pub async fn get_session(&self, id: &SessionId) -> Result<SessionSnapshot> {
268 let entry = self
269 .sessions
270 .get(id)
271 .ok_or_else(|| ServerError::NotFound("session not found".into()))?;
272 if entry.expires_at <= SystemTime::now() {
273 drop(entry);
274 self.sessions.remove(id);
275 return Err(ServerError::SessionExpired("session expired".into()));
276 }
277 Ok(SessionSnapshot {
278 id: entry.id.clone(),
279 has_transaction: entry.txn_handle.is_some(),
280 created_at: entry.created_at,
281 last_active: entry.last_active,
282 expires_at: entry.expires_at,
283 state: entry.state,
284 })
285 }
286
287 pub async fn begin_transaction(&self, id: &SessionId) -> Result<TxnHandle> {
288 let mut entry = self
289 .sessions
290 .get_mut(id)
291 .ok_or_else(|| ServerError::NotFound("session not found".into()))?;
292 if entry.expires_at <= SystemTime::now() {
293 drop(entry);
294 self.sessions.remove(id);
295 return Err(ServerError::SessionExpired("session expired".into()));
296 }
297 if entry.txn_handle.is_some() {
298 return Err(ServerError::Conflict("transaction already active".into()));
299 }
300 let txn = (self.txn_factory)().await?;
301 let handle = TxnHandle::new(txn);
302 entry.txn_handle = Some(handle.clone());
303 entry.last_active = SystemTime::now();
304 entry.state = SessionState::InTransaction;
305 Ok(handle)
306 }
307
308 pub async fn get_transaction(&self, id: &SessionId) -> Result<TxnHandle> {
309 let mut entry = self
310 .sessions
311 .get_mut(id)
312 .ok_or_else(|| ServerError::NotFound("session not found".into()))?;
313 if entry.expires_at <= SystemTime::now() {
314 drop(entry);
315 self.sessions.remove(id);
316 return Err(ServerError::SessionExpired("session expired".into()));
317 }
318 let handle = entry
319 .txn_handle
320 .clone()
321 .ok_or_else(|| ServerError::BadRequest("transaction not started".into()))?;
322 entry.last_active = SystemTime::now();
323 entry.state = SessionState::InTransaction;
324 Ok(handle)
325 }
326
327 pub async fn execute_in_session(&self, id: &SessionId, sql: &str) -> Result<ExecutionResult> {
328 let handle = {
329 let mut entry = self
330 .sessions
331 .get_mut(id)
332 .ok_or_else(|| ServerError::NotFound("session not found".into()))?;
333 if entry.expires_at <= SystemTime::now() {
334 drop(entry);
335 self.sessions.remove(id);
336 return Err(ServerError::SessionExpired("session expired".into()));
337 }
338 let handle = entry
339 .txn_handle
340 .clone()
341 .ok_or_else(|| ServerError::BadRequest("transaction not started".into()))?;
342 entry.last_active = SystemTime::now();
343 handle
344 };
345
346 handle
347 .execute(sql)
348 .await
349 .map_err(|err| ServerError::Sql(err.into()))
350 }
351
352 pub async fn commit(&self, id: &SessionId) -> Result<Vec<TableLifecycleEffect>> {
353 let handle = self.take_handle(id, SessionState::Committing)?;
354 let effects = handle
355 .commit()
356 .await
357 .map_err(|err| ServerError::Sql(err.into()))?;
358 Ok(effects)
359 }
360
361 pub async fn rollback(&self, id: &SessionId) -> Result<Vec<CatalogRollbackEffect>> {
362 let handle = self.take_handle(id, SessionState::RollingBack)?;
363 let effects = handle
364 .rollback()
365 .await
366 .map_err(|err| ServerError::Sql(err.into()))?;
367 Ok(effects)
368 }
369
370 pub fn cleanup_expired(&self) {
371 let now = SystemTime::now();
372 let expired: Vec<SessionId> = self
373 .sessions
374 .iter()
375 .filter(|entry| entry.expires_at <= now)
376 .map(|entry| entry.id.clone())
377 .collect();
378 for id in expired {
379 self.sessions.remove(&id);
380 }
381 }
382
383 fn take_handle(&self, id: &SessionId, state: SessionState) -> Result<TxnHandle> {
384 let mut entry = self
385 .sessions
386 .get_mut(id)
387 .ok_or_else(|| ServerError::NotFound("session not found".into()))?;
388 if entry.expires_at <= SystemTime::now() {
389 drop(entry);
390 self.sessions.remove(id);
391 return Err(ServerError::SessionExpired("session expired".into()));
392 }
393 let handle = entry
394 .txn_handle
395 .take()
396 .ok_or_else(|| ServerError::BadRequest("transaction not started".into()))?;
397 entry.state = state;
398 entry.last_active = SystemTime::now();
399 Ok(handle)
400 }
401}