1use std::{
10 collections::{BTreeMap, BTreeSet, VecDeque},
11 error::Error,
12 fmt,
13 sync::{
14 Arc, Weak,
15 atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering},
16 },
17 thread::JoinHandle,
18 time::{Duration, Instant},
19};
20
21use anyhow::{Result, anyhow};
22use libsqlite3_sys::{
23 SQLITE_OPEN_CREATE, SQLITE_OPEN_READWRITE, sqlite3_compileoption_used, sqlite3_get_autocommit,
24};
25use parking_lot::{Condvar, Mutex};
26use tokio::sync::{Notify, oneshot, watch};
27
28use crate::{
29 guard::{ConnectionGuard, InstalledGuard},
30 optimization_flags::sqlite_optimization_flags,
31 page_buffer::PageBudget,
32 query::{
33 BindParam, ExecuteResult, QueryResult, exec_statements, exec_statements_profiled,
34 execute_single_statement, execute_single_statement_profiled,
35 },
36 vfs::{
37 NativeConnection, NativeVfsHandle, SqliteCommitIndeterminateError, SqliteOperationProfile,
38 SqliteRoundTripCounts, SqliteVfsMetrics, configure_connection_for_database,
39 open_connection, sqlite_exec,
40 },
41};
42
43pub const SQLITE_WORKER_QUEUE_CAPACITY: usize = 128;
46const SQLITE_WORKER_CLOSE_TIMEOUT: Duration = Duration::from_secs(5);
47const SQLITE_SLOW_TRANSACTION_THRESHOLD: Duration = Duration::from_millis(2500);
51
52pub const SQLITE_WORKER_THREADS_ENV: &str = "RIVETKIT_SQLITE_WORKER_THREADS";
54pub const DEFAULT_SQLITE_WORKER_THREADS: usize = 256;
55const MAX_SQLITE_WORKER_THREADS: usize = 4096;
56const SQLITE_WORKER_STACK_BYTES: usize = 512 * 1024;
58const SQLITE_POOL_SATURATED_WARN: Duration = Duration::from_secs(1);
60const ALLOCATOR_TRIM_INTERVAL: Duration = Duration::from_secs(5);
64const BACKGROUND_TURN_EVERY: usize = 4;
66const IDLE_RELEASES_PER_WAKE: usize = 64;
68
69const STATE_RUNNING: u8 = 0;
70const STATE_CLOSING: u8 = 1;
71const STATE_CLOSED: u8 = 2;
72const STATE_DEAD: u8 = 3;
73
74pub trait SqlitePoolMetrics: Send + Sync {
76 fn set_pool_threads(&self, threads: u64);
78
79 fn set_pool_busy(&self, busy: u64);
81
82 fn set_pool_runnable(&self, runnable: u64);
84
85 fn observe_pool_queue_wait(&self, wait_ns: u64);
87
88 fn observe_pool_lock_wait(&self, lock: &'static str, wait_ns: u64);
90
91 fn set_page_buffer_capacity_pages(&self, _pages: u64) {}
93
94 fn set_page_buffer_pages(&self, _pages: u64) {}
96
97 fn set_pager_cache_capacity_bytes(&self, _bytes: u64) {}
99
100 fn set_pager_cache_bytes(&self, _bytes: u64) {}
102
103 fn set_pager_cache_pages(&self, _pages: u64) {}
105
106 fn set_pager_cache_unbudgeted_bytes(&self, _bytes: u64) {}
109}
110
111#[derive(Clone)]
115pub struct SqlitePool {
116 owner: Arc<PoolOwner>,
117 page_budget: Arc<PageBudget>,
118}
119
120struct PoolOwner {
123 shared: Arc<PoolShared>,
124}
125
126struct PoolShared {
127 max_threads: usize,
128 state: Mutex<PoolState>,
130 idle: Mutex<IdleQueue<DatabaseSlot>>,
133 epoch: Instant,
135 wake: Condvar,
136 metrics: Option<Arc<dyn SqlitePoolMetrics>>,
137}
138
139struct IdleQueue<T> {
144 queue: BTreeSet<(Instant, u64)>,
145 entries: BTreeMap<u64, (Instant, Weak<T>)>,
146}
147
148impl<T> Default for IdleQueue<T> {
149 fn default() -> Self {
150 Self {
151 queue: BTreeSet::new(),
152 entries: BTreeMap::new(),
153 }
154 }
155}
156
157impl<T> IdleQueue<T> {
158 fn insert(&mut self, id: u64, at: Instant, item: Weak<T>) -> bool {
161 if let Some((old, _)) = self.entries.insert(id, (at, item)) {
162 self.queue.remove(&(old, id));
163 }
164 self.queue.insert((at, id));
165 self.queue.first() == Some(&(at, id))
166 }
167
168 fn cancel(&mut self, id: u64) {
169 if let Some((at, _)) = self.entries.remove(&id) {
170 self.queue.remove(&(at, id));
171 }
172 }
173
174 fn head(&self) -> Option<Instant> {
175 self.queue.first().map(|(at, _)| *at)
176 }
177
178 fn pop_due(
181 &mut self,
182 now: Instant,
183 limit: usize,
184 due_at: impl Fn(&Arc<T>) -> Option<Instant>,
185 ) -> Vec<Arc<T>> {
186 let mut due = Vec::new();
187 while due.len() < limit {
188 let Some(&(at, id)) = self.queue.first() else {
189 break;
190 };
191 if at > now {
192 break;
193 }
194 self.queue.pop_first();
195 let Some((_, item)) = self.entries.remove(&id) else {
196 continue;
197 };
198 let Some(item) = item.upgrade() else {
199 continue;
200 };
201 match due_at(&item) {
202 Some(later) if later > now => {
203 self.entries.insert(id, (later, Arc::downgrade(&item)));
204 self.queue.insert((later, id));
205 }
206 Some(_) | None => due.push(item),
207 }
208 }
209 due
210 }
211}
212
213struct RunQueue<T> {
218 foreground: VecDeque<(T, Instant)>,
219 background: VecDeque<(T, Instant)>,
220 foreground_streak: usize,
222}
223
224impl<T> Default for RunQueue<T> {
225 fn default() -> Self {
226 Self {
227 foreground: VecDeque::new(),
228 background: VecDeque::new(),
229 foreground_streak: 0,
230 }
231 }
232}
233
234impl<T> RunQueue<T> {
235 fn push(&mut self, item: T, background: bool, now: Instant) {
236 if background {
237 self.background.push_back((item, now));
238 } else {
239 self.foreground.push_back((item, now));
240 }
241 }
242
243 fn pop(&mut self) -> Option<(T, Instant)> {
244 let background_turn = !self.background.is_empty()
245 && (self.foreground.is_empty() || self.foreground_streak >= BACKGROUND_TURN_EVERY);
246 if background_turn {
247 self.foreground_streak = 0;
248 return self.background.pop_front();
249 }
250 let next = self.foreground.pop_front()?;
251 if self.background.is_empty() {
252 self.foreground_streak = 0;
253 } else {
254 self.foreground_streak += 1;
255 }
256 Some(next)
257 }
258
259 fn len(&self) -> usize {
260 self.foreground.len() + self.background.len()
261 }
262
263 fn is_empty(&self) -> bool {
264 self.foreground.is_empty() && self.background.is_empty()
265 }
266}
267
268#[derive(Default)]
269struct PoolState {
270 runnable: RunQueue<Arc<DatabaseSlot>>,
271 orphans: Vec<NativeConnection>,
274 threads: Vec<JoinHandle<()>>,
275 waiting: usize,
277 busy: usize,
278 saturation: Saturation,
279 next_database: u64,
280 shutdown: bool,
281 trim_due_at: Option<Instant>,
283 last_trim_at: Option<Instant>,
285}
286
287#[derive(Default)]
291struct Saturation {
292 since: Option<Instant>,
293 warned: bool,
294}
295
296impl Saturation {
297 fn freed(&mut self) {
299 self.since = None;
300 self.warned = false;
301 }
302
303 fn work_waiting(&mut self, saturated: bool, now: Instant) -> bool {
307 if !saturated {
308 return false;
309 }
310 let since = *self.since.get_or_insert(now);
311 if self.warned || now.duration_since(since) < SQLITE_POOL_SATURATED_WARN {
312 return false;
313 }
314 self.warned = true;
315 true
316 }
317}
318
319struct DatabaseSlot {
322 opening_trace: Option<WorkerTrace>,
323 id: u64,
324 inner: Arc<SqliteWorkerInner>,
325 shared: Arc<PoolShared>,
326 vfs: NativeVfsHandle,
327 file_name: String,
328 idle_release: Option<Duration>,
329 last_used_ns: AtomicU64,
331 idle_queued: AtomicBool,
333 background: AtomicBool,
336 guard: Option<Box<InstalledGuard>>,
338 work: Mutex<SlotWork>,
339}
340
341#[derive(Clone)]
344struct WorkerTrace {
345 span: tracing::Span,
346 dispatch: tracing::Dispatch,
347}
348impl Drop for WorkerTrace {
349 fn drop(&mut self) {
350 tracing::dispatcher::with_default(&self.dispatch, || {
353 drop(std::mem::replace(&mut self.span, tracing::Span::none()));
354 });
355 }
356}
357
358impl WorkerTrace {
359 fn capture() -> Option<Self> {
360 tracing::enabled!(target: "sqlite_prefetch::trace", tracing::Level::TRACE).then(|| Self {
361 span: tracing::Span::current(),
362 dispatch: tracing::dispatcher::get_default(Clone::clone),
363 })
364 }
365}
366struct QueuedCommand {
367 command: SqliteCommand,
368 trace: Option<WorkerTrace>,
369}
370
371struct SlotWork {
372 commands: VecDeque<QueuedCommand>,
373 scheduled: bool,
375 idle_due: bool,
377 connection: ConnectionSlot,
378 close_requested_at: Option<Instant>,
379}
380
381enum ConnectionSlot {
382 Unopened(Option<oneshot::Sender<Result<()>>>),
384 Parked(OpenConnection),
386 Running,
388 Gone,
390}
391
392struct OpenConnection {
393 db: NativeConnection,
394 transaction: Option<TransactionTracker>,
395 pending_uncork: Option<oneshot::Sender<Result<()>>>,
398}
399
400enum Action {
402 Open(Option<oneshot::Sender<Result<()>>>),
403 Run(OpenConnection, QueuedCommand),
404 Release(OpenConnection),
405 Close(Option<OpenConnection>),
406}
407
408enum PoolTask {
410 Database(Arc<DatabaseSlot>, Instant),
411 IdleReleases(Vec<Arc<DatabaseSlot>>),
412 Orphan(NativeConnection),
413 TrimAllocator,
414}
415
416fn process_page_budget(metrics: Option<Arc<dyn SqlitePoolMetrics>>) -> Arc<PageBudget> {
419 let flags = sqlite_optimization_flags();
420 let caches_pages = flags.vfs_page_cache_mode.caches_any_pages();
421 let (capacity, ttl_ms) = if caches_pages {
422 (
423 flags.vfs_page_buffer_process_pages,
424 flags.vfs_staging_cache_ttl_ms,
425 )
426 } else {
427 (0, 0)
428 };
429 PageBudget::new(capacity, ttl_ms, metrics)
430}
431
432impl SqlitePool {
433 pub fn from_env(metrics: Option<Arc<dyn SqlitePoolMetrics>>) -> Self {
435 let threads = std::env::var(SQLITE_WORKER_THREADS_ENV)
436 .ok()
437 .and_then(|value| value.trim().parse::<usize>().ok())
438 .filter(|threads| *threads > 0)
439 .unwrap_or(DEFAULT_SQLITE_WORKER_THREADS)
440 .min(MAX_SQLITE_WORKER_THREADS);
441 Self::new(threads, metrics)
442 }
443
444 pub fn new(max_threads: usize, metrics: Option<Arc<dyn SqlitePoolMetrics>>) -> Self {
446 let max_threads = max_threads.max(1);
447 if let Some(metrics) = &metrics {
448 metrics.set_pool_threads(max_threads as u64);
449 }
450 let page_budget = process_page_budget(metrics.clone());
451 crate::pager_cache::install(
454 sqlite_optimization_flags()
455 .pager_process_cache_kib
456 .saturating_mul(1024),
457 metrics.clone(),
458 );
459 warn_if_direct_overflow_reads();
460 Self {
461 page_budget,
462 owner: Arc::new(PoolOwner {
463 shared: Arc::new(PoolShared {
464 max_threads,
465 state: Mutex::new(PoolState::default()),
466 idle: Mutex::new(IdleQueue::default()),
467 epoch: Instant::now(),
468 wake: Condvar::new(),
469 metrics,
470 }),
471 }),
472 }
473 }
474
475 pub fn page_budget(&self) -> &Arc<PageBudget> {
477 &self.page_budget
478 }
479
480 pub fn request_allocator_trim(&self) {
484 self.owner.shared.request_allocator_trim();
485 }
486
487 pub fn live_threads(&self) -> usize {
488 self.owner.shared.state.lock().threads.len()
489 }
490
491 pub fn max_threads(&self) -> usize {
492 self.owner.shared.max_threads
493 }
494}
495
496impl Drop for PoolOwner {
497 fn drop(&mut self) {
498 let threads = {
499 let mut state = self.shared.state.lock();
500 state.shutdown = true;
501 std::mem::take(&mut state.threads)
502 };
503 self.shared.wake.notify_all();
504 if threads.is_empty() {
505 return;
506 }
507 let joiner = std::thread::Builder::new()
509 .name("sqlite-pool-join".to_owned())
510 .stack_size(64 * 1024)
511 .spawn(move || {
512 for thread in threads {
513 if thread.join().is_err() {
514 tracing::error!("sqlite pool thread panicked during shutdown");
515 }
516 }
517 });
518 if let Err(error) = joiner {
519 tracing::error!(
520 ?error,
521 "failed to spawn sqlite pool joiner; threads exit unjoined"
522 );
523 }
524 }
525}
526
527impl PoolShared {
528 fn schedule(self: &Arc<Self>, slot: Arc<DatabaseSlot>) {
530 let mut state = self.lock_state();
531 let now = Instant::now();
532 let background = slot.background.load(Ordering::Acquire);
533 state.runnable.push(slot, background, now);
534 if let Some(metrics) = &self.metrics {
535 metrics.set_pool_runnable(state.runnable.len() as u64);
536 }
537 let mut failed_slots = Vec::new();
538 if state.waiting == 0 && state.threads.len() < self.max_threads && !state.shutdown {
539 if !self.spawn_thread(&mut state) && state.threads.is_empty() {
540 while let Some((slot, _)) = state.runnable.pop() {
542 failed_slots.push(slot);
543 }
544 if let Some(metrics) = &self.metrics {
545 metrics.set_pool_runnable(0);
546 }
547 }
548 }
549 self.note_waiting_work(&mut state, now);
550 drop(state);
551 for slot in failed_slots {
552 slot.fail_without_worker();
553 }
554 self.wake.notify_one();
555 }
556
557 fn note_waiting_work(&self, state: &mut PoolState, now: Instant) {
560 let saturated = state.waiting == 0 && state.threads.len() >= self.max_threads;
561 if state.saturation.work_waiting(saturated, now) {
562 tracing::warn!(
563 threads = self.max_threads,
564 runnable = state.runnable.len(),
565 "every sqlite pool thread has been busy for over a second while work waited; raise RIVETKIT_SQLITE_WORKER_THREADS or look for slow VFS round trips"
566 );
567 }
568 }
569
570 fn spawn_thread(self: &Arc<Self>, state: &mut PoolState) -> bool {
571 let shared = Arc::clone(self);
572 let index = state.threads.len();
573 let spawned = std::thread::Builder::new()
574 .name(format!("sqlite-pool-{index}"))
575 .stack_size(SQLITE_WORKER_STACK_BYTES)
576 .spawn(move || pool_thread(shared));
577 match spawned {
578 Ok(thread) => {
579 state.threads.push(thread);
580 true
581 }
582 Err(error) => {
583 tracing::error!(
585 ?error,
586 threads = state.threads.len(),
587 "failed to spawn sqlite pool thread"
588 );
589 false
590 }
591 }
592 }
593
594 fn lock_state(&self) -> parking_lot::MutexGuard<'_, PoolState> {
596 self.timed_lock(&self.state, "run_queue")
597 }
598
599 fn lock_idle(&self) -> parking_lot::MutexGuard<'_, IdleQueue<DatabaseSlot>> {
600 self.timed_lock(&self.idle, "idle")
601 }
602
603 fn timed_lock<'a, T>(
604 &self,
605 mutex: &'a Mutex<T>,
606 lock: &'static str,
607 ) -> parking_lot::MutexGuard<'a, T> {
608 if let Some(guard) = mutex.try_lock() {
609 return guard;
610 }
611 let started = Instant::now();
612 let guard = mutex.lock();
613 if let Some(metrics) = &self.metrics {
614 metrics.observe_pool_lock_wait(lock, started.elapsed().as_nanos() as u64);
615 }
616 guard
617 }
618
619 fn now_ns(&self) -> u64 {
620 self.epoch.elapsed().as_nanos().min(u64::MAX as u128) as u64
621 }
622
623 fn note_used(&self, slot: &Arc<DatabaseSlot>, idle: Duration) {
626 slot.last_used_ns.store(self.now_ns(), Ordering::SeqCst);
627 if slot.idle_queued.swap(true, Ordering::SeqCst) {
628 return;
629 }
630 let at = Instant::now() + idle;
631 let head = self.lock_idle().insert(slot.id, at, Arc::downgrade(slot));
632 if head {
633 drop(self.lock_state());
636 self.wake.notify_one();
637 }
638 }
639
640 fn idle_due_at(&self, slot: &DatabaseSlot) -> Option<Instant> {
642 let idle = slot.idle_release?;
643 let last_used = Duration::from_nanos(slot.last_used_ns.load(Ordering::SeqCst));
644 Some(self.epoch + last_used + idle)
645 }
646
647 fn cancel_idle(&self, id: u64) {
648 self.lock_idle().cancel(id);
649 }
650
651 fn due_idle_releases(&self, now: Instant) -> Vec<Arc<DatabaseSlot>> {
654 let mut idle = self.lock_idle();
655 let due = idle.pop_due(now, IDLE_RELEASES_PER_WAKE, |slot| self.idle_due_at(slot));
656 let mut released = Vec::with_capacity(due.len());
657 for slot in due {
658 slot.idle_queued.store(false, Ordering::SeqCst);
659 match self.idle_due_at(&slot) {
662 Some(at) if at > now => {
663 if !slot.idle_queued.swap(true, Ordering::SeqCst) {
664 idle.insert(slot.id, at, Arc::downgrade(&slot));
665 }
666 }
667 Some(_) | None => released.push(slot),
668 }
669 }
670 released
671 }
672
673 fn orphan(&self, connection: NativeConnection) {
674 let mut state = self.lock_state();
675 state.orphans.push(connection);
676 drop(state);
677 self.wake.notify_one();
678 }
679
680 fn next_task(&self) -> Option<PoolTask> {
682 let mut state = self.lock_state();
683 loop {
684 if let Some(connection) = state.orphans.pop() {
685 return Some(PoolTask::Orphan(connection));
686 }
687 if let Some((slot, queued_at)) = state.runnable.pop() {
688 if let Some(metrics) = &self.metrics {
689 metrics.set_pool_runnable(state.runnable.len() as u64);
690 }
691 if !state.runnable.is_empty() {
692 self.note_waiting_work(&mut state, Instant::now());
693 }
694 return Some(PoolTask::Database(slot, queued_at));
695 }
696 let due = self.due_idle_releases(Instant::now());
697 if !due.is_empty() {
698 return Some(PoolTask::IdleReleases(due));
699 }
700 if state.trim_due_at.is_some_and(|at| at <= Instant::now()) {
701 state.trim_due_at = None;
702 return Some(PoolTask::TrimAllocator);
703 }
704 if state.shutdown {
705 return None;
706 }
707 state.waiting += 1;
708 state.saturation.freed();
709 let idle_head = self.lock_idle().head();
710 let head = match (idle_head, state.trim_due_at) {
711 (Some(idle), Some(trim)) => Some(idle.min(trim)),
712 (idle, trim) => idle.or(trim),
713 };
714 match head {
715 Some(at) => {
716 self.wake.wait_until(&mut state, at);
717 }
718 None => self.wake.wait(&mut state),
719 }
720 state.waiting -= 1;
721 }
722 }
723
724 fn request_allocator_trim(self: &Arc<Self>) {
727 let mut state = self.lock_state();
728 if state.trim_due_at.is_some() || state.shutdown {
729 return;
730 }
731 let now = Instant::now();
732 let due = state
733 .last_trim_at
734 .map_or(now, |last| (last + ALLOCATOR_TRIM_INTERVAL).max(now));
735 state.trim_due_at = Some(due);
736 if state.waiting == 0 && state.threads.is_empty() {
737 self.spawn_thread(&mut state);
738 }
739 drop(state);
740 self.wake.notify_one();
742 }
743
744 fn set_busy(&self, delta: isize) {
746 let mut state = self.lock_state();
747 state.busy = state.busy.saturating_add_signed(delta);
748 if let Some(metrics) = &self.metrics {
749 metrics.set_pool_busy(state.busy as u64);
750 }
751 }
752}
753
754#[cfg(all(unix, target_env = "gnu"))]
757fn trim_allocator() {
758 unsafe extern "C" {
759 fn malloc_trim(pad: usize) -> i32;
760 }
761
762 let started = Instant::now();
763 let rc = unsafe { malloc_trim(0) };
764 tracing::debug!(
765 rc,
766 duration_us = started.elapsed().as_micros() as u64,
767 "trimmed native allocator"
768 );
769}
770
771#[cfg(not(all(unix, target_env = "gnu")))]
772fn trim_allocator() {}
773
774fn pool_thread(shared: Arc<PoolShared>) {
775 while let Some(task) = shared.next_task() {
776 shared.set_busy(1);
777 match task {
778 PoolTask::Database(slot, queued_at) => {
779 if let Some(metrics) = &shared.metrics {
780 metrics.observe_pool_queue_wait(queued_at.elapsed().as_nanos() as u64);
781 }
782 slot.run_one();
783 }
784 PoolTask::IdleReleases(slots) => {
785 for slot in slots {
786 slot.mark_idle_due();
787 }
788 }
789 PoolTask::Orphan(connection) => drop(connection),
790 PoolTask::TrimAllocator => {
791 trim_allocator();
792 shared.lock_state().last_trim_at = Some(Instant::now());
793 }
794 }
795 shared.set_busy(-1);
796 }
797}
798
799impl DatabaseSlot {
800 fn fail_without_worker(&self) {
802 self.inner.state.store(STATE_DEAD, Ordering::Release);
803 self.inner.explicit_transaction.send_replace(false);
804 let mut work = self.work.lock();
805 debug_assert!(matches!(work.connection, ConnectionSlot::Unopened(_)));
806 work.connection = ConnectionSlot::Gone;
807 work.scheduled = false;
808 fail_queued_commands(&mut work.commands);
809 drop(work);
810 self.inner.closed.notify_waiters();
811 }
812
813 fn enqueue(self: &Arc<Self>, command: SqliteCommand) -> Result<()> {
815 match self.inner.state.load(Ordering::Acquire) {
816 STATE_RUNNING => {}
817 STATE_CLOSING | STATE_CLOSED => return Err(sqlite_closing_error()),
818 STATE_DEAD => return Err(sqlite_worker_dead_error()),
819 other => return Err(anyhow!("unknown sqlite worker state {other}")),
820 }
821 let mut work = self.work.lock();
822 if work.commands.len() >= SQLITE_WORKER_QUEUE_CAPACITY {
823 drop(work);
824 if let Some(metrics) = &self.inner.metrics {
825 metrics.record_worker_queue_overload();
826 }
827 return Err(SqliteWorkerOverloadedError.into());
830 }
831 work.commands.push_back(QueuedCommand {
832 command,
833 trace: WorkerTrace::capture(),
834 });
835 self.inner.record_queue_depth(work.commands.len() as u64);
836 self.schedule_locked(work);
837 Ok(())
838 }
839
840 fn request_close(self: &Arc<Self>) {
842 let mut work = self.work.lock();
843 work.close_requested_at.get_or_insert_with(Instant::now);
844 self.schedule_locked(work);
845 }
846
847 fn mark_idle_due(self: &Arc<Self>) {
848 let mut work = self.work.lock();
849 if !matches!(work.connection, ConnectionSlot::Parked(_)) {
850 return;
851 }
852 work.idle_due = true;
853 self.schedule_locked(work);
854 }
855
856 fn schedule_locked(self: &Arc<Self>, mut work: parking_lot::MutexGuard<'_, SlotWork>) {
859 if work.scheduled {
860 return;
861 }
862 work.scheduled = true;
863 drop(work);
864 self.shared.schedule(Arc::clone(self));
865 }
866
867 fn run_one(self: &Arc<Self>) {
870 let action = {
871 let mut work = self.work.lock();
872 let closing = self.inner.state.load(Ordering::Acquire) == STATE_CLOSING;
873 let connection = std::mem::replace(&mut work.connection, ConnectionSlot::Running);
874 match connection {
875 ConnectionSlot::Unopened(ready) if closing => {
876 drop(ready);
877 Some(Action::Close(None))
878 }
879 ConnectionSlot::Unopened(ready) => Some(Action::Open(ready)),
880 ConnectionSlot::Parked(open) if closing => Some(Action::Close(Some(open))),
881 ConnectionSlot::Parked(open) => match work.commands.pop_front() {
882 Some(command) => {
883 self.inner.record_queue_depth(work.commands.len() as u64);
884 Some(Action::Run(open, command))
885 }
886 None if work.idle_due => Some(Action::Release(open)),
887 None => {
888 work.connection = ConnectionSlot::Parked(open);
889 None
890 }
891 },
892 ConnectionSlot::Running => {
893 tracing::error!("sqlite database scheduled while a thread holds it");
895 None
896 }
897 ConnectionSlot::Gone => {
898 work.connection = ConnectionSlot::Gone;
899 fail_queued_commands(&mut work.commands);
900 None
901 }
902 }
903 };
904 let Some(action) = action else {
905 self.finish_idle();
906 return;
907 };
908
909 let ran_command = matches!(action, Action::Run(..));
910 let released = matches!(action, Action::Release(_));
911 let outcome =
912 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.perform(action)));
913 let next = match outcome {
914 Ok(next) => next,
915 Err(panic) => {
916 self.inner.state.store(STATE_DEAD, Ordering::Release);
917 self.inner.explicit_transaction.send_replace(false);
918 if let Some(metrics) = &self.inner.metrics {
919 metrics.record_worker_crash();
920 metrics.set_vfs_cached_bytes(0);
921 }
922 self.inner.closed.notify_waiters();
923 tracing::error!(msg = panic_message(&panic), "sqlite worker panicked");
924 ConnectionSlot::Gone
925 }
926 };
927 let gone = matches!(next, ConnectionSlot::Gone);
928
929 let requeue = {
930 let mut work = self.work.lock();
931 work.connection = next;
932 if released {
933 work.idle_due = false;
934 }
935 if gone {
936 fail_queued_commands(&mut work.commands);
937 }
938 let more = !gone
939 && (!work.commands.is_empty()
940 || work.idle_due
941 || self.inner.state.load(Ordering::Acquire) == STATE_CLOSING);
942 if !more {
943 work.scheduled = false;
944 }
945 more
946 };
947 if gone {
948 self.shared.cancel_idle(self.id);
949 } else if ran_command && let Some(idle) = self.idle_release {
950 self.shared.note_used(self, idle);
951 }
952 if requeue {
953 self.shared.schedule(Arc::clone(self));
954 }
955 }
956
957 fn finish_idle(self: &Arc<Self>) {
959 let requeue = {
960 let mut work = self.work.lock();
961 let more = !work.commands.is_empty()
962 || (matches!(work.connection, ConnectionSlot::Parked(_))
963 && self.inner.state.load(Ordering::Acquire) == STATE_CLOSING);
964 if !more {
965 work.scheduled = false;
966 }
967 more
968 };
969 if requeue {
970 self.shared.schedule(Arc::clone(self));
971 }
972 }
973
974 fn perform(&self, action: Action) -> ConnectionSlot {
976 let trace = match &action {
977 Action::Run(_, command) => command.trace.clone(),
978 _ => self.opening_trace.clone(),
979 };
980 let _dispatch = trace
983 .as_ref()
984 .map(|trace| tracing::dispatcher::set_default(&trace.dispatch));
985 let _span = trace.as_ref().map(|trace| trace.span.enter());
986 let metrics = self.inner.metrics.as_deref();
987 match action {
988 Action::Open(ready) => match open_database_connection(&self.vfs, &self.file_name)
989 .and_then(|db| {
990 if let Some(guard) = &self.guard {
991 guard.install(db.as_ptr())?;
992 }
993 Ok(db)
994 }) {
995 Ok(db) => {
996 if let Some(ready) = ready {
997 let _ = ready.send(Ok(()));
998 }
999 ConnectionSlot::Parked(OpenConnection {
1000 db,
1001 transaction: None,
1002 pending_uncork: None,
1003 })
1004 }
1005 Err(error) => {
1006 if let Some(ready) = ready {
1007 let _ = ready.send(Err(error));
1008 }
1009 self.inner.state.store(STATE_DEAD, Ordering::Release);
1010 self.inner.closed.notify_waiters();
1011 ConnectionSlot::Gone
1012 }
1013 },
1014 Action::Run(mut open, command) => {
1015 if let Some(guard) = &self.guard {
1016 guard.reset();
1017 }
1018 let _inflight = WorkerInflightGuard::new(metrics);
1019 let max_result_bytes = self.guard.as_ref().map(|guard| guard.max_result_bytes());
1020 run_command(
1021 &mut open.db,
1022 &self.vfs,
1023 command.command,
1024 max_result_bytes,
1025 metrics,
1026 &self.file_name,
1027 &mut open.transaction,
1028 &mut open.pending_uncork,
1029 &self.inner.explicit_transaction,
1030 );
1031 report_vfs_memory(&open.db, metrics);
1032 ConnectionSlot::Parked(open)
1033 }
1034 Action::Release(mut open) => {
1035 release_idle_memory(&mut open.db, metrics);
1036 ConnectionSlot::Parked(open)
1037 }
1038 Action::Close(open) => {
1039 let requested_at = {
1040 let mut work = self.work.lock();
1041 fail_queued_commands(&mut work.commands);
1042 work.close_requested_at
1043 };
1044 drop(open);
1045 if let Some(metrics) = metrics {
1046 metrics.set_vfs_cached_bytes(0);
1047 if let Some(requested_at) = requested_at {
1048 metrics
1049 .observe_worker_close_duration(requested_at.elapsed().as_nanos() as u64);
1050 }
1051 }
1052 self.inner.explicit_transaction.send_replace(false);
1053 self.inner.state.store(STATE_CLOSED, Ordering::Release);
1054 self.inner.closed.notify_waiters();
1055 ConnectionSlot::Gone
1056 }
1057 }
1058 }
1059}
1060
1061impl Drop for DatabaseSlot {
1062 fn drop(&mut self) {
1063 let work = self.work.get_mut();
1064 fail_queued_commands(&mut work.commands);
1065 if let ConnectionSlot::Parked(open) =
1066 std::mem::replace(&mut work.connection, ConnectionSlot::Gone)
1067 {
1068 self.shared.orphan(open.db);
1069 }
1070 self.shared.cancel_idle(self.id);
1071 if self.inner.state.load(Ordering::Acquire) == STATE_RUNNING {
1072 if let Some(metrics) = &self.inner.metrics {
1073 metrics.record_worker_unclean_close();
1074 metrics.set_vfs_cached_bytes(0);
1075 }
1076 tracing::error!("sqlite worker handle dropped without clean close");
1078 self.inner.explicit_transaction.send_replace(false);
1079 self.inner.state.store(STATE_CLOSED, Ordering::Release);
1080 self.inner.closed.notify_waiters();
1081 }
1082 }
1083}
1084
1085fn fail_queued_commands(commands: &mut VecDeque<QueuedCommand>) {
1086 for command in commands.drain(..) {
1087 fail_command(command.command);
1088 }
1089}
1090
1091#[derive(Clone)]
1092pub struct SqliteWorkerHandle {
1093 slot: Arc<DatabaseSlot>,
1094 _pool: SqlitePool,
1095}
1096
1097struct SqliteWorkerInner {
1098 metrics: Option<Arc<dyn SqliteVfsMetrics>>,
1099 state: AtomicU8,
1100 closed: Notify,
1101 ready: Mutex<Option<oneshot::Receiver<Result<()>>>>,
1102 explicit_transaction: watch::Sender<bool>,
1106}
1107
1108enum SqliteCommand {
1109 Execute {
1110 sql: String,
1111 params: Option<Vec<BindParam>>,
1112 enqueued_at: Option<Instant>,
1113 reply: oneshot::Sender<Result<SqliteWorkerResult<ExecuteResult>>>,
1114 },
1115 Exec {
1116 sql: String,
1117 enqueued_at: Option<Instant>,
1118 reply: oneshot::Sender<Result<SqliteWorkerResult<QueryResult>>>,
1119 },
1120 ExecuteBatch {
1122 statements: Vec<(String, Option<Vec<BindParam>>)>,
1123 reply: oneshot::Sender<Result<SqliteWorkerResult<Vec<ExecuteResult>>>>,
1124 },
1125 Cork { reply: oneshot::Sender<Result<()>> },
1127 Uncork { reply: oneshot::Sender<Result<()>> },
1129}
1130
1131#[derive(Debug)]
1132pub struct SqliteWorkerResult<T> {
1133 pub result: Result<T>,
1134 pub profile: SqliteOperationProfile,
1135}
1136
1137struct WorkerInflightGuard<'a>(Option<&'a dyn SqliteVfsMetrics>);
1138
1139impl<'a> WorkerInflightGuard<'a> {
1140 fn new(metrics: Option<&'a dyn SqliteVfsMetrics>) -> Self {
1141 if let Some(metrics) = metrics {
1142 metrics.set_worker_inflight(true);
1143 }
1144 Self(metrics)
1145 }
1146}
1147
1148impl Drop for WorkerInflightGuard<'_> {
1149 fn drop(&mut self) {
1150 if let Some(metrics) = self.0 {
1151 metrics.set_worker_inflight(false);
1152 }
1153 }
1154}
1155
1156struct TransactionTracker {
1163 started_at: Instant,
1164 start_round_trips: SqliteRoundTripCounts,
1165 statement_count: u64,
1166}
1167
1168fn release_idle_memory(db: &mut NativeConnection, metrics: Option<&dyn SqliteVfsMetrics>) {
1171 if command_in_tx(db) {
1172 return;
1173 }
1174 if db.release_idle_memory() {
1175 report_vfs_memory(db, metrics);
1176 }
1177}
1178
1179fn report_vfs_memory(db: &NativeConnection, metrics: Option<&dyn SqliteVfsMetrics>) {
1182 if let Some(metrics) = metrics {
1183 metrics.set_vfs_cached_bytes(db.cached_page_bytes());
1184 }
1185}
1186
1187impl SqliteWorkerHandle {
1188 pub fn start(
1190 vfs: NativeVfsHandle,
1191 file_name: String,
1192 metrics: Option<Arc<dyn SqliteVfsMetrics>>,
1193 ) -> Result<Self> {
1194 Self::start_on(&SqlitePool::new(1, None), vfs, file_name, metrics)
1195 }
1196
1197 pub fn start_on(
1200 pool: &SqlitePool,
1201 vfs: NativeVfsHandle,
1202 file_name: String,
1203 metrics: Option<Arc<dyn SqliteVfsMetrics>>,
1204 ) -> Result<Self> {
1205 Self::start_on_with_guard(pool, vfs, file_name, metrics, None)
1206 }
1207
1208 pub fn start_on_with_guard(
1211 pool: &SqlitePool,
1212 vfs: NativeVfsHandle,
1213 file_name: String,
1214 metrics: Option<Arc<dyn SqliteVfsMetrics>>,
1215 guard: Option<ConnectionGuard>,
1216 ) -> Result<Self> {
1217 let (ready_tx, ready_rx) = oneshot::channel();
1218 let inner = Arc::new(SqliteWorkerInner {
1219 metrics,
1220 state: AtomicU8::new(STATE_RUNNING),
1221 closed: Notify::new(),
1222 ready: Mutex::new(Some(ready_rx)),
1223 explicit_transaction: watch::Sender::new(false),
1224 });
1225 let shared = &pool.owner.shared;
1226 let id = {
1227 let mut state = shared.state.lock();
1228 if state.shutdown {
1229 return Err(anyhow!("sqlite pool is shutting down"));
1230 }
1231 state.next_database += 1;
1232 state.next_database
1233 };
1234 let idle_release = vfs.idle_release();
1235 let slot = Arc::new(DatabaseSlot {
1236 opening_trace: WorkerTrace::capture(),
1237 id,
1238 inner,
1239 shared: Arc::clone(shared),
1240 vfs,
1241 file_name,
1242 idle_release,
1243 last_used_ns: AtomicU64::new(0),
1244 idle_queued: AtomicBool::new(false),
1245 background: AtomicBool::new(false),
1246 guard: guard.map(InstalledGuard::new),
1247 work: Mutex::new(SlotWork {
1248 commands: VecDeque::new(),
1249 scheduled: false,
1250 idle_due: false,
1251 connection: ConnectionSlot::Unopened(Some(ready_tx)),
1252 close_requested_at: None,
1253 }),
1254 });
1255 let work = slot.work.lock();
1256 slot.schedule_locked(work);
1257 Ok(Self {
1258 slot,
1259 _pool: pool.clone(),
1260 })
1261 }
1262
1263 fn inner(&self) -> &SqliteWorkerInner {
1264 &self.slot.inner
1265 }
1266
1267 pub async fn wait_ready(&self) -> Result<()> {
1268 let ready = self.inner().ready.lock().take();
1269 let Some(ready) = ready else {
1270 return Ok(());
1271 };
1272 ready
1273 .await
1274 .map_err(|_| sqlite_worker_dead_error())?
1275 .map_err(|err| anyhow!("failed to initialize sqlite worker: {err}"))
1276 }
1277
1278 pub fn set_background(&self, background: bool) {
1283 self.slot.background.store(background, Ordering::Release);
1284 }
1285
1286 pub fn explicit_transaction_open(&self) -> bool {
1287 *self.inner().explicit_transaction.borrow()
1288 }
1289
1290 pub fn subscribe_explicit_transaction(&self) -> watch::Receiver<bool> {
1293 self.inner().explicit_transaction.subscribe()
1294 }
1295
1296 pub async fn exec(&self, sql: String) -> Result<QueryResult> {
1297 self.exec_inner(sql, None).await?.result
1298 }
1299
1300 pub async fn exec_profiled(&self, sql: String) -> Result<SqliteWorkerResult<QueryResult>> {
1301 self.exec_inner(sql, Some(Instant::now())).await
1302 }
1303
1304 async fn exec_inner(
1305 &self,
1306 sql: String,
1307 enqueued_at: Option<Instant>,
1308 ) -> Result<SqliteWorkerResult<QueryResult>> {
1309 let (reply, result) = oneshot::channel();
1310 self.slot.enqueue(SqliteCommand::Exec {
1311 sql,
1312 enqueued_at,
1313 reply,
1314 })?;
1315 result.await.map_err(|_| sqlite_worker_dead_error())?
1316 }
1317
1318 pub async fn execute(
1319 &self,
1320 sql: String,
1321 params: Option<Vec<BindParam>>,
1322 ) -> Result<ExecuteResult> {
1323 self.execute_inner(sql, params, None).await?.result
1324 }
1325
1326 pub async fn execute_profiled(
1327 &self,
1328 sql: String,
1329 params: Option<Vec<BindParam>>,
1330 ) -> Result<SqliteWorkerResult<ExecuteResult>> {
1331 self.execute_inner(sql, params, Some(Instant::now())).await
1332 }
1333
1334 async fn execute_inner(
1335 &self,
1336 sql: String,
1337 params: Option<Vec<BindParam>>,
1338 enqueued_at: Option<Instant>,
1339 ) -> Result<SqliteWorkerResult<ExecuteResult>> {
1340 let (reply, result) = oneshot::channel();
1341 self.slot.enqueue(SqliteCommand::Execute {
1342 sql,
1343 params,
1344 enqueued_at,
1345 reply,
1346 })?;
1347 result.await.map_err(|_| sqlite_worker_dead_error())?
1348 }
1349
1350 pub async fn execute_batch(
1354 &self,
1355 statements: Vec<(String, Option<Vec<BindParam>>)>,
1356 ) -> Result<Vec<ExecuteResult>> {
1357 let (reply, result) = oneshot::channel();
1358 self.slot
1359 .enqueue(SqliteCommand::ExecuteBatch { statements, reply })?;
1360 result
1361 .await
1362 .map_err(|_| sqlite_worker_dead_error())??
1363 .result
1364 }
1365
1366 pub async fn cork_commits(&self) -> Result<()> {
1370 let (reply, result) = oneshot::channel();
1371 self.slot.enqueue(SqliteCommand::Cork { reply })?;
1372 result.await.map_err(|_| sqlite_worker_dead_error())?
1373 }
1374
1375 pub async fn uncork_commits(&self) -> Result<()> {
1378 let (reply, result) = oneshot::channel();
1379 self.slot.enqueue(SqliteCommand::Uncork { reply })?;
1380 result.await.map_err(|_| sqlite_worker_dead_error())?
1381 }
1382
1383 pub async fn close(&self) -> Result<()> {
1384 self.close_with_timeout(Some(SQLITE_WORKER_CLOSE_TIMEOUT))
1385 .await
1386 }
1387
1388 pub async fn close_and_wait(&self) -> Result<()> {
1390 self.close_with_timeout(None).await
1391 }
1392
1393 async fn close_with_timeout(&self, close_timeout: Option<Duration>) -> Result<()> {
1394 let start = Instant::now();
1395 if self.inner().mark_closing() {
1398 self.slot.request_close();
1399 }
1400
1401 let wait_closed = async {
1402 loop {
1403 let closed = self.inner().closed.notified();
1404 match self.inner().state.load(Ordering::Acquire) {
1405 STATE_CLOSED => return Ok(()),
1406 STATE_DEAD => return Err(sqlite_worker_dead_error()),
1407 STATE_RUNNING | STATE_CLOSING => closed.await,
1408 other => return Err(anyhow!("unknown sqlite worker state {other}")),
1409 }
1410 }
1411 };
1412
1413 match close_timeout {
1414 Some(close_timeout) => match tokio::time::timeout(close_timeout, wait_closed).await {
1415 Ok(result) => result,
1416 Err(_) => {
1417 if let Some(metrics) = &self.inner().metrics {
1418 metrics.record_worker_close_timeout();
1419 }
1420 Err(SqliteWorkerCloseTimeoutError.into())
1423 }
1424 },
1425 None => {
1426 tokio::pin!(wait_closed);
1427 tokio::select! {
1428 result = &mut wait_closed => result,
1429 _ = tokio::time::sleep(SQLITE_WORKER_CLOSE_TIMEOUT) => {
1430 if let Some(metrics) = &self.inner().metrics {
1431 metrics.record_worker_close_timeout();
1432 }
1433 tracing::warn!(
1434 duration_ms = start.elapsed().as_millis() as u64,
1435 "waiting for SQLite worker to finish strict close",
1436 );
1437 wait_closed.await
1438 }
1439 }
1440 }
1441 }
1442 }
1443
1444 pub async fn wait_for_failure(&self) -> bool {
1445 loop {
1446 let closed = self.inner().closed.notified();
1447 match self.inner().state.load(Ordering::Acquire) {
1448 STATE_DEAD => return true,
1449 STATE_CLOSED => return false,
1450 STATE_RUNNING | STATE_CLOSING => closed.await,
1451 _ => return true,
1452 }
1453 }
1454 }
1455}
1456
1457impl SqliteWorkerInner {
1458 fn mark_closing(&self) -> bool {
1459 self.state
1460 .compare_exchange(
1461 STATE_RUNNING,
1462 STATE_CLOSING,
1463 Ordering::AcqRel,
1464 Ordering::Acquire,
1465 )
1466 .is_ok()
1467 }
1468
1469 fn record_queue_depth(&self, depth: u64) {
1470 if let Some(metrics) = &self.metrics {
1471 metrics.set_worker_queue_depth(depth);
1472 }
1473 }
1474}
1475
1476fn warn_if_direct_overflow_reads() {
1481 let enabled = unsafe { sqlite3_compileoption_used(c"DIRECT_OVERFLOW_READ".as_ptr()) } != 0;
1484 if enabled {
1485 tracing::warn!(
1486 "sqlite was built with SQLITE_DIRECT_OVERFLOW_READ, likely without the repo's cargo config, so reads of large rows bypass the page cache and refetch from depot"
1487 );
1488 }
1489}
1490
1491fn open_database_connection(vfs: &NativeVfsHandle, file_name: &str) -> Result<NativeConnection> {
1492 let connection = open_connection(
1493 vfs.clone(),
1494 file_name,
1495 SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
1496 )
1497 .map_err(anyhow::Error::msg)?;
1498 configure_connection_for_database(connection.as_ptr(), vfs, file_name)?;
1499 Ok(connection)
1500}
1501
1502#[allow(clippy::too_many_arguments)]
1503fn run_command(
1504 db: &mut NativeConnection,
1505 vfs: &NativeVfsHandle,
1506 command: SqliteCommand,
1507 max_result_bytes: Option<u64>,
1508 metrics: Option<&dyn SqliteVfsMetrics>,
1509 file_name: &str,
1510 transaction: &mut Option<TransactionTracker>,
1511 pending_uncork: &mut Option<oneshot::Sender<Result<()>>>,
1512 explicit_transaction: &watch::Sender<bool>,
1513) {
1514 let _ = vfs.take_storage_error();
1517 run_command_inner(
1518 db,
1519 vfs,
1520 command,
1521 max_result_bytes,
1522 metrics,
1523 file_name,
1524 transaction,
1525 pending_uncork,
1526 explicit_transaction,
1527 );
1528
1529 if pending_uncork.is_some() && !command_in_tx(db) {
1531 if let Some(reply) = pending_uncork.take() {
1532 let _ = reply.send(db.uncork_commits());
1533 }
1534 }
1535}
1536
1537#[allow(clippy::too_many_arguments)]
1538fn run_command_inner(
1539 db: &mut NativeConnection,
1540 vfs: &NativeVfsHandle,
1541 command: SqliteCommand,
1542 max_result_bytes: Option<u64>,
1543 metrics: Option<&dyn SqliteVfsMetrics>,
1544 file_name: &str,
1545 transaction: &mut Option<TransactionTracker>,
1546 pending_uncork: &mut Option<oneshot::Sender<Result<()>>>,
1547 explicit_transaction: &watch::Sender<bool>,
1548) {
1549 let start = Instant::now();
1550 match command {
1551 SqliteCommand::Execute {
1552 sql,
1553 params,
1554 enqueued_at,
1555 reply,
1556 } => {
1557 if reply.is_closed() {
1558 return;
1559 }
1560 begin_transaction_if_needed(db, transaction);
1561 let in_tx = command_in_tx(db);
1565 let stmt_kind = classify_statement(&sql);
1566 let worker_result = if let Some(enqueued_at) = enqueued_at {
1567 let worker_wait_ns = enqueued_at.elapsed().as_nanos() as u64;
1568 let operation_profile = db.begin_operation_profile();
1569 let execution_start = Instant::now();
1570 let result = execute_single_statement_profiled(
1571 db.as_ptr(),
1572 &sql,
1573 params.as_deref(),
1574 max_result_bytes,
1575 );
1576 let execution_ns = execution_start.elapsed().as_nanos() as u64;
1577 let mut profile = operation_profile.finish();
1578 profile.worker_wait_ns = worker_wait_ns;
1579 profile.sqlite_execution_ns = execution_ns.saturating_sub(profile.storage_ns);
1580 if let Ok((_, query_profile)) = &result {
1581 profile.bind_count = query_profile.bind_count;
1582 profile.bind_logical_bytes = query_profile.bind_logical_bytes;
1583 profile.result_rows = query_profile.result_rows;
1584 profile.result_columns = query_profile.result_columns;
1585 profile.result_logical_bytes = query_profile.result_logical_bytes;
1586 }
1587 record_command_metrics(
1588 metrics,
1589 "execute",
1590 in_tx,
1591 stmt_kind,
1592 &result,
1593 start.elapsed(),
1594 );
1595 SqliteWorkerResult {
1596 result: result.map(|(value, _)| value),
1597 profile,
1598 }
1599 } else {
1600 let result = execute_single_statement(
1601 db.as_ptr(),
1602 &sql,
1603 params.as_deref(),
1604 max_result_bytes,
1605 );
1606 record_command_metrics(
1607 metrics,
1608 "execute",
1609 in_tx,
1610 stmt_kind,
1611 &result,
1612 start.elapsed(),
1613 );
1614 SqliteWorkerResult {
1615 result,
1616 profile: SqliteOperationProfile::default(),
1617 }
1618 };
1619 finalize_transaction_if_complete(db, metrics, file_name, transaction);
1620 publish_explicit_transaction(db, explicit_transaction);
1621 let _ = reply.send(worker_command_result(db, vfs, worker_result));
1622 }
1623 SqliteCommand::Exec {
1624 sql,
1625 enqueued_at,
1626 reply,
1627 } => {
1628 if reply.is_closed() {
1629 return;
1630 }
1631 begin_transaction_if_needed(db, transaction);
1632 let in_tx = command_in_tx(db);
1633 let stmt_kind = classify_statement(&sql);
1634 let worker_result = if let Some(enqueued_at) = enqueued_at {
1635 let worker_wait_ns = enqueued_at.elapsed().as_nanos() as u64;
1636 let operation_profile = db.begin_operation_profile();
1637 let execution_start = Instant::now();
1638 let result = exec_statements_profiled(db.as_ptr(), &sql, max_result_bytes);
1639 let execution_ns = execution_start.elapsed().as_nanos() as u64;
1640 let mut profile = operation_profile.finish();
1641 profile.worker_wait_ns = worker_wait_ns;
1642 profile.sqlite_execution_ns = execution_ns.saturating_sub(profile.storage_ns);
1643 if let Ok((_, query_profile)) = &result {
1644 profile.bind_count = query_profile.bind_count;
1645 profile.bind_logical_bytes = query_profile.bind_logical_bytes;
1646 profile.result_rows = query_profile.result_rows;
1647 profile.result_columns = query_profile.result_columns;
1648 profile.result_logical_bytes = query_profile.result_logical_bytes;
1649 }
1650 record_command_metrics(metrics, "exec", in_tx, stmt_kind, &result, start.elapsed());
1651 SqliteWorkerResult {
1652 result: result.map(|(value, _)| value),
1653 profile,
1654 }
1655 } else {
1656 let result = exec_statements(db.as_ptr(), &sql, max_result_bytes);
1657 record_command_metrics(metrics, "exec", in_tx, stmt_kind, &result, start.elapsed());
1658 SqliteWorkerResult {
1659 result,
1660 profile: SqliteOperationProfile::default(),
1661 }
1662 };
1663 finalize_transaction_if_complete(db, metrics, file_name, transaction);
1664 publish_explicit_transaction(db, explicit_transaction);
1665 let _ = reply.send(worker_command_result(db, vfs, worker_result));
1666 }
1667 SqliteCommand::ExecuteBatch { statements, reply } => {
1668 if reply.is_closed() {
1669 return;
1670 }
1671 begin_transaction_if_needed(db, transaction);
1672 if let Some(tracker) = transaction.as_mut() {
1673 let extra = statements.len().saturating_sub(1) as u64;
1674 tracker.statement_count = tracker.statement_count.saturating_add(extra);
1675 }
1676 let in_tx = command_in_tx(db);
1677 let stmt_kind = statements
1678 .first()
1679 .map_or("other", |(sql, _)| classify_statement(sql));
1680 let mut results = Vec::with_capacity(statements.len());
1681 let mut failure = None;
1682 for (sql, params) in statements {
1683 match execute_single_statement(
1684 db.as_ptr(),
1685 &sql,
1686 params.as_deref(),
1687 max_result_bytes,
1688 ) {
1689 Ok(result) => results.push(result),
1690 Err(error) => {
1691 failure = Some(error);
1692 break;
1693 }
1694 }
1695 }
1696 let result = match failure {
1697 Some(error) => Err(error),
1698 None => Ok(results),
1699 };
1700 record_command_metrics(
1701 metrics,
1702 "execute_batch",
1703 in_tx,
1704 stmt_kind,
1705 &result,
1706 start.elapsed(),
1707 );
1708 finalize_transaction_if_complete(db, metrics, file_name, transaction);
1709 publish_explicit_transaction(db, explicit_transaction);
1710 let worker_result = SqliteWorkerResult {
1711 result,
1712 profile: SqliteOperationProfile::default(),
1713 };
1714 let _ = reply.send(worker_command_result(db, vfs, worker_result));
1715 }
1716 SqliteCommand::Cork { reply } => {
1717 db.cork_commits();
1718 let _ = reply.send(Ok(()));
1719 }
1720 SqliteCommand::Uncork { reply } => {
1721 if !command_in_tx(db) {
1722 let _ = reply.send(db.uncork_commits());
1723 } else if pending_uncork.is_some() {
1724 let _ = reply.send(Err(anyhow::anyhow!(
1725 "an earlier sqlite uncork is still waiting for its transaction"
1726 )));
1727 } else {
1728 *pending_uncork = Some(reply);
1729 }
1730 }
1731 }
1732}
1733
1734fn worker_command_result<T>(
1735 db: &NativeConnection,
1736 vfs: &NativeVfsHandle,
1737 result: SqliteWorkerResult<T>,
1738) -> Result<SqliteWorkerResult<T>> {
1739 if let Some(operation) = vfs.take_indeterminate_result() {
1740 return Err(SqliteCommitIndeterminateError(operation).into());
1741 }
1742
1743 let storage_error = vfs.take_storage_error();
1746 let result = match (storage_error, result) {
1747 (
1748 Some(storage_error),
1749 SqliteWorkerResult {
1750 result: Err(_),
1751 profile,
1752 },
1753 ) => SqliteWorkerResult {
1754 result: Err(storage_error.into()),
1755 profile,
1756 },
1757 (_, result) => result,
1758 };
1759
1760 match (vfs.take_transaction_too_large(), result) {
1763 (
1764 Some(too_large),
1765 SqliteWorkerResult {
1766 result: Err(_),
1767 profile,
1768 },
1769 ) => {
1770 finish_refused_rollback(db);
1771 Ok(SqliteWorkerResult {
1772 result: Err(too_large.into()),
1773 profile,
1774 })
1775 }
1776 (_, result) => Ok(result),
1777 }
1778}
1779
1780fn finish_refused_rollback(db: &NativeConnection) {
1785 if unsafe { sqlite3_get_autocommit(db.as_ptr()) } == 0 {
1786 return;
1787 }
1788 if let Err(error) = sqlite_exec(db.as_ptr(), "PRAGMA main.schema_version;") {
1789 tracing::warn!(
1790 %error,
1791 "sqlite rollback after a refused oversize transaction failed; it retries on the next access"
1792 );
1793 }
1794}
1795
1796fn begin_transaction_if_needed(
1802 db: &mut NativeConnection,
1803 transaction: &mut Option<TransactionTracker>,
1804) {
1805 match transaction {
1806 Some(tracker) => tracker.statement_count = tracker.statement_count.saturating_add(1),
1807 None => {
1808 *transaction = Some(TransactionTracker {
1809 started_at: Instant::now(),
1810 start_round_trips: db.round_trip_counts(),
1811 statement_count: 1,
1812 });
1813 }
1814 }
1815}
1816
1817fn finalize_transaction_if_complete(
1822 db: &mut NativeConnection,
1823 metrics: Option<&dyn SqliteVfsMetrics>,
1824 file_name: &str,
1825 transaction: &mut Option<TransactionTracker>,
1826) {
1827 if command_in_tx(db) {
1830 return;
1831 }
1832 let Some(tracker) = transaction.take() else {
1833 return;
1834 };
1835
1836 let duration = tracker.started_at.elapsed();
1837 let round_trips = db.round_trip_counts().since(tracker.start_round_trips);
1838
1839 if let Some(metrics) = metrics {
1840 metrics.observe_transaction_round_trips(round_trips.get_pages, round_trips.commit);
1841 }
1842
1843 if duration >= SQLITE_SLOW_TRANSACTION_THRESHOLD {
1844 tracing::warn!(
1845 database = %file_name,
1846 duration_ms = duration.as_millis() as u64,
1847 statement_count = tracker.statement_count,
1848 get_pages_round_trips = round_trips.get_pages,
1849 commit_round_trips = round_trips.commit,
1850 total_round_trips = round_trips.total(),
1851 "slow sqlite transaction"
1852 );
1853 } else {
1854 tracing::debug!(
1855 database = %file_name,
1856 duration_ms = duration.as_millis() as u64,
1857 statement_count = tracker.statement_count,
1858 get_pages_round_trips = round_trips.get_pages,
1859 commit_round_trips = round_trips.commit,
1860 total_round_trips = round_trips.total(),
1861 "sqlite transaction complete"
1862 );
1863 }
1864}
1865
1866fn record_command_metrics<T>(
1867 metrics: Option<&dyn SqliteVfsMetrics>,
1868 operation: &'static str,
1869 in_tx: bool,
1870 stmt_kind: &'static str,
1871 result: &Result<T>,
1872 duration: Duration,
1873) {
1874 let Some(metrics) = metrics else {
1875 return;
1876 };
1877 metrics.observe_worker_command_duration(
1878 operation,
1879 in_tx,
1880 stmt_kind,
1881 duration.as_nanos() as u64,
1882 );
1883 if let Err(error) = result {
1884 metrics.record_worker_command_error(operation, worker_error_code(error));
1885 }
1886}
1887
1888fn publish_explicit_transaction(
1891 db: &mut NativeConnection,
1892 explicit_transaction: &watch::Sender<bool>,
1893) {
1894 let open = command_in_tx(db);
1895 explicit_transaction.send_if_modified(|current| {
1896 if *current == open {
1897 return false;
1898 }
1899 *current = open;
1900 true
1901 });
1902}
1903
1904fn command_in_tx(db: &mut NativeConnection) -> bool {
1910 unsafe { sqlite3_get_autocommit(db.as_ptr()) == 0 }
1911}
1912
1913fn classify_statement(sql: &str) -> &'static str {
1919 let trimmed = strip_leading_sql_noise(sql);
1920 let keyword: String = trimmed
1921 .chars()
1922 .take_while(|c| c.is_ascii_alphabetic())
1923 .collect();
1924 match keyword.to_ascii_uppercase().as_str() {
1925 "SELECT" | "WITH" => "select",
1926 "INSERT" | "REPLACE" => "insert",
1927 "UPDATE" => "update",
1928 "DELETE" => "delete",
1929 "BEGIN" => "begin",
1930 "COMMIT" | "END" => "commit",
1931 "ROLLBACK" => "rollback",
1932 "SAVEPOINT" => "savepoint",
1933 "RELEASE" => "release",
1934 "PRAGMA" => "pragma",
1935 "CREATE" => "create",
1936 "DROP" => "drop",
1937 "ALTER" => "alter",
1938 "VACUUM" => "vacuum",
1939 "ANALYZE" => "analyze",
1940 "ATTACH" => "attach",
1941 "DETACH" => "detach",
1942 _ => "other",
1943 }
1944}
1945
1946fn strip_leading_sql_noise(sql: &str) -> &str {
1947 let mut rest = sql.trim_start();
1948 loop {
1949 if let Some(after) = rest.strip_prefix("--") {
1950 rest = match after.find('\n') {
1951 Some(idx) => after[idx + 1..].trim_start(),
1952 None => "",
1953 };
1954 } else if let Some(after) = rest.strip_prefix("/*") {
1955 rest = match after.find("*/") {
1956 Some(idx) => after[idx + 2..].trim_start(),
1957 None => "",
1958 };
1959 } else {
1960 return rest;
1961 }
1962 }
1963}
1964
1965fn fail_command(command: SqliteCommand) {
1966 match command {
1969 SqliteCommand::Execute { reply, .. } => {
1970 let _ = reply.send(Err(sqlite_closing_error()));
1971 }
1972 SqliteCommand::Exec { reply, .. } => {
1973 let _ = reply.send(Err(sqlite_closing_error()));
1974 }
1975 SqliteCommand::ExecuteBatch { reply, .. } => {
1976 let _ = reply.send(Err(sqlite_closing_error()));
1977 }
1978 SqliteCommand::Cork { reply } | SqliteCommand::Uncork { reply } => {
1979 let _ = reply.send(Err(sqlite_closing_error()));
1980 }
1981 }
1982}
1983
1984fn sqlite_closing_error() -> anyhow::Error {
1985 SqliteWorkerClosingError.into()
1986}
1987
1988fn sqlite_worker_dead_error() -> anyhow::Error {
1989 SqliteWorkerDeadError.into()
1990}
1991
1992fn worker_error_code(error: &anyhow::Error) -> &'static str {
1993 if error
1994 .downcast_ref::<SqliteWorkerOverloadedError>()
1995 .is_some()
1996 {
1997 "overloaded"
1998 } else if error.downcast_ref::<SqliteWorkerFatalError>().is_some() {
1999 "fatal"
2000 } else if error.downcast_ref::<SqliteWorkerClosingError>().is_some() {
2001 "closing"
2002 } else if error.downcast_ref::<SqliteWorkerDeadError>().is_some() {
2003 "dead"
2004 } else if error
2005 .downcast_ref::<SqliteWorkerCloseTimeoutError>()
2006 .is_some()
2007 {
2008 "close_timeout"
2009 } else {
2010 "sqlite"
2011 }
2012}
2013
2014#[derive(Debug)]
2015pub struct SqliteWorkerOverloadedError;
2016
2017impl fmt::Display for SqliteWorkerOverloadedError {
2018 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2019 f.write_str("actor.overloaded: sqlite worker command queue is full")
2020 }
2021}
2022
2023impl Error for SqliteWorkerOverloadedError {}
2024
2025#[derive(Debug)]
2026pub struct SqliteWorkerClosingError;
2027
2028impl fmt::Display for SqliteWorkerClosingError {
2029 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2030 f.write_str("sqlite worker is closing")
2031 }
2032}
2033
2034impl Error for SqliteWorkerClosingError {}
2035
2036#[derive(Debug)]
2037pub struct SqliteWorkerDeadError;
2038
2039impl fmt::Display for SqliteWorkerDeadError {
2040 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2041 f.write_str("sqlite worker is closed")
2042 }
2043}
2044
2045impl Error for SqliteWorkerDeadError {}
2046
2047#[derive(Debug)]
2048pub struct SqliteWorkerCloseTimeoutError;
2049
2050impl fmt::Display for SqliteWorkerCloseTimeoutError {
2051 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2052 f.write_str("sqlite worker close timed out")
2053 }
2054}
2055
2056impl Error for SqliteWorkerCloseTimeoutError {}
2057
2058#[derive(Debug, Clone)]
2059pub struct SqliteWorkerFatalError {
2060 message: String,
2061}
2062
2063impl SqliteWorkerFatalError {
2064 pub fn new(message: String) -> Self {
2065 Self { message }
2066 }
2067
2068 pub fn message(&self) -> &str {
2069 &self.message
2070 }
2071}
2072
2073impl fmt::Display for SqliteWorkerFatalError {
2074 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2075 write!(f, "sqlite worker hit fatal storage error: {}", self.message)
2076 }
2077}
2078
2079impl Error for SqliteWorkerFatalError {}
2080
2081fn panic_message(payload: &Box<dyn std::any::Any + Send>) -> String {
2082 if let Some(message) = payload.downcast_ref::<&str>() {
2083 message.to_string()
2084 } else if let Some(message) = payload.downcast_ref::<String>() {
2085 message.clone()
2086 } else {
2087 "unknown panic".to_string()
2088 }
2089}