Skip to main content

sqlite_embedded/vfs/
transport.rs

1//! Custom SQLite VFS backed by KV operations over the KV channel.
2//!
3//! This crate owns the KV-backed SQLite behavior used by `rivetkit-napi`.
4
5use std::collections::{BTreeMap, HashMap, HashSet};
6use std::ffi::{CStr, CString, c_char, c_int, c_void};
7use std::ptr;
8use std::slice;
9use std::sync::Arc;
10use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
11use std::time::{Duration, Instant};
12
13use crate::page_buffer::{PageBudget, PageBuffer, PageTrace};
14use anyhow::Result;
15use async_trait::async_trait;
16use futures_util::stream::{self, StreamExt};
17use libsqlite3_sys::*;
18use parking_lot::{Mutex, RwLock};
19use rivet_envoy_protocol as protocol;
20use sqlite_types::{PageBytes, is_head_fence_mismatch};
21use tokio::runtime::Handle;
22use tracing::Instrument;
23
24use crate::optimization_flags::{
25	SqliteOptimizationFlags, SqliteVfsPageCacheMode, sqlite_optimization_flags,
26};
27use crate::sqlite_page::{PageClass, classify};
28
29const DEFAULT_MAX_PAGES_PER_STAGE: usize = 4_000;
30const DEFAULT_PAGE_SIZE: usize = 4096;
31const STAGE_PIPELINE_BYTE_BUDGET: usize = 8 * 1024 * 1024;
32const STAGE_MAX_SEGMENT_BYTES: usize = sqlite_types::SHARD_SIZE as usize
33	* sqlite_types::COMMIT_SEGMENT_MAX_SHARDS as usize
34	* DEFAULT_PAGE_SIZE;
35const STAGE_PIPELINE_DEPTH: usize = STAGE_PIPELINE_BYTE_BUDGET / STAGE_MAX_SEGMENT_BYTES;
36const NATIVE_DATABASE_DROP_FLUSH_TIMEOUT: Duration = Duration::from_millis(250);
37const MAX_PATHNAME: c_int = 64;
38const TEMP_AUX_PATH_PREFIX: &str = "__sqlite_temp__";
39const SQLITE_HEADER_MAGIC: &[u8; 16] = b"SQLite format 3\0";
40const EMPTY_DB_PAGE_HEADER_PREFIX: [u8; 108] = [
41	83, 81, 76, 105, 116, 101, 32, 102, 111, 114, 109, 97, 116, 32, 51, 0, 16, 0, 1, 1, 0, 64, 32,
42	32, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
43	0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
44	0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 46, 138, 17, 13, 0, 0, 0, 0, 16, 0, 0,
45];
46
47static NEXT_TEMP_AUX_ID: AtomicU64 = AtomicU64::new(1);
48
49unsafe extern "C" {
50	fn sqlite3_close_v2(db: *mut sqlite3) -> c_int;
51}
52
53fn empty_db_page() -> Vec<u8> {
54	let mut page = vec![0u8; DEFAULT_PAGE_SIZE];
55	page[..EMPTY_DB_PAGE_HEADER_PREFIX.len()].copy_from_slice(&EMPTY_DB_PAGE_HEADER_PREFIX);
56	page
57}
58
59fn sqlite_header_page_size(page: &[u8]) -> Option<usize> {
60	if page.len() < 100 || &page[..SQLITE_HEADER_MAGIC.len()] != SQLITE_HEADER_MAGIC {
61		return None;
62	}
63
64	let raw = u16::from_be_bytes([page[16], page[17]]);
65	let page_size = if raw == 1 { 65_536 } else { usize::from(raw) };
66
67	if (512..=65_536).contains(&page_size) && page_size.is_power_of_two() {
68		Some(page_size)
69	} else {
70		None
71	}
72}
73
74fn sqlite_header_db_size_pages(page: &[u8]) -> Option<u32> {
75	if page.len() < 100 || &page[..SQLITE_HEADER_MAGIC.len()] != SQLITE_HEADER_MAGIC {
76		return None;
77	}
78
79	let db_size_pages = u32::from_be_bytes([page[28], page[29], page[30], page[31]]);
80	Some(db_size_pages.max(1))
81}
82
83fn panic_message(payload: &Box<dyn std::any::Any + Send>) -> String {
84	if let Some(message) = payload.downcast_ref::<&str>() {
85		message.to_string()
86	} else if let Some(message) = payload.downcast_ref::<String>() {
87		message.clone()
88	} else {
89		"unknown panic".to_string()
90	}
91}
92
93macro_rules! vfs_catch_unwind {
94	($err_val:expr, $body:expr) => {
95		match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| $body)) {
96			Ok(result) => result,
97			Err(panic) => {
98				tracing::error!(msg = panic_message(&panic), "sqlite callback panicked");
99				$err_val
100			}
101		}
102	};
103}
104
105#[async_trait]
106pub trait SqliteTransport: Send + Sync {
107	async fn get_pages(
108		&self,
109		request: protocol::SqliteGetPagesRequest,
110	) -> Result<protocol::SqliteGetPagesResponse>;
111
112	async fn commit(
113		&self,
114		request: protocol::SqliteCommitRequest,
115	) -> Result<protocol::SqliteCommitResponse>;
116
117	/// Opens a staged commit, for a commit too large to send in one message.
118	///
119	/// Required rather than defaulted. A default made staging look optional, and the transport that
120	/// runs `depot vacuum` in-process silently took it: every commit above the single-shot threshold
121	/// failed there, which is exactly the commit shape a vacuum produces. A transport that genuinely
122	/// cannot stage has to say so in its own body.
123	async fn commit_stage_begin(
124		&self,
125		request: protocol::SqliteCommitStageBeginRequest,
126	) -> Result<protocol::SqliteCommitStageBeginResponse>;
127
128	/// Stages one shard-aligned segment of an open staged commit.
129	async fn commit_stage_segment(
130		&self,
131		request: protocol::SqliteCommitStageSegmentRequest,
132	) -> Result<protocol::SqliteCommitStageSegmentResponse>;
133
134	/// Publishes an open staged commit, making every staged segment visible at once.
135	async fn commit_finalize(
136		&self,
137		request: protocol::SqliteCommitFinalizeRequest,
138	) -> Result<protocol::SqliteCommitFinalizeResponse>;
139}
140
141pub type SqliteTransportHandle = Arc<dyn SqliteTransport>;
142fn sqlite_now_ms() -> Result<i64> {
143	use std::time::{SystemTime, UNIX_EPOCH};
144
145	Ok(SystemTime::now()
146		.duration_since(UNIX_EPOCH)?
147		.as_millis()
148		.try_into()?)
149}
150
151#[derive(Debug, Clone)]
152pub struct VfsConfig {
153	/// The process-wide budget this database's page buffers share. A database opened on a
154	/// worker pool uses the pool's budget.
155	pub page_budget: Arc<PageBudget>,
156	pub page_cache_mode: SqliteVfsPageCacheMode,
157	pub staging_cache_ttl_ms: u64,
158	pub max_pages_per_stage: usize,
159	/// Idle time after which the worker releases this database's caches. Zero disables.
160	pub idle_release_ms: u64,
161	/// The most pages one transaction may change. Values above Depot's own commit cap,
162	/// `sqlite_types::MAX_COMMIT_DIRTY_PAGES`, are clamped to it.
163	pub max_commit_dirty_pages: usize,
164	
165}
166
167impl Default for VfsConfig {
168	fn default() -> Self {
169		Self::from_optimization_flags(*sqlite_optimization_flags())
170	}
171}
172
173impl VfsConfig {
174	/// The most pages one transaction may change, never above Depot's commit cap.
175	pub fn commit_page_cap(&self) -> usize {
176		self.max_commit_dirty_pages
177			.min(sqlite_types::MAX_COMMIT_DIRTY_PAGES)
178	}
179
180	pub fn from_optimization_flags(flags: SqliteOptimizationFlags) -> Self {
181		let caches_pages = flags.vfs_page_cache_mode.caches_any_pages();
182		Self {
183			page_budget: PageBudget::new(
184				if caches_pages {
185					flags.vfs_page_buffer_process_pages
186				} else {
187					0
188				},
189				if caches_pages {
190					flags.vfs_staging_cache_ttl_ms
191				} else {
192					0
193				},
194				None,
195			),
196			page_cache_mode: flags.vfs_page_cache_mode,
197			staging_cache_ttl_ms: if caches_pages {
198				flags.vfs_staging_cache_ttl_ms
199			} else {
200				0
201			},
202			max_pages_per_stage: DEFAULT_MAX_PAGES_PER_STAGE,
203			idle_release_ms: flags.idle_release_ms,
204			max_commit_dirty_pages: sqlite_types::MAX_COMMIT_DIRTY_PAGES,
205			
206		}
207	}
208}
209
210#[derive(Debug, Clone, Default)]
211pub(crate) struct InitialPages {
212	pub pages: Vec<(u32, PageBytes)>,
213	pub head_txid: Option<u64>,
214	pub requested_page_count: u32,
215}
216
217impl From<Vec<(u32, Vec<u8>)>> for InitialPages {
218	fn from(pages: Vec<(u32, Vec<u8>)>) -> Self {
219		let requested_page_count = pages.len().try_into().unwrap_or(u32::MAX);
220		Self {
221			pages: pages
222				.into_iter()
223				.map(|(pgno, bytes)| (pgno, bytes.into()))
224				.collect(),
225			head_txid: None,
226			requested_page_count,
227		}
228	}
229}
230
231#[derive(Debug, Clone, PartialEq, Eq)]
232pub enum CommitPath {
233	Fast,
234	Slow,
235	/// Written as several staged segments plus a finalize, because the commit was too large to send
236	/// in one message.
237	Staged,
238}
239
240#[derive(Debug, Clone)]
241pub struct BufferedCommitRequest {
242	pub actor_id: String,
243	pub new_db_size_pages: u32,
244	pub dirty_pages: Vec<BufferedDirtyPage>,
245	pub expected_head_txid: Option<u64>,
246}
247
248#[derive(Debug, Clone)]
249pub struct BufferedDirtyPage {
250	pub pgno: u32,
251	pub bytes: PageBytes,
252}
253
254#[derive(Debug, Clone)]
255pub struct BufferedCommitOutcome {
256	pub path: CommitPath,
257	pub db_size_pages: u32,
258	pub head_txid: Option<u64>,
259}
260
261#[derive(Debug, Clone, PartialEq, Eq)]
262pub enum CommitBufferError {
263	FenceMismatch(String),
264	Indeterminate(String),
265	/// Storage refused the commit before running it, or its request timed out before it was
266	/// sent. Nothing was committed, so the transaction rolls back and may run again.
267	Storage(SqliteStorageError),
268	Other(String),
269}
270
271/// A SQLite storage request that failed without changing anything, in a way the caller can act
272/// on. The VFS reports it to SQLite as an I/O error, and the statement's command returns it
273/// instead of SQLite's generic `disk I/O error`.
274#[derive(Debug, Clone, PartialEq, Eq)]
275pub enum SqliteStorageError {
276	/// Storage refused the request before running it, as when the volume's writes are ahead of
277	/// its tablet's apply. The request may be sent again after `retry_after_ms`.
278	Overloaded {
279		retry_after_ms: u64,
280		message: String,
281	},
282	/// The request's deadline passed before storage answered a read, or before a write was
283	/// sent. A write whose request was sent has an unknown outcome instead.
284	Timeout { operation: String },
285}
286
287impl std::fmt::Display for SqliteStorageError {
288	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289		match self {
290			Self::Overloaded {
291				retry_after_ms,
292				message,
293			} => write!(
294				f,
295				"sqlite storage is overloaded, retry after {retry_after_ms} ms: {message}"
296			),
297			Self::Timeout { operation } => {
298				write!(
299					f,
300					"sqlite storage {operation} timed out before storage answered"
301				)
302			}
303		}
304	}
305}
306
307impl std::error::Error for SqliteStorageError {}
308
309/// A transport's report that a SQLite request's deadline passed before its response arrived,
310/// for a read or for a write it never sent. A transport reports a sent write that timed out as
311/// [`SqliteCommitIndeterminateError`] instead, since storage may still have committed it.
312#[derive(Debug)]
313pub struct SqliteStorageTimeoutError {
314	pub operation: String,
315}
316
317impl std::fmt::Display for SqliteStorageTimeoutError {
318	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319		write!(f, "sqlite {} request timed out", self.operation)
320	}
321}
322
323impl std::error::Error for SqliteStorageTimeoutError {}
324
325/// The typed failure a storage error response stands for, if it is a refusal with a hint.
326fn storage_refusal(error: &protocol::SqliteErrorResponse) -> Option<SqliteStorageError> {
327	let retry_after_ms =
328		sqlite_types::storage_overload_retry_after_ms(&error.group, &error.code, &error.message)?;
329	Some(SqliteStorageError::Overloaded {
330		retry_after_ms,
331		message: error.message.to_string(),
332	})
333}
334
335/// The typed failure a transport error stands for, if it is a timeout that changed nothing.
336fn storage_timeout(error: &anyhow::Error) -> Option<SqliteStorageError> {
337	error
338		.downcast_ref::<SqliteStorageTimeoutError>()
339		.map(|timeout| SqliteStorageError::Timeout {
340			operation: timeout.operation.clone(),
341		})
342}
343
344/// Marks a lost commit reply so the VFS can fail closed instead of using stale state.
345#[derive(Debug)]
346pub struct SqliteCommitIndeterminateError(pub String);
347
348impl std::fmt::Display for SqliteCommitIndeterminateError {
349	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
350		write!(f, "sqlite {} result is indeterminate", self.0)
351	}
352}
353
354impl std::error::Error for SqliteCommitIndeterminateError {}
355
356/// A transaction tried to change more pages than one commit may carry.
357///
358/// The VFS refuses the write that would cross the cap, before any of the transaction reaches
359/// Depot, and SQLite rolls the transaction back. The connection stays usable.
360#[derive(Debug, Clone, PartialEq, Eq)]
361pub struct SqliteTransactionTooLargeError {
362	pub max_pages: usize,
363	pub page_size: usize,
364}
365
366impl std::fmt::Display for SqliteTransactionTooLargeError {
367	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368		let max_kib = self.max_pages * self.page_size / 1024;
369		let max_size = if max_kib >= 1024 {
370			format!("{} MiB", max_kib / 1024)
371		} else {
372			format!("{max_kib} KiB")
373		};
374		write!(
375			f,
376			"sqlite transaction changes more than {} pages ({}), the most one commit can hold, \
377			 so the statement failed and its changes were rolled back; split the work into smaller \
378			 transactions. VACUUM rewrites the whole \
379			 database in one transaction, so it only runs while the database's in-use pages fit under \
380			 this limit",
381			self.max_pages, max_size,
382		)
383	}
384}
385
386impl std::error::Error for SqliteTransactionTooLargeError {}
387
388/// A file control opcode that reads this VFS's commit page cap into an `i64`. SQLite reserves
389/// opcodes up to 100 for itself.
390pub(crate) const FCNTL_COMMIT_PAGE_CAP: c_int = 0x5256_0001;
391
392#[derive(Debug, Clone, Copy, Default)]
393pub struct SqliteVfsMetricsSnapshot {
394	pub request_build_ns: u64,
395	pub serialize_ns: u64,
396	pub transport_ns: u64,
397	pub state_update_ns: u64,
398	pub total_ns: u64,
399	pub commit_count: u64,
400	pub page_cache_entries: u64,
401	pub page_cache_weighted_size: u64,
402	pub page_cache_capacity_pages: u64,
403	pub write_buffer_dirty_pages: u64,
404	pub db_size_pages: u64,
405}
406
407pub const MAX_PROFILED_GET_PAGES_REQUESTS: usize = 16;
408pub const MAX_PROFILED_TRANSACTION_STATEMENTS: usize = 32;
409
410/// Bounded details for one physical `get_pages` call made while a native SQLite
411/// command is executing.
412#[derive(Debug, Clone, Default)]
413pub struct SqliteGetPagesProfile {
414	pub ordinal: u64,
415	pub duration_ns: u64,
416	pub demand_requested: u64,
417	pub response_present: u64,
418	pub response_missing: u64,
419	pub overflow_expansion_extra: u64,
420	pub response_bytes: u64,
421	pub success: bool,
422}
423
424/// Fixed-size, connection-local counters collected by VFS callbacks for one
425/// native SQLite command. The worker installs and removes this context around
426/// each command, so VFS activity cannot be attributed to a neighboring query.
427#[derive(Debug, Clone, Default)]
428pub struct SqliteOperationProfile {
429	pub worker_wait_ns: u64,
430	pub sqlite_execution_ns: u64,
431	pub storage_ns: u64,
432	pub bind_count: u64,
433	pub bind_logical_bytes: u64,
434	pub result_rows: u64,
435	pub result_columns: u64,
436	pub result_logical_bytes: u64,
437	pub sqlite_requested_pages: u64,
438	pub cache_hit_pages: u64,
439	pub cache_miss_pages: u64,
440	pub depot_demand_requested_pages: u64,
441	pub response_present_pages: u64,
442	pub response_missing_pages: u64,
443	pub overflow_expansion_extra_pages: u64,
444	pub btree_pages: u64,
445	pub non_btree_pages: u64,
446	pub prefetch_consumed_pages: u64,
447	pub prefetch_unused_pages: u64,
448	pub dirty_pages: u64,
449	pub storage_response_bytes: u64,
450	pub dirty_bytes: u64,
451	pub get_pages_round_trips: u64,
452	pub commit_round_trips: u64,
453	pub get_pages_requests: [Option<SqliteGetPagesProfile>; MAX_PROFILED_GET_PAGES_REQUESTS],
454	pub omitted_get_pages_requests: u64,
455}
456
457#[derive(Debug, Clone)]
458pub struct SqliteOperationMetric {
459	pub operation_type: &'static str,
460	pub fingerprint: String,
461	pub fingerprint_source: &'static str,
462	pub transaction_mode: &'static str,
463	pub storage_transport: &'static str,
464	pub outcome: &'static str,
465	pub sql_bytes: u64,
466	pub total_ns: u64,
467	pub transaction_wait_ns: u64,
468	pub profile: SqliteOperationProfile,
469}
470
471#[derive(Debug, Clone)]
472pub struct SqliteTransactionMetric {
473	pub fingerprint: String,
474	pub fingerprint_source: &'static str,
475	pub shape_fingerprint: String,
476	pub statement_fingerprint_hashes: [Option<[u8; 16]>; MAX_PROFILED_TRANSACTION_STATEMENTS],
477	pub omitted_statement_fingerprints: u64,
478	pub storage_transport: &'static str,
479	pub outcome: &'static str,
480	pub total_ns: u64,
481	pub transaction_wait_ns: u64,
482	pub worker_wait_ns: u64,
483	pub storage_ns: u64,
484	pub local_work_ns: u64,
485	pub application_time_ns: u64,
486	pub commit_ns: u64,
487	pub get_pages_round_trips: u64,
488	pub statement_count: u64,
489	pub dirty_pages: u64,
490	pub dirty_bytes: u64,
491}
492
493impl SqliteOperationProfile {
494	fn push_get_pages(&mut self, request: SqliteGetPagesProfile, request_limit: usize) {
495		self.storage_ns = self.storage_ns.saturating_add(request.duration_ns);
496		self.get_pages_round_trips = self.get_pages_round_trips.saturating_add(1);
497		self.depot_demand_requested_pages = self
498			.depot_demand_requested_pages
499			.saturating_add(request.demand_requested);
500		self.response_present_pages = self
501			.response_present_pages
502			.saturating_add(request.response_present);
503		self.response_missing_pages = self
504			.response_missing_pages
505			.saturating_add(request.response_missing);
506		self.overflow_expansion_extra_pages = self
507			.overflow_expansion_extra_pages
508			.saturating_add(request.overflow_expansion_extra);
509		self.storage_response_bytes = self
510			.storage_response_bytes
511			.saturating_add(request.response_bytes);
512		let index = self.get_pages_round_trips.saturating_sub(1) as usize;
513		if index < request_limit
514			&& let Some(slot) = self.get_pages_requests.get_mut(index)
515		{
516			*slot = Some(request);
517		} else {
518			self.omitted_get_pages_requests = self.omitted_get_pages_requests.saturating_add(1);
519		}
520	}
521}
522
523pub(crate) struct SqliteOperationProfileGuard<'a> {
524	ctx: &'a VfsContext,
525	active: bool,
526}
527
528impl SqliteOperationProfileGuard<'_> {
529	pub(crate) fn finish(mut self) -> SqliteOperationProfile {
530		self.active = false;
531		self.ctx.finish_operation_profile()
532	}
533}
534
535impl Drop for SqliteOperationProfileGuard<'_> {
536	fn drop(&mut self) {
537		if self.active {
538			let _ = self.ctx.finish_operation_profile();
539		}
540	}
541}
542
543/// Cumulative count of network round trips the VFS has issued to the engine.
544///
545/// `get_pages` counts `SqliteGetPagesRequest` fetches and `commit` counts
546/// `SqliteCommitRequest` commits. Diffing two snapshots gives the round trips
547/// performed by the work that ran between them, such as a SQLite transaction.
548#[derive(Debug, Clone, Copy, Default)]
549pub struct SqliteRoundTripCounts {
550	pub get_pages: u64,
551	pub commit: u64,
552}
553
554impl SqliteRoundTripCounts {
555	pub fn total(&self) -> u64 {
556		self.get_pages.saturating_add(self.commit)
557	}
558
559	pub fn since(&self, earlier: SqliteRoundTripCounts) -> SqliteRoundTripCounts {
560		SqliteRoundTripCounts {
561			get_pages: self.get_pages.saturating_sub(earlier.get_pages),
562			commit: self.commit.saturating_sub(earlier.commit),
563		}
564	}
565}
566
567#[derive(Debug, Clone, Copy)]
568pub enum SqliteOpenPhase {
569	InitialPreload,
570	VfsRegister,
571	WorkerReady,
572	Total,
573}
574
575impl SqliteOpenPhase {
576	pub fn as_label(self) -> &'static str {
577		match self {
578			SqliteOpenPhase::InitialPreload => "initial_preload",
579			SqliteOpenPhase::VfsRegister => "vfs_register",
580			SqliteOpenPhase::WorkerReady => "worker_ready",
581			SqliteOpenPhase::Total => "total",
582		}
583	}
584}
585
586pub trait SqliteVfsMetrics: Send + Sync {
587	fn profiling_enabled(&self) -> bool;
588
589	fn max_profiled_get_pages_requests(&self) -> usize;
590
591	/// Records an operation and returns whether its selected fingerprint was admitted.
592	fn observe_operation_profile(&self, profile: &SqliteOperationMetric) -> bool;
593
594	/// Records a transaction and returns whether its selected fingerprint was admitted.
595	fn observe_transaction_profile(&self, profile: &SqliteTransactionMetric) -> bool;
596
597	/// Selects a diagnostic before its caller computes SQL fingerprints.
598	fn operation_diagnostic_selected(&self, _profile: &SqliteOperationMetric) -> bool {
599		true
600	}
601
602	fn transaction_diagnostic_selected(&self, _profile: &SqliteTransactionMetric) -> bool {
603		true
604	}
605
606	/// Emits an event already selected by operation_diagnostic_selected.
607	fn emit_operation_diagnostic_event(
608		&self,
609		actor_id: &str,
610		generation: Option<u64>,
611		profile: &SqliteOperationMetric,
612	);
613
614	fn emit_transaction_diagnostic_event(
615		&self,
616		actor_id: &str,
617		generation: Option<u64>,
618		profile: &SqliteTransactionMetric,
619	);
620
621	fn record_fingerprint_catalog(
622		&self,
623		operation_type: &'static str,
624		fingerprint: &str,
625		identity: &str,
626		format_version: u8,
627	);
628
629	fn record_resolve_pages(&self, requested_pages: u64);
630
631	fn record_resolve_cache_hits(&self, pages: u64);
632
633	fn record_resolve_cache_misses(&self, pages: u64);
634
635	fn record_get_pages_request(&self, pages: u64, prefetch_pages: u64, page_size: u64);
636
637	fn observe_get_pages_duration(&self, duration_ns: u64);
638
639	fn observe_open_phase(&self, phase: SqliteOpenPhase, outcome: &'static str, duration_ns: u64);
640
641	fn record_startup_preload_pages(&self, kind: &'static str, pages: u64);
642
643	/// Records one successful commit and the dirty pages it wrote.
644	fn record_commit(&self, dirty_pages: u64);
645
646	fn observe_commit_phases(
647		&self,
648		request_build_ns: u64,
649		serialize_ns: u64,
650		transport_ns: u64,
651		state_update_ns: u64,
652		total_ns: u64,
653	);
654
655	fn set_worker_queue_depth(&self, depth: u64);
656
657	fn set_worker_active(&self, active: bool);
658
659	fn set_worker_inflight(&self, active: bool);
660
661	/// Reports the bytes this database's VFS page caches hold, after each command, after an
662	/// idle release and as zero when the worker stops.
663	fn set_vfs_cached_bytes(&self, bytes: u64);
664	/// Prefetched pages discarded before their first use, including close and lazy TTL expiry.
665	fn record_prefetch_unused_pages(&self, _pages: u64) {}
666
667	fn set_coordinator_queue_depth(&self, depth: u64);
668
669	fn record_worker_queue_overload(&self);
670
671	fn observe_worker_command_duration(
672		&self,
673		operation: &'static str,
674		in_tx: bool,
675		stmt_kind: &'static str,
676		duration_ns: u64,
677	);
678
679	fn observe_transaction_round_trips(&self, get_pages_round_trips: u64, commit_round_trips: u64);
680
681	fn record_worker_command_error(&self, operation: &'static str, code: &'static str);
682
683	fn observe_worker_close_duration(&self, duration_ns: u64);
684
685	fn record_worker_close_timeout(&self);
686
687	fn record_worker_crash(&self);
688
689	fn record_worker_unclean_close(&self);
690}
691
692#[derive(Debug, Clone, Copy, Default)]
693struct CommitTransportMetrics {
694	serialize_ns: u64,
695	transport_ns: u64,
696}
697
698enum CommitWait<T> {
699	Completed(T),
700	TimedOut,
701}
702
703pub struct VfsContext {
704	actor_id: String,
705	generation: Option<u64>,
706	runtime: Handle,
707	transport: SqliteTransportHandle,
708	config: VfsConfig,
709	state: RwLock<VfsState>,
710	aux_files: RwLock<BTreeMap<String, Arc<AuxFileState>>>,
711	last_error: Mutex<Option<String>>,
712	indeterminate_result: Mutex<Option<String>>,
713	transient_commit_error: Mutex<Option<String>>,
714	/// Set when a write was refused for crossing the commit page cap, until the worker reports it.
715	transaction_too_large: Mutex<Option<SqliteTransactionTooLargeError>>,
716	/// The typed storage failure behind the I/O error SQLite last saw, for the command that
717	/// ran the statement to return instead of SQLite's generic message.
718	storage_error: Mutex<Option<SqliteStorageError>>,
719	fatal_error: RwLock<Option<String>>,
720	
721	
722	
723	
724	commit_atomic_count: AtomicU64,
725	/// Diagnostic attempts, distinct even when a refused/timed-out commit did not advance commit_total.
726	trace_commit_attempts: AtomicU64,
727	io_methods: Box<sqlite3_io_methods>,
728	// Performance counters
729	pub resolve_pages_total: AtomicU64,
730	pub resolve_pages_cache_hits: AtomicU64,
731	pub resolve_pages_fetches: AtomicU64,
732	pub pages_fetched_total: AtomicU64,
733	pub prefetch_pages_total: AtomicU64,
734	/// Pages SQLite read from what the server sent beyond the requested pages.
735	pub prefetch_consumed_total: AtomicU64,
736	pub commit_total: AtomicU64,
737	pub commit_request_build_ns: AtomicU64,
738	pub commit_serialize_ns: AtomicU64,
739	pub commit_transport_ns: AtomicU64,
740	pub commit_state_update_ns: AtomicU64,
741	pub commit_duration_ns_total: AtomicU64,
742	// SQLite invokes VFS callbacks synchronously, so this profiling context cannot
743	// use an async mutex. The native worker releases each guard before returning
744	// to async code.
745	operation_profile_active: AtomicBool,
746	operation_profile: Mutex<Option<SqliteOperationProfile>>,
747	operation_prefetch_unused_start: AtomicU64,
748	reported_prefetch_unused: AtomicU64,
749	metrics: Option<Arc<dyn SqliteVfsMetrics>>,
750}
751
752#[derive(Debug, Clone)]
753struct VfsState {
754	db_size_pages: u32,
755	/// Size depot last acknowledged. `db_size_pages` moving away from this is a pending size change
756	/// that has to reach depot even when no page bytes are dirty, because SQLite reports a shrink
757	/// through `xTruncate` alone.
758	committed_db_size_pages: u32,
759	head_txid: Option<u64>,
760	page_size: usize,
761	/// Pages fetched ahead of SQLite's requests. A page leaves when SQLite reads it.
762	prefetch_buffer: PageBuffer,
763	/// Pages this database committed in the last `staging_cache_ttl_ms`, for re-reads after
764	/// SQLite's pager evicts them. A page leaves when SQLite reads it back.
765	committed_buffer: PageBuffer,
766	prefetch_unused_total: Arc<AtomicU64>,
767	write_buffer: WriteBuffer,
768	/// Pages of transactions SQLite committed while commits are corked, not yet sent to depot.
769	/// `Some` while corked: each commit adds its pages here instead of committing, and uncorking
770	/// sends them all as one commit. Reads see these pages before any cache.
771	corked: Option<BTreeMap<u32, PageBytes>>,
772	dead: bool,
773	/// Database size after the last transaction moved into the cork, which a commit of the cork
774	/// alone carries.
775	corked_db_size_pages: u32,
776	/// Set when a main database write was refused for crossing the commit page cap or a commit
777	/// of the write buffer failed, until SQLite's rollback commits or a new transaction starts.
778	/// Until then the write buffer holds pages of a transaction SQLite is abandoning, which a
779	/// close must discard, not commit.
780	refused_write_pending: bool,
781	/// Set by `NativeDatabase::Drop` once it has published every committed transaction, just
782	/// before SQLite closes. Main file writes after that come from SQLite rolling back a
783	/// transaction left open at close, which restores the state Depot and the cork already
784	/// hold, so commits discard them instead of sending them. A `SqliteVfs` serves one
785	/// connection for its lifetime, so the flag never outlives the connection that set it.
786	closing: bool,
787}
788
789#[derive(Debug, Clone, Copy, PartialEq, Eq)]
790enum CorkOutcome {
791	NotCorked,
792	Corked,
793	Overflow,
794}
795
796#[derive(Debug, Clone, Default)]
797struct WriteBuffer {
798	in_atomic_write: bool,
799	saved_db_size: u32,
800	dirty: BTreeMap<u32, PageBytes>,
801}
802
803impl WriteBuffer {
804	fn snapshot(&self) -> Vec<BufferedDirtyPage> {
805		self.dirty
806			.iter()
807			.map(|(pgno, bytes)| BufferedDirtyPage {
808				pgno: *pgno,
809				bytes: bytes.clone(),
810			})
811			.collect()
812	}
813}
814
815#[derive(Debug)]
816enum GetPagesError {
817	FenceMismatch(String),
818	Storage(SqliteStorageError),
819	Other(String),
820}
821
822#[repr(C)]
823struct VfsFile {
824	base: sqlite3_file,
825	ctx: *const VfsContext,
826	aux: *mut AuxFileHandle,
827}
828
829#[derive(Default)]
830struct AuxFileState {
831	bytes: Mutex<Vec<u8>>,
832	/// Page records written to this file since it was last emptied, counted only for the main
833	/// database's rollback journal.
834	journal_records: AtomicUsize,
835}
836
837struct AuxFileHandle {
838	path: String,
839	state: Arc<AuxFileState>,
840	delete_on_close: bool,
841	/// Whether this is the main database's rollback journal, whose size the commit cap bounds.
842	is_main_journal: bool,
843}
844
845#[derive(Debug, Clone, Copy, PartialEq, Eq)]
846enum PageCacheInsertKind {
847	Target,
848	PartialTarget,
849	Prefetch,
850	Startup,
851}
852
853unsafe impl Send for VfsContext {}
854unsafe impl Sync for VfsContext {}
855
856pub struct SqliteVfs {
857	_registration: SqliteVfsRegistration,
858	_name: CString,
859	ctx: Box<VfsContext>,
860}
861
862unsafe impl Send for SqliteVfs {}
863unsafe impl Sync for SqliteVfs {}
864
865struct SqliteVfsRegistration {
866	vfs_ptr: *mut sqlite3_vfs,
867}
868
869pub struct NativeDatabase {
870	db: *mut sqlite3,
871	_vfs: NativeVfsHandle,
872}
873
874unsafe impl Send for NativeDatabase {}
875
876pub type NativeVfsHandle = Arc<SqliteVfs>;
877pub type NativeConnection = NativeDatabase;
878
879impl VfsState {
880	/// The database size every committed transaction left, including corked ones.
881	fn committed_size_pages_before_transaction(&self) -> u32 {
882		if self.corked.is_some() {
883			self.corked_db_size_pages
884		} else {
885			self.committed_db_size_pages
886		}
887	}
888
889	/// Drops the write buffer of a transaction SQLite has not committed and restores the size
890	/// from before it. Depot and the cork already hold every committed transaction: each
891	/// commit syncs the main file (connections cannot turn `synchronous` off), and the sync
892	/// sends or corks its pages, so the write buffer holds only the unfinished transaction.
893	fn discard_uncommitted_pages(&mut self) {
894		self.write_buffer.dirty.clear();
895		self.write_buffer.in_atomic_write = false;
896		self.db_size_pages = self.committed_size_pages_before_transaction();
897		self.refused_write_pending = false;
898	}
899
900	fn new(config: &VfsConfig, _profiling_enabled: bool) -> Self {
901		let prefetch_unused_total = Arc::new(AtomicU64::new(0));
902		let prefetch_buffer =
903			PageBuffer::with_unused_counter(&config.page_budget, prefetch_unused_total.clone());
904		let committed_buffer = build_page_buffer(config);
905		let mut state = Self {
906			db_size_pages: 1,
907			committed_db_size_pages: 1,
908			head_txid: None,
909			page_size: DEFAULT_PAGE_SIZE,
910			prefetch_buffer,
911			committed_buffer,
912			prefetch_unused_total,
913			write_buffer: WriteBuffer::default(),
914			corked: None,
915			dead: false,
916			corked_db_size_pages: 0,
917			closing: false,
918			refused_write_pending: false,
919		};
920		state.cache_page(
921			config,
922			PageCacheInsertKind::Startup,
923			1,
924			empty_db_page().into(),
925		);
926		state
927	}
928
929	fn cache_page(
930		&mut self,
931		config: &VfsConfig,
932		kind: PageCacheInsertKind,
933		pgno: u32,
934		bytes: PageBytes,
935	) {
936		if !should_cache_page(config, kind, pgno) {
937			return;
938		}
939		cache_page(config, &self.prefetch_buffer, kind, pgno, bytes);
940	}
941
942	/// Hands a buffered page to SQLite: the page leaves the buffer, because SQLite's pager owns
943	/// every page it has read. Page 1 stays, because SQLite also reads its header directly,
944	/// outside the pager. So does a page SQLite reads only part of (`partial`), such as the
945	/// last page of an overflow chain read directly, bypassing the pager: SQLite reads that page
946	/// again whole through the pager.
947	fn take_buffered_page(
948		&self,
949		config: &VfsConfig,
950		pgno: u32,
951		partial: bool,
952	) -> Option<(PageBytes, bool)> {
953		if !can_read_cached_page(config, pgno) {
954			return None;
955		}
956		if pgno == 1 || partial {
957			let bytes = self
958				.committed_buffer
959				.get(&pgno)
960				.or_else(|| self.prefetch_buffer.get(&pgno))?;
961			return Some((bytes, false));
962		}
963
964		// Dropping the page here relies on SQLite 3.46.0 as bundled by libsqlite3-sys 0.30.1; line
965		// numbers are in that crate's `sqlite3/sqlite3.c` amalgamation.
966		// With `SQLITE_DIRECT_OVERFLOW_READ=0` (set in `.cargo/config.toml`), `journal_mode =
967		// DELETE` (so no WAL reads of the database file) and no memory map (`iVersion` 1 has no
968		// `xFetch`), the only whole-page `xRead` of the main database is `readDbPage`
969		// (sqlite3.c:60138, pager.c), which reads
970		// into a page-cache slot that the pager holds pinned while it is in use. The other
971		// main-database reads are partial and only touch page 1: the 100-byte header in
972		// `sqlite3PagerReadFileheader` (sqlite3.c:60994) and the 16-byte change counter in
973		// `pagerSharedLock` (sqlite3.c:62490). The direct overflow read in `accessPayload`
974		// (sqlite3.c:75763) would read every overflow page but the last of a chain whole, past
975		// the cache, but that flag compiles it out.
976		//
977		// SQLite does not keep the page forever. Once unpinned, it leaves the cache when the
978		// process-wide cache in `pager_cache.rs` evicts it under its byte budget (`PRAGMA
979		// cache_size` does not bound it, since `x_cachesize` ignores it), on `xShrink` from
980		// `sqlite3_db_release_memory`, and on `pager_reset`: an I/O error at unlock
981		// (sqlite3.c:59012), a page size change (60888), close (61281), VACUUM's
982		// `sqlite3PagerClearCache` (64577) or hot-journal playback (60009). The change counter
983		// check that resets the cache when another connection wrote (62499) does not run here,
984		// because `locking_mode = EXCLUSIVE` keeps the pager in its read state between
985		// transactions, and the VFS serves exactly one connection per database. A page that
986		// left both caches costs one depot fetch on its next read and never returns wrong data,
987		// because each buffered page is a copy depot already holds: fetched from it, or written
988		// by a commit it acknowledged.
989		if let Some(bytes) = self.committed_buffer.take(&pgno) {
990			// A neighboring miss may have prefetched this page after its commit.
991			// The authoritative committed copy wins; its redundant speculative copy
992			// was not consumed and must leave the buffer (and count as unused).
993			self.prefetch_buffer.invalidate(&pgno);
994			Some((bytes, false))
995		} else {
996			self.prefetch_buffer.take_with_status(&pgno)
997		}
998	}
999
1000	
1001
1002	/// A page a corked commit holds, which no cache may stand in for.
1003	fn corked_page(&self, pgno: u32) -> Option<&PageBytes> {
1004		self.corked.as_ref().and_then(|corked| corked.get(&pgno))
1005	}
1006
1007	/// While corked, moves the finished transaction's pages into the cork instead of
1008	/// committing them.
1009	///
1010	/// The cork is sent as one commit, so it holds at most `max_pages` pages. When the
1011	/// transaction would take it past that, nothing moves and the result is `Overflow`: the
1012	/// caller commits the cork on its own first, then corks the transaction.
1013	fn cork_dirty_pages(&mut self, max_pages: usize) -> CorkOutcome {
1014		let Some(corked) = self.corked.as_mut() else {
1015			return CorkOutcome::NotCorked;
1016		};
1017		let added_pages = self
1018			.write_buffer
1019			.dirty
1020			.keys()
1021			.filter(|pgno| !corked.contains_key(pgno))
1022			.count();
1023		if corked.len() + added_pages > max_pages {
1024			return CorkOutcome::Overflow;
1025		}
1026		corked.append(&mut self.write_buffer.dirty);
1027		self.corked_db_size_pages = self.db_size_pages;
1028		CorkOutcome::Corked
1029	}
1030
1031	/// Keeps a committed page for a re-read after SQLite's pager evicts it, including under another
1032	/// connection's pressure on the process-wide pager cache, and drops any older prefetched copy. SQLite's pager holds the page right after the commit, so read-ahead
1033	/// skips it.
1034	fn note_committed_page(&mut self, config: &VfsConfig, pgno: u32, bytes: &PageBytes) {
1035		self.prefetch_buffer.invalidate(&pgno);
1036		if pgno != 1 {}
1037		if config.staging_cache_ttl_ms == 0 || !config.page_cache_mode.caches_any_pages() {
1038			return;
1039		}
1040		self.committed_buffer.insert(pgno, bytes.clone());
1041	}
1042
1043	fn seed_page(
1044		&mut self,
1045		config: &VfsConfig,
1046		kind: PageCacheInsertKind,
1047		pgno: u32,
1048		page: PageBytes,
1049	) {
1050		if pgno == 1 {
1051			self.seed_main_page(config, kind, page);
1052		} else {
1053			self.cache_page(config, kind, pgno, page);
1054		}
1055	}
1056
1057	fn seed_main_page(&mut self, config: &VfsConfig, kind: PageCacheInsertKind, page: PageBytes) {
1058		if let Some(page_size) = sqlite_header_page_size(&page) {
1059			self.page_size = page_size;
1060		}
1061		if let Some(db_size_pages) = sqlite_header_db_size_pages(&page) {
1062			self.db_size_pages = db_size_pages;
1063			self.committed_db_size_pages = db_size_pages;
1064		}
1065		self.cache_page(config, kind, 1, page);
1066	}
1067
1068	fn invalidate_page_buffers(&mut self) {
1069		self.prefetch_buffer.invalidate_all();
1070		self.committed_buffer.invalidate_all();
1071	}
1072}
1073
1074fn build_page_buffer(config: &VfsConfig) -> PageBuffer {
1075	PageBuffer::new(&config.page_budget)
1076}
1077
1078fn cache_page(
1079	config: &VfsConfig,
1080	prefetch_buffer: &PageBuffer,
1081	kind: PageCacheInsertKind,
1082	pgno: u32,
1083	bytes: PageBytes,
1084) {
1085	if !should_cache_page(config, kind, pgno) {
1086		return;
1087	}
1088	if kind == PageCacheInsertKind::Prefetch || (kind == PageCacheInsertKind::Startup && pgno != 1)
1089	{
1090		prefetch_buffer.insert_prefetched(pgno, bytes);
1091	} else {
1092		prefetch_buffer.insert(pgno, bytes);
1093	}
1094}
1095
1096fn should_cache_page(config: &VfsConfig, kind: PageCacheInsertKind, pgno: u32) -> bool {
1097	match kind {
1098		PageCacheInsertKind::Target => false,
1099		PageCacheInsertKind::PartialTarget => true,
1100		PageCacheInsertKind::Prefetch => {
1101			config.staging_cache_ttl_ms > 0 && config.page_cache_mode.caches_prefetched_pages()
1102		}
1103		PageCacheInsertKind::Startup => {
1104			pgno == 1
1105				|| (config.staging_cache_ttl_ms > 0
1106					&& config.page_cache_mode.caches_startup_preloaded_pages())
1107		}
1108	}
1109}
1110
1111fn can_read_cached_page(config: &VfsConfig, pgno: u32) -> bool {
1112	pgno == 1 || config.page_cache_mode.caches_any_pages()
1113}
1114
1115impl VfsContext {
1116	fn new(
1117		actor_id: String,
1118		generation: Option<u64>,
1119		runtime: Handle,
1120		transport: SqliteTransportHandle,
1121		config: VfsConfig,
1122		io_methods: sqlite3_io_methods,
1123		initial_pages: impl Into<InitialPages>,
1124		metrics: Option<Arc<dyn SqliteVfsMetrics>>,
1125	) -> std::result::Result<Self, String> {
1126		let profiling_enabled = metrics
1127			.as_ref()
1128			.is_some_and(|metrics| metrics.profiling_enabled());
1129		let mut state = VfsState::new(&config, profiling_enabled);
1130		let initial_pages = initial_pages.into();
1131		state.head_txid = initial_pages.head_txid;
1132		for (pgno, page) in initial_pages.pages {
1133			state.seed_page(&config, PageCacheInsertKind::Startup, pgno, page);
1134		}
1135
1136		Ok(Self {
1137			actor_id,
1138			generation,
1139			runtime,
1140			transport,
1141			config: config.clone(),
1142			state: RwLock::new(state),
1143			aux_files: RwLock::new(BTreeMap::new()),
1144			last_error: Mutex::new(None),
1145			indeterminate_result: Mutex::new(None),
1146			transient_commit_error: Mutex::new(None),
1147			transaction_too_large: Mutex::new(None),
1148			storage_error: Mutex::new(None),
1149			fatal_error: RwLock::new(None),
1150			
1151			
1152			
1153			
1154			commit_atomic_count: AtomicU64::new(0),
1155			trace_commit_attempts: AtomicU64::new(0),
1156			io_methods: Box::new(io_methods),
1157			resolve_pages_total: AtomicU64::new(0),
1158			resolve_pages_cache_hits: AtomicU64::new(0),
1159			resolve_pages_fetches: AtomicU64::new(0),
1160			pages_fetched_total: AtomicU64::new(0),
1161			prefetch_pages_total: AtomicU64::new(0),
1162			prefetch_consumed_total: AtomicU64::new(0),
1163			commit_total: AtomicU64::new(0),
1164			commit_request_build_ns: AtomicU64::new(0),
1165			commit_serialize_ns: AtomicU64::new(0),
1166			commit_transport_ns: AtomicU64::new(0),
1167			commit_state_update_ns: AtomicU64::new(0),
1168			commit_duration_ns_total: AtomicU64::new(0),
1169			operation_profile_active: AtomicBool::new(false),
1170			operation_profile: Mutex::new(None),
1171			operation_prefetch_unused_start: AtomicU64::new(0),
1172			reported_prefetch_unused: AtomicU64::new(0),
1173			metrics,
1174		})
1175	}
1176
1177	fn begin_operation_profile(&self) {
1178		if self
1179			.metrics
1180			.as_ref()
1181			.is_some_and(|metrics| metrics.profiling_enabled())
1182		{
1183			let unused = self
1184				.state
1185				.read()
1186				.prefetch_unused_total
1187				.load(Ordering::Relaxed);
1188			self.operation_prefetch_unused_start
1189				.store(unused, Ordering::Relaxed);
1190			*self.operation_profile.lock() = Some(SqliteOperationProfile::default());
1191			self.operation_profile_active.store(true, Ordering::Release);
1192		}
1193	}
1194
1195	fn report_prefetch_unused(&self) {
1196		let total = self
1197			.state
1198			.read()
1199			.prefetch_unused_total
1200			.load(Ordering::Relaxed);
1201		let previous = self
1202			.reported_prefetch_unused
1203			.fetch_max(total, Ordering::Relaxed);
1204		if total > previous
1205			&& let Some(metrics) = &self.metrics
1206		{
1207			metrics.record_prefetch_unused_pages(total - previous);
1208		}
1209	}
1210
1211	fn finish_operation_profile(&self) -> SqliteOperationProfile {
1212		self.report_prefetch_unused();
1213		if !self.operation_profile_active.swap(false, Ordering::AcqRel) {
1214			return SqliteOperationProfile::default();
1215		}
1216		let mut profile = self.operation_profile.lock().take().unwrap_or_default();
1217		let unused = self
1218			.state
1219			.read()
1220			.prefetch_unused_total
1221			.load(Ordering::Relaxed)
1222			.saturating_sub(self.operation_prefetch_unused_start.load(Ordering::Relaxed));
1223		profile.prefetch_unused_pages = unused;
1224		profile
1225	}
1226
1227	fn update_operation_profile(&self, update: impl FnOnce(&mut SqliteOperationProfile)) {
1228		if !self.operation_profile_active.load(Ordering::Relaxed) {
1229			return;
1230		}
1231		if let Some(profile) = self.operation_profile.lock().as_mut() {
1232			update(profile);
1233		}
1234	}
1235
1236	fn profile_request_limit(&self) -> usize {
1237		self.metrics
1238			.as_ref()
1239			.map_or(0, |metrics| metrics.max_profiled_get_pages_requests())
1240			.min(MAX_PROFILED_GET_PAGES_REQUESTS)
1241	}
1242
1243	fn clear_last_error(&self) {
1244		*self.last_error.lock() = None;
1245	}
1246
1247	fn set_last_error(&self, message: String) {
1248		*self.last_error.lock() = Some(message);
1249	}
1250
1251	fn mark_indeterminate(&self, operation: String) {
1252		*self.indeterminate_result.lock() = Some(operation.clone());
1253		self.mark_fatal(SqliteCommitIndeterminateError(operation).to_string());
1254	}
1255
1256	fn take_indeterminate_result(&self) -> Option<String> {
1257		self.indeterminate_result.lock().take()
1258	}
1259
1260	fn clone_last_error(&self) -> Option<String> {
1261		self.last_error.lock().clone()
1262	}
1263
1264	fn clone_fatal_error(&self) -> Option<String> {
1265		self.fatal_error.read().clone()
1266	}
1267
1268	fn defer_transient_commit_error(&self, message: String) {
1269		*self.transient_commit_error.lock() = Some(message);
1270	}
1271
1272	fn take_transient_commit_error(&self) -> Option<String> {
1273		self.transient_commit_error.lock().take()
1274	}
1275
1276	fn transaction_too_large_error(&self) -> SqliteTransactionTooLargeError {
1277		SqliteTransactionTooLargeError {
1278			max_pages: self.config.commit_page_cap(),
1279			page_size: self.page_size(),
1280		}
1281	}
1282
1283	/// Refuses a write that would take the transaction past the commit page cap. SQLite treats
1284	/// `SQLITE_FULL` like a full disk: it fails the statement and rolls back, restoring the
1285	/// pages from its journal, so nothing over the cap is ever held or sent.
1286	fn refuse_oversize_transaction(&self) -> c_int {
1287		let error = self.transaction_too_large_error();
1288		tracing::warn!(
1289			actor_id = %self.actor_id,
1290			max_pages = error.max_pages,
1291			"sqlite transaction exceeds the commit page cap; the application must split it into smaller transactions"
1292		);
1293		self.set_last_error(error.to_string());
1294		*self.transaction_too_large.lock() = Some(error);
1295		SQLITE_FULL
1296	}
1297
1298	/// A commit refused for crossing the cap. The write cap keeps every transaction under it,
1299	/// so this is only a backstop.
1300	fn transaction_too_large_commit_error(&self) -> CommitBufferError {
1301		CommitBufferError::Other(self.transaction_too_large_error().to_string())
1302	}
1303
1304	/// Commits the transactions held in the cork as one commit and leaves the cork empty, for
1305	/// when the next transaction would take the cork past one commit. Corked transactions were
1306	/// never promised to be atomic together, only to become durable, and each commit still
1307	/// holds whole transactions.
1308	fn commit_cork_alone(&self) -> std::result::Result<(), CommitBufferError> {
1309		let request = {
1310			let state = self.state.read();
1311			let Some(corked) = state.corked.as_ref() else {
1312				return Ok(());
1313			};
1314			BufferedCommitRequest {
1315				actor_id: self.actor_id.clone(),
1316				new_db_size_pages: state.corked_db_size_pages,
1317				expected_head_txid: state.head_txid,
1318				dirty_pages: corked
1319					.iter()
1320					.map(|(pgno, bytes)| BufferedDirtyPage {
1321						pgno: *pgno,
1322						bytes: bytes.clone(),
1323					})
1324					.collect(),
1325			}
1326		};
1327
1328		let outcome = match self.block_on_buffered_commit(request.clone(), None) {
1329			Ok(CommitWait::Completed((outcome, _))) => outcome,
1330			Ok(CommitWait::TimedOut) => {
1331				return Err(CommitBufferError::Other(
1332					"sqlite commit timed out".to_string(),
1333				));
1334			}
1335			Err(err) => {
1336				tracing::error!(
1337					actor_id = %self.actor_id,
1338					dirty_pages = request.dirty_pages.len(),
1339					?err,
1340					"sqlite commit of a full cork failed"
1341				);
1342				handle_non_finalize_commit_error(self, &err);
1343				return Err(err);
1344			}
1345		};
1346		self.commit_total.fetch_add(1, Ordering::Relaxed);
1347		if let Some(metrics) = &self.metrics {
1348			metrics.record_commit(request.dirty_pages.len() as u64);
1349		}
1350
1351		let mut state = self.state.write();
1352		state.committed_db_size_pages = outcome.db_size_pages;
1353		state.head_txid = outcome
1354			.head_txid
1355			.or_else(|| state.head_txid.map(|head_txid| head_txid.saturating_add(1)));
1356		for dirty_page in &request.dirty_pages {
1357			state.note_committed_page(&self.config, dirty_page.pgno, &dirty_page.bytes);
1358		}
1359		if let Some(corked) = state.corked.as_mut() {
1360			corked.clear();
1361		}
1362		Ok(())
1363	}
1364
1365	pub(crate) fn take_transaction_too_large(&self) -> Option<SqliteTransactionTooLargeError> {
1366		self.transaction_too_large.lock().take()
1367	}
1368
1369	/// Records a typed storage failure SQLite is about to see as an I/O error.
1370	fn set_storage_error(&self, error: SqliteStorageError) {
1371		self.set_last_error(error.to_string());
1372		*self.storage_error.lock() = Some(error);
1373	}
1374
1375	pub(crate) fn take_storage_error(&self) -> Option<SqliteStorageError> {
1376		self.storage_error.lock().take()
1377	}
1378
1379	/// Discards the pages of a transaction whose write was refused, when the database closes
1380	/// before SQLite has rolled it back. Depot and the cork still hold the state from before
1381	/// that transaction, which is exactly what the rollback would restore.
1382	fn discard_refused_transaction(&self) {
1383		let mut state = self.state.write();
1384		if !state.refused_write_pending {
1385			return;
1386		}
1387		tracing::debug!(
1388			actor_id = %self.actor_id,
1389			dirty_pages = state.write_buffer.dirty.len(),
1390			"discarding the pages of an abandoned sqlite transaction at close"
1391		);
1392		state.discard_uncommitted_pages();
1393	}
1394
1395	/// Marks the write buffer as holding a transaction whose commit failed. SQLite reports the
1396	/// failure and rolls the transaction back on its next access, which rewrites the journaled
1397	/// pages and commits them; a close before that access must discard the buffer instead.
1398	fn abandon_write_buffer(&self) {
1399		self.state.write().refused_write_pending = true;
1400	}
1401
1402	/// Discards the pages of a transaction still open when the database closes. SQLite never
1403	/// committed it, so none of its pages may reach Depot.
1404	fn discard_open_transaction(&self) {
1405		let mut state = self.state.write();
1406		if state.write_buffer.dirty.is_empty()
1407			&& !state.write_buffer.in_atomic_write
1408			&& state.db_size_pages == state.committed_size_pages_before_transaction()
1409		{
1410			return;
1411		}
1412		tracing::debug!(
1413			actor_id = %self.actor_id,
1414			dirty_pages = state.write_buffer.dirty.len(),
1415			"discarding the pages of an uncommitted sqlite transaction at close"
1416		);
1417		state.discard_uncommitted_pages();
1418	}
1419
1420	/// Drops every page a closing connection failed to send, including the commits SQLite
1421	/// completed while corked. Those commits are lost for this connection, so the close no
1422	/// longer holds them and the next open reads whatever Depot accepted. Returns how many
1423	/// pages were dropped.
1424	fn discard_unsent_commits(&self) -> usize {
1425		let mut state = self.state.write();
1426		let dirty_pages = state.write_buffer.dirty.len();
1427		state.discard_uncommitted_pages();
1428		dirty_pages
1429	}
1430
1431	
1432
1433	pub(crate) fn take_last_error(&self) -> Option<String> {
1434		self.last_error.lock().take()
1435	}
1436
1437	fn add_commit_phase_metrics(
1438		&self,
1439		request_build_ns: u64,
1440		transport_metrics: CommitTransportMetrics,
1441		state_update_ns: u64,
1442		total_ns: u64,
1443	) {
1444		self.update_operation_profile(|profile| {
1445			profile.storage_ns = profile
1446				.storage_ns
1447				.saturating_add(transport_metrics.transport_ns);
1448			profile.commit_round_trips = profile.commit_round_trips.saturating_add(1);
1449		});
1450		if let Some(metrics) = &self.metrics {
1451			metrics.observe_commit_phases(
1452				request_build_ns,
1453				transport_metrics.serialize_ns,
1454				transport_metrics.transport_ns,
1455				state_update_ns,
1456				total_ns,
1457			);
1458		}
1459		self.commit_request_build_ns
1460			.fetch_add(request_build_ns, Ordering::Relaxed);
1461		self.commit_serialize_ns
1462			.fetch_add(transport_metrics.serialize_ns, Ordering::Relaxed);
1463		self.commit_transport_ns
1464			.fetch_add(transport_metrics.transport_ns, Ordering::Relaxed);
1465		self.commit_state_update_ns
1466			.fetch_add(state_update_ns, Ordering::Relaxed);
1467		self.commit_duration_ns_total
1468			.fetch_add(total_ns, Ordering::Relaxed);
1469	}
1470
1471	fn sqlite_vfs_metrics(&self) -> SqliteVfsMetricsSnapshot {
1472		let state = self.state.read();
1473
1474		SqliteVfsMetricsSnapshot {
1475			request_build_ns: self.commit_request_build_ns.load(Ordering::Relaxed),
1476			serialize_ns: self.commit_serialize_ns.load(Ordering::Relaxed),
1477			transport_ns: self.commit_transport_ns.load(Ordering::Relaxed),
1478			state_update_ns: self.commit_state_update_ns.load(Ordering::Relaxed),
1479			total_ns: self.commit_duration_ns_total.load(Ordering::Relaxed),
1480			commit_count: self.commit_total.load(Ordering::Relaxed),
1481			page_cache_entries: state
1482				.prefetch_buffer
1483				.entry_count()
1484				.saturating_add(state.committed_buffer.entry_count()),
1485			page_cache_weighted_size: state.prefetch_buffer.weighted_size(),
1486			page_cache_capacity_pages: self.config.page_budget.capacity_pages(),
1487			write_buffer_dirty_pages: state.write_buffer.dirty.len() as u64,
1488			db_size_pages: state.db_size_pages as u64,
1489		}
1490	}
1491
1492	fn block_on_buffered_commit(
1493		&self,
1494		request: BufferedCommitRequest,
1495		timeout: Option<Duration>,
1496	) -> std::result::Result<
1497		CommitWait<(BufferedCommitOutcome, CommitTransportMetrics)>,
1498		CommitBufferError,
1499	> {
1500		let trace_pages =
1501			tracing::enabled!(target: "sqlite_prefetch::trace", tracing::Level::TRACE).then(|| {
1502				(
1503					request
1504						.dirty_pages
1505						.iter()
1506						.map(|page| page.pgno)
1507						.collect::<Vec<_>>(),
1508					request.new_db_size_pages,
1509				)
1510			});
1511		let attempt = trace_pages
1512			.as_ref()
1513			.map(|_| self.trace_commit_attempts.fetch_add(1, Ordering::Relaxed));
1514		let span = tracing::trace_span!(target: "sqlite_prefetch::trace", "native_sqlite_commit",
1515            stream_id = %format_args!("sqlite-{:p}", self), actor_id = %self.actor_id,
1516            generation = self.generation, commit_attempt = attempt,
1517            local_request_id = %format_args!("c{}", self.commit_total.load(Ordering::Relaxed)));
1518		let commit =
1519			commit_buffered_pages(&*self.transport, request, self.config.commit_page_cap())
1520				.instrument(span.clone());
1521		let result = if let Some(timeout) = timeout {
1522			match self
1523				.runtime
1524				.block_on(async { tokio::time::timeout(timeout, commit).await })
1525			{
1526				Ok(result) => result,
1527				Err(_) => {
1528					span.in_scope(|| tracing::trace!(target: "sqlite_prefetch::trace", event = "commit_result", outcome = "timeout"));
1529					return Ok(CommitWait::TimedOut);
1530				}
1531			}
1532		} else {
1533			self.runtime.block_on(commit)
1534		};
1535
1536		let _span = span.enter();
1537		tracing::trace!(target: "sqlite_prefetch::trace", event = "commit_result",
1538            outcome = if result.is_ok() { "acknowledged" } else { "error" });
1539
1540		if result.is_ok()
1541			&& let Some((pages, size_pages)) = trace_pages
1542		{
1543			let request_id = self.commit_total.load(Ordering::Relaxed);
1544			for pgno in pages {
1545				tracing::trace!(target: "sqlite_prefetch::trace", event = "access", op = "write",
1546                    stream_id = %format_args!("sqlite-{:p}", self),
1547                    pgno, hit = false, request_id = %format_args!("c{request_id}"));
1548			}
1549			tracing::trace!(target: "sqlite_prefetch::trace", event = "commit",
1550                stream_id = %format_args!("sqlite-{:p}", self),
1551                request_id = %format_args!("c{request_id}"), size_pages);
1552		}
1553		result.map(CommitWait::Completed)
1554	}
1555
1556	fn round_trip_counts(&self) -> SqliteRoundTripCounts {
1557		SqliteRoundTripCounts {
1558			get_pages: self.resolve_pages_fetches.load(Ordering::Relaxed),
1559			commit: self.commit_total.load(Ordering::Relaxed),
1560		}
1561	}
1562
1563	fn page_size(&self) -> usize {
1564		self.state.read().page_size.max(DEFAULT_PAGE_SIZE)
1565	}
1566
1567	fn open_aux_file(&self, path: &str) -> Arc<AuxFileState> {
1568		let mut aux_files = self.aux_files.write();
1569		aux_files
1570			.entry(path.to_string())
1571			.or_insert_with(|| Arc::new(AuxFileState::default()))
1572			.clone()
1573	}
1574
1575	fn aux_file_exists(&self, path: &str) -> bool {
1576		self.aux_files.read().contains_key(path)
1577	}
1578
1579	fn read_aux_file(&self, path: &str) -> Option<Vec<u8>> {
1580		let state = self.aux_files.read().get(path).cloned()?;
1581		let bytes = state.bytes.lock().clone();
1582		Some(bytes)
1583	}
1584
1585	fn delete_aux_file(&self, path: &str) {
1586		self.aux_files.write().remove(path);
1587	}
1588
1589	
1590
1591	
1592
1593	
1594
1595	
1596
1597	fn is_dead(&self) -> bool {
1598		self.state.read().dead
1599	}
1600
1601	fn mark_fatal(&self, message: String) {
1602		self.set_last_error(message.clone());
1603		self.state.write().dead = true;
1604		let mut fatal_error = self.fatal_error.write();
1605		if fatal_error.is_none() {
1606			*fatal_error = Some(message);
1607		}
1608	}
1609
1610	/// Resolves `target_pgnos` from the buffers or the transport. `partial` marks a read of
1611	/// less than a whole page, which leaves buffered pages in place; see `take_buffered_page`.
1612	fn resolve_pages(
1613		&self,
1614		target_pgnos: &[u32],
1615		partial: bool,
1616	) -> std::result::Result<HashMap<u32, Option<PageBytes>>, GetPagesError> {
1617		use std::sync::atomic::Ordering::Relaxed;
1618		let request_id = self.resolve_pages_total.fetch_add(1, Relaxed);
1619		self.update_operation_profile(|profile| {
1620			profile.sqlite_requested_pages = profile
1621				.sqlite_requested_pages
1622				.saturating_add(target_pgnos.len() as u64);
1623		});
1624		if let Some(metrics) = &self.metrics {
1625			metrics.record_resolve_pages(target_pgnos.len() as u64);
1626		}
1627
1628		let mut resolved = HashMap::new();
1629		let mut missing = Vec::new();
1630		let mut seen = HashSet::new();
1631		let mut prefetch_consumed = 0_u64;
1632
1633		{
1634			let state = self.state.read();
1635			if state.dead {
1636				return Err(GetPagesError::Other(
1637					"sqlite actor lost its fence".to_string(),
1638				));
1639			}
1640
1641			for pgno in target_pgnos.iter().copied() {
1642				if !seen.insert(pgno) {
1643					continue;
1644				}
1645				if let Some(bytes) = state.write_buffer.dirty.get(&pgno) {
1646					resolved.insert(pgno, Some(bytes.clone()));
1647					continue;
1648				}
1649				if let Some(bytes) = state.corked_page(pgno) {
1650					resolved.insert(pgno, Some(bytes.clone()));
1651					continue;
1652				}
1653				if let Some((bytes, was_prefetched)) =
1654					state.take_buffered_page(&self.config, pgno, partial)
1655				{
1656					prefetch_consumed = prefetch_consumed.saturating_add(u64::from(was_prefetched));
1657					resolved.insert(pgno, Some(bytes));
1658					continue;
1659				}
1660				missing.push(pgno);
1661			}
1662		}
1663		if tracing::enabled!(target: "sqlite_prefetch::trace", tracing::Level::TRACE) {
1664			for pgno in target_pgnos {
1665				tracing::trace!(target: "sqlite_prefetch::trace", event = "access", op = "read",
1666                    stream_id = %format_args!("sqlite-{:p}", self),
1667                    pgno, hit = resolved.contains_key(pgno), request_id);
1668			}
1669		}
1670		if prefetch_consumed > 0 {
1671			self.prefetch_consumed_total
1672				.fetch_add(prefetch_consumed, Relaxed);
1673			self.update_operation_profile(|profile| {
1674				profile.prefetch_consumed_pages = profile
1675					.prefetch_consumed_pages
1676					.saturating_add(prefetch_consumed);
1677			});
1678		}
1679
1680		if missing.is_empty() {
1681			self.resolve_pages_cache_hits
1682				.fetch_add(target_pgnos.len() as u64, Relaxed);
1683			if let Some(metrics) = &self.metrics {
1684				metrics.record_resolve_cache_hits(target_pgnos.len() as u64);
1685			}
1686			self.update_operation_profile(|profile| {
1687				profile.cache_hit_pages = profile
1688					.cache_hit_pages
1689					.saturating_add(target_pgnos.len() as u64);
1690			});
1691			return Ok(resolved);
1692		}
1693		self.resolve_pages_cache_hits
1694			.fetch_add((seen.len() - missing.len()) as u64, Relaxed);
1695		if let Some(metrics) = &self.metrics {
1696			metrics.record_resolve_cache_hits((seen.len() - missing.len()) as u64);
1697			metrics.record_resolve_cache_misses(missing.len() as u64);
1698		}
1699		self.update_operation_profile(|profile| {
1700			profile.cache_hit_pages = profile
1701				.cache_hit_pages
1702				.saturating_add((seen.len() - missing.len()) as u64);
1703			profile.cache_miss_pages = profile
1704				.cache_miss_pages
1705				.saturating_add(missing.len() as u64);
1706		});
1707
1708		let (to_fetch, page_size, db_size_pages, expected_head_txid) = {
1709			let state = self.state.read();
1710			// The page server plans every prefetch; the client asks for what SQLite missed.
1711			let to_fetch = missing.clone();
1712			(
1713				to_fetch,
1714				state.page_size.max(1),
1715				state.db_size_pages,
1716				state.head_txid,
1717			)
1718		};
1719
1720		{
1721			self.resolve_pages_fetches.fetch_add(1, Relaxed);
1722			self.pages_fetched_total
1723				.fetch_add(to_fetch.len() as u64, Relaxed);
1724			tracing::debug!(
1725				actor_id = %self.actor_id,
1726				generation = ?self.generation,
1727				requested_pages = ?target_pgnos,
1728				missing_pages = ?missing,
1729				total_fetch_pages = to_fetch.len(),
1730				total_fetch_bytes = to_fetch.len().saturating_mul(page_size),
1731				db_size_pages,
1732				expected_head_txid,
1733				"vfs get_pages fetch"
1734			);
1735		}
1736
1737		let trace_span = tracing::trace_span!(target: "sqlite_prefetch::trace", "native_sqlite_read",
1738            stream_id = %format_args!("sqlite-{:p}", self), actor_id = %self.actor_id,
1739            generation = self.generation, local_request_id = request_id);
1740		let get_pages_start = Instant::now();
1741		// Transport rejection, including envoy shutdown while a VFS callback is
1742		// active, becomes GetPagesError here. The SQLite callback maps that to
1743		// SQLITE_IOERR_* because VFS has no richer async transport error channel.
1744		let response = self.runtime.block_on(
1745			self.transport
1746				.get_pages(protocol::SqliteGetPagesRequest {
1747					actor_id: self.actor_id.clone().into(),
1748					pgnos: to_fetch.clone(),
1749					expected_generation: None,
1750					expected_head_txid,
1751				})
1752				.instrument(trace_span.clone()),
1753		);
1754		// Only synchronous response handling holds this guard; the transport future above
1755		// carries its span across polls instead of entering a span across an await.
1756		let _trace_guard = trace_span.enter();
1757		let get_pages_duration_ns = get_pages_start.elapsed().as_nanos() as u64;
1758		if let Some(metrics) = &self.metrics {
1759			metrics.observe_get_pages_duration(get_pages_duration_ns);
1760		}
1761		let profile_active = self.operation_profile_active.load(Ordering::Relaxed);
1762		let ordinal = if profile_active {
1763			self.operation_profile
1764				.lock()
1765				.as_ref()
1766				.map_or(1, |profile| profile.get_pages_round_trips.saturating_add(1))
1767		} else {
1768			1
1769		};
1770		let response = match response {
1771			Ok(response) => response,
1772			Err(err) => {
1773				self.update_operation_profile(|profile| {
1774					profile.push_get_pages(
1775						SqliteGetPagesProfile {
1776							ordinal,
1777							duration_ns: get_pages_duration_ns,
1778							demand_requested: missing.len() as u64,
1779							..Default::default()
1780						},
1781						self.profile_request_limit(),
1782					);
1783				});
1784				if let Some(error) = storage_timeout(&err) {
1785					return Err(GetPagesError::Storage(error));
1786				}
1787				return Err(GetPagesError::Other(err.to_string()));
1788			}
1789		};
1790
1791		match response {
1792			protocol::SqliteGetPagesResponse::SqliteGetPagesOk(ok) => {
1793				if profile_active {
1794					let requested = to_fetch.iter().copied().collect::<HashSet<_>>();
1795					let mut request_profile = SqliteGetPagesProfile {
1796						ordinal,
1797						duration_ns: get_pages_duration_ns,
1798						demand_requested: missing.len() as u64,
1799						success: true,
1800						..Default::default()
1801					};
1802					let mut btree_pages = 0_u64;
1803					let mut non_btree_pages = 0_u64;
1804					for fetched in &ok.pages {
1805						if !requested.contains(&fetched.pgno) {
1806							request_profile.overflow_expansion_extra =
1807								request_profile.overflow_expansion_extra.saturating_add(1);
1808						}
1809						if let Some(bytes) = &fetched.bytes {
1810							request_profile.response_present =
1811								request_profile.response_present.saturating_add(1);
1812							request_profile.response_bytes = request_profile
1813								.response_bytes
1814								.saturating_add(bytes.len() as u64);
1815							match classify(fetched.pgno, bytes) {
1816								PageClass::Btree => btree_pages = btree_pages.saturating_add(1),
1817								PageClass::Overflow => {
1818									non_btree_pages = non_btree_pages.saturating_add(1)
1819								}
1820							}
1821						} else {
1822							request_profile.response_missing =
1823								request_profile.response_missing.saturating_add(1);
1824						}
1825					}
1826					self.update_operation_profile(|profile| {
1827						profile.btree_pages = profile.btree_pages.saturating_add(btree_pages);
1828						profile.non_btree_pages =
1829							profile.non_btree_pages.saturating_add(non_btree_pages);
1830						profile.push_get_pages(request_profile, self.profile_request_limit());
1831					});
1832				}
1833				let response_head_txid = ok.head_txid;
1834				if let Some(head_txid) = response_head_txid {
1835					self.state.write().head_txid = Some(head_txid);
1836				}
1837				let missing_pages = missing.iter().copied().collect::<HashSet<_>>();
1838				let prefetch_buffer = self.state.read().prefetch_buffer.clone();
1839				#[cfg(debug_assertions)]
1840				let mut returned_pgnos = HashSet::new();
1841				#[cfg(debug_assertions)]
1842				let mut returned_missing_pages = Vec::new();
1843				#[cfg(debug_assertions)]
1844				let mut returned_missing_in_range_pages = Vec::new();
1845				let mut server_prefetched = 0_u64;
1846				for (ordinal, fetched) in ok.pages.into_iter().enumerate() {
1847					tracing::trace!(target: "sqlite_prefetch::trace", event = "page_received",
1848                        ordinal, pgno = fetched.pgno, present = fetched.bytes.is_some(),
1849                        bytes = fetched.bytes.as_ref().map_or(0, |bytes| bytes.len()),
1850                        demand = missing_pages.contains(&fetched.pgno));
1851					// Missing pages and the synthetic empty header are not delivered data.
1852					let delivered = fetched
1853						.bytes
1854						.as_ref()
1855						.and_then(|bytes| PageTrace::capture(ordinal, fetched.pgno, bytes.len()));
1856					#[cfg(debug_assertions)]
1857					{
1858						returned_pgnos.insert(fetched.pgno);
1859						if fetched.bytes.is_none() {
1860							returned_missing_pages.push(fetched.pgno);
1861							if fetched.pgno <= db_size_pages {
1862								returned_missing_in_range_pages.push(fetched.pgno);
1863							}
1864						}
1865					}
1866					let bytes = if fetched.bytes.is_none()
1867						&& self.commit_total.load(Relaxed) == 0
1868						&& missing_pages.contains(&fetched.pgno)
1869						&& fetched.pgno == 1
1870					{
1871						// An empty file can already have a nonzero inode version. Its
1872						// synthetic SQLite page must retain the storage response's fence.
1873						self.state.write().head_txid = Some(response_head_txid.unwrap_or(0));
1874						Some(PageBytes::from(empty_db_page()))
1875					} else {
1876						fetched.bytes.map(PageBytes::from)
1877					};
1878					if let Some(bytes) = &bytes {
1879						// A page fetched for a partial read is read again whole next, so it
1880						// stays buffered like a read-ahead page for that read.
1881						let kind = if missing_pages.contains(&fetched.pgno) {
1882							if partial {
1883								PageCacheInsertKind::PartialTarget
1884							} else {
1885								PageCacheInsertKind::Target
1886							}
1887						} else {
1888							PageCacheInsertKind::Prefetch
1889						};
1890						if kind == PageCacheInsertKind::Prefetch {
1891							server_prefetched += 1;
1892							if should_cache_page(&self.config, kind, fetched.pgno) {
1893								prefetch_buffer.insert_prefetched_traced(
1894									fetched.pgno,
1895									bytes.clone(),
1896									delivered,
1897								);
1898							} else if let Some(trace) = delivered {
1899								trace.disposition("unused");
1900							}
1901						} else {
1902							// A partial read already consumed these bytes even though a
1903							// copy remains for a later whole-page read.
1904							if let Some(trace) = delivered {
1905								trace.disposition("consumed");
1906							}
1907							cache_page(
1908								&self.config,
1909								&prefetch_buffer,
1910								kind,
1911								fetched.pgno,
1912								bytes.clone(),
1913							);
1914						}
1915					}
1916					resolved.insert(fetched.pgno, bytes);
1917				}
1918				self.prefetch_pages_total
1919					.fetch_add(server_prefetched, Relaxed);
1920				if let Some(metrics) = &self.metrics {
1921					metrics.record_get_pages_request(
1922						to_fetch.len() as u64,
1923						server_prefetched,
1924						page_size as u64,
1925					);
1926				}
1927				#[cfg(debug_assertions)]
1928				{
1929					let absent_response_pages = to_fetch
1930						.iter()
1931						.copied()
1932						.filter(|pgno| !returned_pgnos.contains(pgno))
1933						.collect::<Vec<_>>();
1934					let absent_in_range_pages = absent_response_pages
1935						.iter()
1936						.copied()
1937						.filter(|pgno| *pgno <= db_size_pages)
1938						.collect::<Vec<_>>();
1939					if !returned_missing_in_range_pages.is_empty()
1940						|| !absent_in_range_pages.is_empty()
1941					{
1942						tracing::warn!(
1943							actor_id = %self.actor_id,
1944							requested_pages = ?target_pgnos,
1945							missing_pages = ?missing,
1946							fetch_pages = ?to_fetch,
1947							db_size_pages,
1948							returned_missing_pages = ?returned_missing_pages,
1949							returned_missing_in_range_pages = ?returned_missing_in_range_pages,
1950							absent_response_pages = ?absent_response_pages,
1951							absent_in_range_pages = ?absent_in_range_pages,
1952							"sqlite get_pages returned missing pages within declared db size"
1953						);
1954					}
1955				}
1956				for pgno in missing {
1957					resolved.entry(pgno).or_insert(None);
1958				}
1959				Ok(resolved)
1960			}
1961			protocol::SqliteGetPagesResponse::SqliteErrorResponse(error) => {
1962				self.update_operation_profile(|profile| {
1963					profile.push_get_pages(
1964						SqliteGetPagesProfile {
1965							ordinal,
1966							duration_ns: get_pages_duration_ns,
1967							demand_requested: missing.len() as u64,
1968							..Default::default()
1969						},
1970						self.profile_request_limit(),
1971					);
1972				});
1973				if self.commit_total.load(Relaxed) == 0
1974					&& missing.contains(&1)
1975					&& is_initial_main_page_missing(&error.message)
1976				{
1977					for pgno in missing {
1978						let bytes = if pgno == 1 {
1979							Some(PageBytes::from(empty_db_page()))
1980						} else {
1981							None
1982						};
1983						resolved.entry(pgno).or_insert(bytes);
1984					}
1985					return Ok(resolved);
1986				}
1987				if is_head_fence_mismatch_response(&error) {
1988					return Err(GetPagesError::FenceMismatch(error.message.to_string()));
1989				}
1990				if let Some(refusal) = storage_refusal(&error) {
1991					return Err(GetPagesError::Storage(refusal));
1992				}
1993				Err(GetPagesError::Other(error.message.to_string()))
1994			}
1995		}
1996	}
1997
1998	fn flush_dirty_pages(
1999		&self,
2000	) -> std::result::Result<Option<BufferedCommitOutcome>, CommitBufferError> {
2001		match self.flush_dirty_pages_with_timeout(None)? {
2002			CommitWait::Completed(outcome) => Ok(outcome),
2003			CommitWait::TimedOut => Err(CommitBufferError::Other(
2004				"sqlite commit timed out".to_string(),
2005			)),
2006		}
2007	}
2008
2009	fn flush_dirty_pages_with_timeout(
2010		&self,
2011		timeout: Option<Duration>,
2012	) -> std::result::Result<CommitWait<Option<BufferedCommitOutcome>>, CommitBufferError> {
2013		let total_start = Instant::now();
2014		let request_build_start = Instant::now();
2015		let request = {
2016			let mut state = self.state.write();
2017			if state.dead {
2018				return Err(CommitBufferError::Other(
2019					"sqlite actor lost its fence".to_string(),
2020				));
2021			}
2022			if state.closing {
2023				state.discard_uncommitted_pages();
2024				return Ok(CommitWait::Completed(None));
2025			}
2026			if state.write_buffer.in_atomic_write
2027				|| (state.write_buffer.dirty.is_empty()
2028					&& state.db_size_pages == state.committed_db_size_pages)
2029			{
2030				return Ok(CommitWait::Completed(None));
2031			}
2032			// A corked commit is complete for SQLite once its pages are in the cork.
2033			match state.cork_dirty_pages(self.config.commit_page_cap()) {
2034				CorkOutcome::NotCorked => {}
2035				CorkOutcome::Corked => {
2036					state.refused_write_pending = false;
2037					return Ok(CommitWait::Completed(None));
2038				}
2039				CorkOutcome::Overflow => {
2040					drop(state);
2041					self.commit_cork_alone()?;
2042					let mut state = self.state.write();
2043					return match state.cork_dirty_pages(self.config.commit_page_cap()) {
2044						CorkOutcome::Corked | CorkOutcome::NotCorked => {
2045							state.refused_write_pending = false;
2046							Ok(CommitWait::Completed(None))
2047						}
2048						CorkOutcome::Overflow => Err(self.transaction_too_large_commit_error()),
2049					};
2050				}
2051			}
2052
2053			BufferedCommitRequest {
2054				actor_id: self.actor_id.clone(),
2055				new_db_size_pages: state.db_size_pages,
2056				expected_head_txid: state.head_txid,
2057				dirty_pages: state.write_buffer.snapshot(),
2058			}
2059		};
2060		let request_build_ns = request_build_start.elapsed().as_nanos() as u64;
2061		self.update_operation_profile(|profile| {
2062			profile.dirty_pages = profile
2063				.dirty_pages
2064				.saturating_add(request.dirty_pages.len() as u64);
2065			profile.dirty_bytes = profile.dirty_bytes.saturating_add(
2066				request
2067					.dirty_pages
2068					.iter()
2069					.map(|page| page.bytes.len() as u64)
2070					.sum::<u64>(),
2071			);
2072		});
2073
2074		let (outcome, transport_metrics) =
2075			// Transport rejection, including envoy shutdown while a VFS callback is
2076			// active, becomes CommitBufferError here. xSync and xClose then surface
2077			// it to SQLite as SQLITE_IOERR_*.
2078			match self.block_on_buffered_commit(request.clone(), timeout) {
2079				Ok(CommitWait::Completed(outcome)) => outcome,
2080				Ok(CommitWait::TimedOut) => return Ok(CommitWait::TimedOut),
2081				Err(err) => {
2082					tracing::error!(
2083						actor_id = %self.actor_id,
2084						new_db_size_pages = request.new_db_size_pages,
2085						dirty_pages = request.dirty_pages.len(),
2086						?err,
2087						"sqlite flush commit failed"
2088					);
2089					handle_non_finalize_commit_error(self, &err);
2090					return Err(err);
2091				}
2092			};
2093		self.commit_total
2094			.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2095		if let Some(metrics) = &self.metrics {
2096			metrics.record_commit(request.dirty_pages.len() as u64);
2097		}
2098		tracing::debug!(
2099			dirty_pages = request.dirty_pages.len(),
2100			path = ?outcome.path,
2101			requested_db_size_pages = request.new_db_size_pages,
2102			db_size_pages = outcome.db_size_pages,
2103			request_build_ns,
2104			serialize_ns = transport_metrics.serialize_ns,
2105			transport_ns = transport_metrics.transport_ns,
2106			"vfs commit complete (flush)"
2107		);
2108		#[cfg(debug_assertions)]
2109		{
2110			if outcome.db_size_pages != request.new_db_size_pages {
2111				tracing::warn!(
2112					actor_id = %self.actor_id,
2113					dirty_pages = request.dirty_pages.len(),
2114					path = ?outcome.path,
2115					requested_db_size_pages = request.new_db_size_pages,
2116					outcome_db_size_pages = outcome.db_size_pages,
2117					"sqlite flush commit returned db size different from request"
2118				);
2119			}
2120		}
2121		let state_update_start = Instant::now();
2122		let mut state = self.state.write();
2123		state.db_size_pages = outcome.db_size_pages;
2124		state.committed_db_size_pages = outcome.db_size_pages;
2125		state.head_txid = outcome
2126			.head_txid
2127			.or_else(|| state.head_txid.map(|head_txid| head_txid.saturating_add(1)));
2128		for dirty_page in &request.dirty_pages {
2129			state.note_committed_page(&self.config, dirty_page.pgno, &dirty_page.bytes);
2130		}
2131		state.write_buffer.dirty.clear();
2132		state.refused_write_pending = false;
2133		let state_update_ns = state_update_start.elapsed().as_nanos() as u64;
2134		self.add_commit_phase_metrics(
2135			request_build_ns,
2136			transport_metrics,
2137			state_update_ns,
2138			total_start.elapsed().as_nanos() as u64,
2139		);
2140		Ok(CommitWait::Completed(Some(outcome)))
2141	}
2142
2143	fn commit_atomic_write(&self) -> std::result::Result<(), CommitBufferError> {
2144		match self.commit_atomic_write_with_timeout(None)? {
2145			CommitWait::Completed(()) => Ok(()),
2146			CommitWait::TimedOut => Err(CommitBufferError::Other(
2147				"sqlite commit timed out".to_string(),
2148			)),
2149		}
2150	}
2151
2152	fn commit_atomic_write_with_timeout(
2153		&self,
2154		timeout: Option<Duration>,
2155	) -> std::result::Result<CommitWait<()>, CommitBufferError> {
2156		let total_start = Instant::now();
2157		let request_build_start = Instant::now();
2158		let request = {
2159			let mut state = self.state.write();
2160			if state.dead {
2161				return Err(CommitBufferError::Other(
2162					"sqlite actor lost its fence".to_string(),
2163				));
2164			}
2165			if !state.write_buffer.in_atomic_write {
2166				return Ok(CommitWait::Completed(()));
2167			}
2168			if state.closing {
2169				state.discard_uncommitted_pages();
2170				return Ok(CommitWait::Completed(()));
2171			}
2172			if state.write_buffer.dirty.is_empty()
2173				&& state.db_size_pages == state.committed_db_size_pages
2174			{
2175				state.write_buffer.in_atomic_write = false;
2176				return Ok(CommitWait::Completed(()));
2177			}
2178			match state.cork_dirty_pages(self.config.commit_page_cap()) {
2179				CorkOutcome::NotCorked => {}
2180				CorkOutcome::Corked => {
2181					state.write_buffer.in_atomic_write = false;
2182					state.refused_write_pending = false;
2183					return Ok(CommitWait::Completed(()));
2184				}
2185				CorkOutcome::Overflow => {
2186					drop(state);
2187					self.commit_cork_alone()?;
2188					let mut state = self.state.write();
2189					return match state.cork_dirty_pages(self.config.commit_page_cap()) {
2190						CorkOutcome::Corked | CorkOutcome::NotCorked => {
2191							state.write_buffer.in_atomic_write = false;
2192							state.refused_write_pending = false;
2193							Ok(CommitWait::Completed(()))
2194						}
2195						CorkOutcome::Overflow => Err(self.transaction_too_large_commit_error()),
2196					};
2197				}
2198			}
2199
2200			BufferedCommitRequest {
2201				actor_id: self.actor_id.clone(),
2202				new_db_size_pages: state.db_size_pages,
2203				expected_head_txid: state.head_txid,
2204				dirty_pages: state.write_buffer.snapshot(),
2205			}
2206		};
2207		let request_build_ns = request_build_start.elapsed().as_nanos() as u64;
2208		self.update_operation_profile(|profile| {
2209			profile.dirty_pages = profile
2210				.dirty_pages
2211				.saturating_add(request.dirty_pages.len() as u64);
2212			profile.dirty_bytes = profile.dirty_bytes.saturating_add(
2213				request
2214					.dirty_pages
2215					.iter()
2216					.map(|page| page.bytes.len() as u64)
2217					.sum::<u64>(),
2218			);
2219		});
2220
2221		let (outcome, transport_metrics) =
2222			match self.block_on_buffered_commit(request.clone(), timeout) {
2223				Ok(CommitWait::Completed(outcome)) => outcome,
2224				Ok(CommitWait::TimedOut) => return Ok(CommitWait::TimedOut),
2225				Err(err) => {
2226					tracing::error!(
2227						actor_id = %self.actor_id,
2228						new_db_size_pages = request.new_db_size_pages,
2229						dirty_pages = request.dirty_pages.len(),
2230						?err,
2231						"sqlite atomic commit failed"
2232					);
2233					handle_non_finalize_commit_error(self, &err);
2234					return Err(err);
2235				}
2236			};
2237		self.commit_total
2238			.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2239		if let Some(metrics) = &self.metrics {
2240			metrics.record_commit(request.dirty_pages.len() as u64);
2241		}
2242		tracing::debug!(
2243			dirty_pages = request.dirty_pages.len(),
2244			path = ?outcome.path,
2245			requested_db_size_pages = request.new_db_size_pages,
2246			db_size_pages = outcome.db_size_pages,
2247			request_build_ns,
2248			serialize_ns = transport_metrics.serialize_ns,
2249			transport_ns = transport_metrics.transport_ns,
2250			"vfs commit complete (atomic)"
2251		);
2252		#[cfg(debug_assertions)]
2253		{
2254			if outcome.db_size_pages != request.new_db_size_pages {
2255				tracing::warn!(
2256					actor_id = %self.actor_id,
2257					dirty_pages = request.dirty_pages.len(),
2258					path = ?outcome.path,
2259					requested_db_size_pages = request.new_db_size_pages,
2260					outcome_db_size_pages = outcome.db_size_pages,
2261					"sqlite atomic commit returned db size different from request"
2262				);
2263			}
2264		}
2265		self.clear_last_error();
2266		let state_update_start = Instant::now();
2267		let mut state = self.state.write();
2268		state.db_size_pages = outcome.db_size_pages;
2269		state.committed_db_size_pages = outcome.db_size_pages;
2270		state.head_txid = outcome
2271			.head_txid
2272			.or_else(|| state.head_txid.map(|head_txid| head_txid.saturating_add(1)));
2273		for dirty_page in &request.dirty_pages {
2274			state.note_committed_page(&self.config, dirty_page.pgno, &dirty_page.bytes);
2275		}
2276		state.write_buffer.dirty.clear();
2277		state.write_buffer.in_atomic_write = false;
2278		state.refused_write_pending = false;
2279		let state_update_ns = state_update_start.elapsed().as_nanos() as u64;
2280		self.add_commit_phase_metrics(
2281			request_build_ns,
2282			transport_metrics,
2283			state_update_ns,
2284			total_start.elapsed().as_nanos() as u64,
2285		);
2286		Ok(CommitWait::Completed(()))
2287	}
2288
2289	/// Returns true when the truncate left behind a size change that only a commit can carry to
2290	/// depot.
2291	///
2292	/// SQLite's batch-atomic commit path truncates *after* `SQLITE_FCNTL_COMMIT_ATOMIC_WRITE` and
2293	/// does not sync afterwards, so a shrink recorded only in memory here never reaches depot. When
2294	/// the write buffer is empty the caller has to issue a size-only commit. When it is not, the
2295	/// buffered pages carry the new size on the commit that follows, and committing here would
2296	/// publish a transaction SQLite has not finished.
2297	fn truncate_main_file(&self, size: sqlite3_int64) -> bool {
2298		let page_size = self.page_size() as i64;
2299		let truncated_pages = ((size + page_size - 1) / page_size) as u32;
2300		let mut state = self.state.write();
2301		state.db_size_pages = truncated_pages;
2302		state
2303			.write_buffer
2304			.dirty
2305			.retain(|pgno, _| *pgno <= truncated_pages);
2306		let between_transactions =
2307			!state.write_buffer.in_atomic_write && state.write_buffer.dirty.is_empty();
2308		if let Some(corked) = state.corked.as_mut() {
2309			corked.retain(|pgno, _| *pgno <= truncated_pages);
2310		}
2311		// A shrink between transactions belongs to the last corked one.
2312		if state.corked.is_some() && between_transactions {
2313			state.corked_db_size_pages = truncated_pages;
2314		}
2315		state.invalidate_page_buffers();
2316		// A corked size change rides the uncorking commit.
2317		state.corked.is_none()
2318			&& !state.write_buffer.in_atomic_write
2319			&& state.write_buffer.dirty.is_empty()
2320			&& state.db_size_pages != state.committed_db_size_pages
2321	}
2322
2323	/// Corks commits: until `uncork`, each transaction SQLite commits stays in memory instead
2324	/// of going to depot. SQLite and its readers see the transactions as committed; none is
2325	/// durable until uncorking commits them all at once.
2326	fn cork(&self) {
2327		let mut state = self.state.write();
2328		if state.corked.is_none() {
2329			state.corked = Some(BTreeMap::new());
2330			// A cork inside a transaction starts from the committed size, not the size the
2331			// open transaction's spilled pages reached, so a close that discards the
2332			// transaction does not publish its growth.
2333			state.corked_db_size_pages = state.committed_db_size_pages;
2334		}
2335	}
2336
2337	/// Sends every transaction committed while corked to depot as one commit, then commits
2338	/// directly again. It must run between transactions, and fails without uncorking inside
2339	/// one. On a failed commit the pages are not durable and the database is unusable, as
2340	/// after any failed commit.
2341	fn uncork(&self) -> std::result::Result<(), CommitBufferError> {
2342		{
2343			let mut state = self.state.write();
2344			if state.corked.is_none() {
2345				return Ok(());
2346			}
2347			if state.write_buffer.in_atomic_write || !state.write_buffer.dirty.is_empty() {
2348				return Err(CommitBufferError::Other(
2349					"cannot uncork sqlite commits inside a transaction".to_string(),
2350				));
2351			}
2352			state.write_buffer.dirty = state.corked.take().unwrap_or_default();
2353		}
2354		self.flush_dirty_pages().map(|_| ())
2355	}
2356}
2357
2358impl Drop for VfsContext {
2359	fn drop(&mut self) {
2360		self.state.write().invalidate_page_buffers();
2361		self.report_prefetch_unused();
2362		let state = self.state.read();
2363		let page_cache_entries = state
2364			.prefetch_buffer
2365			.entry_count()
2366			.saturating_add(state.committed_buffer.entry_count());
2367		let page_cache_weighted_size = state.prefetch_buffer.weighted_size();
2368		tracing::debug!(
2369			actor_id = %self.actor_id,
2370			generation = ?self.generation,
2371			resolve_pages_calls = self.resolve_pages_total.load(Ordering::Relaxed),
2372			resolve_pages_cache_hits = self.resolve_pages_cache_hits.load(Ordering::Relaxed),
2373			get_pages_round_trips = self.resolve_pages_fetches.load(Ordering::Relaxed),
2374			pages_fetched_total = self.pages_fetched_total.load(Ordering::Relaxed),
2375			prefetch_pages_total = self.prefetch_pages_total.load(Ordering::Relaxed),
2376			prefetch_consumed_total = self.prefetch_consumed_total.load(Ordering::Relaxed),
2377			commit_count = self.commit_total.load(Ordering::Relaxed),
2378			db_size_pages = state.db_size_pages,
2379			head_txid = state.head_txid,
2380			page_cache_entries,
2381			page_cache_weighted_size,
2382			page_cache_capacity_pages = self.config.page_budget.capacity_pages(),
2383			"sqlite vfs close summary"
2384		);
2385	}
2386}
2387
2388fn cleanup_batch_atomic_probe(db: *mut sqlite3) {
2389	if let Err(err) = sqlite_exec(db, "DROP TABLE IF EXISTS __rivet_batch_probe;") {
2390		tracing::warn!(%err, "failed to clean up sqlite batch atomic probe table");
2391	}
2392}
2393
2394fn assert_batch_atomic_probe(db: *mut sqlite3, vfs: &SqliteVfs) -> std::result::Result<(), String> {
2395	let commit_atomic_before = vfs.commit_atomic_count();
2396	let probe_sql = "\
2397		BEGIN IMMEDIATE;\
2398		CREATE TABLE IF NOT EXISTS __rivet_batch_probe(x INTEGER);\
2399		INSERT INTO __rivet_batch_probe VALUES(1);\
2400		DELETE FROM __rivet_batch_probe;\
2401		DROP TABLE IF EXISTS __rivet_batch_probe;\
2402		COMMIT;\
2403	";
2404
2405	if let Err(err) = sqlite_exec(db, probe_sql) {
2406		let last_error = vfs.clone_last_error();
2407		tracing::error!(
2408			%err,
2409			last_error = ?last_error,
2410			commit_atomic_before,
2411			"sqlite batch atomic probe failed"
2412		);
2413		cleanup_batch_atomic_probe(db);
2414		if let Some(last_error) = last_error {
2415			return Err(format!(
2416				"batch atomic probe failed: {err}; vfs last_error: {last_error}"
2417			));
2418		}
2419		return Err(format!("batch atomic probe failed: {err}"));
2420	}
2421
2422	let commit_atomic_after = vfs.commit_atomic_count();
2423	if commit_atomic_after == commit_atomic_before {
2424		tracing::error!(
2425			commit_atomic_before,
2426			commit_atomic_after,
2427			last_error = ?vfs.clone_last_error(),
2428			"batch atomic writes not active for sqlite, SQLITE_ENABLE_BATCH_ATOMIC_WRITE may be missing"
2429		);
2430		cleanup_batch_atomic_probe(db);
2431		return Err(
2432			"batch atomic writes not active for sqlite, SQLITE_ENABLE_BATCH_ATOMIC_WRITE may be missing"
2433				.to_string(),
2434		);
2435	}
2436
2437	Ok(())
2438}
2439
2440/// Ephemeral commit sink used only by the process-startup batch-atomic probe.
2441///
2442/// The probe transaction is intentionally small enough to use a single-shot commit. Pages only
2443/// need to remain visible inside that transaction, where the VFS write buffer owns them, so the
2444/// sink does not need to retain committed data.
2445struct BatchAtomicProbeTransport;
2446
2447#[async_trait]
2448impl SqliteTransport for BatchAtomicProbeTransport {
2449	async fn get_pages(
2450		&self,
2451		request: protocol::SqliteGetPagesRequest,
2452	) -> Result<protocol::SqliteGetPagesResponse> {
2453		Ok(protocol::SqliteGetPagesResponse::SqliteGetPagesOk(
2454			protocol::SqliteGetPagesOk {
2455				pages: request
2456					.pgnos
2457					.into_iter()
2458					.map(|pgno| protocol::SqliteFetchedPage { pgno, bytes: None })
2459					.collect(),
2460				head_txid: Some(0),
2461			},
2462		))
2463	}
2464
2465	async fn commit(
2466		&self,
2467		_request: protocol::SqliteCommitRequest,
2468	) -> Result<protocol::SqliteCommitResponse> {
2469		Ok(protocol::SqliteCommitResponse::SqliteCommitOk(
2470			protocol::SqliteCommitOk { head_txid: None },
2471		))
2472	}
2473
2474	async fn commit_stage_begin(
2475		&self,
2476		_request: protocol::SqliteCommitStageBeginRequest,
2477	) -> Result<protocol::SqliteCommitStageBeginResponse> {
2478		anyhow::bail!("batch-atomic startup probe unexpectedly attempted a staged commit")
2479	}
2480
2481	async fn commit_stage_segment(
2482		&self,
2483		_request: protocol::SqliteCommitStageSegmentRequest,
2484	) -> Result<protocol::SqliteCommitStageSegmentResponse> {
2485		anyhow::bail!("batch-atomic startup probe unexpectedly attempted a staged commit")
2486	}
2487
2488	async fn commit_finalize(
2489		&self,
2490		_request: protocol::SqliteCommitFinalizeRequest,
2491	) -> Result<protocol::SqliteCommitFinalizeResponse> {
2492		anyhow::bail!("batch-atomic startup probe unexpectedly attempted a staged commit")
2493	}
2494}
2495
2496fn verify_batch_atomic_writes_at_process_startup_blocking(
2497	runtime: Handle,
2498	config: VfsConfig,
2499) -> std::result::Result<(), String> {
2500	let probe_id = NEXT_TEMP_AUX_ID.fetch_add(1, Ordering::Relaxed);
2501	let file_name = format!("batch-atomic-startup-probe-{probe_id}.db");
2502	let vfs = SqliteVfs::register_with_transport_and_initial_pages(
2503		&format!("batch-atomic-startup-probe-{probe_id}"),
2504		Arc::new(BatchAtomicProbeTransport),
2505		file_name.clone(),
2506		runtime,
2507		config,
2508		InitialPages::default(),
2509		None,
2510	)?;
2511	let connection = open_connection(
2512		Arc::new(vfs),
2513		&file_name,
2514		SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
2515	)?;
2516	configure_connection_for_database(connection.as_ptr(), &connection._vfs, &file_name)
2517		.map_err(|error| error.to_string())?;
2518	assert_batch_atomic_probe(connection.as_ptr(), &connection._vfs)
2519}
2520
2521/// Verifies the linked SQLite pager and the production VFS batch-atomic callbacks before this
2522/// process becomes eligible to run actors.
2523pub async fn verify_batch_atomic_writes_at_process_startup() -> Result<()> {
2524	verify_batch_atomic_writes_at_process_startup_with_config(VfsConfig::default()).await
2525}
2526
2527async fn verify_batch_atomic_writes_at_process_startup_with_config(
2528	config: VfsConfig,
2529) -> Result<()> {
2530	let runtime = Handle::try_current().map_err(|err| {
2531		anyhow::anyhow!("batch-atomic startup probe requires a Tokio runtime: {err}")
2532	})?;
2533	let result = tokio::task::spawn_blocking(move || {
2534		verify_batch_atomic_writes_at_process_startup_blocking(runtime, config)
2535	})
2536	.await
2537	.map_err(|err| anyhow::anyhow!("batch-atomic startup probe task failed: {err}"))?;
2538	result.map_err(anyhow::Error::msg)
2539}
2540
2541fn handle_non_finalize_commit_error(ctx: &VfsContext, err: &CommitBufferError) {
2542	match err {
2543		CommitBufferError::FenceMismatch(message) => ctx.mark_fatal(message.clone()),
2544		CommitBufferError::Indeterminate(operation) => {
2545			ctx.mark_indeterminate(operation.clone());
2546		}
2547		CommitBufferError::Storage(error) => ctx.set_storage_error(error.clone()),
2548		CommitBufferError::Other(message) => ctx.set_last_error(message.clone()),
2549	}
2550}
2551
2552fn handle_finalize_fence_error(ctx: &VfsContext, err: &CommitBufferError) {
2553	if let CommitBufferError::FenceMismatch(reason) = err {
2554		ctx.mark_fatal(reason.clone());
2555	}
2556}
2557
2558
2559
2560pub(crate) async fn fetch_initial_pages_for_registration(
2561	transport: SqliteTransportHandle,
2562	actor_id: &str,
2563	generation: u64,
2564	_config: &VfsConfig,
2565) -> std::result::Result<InitialPages, String> {
2566	fetch_initial_pages(transport, actor_id.to_string(), generation, 1).await
2567}
2568
2569async fn fetch_initial_pages(
2570	transport: SqliteTransportHandle,
2571	actor_id: String,
2572	generation: u64,
2573	page_count: u32,
2574) -> std::result::Result<InitialPages, String> {
2575	tracing::debug!(
2576		actor_id = %actor_id,
2577		generation,
2578		page_count,
2579		"sqlite initial page preload request"
2580	);
2581
2582	let request_actor_id = actor_id.clone();
2583	let response = transport
2584		.get_pages(protocol::SqliteGetPagesRequest {
2585			actor_id: request_actor_id.into(),
2586			pgnos: (1..=page_count).collect(),
2587			expected_generation: None,
2588			expected_head_txid: None,
2589		})
2590		.await;
2591
2592	match response {
2593		Ok(protocol::SqliteGetPagesResponse::SqliteGetPagesOk(ok)) => {
2594			let head_txid = ok.head_txid;
2595			let pages: Vec<_> = ok
2596				.pages
2597				.into_iter()
2598				.filter_map(|page| page.bytes.map(|bytes| (page.pgno, bytes.into())))
2599				.collect();
2600			tracing::debug!(
2601				actor_id = %actor_id,
2602				generation,
2603				page_count,
2604				loaded_pages = pages.len(),
2605				head_txid,
2606				"sqlite initial page preload result"
2607			);
2608			Ok(InitialPages {
2609				pages,
2610				head_txid,
2611				requested_page_count: page_count,
2612			})
2613		}
2614		Ok(protocol::SqliteGetPagesResponse::SqliteErrorResponse(error)) => {
2615			if !is_initial_main_page_missing(&error.message) {
2616				return Err(format!(
2617					"sqlite initial page fetch failed: {}",
2618					error.message
2619				));
2620			}
2621			tracing::info!(
2622				actor_id = %actor_id,
2623				generation,
2624				page_count,
2625				error = %error.message,
2626				"sqlite initial page fetch did not find persisted data"
2627			);
2628			Ok(InitialPages {
2629				pages: Vec::new(),
2630				head_txid: Some(0),
2631				requested_page_count: page_count,
2632			})
2633		}
2634		Err(err) => Err(format!("sqlite initial page fetch failed: {err}")),
2635	}
2636}
2637
2638fn is_initial_main_page_missing(message: &str) -> bool {
2639	message.contains("sqlite database was not found in this bucket branch")
2640		|| message.contains("sqlite meta missing for get_pages")
2641		|| message == "actor does not exist"
2642}
2643
2644fn next_temp_aux_path() -> String {
2645	format!(
2646		"{TEMP_AUX_PATH_PREFIX}-{}",
2647		NEXT_TEMP_AUX_ID.fetch_add(1, Ordering::Relaxed)
2648	)
2649}
2650
2651unsafe fn get_aux_state(file: &VfsFile) -> Option<&AuxFileHandle> {
2652	(!file.aux.is_null()).then(|| &*file.aux)
2653}
2654
2655/// Dirty pages above which a commit is staged in segments rather than sent as one message.
2656///
2657/// Matches the engine's single-shot cap. A commit at or under it fits one FDB transaction with its
2658/// page bytes included, so it takes the single round trip; anything larger has to be staged because
2659/// the engine would reject it, not merely because staging is faster.
2660///
2661/// Kept as its own value rather than imported, since the engine is free to lower its cap and this
2662/// side must keep working against an engine on either side of that change. Sending a single-shot
2663/// commit the engine refuses fails the commit, while staging one it would have accepted only costs
2664/// extra round trips.
2665const COMMIT_STAGE_THRESHOLD_PAGES: usize = 320;
2666
2667async fn commit_buffered_pages(
2668	transport: &dyn SqliteTransport,
2669	request: BufferedCommitRequest,
2670	max_dirty_pages: usize,
2671) -> std::result::Result<(BufferedCommitOutcome, CommitTransportMetrics), CommitBufferError> {
2672	// Refused before sending rather than discovered partway through. The engine enforces the same
2673	// cap and is the authority, but finding out from it means staging up to 128 MiB of segments
2674	// first, and the bytes already staged are only reclaimed when some later commit reopens the txid.
2675	if request.dirty_pages.len() > max_dirty_pages {
2676		return Err(CommitBufferError::Other(format!(
2677			"commit of {} dirty pages exceeds the {} page maximum",
2678			request.dirty_pages.len(),
2679			max_dirty_pages,
2680		)));
2681	}
2682
2683	if request.dirty_pages.len() > COMMIT_STAGE_THRESHOLD_PAGES {
2684		return commit_staged_pages(transport, request).await;
2685	}
2686
2687	let mut metrics = CommitTransportMetrics::default();
2688	let serialize_start = Instant::now();
2689	let BufferedCommitRequest {
2690		actor_id,
2691		new_db_size_pages,
2692		dirty_pages,
2693		expected_head_txid,
2694	} = request;
2695	let commit_request = protocol::SqliteCommitRequest {
2696		actor_id: actor_id.into(),
2697		dirty_pages: dirty_pages
2698			.into_iter()
2699			.map(|page| protocol::SqliteDirtyPage {
2700				pgno: page.pgno,
2701				bytes: page.bytes.into_bytes(),
2702			})
2703			.collect(),
2704		db_size_pages: new_db_size_pages,
2705		now_ms: sqlite_now_ms().map_err(|err| CommitBufferError::Other(err.to_string()))?,
2706		expected_generation: None,
2707		expected_head_txid,
2708	};
2709	metrics.serialize_ns += serialize_start.elapsed().as_nanos() as u64;
2710	let transport_start = Instant::now();
2711	match transport
2712		.commit(commit_request)
2713		.await
2714		.map_err(commit_transport_error)?
2715	{
2716		protocol::SqliteCommitResponse::SqliteCommitOk(ok) => {
2717			metrics.transport_ns += transport_start.elapsed().as_nanos() as u64;
2718			Ok((
2719				BufferedCommitOutcome {
2720					path: CommitPath::Fast,
2721					db_size_pages: new_db_size_pages,
2722					head_txid: ok.head_txid,
2723				},
2724				metrics,
2725			))
2726		}
2727		protocol::SqliteCommitResponse::SqliteErrorResponse(error) => {
2728			if is_head_fence_mismatch_response(&error) {
2729				Err(CommitBufferError::FenceMismatch(error.message.to_string()))
2730			} else if let Some(refusal) = storage_refusal(&error) {
2731				Err(CommitBufferError::Storage(refusal))
2732			} else {
2733				Err(CommitBufferError::Other(error.message.to_string()))
2734			}
2735		}
2736	}
2737}
2738
2739/// Commits pages too numerous for one message by staging them in shard-aligned segments.
2740///
2741/// The whole commit fails on any error, exactly as the single-shot path does: SQLite sees one failed
2742/// commit, never a partial one. Nothing staged is visible until finalize, so an abandoned attempt
2743/// leaves the database exactly as it was, and the engine reclaims the staged bytes when the next
2744/// attempt reopens the same txid.
2745async fn commit_staged_pages(
2746	transport: &dyn SqliteTransport,
2747	request: BufferedCommitRequest,
2748) -> std::result::Result<(BufferedCommitOutcome, CommitTransportMetrics), CommitBufferError> {
2749	let mut metrics = CommitTransportMetrics::default();
2750	let serialize_start = Instant::now();
2751	let mut dirty_pages = request.dirty_pages;
2752	// Cutting segments needs ascending pages, and sorting once here keeps the per-segment work to a
2753	// slice.
2754	dirty_pages.sort_by_key(|page| page.pgno);
2755	metrics.serialize_ns += serialize_start.elapsed().as_nanos() as u64;
2756
2757	let transport_start = Instant::now();
2758	let txid = match transport
2759		.commit_stage_begin(protocol::SqliteCommitStageBeginRequest {
2760			actor_id: request.actor_id.clone().into(),
2761			expected_generation: None,
2762			expected_head_txid: request.expected_head_txid,
2763		})
2764		.await
2765		.map_err(commit_transport_error)?
2766	{
2767		protocol::SqliteCommitStageBeginResponse::SqliteCommitStageBeginOk(ok) => ok.txid,
2768		protocol::SqliteCommitStageBeginResponse::SqliteErrorResponse(error) => {
2769			return Err(staged_commit_error(error));
2770		}
2771	};
2772
2773	let segments = sqlite_types::cut_page_segments(&dirty_pages, |page| page.pgno);
2774	let segment_first_pgnos = segments.iter().map(|(first_pgno, _)| *first_pgno).collect();
2775	// Each segment spans at most five shards. Buffered requests cap their combined page bytes,
2776	// while ordered responses preserve the first failure and finalize waits for every admission.
2777	let responses = stream::iter(segments)
2778		.map(|(first_pgno, segment)| {
2779			let dirty_pages = segment
2780				.iter()
2781				.map(|page| protocol::SqliteDirtyPage {
2782					pgno: page.pgno,
2783					bytes: page.bytes.clone().into_bytes(),
2784				})
2785				.collect();
2786			let actor_id = request.actor_id.clone();
2787			async move {
2788				match transport
2789					.commit_stage_segment(protocol::SqliteCommitStageSegmentRequest {
2790						actor_id: actor_id.into(),
2791						expected_generation: None,
2792						txid,
2793						first_pgno,
2794						dirty_pages,
2795					})
2796					.await
2797					.map_err(commit_transport_error)?
2798				{
2799					protocol::SqliteCommitStageSegmentResponse::SqliteCommitStageSegmentOk(_) => {
2800						Ok(())
2801					}
2802					protocol::SqliteCommitStageSegmentResponse::SqliteErrorResponse(error) => {
2803						Err(staged_commit_error(error))
2804					}
2805				}
2806			}
2807		})
2808		.buffered(STAGE_PIPELINE_DEPTH)
2809		.collect::<Vec<_>>()
2810		.await;
2811	for response in responses {
2812		response?;
2813	}
2814
2815	let head_txid = match transport
2816		.commit_finalize(protocol::SqliteCommitFinalizeRequest {
2817			actor_id: request.actor_id.clone().into(),
2818			expected_generation: None,
2819			txid,
2820			new_db_size_pages: request.new_db_size_pages,
2821			now_ms: sqlite_now_ms().map_err(|err| CommitBufferError::Other(err.to_string()))?,
2822			segment_first_pgnos,
2823		})
2824		.await
2825		.map_err(commit_transport_error)?
2826	{
2827		protocol::SqliteCommitFinalizeResponse::SqliteCommitFinalizeOk(ok) => ok.head_txid,
2828		protocol::SqliteCommitFinalizeResponse::SqliteErrorResponse(error) => {
2829			return Err(staged_commit_error(error));
2830		}
2831	};
2832	metrics.transport_ns += transport_start.elapsed().as_nanos() as u64;
2833
2834	Ok((
2835		BufferedCommitOutcome {
2836			path: CommitPath::Staged,
2837			db_size_pages: request.new_db_size_pages,
2838			head_txid,
2839		},
2840		metrics,
2841	))
2842}
2843
2844fn commit_transport_error(error: anyhow::Error) -> CommitBufferError {
2845	if let Some(timeout) = storage_timeout(&error) {
2846		return CommitBufferError::Storage(timeout);
2847	}
2848	match error.downcast::<SqliteCommitIndeterminateError>() {
2849		Ok(error) => CommitBufferError::Indeterminate(error.0),
2850		Err(error) => CommitBufferError::Other(error.to_string()),
2851	}
2852}
2853
2854fn staged_commit_error(error: protocol::SqliteErrorResponse) -> CommitBufferError {
2855	if is_head_fence_mismatch_response(&error) {
2856		CommitBufferError::FenceMismatch(error.message.to_string())
2857	} else if let Some(refusal) = storage_refusal(&error) {
2858		CommitBufferError::Storage(refusal)
2859	} else {
2860		CommitBufferError::Other(error.message.to_string())
2861	}
2862}
2863
2864fn is_head_fence_mismatch_response(error: &protocol::SqliteErrorResponse) -> bool {
2865	is_head_fence_mismatch(&error.group, &error.code)
2866}
2867
2868unsafe fn get_file(p: *mut sqlite3_file) -> &'static mut VfsFile {
2869	&mut *(p as *mut VfsFile)
2870}
2871
2872unsafe fn get_vfs_ctx(p: *mut sqlite3_vfs) -> &'static VfsContext {
2873	&*((*p).pAppData as *const VfsContext)
2874}
2875
2876fn sqlite_error_message(db: *mut sqlite3) -> String {
2877	unsafe {
2878		if db.is_null() {
2879			"unknown sqlite error".to_string()
2880		} else {
2881			CStr::from_ptr(sqlite3_errmsg(db))
2882				.to_string_lossy()
2883				.into_owned()
2884		}
2885	}
2886}
2887
2888pub(crate) fn sqlite_exec(db: *mut sqlite3, sql: &str) -> std::result::Result<(), String> {
2889	let c_sql = CString::new(sql).map_err(|err| err.to_string())?;
2890	let rc = unsafe { sqlite3_exec(db, c_sql.as_ptr(), None, ptr::null_mut(), ptr::null_mut()) };
2891	if rc != SQLITE_OK {
2892		return Err(format!(
2893			"`{sql}` failed with code {rc}: {}",
2894			sqlite_error_message(db)
2895		));
2896	}
2897	Ok(())
2898}
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918fn page_span(offset: i64, length: usize, page_size: usize) -> std::result::Result<Vec<u32>, ()> {
2919	if offset < 0 {
2920		return Err(());
2921	}
2922	if length == 0 {
2923		return Ok(Vec::new());
2924	}
2925
2926	let start = offset as usize / page_size + 1;
2927	let end = (offset as usize + length - 1) / page_size + 1;
2928	Ok((start as u32..=end as u32).collect())
2929}
2930
2931unsafe extern "C" fn io_close(p_file: *mut sqlite3_file) -> c_int {
2932	vfs_catch_unwind!(SQLITE_IOERR, {
2933		if p_file.is_null() {
2934			return SQLITE_OK;
2935		}
2936		let file = get_file(p_file);
2937		let result = if !file.aux.is_null() {
2938			let aux = Box::from_raw(file.aux);
2939			if aux.delete_on_close {
2940				let ctx = &*file.ctx;
2941				ctx.delete_aux_file(&aux.path);
2942			}
2943			file.aux = ptr::null_mut();
2944			Ok(())
2945		} else {
2946			let ctx = &*file.ctx;
2947			
2948			ctx.discard_refused_transaction();
2949			let should_flush = {
2950				let state = ctx.state.read();
2951				state.write_buffer.in_atomic_write
2952					|| !state.write_buffer.dirty.is_empty()
2953					|| state.db_size_pages != state.committed_db_size_pages
2954					|| state.corked.is_some()
2955			};
2956			if should_flush {
2957				if ctx.state.read().write_buffer.in_atomic_write {
2958					ctx.commit_atomic_write().map(|_| ())
2959				} else {
2960					ctx.uncork()
2961						.and_then(|()| ctx.flush_dirty_pages().map(|_| ()))
2962				}
2963			} else {
2964				Ok(())
2965			}
2966		};
2967		file.base.pMethods = ptr::null();
2968		match result {
2969			Ok(()) => SQLITE_OK,
2970			Err(err) => {
2971				let ctx = &*file.ctx;
2972				handle_finalize_fence_error(ctx, &err);
2973				SQLITE_IOERR
2974			}
2975		}
2976	})
2977}
2978
2979unsafe extern "C" fn io_read(
2980	p_file: *mut sqlite3_file,
2981	p_buf: *mut c_void,
2982	i_amt: c_int,
2983	i_offset: sqlite3_int64,
2984) -> c_int {
2985	vfs_catch_unwind!(SQLITE_IOERR_READ, {
2986		if i_amt <= 0 {
2987			return SQLITE_OK;
2988		}
2989
2990		let file = get_file(p_file);
2991		if let Some(aux) = get_aux_state(file) {
2992			if i_offset < 0 {
2993				return SQLITE_IOERR_READ;
2994			}
2995
2996			let offset = i_offset as usize;
2997			let requested = i_amt as usize;
2998			let buf = slice::from_raw_parts_mut(p_buf.cast::<u8>(), requested);
2999			buf.fill(0);
3000
3001			let bytes = aux.state.bytes.lock();
3002			if offset >= bytes.len() {
3003				return SQLITE_IOERR_SHORT_READ;
3004			}
3005
3006			let copy_len = requested.min(bytes.len() - offset);
3007			buf[..copy_len].copy_from_slice(&bytes[offset..offset + copy_len]);
3008			return if copy_len < requested {
3009				SQLITE_IOERR_SHORT_READ
3010			} else {
3011				SQLITE_OK
3012			};
3013		}
3014
3015		let ctx = &*file.ctx;
3016		if ctx.is_dead() {
3017			return SQLITE_IOERR_READ;
3018		}
3019
3020		let buf = slice::from_raw_parts_mut(p_buf.cast::<u8>(), i_amt as usize);
3021		let requested_pages = match page_span(i_offset, i_amt as usize, ctx.page_size()) {
3022			Ok(pages) => pages,
3023			Err(_) => return SQLITE_IOERR_READ,
3024		};
3025		let page_size = ctx.page_size();
3026		let (file_size, db_size_pages) = {
3027			let state = ctx.state.read();
3028			(
3029				state.db_size_pages as usize * state.page_size,
3030				state.db_size_pages,
3031			)
3032		};
3033
3034		let partial = i_offset as usize % page_size != 0 || i_amt as usize % page_size != 0;
3035		let resolved = match ctx.resolve_pages(&requested_pages, partial) {
3036			Ok(pages) => pages,
3037			Err(GetPagesError::FenceMismatch(message)) => {
3038				tracing::error!(
3039					actor_id = %ctx.actor_id,
3040					requested_pages = ?requested_pages,
3041					error = %message,
3042					"sqlite xRead hit fatal sqlite error"
3043				);
3044				ctx.mark_fatal(message);
3045				return SQLITE_IOERR_READ;
3046			}
3047			Err(GetPagesError::Storage(error)) => {
3048				tracing::warn!(
3049					actor_id = %ctx.actor_id,
3050					requested_pages = ?requested_pages,
3051					%error,
3052					"sqlite xRead failed without changing storage; the statement fails with a typed storage error"
3053				);
3054				ctx.set_storage_error(error);
3055				return SQLITE_IOERR_READ;
3056			}
3057			Err(GetPagesError::Other(message)) => {
3058				tracing::error!(
3059					actor_id = %ctx.actor_id,
3060					requested_pages = ?requested_pages,
3061					error = %message,
3062					"sqlite xRead failed to resolve pages"
3063				);
3064				ctx.set_last_error(message);
3065				return SQLITE_IOERR_READ;
3066			}
3067		};
3068		ctx.clear_last_error();
3069
3070		#[cfg(debug_assertions)]
3071		{
3072			let missing_in_range_pages = requested_pages
3073				.iter()
3074				.copied()
3075				.filter(|pgno| *pgno <= db_size_pages)
3076				.filter(|pgno| !matches!(resolved.get(pgno), Some(Some(_))))
3077				.collect::<Vec<_>>();
3078			if !missing_in_range_pages.is_empty() {
3079				tracing::warn!(
3080					actor_id = %ctx.actor_id,
3081					offset = i_offset,
3082					amount = i_amt,
3083					page_size,
3084					db_size_pages,
3085					file_size,
3086					requested_pages = ?requested_pages,
3087					missing_in_range_pages = ?missing_in_range_pages,
3088					"sqlite xRead would zero-fill pages within declared db size"
3089				);
3090			}
3091		}
3092
3093		buf.fill(0);
3094		for pgno in requested_pages.iter().copied() {
3095			let Some(Some(bytes)) = resolved.get(&pgno) else {
3096				continue;
3097			};
3098			let page_start = (pgno as usize - 1) * page_size;
3099			let copy_start = page_start.max(i_offset as usize);
3100			let copy_end = (page_start + page_size).min(i_offset as usize + i_amt as usize);
3101			if copy_start >= copy_end {
3102				continue;
3103			}
3104			let page_offset = copy_start - page_start;
3105			let dest_offset = copy_start - i_offset as usize;
3106			let copy_len = copy_end - copy_start;
3107			buf[dest_offset..dest_offset + copy_len]
3108				.copy_from_slice(&bytes[page_offset..page_offset + copy_len]);
3109		}
3110
3111		if i_offset as usize + i_amt as usize > file_size {
3112			return SQLITE_IOERR_SHORT_READ;
3113		}
3114
3115		SQLITE_OK
3116	})
3117}
3118
3119/// How many page records a write adds to the main rollback journal.
3120///
3121/// A rollback replays every journal record, so the journal holds at most as many records as
3122/// one commit may carry. SQLite writes a record as a 4 byte page number, the page at an offset
3123/// that is never sector-aligned, and a 4 byte checksum; headers are page-sized but
3124/// sector-aligned. When it moves an in-memory journal to the file, it copies it in chunks of
3125/// any size, so other writes that extend the file count every record they could hold. That
3126/// overcounts the headers, which only makes the cap refuse a little earlier.
3127fn journal_records_added(offset: usize, len: usize, file_len: usize, page_size: usize) -> usize {
3128	if len == page_size {
3129		return usize::from(offset % DEFAULT_PAGE_SIZE != 0);
3130	}
3131	if len == 4 {
3132		return 0;
3133	}
3134	let appended = (offset + len).saturating_sub(file_len.max(offset));
3135	appended.div_ceil(page_size + 8)
3136}
3137
3138unsafe extern "C" fn io_write(
3139	p_file: *mut sqlite3_file,
3140	p_buf: *const c_void,
3141	i_amt: c_int,
3142	i_offset: sqlite3_int64,
3143) -> c_int {
3144	vfs_catch_unwind!(SQLITE_IOERR_WRITE, {
3145		if i_amt <= 0 {
3146			return SQLITE_OK;
3147		}
3148
3149		let file = get_file(p_file);
3150		if let Some(aux) = get_aux_state(file) {
3151			if i_offset < 0 {
3152				return SQLITE_IOERR_WRITE;
3153			}
3154
3155			let offset = i_offset as usize;
3156			let source = slice::from_raw_parts(p_buf.cast::<u8>(), i_amt as usize);
3157			let end = offset + source.len();
3158			let mut bytes = aux.state.bytes.lock();
3159			if aux.is_main_journal {
3160				let ctx = &*file.ctx;
3161				let added =
3162					journal_records_added(offset, source.len(), bytes.len(), ctx.page_size());
3163				let records = aux.state.journal_records.load(Ordering::Relaxed);
3164				if added > 0 && records + added > ctx.config.commit_page_cap() {
3165					drop(bytes);
3166					return ctx.refuse_oversize_transaction();
3167				}
3168				aux.state
3169					.journal_records
3170					.store(records + added, Ordering::Relaxed);
3171			}
3172			if bytes.len() < end {
3173				bytes.resize(end, 0);
3174			}
3175			bytes[offset..end].copy_from_slice(source);
3176			return SQLITE_OK;
3177		}
3178
3179		let ctx = &*file.ctx;
3180		if ctx.is_dead() {
3181			return SQLITE_IOERR_WRITE;
3182		}
3183
3184		let page_size = ctx.page_size();
3185		let source = slice::from_raw_parts(p_buf.cast::<u8>(), i_amt as usize);
3186		let target_pages = match page_span(i_offset, i_amt as usize, page_size) {
3187			Ok(pages) => pages,
3188			Err(_) => return SQLITE_IOERR_WRITE,
3189		};
3190
3191		// Bound the transaction's dirty pages before holding them, not when committing them.
3192		// Pages the transaction already changed stay writable, so SQLite's rollback, which
3193		// rewrites journaled pages, always fits.
3194		{
3195			let state = ctx.state.read();
3196			let new_pages = target_pages
3197				.iter()
3198				.filter(|pgno| !state.write_buffer.dirty.contains_key(pgno))
3199				.count();
3200			if state.write_buffer.dirty.len() + new_pages > ctx.config.commit_page_cap() {
3201				drop(state);
3202				ctx.state.write().refused_write_pending = true;
3203				return ctx.refuse_oversize_transaction();
3204			}
3205		}
3206
3207		// Fast path: for full-page aligned writes we don't need the existing
3208		// page data because we're overwriting every byte. Skip resolve_pages
3209		// to eliminate a round trip to the engine per page. Also, for pages
3210		// beyond db_size_pages (new allocations), there's nothing to fetch.
3211		let offset = i_offset as usize;
3212		let amt = i_amt as usize;
3213		let is_aligned_full_page = offset % page_size == 0 && amt % page_size == 0;
3214
3215		let mut resolved = HashMap::new();
3216		if !is_aligned_full_page {
3217			let (db_size_pages, pages_to_resolve): (u32, Vec<u32>) = {
3218				let state = ctx.state.read();
3219				let known_max = state.db_size_pages;
3220				(
3221					known_max,
3222					target_pages
3223						.iter()
3224						.copied()
3225						.filter(|pgno| {
3226							*pgno <= known_max
3227								&& !state.write_buffer.dirty.contains_key(pgno)
3228								&& state.corked_page(*pgno).is_none()
3229						})
3230						.collect(),
3231				)
3232			};
3233
3234			if !pages_to_resolve.is_empty() {
3235				resolved = match ctx.resolve_pages(&pages_to_resolve, false) {
3236					Ok(pages) => pages,
3237					Err(GetPagesError::FenceMismatch(message)) => {
3238						ctx.mark_fatal(message);
3239						return SQLITE_IOERR_WRITE;
3240					}
3241					Err(GetPagesError::Storage(error)) => {
3242						ctx.set_storage_error(error);
3243						return SQLITE_IOERR_WRITE;
3244					}
3245					Err(GetPagesError::Other(message)) => {
3246						ctx.set_last_error(message);
3247						return SQLITE_IOERR_WRITE;
3248					}
3249				};
3250			}
3251			for pgno in &target_pages {
3252				if *pgno > db_size_pages {
3253					resolved.entry(*pgno).or_insert(None);
3254				}
3255			}
3256			#[cfg(debug_assertions)]
3257			{
3258				let missing_existing_pages = target_pages
3259					.iter()
3260					.copied()
3261					.filter(|pgno| *pgno <= db_size_pages)
3262					.filter(|pgno| pages_to_resolve.contains(pgno))
3263					.filter(|pgno| !matches!(resolved.get(pgno), Some(Some(_))))
3264					.collect::<Vec<_>>();
3265				if !missing_existing_pages.is_empty() {
3266					tracing::warn!(
3267						actor_id = %ctx.actor_id,
3268						offset = i_offset,
3269						amount = i_amt,
3270						page_size,
3271						db_size_pages,
3272						target_pages = ?target_pages,
3273						missing_existing_pages = ?missing_existing_pages,
3274						"sqlite xWrite partial update would synthesize existing pages from zeros"
3275					);
3276				}
3277			}
3278		}
3279
3280		let mut state = ctx.state.write();
3281		for pgno in target_pages {
3282			let page_start = (pgno as usize - 1) * page_size;
3283			let patch_start = page_start.max(offset);
3284			let patch_end = (page_start + page_size).min(offset + amt);
3285			let Some(copy_len) = patch_end.checked_sub(patch_start) else {
3286				continue;
3287			};
3288			if copy_len == 0 {
3289				continue;
3290			}
3291
3292			let page_offset = patch_start - page_start;
3293			let source_offset = patch_start - offset;
3294			if is_aligned_full_page {
3295				state.write_buffer.dirty.insert(
3296					pgno,
3297					source[source_offset..source_offset + copy_len]
3298						.to_vec()
3299						.into(),
3300				);
3301				continue;
3302			}
3303
3304			let mut page = state
3305				.write_buffer
3306				.dirty
3307				.get_mut(&pgno)
3308				.map(std::mem::take)
3309				.map(PageBytes::into_vec)
3310				.unwrap_or_else(|| {
3311					resolved
3312						.remove(&pgno)
3313						.flatten()
3314						.map(PageBytes::into_vec)
3315						.unwrap_or_else(|| vec![0; page_size])
3316				});
3317			if page.len() < page_size {
3318				page.resize(page_size, 0);
3319			}
3320			page[page_offset..page_offset + copy_len]
3321				.copy_from_slice(&source[source_offset..source_offset + copy_len]);
3322			state.write_buffer.dirty.insert(pgno, page.into());
3323		}
3324
3325		let end_page = ((offset + amt) + page_size - 1) / page_size;
3326		state.db_size_pages = state.db_size_pages.max(end_page as u32);
3327		
3328		ctx.clear_last_error();
3329		SQLITE_OK
3330	})
3331}
3332
3333unsafe extern "C" fn io_truncate(p_file: *mut sqlite3_file, size: sqlite3_int64) -> c_int {
3334	vfs_catch_unwind!(SQLITE_IOERR_TRUNCATE, {
3335		if size < 0 {
3336			return SQLITE_IOERR_TRUNCATE;
3337		}
3338		let file = get_file(p_file);
3339		if let Some(aux) = get_aux_state(file) {
3340			let mut bytes = aux.state.bytes.lock();
3341			bytes.truncate(size as usize);
3342			bytes.shrink_to_fit();
3343			if size == 0 {
3344				aux.state.journal_records.store(0, Ordering::Relaxed);
3345			}
3346			return SQLITE_OK;
3347		}
3348		let ctx = &*file.ctx;
3349		if !ctx.truncate_main_file(size) {
3350			return SQLITE_OK;
3351		}
3352		match ctx.flush_dirty_pages() {
3353			Ok(_) => SQLITE_OK,
3354			Err(err) => {
3355				tracing::error!(
3356					actor_id = %ctx.actor_id,
3357					last_error = ?ctx.clone_last_error(),
3358					?err,
3359					"sqlite truncate commit failed"
3360				);
3361				handle_finalize_fence_error(ctx, &err);
3362				ctx.abandon_write_buffer();
3363				SQLITE_IOERR_TRUNCATE
3364			}
3365		}
3366	})
3367}
3368
3369/// xSync returns once `ctx.flush_dirty_pages()` resolves. Durability of those
3370/// bytes is delegated to depot's `sqlite_commit` reply. If pegboard-envoy ever
3371/// pre-acks before the FDB tx commit, xSync's durability contract is broken.
3372unsafe extern "C" fn io_sync(p_file: *mut sqlite3_file, _flags: c_int) -> c_int {
3373	vfs_catch_unwind!(SQLITE_IOERR_FSYNC, {
3374		let file = get_file(p_file);
3375		if get_aux_state(file).is_some() {
3376			return SQLITE_OK;
3377		}
3378		let ctx = &*file.ctx;
3379		if let Some(message) = ctx.take_transient_commit_error() {
3380			ctx.set_last_error(message);
3381			ctx.abandon_write_buffer();
3382			return SQLITE_IOERR_FSYNC;
3383		}
3384		match ctx.flush_dirty_pages() {
3385			Ok(_) => SQLITE_OK,
3386			Err(err) => {
3387				tracing::error!(
3388					actor_id = %ctx.actor_id,
3389					last_error = ?ctx.clone_last_error(),
3390					?err,
3391					"sqlite sync failed"
3392				);
3393				handle_finalize_fence_error(ctx, &err);
3394				ctx.abandon_write_buffer();
3395				SQLITE_IOERR_FSYNC
3396			}
3397		}
3398	})
3399}
3400
3401unsafe extern "C" fn io_file_size(p_file: *mut sqlite3_file, p_size: *mut sqlite3_int64) -> c_int {
3402	vfs_catch_unwind!(SQLITE_IOERR_FSTAT, {
3403		let file = get_file(p_file);
3404		if let Some(aux) = get_aux_state(file) {
3405			*p_size = aux.state.bytes.lock().len() as sqlite3_int64;
3406			return SQLITE_OK;
3407		}
3408		let ctx = &*file.ctx;
3409		let state = ctx.state.read();
3410		*p_size = (state.db_size_pages as usize * state.page_size) as sqlite3_int64;
3411		SQLITE_OK
3412	})
3413}
3414
3415// Lock callbacks are intentional no-ops. Pegboard guarantees a single actor
3416// process per actor_id, the database is opened with `locking_mode=EXCLUSIVE`,
3417// and only one SQLite connection runs against it, so SQLite's internal lock
3418// state machine is single-party and has nothing to coordinate with. Flipping
3419// to a non-EXCLUSIVE locking mode or a multi-connection setup would require
3420// implementing a real lock state ladder here.
3421unsafe extern "C" fn io_lock(_p_file: *mut sqlite3_file, _level: c_int) -> c_int {
3422	vfs_catch_unwind!(SQLITE_IOERR_LOCK, SQLITE_OK)
3423}
3424
3425unsafe extern "C" fn io_unlock(_p_file: *mut sqlite3_file, _level: c_int) -> c_int {
3426	vfs_catch_unwind!(SQLITE_IOERR_UNLOCK, SQLITE_OK)
3427}
3428
3429unsafe extern "C" fn io_check_reserved_lock(
3430	_p_file: *mut sqlite3_file,
3431	p_res_out: *mut c_int,
3432) -> c_int {
3433	vfs_catch_unwind!(SQLITE_IOERR, {
3434		*p_res_out = 0;
3435		SQLITE_OK
3436	})
3437}
3438
3439unsafe extern "C" fn io_file_control(
3440	p_file: *mut sqlite3_file,
3441	op: c_int,
3442	p_arg: *mut c_void,
3443) -> c_int {
3444	vfs_catch_unwind!(SQLITE_IOERR, {
3445		let file = get_file(p_file);
3446		if get_aux_state(file).is_some() {
3447			return SQLITE_NOTFOUND;
3448		}
3449		let ctx = &*file.ctx;
3450
3451		match op {
3452			SQLITE_FCNTL_BEGIN_ATOMIC_WRITE => {
3453				let mut state = ctx.state.write();
3454				state.write_buffer.in_atomic_write = true;
3455				state.write_buffer.saved_db_size = state.db_size_pages;
3456				state.write_buffer.dirty.clear();
3457				state.refused_write_pending = false;
3458				SQLITE_OK
3459			}
3460			SQLITE_FCNTL_COMMIT_ATOMIC_WRITE => match ctx.commit_atomic_write() {
3461				Ok(()) => {
3462					ctx.commit_atomic_count.fetch_add(1, Ordering::Relaxed);
3463					SQLITE_OK
3464				}
3465				Err(err) => {
3466					tracing::error!(
3467						actor_id = %ctx.actor_id,
3468						last_error = ?ctx.clone_last_error(),
3469						?err,
3470						"sqlite atomic write file control failed"
3471					);
3472					match &err {
3473						CommitBufferError::Other(message) => {
3474							ctx.defer_transient_commit_error(message.clone());
3475						}
3476						CommitBufferError::Storage(error) => {
3477							ctx.defer_transient_commit_error(error.to_string());
3478						}
3479						CommitBufferError::FenceMismatch(_)
3480						| CommitBufferError::Indeterminate(_) => {}
3481					}
3482					handle_finalize_fence_error(ctx, &err);
3483					SQLITE_IOERR
3484				}
3485			},
3486			SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE => {
3487				let mut state = ctx.state.write();
3488				state.write_buffer.dirty.clear();
3489				state.write_buffer.in_atomic_write = false;
3490				state.db_size_pages = state.write_buffer.saved_db_size;
3491				state.refused_write_pending = false;
3492				SQLITE_OK
3493			}
3494			// VACUUM announces the size of the database it is about to write over this one,
3495			// after building the copy and before writing any of it.
3496			SQLITE_FCNTL_OVERWRITE => {
3497				if p_arg.is_null() {
3498					return SQLITE_NOTFOUND;
3499				}
3500				let bytes = *(p_arg as *const i64);
3501				let pages = (bytes.max(0) as usize).div_ceil(ctx.page_size());
3502				if pages > ctx.config.commit_page_cap() {
3503					ctx.refuse_oversize_transaction()
3504				} else {
3505					SQLITE_NOTFOUND
3506				}
3507			}
3508			FCNTL_COMMIT_PAGE_CAP => {
3509				if p_arg.is_null() {
3510					return SQLITE_MISUSE;
3511				}
3512				*(p_arg as *mut i64) = ctx.config.commit_page_cap() as i64;
3513				SQLITE_OK
3514			}
3515			_ => SQLITE_NOTFOUND,
3516		}
3517	})
3518}
3519
3520unsafe extern "C" fn io_sector_size(_p_file: *mut sqlite3_file) -> c_int {
3521	vfs_catch_unwind!(DEFAULT_PAGE_SIZE as c_int, DEFAULT_PAGE_SIZE as c_int)
3522}
3523
3524unsafe extern "C" fn io_device_characteristics(p_file: *mut sqlite3_file) -> c_int {
3525	vfs_catch_unwind!(0, {
3526		let file = get_file(p_file);
3527		if get_aux_state(file).is_some() {
3528			0
3529		} else {
3530			
3531			SQLITE_IOCAP_BATCH_ATOMIC
3532		}
3533	})
3534}
3535
3536unsafe extern "C" fn vfs_open(
3537	p_vfs: *mut sqlite3_vfs,
3538	z_name: *const c_char,
3539	p_file: *mut sqlite3_file,
3540	flags: c_int,
3541	p_out_flags: *mut c_int,
3542) -> c_int {
3543	vfs_catch_unwind!(SQLITE_CANTOPEN, {
3544		let ctx = get_vfs_ctx(p_vfs);
3545		let delete_on_close = (flags & SQLITE_OPEN_DELETEONCLOSE) != 0;
3546		let path = if z_name.is_null() {
3547			if delete_on_close {
3548				next_temp_aux_path()
3549			} else {
3550				return SQLITE_CANTOPEN;
3551			}
3552		} else {
3553			match CStr::from_ptr(z_name).to_str() {
3554				Ok(path) => path.to_string(),
3555				Err(_) => return SQLITE_CANTOPEN,
3556			}
3557		};
3558		let is_main =
3559			path == ctx.actor_id && !delete_on_close && (flags & SQLITE_OPEN_MAIN_DB) != 0;
3560
3561		
3562
3563		let base = sqlite3_file {
3564			pMethods: ctx.io_methods.as_ref(),
3565		};
3566		
3567		let aux = if is_main {
3568			ptr::null_mut()
3569		} else {
3570			Box::into_raw(Box::new(AuxFileHandle {
3571				is_main_journal: (flags & SQLITE_OPEN_MAIN_JOURNAL) != 0
3572					&& path.strip_suffix("-journal") == Some(ctx.actor_id.as_str()),
3573				path: path.clone(),
3574				state: ctx.open_aux_file(&path),
3575				delete_on_close,
3576			}))
3577		};
3578		ptr::write(
3579			p_file.cast::<VfsFile>(),
3580			VfsFile {
3581				base,
3582				ctx: ctx as *const VfsContext,
3583				aux,
3584			},
3585		);
3586
3587		if !p_out_flags.is_null() {
3588			*p_out_flags = flags;
3589		}
3590
3591		SQLITE_OK
3592	})
3593}
3594
3595unsafe extern "C" fn vfs_delete(
3596	p_vfs: *mut sqlite3_vfs,
3597	z_name: *const c_char,
3598	_sync_dir: c_int,
3599) -> c_int {
3600	vfs_catch_unwind!(SQLITE_IOERR_DELETE, {
3601		if z_name.is_null() {
3602			return SQLITE_OK;
3603		}
3604
3605		let ctx = get_vfs_ctx(p_vfs);
3606		let path = match CStr::from_ptr(z_name).to_str() {
3607			Ok(path) => path,
3608			Err(_) => return SQLITE_OK,
3609		};
3610		if path == ctx.actor_id {
3611			// Main database deletion is unsupported because xDelete cannot remove persisted depot state.
3612			ctx.set_last_error("main database deletion is unsupported".to_string());
3613			return SQLITE_IOERR_DELETE;
3614		} else {
3615			
3616			ctx.delete_aux_file(path);
3617		}
3618		SQLITE_OK
3619	})
3620}
3621
3622unsafe extern "C" fn vfs_access(
3623	p_vfs: *mut sqlite3_vfs,
3624	z_name: *const c_char,
3625	_flags: c_int,
3626	p_res_out: *mut c_int,
3627) -> c_int {
3628	vfs_catch_unwind!(SQLITE_IOERR_ACCESS, {
3629		if z_name.is_null() {
3630			*p_res_out = 0;
3631			return SQLITE_OK;
3632		}
3633
3634		let ctx = get_vfs_ctx(p_vfs);
3635		let path = match CStr::from_ptr(z_name).to_str() {
3636			Ok(path) => path,
3637			Err(_) => {
3638				*p_res_out = 0;
3639				return SQLITE_OK;
3640			}
3641		};
3642
3643		*p_res_out = if path == ctx.actor_id || ctx.aux_file_exists(path) {
3644			1
3645		} else {
3646			0
3647		};
3648		SQLITE_OK
3649	})
3650}
3651
3652unsafe extern "C" fn vfs_full_pathname(
3653	_p_vfs: *mut sqlite3_vfs,
3654	z_name: *const c_char,
3655	n_out: c_int,
3656	z_out: *mut c_char,
3657) -> c_int {
3658	vfs_catch_unwind!(SQLITE_IOERR, {
3659		if z_name.is_null() || z_out.is_null() || n_out <= 0 {
3660			return SQLITE_IOERR;
3661		}
3662
3663		let name = CStr::from_ptr(z_name);
3664		let bytes = name.to_bytes_with_nul();
3665		if bytes.len() >= n_out as usize {
3666			return SQLITE_IOERR;
3667		}
3668
3669		ptr::copy_nonoverlapping(bytes.as_ptr().cast::<c_char>(), z_out, bytes.len());
3670		SQLITE_OK
3671	})
3672}
3673
3674unsafe extern "C" fn vfs_randomness(
3675	_p_vfs: *mut sqlite3_vfs,
3676	n_byte: c_int,
3677	z_out: *mut c_char,
3678) -> c_int {
3679	vfs_catch_unwind!(0, {
3680		let buf = slice::from_raw_parts_mut(z_out.cast::<u8>(), n_byte as usize);
3681		match getrandom::getrandom(buf) {
3682			Ok(()) => n_byte,
3683			Err(_) => 0,
3684		}
3685	})
3686}
3687
3688unsafe extern "C" fn vfs_sleep(_p_vfs: *mut sqlite3_vfs, microseconds: c_int) -> c_int {
3689	vfs_catch_unwind!(0, {
3690		std::thread::sleep(std::time::Duration::from_micros(microseconds as u64));
3691		microseconds
3692	})
3693}
3694
3695unsafe extern "C" fn vfs_current_time(_p_vfs: *mut sqlite3_vfs, p_time_out: *mut f64) -> c_int {
3696	vfs_catch_unwind!(SQLITE_IOERR, {
3697		let now = std::time::SystemTime::now()
3698			.duration_since(std::time::UNIX_EPOCH)
3699			.unwrap_or_default();
3700		*p_time_out = 2440587.5 + (now.as_secs_f64() / 86400.0);
3701		SQLITE_OK
3702	})
3703}
3704
3705unsafe extern "C" fn vfs_get_last_error(
3706	p_vfs: *mut sqlite3_vfs,
3707	n_byte: c_int,
3708	z_err_msg: *mut c_char,
3709) -> c_int {
3710	vfs_catch_unwind!(SQLITE_IOERR, {
3711		if n_byte <= 0 || z_err_msg.is_null() {
3712			return 0;
3713		}
3714
3715		let ctx = get_vfs_ctx(p_vfs);
3716		let Some(message) = ctx.clone_last_error() else {
3717			*z_err_msg = 0;
3718			return 0;
3719		};
3720
3721		let bytes = message.as_bytes();
3722		let max_len = (n_byte as usize).saturating_sub(1);
3723		let copy_len = bytes.len().min(max_len);
3724		let dst = z_err_msg.cast::<u8>();
3725		ptr::copy_nonoverlapping(bytes.as_ptr(), dst, copy_len);
3726		*dst.add(copy_len) = 0;
3727		0
3728	})
3729}
3730
3731impl SqliteVfs {
3732	pub(crate) fn take_last_error(&self) -> Option<String> {
3733		self.ctx.take_last_error()
3734	}
3735
3736	pub(crate) fn take_indeterminate_result(&self) -> Option<String> {
3737		self.ctx.take_indeterminate_result()
3738	}
3739
3740	pub(crate) fn take_transaction_too_large(&self) -> Option<SqliteTransactionTooLargeError> {
3741		self.ctx.take_transaction_too_large()
3742	}
3743
3744	pub(crate) fn take_storage_error(&self) -> Option<SqliteStorageError> {
3745		self.ctx.take_storage_error()
3746	}
3747
3748	fn clone_last_error(&self) -> Option<String> {
3749		self.ctx.clone_last_error()
3750	}
3751
3752	pub fn clone_fatal_error(&self) -> Option<String> {
3753		self.ctx.clone_fatal_error()
3754	}
3755
3756	/// Reads back an auxiliary file this VFS holds in memory. `VACUUM INTO` and other statements
3757	/// that name an output file resolve it through the connection's VFS, so the only way to get
3758	/// those bytes onto the host filesystem is to copy them out here.
3759	pub(crate) fn read_aux_file(&self, path: &str) -> Option<Vec<u8>> {
3760		self.ctx.read_aux_file(path)
3761	}
3762
3763	pub(crate) fn delete_aux_file(&self, path: &str) {
3764		self.ctx.delete_aux_file(path);
3765	}
3766
3767	pub(crate) fn sqlite_vfs_metrics(&self) -> SqliteVfsMetricsSnapshot {
3768		self.ctx.sqlite_vfs_metrics()
3769	}
3770
3771	
3772
3773	
3774
3775	pub(crate) fn register_with_transport_and_initial_pages(
3776		name: &str,
3777		transport: SqliteTransportHandle,
3778		actor_id: String,
3779		runtime: Handle,
3780		config: VfsConfig,
3781		initial_pages: InitialPages,
3782		metrics: Option<Arc<dyn SqliteVfsMetrics>>,
3783	) -> std::result::Result<Self, String> {
3784		let mut io_methods: sqlite3_io_methods = unsafe { std::mem::zeroed() };
3785		io_methods.iVersion = 1;
3786		io_methods.xClose = Some(io_close);
3787		io_methods.xRead = Some(io_read);
3788		io_methods.xWrite = Some(io_write);
3789		io_methods.xTruncate = Some(io_truncate);
3790		io_methods.xSync = Some(io_sync);
3791		io_methods.xFileSize = Some(io_file_size);
3792		io_methods.xLock = Some(io_lock);
3793		io_methods.xUnlock = Some(io_unlock);
3794		io_methods.xCheckReservedLock = Some(io_check_reserved_lock);
3795		io_methods.xFileControl = Some(io_file_control);
3796		io_methods.xSectorSize = Some(io_sector_size);
3797		io_methods.xDeviceCharacteristics = Some(io_device_characteristics);
3798
3799		let generation = name
3800			.rsplit_once("-g")
3801			.and_then(|(_, generation)| generation.parse::<u64>().ok());
3802		let mut ctx = Box::new(VfsContext::new(
3803			actor_id,
3804			generation,
3805			runtime,
3806			transport,
3807			config,
3808			io_methods,
3809			initial_pages,
3810			metrics,
3811		)?);
3812		tracing::trace!(target: "sqlite_prefetch::trace", event = "open",
3813            stream_id = %format_args!("sqlite-{:p}", &*ctx), actor_id = %ctx.actor_id, generation = ctx.generation);
3814		let ctx_ptr = (&mut *ctx) as *mut VfsContext;
3815		let name_cstring = CString::new(name).map_err(|err| err.to_string())?;
3816
3817		let mut vfs: sqlite3_vfs = unsafe { std::mem::zeroed() };
3818		vfs.iVersion = 1;
3819		vfs.szOsFile = std::mem::size_of::<VfsFile>() as c_int;
3820		vfs.mxPathname = MAX_PATHNAME;
3821		vfs.zName = name_cstring.as_ptr();
3822		vfs.pAppData = ctx_ptr.cast::<c_void>();
3823		vfs.xOpen = Some(vfs_open);
3824		vfs.xDelete = Some(vfs_delete);
3825		vfs.xAccess = Some(vfs_access);
3826		vfs.xFullPathname = Some(vfs_full_pathname);
3827		vfs.xRandomness = Some(vfs_randomness);
3828		vfs.xSleep = Some(vfs_sleep);
3829		vfs.xCurrentTime = Some(vfs_current_time);
3830		vfs.xGetLastError = Some(vfs_get_last_error);
3831
3832		let registration = SqliteVfsRegistration::register(vfs)?;
3833
3834		Ok(Self {
3835			_registration: registration,
3836			_name: name_cstring,
3837			ctx,
3838		})
3839	}
3840
3841	pub fn name_ptr(&self) -> *const c_char {
3842		self._name.as_ptr()
3843	}
3844
3845	/// Bytes held by the VFS prefetch buffer and committed-page buffer, which hold only durable
3846	/// copies.
3847	pub fn cached_page_bytes(&self) -> u64 {
3848		let state = self.ctx.state.read();
3849		state
3850			.prefetch_buffer
3851			.entry_count()
3852			.saturating_add(state.committed_buffer.entry_count())
3853			.saturating_mul(state.page_size as u64)
3854	}
3855
3856	/// How long the worker waits without a command before releasing this database's caches.
3857	pub fn idle_release(&self) -> Option<std::time::Duration> {
3858		match self.ctx.config.idle_release_ms {
3859			0 => None,
3860			ms => Some(std::time::Duration::from_millis(ms)),
3861		}
3862	}
3863
3864	
3865
3866	fn ctx(&self) -> &VfsContext {
3867		&self.ctx
3868	}
3869
3870	fn commit_atomic_count(&self) -> u64 {
3871		self.ctx.commit_atomic_count.load(Ordering::Relaxed)
3872	}
3873}
3874
3875impl SqliteVfsRegistration {
3876	fn register(vfs: sqlite3_vfs) -> std::result::Result<Self, String> {
3877		let vfs_ptr = Box::into_raw(Box::new(vfs));
3878		let rc = unsafe { sqlite3_vfs_register(vfs_ptr, 0) };
3879		if rc != SQLITE_OK {
3880			unsafe {
3881				drop(Box::from_raw(vfs_ptr));
3882			}
3883			return Err(format!("sqlite3_vfs_register failed with code {rc}"));
3884		}
3885
3886		Ok(Self { vfs_ptr })
3887	}
3888}
3889
3890impl Drop for SqliteVfsRegistration {
3891	fn drop(&mut self) {
3892		unsafe {
3893			sqlite3_vfs_unregister(self.vfs_ptr);
3894			drop(Box::from_raw(self.vfs_ptr));
3895		}
3896	}
3897}
3898
3899impl NativeDatabase {
3900	pub fn as_ptr(&self) -> *mut sqlite3 {
3901		self.db
3902	}
3903
3904	pub fn take_last_kv_error(&self) -> Option<String> {
3905		self._vfs.take_last_error()
3906	}
3907
3908	pub fn sqlite_vfs_metrics(&self) -> SqliteVfsMetricsSnapshot {
3909		self._vfs.ctx.sqlite_vfs_metrics()
3910	}
3911
3912	pub fn round_trip_counts(&self) -> SqliteRoundTripCounts {
3913		self._vfs.ctx.round_trip_counts()
3914	}
3915
3916	pub(crate) fn begin_operation_profile(&self) -> SqliteOperationProfileGuard<'_> {
3917		self._vfs.ctx.begin_operation_profile();
3918		SqliteOperationProfileGuard {
3919			ctx: &self._vfs.ctx,
3920			active: true,
3921		}
3922	}
3923
3924	/// Bytes held by the VFS page buffers, which hold only durable copies.
3925	pub fn cached_page_bytes(&self) -> u64 {
3926		self._vfs.cached_page_bytes()
3927	}
3928
3929	/// Frees the pager's unpinned pages and the VFS page buffers for an idle database. Returns
3930	/// false without releasing while a write is buffered; the caller must also skip the release
3931	/// while an explicit transaction is open. Every released page is a durable copy that the
3932	/// next read fetches again.
3933	/// Holds every later commit in memory until `uncork_commits`, which sends them to depot as
3934	/// one commit. The transactions are committed for this connection meanwhile, but none is
3935	/// durable, so nothing may report them done until uncorking succeeds.
3936	pub fn cork_commits(&self) {
3937		self._vfs.ctx().cork();
3938	}
3939
3940	/// Sends the transactions committed since `cork_commits` to depot as one commit. Must run
3941	/// outside a transaction.
3942	/// Sends every commit made since `cork_commits` to depot as one commit.
3943	///
3944	/// Fails while the connection has an open transaction, because its pages
3945	/// may still sit in SQLite's page cache where the VFS cannot see them.
3946	pub fn uncork_commits(&self) -> Result<()> {
3947		// SAFETY: `as_ptr` is a live connection owned by this database.
3948		let autocommit = unsafe { sqlite3_get_autocommit(self.as_ptr()) };
3949		if autocommit == 0 {
3950			anyhow::bail!("cannot uncork sqlite commits inside a transaction");
3951		}
3952		self._vfs
3953			.ctx()
3954			.uncork()
3955			.map_err(|error| anyhow::anyhow!("uncork sqlite commits: {error:?}"))
3956	}
3957
3958	pub fn release_idle_memory(&self) -> bool {
3959		{
3960			let state = self._vfs.ctx.state.read();
3961			if state.write_buffer.in_atomic_write
3962				|| !state.write_buffer.dirty.is_empty()
3963				|| state.corked.is_some()
3964			{
3965				return false;
3966			}
3967		}
3968		unsafe {
3969			sqlite3_db_release_memory(self.db);
3970		}
3971		self._vfs.ctx.state.write().invalidate_page_buffers();
3972		self._vfs.ctx.report_prefetch_unused();
3973		true
3974	}
3975}
3976
3977impl Drop for NativeDatabase {
3978	fn drop(&mut self) {
3979		if !self.db.is_null() {
3980			let ctx = self._vfs.ctx();
3981			ctx.discard_refused_transaction();
3982			// A transaction still open at close never committed, so the flush below must carry
3983			// none of its spilled pages. SQLite rolls it back as the connection closes.
3984			if write_transaction_open(self.db) {
3985				ctx.discard_open_transaction();
3986			}
3987			// Corked pages join the write buffer, so the flush below carries them.
3988			{
3989				let mut state = ctx.state.write();
3990				if let Some(mut corked) = state.corked.take() {
3991					corked.append(&mut state.write_buffer.dirty);
3992					state.write_buffer.dirty = corked;
3993				}
3994			}
3995			let should_flush = {
3996				let state = ctx.state.read();
3997				state.write_buffer.in_atomic_write
3998					|| !state.write_buffer.dirty.is_empty()
3999					|| state.db_size_pages != state.committed_db_size_pages
4000			};
4001			if should_flush {
4002				let result = if ctx.state.read().write_buffer.in_atomic_write {
4003					ctx.commit_atomic_write_with_timeout(Some(NATIVE_DATABASE_DROP_FLUSH_TIMEOUT))
4004				} else {
4005					ctx.flush_dirty_pages_with_timeout(Some(NATIVE_DATABASE_DROP_FLUSH_TIMEOUT))
4006						.map(|wait| match wait {
4007							CommitWait::Completed(_) => CommitWait::Completed(()),
4008							CommitWait::TimedOut => CommitWait::TimedOut,
4009						})
4010				};
4011				// A flush that fails or runs out of time leaves the connection with commits it
4012				// cannot send. The connection still closes: keeping it open would leak SQLite's
4013				// connection and its share of the process-wide page cache, and the close below
4014				// must not try to send the same pages again.
4015				match result {
4016					Ok(CommitWait::Completed(())) => {}
4017					Ok(CommitWait::TimedOut) => {
4018						let discarded_pages = ctx.discard_unsent_commits();
4019						tracing::error!(
4020							actor_id = %ctx.actor_id,
4021							timeout_ms = NATIVE_DATABASE_DROP_FLUSH_TIMEOUT.as_millis(),
4022							discarded_pages,
4023							"timed out flushing sqlite database before close, discarding its unsent commits"
4024						);
4025					}
4026					Err(err) => {
4027						handle_non_finalize_commit_error(ctx, &err);
4028						let discarded_pages = ctx.discard_unsent_commits();
4029						tracing::error!(
4030							actor_id = %ctx.actor_id,
4031							?err,
4032							discarded_pages,
4033							"failed to flush sqlite database before close, discarding its unsent commits"
4034						);
4035					}
4036				}
4037			}
4038
4039			// Every committed transaction is now in Depot or discarded. SQLite's rollback of an
4040			// open transaction during close only restores that state, so nothing it writes is
4041			// sent. Every statement the connection's callers prepare is finalized before their
4042			// call returns, so the close is never deferred by a statement nobody owns.
4043			ctx.state.write().closing = true;
4044			let rc = unsafe { sqlite3_close_v2(self.db) };
4045			if rc != SQLITE_OK {
4046				tracing::warn!(
4047					rc,
4048					error = sqlite_error_message(self.db),
4049					"failed to close sqlite database"
4050				);
4051			}
4052			self.db = ptr::null_mut();
4053		}
4054	}
4055}
4056
4057/// Whether the connection holds a write transaction SQLite has not committed: an explicit
4058/// transaction after `BEGIN`, or a statement whose implicit transaction has not finished.
4059fn write_transaction_open(db: *mut sqlite3) -> bool {
4060	// SAFETY: `db` is a live connection owned by the caller.
4061	let autocommit = unsafe { sqlite3_get_autocommit(db) };
4062	let txn_state = unsafe { sqlite3_txn_state(db, ptr::null()) };
4063	autocommit == 0 || txn_state == SQLITE_TXN_WRITE
4064}
4065
4066pub fn open_database(vfs: SqliteVfs, file_name: &str) -> Result<NativeDatabase> {
4067	open_connection(
4068		Arc::new(vfs),
4069		file_name,
4070		SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
4071	)
4072	.map_err(anyhow::Error::msg)
4073	.and_then(|connection| {
4074		configure_connection_for_database(connection.as_ptr(), &connection._vfs, file_name)?;
4075		Ok(connection)
4076	})
4077}
4078
4079pub fn open_connection(
4080	vfs: NativeVfsHandle,
4081	file_name: &str,
4082	flags: c_int,
4083) -> std::result::Result<NativeConnection, String> {
4084	let c_name = CString::new(file_name).map_err(|err| err.to_string())?;
4085	let mut db: *mut sqlite3 = ptr::null_mut();
4086
4087	let rc = unsafe { sqlite3_open_v2(c_name.as_ptr(), &mut db, flags, vfs.name_ptr()) };
4088	if rc != SQLITE_OK {
4089		let message = sqlite_error_message(db);
4090		tracing::error!(
4091			file_name,
4092			rc,
4093			%message,
4094			last_error = ?vfs.clone_last_error(),
4095			"failed to open sqlite database with custom VFS"
4096		);
4097		if !db.is_null() {
4098			unsafe {
4099				sqlite3_close(db);
4100			}
4101		}
4102		return Err(format!("sqlite3_open_v2 failed with code {rc}: {message}"));
4103	}
4104
4105	Ok(NativeDatabase { db, _vfs: vfs })
4106}
4107
4108#[derive(rivet_error::RivetError)]
4109#[error(
4110	"sqlite",
4111	"invalid_compile_options",
4112	"SQLite requires SQLITE_ENABLE_BATCH_ATOMIC_WRITE and SQLITE_DIRECT_OVERFLOW_READ=0. Rebuild with LIBSQLITE3_FLAGS=\"SQLITE_ENABLE_BATCH_ATOMIC_WRITE SQLITE_DIRECT_OVERFLOW_READ=0\"."
4113)]
4114struct SqliteInvalidCompileOptions;
4115
4116pub fn configure_connection_for_database(
4117	db: *mut sqlite3,
4118	vfs: &SqliteVfs,
4119	file_name: &str,
4120) -> Result<()> {
4121	// SAFETY: SQLite only reads its static compile-option table using these C strings.
4122	let batch_atomic = unsafe { sqlite3_compileoption_used(c"ENABLE_BATCH_ATOMIC_WRITE".as_ptr()) };
4123	let direct_overflow = unsafe { sqlite3_compileoption_used(c"DIRECT_OVERFLOW_READ".as_ptr()) };
4124	if batch_atomic == 0 || direct_overflow != 0 {
4125		return Err(SqliteInvalidCompileOptions.build());
4126	}
4127
4128	// SQLite interprets a negative cache_size as a KiB budget instead of a page count. With the
4129	// process-wide pager cache installed, cache_size reaches the cache only through `xCachesize`,
4130	// which ignores it: the process budget bounds the pages, and SQLite spills a transaction's
4131	// dirty pages only when the cache refuses a `createFlag` 1 fetch. cache_size then matters only
4132	// for SQLite's temporary-file flush heuristic, so it keeps its per-connection value.
4133	let cache_size_kib = sqlite_optimization_flags().pager_cache_size_kib;
4134	let cache_size_pragma = format!("PRAGMA cache_size = -{cache_size_kib};");
4135
4136	let pragmas = [
4137		"PRAGMA page_size = 4096;",
4138		"PRAGMA journal_mode = DELETE;",
4139		"PRAGMA synchronous = NORMAL;",
4140		"PRAGMA temp_store = MEMORY;",
4141		"PRAGMA auto_vacuum = NONE;",
4142		"PRAGMA locking_mode = EXCLUSIVE;",
4143		// An exclusive connection keeps its journal between transactions. Truncating it after
4144		// each one releases the memory a large transaction's journal held.
4145		"PRAGMA journal_size_limit = 0;",
4146		cache_size_pragma.as_str(),
4147	];
4148	for pragma in &pragmas {
4149		if let Err(err) = sqlite_exec(db, pragma) {
4150			tracing::error!(
4151				file_name,
4152				pragma,
4153				%err,
4154				last_error = ?vfs.clone_last_error(),
4155				"failed to configure sqlite database"
4156			);
4157			return Err(anyhow::Error::msg(err));
4158		}
4159	}
4160
4161	let rc = unsafe { sqlite3_set_authorizer(db, Some(authorize_journal_mode), ptr::null_mut()) };
4162	if rc != SQLITE_OK {
4163		anyhow::bail!("failed to install the sqlite authorizer: {rc}");
4164	}
4165
4166	Ok(())
4167}
4168
4169/// Keeps every connection on the `DELETE` rollback journal the VFS depends on. Commits reach
4170/// Depot only through the main file's sync, so a WAL journal would not be durable, a `MEMORY`
4171/// journal could roll back more pages than one commit carries, and `OFF` would let a failed
4172/// transaction's pages be committed. It also pins `journal_size_limit`, and keeps `synchronous`
4173/// at a level that syncs the main file on every commit, so a commit's pages never wait in the
4174/// write buffer where a close could not tell them from a later open transaction's. Reading any
4175/// of them stays allowed. A guarded connection replaces this authorizer with one that refuses
4176/// every journal mode change.
4177extern "C" fn authorize_journal_mode(
4178	_context: *mut c_void,
4179	action: c_int,
4180	first: *const c_char,
4181	second: *const c_char,
4182	_database: *const c_char,
4183	_trigger: *const c_char,
4184) -> c_int {
4185	if action != SQLITE_PRAGMA || first.is_null() || second.is_null() {
4186		return SQLITE_OK;
4187	}
4188	let name = unsafe { CStr::from_ptr(first) }.to_string_lossy();
4189	// The journal's record count resets when the journal is truncated after each transaction,
4190	// which this limit configures.
4191	if name.eq_ignore_ascii_case("journal_size_limit") {
4192		return SQLITE_DENY;
4193	}
4194	let value = unsafe { CStr::from_ptr(second) }.to_string_lossy();
4195	let value = value.trim_matches(|c| matches!(c, '"' | '\''));
4196	if name.eq_ignore_ascii_case("synchronous") {
4197		let syncs_every_commit = [
4198			"normal", "full", "extra", "1", "2", "3", "on", "yes", "true",
4199		]
4200		.iter()
4201		.any(|level| value.eq_ignore_ascii_case(level));
4202		return if syncs_every_commit {
4203			SQLITE_OK
4204		} else {
4205			SQLITE_DENY
4206		};
4207	}
4208	if !name.eq_ignore_ascii_case("journal_mode") {
4209		return SQLITE_OK;
4210	}
4211	if value.eq_ignore_ascii_case("delete") {
4212		SQLITE_OK
4213	} else {
4214		SQLITE_DENY
4215	}
4216}