Skip to main content

teaql_runtime/context/
transaction.rs

1use std::future::Future;
2use std::pin::Pin;
3use std::sync::Mutex;
4
5use teaql_data_service::{
6    MutationExecutor, MutationRequest, MutationResult, QueryExecutor, QueryRequest, QueryResult,
7    Transaction, TransactionExecutor,
8};
9
10use crate::{ContextError, EntityDataService, RuntimeError, UserContext};
11
12/// A real provider transaction bound to a [`UserContext`].
13///
14/// All work must be performed through this object. This makes it impossible to
15/// accidentally open a transaction and then execute against the context's
16/// non-transactional executor, which was the defect in the former placeholder
17/// API.
18#[must_use = "transaction work must be completed through TransactionScope::execute"]
19pub struct TransactionScope<'context, E>
20where
21    E: TransactionExecutor + 'context,
22{
23    context: &'context UserContext,
24    transaction: Option<E::Tx<'context>>,
25    flushed_ledgers: Mutex<Vec<crate::EntityRuntimeState>>,
26}
27
28impl UserContext {
29    /// Begin a typed transaction using the executor registered in this context.
30    async fn start_transaction<E>(&self) -> Result<TransactionScope<'_, E>, RuntimeError>
31    where
32        E: TransactionExecutor + Send + Sync + 'static,
33        for<'transaction> E::Tx<'transaction>: Send + Sync,
34    {
35        let executor = self
36            .require_resource::<E>()
37            .map_err(|error| RuntimeError::Transaction(error.to_string()))?;
38        let transaction = TransactionExecutor::begin(executor)
39            .await
40            .map_err(|error| RuntimeError::Transaction(error.to_string()))?;
41        Ok(TransactionScope {
42            context: self,
43            transaction: Some(transaction),
44            flushed_ledgers: Mutex::new(Vec::new()),
45        })
46    }
47
48    /// Execute work against one typed transaction, committing on success and
49    /// rolling back on error.
50    ///
51    /// The callback receives a [`TransactionScope`]; using the surrounding
52    /// `UserContext` inside the callback would intentionally execute outside the
53    /// transaction.
54    pub async fn execute_in_transaction<'context, E, T, F>(
55        &'context self,
56        operation: F,
57    ) -> Result<T, RuntimeError>
58    where
59        E: TransactionExecutor + Send + Sync + 'static,
60        for<'transaction> E::Tx<'transaction>: Send + Sync,
61        F: for<'scope> FnOnce(
62            &'scope TransactionScope<'context, E>,
63        )
64            -> Pin<Box<dyn Future<Output = Result<T, RuntimeError>> + 'scope>>,
65    {
66        self.start_transaction::<E>()
67            .await?
68            .execute(operation)
69            .await
70    }
71}
72
73impl<'context, E> TransactionScope<'context, E>
74where
75    E: TransactionExecutor + 'context,
76{
77    fn transaction(&self) -> &E::Tx<'context> {
78        self.transaction
79            .as_ref()
80            .expect("transaction scope cannot be used after completion")
81    }
82
83    /// The runtime context that owns metadata, policy, telemetry and audit
84    /// configuration for this transaction.
85    pub fn context(&self) -> &'context UserContext {
86        self.context
87    }
88
89    /// Build an entity data service that is guaranteed to use this transaction.
90    pub fn entity_data_service(
91        &self,
92        entity: impl Into<String>,
93    ) -> Result<EntityDataService<'_, E::Tx<'context>>, ContextError>
94    where
95        E::Tx<'context>: QueryExecutor + MutationExecutor + Send + Sync,
96    {
97        let entity = entity.into();
98        if !self.context.has_entity_data_service(&entity) {
99            return Err(ContextError::MissingEntityDataService(entity));
100        }
101        Ok(EntityDataService::for_executor(
102            self.context,
103            entity,
104            self.transaction(),
105        ))
106    }
107
108    /// Execute a provider-neutral query on the transaction-owned connection.
109    pub async fn query(&self, request: QueryRequest) -> Result<QueryResult, RuntimeError>
110    where
111        E::Tx<'context>: QueryExecutor,
112    {
113        let result = QueryExecutor::query(self.transaction(), request)
114            .await
115            .map_err(|error| RuntimeError::Transaction(error.to_string()))?;
116        self.context.record_metadata_log(&result.metadata);
117        Ok(result)
118    }
119
120    /// Execute a provider-neutral mutation on the transaction-owned connection.
121    pub async fn mutate(&self, request: MutationRequest) -> Result<MutationResult, RuntimeError>
122    where
123        E::Tx<'context>: MutationExecutor,
124    {
125        let result = MutationExecutor::mutate(self.transaction(), request)
126            .await
127            .map_err(|error| RuntimeError::Transaction(error.to_string()))?;
128        self.context.record_metadata_log(&result.metadata);
129        Ok(result)
130    }
131
132    /// Save one audited generated entity through this transaction.
133    ///
134    /// Its mutation ledger remains pending until the enclosing scope commits,
135    /// so a later failure can roll back the database without losing retryable
136    /// in-memory mutation intent.
137    pub async fn save_audited<T>(&self, audited: teaql_core::Audited<T>) -> Result<T, RuntimeError>
138    where
139        T: crate::LedgerEntity + Send + 'static,
140        E::Tx<'context>: QueryExecutor + MutationExecutor + Send + Sync,
141    {
142        let (entity, ledger) = crate::save_audited_ledger_entity_with_executor(
143            audited,
144            self.context,
145            self.transaction(),
146        )
147        .await?;
148        if let Some(ledger) = ledger {
149            let mut ledgers = self
150                .flushed_ledgers
151                .lock()
152                .unwrap_or_else(|error| error.into_inner());
153            if !ledgers.iter().any(|pending| pending == &ledger) {
154                ledgers.push(ledger);
155            }
156        }
157        Ok(entity)
158    }
159
160    /// Run a callback and complete this transaction deterministically.
161    pub async fn execute<T, F>(mut self, operation: F) -> Result<T, RuntimeError>
162    where
163        E::Tx<'context>: Send + Sync,
164        F: for<'scope> FnOnce(
165            &'scope TransactionScope<'context, E>,
166        )
167            -> Pin<Box<dyn Future<Output = Result<T, RuntimeError>> + 'scope>>,
168    {
169        match operation(&self).await {
170            Ok(value) => {
171                let transaction = self
172                    .transaction
173                    .take()
174                    .expect("transaction scope cannot be completed twice");
175                Transaction::commit(transaction)
176                    .await
177                    .map_err(|error| RuntimeError::Transaction(error.to_string()))?;
178                for ledger in self
179                    .flushed_ledgers
180                    .lock()
181                    .unwrap_or_else(|error| error.into_inner())
182                    .drain(..)
183                {
184                    ledger.clear_committed();
185                }
186                Ok(value)
187            }
188            Err(error) => {
189                let transaction = self
190                    .transaction
191                    .take()
192                    .expect("transaction scope cannot be completed twice");
193                Transaction::rollback(transaction)
194                    .await
195                    .map_err(|rollback| {
196                        RuntimeError::Transaction(format!(
197                            "operation failed ({error}); rollback also failed ({rollback})"
198                        ))
199                    })?;
200                Err(error)
201            }
202        }
203    }
204}