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
26#[cfg(feature = "sqlite-local")]
27use super::profiling::{FINGERPRINT_FORMAT_VERSION, TransactionProfile};
28use super::{BindParam, ExecuteResult, QueryResult, SqliteDb, report_sqlite_worker_fatal};
29
30pub const DEFAULT_TRANSACTION_TIMEOUT: Duration = Duration::from_secs(60);
31#[cfg(not(feature = "sqlite-local"))]
32const MAX_TRANSACTION_NAME_BYTES: usize = 128;
33pub const TRANSACTION_COORDINATOR_QUEUE_CAPACITY: usize = 128;
34pub(super) const TRANSACTION_TERMINAL_CAPACITY: usize = 1024;
35#[cfg(feature = "sqlite-local")]
36static UNNAMED_TRANSACTION_WARNINGS: AtomicU64 = AtomicU64::new(0);
37
38#[derive(Clone)]
39pub struct SqliteTransaction {
40 db: SqliteDb,
41 key: String,
42}
43
44impl SqliteTransaction {
45 pub fn key(&self) -> &str {
46 &self.key
47 }
48
49 pub async fn exec(&self, sql: impl Into<String>) -> Result<QueryResult> {
50 self.db.transaction_exec(&self.key, sql.into()).await
51 }
52
53 pub async fn execute(
54 &self,
55 sql: impl Into<String>,
56 params: Option<Vec<BindParam>>,
57 ) -> Result<ExecuteResult> {
58 self.db
59 .transaction_execute(&self.key, sql.into(), params)
60 .await
61 }
62
63 pub async fn commit(&self) -> Result<()> {
64 self.db.finish_transaction(&self.key, true).await
65 }
66
67 pub async fn rollback(&self) -> Result<()> {
68 self.db.finish_transaction(&self.key, false).await
69 }
70
71 pub async fn expire(&self) -> Result<()> {
72 self.db.expire_transaction(&self.key).await
73 }
74}
75
76pub(super) struct TransactionCoordinator {
77 pub(super) gate: Arc<RwLock<()>>,
78 pub(super) admission: Arc<Semaphore>,
79 pub(super) state: AsyncMutex<TransactionCoordinatorState>,
80 epoch: AtomicU64,
81 waiters: AtomicU64,
82}
83
84pub(super) struct TransactionCoordinatorState {
85 pub(super) active: Option<ActiveTransaction>,
86 pub(super) terminal: BTreeMap<String, TransactionTerminalState>,
87 pub(super) terminal_order: VecDeque<String>,
88 pub(super) poisoned: BTreeMap<String, Duration>,
89 pub(super) last_expired_timeout: Option<Duration>,
90 pub(super) closed: bool,
91}
92
93pub(super) struct ActiveTransaction {
94 key: String,
95 timeout: Duration,
96 pub(super) expiring: bool,
97 connection_lost: bool,
98 remote_session: Option<u64>,
99 gate_guard: Option<OwnedRwLockWriteGuard<()>>,
100 operation: Arc<AsyncMutex<()>>,
101 timeout_task: Option<CancellationToken>,
102 #[cfg(feature = "sqlite-local")]
103 profile: Option<TransactionProfile>,
104}
105
106#[derive(Clone, Copy)]
107pub(super) enum TransactionTerminalState {
108 Committed,
109 RolledBack,
110 Expired(Duration),
111 ConnectionLost,
112}
113
114pub(super) struct RegularOperationGuard {
115 _gate: OwnedRwLockReadGuard<()>,
116 _permit: OwnedSemaphorePermit,
117}
118
119struct CoordinatorWaitGuard<'a> {
120 db: &'a SqliteDb,
121}
122
123impl<'a> CoordinatorWaitGuard<'a> {
124 #[cfg(feature = "sqlite-local")]
125 fn new(db: &'a SqliteDb) -> Option<Self> {
126 if !db.profiling.config.enabled {
127 return None;
128 }
129 let metrics = db.vfs_metrics.as_ref()?;
130 let _depth = db
131 .transaction_coordinator
132 .waiters
133 .fetch_add(1, Ordering::AcqRel)
134 .saturating_add(1);
135 metrics.set_coordinator_queue_depth(_depth);
136 Some(Self { db })
137 }
138
139 #[cfg(not(feature = "sqlite-local"))]
140 fn new(_db: &'a SqliteDb) -> Option<Self> {
141 None
142 }
143}
144
145impl Drop for CoordinatorWaitGuard<'_> {
146 fn drop(&mut self) {
147 let _depth = self
148 .db
149 .transaction_coordinator
150 .waiters
151 .fetch_sub(1, Ordering::AcqRel)
152 .saturating_sub(1);
153 #[cfg(feature = "sqlite-local")]
154 if let Some(metrics) = &self.db.vfs_metrics {
155 metrics.set_coordinator_queue_depth(_depth);
156 }
157 }
158}
159
160impl Default for TransactionCoordinator {
161 fn default() -> Self {
162 Self {
163 gate: Arc::new(RwLock::new(())),
164 admission: Arc::new(Semaphore::new(TRANSACTION_COORDINATOR_QUEUE_CAPACITY)),
165 state: AsyncMutex::new(TransactionCoordinatorState {
166 active: None,
167 terminal: BTreeMap::new(),
168 terminal_order: VecDeque::new(),
169 poisoned: BTreeMap::new(),
170 last_expired_timeout: None,
171 closed: false,
172 }),
173 epoch: AtomicU64::new(0),
174 waiters: AtomicU64::new(0),
175 }
176 }
177}
178
179impl SqliteDb {
180 pub(super) fn try_transaction_admission(&self) -> Result<OwnedSemaphorePermit> {
181 match Arc::clone(&self.transaction_coordinator.admission).try_acquire_owned() {
182 Ok(permit) => Ok(permit),
183 Err(TryAcquireError::NoPermits) => Err(transaction_queue_full_error()),
184 Err(TryAcquireError::Closed) => Err(transaction_coordinator_closed_error()),
185 }
186 }
187
188 pub(super) async fn begin_regular_operation(&self) -> Result<RegularOperationGuard> {
189 let epoch = self.transaction_coordinator.epoch.load(Ordering::Acquire);
190 let permit = self.try_transaction_admission()?;
191 let wait = CoordinatorWaitGuard::new(self);
192 let gate = Arc::clone(&self.transaction_coordinator.gate)
193 .read_owned()
194 .await;
195 drop(wait);
196 let state = self.transaction_coordinator.state.lock().await;
197 if state.closed {
198 return Err(transaction_coordinator_closed_error());
199 }
200 if self.transaction_coordinator.epoch.load(Ordering::Acquire) != epoch {
201 return Err(transaction_expired_error(
202 state
203 .last_expired_timeout
204 .unwrap_or(DEFAULT_TRANSACTION_TIMEOUT),
205 ));
206 }
207 drop(state);
208 Ok(RegularOperationGuard {
209 _gate: gate,
210 _permit: permit,
211 })
212 }
213
214 pub async fn begin_transaction(&self, timeout: Option<Duration>) -> Result<SqliteTransaction> {
215 self.begin_named_transaction(None, timeout).await
216 }
217
218 pub async fn begin_named_transaction(
219 &self,
220 name: Option<&str>,
221 timeout: Option<Duration>,
222 ) -> Result<SqliteTransaction> {
223 #[cfg(feature = "sqlite-local")]
224 let max_name_bytes = self.profiling.config.max_transaction_name_bytes;
225 #[cfg(not(feature = "sqlite-local"))]
226 let max_name_bytes = MAX_TRANSACTION_NAME_BYTES;
227 if let Some(name) = name {
228 if name.is_empty() {
229 return Err(transaction_invalid_argument_error(
230 "transaction name must not be empty",
231 ));
232 }
233 if name.len() > max_name_bytes {
234 return Err(transaction_invalid_argument_error(
235 "transaction name exceeds the configured byte limit",
236 ));
237 }
238 }
239 self.begin_transaction_with_key_and_name(
240 uuid::Uuid::new_v4().to_string(),
241 name.map(ToOwned::to_owned),
242 timeout,
243 )
244 .await
245 }
246
247 pub async fn begin_transaction_with_key(
248 &self,
249 key: impl Into<String>,
250 timeout: Option<Duration>,
251 ) -> Result<SqliteTransaction> {
252 self.begin_transaction_with_key_and_name(key, None, timeout)
253 .await
254 }
255
256 async fn begin_transaction_with_key_and_name(
257 &self,
258 key: impl Into<String>,
259 name: Option<String>,
260 timeout: Option<Duration>,
261 ) -> Result<SqliteTransaction> {
262 #[cfg(feature = "sqlite-local")]
263 let started_at = (self.profiling.config.enabled
264 && self.backend() == super::SqliteBackend::LocalNative)
265 .then(crate::time::Instant::now);
266 let key = key.into();
267 let timeout = timeout.unwrap_or(DEFAULT_TRANSACTION_TIMEOUT);
268 if key.is_empty() {
269 return Err(transaction_invalid_argument_error(
270 "transaction key must not be empty",
271 ));
272 }
273 if timeout.is_zero() {
274 return Err(transaction_invalid_argument_error(
275 "transaction timeout must be greater than zero",
276 ));
277 }
278
279 let db = self.clone();
280 run_detached_transaction_task(
281 async move {
282 db.begin_transaction_profiled_inner(
283 key,
284 timeout,
285 name,
286 #[cfg(feature = "sqlite-local")]
287 started_at,
288 )
289 .await
290 },
291 "sqlite transaction begin task failed",
292 )
293 .await
294 }
295
296 #[cfg(test)]
297 pub(super) async fn begin_transaction_inner(
298 &self,
299 key: String,
300 timeout: Duration,
301 ) -> Result<SqliteTransaction> {
302 self.begin_transaction_profiled_inner(
303 key,
304 timeout,
305 None,
306 #[cfg(feature = "sqlite-local")]
307 (self.profiling.config.enabled && self.backend() == super::SqliteBackend::LocalNative)
308 .then(crate::time::Instant::now),
309 )
310 .await
311 }
312
313 async fn begin_transaction_profiled_inner(
314 &self,
315 key: String,
316 timeout: Duration,
317 _name: Option<String>,
318 #[cfg(feature = "sqlite-local")] started_at: Option<crate::time::Instant>,
319 ) -> Result<SqliteTransaction> {
320 #[cfg(feature = "sqlite-local")]
321 let transaction_wait_started_at = started_at.map(|_| crate::time::Instant::now());
322 let epoch = self.transaction_coordinator.epoch.load(Ordering::Acquire);
323 let permit = self.try_transaction_admission()?;
324 let wait = CoordinatorWaitGuard::new(self);
325 let gate_guard = Arc::clone(&self.transaction_coordinator.gate)
326 .write_owned()
327 .await;
328 drop(wait);
329 #[cfg(feature = "sqlite-local")]
330 let transaction_wait = transaction_wait_started_at.map(|started| started.elapsed());
331 {
332 let state = self.transaction_coordinator.state.lock().await;
333 if state.closed {
334 return Err(transaction_coordinator_closed_error());
335 }
336 if self.transaction_coordinator.epoch.load(Ordering::Acquire) != epoch {
337 return Err(transaction_expired_error(
338 state
339 .last_expired_timeout
340 .unwrap_or(DEFAULT_TRANSACTION_TIMEOUT),
341 ));
342 }
343 if let Some(error) = transaction_known_state_error(&state, &key) {
344 return Err(error);
345 }
346 }
347
348 #[cfg(feature = "sqlite-local")]
349 let begin_started_at = started_at.map(|_| crate::time::Instant::now());
350 #[cfg(feature = "sqlite-local")]
351 let (begin_result, begin_profile) = if begin_started_at.is_some() {
352 let profiled = self
353 .execute_backend_profiled("BEGIN".to_owned(), None)
354 .await;
355 (
356 profiled.result.map(|result| (result, None)),
357 profiled.profile,
358 )
359 } else {
360 (
361 self.execute_backend_in_session("BEGIN".to_owned(), None, None)
362 .await,
363 None,
364 )
365 };
366 #[cfg(feature = "sqlite-local")]
367 let begin_duration = begin_started_at.map(|started| started.elapsed());
368 #[cfg(not(feature = "sqlite-local"))]
369 let begin_result = self
370 .execute_backend_in_session("BEGIN".to_owned(), None, None)
371 .await;
372 let (_, remote_session) = begin_result.map_err(map_transaction_connection_error)?;
373 if let Some(session) = remote_session
379 && self.handle()?.connection_session() != Some(session)
380 {
381 return Err(transaction_connection_lost_error());
382 }
383 let operation = Arc::new(AsyncMutex::new(()));
384 {
385 let mut state = self.transaction_coordinator.state.lock().await;
386 if state.closed {
387 drop(state);
388 if let Err(error) = self
389 .execute_backend_in_session("ROLLBACK".to_owned(), None, remote_session)
390 .await
391 {
392 tracing::error!(%error, "sqlite rollback after begin raced shutdown failed");
393 }
394 return Err(transaction_coordinator_closed_error());
395 }
396 state.active = Some(ActiveTransaction {
397 key: key.clone(),
398 timeout,
399 expiring: false,
400 connection_lost: false,
401 remote_session,
402 gate_guard: Some(gate_guard),
403 operation,
404 timeout_task: None,
405 #[cfg(feature = "sqlite-local")]
406 profile: started_at.map(|started_at| {
407 let mut profile = TransactionProfile::new(
408 _name,
409 started_at,
410 self.profiling.config.max_statements_per_transaction_trace,
411 );
412 profile.transaction_wait_ns = transaction_wait
413 .unwrap_or_default()
414 .as_nanos()
415 .try_into()
416 .unwrap_or(u64::MAX);
417 let default_profile = depot_client::vfs::SqliteOperationProfile::default();
418 profile.record_control(
419 begin_profile.as_ref().unwrap_or(&default_profile),
420 begin_duration
421 .unwrap_or_default()
422 .as_nanos()
423 .try_into()
424 .unwrap_or(u64::MAX),
425 false,
426 );
427 profile
428 }),
429 });
430 }
431 drop(permit);
432
433 let timeout_task = CancellationToken::new();
434 let mut state = self.transaction_coordinator.state.lock().await;
435 if let Some(active) = state.active.as_mut().filter(|active| active.key == key) {
436 active.timeout_task = Some(timeout_task.clone());
437 } else {
438 timeout_task.cancel();
439 }
440 drop(state);
441 spawn_transaction_timeout(
442 self.clone(),
443 key.clone(),
444 timeout,
445 timeout_task,
446 remote_session,
447 );
448
449 Ok(SqliteTransaction {
450 db: self.clone(),
451 key,
452 })
453 }
454
455 async fn transaction_operation(&self, key: &str) -> Result<(Arc<AsyncMutex<()>>, Option<u64>)> {
456 let state = self.transaction_coordinator.state.lock().await;
457 if state.closed {
458 return Err(transaction_coordinator_closed_error());
459 }
460 if let Some(active) = state.active.as_ref().filter(|active| active.key == key) {
461 if active.expiring {
462 if active.connection_lost {
463 return Err(transaction_connection_lost_error());
464 }
465 return Err(transaction_expired_owner_error(key, active.timeout));
466 }
467 return Ok((Arc::clone(&active.operation), active.remote_session));
468 }
469 if let Some(error) = transaction_known_state_error(&state, key) {
470 return Err(error);
471 }
472 Err(transaction_unknown_error(key))
473 }
474
475 async fn ensure_active_transaction(&self, key: &str) -> Result<()> {
476 let state = self.transaction_coordinator.state.lock().await;
477 if state.closed {
478 return Err(transaction_coordinator_closed_error());
479 }
480 if let Some(active) = state.active.as_ref().filter(|active| active.key == key) {
481 if active.expiring {
482 if active.connection_lost {
483 return Err(transaction_connection_lost_error());
484 }
485 return Err(transaction_expired_owner_error(key, active.timeout));
486 }
487 return Ok(());
488 }
489 if let Some(error) = transaction_known_state_error(&state, key) {
490 return Err(error);
491 }
492 Err(transaction_unknown_error(key))
493 }
494
495 async fn transaction_exec(&self, key: &str, sql: String) -> Result<QueryResult> {
496 let db = self.clone();
497 let key = key.to_owned();
498 run_detached_transaction_task(
499 async move { db.transaction_exec_inner(&key, sql).await },
500 "sqlite transaction exec task failed",
501 )
502 .await
503 }
504
505 async fn transaction_exec_inner(&self, key: &str, sql: String) -> Result<QueryResult> {
506 #[cfg(feature = "sqlite-local")]
507 let started_at = (self.profiling.config.enabled
508 && self.backend() == super::SqliteBackend::LocalNative)
509 .then(crate::time::Instant::now);
510 let (operation, remote_session) = self.transaction_operation(key).await?;
511 let _operation = operation.lock().await;
512 #[cfg(feature = "sqlite-local")]
513 let transaction_wait = started_at.map(|started| started.elapsed());
514 self.ensure_active_transaction(key).await?;
515 #[cfg(feature = "sqlite-local")]
516 if let Some(started_at) = started_at {
517 let sql_for_profile = sql.clone();
518 let profiled = self.exec_backend_profiled(sql).await;
519 let result = profiled.result;
520 let profile = profiled.profile;
521 if let Some(observation) = self.observe_statement_profile(
522 &sql_for_profile,
523 started_at,
524 transaction_wait.unwrap_or_default(),
525 profile,
526 if result.is_ok() { "success" } else { "error" },
527 "explicit",
528 ) {
529 self.record_transaction_statement(key, &observation).await;
530 }
531 return match result {
532 Ok(result) => Ok(result),
533 Err(error) => Err(self.handle_transaction_backend_error(key, error).await),
534 };
535 }
536 match self.exec_backend_in_session(sql, remote_session).await {
537 Ok((result, _)) => Ok(result),
538 Err(error) => Err(self.handle_transaction_backend_error(key, error).await),
539 }
540 }
541
542 async fn transaction_execute(
543 &self,
544 key: &str,
545 sql: String,
546 params: Option<Vec<BindParam>>,
547 ) -> Result<ExecuteResult> {
548 let db = self.clone();
549 let key = key.to_owned();
550 run_detached_transaction_task(
551 async move { db.transaction_execute_inner(&key, sql, params).await },
552 "sqlite transaction execute task failed",
553 )
554 .await
555 }
556
557 async fn transaction_execute_inner(
558 &self,
559 key: &str,
560 sql: String,
561 params: Option<Vec<BindParam>>,
562 ) -> Result<ExecuteResult> {
563 #[cfg(feature = "sqlite-local")]
564 let started_at = (self.profiling.config.enabled
565 && self.backend() == super::SqliteBackend::LocalNative)
566 .then(crate::time::Instant::now);
567 let (operation, remote_session) = self.transaction_operation(key).await?;
568 let _operation = operation.lock().await;
569 #[cfg(feature = "sqlite-local")]
570 let transaction_wait = started_at.map(|started| started.elapsed());
571 self.ensure_active_transaction(key).await?;
572 #[cfg(feature = "sqlite-local")]
573 if let Some(started_at) = started_at {
574 let sql_for_profile = sql.clone();
575 let profiled = self.execute_backend_profiled(sql, params).await;
576 let result = profiled.result;
577 let profile = profiled.profile;
578 if let Some(observation) = self.observe_statement_profile(
579 &sql_for_profile,
580 started_at,
581 transaction_wait.unwrap_or_default(),
582 profile,
583 if result.is_ok() { "success" } else { "error" },
584 "explicit",
585 ) {
586 self.record_transaction_statement(key, &observation).await;
587 }
588 return match result {
589 Ok(result) => Ok(result),
590 Err(error) => Err(self.handle_transaction_backend_error(key, error).await),
591 };
592 }
593 match self
594 .execute_backend_in_session(sql, params, remote_session)
595 .await
596 {
597 Ok((result, _)) => Ok(result),
598 Err(error) => Err(self.handle_transaction_backend_error(key, error).await),
599 }
600 }
601
602 #[cfg(feature = "sqlite-local")]
603 async fn record_transaction_statement(
604 &self,
605 key: &str,
606 observation: &super::profiling::StatementObservation,
607 ) {
608 let mut state = self.transaction_coordinator.state.lock().await;
609 if let Some(active) = state.active.as_mut().filter(|active| active.key == key) {
610 if let Some(profile) = &mut active.profile {
611 profile.record_statement(observation);
612 }
613 }
614 }
615
616 async fn finish_transaction(&self, key: &str, commit: bool) -> Result<()> {
617 let db = self.clone();
618 let key = key.to_owned();
619 run_detached_transaction_task(
620 async move { db.finish_transaction_inner(&key, commit).await },
621 "sqlite transaction finish task failed",
622 )
623 .await
624 }
625
626 async fn finish_transaction_inner(&self, key: &str, commit: bool) -> Result<()> {
627 let (operation, remote_session) = self.transaction_operation(key).await?;
628 let _operation = operation.lock().await;
629 self.ensure_active_transaction(key).await?;
630
631 let statement = if commit { "COMMIT" } else { "ROLLBACK" };
632 #[cfg(feature = "sqlite-local")]
633 let control_started_at = (self.profiling.config.enabled
634 && self.backend() == super::SqliteBackend::LocalNative)
635 .then(crate::time::Instant::now);
636 #[cfg(feature = "sqlite-local")]
637 let (result, control_profile) = if control_started_at.is_some() {
638 let profiled = self
639 .execute_backend_profiled(statement.to_owned(), None)
640 .await;
641 (
642 profiled.result.map(|result| (result, None)),
643 profiled.profile,
644 )
645 } else {
646 (
647 self.execute_backend_in_session(statement.to_owned(), None, remote_session)
648 .await,
649 None,
650 )
651 };
652 #[cfg(not(feature = "sqlite-local"))]
653 let result = self
654 .execute_backend_in_session(statement.to_owned(), None, remote_session)
655 .await;
656 #[cfg(feature = "sqlite-local")]
657 if let Some(control_started_at) = control_started_at {
658 self.record_transaction_control(
659 key,
660 control_profile.as_ref(),
661 control_started_at.elapsed(),
662 commit,
663 )
664 .await;
665 }
666 if let Err(primary) = result {
667 if is_remote_connection_error(&primary) {
668 self.release_transaction(key, TransactionTerminalState::ConnectionLost, false)
669 .await;
670 return Err(self.attach_actor(map_transaction_connection_error(primary)));
671 }
672 if commit {
673 let rollback_result = self
674 .execute_backend_in_session("ROLLBACK".to_owned(), None, remote_session)
675 .await;
676 let rollback_failed = rollback_result
677 .as_ref()
678 .is_err_and(|error| !is_no_active_transaction_error(error));
679 self.release_transaction(
680 key,
681 TransactionTerminalState::RolledBack,
682 rollback_failed,
683 )
684 .await;
685 if let Err(rollback) = rollback_result
686 && rollback_failed
687 {
688 tracing::error!(%rollback, "sqlite rollback after failed commit failed");
689 }
690 return Err(self.attach_actor(primary));
691 }
692 if is_no_active_transaction_error(&primary) {
693 self.release_transaction(key, TransactionTerminalState::RolledBack, false)
694 .await;
695 return Ok(());
696 }
697
698 self.release_transaction(key, TransactionTerminalState::RolledBack, true)
699 .await;
700 return Err(self.attach_actor(primary));
701 }
702
703 self.release_transaction(
704 key,
705 if commit {
706 TransactionTerminalState::Committed
707 } else {
708 TransactionTerminalState::RolledBack
709 },
710 false,
711 )
712 .await;
713 Ok(())
714 }
715
716 #[cfg(feature = "sqlite-local")]
717 async fn record_transaction_control(
718 &self,
719 key: &str,
720 profile: Option<&depot_client::vfs::SqliteOperationProfile>,
721 duration: Duration,
722 is_commit: bool,
723 ) {
724 let mut state = self.transaction_coordinator.state.lock().await;
725 if let Some(active) = state.active.as_mut().filter(|active| active.key == key) {
726 if let Some(transaction_profile) = &mut active.profile {
727 let default_profile = depot_client::vfs::SqliteOperationProfile::default();
728 transaction_profile.record_control(
729 profile.unwrap_or(&default_profile),
730 duration.as_nanos().try_into().unwrap_or(u64::MAX),
731 is_commit,
732 );
733 }
734 }
735 }
736
737 async fn expire_transaction(&self, key: &str) -> Result<()> {
738 let db = self.clone();
739 let key = key.to_owned();
740 run_detached_transaction_task(
741 async move { db.expire_transaction_inner(&key).await },
742 "sqlite transaction expiry task failed",
743 )
744 .await
745 }
746
747 async fn expire_transaction_inner(&self, key: &str) -> Result<()> {
748 let (operation, timeout, remote_session) = {
749 let mut state = self.transaction_coordinator.state.lock().await;
750 if state.closed {
751 return Err(transaction_coordinator_closed_error());
752 }
753 if let Some(active) = state.active.as_mut().filter(|active| active.key == key) {
754 active.expiring = true;
755 (
756 Arc::clone(&active.operation),
757 active.timeout,
758 active.remote_session,
759 )
760 } else if let Some(error) = transaction_known_state_error(&state, key) {
761 return Err(error);
762 } else {
763 return Err(transaction_unknown_error(key));
764 }
765 };
766 let _operation = operation.lock().await;
767 {
768 let state = self.transaction_coordinator.state.lock().await;
769 if state.closed {
770 return Err(transaction_coordinator_closed_error());
771 }
772 if !state
773 .active
774 .as_ref()
775 .is_some_and(|active| active.key == key)
776 {
777 if let Some(error) = transaction_known_state_error(&state, key) {
778 return Err(error);
779 }
780 return Err(transaction_unknown_error(key));
781 }
782 }
783 self.transaction_coordinator
787 .epoch
788 .fetch_add(1, Ordering::AcqRel);
789 let rollback = self
790 .execute_backend_in_session("ROLLBACK".to_owned(), None, remote_session)
791 .await;
792 let rollback_failed = rollback.as_ref().is_err_and(|error| {
793 !is_no_active_transaction_error(error) && !is_remote_connection_error(error)
794 });
795 self.transaction_coordinator
799 .epoch
800 .fetch_add(1, Ordering::AcqRel);
801 self.release_transaction(
802 key,
803 TransactionTerminalState::Expired(timeout),
804 rollback_failed,
805 )
806 .await;
807 if let Err(error) = rollback
808 && rollback_failed
809 {
810 return Err(self.attach_actor(error));
811 }
812 Ok(())
813 }
814
815 async fn connection_lost_transaction(&self, key: &str, expected_session: u64) -> Result<()> {
816 let (operation, still_expected) = {
817 let mut state = self.transaction_coordinator.state.lock().await;
818 let Some(active) = state.active.as_mut().filter(|active| active.key == key) else {
819 return Ok(());
820 };
821 let still_expected = active.remote_session == Some(expected_session);
822 if still_expected {
823 active.expiring = true;
824 active.connection_lost = true;
825 }
826 (Arc::clone(&active.operation), still_expected)
827 };
828 if !still_expected {
829 return Ok(());
830 }
831 let _operation = operation.lock().await;
832
833 self.release_transaction(key, TransactionTerminalState::ConnectionLost, false)
839 .await;
840 Ok(())
841 }
842
843 async fn handle_transaction_backend_error(
844 &self,
845 key: &str,
846 error: anyhow::Error,
847 ) -> anyhow::Error {
848 if is_remote_connection_error(&error) {
849 self.release_transaction(key, TransactionTerminalState::ConnectionLost, false)
850 .await;
851 }
852 self.attach_actor(map_transaction_connection_error(error))
853 }
854
855 pub(super) async fn release_transaction(
856 &self,
857 key: &str,
858 terminal: TransactionTerminalState,
859 close: bool,
860 ) {
861 let mut state = self.transaction_coordinator.state.lock().await;
862 if !state
863 .active
864 .as_ref()
865 .is_some_and(|active| active.key == key)
866 {
867 return;
868 }
869 let mut active = state.active.take().expect("active transaction was checked");
870 if let Some(task) = active.timeout_task.take() {
871 task.cancel();
872 }
873 if let TransactionTerminalState::Expired(timeout) = terminal {
874 state.last_expired_timeout = Some(timeout);
875 }
876 insert_terminal_state(&mut state, key.to_owned(), terminal);
877 state.closed |= close;
878 if close {
879 self.transaction_coordinator.admission.close();
880 if let Ok(config) = self.runtime_config() {
881 report_sqlite_worker_fatal(
882 &self.worker_fatal_reported,
883 config,
884 "sqlite transaction rollback failed; coordinator closed".to_owned(),
885 );
886 }
887 }
888 drop(active.gate_guard.take());
889 #[cfg(feature = "sqlite-local")]
890 let profile = active.profile;
891 drop(state);
892
893 #[cfg(feature = "sqlite-local")]
894 if self.backend() == super::SqliteBackend::LocalNative
895 && let Some(profile) = profile
896 {
897 let (fingerprint, fingerprint_source) = profile.fingerprint();
898 if profile.name.is_none() {
899 let warning = UNNAMED_TRANSACTION_WARNINGS.fetch_add(1, Ordering::Relaxed);
900 if warning == 0 || warning.is_power_of_two() {
901 tracing::warn!(
902 fingerprint,
903 "SQLite transaction is unnamed. Add a static `name` to group transaction, storage, and commit performance across executions. Individual statement profiling remains available."
904 );
905 }
906 }
907 if let Some(metrics) = &self.vfs_metrics {
908 let total_ns = profile
909 .started_at
910 .elapsed()
911 .as_nanos()
912 .try_into()
913 .unwrap_or(u64::MAX);
914 let application_time_ns = total_ns
915 .saturating_sub(profile.transaction_wait_ns)
916 .saturating_sub(profile.worker_wait_ns)
917 .saturating_sub(profile.storage_ns)
918 .saturating_sub(profile.local_work_ns);
919 let metric = depot_client::vfs::SqliteTransactionMetric {
920 fingerprint,
921 fingerprint_source,
922 shape_fingerprint: profile.shape_fingerprint(),
923 statement_fingerprint_hashes: profile.statement_fingerprint_hashes,
924 omitted_statement_fingerprints: profile.omitted_statement_fingerprints,
925 storage_transport: "proxy",
926 outcome: match terminal {
927 TransactionTerminalState::Committed => "success",
928 TransactionTerminalState::RolledBack => "rollback",
929 TransactionTerminalState::Expired(_) => "expired",
930 TransactionTerminalState::ConnectionLost => "connection_lost",
931 },
932 total_ns,
933 transaction_wait_ns: profile.transaction_wait_ns,
934 worker_wait_ns: profile.worker_wait_ns,
935 storage_ns: profile.storage_ns,
936 local_work_ns: profile.local_work_ns,
937 application_time_ns,
938 commit_ns: profile.commit_ns,
939 get_pages_round_trips: profile.get_pages_round_trips,
940 statement_count: profile.statement_count,
941 dirty_pages: profile.dirty_pages,
942 dirty_bytes: profile.dirty_bytes,
943 };
944 if metrics.observe_transaction_profile(&metric)
945 && self.profiling.mark_cataloged(&metric.fingerprint)
946 {
947 metrics.record_fingerprint_catalog(
948 "transaction",
949 &metric.fingerprint,
950 profile.name.as_deref().unwrap_or(&metric.fingerprint),
951 FINGERPRINT_FORMAT_VERSION,
952 );
953 }
954 metrics.emit_transaction_diagnostic_event(
955 self.actor_id.as_deref().unwrap_or("unknown"),
956 self.generation,
957 &metric,
958 );
959 }
960 }
961 }
962
963 pub(super) async fn shutdown_transaction_coordinator(&self) -> OwnedRwLockWriteGuard<()> {
964 let active_operation = {
965 let mut state = self.transaction_coordinator.state.lock().await;
966 state.closed = true;
967 state.active.as_ref().map(|active| {
968 (
969 active.key.clone(),
970 Arc::clone(&active.operation),
971 active.remote_session,
972 )
973 })
974 };
975 self.transaction_coordinator.admission.close();
976 if let Some((key, operation, remote_session)) = active_operation {
977 let _operation = operation.lock().await;
978 let still_active = {
979 let state = self.transaction_coordinator.state.lock().await;
980 state
981 .active
982 .as_ref()
983 .is_some_and(|active| active.key == key)
984 };
985 if still_active {
986 let rollback = self
987 .execute_backend_in_session("ROLLBACK".to_owned(), None, remote_session)
988 .await;
989 let rollback_failed = rollback.as_ref().is_err_and(|error| {
990 !is_no_active_transaction_error(error) && !is_remote_connection_error(error)
991 });
992 if let Err(error) = &rollback
993 && rollback_failed
994 {
995 tracing::error!(%error, "sqlite rollback during coordinator shutdown failed");
996 }
997 self.transaction_coordinator
998 .epoch
999 .fetch_add(1, Ordering::AcqRel);
1000 self.release_transaction(
1001 &key,
1002 TransactionTerminalState::RolledBack,
1003 rollback_failed,
1004 )
1005 .await;
1006 }
1007 }
1008 Arc::clone(&self.transaction_coordinator.gate)
1009 .write_owned()
1010 .await
1011 }
1012}
1013
1014#[derive(rivet_error::RivetError, Debug, Serialize)]
1015#[error(
1016 "sqlite",
1017 "transaction_queue_full",
1018 "SQLite transaction queue is full.",
1019 "SQLite transaction coordinator queue is full. Limit is 128 operations."
1020)]
1021pub struct TransactionQueueFullError;
1022
1023impl fmt::Display for TransactionQueueFullError {
1024 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1025 f.write_str("sqlite transaction coordinator queue is full")
1026 }
1027}
1028
1029impl Error for TransactionQueueFullError {}
1030
1031#[derive(rivet_error::RivetError, Debug, Serialize)]
1032#[error(
1033 "sqlite",
1034 "transaction_closed",
1035 "SQLite transaction coordinator is closed."
1036)]
1037pub struct TransactionCoordinatorClosedError;
1038
1039impl fmt::Display for TransactionCoordinatorClosedError {
1040 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1041 f.write_str("sqlite transaction coordinator is closed")
1042 }
1043}
1044
1045impl Error for TransactionCoordinatorClosedError {}
1046
1047#[derive(rivet_error::RivetError, Debug, Serialize)]
1048#[error(
1049 "sqlite",
1050 "transaction_invalid_argument",
1051 "Invalid SQLite transaction argument.",
1052 "{message}"
1053)]
1054pub struct TransactionInvalidArgumentError {
1055 pub message: &'static str,
1056}
1057
1058impl fmt::Display for TransactionInvalidArgumentError {
1059 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1060 f.write_str(self.message)
1061 }
1062}
1063
1064impl Error for TransactionInvalidArgumentError {}
1065
1066#[derive(rivet_error::RivetError, Debug, Serialize)]
1067#[error(
1068 "sqlite",
1069 "transaction_connection_lost",
1070 "SQLite transaction connection was lost.",
1071 "The Envoy connection that owned this SQLite transaction disconnected. The transaction was rolled back and cannot be resumed; start a new db.transaction()."
1072)]
1073pub struct TransactionConnectionLostError;
1074
1075impl fmt::Display for TransactionConnectionLostError {
1076 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1077 f.write_str(
1078 "the Envoy connection that owned this SQLite transaction disconnected; the transaction was rolled back and cannot be resumed",
1079 )
1080 }
1081}
1082
1083impl Error for TransactionConnectionLostError {}
1084
1085#[derive(rivet_error::RivetError, Debug, Serialize)]
1086#[error(
1087 "sqlite",
1088 "transaction_unknown",
1089 "Unknown SQLite transaction handle.",
1090 "Unknown SQLite transaction handle `{key}`."
1091)]
1092pub struct TransactionUnknownError {
1093 pub key: String,
1094}
1095
1096impl fmt::Display for TransactionUnknownError {
1097 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1098 write!(f, "unknown sqlite transaction handle `{}`", self.key)
1099 }
1100}
1101
1102impl Error for TransactionUnknownError {}
1103
1104#[derive(rivet_error::RivetError, Debug, Serialize)]
1105#[error(
1106 "sqlite",
1107 "transaction_terminal",
1108 "SQLite transaction handle is terminal.",
1109 "SQLite transaction handle `{key}` is already {state}."
1110)]
1111pub struct TransactionTerminalError {
1112 pub key: String,
1113 pub state: &'static str,
1114}
1115
1116impl fmt::Display for TransactionTerminalError {
1117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1118 write!(
1119 f,
1120 "sqlite transaction handle `{}` is already {}",
1121 self.key, self.state
1122 )
1123 }
1124}
1125
1126impl Error for TransactionTerminalError {}
1127
1128#[derive(rivet_error::RivetError, Debug, Serialize)]
1129#[error(
1130 "sqlite",
1131 "transaction_expired",
1132 "SQLite transaction expired.",
1133 "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."
1134)]
1135pub struct TransactionExpiredError {
1136 pub timeout_ms: u64,
1137}
1138
1139impl fmt::Display for TransactionExpiredError {
1140 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1141 write!(
1142 f,
1143 "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",
1144 self.timeout_ms
1145 )
1146 }
1147}
1148
1149impl Error for TransactionExpiredError {}
1150
1151impl TransactionExpiredError {
1152 fn owner(_key: &str, timeout: Duration) -> Self {
1153 Self {
1154 timeout_ms: duration_millis(timeout),
1155 }
1156 }
1157
1158 fn parked(timeout: Duration) -> Self {
1159 Self {
1160 timeout_ms: duration_millis(timeout),
1161 }
1162 }
1163}
1164
1165fn duration_millis(duration: Duration) -> u64 {
1166 duration.as_millis().try_into().unwrap_or(u64::MAX)
1167}
1168
1169async fn sleep_transaction_timeout(mut timeout: Duration) {
1170 const MAX_TIMER_WAIT: Duration = Duration::from_millis(2_000_000_000);
1173 while timeout > MAX_TIMER_WAIT {
1174 crate::time::sleep(MAX_TIMER_WAIT).await;
1175 timeout = timeout.saturating_sub(MAX_TIMER_WAIT);
1176 }
1177 crate::time::sleep(timeout).await;
1178}
1179
1180fn spawn_transaction_timeout(
1181 db: SqliteDb,
1182 key: String,
1183 timeout: Duration,
1184 cancel: CancellationToken,
1185 remote_session: Option<u64>,
1186) {
1187 let task = async move {
1188 let connection_lost = async {
1189 let Some(expected) = remote_session else {
1190 std::future::pending::<()>().await;
1191 return;
1192 };
1193 let Ok(handle) = db.handle() else {
1194 return;
1195 };
1196 let mut sessions = handle.subscribe_connection_session();
1197 loop {
1198 if *sessions.borrow_and_update() != expected {
1199 return;
1200 }
1201 if sessions.changed().await.is_err() {
1202 return;
1203 }
1204 }
1205 };
1206 tokio::pin!(connection_lost);
1207
1208 let expired = tokio::select! {
1209 _ = cancel.cancelled() => return,
1210 _ = sleep_transaction_timeout(timeout) => true,
1211 _ = &mut connection_lost => false,
1212 };
1213 let result = if expired {
1214 db.expire_transaction(&key).await
1215 } else {
1216 db.connection_lost_transaction(&key, remote_session.expect("checked above"))
1223 .await
1224 };
1225 if let Err(error) = result {
1226 if error.downcast_ref::<TransactionTerminalError>().is_none() {
1227 tracing::error!(%error, "sqlite transaction terminal cleanup failed");
1228 }
1229 }
1230 };
1231
1232 #[cfg(target_arch = "wasm32")]
1233 wasm_bindgen_futures::spawn_local(task);
1234
1235 #[cfg(not(target_arch = "wasm32"))]
1236 RuntimeSpawner::spawn(task);
1237}
1238
1239#[cfg(not(target_arch = "wasm32"))]
1240pub(super) async fn run_detached_transaction_task<F, T>(
1241 future: F,
1242 context: &'static str,
1243) -> Result<T>
1244where
1245 F: Future<Output = Result<T>> + Send + 'static,
1246 T: Send + 'static,
1247{
1248 RuntimeSpawner::spawn(future).await.context(context)?
1249}
1250
1251#[cfg(target_arch = "wasm32")]
1252pub(super) async fn run_detached_transaction_task<F, T>(
1253 future: F,
1254 context: &'static str,
1255) -> Result<T>
1256where
1257 F: Future<Output = Result<T>> + 'static,
1258 T: 'static,
1259{
1260 let (response_tx, response_rx) = oneshot::channel();
1261 wasm_bindgen_futures::spawn_local(async move {
1262 let _ = response_tx.send(future.await);
1263 });
1264 response_rx.await.context(context)?
1265}
1266
1267pub(super) fn insert_terminal_state(
1268 state: &mut TransactionCoordinatorState,
1269 key: String,
1270 terminal: TransactionTerminalState,
1271) {
1272 if let TransactionTerminalState::Expired(timeout) = terminal {
1273 state.poisoned.insert(key, timeout);
1274 return;
1275 }
1276 if state.terminal.insert(key.clone(), terminal).is_none() {
1277 state.terminal_order.push_back(key);
1278 }
1279 while state.terminal_order.len() > TRANSACTION_TERMINAL_CAPACITY {
1280 if let Some(expired_key) = state.terminal_order.pop_front() {
1281 state.terminal.remove(&expired_key);
1282 }
1283 }
1284}
1285
1286fn transaction_known_state_error(
1287 state: &TransactionCoordinatorState,
1288 key: &str,
1289) -> Option<anyhow::Error> {
1290 if let Some(timeout) = state.poisoned.get(key).copied() {
1291 return Some(transaction_expired_owner_error(key, timeout));
1292 }
1293 state
1294 .terminal
1295 .get(key)
1296 .copied()
1297 .map(|terminal| transaction_terminal_error(key, terminal))
1298}
1299
1300fn is_no_active_transaction_error(error: &anyhow::Error) -> bool {
1301 error.chain().any(|cause| {
1309 cause
1310 .to_string()
1311 .to_ascii_lowercase()
1312 .contains("no transaction is active")
1313 })
1314}
1315
1316fn transaction_queue_full_error() -> anyhow::Error {
1317 TransactionQueueFullError
1318 .build()
1319 .context(TransactionQueueFullError)
1320}
1321
1322fn transaction_coordinator_closed_error() -> anyhow::Error {
1323 TransactionCoordinatorClosedError
1324 .build()
1325 .context(TransactionCoordinatorClosedError)
1326}
1327
1328fn transaction_unknown_error(key: &str) -> anyhow::Error {
1329 TransactionUnknownError {
1330 key: key.to_owned(),
1331 }
1332 .build()
1333 .context(TransactionUnknownError {
1334 key: key.to_owned(),
1335 })
1336}
1337
1338fn transaction_expired_error(timeout: Duration) -> anyhow::Error {
1339 TransactionExpiredError::parked(timeout)
1340 .build()
1341 .context(TransactionExpiredError::parked(timeout))
1342}
1343
1344fn transaction_expired_owner_error(key: &str, timeout: Duration) -> anyhow::Error {
1345 TransactionExpiredError::owner(key, timeout)
1346 .build()
1347 .context(TransactionExpiredError::owner(key, timeout))
1348}
1349
1350fn transaction_terminal_error(key: &str, terminal: TransactionTerminalState) -> anyhow::Error {
1351 match terminal {
1352 TransactionTerminalState::Expired(timeout) => transaction_expired_owner_error(key, timeout),
1353 TransactionTerminalState::Committed => transaction_terminal_state_error(key, "committed"),
1354 TransactionTerminalState::RolledBack => {
1355 transaction_terminal_state_error(key, "rolled back")
1356 }
1357 TransactionTerminalState::ConnectionLost => transaction_connection_lost_error(),
1358 }
1359}
1360
1361fn transaction_invalid_argument_error(message: &'static str) -> anyhow::Error {
1362 TransactionInvalidArgumentError { message }
1363 .build()
1364 .context(TransactionInvalidArgumentError { message })
1365}
1366
1367fn transaction_connection_lost_error() -> anyhow::Error {
1368 TransactionConnectionLostError
1369 .build()
1370 .context(TransactionConnectionLostError)
1371}
1372
1373fn is_remote_connection_error(error: &anyhow::Error) -> bool {
1374 error
1375 .downcast_ref::<super::RemoteSqliteConnectionSessionLostError>()
1376 .is_some()
1377 || error
1378 .downcast_ref::<super::RemoteSqliteIndeterminateResultError>()
1379 .is_some()
1380}
1381
1382fn map_transaction_connection_error(error: anyhow::Error) -> anyhow::Error {
1383 if error
1384 .downcast_ref::<super::RemoteSqliteConnectionSessionLostError>()
1385 .is_some()
1386 {
1387 return transaction_connection_lost_error();
1388 }
1389 super::remote_request_error(error)
1390}
1391
1392fn transaction_terminal_state_error(key: &str, state: &'static str) -> anyhow::Error {
1393 TransactionTerminalError {
1394 key: key.to_owned(),
1395 state,
1396 }
1397 .build()
1398 .context(TransactionTerminalError {
1399 key: key.to_owned(),
1400 state,
1401 })
1402}