Skip to main content

sqlite_embedded/
worker.rs

1//! Runs each database's SQLite commands on a shared pool of plain OS threads.
2//!
3//! Every database is a serial queue: at most one pool thread holds its connection at a time,
4//! runs one command, and requeues the database if more work is waiting. A connection with an
5//! open explicit transaction stays with its database between commands and holds no thread.
6//! Pool threads must be plain OS threads, never Tokio workers, because VFS callbacks block on
7//! the Tokio runtime with `block_on` and sleep with `std::thread::sleep`.
8
9use 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
43// Keep the first worker version intentionally fixed-size. A full queue maps to
44// actor.overloaded so callers get explicit backpressure instead of hidden work.
45pub const SQLITE_WORKER_QUEUE_CAPACITY: usize = 128;
46const SQLITE_WORKER_CLOSE_TIMEOUT: Duration = Duration::from_secs(5);
47// Transactions slower than this are logged at warn so operators can spot SQLite
48// work that is stalling on engine round trips. Everything below still logs at
49// debug and records its round trip count to the histogram.
50const SQLITE_SLOW_TRANSACTION_THRESHOLD: Duration = Duration::from_millis(2500);
51
52/// Environment variable that sizes the process pool.
53pub 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;
56/// Explicit, so thousands of databases never imply thousands of 2 MiB default stacks.
57const SQLITE_WORKER_STACK_BYTES: usize = 512 * 1024;
58/// Warn when work has waited behind a pool with no free thread for this long.
59const SQLITE_POOL_SATURATED_WARN: Duration = Duration::from_secs(1);
60/// Shortest time between two allocator trims. A trim walks every allocator arena under its
61/// lock and returns freed pages to the OS, which interrupts every thread of the process, so a
62/// burst of closing databases asks for many trims and gets one.
63const ALLOCATOR_TRIM_INTERVAL: Duration = Duration::from_secs(5);
64/// Background databases get one pool turn in this many while foreground work waits.
65const BACKGROUND_TURN_EVERY: usize = 4;
66/// Due idle releases one pool thread collects per wake.
67const 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
74/// Process-level pool gauges. Implemented by the runtime that owns the pool.
75pub trait SqlitePoolMetrics: Send + Sync {
76	/// The configured thread cap.
77	fn set_pool_threads(&self, threads: u64);
78
79	/// Threads currently running a database action.
80	fn set_pool_busy(&self, busy: u64);
81
82	/// Databases waiting for a thread.
83	fn set_pool_runnable(&self, runnable: u64);
84
85	/// Time a database waited between becoming runnable and a thread taking it.
86	fn observe_pool_queue_wait(&self, wait_ns: u64);
87
88	/// Time a caller waited to take one of the pool's locks: `run_queue` or `idle`.
89	fn observe_pool_lock_wait(&self, lock: &'static str, wait_ns: u64);
90
91	/// Configured capacity, in pages, of the process-wide VFS page budget.
92	fn set_page_buffer_capacity_pages(&self, _pages: u64) {}
93
94	/// Pages the process-wide VFS page budget holds across every database.
95	fn set_page_buffer_pages(&self, _pages: u64) {}
96
97	/// Configured byte budget of the process-wide SQLite pager cache.
98	fn set_pager_cache_capacity_bytes(&self, _bytes: u64) {}
99
100	/// Bytes the process-wide SQLite pager cache holds across every connection.
101	fn set_pager_cache_bytes(&self, _bytes: u64) {}
102
103	/// Pages the process-wide SQLite pager cache holds across every connection.
104	fn set_pager_cache_pages(&self, _pages: u64) {}
105
106	/// Bytes of in-memory and temporary database pages the process-wide SQLite pager cache holds
107	/// outside its budget.
108	fn set_pager_cache_unbudgeted_bytes(&self, _bytes: u64) {}
109}
110
111/// A bounded pool of plain OS threads shared by every database of one runtime process. The
112/// pool spawns threads lazily, up to its cap, only when no thread is free. Cloning shares the
113/// pool; it shuts down once the owner and every database handle have dropped their clones.
114#[derive(Clone)]
115pub struct SqlitePool {
116	owner: Arc<PoolOwner>,
117	page_budget: Arc<PageBudget>,
118}
119
120/// Signals shutdown when the last pool clone drops. Threads finish their current action and
121/// exit; a detached joiner reaps them so dropping never blocks an async caller.
122struct PoolOwner {
123	shared: Arc<PoolShared>,
124}
125
126struct PoolShared {
127	max_threads: usize,
128	/// The run queue. Threads wait on `wake` with this lock held.
129	state: Mutex<PoolState>,
130	/// Idle release bookkeeping, taken only when a database first goes idle, when one is
131	/// cancelled, and by a thread looking for due releases. Lock order: `state`, then `idle`.
132	idle: Mutex<IdleQueue<DatabaseSlot>>,
133	/// Origin of every slot's `last_used_ns`.
134	epoch: Instant,
135	wake: Condvar,
136	metrics: Option<Arc<dyn SqlitePoolMetrics>>,
137}
138
139/// Databases waiting for their idle release, each queued at most once.
140///
141/// An entry is a check time, not the release time: a database used again after it was
142/// queued keeps its entry, and the check moves it to its new due time instead of releasing.
143struct 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	/// Queues `id` at `at`, replacing any earlier entry. Returns whether it became the head,
159	/// which is the only case in which a sleeping thread must wake.
160	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	/// Removes up to `limit` entries due at `now`. `due_at` reads a live item's current due
179	/// time: one still in the future is queued again at that time, and the rest are returned.
180	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
213/// Databases waiting for a thread, in two classes. Foreground work (an actor starting or
214/// serving) goes first, so a mass of actors going to sleep cannot hold a new actor's start
215/// behind their teardown. Background work still gets one turn in every
216/// `BACKGROUND_TURN_EVERY`, so a busy node keeps finishing its sleeps.
217struct RunQueue<T> {
218	foreground: VecDeque<(T, Instant)>,
219	background: VecDeque<(T, Instant)>,
220	/// Foreground turns taken in a row while background work waited.
221	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	/// Connections whose handles were dropped without a close. They are closed on a pool
272	/// thread because closing may flush through the VFS.
273	orphans: Vec<NativeConnection>,
274	threads: Vec<JoinHandle<()>>,
275	/// Threads blocked waiting for work.
276	waiting: usize,
277	busy: usize,
278	saturation: Saturation,
279	next_database: u64,
280	shutdown: bool,
281	/// When the requested allocator trim may run, or `None` when none is requested.
282	trim_due_at: Option<Instant>,
283	/// When the last allocator trim finished.
284	last_trim_at: Option<Instant>,
285}
286
287/// How long the pool has gone without a free thread while work waited. A thread that finishes
288/// one action and takes the next queued one never becomes free, so the stretch continues
289/// across task boundaries; it ends only when a thread waits for work.
290#[derive(Default)]
291struct Saturation {
292	since: Option<Instant>,
293	warned: bool,
294}
295
296impl Saturation {
297	/// A thread is waiting for work, so the pool is not saturated.
298	fn freed(&mut self) {
299		self.since = None;
300		self.warned = false;
301	}
302
303	/// Notes work waiting for a thread at `now`. `saturated` says every thread the pool may
304	/// have exists and none is free. Returns true once per stretch, when it reaches
305	/// `SQLITE_POOL_SATURATED_WARN`.
306	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
319/// One database's serial queue. The connection is either parked here or held by exactly one
320/// pool thread.
321struct 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	/// When this database last finished a command, in nanoseconds since the pool epoch.
330	last_used_ns: AtomicU64,
331	/// Whether this database has an entry in the pool's idle queue.
332	idle_queued: AtomicBool,
333	/// Whether this database's work is background work, such as an actor going to sleep. Read
334	/// each time the database is queued.
335	background: AtomicBool,
336	/// Installed on the connection when it opens; boxed so SQLite's pointer to it stays valid.
337	guard: Option<Box<InstalledGuard>>,
338	work: Mutex<SlotWork>,
339}
340
341/// Diagnostics travel with the existing bounded command queue. Neither SQL nor its
342/// parameters are recorded; disabled TRACE retains no span or subscriber.
343#[derive(Clone)]
344struct WorkerTrace {
345	span: tracing::Span,
346	dispatch: tracing::Dispatch,
347}
348impl Drop for WorkerTrace {
349	fn drop(&mut self) {
350		// The registry closes parent references through the ambient subscriber.
351		// Retained contexts can be released by another worker or request collector.
352		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	/// In the runnable queue or held by a thread.
374	scheduled: bool,
375	/// The idle release came due; the next action releases caches.
376	idle_due: bool,
377	connection: ConnectionSlot,
378	close_requested_at: Option<Instant>,
379}
380
381enum ConnectionSlot {
382	/// Not yet opened; the first action opens it and reports readiness.
383	Unopened(Option<oneshot::Sender<Result<()>>>),
384	/// Parked between actions, possibly inside an explicit transaction.
385	Parked(OpenConnection),
386	/// A pool thread holds it.
387	Running,
388	/// Closed, failed to open, or lost to a panic.
389	Gone,
390}
391
392struct OpenConnection {
393	db: NativeConnection,
394	transaction: Option<TransactionTracker>,
395	/// An uncork that arrived inside an explicit transaction. It runs once the
396	/// connection returns to autocommit.
397	pending_uncork: Option<oneshot::Sender<Result<()>>>,
398}
399
400/// What a pool thread does with a database it took.
401enum Action {
402	Open(Option<oneshot::Sender<Result<()>>>),
403	Run(OpenConnection, QueuedCommand),
404	Release(OpenConnection),
405	Close(Option<OpenConnection>),
406}
407
408/// Work a pool thread takes from the pool.
409enum PoolTask {
410	Database(Arc<DatabaseSlot>, Instant),
411	IdleReleases(Vec<Arc<DatabaseSlot>>),
412	Orphan(NativeConnection),
413	TrimAllocator,
414}
415
416/// The process-wide VFS page budget, sized from the optimization flags. Disabled page caching
417/// gives it no capacity.
418fn 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	/// Reads the thread cap from `RIVETKIT_SQLITE_WORKER_THREADS`.
434	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	/// Starts no threads; the first database action spawns the first one.
445	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		// Every connection opens on a pool, so the first pool installs the pager cache before
452		// SQLite initializes.
453		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	/// The page budget every database on this pool shares.
476	pub fn page_budget(&self) -> &Arc<PageBudget> {
477		&self.page_budget
478	}
479
480	/// Threads spawned so far, never more than the cap.
481	/// Returns freed native memory to the OS soon, on a pool thread, coalescing requests so
482	/// trims run at most once per `ALLOCATOR_TRIM_INTERVAL`. Call after releasing a database.
483	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		// Joining here could block an async caller behind a thread inside a VFS callback.
508		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	/// Queues a database for a thread, spawning one if none is free and the cap allows.
529	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				// No worker can take these slots, including their pending ready senders.
541				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	/// Warns once per stretch in which queued work has waited behind a pool whose every
558	/// thread is busy for longer than `SQLITE_POOL_SATURATED_WARN`.
559	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				// Existing threads still drain the queue; a pool with no thread cannot.
584				tracing::error!(
585					?error,
586					threads = state.threads.len(),
587					"failed to spawn sqlite pool thread"
588				);
589				false
590			}
591		}
592	}
593
594	/// Takes the run queue lock, recording how long it waited.
595	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	/// Records a finished command and queues the database's idle check once, when it first
624	/// goes idle. Later commands only move `last_used_ns`; the check reads it lazily.
625	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			// A thread waiting for an idle check holds the run queue lock from reading the
634			// head until it waits, so taking that lock first means the notify cannot be lost.
635			drop(self.lock_state());
636			self.wake.notify_one();
637		}
638	}
639
640	/// The time `slot`'s idle release is due, from its last use.
641	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	/// Takes the idle checks due at `now`. A database used since it was queued is queued
652	/// again at its new due time; the others are returned for release.
653	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			// A command that finished after the check read its last use saw the database
660			// still queued; queue it again from that use instead of releasing it.
661			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	/// Waits for the next task, or returns `None` at shutdown once the queue is empty.
681	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	/// Asks a pool thread to return freed native memory to the OS, at most once per
725	/// `ALLOCATOR_TRIM_INTERVAL`. A request while one is pending joins it.
726	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		// A waiting thread recomputes its wake time from the new due time.
741		self.wake.notify_one();
742	}
743
744	/// Publishes the number of threads running an action.
745	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/// Returns the allocator's freed pages to the OS. glibc keeps freed memory in its arenas, so a
755/// node that closed many databases would otherwise hold their peak memory.
756#[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	/// Completes an open that no pool thread can service after thread creation failed.
801	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	/// Queues one command, or fails at once when the database is closing or its queue is full.
814	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			// SQL backpressure is actor backpressure. Waiting for capacity here
828			// would let a single actor build hidden native SQLite backlog.
829			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	/// Requests a close that runs after any command a thread is running now.
841	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	/// Queues the database unless it is already queued or running, then releases the lock
857	/// before taking the pool lock.
858	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	/// Takes the connection and the next action, runs it without any lock, then parks the
868	/// connection and requeues the database if more work is waiting.
869	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					// Only the thread holding the database replaces `Running`.
894					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	/// Clears the scheduled flag when a thread found nothing to do.
958	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	/// Runs one action on this thread and returns what to park in the slot.
975	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		// This method is synchronous, including the blocking VFS callbacks. No span
981		// guard crosses an async suspension or follows the connection to another thread.
982		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			// The owning runtime lost the handle without running SQLite shutdown.
1077			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	/// Whether the connection is outside autocommit mode, published before each
1103	/// command's reply so a caller that sees the reply also sees the new state.
1104	/// Reset to `false` when the database stops because the connection is gone.
1105	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	/// Runs statements in order on one pool thread hold, stopping at the first that fails.
1121	ExecuteBatch {
1122		statements: Vec<(String, Option<Vec<BindParam>>)>,
1123		reply: oneshot::Sender<Result<SqliteWorkerResult<Vec<ExecuteResult>>>>,
1124	},
1125	/// Holds later commits in memory; see `NativeDatabase::cork_commits`.
1126	Cork { reply: oneshot::Sender<Result<()>> },
1127	/// Sends the corked commits as one; see `NativeDatabase::uncork_commits`.
1128	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
1156/// Tracks an in-progress SQLite transaction so its wall-clock duration and the
1157/// network round trips it issued can be reported once it commits or rolls back.
1158///
1159/// A transaction begins on the first command run while none is open and ends
1160/// when the connection returns to autocommit mode (an autocommit statement
1161/// finishes, or an explicit `COMMIT`/`ROLLBACK` runs).
1162struct TransactionTracker {
1163	started_at: Instant,
1164	start_round_trips: SqliteRoundTripCounts,
1165	statement_count: u64,
1166}
1167
1168/// Releases an idle database's pager and VFS page caches, unless an explicit transaction is
1169/// open: its pages are the transaction's working set and its next command needs them.
1170fn 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
1179/// Reports the memory this database's VFS holds outside SQLite: its page caches and its read
1180/// page buffers.
1181fn 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	/// Runs the database on a dedicated single-thread pool, for callers without a shared one.
1189	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	/// Registers the database with `pool`. A pool thread opens the connection; `wait_ready`
1198	/// reports the outcome.
1199	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	/// Like [`SqliteWorkerHandle::start_on`], with `guard` installed on the connection and its
1209	/// work budget reset before every command.
1210	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	/// Returns whether the connection has an open explicit transaction, such as
1279	/// one started by a manual `BEGIN`, as of the last completed command.
1280	/// Marks this database's later work as background work, which the pool runs after
1281	/// foreground work. Set when its actor starts shutting down.
1282	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	/// Subscribes to explicit transaction state changes. The value changes
1291	/// before the reply of the command that changed it is delivered.
1292	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	/// Runs `statements` in order as one command, so they take one turn on the pool instead of
1351	/// one turn each. Statements after the first failure do not run. The batch does not open a
1352	/// transaction of its own: run it inside an explicit transaction for atomicity.
1353	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	/// Holds every later commit in memory until `uncork_commits`, which sends them to depot as
1367	/// one commit. Commands queue in order, so the transactions after this call are the ones
1368	/// held. None of them is durable until uncorking succeeds.
1369	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	/// Sends the transactions committed since `cork_commits` to depot as one commit. Inside an
1376	/// explicit transaction it waits until the transaction ends, so the commit includes it.
1377	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	/// Closes the database and waits, without a timeout, until no pool thread holds it.
1389	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		// Close is a control path, not SQL work, so it bypasses the bounded command queue.
1396		// Queued commands fail; a command a thread is running finishes first.
1397		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					// The pool still owns the connection and closes it after the running
1421					// command; the VFS must not be dropped before then.
1422					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
1476/// Warns when SQLite was built with direct overflow reads, which `LIBSQLITE3_FLAGS` in
1477/// `.cargo/config.toml` turns off. A build that misses that config reads whole overflow pages
1478/// straight from the file, past SQLite's page cache, so the VFS hands each one over and drops it
1479/// while nothing caches it, and every read of a large row fetches its pages from depot again.
1480fn warn_if_direct_overflow_reads() {
1481	// SAFETY: the argument is a NUL-terminated static string, and the call only scans SQLite's
1482	// static list of compile options.
1483	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	// A storage failure belongs to the command whose statement saw it. One that a command
1515	// left unreported, such as an uncork's, must not relabel a later command's own failure.
1516	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	// A deferred uncork runs as soon as the transaction it waited on ends.
1530	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			// Read the transaction state before running so the label reflects the
1562			// transaction the statement executed against, not the state it leaves
1563			// behind (a BEGIN flips autocommit off, a COMMIT flips it back on).
1564			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	// A statement that failed on a storage request that changed nothing, a refusal with a
1744	// retry hint or a timeout, reports that cause instead of SQLite's generic "disk I/O error".
1745	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	// A statement that failed because its transaction crossed the commit page cap reports that
1761	// cause instead of SQLite's generic "database or disk is full".
1762	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
1780/// Makes SQLite finish rolling back a transaction refused for its size before the command
1781/// returns. A refused cache spill leaves SQLite's rollback for its next access to the database,
1782/// so until then the VFS still holds the abandoned pages. Reading the schema version is that
1783/// access, and it restores the pages from the journal and commits them, releasing the memory.
1784fn 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
1796/// Opens a transaction tracker before running a command when none is active.
1797///
1798/// The first command after autocommit resumes starts a new transaction, whether
1799/// it is an autocommit statement or an explicit `BEGIN`. The round trip baseline
1800/// is captured here so the delta covers the whole transaction.
1801fn 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
1817/// Reports a transaction once the connection returns to autocommit mode.
1818///
1819/// Always records the round trip count to the histogram and logs at debug.
1820/// Transactions slower than `SQLITE_SLOW_TRANSACTION_THRESHOLD` also log at warn.
1821fn finalize_transaction_if_complete(
1822	db: &mut NativeConnection,
1823	metrics: Option<&dyn SqliteVfsMetrics>,
1824	file_name: &str,
1825	transaction: &mut Option<TransactionTracker>,
1826) {
1827	// Autocommit off means an explicit transaction is still open across the next
1828	// command, so the transaction has not finished yet.
1829	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
1888/// Publishes the connection's explicit transaction state, waking subscribers
1889/// only when it changed.
1890fn 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
1904/// Returns whether an explicit transaction is open on the connection.
1905///
1906/// `sqlite3_get_autocommit` returns non-zero in autocommit mode (no open
1907/// transaction) and zero once a `BEGIN` opens one. Implicit per-statement
1908/// transactions keep autocommit on, so this only reports explicit transactions.
1909fn command_in_tx(db: &mut NativeConnection) -> bool {
1910	unsafe { sqlite3_get_autocommit(db.as_ptr()) == 0 }
1911}
1912
1913/// Classifies a SQL command by its leading keyword for metric labeling.
1914///
1915/// Leading line and block comments are skipped so generated SQL with a header
1916/// comment still classifies by its first statement. Multi-statement `exec`
1917/// payloads are labeled by their first statement.
1918fn 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	// Cancelled requests may sit in the bounded queue until the worker observes
1967	// them. Once observed during shutdown, they fail instead of running.
1968	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}