1use std::{
2 collections::{BTreeMap, VecDeque},
3 error::Error,
4 fmt,
5 future::Future,
6 sync::{
7 Arc, LazyLock,
8 atomic::{AtomicU64, Ordering},
9 },
10 time::Duration,
11};
12
13use anyhow::{Context, Result};
14use rivet_metrics::prometheus::{IntCounter, IntGauge};
15use serde::Serialize;
16#[cfg(target_arch = "wasm32")]
17use tokio::sync::oneshot;
18use tokio::sync::{
19 Mutex as AsyncMutex, OwnedRwLockReadGuard, OwnedRwLockWriteGuard, OwnedSemaphorePermit, RwLock,
20 Semaphore, TryAcquireError,
21};
22use tokio_util::sync::CancellationToken;
23
24#[cfg(not(target_arch = "wasm32"))]
25use crate::runtime::RuntimeSpawner;
26use crate::telemetry::SqliteOperation;
27
28#[cfg(feature = "sqlite-local")]
29use super::profiling::{FINGERPRINT_FORMAT_VERSION, TransactionProfile};
30use super::{BindParam, ExecuteResult, QueryResult, SqliteDb, report_sqlite_worker_fatal};
31
32pub const DEFAULT_TRANSACTION_TIMEOUT: Duration = Duration::from_secs(60);
33#[cfg(not(feature = "sqlite-local"))]
34const MAX_TRANSACTION_NAME_BYTES: usize = 128;
35pub const TRANSACTION_COORDINATOR_QUEUE_CAPACITY: usize = 128;
36pub(super) const TRANSACTION_TERMINAL_CAPACITY: usize = 1024;
37pub(super) const TRANSACTION_EXPIRED_CAPACITY: usize = 1024;
40#[cfg(feature = "sqlite-local")]
41static UNNAMED_TRANSACTION_WARNINGS: AtomicU64 = AtomicU64::new(0);
42
43#[derive(Clone)]
44pub struct SqliteTransaction {
45 db: SqliteDb,
46 key: String,
47}
48
49impl SqliteTransaction {
50 pub fn key(&self) -> &str {
51 &self.key
52 }
53
54 pub async fn exec(&self, sql: impl Into<String>) -> Result<QueryResult> {
55 self.db.transaction_exec(&self.key, sql.into()).await
56 }
57
58 pub async fn execute(
59 &self,
60 sql: impl Into<String>,
61 params: Option<Vec<BindParam>>,
62 ) -> Result<ExecuteResult> {
63 self.db
64 .transaction_execute(&self.key, sql.into(), params)
65 .await
66 }
67
68 pub async fn commit(&self) -> Result<()> {
69 self.db.finish_transaction(&self.key, true).await
70 }
71
72 pub async fn rollback(&self) -> Result<()> {
73 self.db.finish_transaction(&self.key, false).await
74 }
75
76 pub async fn expire(&self) -> Result<()> {
77 self.db.expire_transaction(&self.key).await
78 }
79}
80
81pub(super) struct TransactionCoordinator {
82 pub(super) gate: Arc<RwLock<()>>,
83 pub(super) admission: Arc<Semaphore>,
84 pub(super) state: AsyncMutex<TransactionCoordinatorState>,
85 epoch: AtomicU64,
86 waiters: AtomicU64,
87 pub(super) manual_transaction_waiters: AtomicU64,
89 closed_token: CancellationToken,
92}
93
94pub(super) struct TransactionCoordinatorState {
95 pub(super) active: Option<ActiveTransaction>,
96 pub(super) terminal: BTreeMap<String, TransactionTerminalState>,
97 pub(super) terminal_order: VecDeque<String>,
98 pub(super) poisoned: BTreeMap<String, ExpiredKey>,
99 pub(super) poisoned_order: VecDeque<String>,
100 pub(super) last_expired_timeout: Option<Duration>,
101 pub(super) closed: bool,
102}
103
104pub(super) struct ExpiredKey {
106 pub(super) timeout: Duration,
107 _metric: rivet_metrics::IntGaugeGuard,
108}
109
110struct ExpiredKeyMetrics {
113 keys: IntGauge,
114 evicted_total: IntCounter,
115}
116
117static EXPIRED_KEY_METRICS: LazyLock<ExpiredKeyMetrics> = LazyLock::new(|| {
118 let keys = IntGauge::new(
119 "rivetkit_sqlite_transaction_expired_keys",
120 "expired SQLite transaction keys remembered across all actors in the process",
121 )
122 .expect("create rivetkit_sqlite_transaction_expired_keys gauge");
123 let capacity = IntGauge::new(
124 "rivetkit_sqlite_transaction_expired_keys_capacity",
125 "maximum expired SQLite transaction keys remembered per actor",
126 )
127 .expect("create rivetkit_sqlite_transaction_expired_keys_capacity gauge");
128 let evicted_total = IntCounter::new(
129 "rivetkit_sqlite_transaction_expired_keys_evicted_total",
130 "total expired SQLite transaction keys evicted because an actor reached the cap",
131 )
132 .expect("create rivetkit_sqlite_transaction_expired_keys_evicted_total counter");
133 capacity.set(TRANSACTION_EXPIRED_CAPACITY as i64);
134
135 for collector in [
136 Box::new(keys.clone()) as Box<dyn rivet_metrics::prometheus::core::Collector>,
137 Box::new(capacity),
138 Box::new(evicted_total.clone()),
139 ] {
140 if let Err(error) = rivet_metrics::REGISTRY.register(collector) {
141 tracing::warn!(
142 ?error,
143 "sqlite transaction metric registration failed, using existing collector"
144 );
145 }
146 }
147
148 ExpiredKeyMetrics {
149 keys,
150 evicted_total,
151 }
152});
153
154struct ManualTransactionWaitMetrics {
157 waiters: IntGauge,
158 timeouts_total: IntCounter,
159}
160
161static MANUAL_TRANSACTION_WAIT_METRICS: LazyLock<ManualTransactionWaitMetrics> = LazyLock::new(
162 || {
163 let waiters = IntGauge::new(
164 "rivetkit_sqlite_manual_transaction_waiters",
165 "coordinated SQLite transactions waiting for a manual transaction to finish across all actors in the process",
166 )
167 .expect("create rivetkit_sqlite_manual_transaction_waiters gauge");
168 let timeouts_total = IntCounter::new(
169 "rivetkit_sqlite_manual_transaction_wait_timeouts_total",
170 "total coordinated SQLite transactions that timed out waiting for a manual transaction to finish",
171 )
172 .expect("create rivetkit_sqlite_manual_transaction_wait_timeouts_total counter");
173
174 for collector in [
175 Box::new(waiters.clone()) as Box<dyn rivet_metrics::prometheus::core::Collector>,
176 Box::new(timeouts_total.clone()),
177 ] {
178 if let Err(error) = rivet_metrics::REGISTRY.register(collector) {
179 tracing::warn!(
180 ?error,
181 "sqlite manual transaction metric registration failed, using existing collector"
182 );
183 }
184 }
185
186 ManualTransactionWaitMetrics {
187 waiters,
188 timeouts_total,
189 }
190 },
191);
192
193struct ManualTransactionWaitGuard<'a> {
195 coordinator: &'a TransactionCoordinator,
196 _metric: rivet_metrics::IntGaugeGuard,
197}
198
199impl<'a> ManualTransactionWaitGuard<'a> {
200 fn new(db: &'a SqliteDb, timeout: Duration) -> Self {
201 let coordinator = db.transaction_coordinator.as_ref();
202 let previous = coordinator
203 .manual_transaction_waiters
204 .fetch_add(1, Ordering::AcqRel);
205 if previous == 0 {
206 tracing::warn!(
207 actor_id = db.actor_id.as_deref().unwrap_or("unknown"),
208 timeout_ms = duration_millis(timeout),
209 "sqlite transaction is waiting for a manual transaction to finish; a BEGIN issued through ordinary statements has not reached COMMIT or ROLLBACK, so actor state persistence and db.transaction() are delayed"
210 );
211 }
212 Self {
213 coordinator,
214 _metric: rivet_metrics::GaugeGuardExt::inc_guard(
215 &MANUAL_TRANSACTION_WAIT_METRICS.waiters,
216 ),
217 }
218 }
219}
220
221impl Drop for ManualTransactionWaitGuard<'_> {
222 fn drop(&mut self) {
223 self.coordinator
224 .manual_transaction_waiters
225 .fetch_sub(1, Ordering::AcqRel);
226 }
227}
228
229pub(super) struct ActiveTransaction {
230 key: String,
231 timeout: Duration,
232 pub(super) expiring: bool,
233 connection_lost: bool,
234 remote_session: Option<u64>,
235 gate_guard: Option<OwnedRwLockWriteGuard<()>>,
236 operation: Arc<AsyncMutex<()>>,
237 timeout_task: Option<CancellationToken>,
238 #[cfg(feature = "sqlite-local")]
239 profile: Option<TransactionProfile>,
240}
241
242#[derive(Clone, Copy)]
243pub(super) enum TransactionTerminalState {
244 Committed,
245 RolledBack,
246 Expired(Duration),
247 ConnectionLost,
248}
249
250pub(super) struct RegularOperationGuard {
251 _gate: OwnedRwLockReadGuard<()>,
252 _permit: OwnedSemaphorePermit,
253}
254
255struct CoordinatorWaitGuard<'a> {
256 db: &'a SqliteDb,
257}
258
259impl<'a> CoordinatorWaitGuard<'a> {
260 #[cfg(feature = "sqlite-local")]
261 fn new(db: &'a SqliteDb) -> Option<Self> {
262 let metrics = db.vfs_metrics.as_ref()?;
263 let _depth = db
264 .transaction_coordinator
265 .waiters
266 .fetch_add(1, Ordering::AcqRel)
267 .saturating_add(1);
268 metrics.set_coordinator_queue_depth(_depth);
269 Some(Self { db })
270 }
271
272 #[cfg(not(feature = "sqlite-local"))]
273 fn new(_db: &'a SqliteDb) -> Option<Self> {
274 None
275 }
276}
277
278impl Drop for CoordinatorWaitGuard<'_> {
279 fn drop(&mut self) {
280 let _depth = self
281 .db
282 .transaction_coordinator
283 .waiters
284 .fetch_sub(1, Ordering::AcqRel)
285 .saturating_sub(1);
286 #[cfg(feature = "sqlite-local")]
287 if let Some(metrics) = &self.db.vfs_metrics {
288 metrics.set_coordinator_queue_depth(_depth);
289 }
290 }
291}
292
293impl Default for TransactionCoordinator {
294 fn default() -> Self {
295 Self {
296 gate: Arc::new(RwLock::new(())),
297 admission: Arc::new(Semaphore::new(TRANSACTION_COORDINATOR_QUEUE_CAPACITY)),
298 state: AsyncMutex::new(TransactionCoordinatorState {
299 active: None,
300 terminal: BTreeMap::new(),
301 terminal_order: VecDeque::new(),
302 poisoned: BTreeMap::new(),
303 poisoned_order: VecDeque::new(),
304 last_expired_timeout: None,
305 closed: false,
306 }),
307 epoch: AtomicU64::new(0),
308 waiters: AtomicU64::new(0),
309 manual_transaction_waiters: AtomicU64::new(0),
310 closed_token: CancellationToken::new(),
311 }
312 }
313}
314
315impl SqliteDb {
316 pub(super) fn try_transaction_admission(&self) -> Result<OwnedSemaphorePermit> {
317 match Arc::clone(&self.transaction_coordinator.admission).try_acquire_owned() {
318 Ok(permit) => Ok(permit),
319 Err(TryAcquireError::NoPermits) => Err(transaction_queue_full_error()),
320 Err(TryAcquireError::Closed) => Err(transaction_coordinator_closed_error()),
321 }
322 }
323
324 pub(super) async fn begin_regular_operation(&self) -> Result<RegularOperationGuard> {
325 if self.has_deferred_bootstrap() {
328 Box::pin(self.commit_deferred_bootstrap()).await?;
329 }
330 self.begin_admitted_operation().await
331 }
332
333 pub(super) async fn begin_admitted_operation(&self) -> Result<RegularOperationGuard> {
336 let epoch = self.transaction_coordinator.epoch.load(Ordering::Acquire);
337 let permit = self.try_transaction_admission()?;
338 let wait = CoordinatorWaitGuard::new(self);
339 let gate = Arc::clone(&self.transaction_coordinator.gate)
340 .read_owned()
341 .await;
342 drop(wait);
343 let state = self.transaction_coordinator.state.lock().await;
344 if state.closed {
345 return Err(transaction_coordinator_closed_error());
346 }
347 if self.transaction_coordinator.epoch.load(Ordering::Acquire) != epoch {
348 return Err(transaction_expired_error(
349 state
350 .last_expired_timeout
351 .unwrap_or(DEFAULT_TRANSACTION_TIMEOUT),
352 ));
353 }
354 drop(state);
355 Ok(RegularOperationGuard {
356 _gate: gate,
357 _permit: permit,
358 })
359 }
360
361 pub async fn begin_transaction(&self, timeout: Option<Duration>) -> Result<SqliteTransaction> {
362 self.begin_named_transaction(None, timeout).await
363 }
364
365 pub async fn begin_named_transaction(
366 &self,
367 name: Option<&str>,
368 timeout: Option<Duration>,
369 ) -> Result<SqliteTransaction> {
370 #[cfg(feature = "sqlite-local")]
371 let max_name_bytes = self.profiling.config.max_transaction_name_bytes;
372 #[cfg(not(feature = "sqlite-local"))]
373 let max_name_bytes = MAX_TRANSACTION_NAME_BYTES;
374 if let Some(name) = name {
375 if name.is_empty() {
376 return Err(transaction_invalid_argument_error(
377 "transaction name must not be empty",
378 ));
379 }
380 if name.len() > max_name_bytes {
381 return Err(transaction_invalid_argument_error(
382 "transaction name exceeds the configured byte limit",
383 ));
384 }
385 }
386 self.begin_transaction_with_key_and_name(
387 uuid::Uuid::new_v4().to_string(),
388 name.map(ToOwned::to_owned),
389 timeout,
390 )
391 .await
392 }
393
394 pub async fn begin_transaction_with_key(
395 &self,
396 key: impl Into<String>,
397 timeout: Option<Duration>,
398 ) -> Result<SqliteTransaction> {
399 self.begin_transaction_with_key_and_name(key, None, timeout)
400 .await
401 }
402
403 async fn begin_transaction_with_key_and_name(
404 &self,
405 key: impl Into<String>,
406 name: Option<String>,
407 timeout: Option<Duration>,
408 ) -> Result<SqliteTransaction> {
409 #[cfg(feature = "sqlite-local")]
410 let started_at =
411 (self.backend() == super::SqliteBackend::LocalNative).then(crate::time::Instant::now);
412 let key = key.into();
413 let timeout = timeout.unwrap_or(DEFAULT_TRANSACTION_TIMEOUT);
414 if key.is_empty() {
415 return Err(transaction_invalid_argument_error(
416 "transaction key must not be empty",
417 ));
418 }
419 if timeout.is_zero() {
420 return Err(transaction_invalid_argument_error(
421 "transaction timeout must be greater than zero",
422 ));
423 }
424
425 let db = self.clone();
426 let task = run_detached_transaction_task(
427 async move {
428 db.begin_transaction_profiled_inner(
429 key,
430 timeout,
431 name,
432 #[cfg(feature = "sqlite-local")]
433 started_at,
434 )
435 .await
436 },
437 "sqlite transaction begin task failed",
438 );
439 self.traced(SqliteOperation::TransactionBegin, task).await
440 }
441
442
443
444 async fn begin_transaction_profiled_inner(
445 &self,
446 key: String,
447 timeout: Duration,
448 _name: Option<String>,
449 #[cfg(feature = "sqlite-local")] started_at: Option<crate::time::Instant>,
450 ) -> Result<SqliteTransaction> {
451 #[cfg(feature = "sqlite-local")]
452 let transaction_wait_started_at = started_at.map(|_| crate::time::Instant::now());
453 let epoch = self.transaction_coordinator.epoch.load(Ordering::Acquire);
454 let permit = self.try_transaction_admission()?;
455 let wait = CoordinatorWaitGuard::new(self);
456 let gate_guard = self
457 .acquire_gate_outside_manual_transaction(timeout)
458 .await?;
459 drop(wait);
460 #[cfg(feature = "sqlite-local")]
461 let transaction_wait = transaction_wait_started_at.map(|started| started.elapsed());
462 {
463 let state = self.transaction_coordinator.state.lock().await;
464 if state.closed {
465 return Err(transaction_coordinator_closed_error());
466 }
467 if self.transaction_coordinator.epoch.load(Ordering::Acquire) != epoch {
468 return Err(transaction_expired_error(
469 state
470 .last_expired_timeout
471 .unwrap_or(DEFAULT_TRANSACTION_TIMEOUT),
472 ));
473 }
474 if let Some(error) = transaction_known_state_error(&state, &key) {
475 return Err(error);
476 }
477 }
478
479 #[cfg(feature = "sqlite-local")]
480 let begin_started_at = started_at.map(|_| crate::time::Instant::now());
481 #[cfg(feature = "sqlite-local")]
482 let (begin_result, begin_profile) = if begin_started_at.is_some() {
483 let profiled = self
484 .execute_backend_profiled("BEGIN".to_owned(), None)
485 .await;
486 (
487 profiled.result.map(|result| (result, None)),
488 profiled.profile,
489 )
490 } else {
491 (
492 self.execute_backend_in_session("BEGIN".to_owned(), None, None)
493 .await,
494 None,
495 )
496 };
497 #[cfg(feature = "sqlite-local")]
498 let begin_duration = begin_started_at.map(|started| started.elapsed());
499 #[cfg(not(feature = "sqlite-local"))]
500 let begin_result = self
501 .execute_backend_in_session("BEGIN".to_owned(), None, None)
502 .await;
503 let (_, remote_session) = begin_result.map_err(map_transaction_connection_error)?;
504 if let Some(session) = remote_session
510 && self.handle()?.connection_session() != Some(session)
511 {
512 return Err(transaction_connection_lost_error());
513 }
514 let operation = Arc::new(AsyncMutex::new(()));
515 {
516 let mut state = self.transaction_coordinator.state.lock().await;
517 if state.closed {
518 drop(state);
519 if let Err(error) = self
520 .execute_backend_in_session("ROLLBACK".to_owned(), None, remote_session)
521 .await
522 {
523 tracing::error!(%error, "sqlite rollback after begin raced shutdown failed");
524 }
525 return Err(transaction_coordinator_closed_error());
526 }
527 state.active = Some(ActiveTransaction {
528 key: key.clone(),
529 timeout,
530 expiring: false,
531 connection_lost: false,
532 remote_session,
533 gate_guard: Some(gate_guard),
534 operation,
535 timeout_task: None,
536 #[cfg(feature = "sqlite-local")]
537 profile: started_at.map(|started_at| {
538 let mut profile = TransactionProfile::new(
539 _name,
540 started_at,
541 self.profiling.config.max_statements_per_transaction_trace,
542 self.profiling.selection.tracks_transactions(),
543 );
544 profile.transaction_wait_ns = transaction_wait
545 .unwrap_or_default()
546 .as_nanos()
547 .try_into()
548 .unwrap_or(u64::MAX);
549 let default_profile = sqlite_embedded::vfs::SqliteOperationProfile::default();
550 profile.record_control(
551 begin_profile.as_ref().unwrap_or(&default_profile),
552 begin_duration
553 .unwrap_or_default()
554 .as_nanos()
555 .try_into()
556 .unwrap_or(u64::MAX),
557 false,
558 );
559 profile
560 }),
561 });
562 }
563 drop(permit);
564
565 let timeout_task = CancellationToken::new();
566 let mut state = self.transaction_coordinator.state.lock().await;
567 if let Some(active) = state.active.as_mut().filter(|active| active.key == key) {
568 active.timeout_task = Some(timeout_task.clone());
569 } else {
570 timeout_task.cancel();
571 }
572 drop(state);
573 spawn_transaction_timeout(
574 self.clone(),
575 key.clone(),
576 timeout,
577 timeout_task,
578 remote_session,
579 );
580
581 if self.has_deferred_bootstrap() {
583 Box::pin(self.apply_deferred_bootstrap(&key)).await?;
584 }
585
586 Ok(SqliteTransaction {
587 db: self.clone(),
588 key,
589 })
590 }
591
592 async fn acquire_gate_outside_manual_transaction(
607 &self,
608 timeout: Duration,
609 ) -> Result<OwnedRwLockWriteGuard<()>> {
610 let deadline = crate::time::Instant::now() + timeout;
611 let mut wait_guard = None;
612 loop {
613 let gate_guard = Arc::clone(&self.transaction_coordinator.gate)
614 .write_owned()
615 .await;
616 let Some(mut explicit_transaction) = self.subscribe_explicit_transaction() else {
617 return Ok(gate_guard);
618 };
619 if !*explicit_transaction.borrow_and_update() {
620 return Ok(gate_guard);
621 }
622 drop(gate_guard);
623
624 if wait_guard.is_none() {
625 wait_guard = Some(ManualTransactionWaitGuard::new(self, timeout));
626 }
627 tokio::select! {
628 _ = explicit_transaction.wait_for(|open| !*open) => {}
629 _ = self.transaction_coordinator.closed_token.cancelled() => {
630 return Err(transaction_coordinator_closed_error());
631 }
632 _ = crate::time::sleep_until(deadline) => {
633 MANUAL_TRANSACTION_WAIT_METRICS.timeouts_total.inc();
634 return Err(manual_transaction_open_error(timeout));
635 }
636 }
637 }
638 }
639
640 async fn transaction_operation(&self, key: &str) -> Result<(Arc<AsyncMutex<()>>, Option<u64>)> {
641 let state = self.transaction_coordinator.state.lock().await;
642 if state.closed {
643 return Err(transaction_coordinator_closed_error());
644 }
645 if let Some(active) = state.active.as_ref().filter(|active| active.key == key) {
646 if active.expiring {
647 if active.connection_lost {
648 return Err(transaction_connection_lost_error());
649 }
650 return Err(transaction_expired_owner_error(key, active.timeout));
651 }
652 return Ok((Arc::clone(&active.operation), active.remote_session));
653 }
654 if let Some(error) = transaction_known_state_error(&state, key) {
655 return Err(error);
656 }
657 Err(transaction_unknown_error(key))
658 }
659
660 async fn ensure_active_transaction(&self, key: &str) -> Result<()> {
661 let state = self.transaction_coordinator.state.lock().await;
662 if state.closed {
663 return Err(transaction_coordinator_closed_error());
664 }
665 if let Some(active) = state.active.as_ref().filter(|active| active.key == key) {
666 if active.expiring {
667 if active.connection_lost {
668 return Err(transaction_connection_lost_error());
669 }
670 return Err(transaction_expired_owner_error(key, active.timeout));
671 }
672 return Ok(());
673 }
674 if let Some(error) = transaction_known_state_error(&state, key) {
675 return Err(error);
676 }
677 Err(transaction_unknown_error(key))
678 }
679
680 async fn transaction_exec(&self, key: &str, sql: String) -> Result<QueryResult> {
681 let db = self.clone();
682 let key = key.to_owned();
683 let task = run_detached_transaction_task(
684 async move { db.transaction_exec_inner(&key, sql).await },
685 "sqlite transaction exec task failed",
686 );
687 self.traced(SqliteOperation::TransactionExec, task).await
688 }
689
690 async fn transaction_exec_inner(&self, key: &str, sql: String) -> Result<QueryResult> {
691 #[cfg(feature = "sqlite-local")]
692 let started_at =
693 (self.backend() == super::SqliteBackend::LocalNative).then(crate::time::Instant::now);
694 let (operation, remote_session) = self.transaction_operation(key).await?;
695 let _operation = operation.lock().await;
696 #[cfg(feature = "sqlite-local")]
697 let transaction_wait = started_at.map(|started| started.elapsed());
698 self.ensure_active_transaction(key).await?;
699 #[cfg(feature = "sqlite-local")]
700 if let Some(started_at) = started_at {
701 let sql_for_profile = sql.clone();
702 let profiled = self.exec_backend_profiled(sql).await;
703 let result = profiled.result;
704 let profile = profiled.profile;
705 if let Some(observation) = self.observe_statement_profile(
706 &sql_for_profile,
707 started_at,
708 transaction_wait.unwrap_or_default(),
709 profile,
710 if result.is_ok() { "success" } else { "error" },
711 "explicit",
712 ) {
713 self.record_transaction_statement(key, &observation).await;
714 }
715 return match result {
716 Ok(result) => Ok(result),
717 Err(error) => Err(self.handle_transaction_backend_error(key, error).await),
718 };
719 }
720 match self.exec_backend_in_session(sql, remote_session).await {
721 Ok((result, _)) => Ok(result),
722 Err(error) => Err(self.handle_transaction_backend_error(key, error).await),
723 }
724 }
725
726 async fn transaction_execute(
727 &self,
728 key: &str,
729 sql: String,
730 params: Option<Vec<BindParam>>,
731 ) -> Result<ExecuteResult> {
732 let db = self.clone();
733 let key = key.to_owned();
734 let task = run_detached_transaction_task(
735 async move { db.transaction_execute_inner(&key, sql, params).await },
736 "sqlite transaction execute task failed",
737 );
738 self.traced(SqliteOperation::TransactionExecute, task).await
739 }
740
741 pub(super) async fn transaction_execute_inner(
742 &self,
743 key: &str,
744 sql: String,
745 params: Option<Vec<BindParam>>,
746 ) -> Result<ExecuteResult> {
747 #[cfg(feature = "sqlite-local")]
748 let started_at =
749 (self.backend() == super::SqliteBackend::LocalNative).then(crate::time::Instant::now);
750 let (operation, remote_session) = self.transaction_operation(key).await?;
751 let _operation = operation.lock().await;
752 #[cfg(feature = "sqlite-local")]
753 let transaction_wait = started_at.map(|started| started.elapsed());
754 self.ensure_active_transaction(key).await?;
755 #[cfg(feature = "sqlite-local")]
756 if let Some(started_at) = started_at {
757 let sql_for_profile = sql.clone();
758 let profiled = self.execute_backend_profiled(sql, params).await;
759 let result = profiled.result;
760 let profile = profiled.profile;
761 if let Some(observation) = self.observe_statement_profile(
762 &sql_for_profile,
763 started_at,
764 transaction_wait.unwrap_or_default(),
765 profile,
766 if result.is_ok() { "success" } else { "error" },
767 "explicit",
768 ) {
769 self.record_transaction_statement(key, &observation).await;
770 }
771 return match result {
772 Ok(result) => Ok(result),
773 Err(error) => Err(self.handle_transaction_backend_error(key, error).await),
774 };
775 }
776 match self
777 .execute_backend_in_session(sql, params, remote_session)
778 .await
779 {
780 Ok((result, _)) => Ok(result),
781 Err(error) => Err(self.handle_transaction_backend_error(key, error).await),
782 }
783 }
784
785 pub(super) async fn transaction_execute_statements_inner(
789 &self,
790 key: &str,
791 statements: Vec<super::SqliteBatchStatement>,
792 ) -> Result<()> {
793 if self.backend() != super::SqliteBackend::LocalNative {
794 for statement in statements {
795 self.transaction_execute_inner(key, statement.sql, statement.params)
796 .await?;
797 }
798 return Ok(());
799 }
800
801 let (operation, _) = self.transaction_operation(key).await?;
802 let _operation = operation.lock().await;
803 self.ensure_active_transaction(key).await?;
804 match self.local_execute_batch(statements).await {
805 Ok(()) => Ok(()),
806 Err(error) => Err(self.handle_transaction_backend_error(key, error).await),
807 }
808 }
809
810 #[cfg(feature = "sqlite-local")]
811 async fn record_transaction_statement(
812 &self,
813 key: &str,
814 observation: &super::profiling::StatementObservation,
815 ) {
816 let mut state = self.transaction_coordinator.state.lock().await;
817 if let Some(active) = state.active.as_mut().filter(|active| active.key == key) {
818 if let Some(profile) = &mut active.profile {
819 profile.record_statement(observation);
820 }
821 }
822 }
823
824 async fn finish_transaction(&self, key: &str, commit: bool) -> Result<()> {
825 let db = self.clone();
826 let key = key.to_owned();
827 let operation = if commit {
828 SqliteOperation::TransactionCommit
829 } else {
830 SqliteOperation::TransactionRollback
831 };
832 let task = run_detached_transaction_task(
833 async move { db.finish_transaction_inner(&key, commit).await },
834 "sqlite transaction finish task failed",
835 );
836 self.traced(operation, task).await
837 }
838
839 pub(super) async fn finish_transaction_inner(&self, key: &str, commit: bool) -> Result<()> {
840 let (operation, remote_session) = self.transaction_operation(key).await?;
841 let _operation = operation.lock().await;
842 self.ensure_active_transaction(key).await?;
843
844 let statement = if commit { "COMMIT" } else { "ROLLBACK" };
845 #[cfg(feature = "sqlite-local")]
846 let control_started_at =
847 (self.backend() == super::SqliteBackend::LocalNative).then(crate::time::Instant::now);
848 #[cfg(feature = "sqlite-local")]
849 let (result, control_profile) = if control_started_at.is_some() {
850 let profiled = self
851 .execute_backend_profiled(statement.to_owned(), None)
852 .await;
853 (
854 profiled.result.map(|result| (result, None)),
855 profiled.profile,
856 )
857 } else {
858 (
859 self.execute_backend_in_session(statement.to_owned(), None, remote_session)
860 .await,
861 None,
862 )
863 };
864 #[cfg(not(feature = "sqlite-local"))]
865 let result = self
866 .execute_backend_in_session(statement.to_owned(), None, remote_session)
867 .await;
868 #[cfg(feature = "sqlite-local")]
869 if let Some(control_started_at) = control_started_at {
870 self.record_transaction_control(
871 key,
872 control_profile.as_ref(),
873 control_started_at.elapsed(),
874 commit,
875 )
876 .await;
877 }
878 if let Err(primary) = result {
879 if is_remote_connection_error(&primary) {
880 self.release_transaction(key, TransactionTerminalState::ConnectionLost, false)
881 .await;
882 return Err(self.attach_actor(map_transaction_connection_error(primary)));
883 }
884 if commit {
885 let rollback_result = self
886 .execute_backend_in_session("ROLLBACK".to_owned(), None, remote_session)
887 .await;
888 let rollback_failed = rollback_result
889 .as_ref()
890 .is_err_and(|error| !is_no_active_transaction_error(error));
891 self.release_transaction(
892 key,
893 TransactionTerminalState::RolledBack,
894 rollback_failed,
895 )
896 .await;
897 if let Err(rollback) = rollback_result
898 && rollback_failed
899 {
900 tracing::error!(%rollback, "sqlite rollback after failed commit failed");
901 }
902 return Err(self.attach_actor(primary));
903 }
904 if is_no_active_transaction_error(&primary) {
905 self.release_transaction(key, TransactionTerminalState::RolledBack, false)
906 .await;
907 return Ok(());
908 }
909
910 self.release_transaction(key, TransactionTerminalState::RolledBack, true)
911 .await;
912 return Err(self.attach_actor(primary));
913 }
914
915 if commit {
916 self.deferred_bootstrap_committed(key);
917 }
918 self.release_transaction(
919 key,
920 if commit {
921 TransactionTerminalState::Committed
922 } else {
923 TransactionTerminalState::RolledBack
924 },
925 false,
926 )
927 .await;
928 Ok(())
929 }
930
931 #[cfg(feature = "sqlite-local")]
932 async fn record_transaction_control(
933 &self,
934 key: &str,
935 profile: Option<&sqlite_embedded::vfs::SqliteOperationProfile>,
936 duration: Duration,
937 is_commit: bool,
938 ) {
939 let mut state = self.transaction_coordinator.state.lock().await;
940 if let Some(active) = state.active.as_mut().filter(|active| active.key == key) {
941 if let Some(transaction_profile) = &mut active.profile {
942 let default_profile = sqlite_embedded::vfs::SqliteOperationProfile::default();
943 transaction_profile.record_control(
944 profile.unwrap_or(&default_profile),
945 duration.as_nanos().try_into().unwrap_or(u64::MAX),
946 is_commit,
947 );
948 }
949 }
950 }
951
952 async fn expire_transaction(&self, key: &str) -> Result<()> {
953 let db = self.clone();
954 let key = key.to_owned();
955 run_detached_transaction_task(
956 async move { db.expire_transaction_inner(&key).await },
957 "sqlite transaction expiry task failed",
958 )
959 .await
960 }
961
962 async fn expire_transaction_inner(&self, key: &str) -> Result<()> {
963 let (operation, timeout, remote_session) = {
964 let mut state = self.transaction_coordinator.state.lock().await;
965 if state.closed {
966 return Err(transaction_coordinator_closed_error());
967 }
968 if let Some(active) = state.active.as_mut().filter(|active| active.key == key) {
969 active.expiring = true;
970 (
971 Arc::clone(&active.operation),
972 active.timeout,
973 active.remote_session,
974 )
975 } else if let Some(error) = transaction_known_state_error(&state, key) {
976 return Err(error);
977 } else {
978 return Err(transaction_unknown_error(key));
979 }
980 };
981 let _operation = operation.lock().await;
982 {
983 let state = self.transaction_coordinator.state.lock().await;
984 if state.closed {
985 return Err(transaction_coordinator_closed_error());
986 }
987 if !state
988 .active
989 .as_ref()
990 .is_some_and(|active| active.key == key)
991 {
992 if let Some(error) = transaction_known_state_error(&state, key) {
993 return Err(error);
994 }
995 return Err(transaction_unknown_error(key));
996 }
997 }
998 self.transaction_coordinator
1002 .epoch
1003 .fetch_add(1, Ordering::AcqRel);
1004 let rollback = self
1005 .execute_backend_in_session("ROLLBACK".to_owned(), None, remote_session)
1006 .await;
1007 let rollback_failed = rollback.as_ref().is_err_and(|error| {
1008 !is_no_active_transaction_error(error) && !is_remote_connection_error(error)
1009 });
1010 self.transaction_coordinator
1014 .epoch
1015 .fetch_add(1, Ordering::AcqRel);
1016 self.release_transaction(
1017 key,
1018 TransactionTerminalState::Expired(timeout),
1019 rollback_failed,
1020 )
1021 .await;
1022 if let Err(error) = rollback
1023 && rollback_failed
1024 {
1025 return Err(self.attach_actor(error));
1026 }
1027 Ok(())
1028 }
1029
1030 async fn connection_lost_transaction(&self, key: &str, expected_session: u64) -> Result<()> {
1031 let (operation, still_expected) = {
1032 let mut state = self.transaction_coordinator.state.lock().await;
1033 let Some(active) = state.active.as_mut().filter(|active| active.key == key) else {
1034 return Ok(());
1035 };
1036 let still_expected = active.remote_session == Some(expected_session);
1037 if still_expected {
1038 active.expiring = true;
1039 active.connection_lost = true;
1040 }
1041 (Arc::clone(&active.operation), still_expected)
1042 };
1043 if !still_expected {
1044 return Ok(());
1045 }
1046 let _operation = operation.lock().await;
1047
1048 self.release_transaction(key, TransactionTerminalState::ConnectionLost, false)
1054 .await;
1055 Ok(())
1056 }
1057
1058 async fn handle_transaction_backend_error(
1059 &self,
1060 key: &str,
1061 error: anyhow::Error,
1062 ) -> anyhow::Error {
1063 if is_remote_connection_error(&error) {
1064 self.release_transaction(key, TransactionTerminalState::ConnectionLost, false)
1065 .await;
1066 }
1067 self.attach_actor(map_transaction_connection_error(error))
1068 }
1069
1070 pub(super) async fn release_transaction(
1071 &self,
1072 key: &str,
1073 terminal: TransactionTerminalState,
1074 close: bool,
1075 ) {
1076 let mut state = self.transaction_coordinator.state.lock().await;
1077 if !state
1078 .active
1079 .as_ref()
1080 .is_some_and(|active| active.key == key)
1081 {
1082 return;
1083 }
1084 let mut active = state.active.take().expect("active transaction was checked");
1085 if let Some(task) = active.timeout_task.take() {
1086 task.cancel();
1087 }
1088 if let TransactionTerminalState::Expired(timeout) = terminal {
1089 state.last_expired_timeout = Some(timeout);
1090 }
1091 insert_terminal_state(&mut state, key.to_owned(), terminal);
1092 state.closed |= close;
1093 if close {
1094 self.transaction_coordinator.admission.close();
1095 self.transaction_coordinator.closed_token.cancel();
1096 if let Ok(config) = self.runtime_config() {
1097 report_sqlite_worker_fatal(
1098 &self.worker_fatal_reported,
1099 config,
1100 "sqlite transaction rollback failed; coordinator closed".to_owned(),
1101 );
1102 }
1103 }
1104 drop(active.gate_guard.take());
1105 #[cfg(feature = "sqlite-local")]
1106 let profile = active.profile;
1107 drop(state);
1108
1109 #[cfg(feature = "sqlite-local")]
1110 if self.backend() == super::SqliteBackend::LocalNative
1111 && let Some(profile) = profile
1112 {
1113 let (fingerprint, fingerprint_source) = profile.fingerprint();
1114 if self.profiling.selection.tracks_transactions() && profile.name.is_none() {
1115 let warning = UNNAMED_TRANSACTION_WARNINGS.fetch_add(1, Ordering::Relaxed);
1116 if warning == 0 || warning.is_power_of_two() {
1117 tracing::warn!(
1118 fingerprint,
1119 "SQLite transaction is unnamed. Add a static `name` to group transaction, storage, and commit performance across executions. Individual statement profiling remains available."
1120 );
1121 }
1122 }
1123 if let Some(metrics) = &self.vfs_metrics {
1124 let total_ns = profile
1125 .started_at
1126 .elapsed()
1127 .as_nanos()
1128 .try_into()
1129 .unwrap_or(u64::MAX);
1130 let application_time_ns = total_ns
1131 .saturating_sub(profile.transaction_wait_ns)
1132 .saturating_sub(profile.worker_wait_ns)
1133 .saturating_sub(profile.storage_ns)
1134 .saturating_sub(profile.local_work_ns);
1135 let metric = sqlite_embedded::vfs::SqliteTransactionMetric {
1136 fingerprint,
1137 fingerprint_source,
1138 shape_fingerprint: profile.shape_fingerprint(),
1139 statement_fingerprint_hashes: profile.statement_fingerprint_hashes,
1140 omitted_statement_fingerprints: profile.omitted_statement_fingerprints,
1141 storage_transport: "proxy",
1142 outcome: match terminal {
1143 TransactionTerminalState::Committed => "success",
1144 TransactionTerminalState::RolledBack => "rollback",
1145 TransactionTerminalState::Expired(_) => "expired",
1146 TransactionTerminalState::ConnectionLost => "connection_lost",
1147 },
1148 total_ns,
1149 transaction_wait_ns: profile.transaction_wait_ns,
1150 worker_wait_ns: profile.worker_wait_ns,
1151 storage_ns: profile.storage_ns,
1152 local_work_ns: profile.local_work_ns,
1153 application_time_ns,
1154 commit_ns: profile.commit_ns,
1155 get_pages_round_trips: profile.get_pages_round_trips,
1156 statement_count: profile.statement_count,
1157 dirty_pages: profile.dirty_pages,
1158 dirty_bytes: profile.dirty_bytes,
1159 };
1160 if metrics.observe_transaction_profile(&metric)
1161 && self.profiling.mark_cataloged(&metric.fingerprint)
1162 {
1163 metrics.record_fingerprint_catalog(
1164 "transaction",
1165 &metric.fingerprint,
1166 profile.name.as_deref().unwrap_or(&metric.fingerprint),
1167 FINGERPRINT_FORMAT_VERSION,
1168 );
1169 }
1170 if metrics.transaction_diagnostic_selected(&metric) {
1171 let mut diagnostic = metric;
1172 (diagnostic.fingerprint, diagnostic.fingerprint_source) =
1173 profile.diagnostic_fingerprint();
1174 if self.profiling.mark_cataloged(&diagnostic.fingerprint) {
1175 metrics.record_fingerprint_catalog(
1176 "transaction",
1177 &diagnostic.fingerprint,
1178 profile.name.as_deref().unwrap_or(&diagnostic.fingerprint),
1179 FINGERPRINT_FORMAT_VERSION,
1180 );
1181 }
1182 metrics.emit_transaction_diagnostic_event(
1183 self.actor_id.as_deref().unwrap_or("unknown"),
1184 self.generation,
1185 &diagnostic,
1186 );
1187 }
1188 }
1189 }
1190 }
1191
1192 pub(super) async fn shutdown_transaction_coordinator(&self) -> OwnedRwLockWriteGuard<()> {
1193 let active_operation = {
1194 let mut state = self.transaction_coordinator.state.lock().await;
1195 state.closed = true;
1196 self.transaction_coordinator.closed_token.cancel();
1197 state.active.as_ref().map(|active| {
1198 (
1199 active.key.clone(),
1200 Arc::clone(&active.operation),
1201 active.remote_session,
1202 )
1203 })
1204 };
1205 self.transaction_coordinator.admission.close();
1206 if let Some((key, operation, remote_session)) = active_operation {
1207 let _operation = operation.lock().await;
1208 let still_active = {
1209 let state = self.transaction_coordinator.state.lock().await;
1210 state
1211 .active
1212 .as_ref()
1213 .is_some_and(|active| active.key == key)
1214 };
1215 if still_active {
1216 let rollback = self
1217 .execute_backend_in_session("ROLLBACK".to_owned(), None, remote_session)
1218 .await;
1219 let rollback_failed = rollback.as_ref().is_err_and(|error| {
1220 !is_no_active_transaction_error(error) && !is_remote_connection_error(error)
1221 });
1222 if let Err(error) = &rollback
1223 && rollback_failed
1224 {
1225 tracing::error!(%error, "sqlite rollback during coordinator shutdown failed");
1226 }
1227 self.transaction_coordinator
1228 .epoch
1229 .fetch_add(1, Ordering::AcqRel);
1230 self.release_transaction(
1231 &key,
1232 TransactionTerminalState::RolledBack,
1233 rollback_failed,
1234 )
1235 .await;
1236 }
1237 }
1238 Arc::clone(&self.transaction_coordinator.gate)
1239 .write_owned()
1240 .await
1241 }
1242
1243 pub(crate) async fn rollback_manual_transaction(&self) -> Result<bool> {
1251 let Some(explicit_transaction) = self.subscribe_explicit_transaction() else {
1252 return Ok(false);
1253 };
1254 if !*explicit_transaction.borrow() {
1255 return Ok(false);
1256 }
1257 let _gate = Arc::clone(&self.transaction_coordinator.gate)
1258 .write_owned()
1259 .await;
1260 if !*explicit_transaction.borrow() {
1261 return Ok(false);
1262 }
1263
1264 tracing::warn!(
1265 actor_id = self.actor_id.as_deref().unwrap_or("unknown"),
1266 "rolling back manual sqlite transaction left open at shutdown; the action that issued BEGIN did not reach COMMIT or ROLLBACK within the grace period"
1267 );
1268 match self
1269 .execute_backend_in_session("ROLLBACK".to_owned(), None, None)
1270 .await
1271 {
1272 Ok(_) => Ok(true),
1273 Err(error) if is_no_active_transaction_error(&error) => Ok(false),
1274 Err(error) => Err(self
1275 .attach_actor(error)
1276 .context("roll back manual sqlite transaction at shutdown")),
1277 }
1278 }
1279}
1280
1281#[derive(rivet_error::RivetError, Debug, Serialize)]
1282#[error(
1283 "sqlite",
1284 "transaction_queue_full",
1285 "SQLite transaction queue is full.",
1286 "SQLite transaction coordinator queue is full. Limit is 128 operations."
1287)]
1288pub struct TransactionQueueFullError;
1289
1290impl fmt::Display for TransactionQueueFullError {
1291 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1292 f.write_str("sqlite transaction coordinator queue is full")
1293 }
1294}
1295
1296impl Error for TransactionQueueFullError {}
1297
1298#[derive(rivet_error::RivetError, Debug, Serialize)]
1299#[error(
1300 "sqlite",
1301 "manual_transaction_open",
1302 "SQLite manual transaction stayed open.",
1303 "Waited {timeout_ms} ms for a manual SQLite transaction to finish. A BEGIN issued through ordinary statements must reach COMMIT or ROLLBACK before actor state persistence or db.transaction() can start; prefer db.transaction() for multi-statement transactions."
1304)]
1305pub struct ManualTransactionOpenError {
1306 pub timeout_ms: u64,
1307}
1308
1309impl fmt::Display for ManualTransactionOpenError {
1310 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1311 write!(
1312 f,
1313 "waited {} ms for a manual sqlite transaction to finish; a BEGIN issued through ordinary statements must reach COMMIT or ROLLBACK before actor state persistence or db.transaction() can start",
1314 self.timeout_ms
1315 )
1316 }
1317}
1318
1319impl Error for ManualTransactionOpenError {}
1320
1321#[derive(rivet_error::RivetError, Debug, Serialize)]
1322#[error(
1323 "sqlite",
1324 "transaction_closed",
1325 "SQLite transaction coordinator is closed."
1326)]
1327pub struct TransactionCoordinatorClosedError;
1328
1329impl fmt::Display for TransactionCoordinatorClosedError {
1330 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1331 f.write_str("sqlite transaction coordinator is closed")
1332 }
1333}
1334
1335impl Error for TransactionCoordinatorClosedError {}
1336
1337#[derive(rivet_error::RivetError, Debug, Serialize)]
1338#[error(
1339 "sqlite",
1340 "transaction_invalid_argument",
1341 "Invalid SQLite transaction argument.",
1342 "{message}"
1343)]
1344pub struct TransactionInvalidArgumentError {
1345 pub message: &'static str,
1346}
1347
1348impl fmt::Display for TransactionInvalidArgumentError {
1349 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1350 f.write_str(self.message)
1351 }
1352}
1353
1354impl Error for TransactionInvalidArgumentError {}
1355
1356#[derive(rivet_error::RivetError, Debug, Serialize)]
1357#[error(
1358 "sqlite",
1359 "transaction_connection_lost",
1360 "SQLite transaction connection was lost.",
1361 "The Envoy connection that owned this SQLite transaction disconnected. The transaction was rolled back and cannot be resumed; start a new db.transaction()."
1362)]
1363pub struct TransactionConnectionLostError;
1364
1365impl fmt::Display for TransactionConnectionLostError {
1366 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1367 f.write_str(
1368 "the Envoy connection that owned this SQLite transaction disconnected; the transaction was rolled back and cannot be resumed",
1369 )
1370 }
1371}
1372
1373impl Error for TransactionConnectionLostError {}
1374
1375#[derive(rivet_error::RivetError, Debug, Serialize)]
1376#[error(
1377 "sqlite",
1378 "transaction_unknown",
1379 "Unknown SQLite transaction handle.",
1380 "Unknown SQLite transaction handle `{key}`."
1381)]
1382pub struct TransactionUnknownError {
1383 pub key: String,
1384}
1385
1386impl fmt::Display for TransactionUnknownError {
1387 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1388 write!(f, "unknown sqlite transaction handle `{}`", self.key)
1389 }
1390}
1391
1392impl Error for TransactionUnknownError {}
1393
1394#[derive(rivet_error::RivetError, Debug, Serialize)]
1395#[error(
1396 "sqlite",
1397 "transaction_terminal",
1398 "SQLite transaction handle is terminal.",
1399 "SQLite transaction handle `{key}` is already {state}."
1400)]
1401pub struct TransactionTerminalError {
1402 pub key: String,
1403 pub state: &'static str,
1404}
1405
1406impl fmt::Display for TransactionTerminalError {
1407 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1408 write!(
1409 f,
1410 "sqlite transaction handle `{}` is already {}",
1411 self.key, self.state
1412 )
1413 }
1414}
1415
1416impl Error for TransactionTerminalError {}
1417
1418#[derive(rivet_error::RivetError, Debug, Serialize)]
1419#[error(
1420 "sqlite",
1421 "transaction_expired",
1422 "SQLite transaction expired.",
1423 "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."
1424)]
1425pub struct TransactionExpiredError {
1426 pub timeout_ms: u64,
1427}
1428
1429impl fmt::Display for TransactionExpiredError {
1430 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1431 write!(
1432 f,
1433 "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",
1434 self.timeout_ms
1435 )
1436 }
1437}
1438
1439impl Error for TransactionExpiredError {}
1440
1441impl TransactionExpiredError {
1442 fn owner(_key: &str, timeout: Duration) -> Self {
1443 Self {
1444 timeout_ms: duration_millis(timeout),
1445 }
1446 }
1447
1448 fn parked(timeout: Duration) -> Self {
1449 Self {
1450 timeout_ms: duration_millis(timeout),
1451 }
1452 }
1453}
1454
1455fn duration_millis(duration: Duration) -> u64 {
1456 duration.as_millis().try_into().unwrap_or(u64::MAX)
1457}
1458
1459async fn sleep_transaction_timeout(mut timeout: Duration) {
1460 const MAX_TIMER_WAIT: Duration = Duration::from_millis(2_000_000_000);
1463 while timeout > MAX_TIMER_WAIT {
1464 crate::time::sleep(MAX_TIMER_WAIT).await;
1465 timeout = timeout.saturating_sub(MAX_TIMER_WAIT);
1466 }
1467 crate::time::sleep(timeout).await;
1468}
1469
1470fn spawn_transaction_timeout(
1471 db: SqliteDb,
1472 key: String,
1473 timeout: Duration,
1474 cancel: CancellationToken,
1475 remote_session: Option<u64>,
1476) {
1477 let task = async move {
1478 let connection_lost = async {
1479 let Some(expected) = remote_session else {
1480 std::future::pending::<()>().await;
1481 return;
1482 };
1483 let Ok(handle) = db.handle() else {
1484 return;
1485 };
1486 let mut sessions = handle.subscribe_connection_session();
1487 loop {
1488 if *sessions.borrow_and_update() != expected {
1489 return;
1490 }
1491 if sessions.changed().await.is_err() {
1492 return;
1493 }
1494 }
1495 };
1496 tokio::pin!(connection_lost);
1497
1498 let expired = tokio::select! {
1499 _ = cancel.cancelled() => return,
1500 _ = sleep_transaction_timeout(timeout) => true,
1501 _ = &mut connection_lost => false,
1502 };
1503 let result = if expired {
1504 db.expire_transaction(&key).await
1505 } else {
1506 db.connection_lost_transaction(&key, remote_session.expect("checked above"))
1513 .await
1514 };
1515 if let Err(error) = result {
1516 if error.downcast_ref::<TransactionTerminalError>().is_none() {
1517 tracing::error!(%error, "sqlite transaction terminal cleanup failed");
1518 }
1519 }
1520 };
1521
1522 #[cfg(target_arch = "wasm32")]
1523 wasm_bindgen_futures::spawn_local(task);
1524
1525 #[cfg(not(target_arch = "wasm32"))]
1526 RuntimeSpawner::spawn(task);
1527}
1528
1529#[cfg(not(target_arch = "wasm32"))]
1530pub(super) async fn run_detached_transaction_task<F, T>(
1531 future: F,
1532 context: &'static str,
1533) -> Result<T>
1534where
1535 F: Future<Output = Result<T>> + Send + 'static,
1536 T: Send + 'static,
1537{
1538 RuntimeSpawner::spawn(future).await.context(context)?
1539}
1540
1541#[cfg(target_arch = "wasm32")]
1542pub(super) async fn run_detached_transaction_task<F, T>(
1543 future: F,
1544 context: &'static str,
1545) -> Result<T>
1546where
1547 F: Future<Output = Result<T>> + 'static,
1548 T: 'static,
1549{
1550 let (response_tx, response_rx) = oneshot::channel();
1551 wasm_bindgen_futures::spawn_local(async move {
1552 let _ = response_tx.send(future.await);
1553 });
1554 response_rx.await.context(context)?
1555}
1556
1557pub(super) fn insert_terminal_state(
1558 state: &mut TransactionCoordinatorState,
1559 key: String,
1560 terminal: TransactionTerminalState,
1561) {
1562 if let TransactionTerminalState::Expired(timeout) = terminal {
1566 let expired = ExpiredKey {
1567 timeout,
1568 _metric: rivet_metrics::GaugeGuardExt::inc_guard(&EXPIRED_KEY_METRICS.keys),
1569 };
1570 if state.poisoned.insert(key.clone(), expired).is_none() {
1571 state.poisoned_order.push_back(key);
1572 if state.poisoned_order.len() == TRANSACTION_EXPIRED_CAPACITY + 1 {
1573 tracing::warn!(
1574 capacity = TRANSACTION_EXPIRED_CAPACITY,
1575 "expired sqlite transaction keys reached capacity; transactions keep timing out, and the oldest stale handles will report unknown transaction"
1576 );
1577 }
1578 }
1579 while state.poisoned_order.len() > TRANSACTION_EXPIRED_CAPACITY {
1580 let Some(evicted_key) = state.poisoned_order.pop_front() else {
1581 break;
1582 };
1583 if state.poisoned.remove(&evicted_key).is_some() {
1584 EXPIRED_KEY_METRICS.evicted_total.inc();
1585 }
1586 }
1587 return;
1588 }
1589 if state.terminal.insert(key.clone(), terminal).is_none() {
1590 state.terminal_order.push_back(key);
1591 }
1592 while state.terminal_order.len() > TRANSACTION_TERMINAL_CAPACITY {
1593 if let Some(expired_key) = state.terminal_order.pop_front() {
1594 state.terminal.remove(&expired_key);
1595 }
1596 }
1597}
1598
1599fn transaction_known_state_error(
1600 state: &TransactionCoordinatorState,
1601 key: &str,
1602) -> Option<anyhow::Error> {
1603 if let Some(timeout) = state.poisoned.get(key).map(|expired| expired.timeout) {
1604 return Some(transaction_expired_owner_error(key, timeout));
1605 }
1606 state
1607 .terminal
1608 .get(key)
1609 .copied()
1610 .map(|terminal| transaction_terminal_error(key, terminal))
1611}
1612
1613fn is_no_active_transaction_error(error: &anyhow::Error) -> bool {
1614 error.chain().any(|cause| {
1622 cause
1623 .to_string()
1624 .to_ascii_lowercase()
1625 .contains("no transaction is active")
1626 })
1627}
1628
1629fn transaction_queue_full_error() -> anyhow::Error {
1630 TransactionQueueFullError
1631 .build()
1632 .context(TransactionQueueFullError)
1633}
1634
1635fn manual_transaction_open_error(timeout: Duration) -> anyhow::Error {
1636 let error = || ManualTransactionOpenError {
1637 timeout_ms: duration_millis(timeout),
1638 };
1639 error().build().context(error())
1640}
1641
1642fn transaction_coordinator_closed_error() -> anyhow::Error {
1643 TransactionCoordinatorClosedError
1644 .build()
1645 .context(TransactionCoordinatorClosedError)
1646}
1647
1648fn transaction_unknown_error(key: &str) -> anyhow::Error {
1649 TransactionUnknownError {
1650 key: key.to_owned(),
1651 }
1652 .build()
1653 .context(TransactionUnknownError {
1654 key: key.to_owned(),
1655 })
1656}
1657
1658fn transaction_expired_error(timeout: Duration) -> anyhow::Error {
1659 TransactionExpiredError::parked(timeout)
1660 .build()
1661 .context(TransactionExpiredError::parked(timeout))
1662}
1663
1664fn transaction_expired_owner_error(key: &str, timeout: Duration) -> anyhow::Error {
1665 TransactionExpiredError::owner(key, timeout)
1666 .build()
1667 .context(TransactionExpiredError::owner(key, timeout))
1668}
1669
1670fn transaction_terminal_error(key: &str, terminal: TransactionTerminalState) -> anyhow::Error {
1671 match terminal {
1672 TransactionTerminalState::Expired(timeout) => transaction_expired_owner_error(key, timeout),
1673 TransactionTerminalState::Committed => transaction_terminal_state_error(key, "committed"),
1674 TransactionTerminalState::RolledBack => {
1675 transaction_terminal_state_error(key, "rolled back")
1676 }
1677 TransactionTerminalState::ConnectionLost => transaction_connection_lost_error(),
1678 }
1679}
1680
1681fn transaction_invalid_argument_error(message: &'static str) -> anyhow::Error {
1682 TransactionInvalidArgumentError { message }
1683 .build()
1684 .context(TransactionInvalidArgumentError { message })
1685}
1686
1687fn transaction_connection_lost_error() -> anyhow::Error {
1688 TransactionConnectionLostError
1689 .build()
1690 .context(TransactionConnectionLostError)
1691}
1692
1693fn is_remote_connection_error(error: &anyhow::Error) -> bool {
1694 error
1695 .downcast_ref::<super::RemoteSqliteConnectionSessionLostError>()
1696 .is_some()
1697 || error
1698 .downcast_ref::<super::RemoteSqliteIndeterminateResultError>()
1699 .is_some()
1700}
1701
1702fn map_transaction_connection_error(error: anyhow::Error) -> anyhow::Error {
1703 if error
1704 .downcast_ref::<super::RemoteSqliteConnectionSessionLostError>()
1705 .is_some()
1706 {
1707 return transaction_connection_lost_error();
1708 }
1709 super::remote_request_error(error)
1710}
1711
1712fn transaction_terminal_state_error(key: &str, state: &'static str) -> anyhow::Error {
1713 TransactionTerminalError {
1714 key: key.to_owned(),
1715 state,
1716 }
1717 .build()
1718 .context(TransactionTerminalError {
1719 key: key.to_owned(),
1720 state,
1721 })
1722}