1use std::{
2 collections::{BTreeMap, VecDeque},
3 error::Error,
4 fmt,
5 future::Future,
6 sync::{
7 Arc,
8 atomic::{AtomicU64, Ordering},
9 },
10 time::Duration,
11};
12
13use anyhow::{Context, Result};
14use serde::Serialize;
15#[cfg(target_arch = "wasm32")]
16use tokio::sync::oneshot;
17use tokio::sync::{
18 Mutex as AsyncMutex, OwnedRwLockReadGuard, OwnedRwLockWriteGuard, OwnedSemaphorePermit, RwLock,
19 Semaphore, TryAcquireError,
20};
21use tokio_util::sync::CancellationToken;
22
23#[cfg(not(target_arch = "wasm32"))]
24use crate::runtime::RuntimeSpawner;
25
26use super::{BindParam, ExecuteResult, QueryResult, SqliteDb, report_sqlite_worker_fatal};
27
28pub const DEFAULT_TRANSACTION_TIMEOUT: Duration = Duration::from_secs(60);
29pub const TRANSACTION_COORDINATOR_QUEUE_CAPACITY: usize = 128;
30pub(super) const TRANSACTION_TERMINAL_CAPACITY: usize = 1024;
31
32#[derive(Clone)]
33pub struct SqliteTransaction {
34 db: SqliteDb,
35 key: String,
36}
37
38impl SqliteTransaction {
39 pub fn key(&self) -> &str {
40 &self.key
41 }
42
43 pub async fn exec(&self, sql: impl Into<String>) -> Result<QueryResult> {
44 self.db.transaction_exec(&self.key, sql.into()).await
45 }
46
47 pub async fn execute(
48 &self,
49 sql: impl Into<String>,
50 params: Option<Vec<BindParam>>,
51 ) -> Result<ExecuteResult> {
52 self.db
53 .transaction_execute(&self.key, sql.into(), params)
54 .await
55 }
56
57 pub async fn commit(&self) -> Result<()> {
58 self.db.finish_transaction(&self.key, true).await
59 }
60
61 pub async fn rollback(&self) -> Result<()> {
62 self.db.finish_transaction(&self.key, false).await
63 }
64
65 pub async fn expire(&self) -> Result<()> {
66 self.db.expire_transaction(&self.key).await
67 }
68}
69
70pub(super) struct TransactionCoordinator {
71 pub(super) gate: Arc<RwLock<()>>,
72 pub(super) admission: Arc<Semaphore>,
73 pub(super) state: AsyncMutex<TransactionCoordinatorState>,
74 epoch: AtomicU64,
75}
76
77pub(super) struct TransactionCoordinatorState {
78 pub(super) active: Option<ActiveTransaction>,
79 pub(super) terminal: BTreeMap<String, TransactionTerminalState>,
80 pub(super) terminal_order: VecDeque<String>,
81 pub(super) poisoned: BTreeMap<String, Duration>,
82 pub(super) last_expired_timeout: Option<Duration>,
83 pub(super) closed: bool,
84}
85
86pub(super) struct ActiveTransaction {
87 key: String,
88 timeout: Duration,
89 pub(super) expiring: bool,
90 connection_lost: bool,
91 remote_session: Option<u64>,
92 gate_guard: Option<OwnedRwLockWriteGuard<()>>,
93 operation: Arc<AsyncMutex<()>>,
94 timeout_task: Option<CancellationToken>,
95}
96
97#[derive(Clone, Copy)]
98pub(super) enum TransactionTerminalState {
99 Committed,
100 RolledBack,
101 Expired(Duration),
102 ConnectionLost,
103}
104
105pub(super) struct RegularOperationGuard {
106 _gate: OwnedRwLockReadGuard<()>,
107 _permit: OwnedSemaphorePermit,
108}
109
110impl Default for TransactionCoordinator {
111 fn default() -> Self {
112 Self {
113 gate: Arc::new(RwLock::new(())),
114 admission: Arc::new(Semaphore::new(TRANSACTION_COORDINATOR_QUEUE_CAPACITY)),
115 state: AsyncMutex::new(TransactionCoordinatorState {
116 active: None,
117 terminal: BTreeMap::new(),
118 terminal_order: VecDeque::new(),
119 poisoned: BTreeMap::new(),
120 last_expired_timeout: None,
121 closed: false,
122 }),
123 epoch: AtomicU64::new(0),
124 }
125 }
126}
127
128impl SqliteDb {
129 pub(super) fn try_transaction_admission(&self) -> Result<OwnedSemaphorePermit> {
130 match Arc::clone(&self.transaction_coordinator.admission).try_acquire_owned() {
131 Ok(permit) => Ok(permit),
132 Err(TryAcquireError::NoPermits) => Err(transaction_queue_full_error()),
133 Err(TryAcquireError::Closed) => Err(transaction_coordinator_closed_error()),
134 }
135 }
136
137 pub(super) async fn begin_regular_operation(&self) -> Result<RegularOperationGuard> {
138 let epoch = self.transaction_coordinator.epoch.load(Ordering::Acquire);
139 let permit = self.try_transaction_admission()?;
140 let gate = Arc::clone(&self.transaction_coordinator.gate)
141 .read_owned()
142 .await;
143 let state = self.transaction_coordinator.state.lock().await;
144 if state.closed {
145 return Err(transaction_coordinator_closed_error());
146 }
147 if self.transaction_coordinator.epoch.load(Ordering::Acquire) != epoch {
148 return Err(transaction_expired_error(
149 state
150 .last_expired_timeout
151 .unwrap_or(DEFAULT_TRANSACTION_TIMEOUT),
152 ));
153 }
154 drop(state);
155 Ok(RegularOperationGuard {
156 _gate: gate,
157 _permit: permit,
158 })
159 }
160
161 pub async fn begin_transaction(&self, timeout: Option<Duration>) -> Result<SqliteTransaction> {
162 self.begin_transaction_with_key(uuid::Uuid::new_v4().to_string(), timeout)
163 .await
164 }
165
166 pub async fn begin_transaction_with_key(
167 &self,
168 key: impl Into<String>,
169 timeout: Option<Duration>,
170 ) -> Result<SqliteTransaction> {
171 let key = key.into();
172 let timeout = timeout.unwrap_or(DEFAULT_TRANSACTION_TIMEOUT);
173 if key.is_empty() {
174 return Err(transaction_invalid_argument_error(
175 "transaction key must not be empty",
176 ));
177 }
178 if timeout.is_zero() {
179 return Err(transaction_invalid_argument_error(
180 "transaction timeout must be greater than zero",
181 ));
182 }
183
184 let db = self.clone();
185 run_detached_transaction_task(
186 async move { db.begin_transaction_inner(key, timeout).await },
187 "sqlite transaction begin task failed",
188 )
189 .await
190 }
191
192 pub(super) async fn begin_transaction_inner(
193 &self,
194 key: String,
195 timeout: Duration,
196 ) -> Result<SqliteTransaction> {
197 let epoch = self.transaction_coordinator.epoch.load(Ordering::Acquire);
198 let permit = self.try_transaction_admission()?;
199 let gate_guard = Arc::clone(&self.transaction_coordinator.gate)
200 .write_owned()
201 .await;
202 {
203 let state = self.transaction_coordinator.state.lock().await;
204 if state.closed {
205 return Err(transaction_coordinator_closed_error());
206 }
207 if self.transaction_coordinator.epoch.load(Ordering::Acquire) != epoch {
208 return Err(transaction_expired_error(
209 state
210 .last_expired_timeout
211 .unwrap_or(DEFAULT_TRANSACTION_TIMEOUT),
212 ));
213 }
214 if let Some(error) = transaction_known_state_error(&state, &key) {
215 return Err(error);
216 }
217 }
218
219 let (_, remote_session) = self
220 .execute_backend_in_session("BEGIN".to_owned(), None, None)
221 .await
222 .map_err(map_transaction_connection_error)?;
223 if let Some(session) = remote_session
229 && self.handle()?.connection_session() != Some(session)
230 {
231 return Err(transaction_connection_lost_error());
232 }
233 let operation = Arc::new(AsyncMutex::new(()));
234 {
235 let mut state = self.transaction_coordinator.state.lock().await;
236 if state.closed {
237 drop(state);
238 if let Err(error) = self
239 .execute_backend_in_session("ROLLBACK".to_owned(), None, remote_session)
240 .await
241 {
242 tracing::error!(%error, "sqlite rollback after begin raced shutdown failed");
243 }
244 return Err(transaction_coordinator_closed_error());
245 }
246 state.active = Some(ActiveTransaction {
247 key: key.clone(),
248 timeout,
249 expiring: false,
250 connection_lost: false,
251 remote_session,
252 gate_guard: Some(gate_guard),
253 operation,
254 timeout_task: None,
255 });
256 }
257 drop(permit);
258
259 let timeout_task = CancellationToken::new();
260 let mut state = self.transaction_coordinator.state.lock().await;
261 if let Some(active) = state.active.as_mut().filter(|active| active.key == key) {
262 active.timeout_task = Some(timeout_task.clone());
263 } else {
264 timeout_task.cancel();
265 }
266 drop(state);
267 spawn_transaction_timeout(
268 self.clone(),
269 key.clone(),
270 timeout,
271 timeout_task,
272 remote_session,
273 );
274
275 Ok(SqliteTransaction {
276 db: self.clone(),
277 key,
278 })
279 }
280
281 async fn transaction_operation(&self, key: &str) -> Result<(Arc<AsyncMutex<()>>, Option<u64>)> {
282 let state = self.transaction_coordinator.state.lock().await;
283 if state.closed {
284 return Err(transaction_coordinator_closed_error());
285 }
286 if let Some(active) = state.active.as_ref().filter(|active| active.key == key) {
287 if active.expiring {
288 if active.connection_lost {
289 return Err(transaction_connection_lost_error());
290 }
291 return Err(transaction_expired_owner_error(key, active.timeout));
292 }
293 return Ok((Arc::clone(&active.operation), active.remote_session));
294 }
295 if let Some(error) = transaction_known_state_error(&state, key) {
296 return Err(error);
297 }
298 Err(transaction_unknown_error(key))
299 }
300
301 async fn ensure_active_transaction(&self, key: &str) -> Result<()> {
302 let state = self.transaction_coordinator.state.lock().await;
303 if state.closed {
304 return Err(transaction_coordinator_closed_error());
305 }
306 if let Some(active) = state.active.as_ref().filter(|active| active.key == key) {
307 if active.expiring {
308 if active.connection_lost {
309 return Err(transaction_connection_lost_error());
310 }
311 return Err(transaction_expired_owner_error(key, active.timeout));
312 }
313 return Ok(());
314 }
315 if let Some(error) = transaction_known_state_error(&state, key) {
316 return Err(error);
317 }
318 Err(transaction_unknown_error(key))
319 }
320
321 async fn transaction_exec(&self, key: &str, sql: String) -> Result<QueryResult> {
322 let db = self.clone();
323 let key = key.to_owned();
324 run_detached_transaction_task(
325 async move { db.transaction_exec_inner(&key, sql).await },
326 "sqlite transaction exec task failed",
327 )
328 .await
329 }
330
331 async fn transaction_exec_inner(&self, key: &str, sql: String) -> Result<QueryResult> {
332 let (operation, remote_session) = self.transaction_operation(key).await?;
333 let _operation = operation.lock().await;
334 self.ensure_active_transaction(key).await?;
335 match self.exec_backend_in_session(sql, remote_session).await {
336 Ok((result, _)) => Ok(result),
337 Err(error) => Err(self.handle_transaction_backend_error(key, error).await),
338 }
339 }
340
341 async fn transaction_execute(
342 &self,
343 key: &str,
344 sql: String,
345 params: Option<Vec<BindParam>>,
346 ) -> Result<ExecuteResult> {
347 let db = self.clone();
348 let key = key.to_owned();
349 run_detached_transaction_task(
350 async move { db.transaction_execute_inner(&key, sql, params).await },
351 "sqlite transaction execute task failed",
352 )
353 .await
354 }
355
356 async fn transaction_execute_inner(
357 &self,
358 key: &str,
359 sql: String,
360 params: Option<Vec<BindParam>>,
361 ) -> Result<ExecuteResult> {
362 let (operation, remote_session) = self.transaction_operation(key).await?;
363 let _operation = operation.lock().await;
364 self.ensure_active_transaction(key).await?;
365 match self
366 .execute_backend_in_session(sql, params, remote_session)
367 .await
368 {
369 Ok((result, _)) => Ok(result),
370 Err(error) => Err(self.handle_transaction_backend_error(key, error).await),
371 }
372 }
373
374 async fn finish_transaction(&self, key: &str, commit: bool) -> Result<()> {
375 let db = self.clone();
376 let key = key.to_owned();
377 run_detached_transaction_task(
378 async move { db.finish_transaction_inner(&key, commit).await },
379 "sqlite transaction finish task failed",
380 )
381 .await
382 }
383
384 async fn finish_transaction_inner(&self, key: &str, commit: bool) -> Result<()> {
385 let (operation, remote_session) = self.transaction_operation(key).await?;
386 let _operation = operation.lock().await;
387 self.ensure_active_transaction(key).await?;
388
389 let statement = if commit { "COMMIT" } else { "ROLLBACK" };
390 let result = self
391 .execute_backend_in_session(statement.to_owned(), None, remote_session)
392 .await;
393 if let Err(primary) = result {
394 if is_remote_connection_error(&primary) {
395 self.release_transaction(key, TransactionTerminalState::ConnectionLost, false)
396 .await;
397 return Err(self.attach_actor(map_transaction_connection_error(primary)));
398 }
399 if commit {
400 let rollback_result = self
401 .execute_backend_in_session("ROLLBACK".to_owned(), None, remote_session)
402 .await;
403 let rollback_failed = rollback_result
404 .as_ref()
405 .is_err_and(|error| !is_no_active_transaction_error(error));
406 self.release_transaction(
407 key,
408 TransactionTerminalState::RolledBack,
409 rollback_failed,
410 )
411 .await;
412 if let Err(rollback) = rollback_result
413 && rollback_failed
414 {
415 tracing::error!(%rollback, "sqlite rollback after failed commit failed");
416 }
417 return Err(self.attach_actor(primary));
418 }
419 if is_no_active_transaction_error(&primary) {
420 self.release_transaction(key, TransactionTerminalState::RolledBack, false)
421 .await;
422 return Ok(());
423 }
424
425 self.release_transaction(key, TransactionTerminalState::RolledBack, true)
426 .await;
427 return Err(self.attach_actor(primary));
428 }
429
430 self.release_transaction(
431 key,
432 if commit {
433 TransactionTerminalState::Committed
434 } else {
435 TransactionTerminalState::RolledBack
436 },
437 false,
438 )
439 .await;
440 Ok(())
441 }
442
443 async fn expire_transaction(&self, key: &str) -> Result<()> {
444 let db = self.clone();
445 let key = key.to_owned();
446 run_detached_transaction_task(
447 async move { db.expire_transaction_inner(&key).await },
448 "sqlite transaction expiry task failed",
449 )
450 .await
451 }
452
453 async fn expire_transaction_inner(&self, key: &str) -> Result<()> {
454 let (operation, timeout, remote_session) = {
455 let mut state = self.transaction_coordinator.state.lock().await;
456 if state.closed {
457 return Err(transaction_coordinator_closed_error());
458 }
459 if let Some(active) = state.active.as_mut().filter(|active| active.key == key) {
460 active.expiring = true;
461 (
462 Arc::clone(&active.operation),
463 active.timeout,
464 active.remote_session,
465 )
466 } else if let Some(error) = transaction_known_state_error(&state, key) {
467 return Err(error);
468 } else {
469 return Err(transaction_unknown_error(key));
470 }
471 };
472 let _operation = operation.lock().await;
473 {
474 let state = self.transaction_coordinator.state.lock().await;
475 if state.closed {
476 return Err(transaction_coordinator_closed_error());
477 }
478 if !state
479 .active
480 .as_ref()
481 .is_some_and(|active| active.key == key)
482 {
483 if let Some(error) = transaction_known_state_error(&state, key) {
484 return Err(error);
485 }
486 return Err(transaction_unknown_error(key));
487 }
488 }
489 self.transaction_coordinator
493 .epoch
494 .fetch_add(1, Ordering::AcqRel);
495 let rollback = self
496 .execute_backend_in_session("ROLLBACK".to_owned(), None, remote_session)
497 .await;
498 let rollback_failed = rollback.as_ref().is_err_and(|error| {
499 !is_no_active_transaction_error(error) && !is_remote_connection_error(error)
500 });
501 self.transaction_coordinator
505 .epoch
506 .fetch_add(1, Ordering::AcqRel);
507 self.release_transaction(
508 key,
509 TransactionTerminalState::Expired(timeout),
510 rollback_failed,
511 )
512 .await;
513 if let Err(error) = rollback
514 && rollback_failed
515 {
516 return Err(self.attach_actor(error));
517 }
518 Ok(())
519 }
520
521 async fn connection_lost_transaction(&self, key: &str, expected_session: u64) -> Result<()> {
522 let (operation, still_expected) = {
523 let mut state = self.transaction_coordinator.state.lock().await;
524 let Some(active) = state.active.as_mut().filter(|active| active.key == key) else {
525 return Ok(());
526 };
527 let still_expected = active.remote_session == Some(expected_session);
528 if still_expected {
529 active.expiring = true;
530 active.connection_lost = true;
531 }
532 (Arc::clone(&active.operation), still_expected)
533 };
534 if !still_expected {
535 return Ok(());
536 }
537 let _operation = operation.lock().await;
538
539 self.release_transaction(key, TransactionTerminalState::ConnectionLost, false)
545 .await;
546 Ok(())
547 }
548
549 async fn handle_transaction_backend_error(
550 &self,
551 key: &str,
552 error: anyhow::Error,
553 ) -> anyhow::Error {
554 if is_remote_connection_error(&error) {
555 self.release_transaction(key, TransactionTerminalState::ConnectionLost, false)
556 .await;
557 }
558 self.attach_actor(map_transaction_connection_error(error))
559 }
560
561 pub(super) async fn release_transaction(
562 &self,
563 key: &str,
564 terminal: TransactionTerminalState,
565 close: bool,
566 ) {
567 let mut state = self.transaction_coordinator.state.lock().await;
568 if !state
569 .active
570 .as_ref()
571 .is_some_and(|active| active.key == key)
572 {
573 return;
574 }
575 let mut active = state.active.take().expect("active transaction was checked");
576 if let Some(task) = active.timeout_task.take() {
577 task.cancel();
578 }
579 if let TransactionTerminalState::Expired(timeout) = terminal {
580 state.last_expired_timeout = Some(timeout);
581 }
582 insert_terminal_state(&mut state, key.to_owned(), terminal);
583 state.closed |= close;
584 if close {
585 self.transaction_coordinator.admission.close();
586 if let Ok(config) = self.runtime_config() {
587 report_sqlite_worker_fatal(
588 &self.worker_fatal_reported,
589 config,
590 "sqlite transaction rollback failed; coordinator closed".to_owned(),
591 );
592 }
593 }
594 drop(active.gate_guard.take());
595 }
596
597 pub(super) async fn shutdown_transaction_coordinator(&self) -> OwnedRwLockWriteGuard<()> {
598 let active_operation = {
599 let mut state = self.transaction_coordinator.state.lock().await;
600 state.closed = true;
601 state.active.as_ref().map(|active| {
602 (
603 active.key.clone(),
604 Arc::clone(&active.operation),
605 active.remote_session,
606 )
607 })
608 };
609 self.transaction_coordinator.admission.close();
610 if let Some((key, operation, remote_session)) = active_operation {
611 let _operation = operation.lock().await;
612 let still_active = {
613 let state = self.transaction_coordinator.state.lock().await;
614 state
615 .active
616 .as_ref()
617 .is_some_and(|active| active.key == key)
618 };
619 if still_active {
620 let rollback = self
621 .execute_backend_in_session("ROLLBACK".to_owned(), None, remote_session)
622 .await;
623 let rollback_failed = rollback.as_ref().is_err_and(|error| {
624 !is_no_active_transaction_error(error) && !is_remote_connection_error(error)
625 });
626 if let Err(error) = &rollback
627 && rollback_failed
628 {
629 tracing::error!(%error, "sqlite rollback during coordinator shutdown failed");
630 }
631 self.transaction_coordinator
632 .epoch
633 .fetch_add(1, Ordering::AcqRel);
634 self.release_transaction(
635 &key,
636 TransactionTerminalState::RolledBack,
637 rollback_failed,
638 )
639 .await;
640 }
641 }
642 Arc::clone(&self.transaction_coordinator.gate)
643 .write_owned()
644 .await
645 }
646}
647
648#[derive(rivet_error::RivetError, Debug, Serialize)]
649#[error(
650 "sqlite",
651 "transaction_queue_full",
652 "SQLite transaction queue is full.",
653 "SQLite transaction coordinator queue is full. Limit is 128 operations."
654)]
655pub struct TransactionQueueFullError;
656
657impl fmt::Display for TransactionQueueFullError {
658 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
659 f.write_str("sqlite transaction coordinator queue is full")
660 }
661}
662
663impl Error for TransactionQueueFullError {}
664
665#[derive(rivet_error::RivetError, Debug, Serialize)]
666#[error(
667 "sqlite",
668 "transaction_closed",
669 "SQLite transaction coordinator is closed."
670)]
671pub struct TransactionCoordinatorClosedError;
672
673impl fmt::Display for TransactionCoordinatorClosedError {
674 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
675 f.write_str("sqlite transaction coordinator is closed")
676 }
677}
678
679impl Error for TransactionCoordinatorClosedError {}
680
681#[derive(rivet_error::RivetError, Debug, Serialize)]
682#[error(
683 "sqlite",
684 "transaction_invalid_argument",
685 "Invalid SQLite transaction argument.",
686 "{message}"
687)]
688pub struct TransactionInvalidArgumentError {
689 pub message: &'static str,
690}
691
692impl fmt::Display for TransactionInvalidArgumentError {
693 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
694 f.write_str(self.message)
695 }
696}
697
698impl Error for TransactionInvalidArgumentError {}
699
700#[derive(rivet_error::RivetError, Debug, Serialize)]
701#[error(
702 "sqlite",
703 "transaction_connection_lost",
704 "SQLite transaction connection was lost.",
705 "The Envoy connection that owned this SQLite transaction disconnected. The transaction was rolled back and cannot be resumed; start a new db.transaction()."
706)]
707pub struct TransactionConnectionLostError;
708
709impl fmt::Display for TransactionConnectionLostError {
710 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
711 f.write_str(
712 "the Envoy connection that owned this SQLite transaction disconnected; the transaction was rolled back and cannot be resumed",
713 )
714 }
715}
716
717impl Error for TransactionConnectionLostError {}
718
719#[derive(rivet_error::RivetError, Debug, Serialize)]
720#[error(
721 "sqlite",
722 "transaction_unknown",
723 "Unknown SQLite transaction handle.",
724 "Unknown SQLite transaction handle `{key}`."
725)]
726pub struct TransactionUnknownError {
727 pub key: String,
728}
729
730impl fmt::Display for TransactionUnknownError {
731 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
732 write!(f, "unknown sqlite transaction handle `{}`", self.key)
733 }
734}
735
736impl Error for TransactionUnknownError {}
737
738#[derive(rivet_error::RivetError, Debug, Serialize)]
739#[error(
740 "sqlite",
741 "transaction_terminal",
742 "SQLite transaction handle is terminal.",
743 "SQLite transaction handle `{key}` is already {state}."
744)]
745pub struct TransactionTerminalError {
746 pub key: String,
747 pub state: &'static str,
748}
749
750impl fmt::Display for TransactionTerminalError {
751 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
752 write!(
753 f,
754 "sqlite transaction handle `{}` is already {}",
755 self.key, self.state
756 )
757 }
758}
759
760impl Error for TransactionTerminalError {}
761
762#[derive(rivet_error::RivetError, Debug, Serialize)]
763#[error(
764 "sqlite",
765 "transaction_expired",
766 "SQLite transaction expired.",
767 "SQLite transaction expired after {timeout_ms} ms and was rolled back; this timeout is a deadlock-safety backstop. Increase the db.transaction() `timeout` option if the transaction legitimately needs longer, and check for a nested transaction or use of the outer `db` instead of `tx` inside the callback."
768)]
769pub struct TransactionExpiredError {
770 pub timeout_ms: u64,
771}
772
773impl fmt::Display for TransactionExpiredError {
774 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
775 write!(
776 f,
777 "sqlite transaction expired after {} ms and was rolled back; this timeout is a deadlock-safety backstop. Increase the db.transaction() `timeout` option if the transaction legitimately needs longer, and check for a nested transaction or use of the outer `db` instead of `tx` inside the callback",
778 self.timeout_ms
779 )
780 }
781}
782
783impl Error for TransactionExpiredError {}
784
785impl TransactionExpiredError {
786 fn owner(_key: &str, timeout: Duration) -> Self {
787 Self {
788 timeout_ms: duration_millis(timeout),
789 }
790 }
791
792 fn parked(timeout: Duration) -> Self {
793 Self {
794 timeout_ms: duration_millis(timeout),
795 }
796 }
797}
798
799fn duration_millis(duration: Duration) -> u64 {
800 duration.as_millis().try_into().unwrap_or(u64::MAX)
801}
802
803async fn sleep_transaction_timeout(mut timeout: Duration) {
804 const MAX_TIMER_WAIT: Duration = Duration::from_millis(2_000_000_000);
807 while timeout > MAX_TIMER_WAIT {
808 crate::time::sleep(MAX_TIMER_WAIT).await;
809 timeout = timeout.saturating_sub(MAX_TIMER_WAIT);
810 }
811 crate::time::sleep(timeout).await;
812}
813
814fn spawn_transaction_timeout(
815 db: SqliteDb,
816 key: String,
817 timeout: Duration,
818 cancel: CancellationToken,
819 remote_session: Option<u64>,
820) {
821 let task = async move {
822 let connection_lost = async {
823 let Some(expected) = remote_session else {
824 std::future::pending::<()>().await;
825 return;
826 };
827 let Ok(handle) = db.handle() else {
828 return;
829 };
830 let mut sessions = handle.subscribe_connection_session();
831 loop {
832 if *sessions.borrow_and_update() != expected {
833 return;
834 }
835 if sessions.changed().await.is_err() {
836 return;
837 }
838 }
839 };
840 tokio::pin!(connection_lost);
841
842 let expired = tokio::select! {
843 _ = cancel.cancelled() => return,
844 _ = sleep_transaction_timeout(timeout) => true,
845 _ = &mut connection_lost => false,
846 };
847 let result = if expired {
848 db.expire_transaction(&key).await
849 } else {
850 db.connection_lost_transaction(&key, remote_session.expect("checked above"))
857 .await
858 };
859 if let Err(error) = result {
860 if error.downcast_ref::<TransactionTerminalError>().is_none() {
861 tracing::error!(%error, "sqlite transaction terminal cleanup failed");
862 }
863 }
864 };
865
866 #[cfg(target_arch = "wasm32")]
867 wasm_bindgen_futures::spawn_local(task);
868
869 #[cfg(not(target_arch = "wasm32"))]
870 RuntimeSpawner::spawn(task);
871}
872
873#[cfg(not(target_arch = "wasm32"))]
874pub(super) async fn run_detached_transaction_task<F, T>(
875 future: F,
876 context: &'static str,
877) -> Result<T>
878where
879 F: Future<Output = Result<T>> + Send + 'static,
880 T: Send + 'static,
881{
882 RuntimeSpawner::spawn(future).await.context(context)?
883}
884
885#[cfg(target_arch = "wasm32")]
886pub(super) async fn run_detached_transaction_task<F, T>(
887 future: F,
888 context: &'static str,
889) -> Result<T>
890where
891 F: Future<Output = Result<T>> + 'static,
892 T: 'static,
893{
894 let (response_tx, response_rx) = oneshot::channel();
895 wasm_bindgen_futures::spawn_local(async move {
896 let _ = response_tx.send(future.await);
897 });
898 response_rx.await.context(context)?
899}
900
901pub(super) fn insert_terminal_state(
902 state: &mut TransactionCoordinatorState,
903 key: String,
904 terminal: TransactionTerminalState,
905) {
906 if let TransactionTerminalState::Expired(timeout) = terminal {
907 state.poisoned.insert(key, timeout);
908 return;
909 }
910 if state.terminal.insert(key.clone(), terminal).is_none() {
911 state.terminal_order.push_back(key);
912 }
913 while state.terminal_order.len() > TRANSACTION_TERMINAL_CAPACITY {
914 if let Some(expired_key) = state.terminal_order.pop_front() {
915 state.terminal.remove(&expired_key);
916 }
917 }
918}
919
920fn transaction_known_state_error(
921 state: &TransactionCoordinatorState,
922 key: &str,
923) -> Option<anyhow::Error> {
924 if let Some(timeout) = state.poisoned.get(key).copied() {
925 return Some(transaction_expired_owner_error(key, timeout));
926 }
927 state
928 .terminal
929 .get(key)
930 .copied()
931 .map(|terminal| transaction_terminal_error(key, terminal))
932}
933
934fn is_no_active_transaction_error(error: &anyhow::Error) -> bool {
935 error.chain().any(|cause| {
943 cause
944 .to_string()
945 .to_ascii_lowercase()
946 .contains("no transaction is active")
947 })
948}
949
950fn transaction_queue_full_error() -> anyhow::Error {
951 TransactionQueueFullError
952 .build()
953 .context(TransactionQueueFullError)
954}
955
956fn transaction_coordinator_closed_error() -> anyhow::Error {
957 TransactionCoordinatorClosedError
958 .build()
959 .context(TransactionCoordinatorClosedError)
960}
961
962fn transaction_unknown_error(key: &str) -> anyhow::Error {
963 TransactionUnknownError {
964 key: key.to_owned(),
965 }
966 .build()
967 .context(TransactionUnknownError {
968 key: key.to_owned(),
969 })
970}
971
972fn transaction_expired_error(timeout: Duration) -> anyhow::Error {
973 TransactionExpiredError::parked(timeout)
974 .build()
975 .context(TransactionExpiredError::parked(timeout))
976}
977
978fn transaction_expired_owner_error(key: &str, timeout: Duration) -> anyhow::Error {
979 TransactionExpiredError::owner(key, timeout)
980 .build()
981 .context(TransactionExpiredError::owner(key, timeout))
982}
983
984fn transaction_terminal_error(key: &str, terminal: TransactionTerminalState) -> anyhow::Error {
985 match terminal {
986 TransactionTerminalState::Expired(timeout) => transaction_expired_owner_error(key, timeout),
987 TransactionTerminalState::Committed => transaction_terminal_state_error(key, "committed"),
988 TransactionTerminalState::RolledBack => {
989 transaction_terminal_state_error(key, "rolled back")
990 }
991 TransactionTerminalState::ConnectionLost => transaction_connection_lost_error(),
992 }
993}
994
995fn transaction_invalid_argument_error(message: &'static str) -> anyhow::Error {
996 TransactionInvalidArgumentError { message }
997 .build()
998 .context(TransactionInvalidArgumentError { message })
999}
1000
1001fn transaction_connection_lost_error() -> anyhow::Error {
1002 TransactionConnectionLostError
1003 .build()
1004 .context(TransactionConnectionLostError)
1005}
1006
1007fn is_remote_connection_error(error: &anyhow::Error) -> bool {
1008 error
1009 .downcast_ref::<super::RemoteSqliteConnectionSessionLostError>()
1010 .is_some()
1011 || error
1012 .downcast_ref::<super::RemoteSqliteIndeterminateResultError>()
1013 .is_some()
1014}
1015
1016fn map_transaction_connection_error(error: anyhow::Error) -> anyhow::Error {
1017 if error
1018 .downcast_ref::<super::RemoteSqliteConnectionSessionLostError>()
1019 .is_some()
1020 {
1021 return transaction_connection_lost_error();
1022 }
1023 super::remote_request_error(error)
1024}
1025
1026fn transaction_terminal_state_error(key: &str, state: &'static str) -> anyhow::Error {
1027 TransactionTerminalError {
1028 key: key.to_owned(),
1029 state,
1030 }
1031 .build()
1032 .context(TransactionTerminalError {
1033 key: key.to_owned(),
1034 state,
1035 })
1036}