Skip to main content

siderite_orm/db/
transaction.rs

1//! Scope ownership and cancellation-safe transaction orchestration.
2
3use super::{Db, Target};
4use crate::backend::{Backend, Executor, Transaction};
5use crate::capabilities::{BackendKind, Feature};
6use crate::capabilities::{IsolationLevel, TransactionSupport};
7use crate::error::{BackendCapabilityError, OrmError, QueryError};
8use std::future::Future;
9use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
10
11fn lock<T>(cell: &Mutex<T>) -> MutexGuard<'_, T> {
12    cell.lock().unwrap_or_else(PoisonError::into_inner)
13}
14
15#[cfg(test)]
16mod tests;
17
18type Hook = Box<dyn FnOnce() + Send>;
19
20#[derive(Default)]
21pub(super) struct Hooks(Mutex<Vec<Hook>>);
22
23impl Hooks {
24    fn push(&self, hook: Hook) {
25        lock(&self.0).push(hook);
26    }
27
28    fn take(&self) -> Vec<Hook> {
29        std::mem::take(&mut *lock(&self.0))
30    }
31}
32
33struct Lifecycle {
34    handle: Option<Arc<dyn Transaction>>,
35    active: u64,
36    next: u64,
37    running: bool,
38    aborted: bool,
39}
40
41pub(super) struct Control(Mutex<Lifecycle>);
42
43impl Control {
44    fn new(handle: Arc<dyn Transaction>) -> Arc<Self> {
45        Arc::new(Self(Mutex::new(Lifecycle {
46            handle: Some(handle),
47            active: 0,
48            next: 0,
49            running: false,
50            aborted: false,
51        })))
52    }
53
54    fn check(state: &Lifecycle, scope: u64) -> Result<(), OrmError> {
55        if state.aborted {
56            return Err(QueryError::TransactionAborted.into());
57        }
58        if state.handle.is_none() {
59            return Err(QueryError::TransactionClosed.into());
60        }
61        if state.active != scope || state.running {
62            return Err(QueryError::TransactionBusy.into());
63        }
64        Ok(())
65    }
66
67    fn ready(&self, scope: u64) -> Result<(), OrmError> {
68        Self::check(&lock(&self.0), scope)
69    }
70
71    fn child(self: &Arc<Self>, parent: u64) -> Result<Scope, OrmError> {
72        let mut state = lock(&self.0);
73        Self::check(&state, parent)?;
74        let id = state
75            .next
76            .checked_add(1)
77            .ok_or_else(|| QueryError::InvalidPlan("scope overflow".into()))?;
78        state.next = id;
79        state.active = id;
80        Ok(Scope {
81            control: Arc::clone(self),
82            id,
83            parent,
84            armed: true,
85            hooks: Arc::default(),
86        })
87    }
88
89    pub(super) fn lease(self: &Arc<Self>, id: u64) -> Result<Lease, OrmError> {
90        let mut state = lock(&self.0);
91        Self::check(&state, id)?;
92        let handle = state
93            .handle
94            .as_ref()
95            .ok_or(QueryError::TransactionClosed)?
96            .clone();
97        state.running = true;
98        Ok(Lease {
99            control: Arc::clone(self),
100            handle,
101            settled: false,
102        })
103    }
104
105    fn close(&self, aborted: bool) {
106        // No backend code or destructor runs while the lifecycle lock is held.
107        let handle = {
108            let mut state = lock(&self.0);
109            state.aborted |= aborted;
110            state.handle.take()
111        };
112        drop(handle);
113    }
114
115    fn end_root(&self) -> Result<(), OrmError> {
116        let (ready, handle) = {
117            let mut state = lock(&self.0);
118            let ready = Self::check(&state, 0);
119            state.aborted |= ready.is_err();
120            (ready, state.handle.take())
121        };
122        drop(handle);
123        ready
124    }
125
126    fn register(&self, scope: u64, hooks: &Hooks, hook: Hook) {
127        let state = lock(&self.0);
128        if !state.aborted && state.handle.is_some() && state.active == scope {
129            hooks.push(hook);
130        }
131    }
132}
133
134/// An executor lease prevents statements overlapping savepoint boundaries.
135pub(super) struct Lease {
136    control: Arc<Control>,
137    handle: Arc<dyn Transaction>,
138    settled: bool,
139}
140
141impl Lease {
142    async fn execute_script(&mut self, sql: &str) -> Result<(), OrmError> {
143        let result = self.handle.execute_script(sql).await;
144        self.settled = true;
145        result
146    }
147}
148
149impl Drop for Lease {
150    fn drop(&mut self) {
151        let handle = {
152            let mut state = self
153                .control
154                .0
155                .lock()
156                .unwrap_or_else(PoisonError::into_inner);
157            state.running = false;
158            if self.settled {
159                None
160            } else {
161                state.aborted = true;
162                state.handle.take()
163            }
164        };
165        drop(handle);
166    }
167}
168
169pub(super) enum ExecutorRef<'a> {
170    Pool(&'a dyn Executor),
171    Leased(Lease),
172}
173
174impl ExecutorRef<'_> {
175    pub(super) fn finish(&mut self) {
176        if let Self::Leased(lease) = self {
177            lease.settled = true;
178        }
179    }
180
181    pub(super) fn as_ref(&self) -> &dyn Executor {
182        match self {
183            Self::Pool(pool) => *pool,
184            Self::Leased(lease) => lease.handle.as_ref(),
185        }
186    }
187}
188
189pub(super) struct TxState {
190    pub(super) control: Arc<Control>,
191    pub(super) pool: Arc<dyn Backend>,
192    pub(super) hooks: Arc<Hooks>,
193    pub(super) scope: u64,
194}
195
196pub(super) struct ConnState {
197    pub(super) control: Arc<Control>,
198    pub(super) pool: Arc<dyn Backend>,
199}
200
201/// Armed before any scope SQL; abandoning it makes the whole handle unusable.
202struct Scope {
203    control: Arc<Control>,
204    id: u64,
205    parent: u64,
206    armed: bool,
207    hooks: Arc<Hooks>,
208}
209
210impl Scope {
211    fn root(control: &Arc<Control>) -> Self {
212        Self {
213            control: Arc::clone(control),
214            id: 0,
215            parent: 0,
216            armed: true,
217            hooks: Arc::default(),
218        }
219    }
220
221    fn release(
222        &mut self,
223        hooks: &Hooks,
224        parent: Option<&Hooks>,
225        keep: bool,
226    ) -> Result<(), OrmError> {
227        let mut state = self
228            .control
229            .0
230            .lock()
231            .unwrap_or_else(PoisonError::into_inner);
232        Control::check(&state, self.id)?;
233        if keep && let Some(parent) = parent {
234            for hook in hooks.take() {
235                parent.push(hook);
236            }
237        }
238        state.active = self.parent;
239        self.armed = false;
240        Ok(())
241    }
242}
243
244impl Drop for Scope {
245    fn drop(&mut self) {
246        if self.armed {
247            self.control.close(true);
248        }
249        // Break cycles when a callback captured its own scoped Db handle.
250        drop(self.hooks.take());
251    }
252}
253
254async fn rollback(handle: &dyn Transaction) {
255    if let Err(error) = handle.rollback().await {
256        tracing::warn!(%error, "transaction rollback failed");
257    }
258}
259
260impl Db {
261    /// Run `f` in a transaction, or a savepoint inside a transaction.
262    ///
263    /// Cancelling a closure, child scope or statement makes the transaction
264    /// unusable. An outer success then fails and rolls back. Concurrent
265    /// statements and sibling scopes are rejected; recursive nesting through
266    /// the child is supported.
267    /// Use the child handle while its scope is active. Escaped handles cannot
268    /// execute after their scope finishes. Cancellation during COMMIT can
269    /// leave the server outcome unknown; callers must not retry blindly.
270    ///
271    /// Args:
272    ///     f: Closure receiving the scoped transaction handle.
273    ///
274    /// Returns:
275    ///     The closure result after commit or confirmed savepoint release.
276    ///
277    /// # Errors
278    /// Closure, backend, capability, busy-scope or aborted-transaction errors.
279    pub async fn transaction<F, Fut, T, E>(&self, f: F) -> Result<T, E>
280    where
281        F: FnOnce(Db) -> Fut,
282        Fut: Future<Output = Result<T, E>>,
283        E: From<OrmError>,
284    {
285        self.run_transaction(None, f).await
286    }
287
288    /// Run a scoped transaction at an explicit isolation level.
289    ///
290    /// Args:
291    ///     isolation: A level supported by the backend; disallowed in nesting.
292    ///     f: Closure receiving the scoped transaction handle.
293    ///
294    /// Returns:
295    ///     The closure result after successful commit.
296    ///
297    /// # Errors
298    /// As [`transaction`](Self::transaction), plus unsupported isolation.
299    pub async fn transaction_with<F, Fut, T, E>(
300        &self,
301        isolation: IsolationLevel,
302        f: F,
303    ) -> Result<T, E>
304    where
305        F: FnOnce(Db) -> Fut,
306        Fut: Future<Output = Result<T, E>>,
307        E: From<OrmError>,
308    {
309        self.run_transaction(Some(isolation), f).await
310    }
311
312    /// Run `f` on a dedicated schema connection, optionally transactional.
313    ///
314    /// SQLite disables foreign keys before BEGIN and restores them afterwards.
315    /// A cancelled nested scope makes the connection unusable; it cannot be
316    /// committed through an escaped handle.
317    ///
318    /// Args:
319    ///     transactional: Whether to wrap schema work in a transaction.
320    ///     f: Closure receiving the dedicated connection handle.
321    ///
322    /// Returns:
323    ///     The closure result after the backend finishes schema work.
324    ///
325    /// # Errors
326    /// Closure/backend errors; SQLite rejects an already-scoped connection.
327    pub async fn schema_change<F, Fut, T, E>(
328        &self,
329        // SQLite prepares this connection before opening a transaction.
330        transactional: bool,
331        f: F,
332    ) -> Result<T, E>
333    where
334        F: FnOnce(Db) -> Fut,
335        Fut: Future<Output = Result<T, E>>,
336        E: From<OrmError>,
337    {
338        match &self.target {
339            Target::Pool(pool) => {
340                let handle: Arc<dyn Transaction> =
341                    Arc::from(pool.begin_schema(transactional).await?);
342                self.root_scope(pool, handle, transactional, f).await
343            }
344            _ if self.capabilities().kind == BackendKind::Sqlite => {
345                let reason = "SQLite schema requires a pool handle";
346                let error = QueryError::InvalidPlan(reason.into());
347                Err(OrmError::from(error).into())
348            }
349            _ => self.run_transaction(None, f).await,
350        }
351    }
352
353    async fn root_scope<F, Fut, T, E>(
354        &self,
355        pool: &Arc<dyn Backend>,
356        handle: Arc<dyn Transaction>,
357        transactional: bool,
358        f: F,
359    ) -> Result<T, E>
360    where
361        F: FnOnce(Db) -> Fut,
362        Fut: Future<Output = Result<T, E>>,
363        E: From<OrmError>,
364    {
365        let control = Control::new(Arc::clone(&handle));
366        let mut guard = Scope::root(&control);
367        let hooks = Arc::clone(&guard.hooks);
368        let target = if transactional {
369            Target::Tx(Arc::new(TxState {
370                control: Arc::clone(&control),
371                pool: Arc::clone(pool),
372                hooks: Arc::clone(&hooks),
373                scope: 0,
374            }))
375        } else {
376            Target::Conn(Arc::new(ConnState {
377                control: Arc::clone(&control),
378                pool: Arc::clone(pool),
379            }))
380        };
381        let result = f(Db {
382            target,
383            signals: self.signals.clone(),
384        })
385        .await;
386        let ready = control.end_root();
387        match result {
388            Ok(value) => {
389                if let Err(error) = ready {
390                    rollback(handle.as_ref()).await;
391                    return Err(error.into());
392                }
393                handle.commit().await?;
394                guard.armed = false;
395                hooks.take().into_iter().for_each(|hook| hook());
396                Ok(value)
397            }
398            Err(error) => {
399                rollback(handle.as_ref()).await;
400                guard.armed = false;
401                Err(error)
402            }
403        }
404    }
405
406    async fn run_transaction<F, Fut, T, E>(
407        &self,
408        isolation: Option<IsolationLevel>,
409        f: F,
410    ) -> Result<T, E>
411    where
412        F: FnOnce(Db) -> Fut,
413        Fut: Future<Output = Result<T, E>>,
414        E: From<OrmError>,
415    {
416        if let Target::Pool(pool) = &self.target {
417            if let Some(level) = isolation {
418                pool.capabilities()
419                    .require(Feature::Isolation(level))
420                    .map_err(OrmError::from)?;
421            }
422            let handle = Arc::from(pool.begin(isolation).await?);
423            return self.root_scope(pool, handle, true, f).await;
424        }
425        if isolation.is_some() {
426            return Err(OrmError::from(QueryError::InvalidPlan(
427                "isolation level cannot be set on a nested transaction".into(),
428            ))
429            .into());
430        }
431        let (control, pool, parent_scope, parent_hooks) = match &self.target {
432            Target::Tx(parent) => {
433                let caps = parent.pool.capabilities();
434                if caps.transactions < TransactionSupport::Savepoints {
435                    let error = BackendCapabilityError::Unsupported {
436                        backend: caps.kind,
437                        feature: Feature::Savepoints,
438                    };
439                    return Err(OrmError::from(error).into());
440                }
441                (
442                    &parent.control,
443                    &parent.pool,
444                    parent.scope,
445                    Some(&parent.hooks),
446                )
447            }
448            Target::Conn(parent) => (&parent.control, &parent.pool, 0, None),
449            Target::Pool(_) => unreachable!("pool handled above"),
450        };
451        let mut guard = control.child(parent_scope)?;
452        let name = format!("siderite_sp_{}", guard.id);
453        let begin = if parent_hooks.is_some() {
454            format!("SAVEPOINT {name}")
455        } else {
456            "BEGIN".into()
457        };
458        control.lease(guard.id)?.execute_script(&begin).await?;
459        let hooks = Arc::clone(&guard.hooks);
460        let child = Db {
461            target: Target::Tx(Arc::new(TxState {
462                control: Arc::clone(control),
463                pool: Arc::clone(pool),
464                hooks: Arc::clone(&hooks),
465                scope: guard.id,
466            })),
467            signals: self.signals.clone(),
468        };
469        let result = f(child).await;
470        if let Err(error) = control.ready(guard.id) {
471            tracing::warn!(%error, "transaction scope became unusable");
472            return match result {
473                Err(original) => Err(original),
474                Ok(_) => Err(error.into()),
475            };
476        }
477        let end = match (&result, parent_hooks) {
478            (Ok(_), Some(_)) => format!("RELEASE SAVEPOINT {name}"),
479            (Err(_), Some(_)) => format!("ROLLBACK TO SAVEPOINT {name}"),
480            (Ok(_), None) => "COMMIT".into(),
481            (Err(_), None) => "ROLLBACK".into(),
482        };
483        let cleanup = async {
484            control.lease(guard.id)?.execute_script(&end).await?;
485            if result.is_err() && parent_hooks.is_some() {
486                control
487                    .lease(guard.id)?
488                    .execute_script(&format!("RELEASE SAVEPOINT {name}"))
489                    .await?;
490            }
491            Ok::<_, OrmError>(())
492        }
493        .await;
494        if let Err(error) = cleanup {
495            tracing::warn!(%error, "transaction scope cleanup failed");
496            return match result {
497                Err(original) => Err(original),
498                Ok(_) => Err(error.into()),
499            };
500        }
501        guard.release(&hooks, parent_hooks.map(Arc::as_ref), result.is_ok())?;
502        if result.is_ok() && parent_hooks.is_none() {
503            hooks.take().into_iter().for_each(|hook| hook());
504        }
505        result
506    }
507
508    /// Register work after the outermost transaction successfully commits.
509    ///
510    /// Hooks of failed or abandoned scopes are discarded. Registration on
511    /// closed, aborted or temporarily inactive handles is discarded as well.
512    /// Outside a transaction the callback runs immediately.
513    ///
514    /// Args:
515    ///     hook: Callback invoked after commit.
516    ///
517    /// Returns:
518    ///     None.
519    pub fn on_commit(&self, hook: impl FnOnce() + Send + 'static) {
520        match &self.target {
521            Target::Pool(_) => hook(),
522            Target::Conn(state) => {
523                if state.control.ready(0).is_ok() {
524                    hook();
525                }
526            }
527            Target::Tx(state) => {
528                state
529                    .control
530                    .register(state.scope, &state.hooks, Box::new(hook));
531            }
532        }
533    }
534}