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