1use 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 async fn commit_stage_begin(
124 &self,
125 request: protocol::SqliteCommitStageBeginRequest,
126 ) -> Result<protocol::SqliteCommitStageBeginResponse>;
127
128 async fn commit_stage_segment(
130 &self,
131 request: protocol::SqliteCommitStageSegmentRequest,
132 ) -> Result<protocol::SqliteCommitStageSegmentResponse>;
133
134 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 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 pub idle_release_ms: u64,
161 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 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 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(SqliteStorageError),
268 Other(String),
269}
270
271#[derive(Debug, Clone, PartialEq, Eq)]
275pub enum SqliteStorageError {
276 Overloaded {
279 retry_after_ms: u64,
280 message: String,
281 },
282 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#[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
325const DEPOT_OVERLOADED_GROUP: &str = "depot";
327const DEPOT_OVERLOADED_CODE: &str = "overloaded";
328
329const DEFAULT_OVERLOADED_RETRY_AFTER_MS: u64 = 100;
331
332fn storage_refusal(error: &protocol::SqliteErrorResponse) -> Option<SqliteStorageError> {
334 if error.group.as_str() != DEPOT_OVERLOADED_GROUP
335 || error.code.as_str() != DEPOT_OVERLOADED_CODE
336 {
337 return None;
338 }
339 let message = error.message.to_string();
340 Some(SqliteStorageError::Overloaded {
341 retry_after_ms: retry_after_hint(&message).unwrap_or(DEFAULT_OVERLOADED_RETRY_AFTER_MS),
342 message,
343 })
344}
345
346fn retry_after_hint(message: &str) -> Option<u64> {
348 let (_, rest) = message.split_once("retry_after_ms=")?;
349 let digits: String = rest.chars().take_while(char::is_ascii_digit).collect();
350 digits.parse().ok()
351}
352
353fn storage_timeout(error: &anyhow::Error) -> Option<SqliteStorageError> {
355 error
356 .downcast_ref::<SqliteStorageTimeoutError>()
357 .map(|timeout| SqliteStorageError::Timeout {
358 operation: timeout.operation.clone(),
359 })
360}
361
362#[derive(Debug)]
364pub struct SqliteCommitIndeterminateError(pub String);
365
366impl std::fmt::Display for SqliteCommitIndeterminateError {
367 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368 write!(f, "sqlite {} result is indeterminate", self.0)
369 }
370}
371
372impl std::error::Error for SqliteCommitIndeterminateError {}
373
374#[derive(Debug, Clone, PartialEq, Eq)]
379pub struct SqliteTransactionTooLargeError {
380 pub max_pages: usize,
381 pub page_size: usize,
382}
383
384impl std::fmt::Display for SqliteTransactionTooLargeError {
385 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
386 let max_kib = self.max_pages * self.page_size / 1024;
387 let max_size = if max_kib >= 1024 {
388 format!("{} MiB", max_kib / 1024)
389 } else {
390 format!("{max_kib} KiB")
391 };
392 write!(
393 f,
394 "sqlite transaction changes more than {} pages ({}), the most one commit can hold, \
395 so the statement failed and its changes were rolled back; split the work into smaller \
396 transactions. VACUUM rewrites the whole \
397 database in one transaction, so it only runs while the database's in-use pages fit under \
398 this limit",
399 self.max_pages, max_size,
400 )
401 }
402}
403
404impl std::error::Error for SqliteTransactionTooLargeError {}
405
406pub(crate) const FCNTL_COMMIT_PAGE_CAP: c_int = 0x5256_0001;
409
410#[derive(Debug, Clone, Copy, Default)]
411pub struct SqliteVfsMetricsSnapshot {
412 pub request_build_ns: u64,
413 pub serialize_ns: u64,
414 pub transport_ns: u64,
415 pub state_update_ns: u64,
416 pub total_ns: u64,
417 pub commit_count: u64,
418 pub page_cache_entries: u64,
419 pub page_cache_weighted_size: u64,
420 pub page_cache_capacity_pages: u64,
421 pub write_buffer_dirty_pages: u64,
422 pub db_size_pages: u64,
423}
424
425pub const MAX_PROFILED_GET_PAGES_REQUESTS: usize = 16;
426pub const MAX_PROFILED_TRANSACTION_STATEMENTS: usize = 32;
427
428#[derive(Debug, Clone, Default)]
431pub struct SqliteGetPagesProfile {
432 pub ordinal: u64,
433 pub duration_ns: u64,
434 pub demand_requested: u64,
435 pub response_present: u64,
436 pub response_missing: u64,
437 pub overflow_expansion_extra: u64,
438 pub response_bytes: u64,
439 pub success: bool,
440}
441
442#[derive(Debug, Clone, Default)]
446pub struct SqliteOperationProfile {
447 pub worker_wait_ns: u64,
448 pub sqlite_execution_ns: u64,
449 pub storage_ns: u64,
450 pub bind_count: u64,
451 pub bind_logical_bytes: u64,
452 pub result_rows: u64,
453 pub result_columns: u64,
454 pub result_logical_bytes: u64,
455 pub sqlite_requested_pages: u64,
456 pub cache_hit_pages: u64,
457 pub cache_miss_pages: u64,
458 pub depot_demand_requested_pages: u64,
459 pub response_present_pages: u64,
460 pub response_missing_pages: u64,
461 pub overflow_expansion_extra_pages: u64,
462 pub btree_pages: u64,
463 pub non_btree_pages: u64,
464 pub prefetch_consumed_pages: u64,
465 pub prefetch_unused_pages: u64,
466 pub dirty_pages: u64,
467 pub storage_response_bytes: u64,
468 pub dirty_bytes: u64,
469 pub get_pages_round_trips: u64,
470 pub commit_round_trips: u64,
471 pub get_pages_requests: [Option<SqliteGetPagesProfile>; MAX_PROFILED_GET_PAGES_REQUESTS],
472 pub omitted_get_pages_requests: u64,
473}
474
475#[derive(Debug, Clone)]
476pub struct SqliteOperationMetric {
477 pub operation_type: &'static str,
478 pub fingerprint: String,
479 pub fingerprint_source: &'static str,
480 pub transaction_mode: &'static str,
481 pub storage_transport: &'static str,
482 pub outcome: &'static str,
483 pub sql_bytes: u64,
484 pub total_ns: u64,
485 pub transaction_wait_ns: u64,
486 pub profile: SqliteOperationProfile,
487}
488
489#[derive(Debug, Clone)]
490pub struct SqliteTransactionMetric {
491 pub fingerprint: String,
492 pub fingerprint_source: &'static str,
493 pub shape_fingerprint: String,
494 pub statement_fingerprint_hashes: [Option<[u8; 16]>; MAX_PROFILED_TRANSACTION_STATEMENTS],
495 pub omitted_statement_fingerprints: u64,
496 pub storage_transport: &'static str,
497 pub outcome: &'static str,
498 pub total_ns: u64,
499 pub transaction_wait_ns: u64,
500 pub worker_wait_ns: u64,
501 pub storage_ns: u64,
502 pub local_work_ns: u64,
503 pub application_time_ns: u64,
504 pub commit_ns: u64,
505 pub get_pages_round_trips: u64,
506 pub statement_count: u64,
507 pub dirty_pages: u64,
508 pub dirty_bytes: u64,
509}
510
511impl SqliteOperationProfile {
512 fn push_get_pages(&mut self, request: SqliteGetPagesProfile, request_limit: usize) {
513 self.storage_ns = self.storage_ns.saturating_add(request.duration_ns);
514 self.get_pages_round_trips = self.get_pages_round_trips.saturating_add(1);
515 self.depot_demand_requested_pages = self
516 .depot_demand_requested_pages
517 .saturating_add(request.demand_requested);
518 self.response_present_pages = self
519 .response_present_pages
520 .saturating_add(request.response_present);
521 self.response_missing_pages = self
522 .response_missing_pages
523 .saturating_add(request.response_missing);
524 self.overflow_expansion_extra_pages = self
525 .overflow_expansion_extra_pages
526 .saturating_add(request.overflow_expansion_extra);
527 self.storage_response_bytes = self
528 .storage_response_bytes
529 .saturating_add(request.response_bytes);
530 let index = self.get_pages_round_trips.saturating_sub(1) as usize;
531 if index < request_limit
532 && let Some(slot) = self.get_pages_requests.get_mut(index)
533 {
534 *slot = Some(request);
535 } else {
536 self.omitted_get_pages_requests = self.omitted_get_pages_requests.saturating_add(1);
537 }
538 }
539}
540
541pub(crate) struct SqliteOperationProfileGuard<'a> {
542 ctx: &'a VfsContext,
543 active: bool,
544}
545
546impl SqliteOperationProfileGuard<'_> {
547 pub(crate) fn finish(mut self) -> SqliteOperationProfile {
548 self.active = false;
549 self.ctx.finish_operation_profile()
550 }
551}
552
553impl Drop for SqliteOperationProfileGuard<'_> {
554 fn drop(&mut self) {
555 if self.active {
556 let _ = self.ctx.finish_operation_profile();
557 }
558 }
559}
560
561#[derive(Debug, Clone, Copy, Default)]
567pub struct SqliteRoundTripCounts {
568 pub get_pages: u64,
569 pub commit: u64,
570}
571
572impl SqliteRoundTripCounts {
573 pub fn total(&self) -> u64 {
574 self.get_pages.saturating_add(self.commit)
575 }
576
577 pub fn since(&self, earlier: SqliteRoundTripCounts) -> SqliteRoundTripCounts {
578 SqliteRoundTripCounts {
579 get_pages: self.get_pages.saturating_sub(earlier.get_pages),
580 commit: self.commit.saturating_sub(earlier.commit),
581 }
582 }
583}
584
585#[derive(Debug, Clone, Copy)]
586pub enum SqliteOpenPhase {
587 InitialPreload,
588 VfsRegister,
589 WorkerReady,
590 Total,
591}
592
593impl SqliteOpenPhase {
594 pub fn as_label(self) -> &'static str {
595 match self {
596 SqliteOpenPhase::InitialPreload => "initial_preload",
597 SqliteOpenPhase::VfsRegister => "vfs_register",
598 SqliteOpenPhase::WorkerReady => "worker_ready",
599 SqliteOpenPhase::Total => "total",
600 }
601 }
602}
603
604pub trait SqliteVfsMetrics: Send + Sync {
605 fn profiling_enabled(&self) -> bool;
606
607 fn max_profiled_get_pages_requests(&self) -> usize;
608
609 fn observe_operation_profile(&self, profile: &SqliteOperationMetric) -> bool;
611
612 fn observe_transaction_profile(&self, profile: &SqliteTransactionMetric) -> bool;
614
615 fn operation_diagnostic_selected(&self, _profile: &SqliteOperationMetric) -> bool {
617 true
618 }
619
620 fn transaction_diagnostic_selected(&self, _profile: &SqliteTransactionMetric) -> bool {
621 true
622 }
623
624 fn emit_operation_diagnostic_event(
626 &self,
627 actor_id: &str,
628 generation: Option<u64>,
629 profile: &SqliteOperationMetric,
630 );
631
632 fn emit_transaction_diagnostic_event(
633 &self,
634 actor_id: &str,
635 generation: Option<u64>,
636 profile: &SqliteTransactionMetric,
637 );
638
639 fn record_fingerprint_catalog(
640 &self,
641 operation_type: &'static str,
642 fingerprint: &str,
643 identity: &str,
644 format_version: u8,
645 );
646
647 fn record_resolve_pages(&self, requested_pages: u64);
648
649 fn record_resolve_cache_hits(&self, pages: u64);
650
651 fn record_resolve_cache_misses(&self, pages: u64);
652
653 fn record_get_pages_request(&self, pages: u64, prefetch_pages: u64, page_size: u64);
654
655 fn observe_get_pages_duration(&self, duration_ns: u64);
656
657 fn observe_open_phase(&self, phase: SqliteOpenPhase, outcome: &'static str, duration_ns: u64);
658
659 fn record_startup_preload_pages(&self, kind: &'static str, pages: u64);
660
661 fn record_commit(&self, dirty_pages: u64);
663
664 fn observe_commit_phases(
665 &self,
666 request_build_ns: u64,
667 serialize_ns: u64,
668 transport_ns: u64,
669 state_update_ns: u64,
670 total_ns: u64,
671 );
672
673 fn set_worker_queue_depth(&self, depth: u64);
674
675 fn set_worker_active(&self, active: bool);
676
677 fn set_worker_inflight(&self, active: bool);
678
679 fn set_vfs_cached_bytes(&self, bytes: u64);
682 fn record_prefetch_unused_pages(&self, _pages: u64) {}
684
685 fn set_coordinator_queue_depth(&self, depth: u64);
686
687 fn record_worker_queue_overload(&self);
688
689 fn observe_worker_command_duration(
690 &self,
691 operation: &'static str,
692 in_tx: bool,
693 stmt_kind: &'static str,
694 duration_ns: u64,
695 );
696
697 fn observe_transaction_round_trips(&self, get_pages_round_trips: u64, commit_round_trips: u64);
698
699 fn record_worker_command_error(&self, operation: &'static str, code: &'static str);
700
701 fn observe_worker_close_duration(&self, duration_ns: u64);
702
703 fn record_worker_close_timeout(&self);
704
705 fn record_worker_crash(&self);
706
707 fn record_worker_unclean_close(&self);
708}
709
710#[derive(Debug, Clone, Copy, Default)]
711struct CommitTransportMetrics {
712 serialize_ns: u64,
713 transport_ns: u64,
714}
715
716enum CommitWait<T> {
717 Completed(T),
718 TimedOut,
719}
720
721pub struct VfsContext {
722 actor_id: String,
723 generation: Option<u64>,
724 runtime: Handle,
725 transport: SqliteTransportHandle,
726 config: VfsConfig,
727 state: RwLock<VfsState>,
728 aux_files: RwLock<BTreeMap<String, Arc<AuxFileState>>>,
729 last_error: Mutex<Option<String>>,
730 indeterminate_result: Mutex<Option<String>>,
731 transient_commit_error: Mutex<Option<String>>,
732 transaction_too_large: Mutex<Option<SqliteTransactionTooLargeError>>,
734 storage_error: Mutex<Option<SqliteStorageError>>,
737 fatal_error: RwLock<Option<String>>,
738
739
740
741
742 commit_atomic_count: AtomicU64,
743 trace_commit_attempts: AtomicU64,
745 io_methods: Box<sqlite3_io_methods>,
746 pub resolve_pages_total: AtomicU64,
748 pub resolve_pages_cache_hits: AtomicU64,
749 pub resolve_pages_fetches: AtomicU64,
750 pub pages_fetched_total: AtomicU64,
751 pub prefetch_pages_total: AtomicU64,
752 pub prefetch_consumed_total: AtomicU64,
754 pub commit_total: AtomicU64,
755 pub commit_request_build_ns: AtomicU64,
756 pub commit_serialize_ns: AtomicU64,
757 pub commit_transport_ns: AtomicU64,
758 pub commit_state_update_ns: AtomicU64,
759 pub commit_duration_ns_total: AtomicU64,
760 operation_profile_active: AtomicBool,
764 operation_profile: Mutex<Option<SqliteOperationProfile>>,
765 operation_prefetch_unused_start: AtomicU64,
766 reported_prefetch_unused: AtomicU64,
767 metrics: Option<Arc<dyn SqliteVfsMetrics>>,
768}
769
770#[derive(Debug, Clone)]
771struct VfsState {
772 db_size_pages: u32,
773 committed_db_size_pages: u32,
777 head_txid: Option<u64>,
778 page_size: usize,
779 prefetch_buffer: PageBuffer,
781 committed_buffer: PageBuffer,
784 prefetch_unused_total: Arc<AtomicU64>,
785 write_buffer: WriteBuffer,
786 corked: Option<BTreeMap<u32, PageBytes>>,
790 dead: bool,
791 corked_db_size_pages: u32,
794 refused_write_pending: bool,
799 closing: bool,
805}
806
807#[derive(Debug, Clone, Copy, PartialEq, Eq)]
808enum CorkOutcome {
809 NotCorked,
810 Corked,
811 Overflow,
812}
813
814#[derive(Debug, Clone, Default)]
815struct WriteBuffer {
816 in_atomic_write: bool,
817 saved_db_size: u32,
818 dirty: BTreeMap<u32, PageBytes>,
819}
820
821impl WriteBuffer {
822 fn snapshot(&self) -> Vec<BufferedDirtyPage> {
823 self.dirty
824 .iter()
825 .map(|(pgno, bytes)| BufferedDirtyPage {
826 pgno: *pgno,
827 bytes: bytes.clone(),
828 })
829 .collect()
830 }
831}
832
833#[derive(Debug)]
834enum GetPagesError {
835 FenceMismatch(String),
836 Storage(SqliteStorageError),
837 Other(String),
838}
839
840#[repr(C)]
841struct VfsFile {
842 base: sqlite3_file,
843 ctx: *const VfsContext,
844 aux: *mut AuxFileHandle,
845}
846
847#[derive(Default)]
848struct AuxFileState {
849 bytes: Mutex<Vec<u8>>,
850 journal_records: AtomicUsize,
853}
854
855struct AuxFileHandle {
856 path: String,
857 state: Arc<AuxFileState>,
858 delete_on_close: bool,
859 is_main_journal: bool,
861}
862
863#[derive(Debug, Clone, Copy, PartialEq, Eq)]
864enum PageCacheInsertKind {
865 Target,
866 PartialTarget,
867 Prefetch,
868 Startup,
869}
870
871unsafe impl Send for VfsContext {}
872unsafe impl Sync for VfsContext {}
873
874pub struct SqliteVfs {
875 _registration: SqliteVfsRegistration,
876 _name: CString,
877 ctx: Box<VfsContext>,
878}
879
880unsafe impl Send for SqliteVfs {}
881unsafe impl Sync for SqliteVfs {}
882
883struct SqliteVfsRegistration {
884 vfs_ptr: *mut sqlite3_vfs,
885}
886
887pub struct NativeDatabase {
888 db: *mut sqlite3,
889 _vfs: NativeVfsHandle,
890}
891
892unsafe impl Send for NativeDatabase {}
893
894pub type NativeVfsHandle = Arc<SqliteVfs>;
895pub type NativeConnection = NativeDatabase;
896
897impl VfsState {
898 fn committed_size_pages_before_transaction(&self) -> u32 {
900 if self.corked.is_some() {
901 self.corked_db_size_pages
902 } else {
903 self.committed_db_size_pages
904 }
905 }
906
907 fn discard_uncommitted_pages(&mut self) {
912 self.write_buffer.dirty.clear();
913 self.write_buffer.in_atomic_write = false;
914 self.db_size_pages = self.committed_size_pages_before_transaction();
915 self.refused_write_pending = false;
916 }
917
918 fn new(config: &VfsConfig, _profiling_enabled: bool) -> Self {
919 let prefetch_unused_total = Arc::new(AtomicU64::new(0));
920 let prefetch_buffer =
921 PageBuffer::with_unused_counter(&config.page_budget, prefetch_unused_total.clone());
922 let committed_buffer = build_page_buffer(config);
923 let mut state = Self {
924 db_size_pages: 1,
925 committed_db_size_pages: 1,
926 head_txid: None,
927 page_size: DEFAULT_PAGE_SIZE,
928 prefetch_buffer,
929 committed_buffer,
930 prefetch_unused_total,
931 write_buffer: WriteBuffer::default(),
932 corked: None,
933 dead: false,
934 corked_db_size_pages: 0,
935 closing: false,
936 refused_write_pending: false,
937 };
938 state.cache_page(
939 config,
940 PageCacheInsertKind::Startup,
941 1,
942 empty_db_page().into(),
943 );
944 state
945 }
946
947 fn cache_page(
948 &mut self,
949 config: &VfsConfig,
950 kind: PageCacheInsertKind,
951 pgno: u32,
952 bytes: PageBytes,
953 ) {
954 if !should_cache_page(config, kind, pgno) {
955 return;
956 }
957 cache_page(config, &self.prefetch_buffer, kind, pgno, bytes);
958 }
959
960 fn take_buffered_page(
966 &self,
967 config: &VfsConfig,
968 pgno: u32,
969 partial: bool,
970 ) -> Option<(PageBytes, bool)> {
971 if !can_read_cached_page(config, pgno) {
972 return None;
973 }
974 if pgno == 1 || partial {
975 let bytes = self
976 .committed_buffer
977 .get(&pgno)
978 .or_else(|| self.prefetch_buffer.get(&pgno))?;
979 return Some((bytes, false));
980 }
981
982 if let Some(bytes) = self.committed_buffer.take(&pgno) {
1008 self.prefetch_buffer.invalidate(&pgno);
1012 Some((bytes, false))
1013 } else {
1014 self.prefetch_buffer.take_with_status(&pgno)
1015 }
1016 }
1017
1018
1019
1020 fn corked_page(&self, pgno: u32) -> Option<&PageBytes> {
1022 self.corked.as_ref().and_then(|corked| corked.get(&pgno))
1023 }
1024
1025 fn cork_dirty_pages(&mut self, max_pages: usize) -> CorkOutcome {
1032 let Some(corked) = self.corked.as_mut() else {
1033 return CorkOutcome::NotCorked;
1034 };
1035 let added_pages = self
1036 .write_buffer
1037 .dirty
1038 .keys()
1039 .filter(|pgno| !corked.contains_key(pgno))
1040 .count();
1041 if corked.len() + added_pages > max_pages {
1042 return CorkOutcome::Overflow;
1043 }
1044 corked.append(&mut self.write_buffer.dirty);
1045 self.corked_db_size_pages = self.db_size_pages;
1046 CorkOutcome::Corked
1047 }
1048
1049 fn note_committed_page(&mut self, config: &VfsConfig, pgno: u32, bytes: &PageBytes) {
1053 self.prefetch_buffer.invalidate(&pgno);
1054 if pgno != 1 {}
1055 if config.staging_cache_ttl_ms == 0 || !config.page_cache_mode.caches_any_pages() {
1056 return;
1057 }
1058 self.committed_buffer.insert(pgno, bytes.clone());
1059 }
1060
1061 fn seed_page(
1062 &mut self,
1063 config: &VfsConfig,
1064 kind: PageCacheInsertKind,
1065 pgno: u32,
1066 page: PageBytes,
1067 ) {
1068 if pgno == 1 {
1069 self.seed_main_page(config, kind, page);
1070 } else {
1071 self.cache_page(config, kind, pgno, page);
1072 }
1073 }
1074
1075 fn seed_main_page(&mut self, config: &VfsConfig, kind: PageCacheInsertKind, page: PageBytes) {
1076 if let Some(page_size) = sqlite_header_page_size(&page) {
1077 self.page_size = page_size;
1078 }
1079 if let Some(db_size_pages) = sqlite_header_db_size_pages(&page) {
1080 self.db_size_pages = db_size_pages;
1081 self.committed_db_size_pages = db_size_pages;
1082 }
1083 self.cache_page(config, kind, 1, page);
1084 }
1085
1086 fn invalidate_page_buffers(&mut self) {
1087 self.prefetch_buffer.invalidate_all();
1088 self.committed_buffer.invalidate_all();
1089 }
1090}
1091
1092fn build_page_buffer(config: &VfsConfig) -> PageBuffer {
1093 PageBuffer::new(&config.page_budget)
1094}
1095
1096fn cache_page(
1097 config: &VfsConfig,
1098 prefetch_buffer: &PageBuffer,
1099 kind: PageCacheInsertKind,
1100 pgno: u32,
1101 bytes: PageBytes,
1102) {
1103 if !should_cache_page(config, kind, pgno) {
1104 return;
1105 }
1106 if kind == PageCacheInsertKind::Prefetch || (kind == PageCacheInsertKind::Startup && pgno != 1)
1107 {
1108 prefetch_buffer.insert_prefetched(pgno, bytes);
1109 } else {
1110 prefetch_buffer.insert(pgno, bytes);
1111 }
1112}
1113
1114fn should_cache_page(config: &VfsConfig, kind: PageCacheInsertKind, pgno: u32) -> bool {
1115 match kind {
1116 PageCacheInsertKind::Target => false,
1117 PageCacheInsertKind::PartialTarget => true,
1118 PageCacheInsertKind::Prefetch => {
1119 config.staging_cache_ttl_ms > 0 && config.page_cache_mode.caches_prefetched_pages()
1120 }
1121 PageCacheInsertKind::Startup => {
1122 pgno == 1
1123 || (config.staging_cache_ttl_ms > 0
1124 && config.page_cache_mode.caches_startup_preloaded_pages())
1125 }
1126 }
1127}
1128
1129fn can_read_cached_page(config: &VfsConfig, pgno: u32) -> bool {
1130 pgno == 1 || config.page_cache_mode.caches_any_pages()
1131}
1132
1133impl VfsContext {
1134 fn new(
1135 actor_id: String,
1136 generation: Option<u64>,
1137 runtime: Handle,
1138 transport: SqliteTransportHandle,
1139 config: VfsConfig,
1140 io_methods: sqlite3_io_methods,
1141 initial_pages: impl Into<InitialPages>,
1142 metrics: Option<Arc<dyn SqliteVfsMetrics>>,
1143 ) -> std::result::Result<Self, String> {
1144 let profiling_enabled = metrics
1145 .as_ref()
1146 .is_some_and(|metrics| metrics.profiling_enabled());
1147 let mut state = VfsState::new(&config, profiling_enabled);
1148 let initial_pages = initial_pages.into();
1149 state.head_txid = initial_pages.head_txid;
1150 for (pgno, page) in initial_pages.pages {
1151 state.seed_page(&config, PageCacheInsertKind::Startup, pgno, page);
1152 }
1153
1154 Ok(Self {
1155 actor_id,
1156 generation,
1157 runtime,
1158 transport,
1159 config: config.clone(),
1160 state: RwLock::new(state),
1161 aux_files: RwLock::new(BTreeMap::new()),
1162 last_error: Mutex::new(None),
1163 indeterminate_result: Mutex::new(None),
1164 transient_commit_error: Mutex::new(None),
1165 transaction_too_large: Mutex::new(None),
1166 storage_error: Mutex::new(None),
1167 fatal_error: RwLock::new(None),
1168
1169
1170
1171
1172 commit_atomic_count: AtomicU64::new(0),
1173 trace_commit_attempts: AtomicU64::new(0),
1174 io_methods: Box::new(io_methods),
1175 resolve_pages_total: AtomicU64::new(0),
1176 resolve_pages_cache_hits: AtomicU64::new(0),
1177 resolve_pages_fetches: AtomicU64::new(0),
1178 pages_fetched_total: AtomicU64::new(0),
1179 prefetch_pages_total: AtomicU64::new(0),
1180 prefetch_consumed_total: AtomicU64::new(0),
1181 commit_total: AtomicU64::new(0),
1182 commit_request_build_ns: AtomicU64::new(0),
1183 commit_serialize_ns: AtomicU64::new(0),
1184 commit_transport_ns: AtomicU64::new(0),
1185 commit_state_update_ns: AtomicU64::new(0),
1186 commit_duration_ns_total: AtomicU64::new(0),
1187 operation_profile_active: AtomicBool::new(false),
1188 operation_profile: Mutex::new(None),
1189 operation_prefetch_unused_start: AtomicU64::new(0),
1190 reported_prefetch_unused: AtomicU64::new(0),
1191 metrics,
1192 })
1193 }
1194
1195 fn begin_operation_profile(&self) {
1196 if self
1197 .metrics
1198 .as_ref()
1199 .is_some_and(|metrics| metrics.profiling_enabled())
1200 {
1201 let unused = self
1202 .state
1203 .read()
1204 .prefetch_unused_total
1205 .load(Ordering::Relaxed);
1206 self.operation_prefetch_unused_start
1207 .store(unused, Ordering::Relaxed);
1208 *self.operation_profile.lock() = Some(SqliteOperationProfile::default());
1209 self.operation_profile_active.store(true, Ordering::Release);
1210 }
1211 }
1212
1213 fn report_prefetch_unused(&self) {
1214 let total = self
1215 .state
1216 .read()
1217 .prefetch_unused_total
1218 .load(Ordering::Relaxed);
1219 let previous = self
1220 .reported_prefetch_unused
1221 .fetch_max(total, Ordering::Relaxed);
1222 if total > previous
1223 && let Some(metrics) = &self.metrics
1224 {
1225 metrics.record_prefetch_unused_pages(total - previous);
1226 }
1227 }
1228
1229 fn finish_operation_profile(&self) -> SqliteOperationProfile {
1230 self.report_prefetch_unused();
1231 if !self.operation_profile_active.swap(false, Ordering::AcqRel) {
1232 return SqliteOperationProfile::default();
1233 }
1234 let mut profile = self.operation_profile.lock().take().unwrap_or_default();
1235 let unused = self
1236 .state
1237 .read()
1238 .prefetch_unused_total
1239 .load(Ordering::Relaxed)
1240 .saturating_sub(self.operation_prefetch_unused_start.load(Ordering::Relaxed));
1241 profile.prefetch_unused_pages = unused;
1242 profile
1243 }
1244
1245 fn update_operation_profile(&self, update: impl FnOnce(&mut SqliteOperationProfile)) {
1246 if !self.operation_profile_active.load(Ordering::Relaxed) {
1247 return;
1248 }
1249 if let Some(profile) = self.operation_profile.lock().as_mut() {
1250 update(profile);
1251 }
1252 }
1253
1254 fn profile_request_limit(&self) -> usize {
1255 self.metrics
1256 .as_ref()
1257 .map_or(0, |metrics| metrics.max_profiled_get_pages_requests())
1258 .min(MAX_PROFILED_GET_PAGES_REQUESTS)
1259 }
1260
1261 fn clear_last_error(&self) {
1262 *self.last_error.lock() = None;
1263 }
1264
1265 fn set_last_error(&self, message: String) {
1266 *self.last_error.lock() = Some(message);
1267 }
1268
1269 fn mark_indeterminate(&self, operation: String) {
1270 *self.indeterminate_result.lock() = Some(operation.clone());
1271 self.mark_fatal(SqliteCommitIndeterminateError(operation).to_string());
1272 }
1273
1274 fn take_indeterminate_result(&self) -> Option<String> {
1275 self.indeterminate_result.lock().take()
1276 }
1277
1278 fn clone_last_error(&self) -> Option<String> {
1279 self.last_error.lock().clone()
1280 }
1281
1282 fn clone_fatal_error(&self) -> Option<String> {
1283 self.fatal_error.read().clone()
1284 }
1285
1286 fn defer_transient_commit_error(&self, message: String) {
1287 *self.transient_commit_error.lock() = Some(message);
1288 }
1289
1290 fn take_transient_commit_error(&self) -> Option<String> {
1291 self.transient_commit_error.lock().take()
1292 }
1293
1294 fn transaction_too_large_error(&self) -> SqliteTransactionTooLargeError {
1295 SqliteTransactionTooLargeError {
1296 max_pages: self.config.commit_page_cap(),
1297 page_size: self.page_size(),
1298 }
1299 }
1300
1301 fn refuse_oversize_transaction(&self) -> c_int {
1305 let error = self.transaction_too_large_error();
1306 tracing::warn!(
1307 actor_id = %self.actor_id,
1308 max_pages = error.max_pages,
1309 "sqlite transaction exceeds the commit page cap; the application must split it into smaller transactions"
1310 );
1311 self.set_last_error(error.to_string());
1312 *self.transaction_too_large.lock() = Some(error);
1313 SQLITE_FULL
1314 }
1315
1316 fn transaction_too_large_commit_error(&self) -> CommitBufferError {
1319 CommitBufferError::Other(self.transaction_too_large_error().to_string())
1320 }
1321
1322 fn commit_cork_alone(&self) -> std::result::Result<(), CommitBufferError> {
1327 let request = {
1328 let state = self.state.read();
1329 let Some(corked) = state.corked.as_ref() else {
1330 return Ok(());
1331 };
1332 BufferedCommitRequest {
1333 actor_id: self.actor_id.clone(),
1334 new_db_size_pages: state.corked_db_size_pages,
1335 expected_head_txid: state.head_txid,
1336 dirty_pages: corked
1337 .iter()
1338 .map(|(pgno, bytes)| BufferedDirtyPage {
1339 pgno: *pgno,
1340 bytes: bytes.clone(),
1341 })
1342 .collect(),
1343 }
1344 };
1345
1346 let outcome = match self.block_on_buffered_commit(request.clone(), None) {
1347 Ok(CommitWait::Completed((outcome, _))) => outcome,
1348 Ok(CommitWait::TimedOut) => {
1349 return Err(CommitBufferError::Other(
1350 "sqlite commit timed out".to_string(),
1351 ));
1352 }
1353 Err(err) => {
1354 tracing::error!(
1355 actor_id = %self.actor_id,
1356 dirty_pages = request.dirty_pages.len(),
1357 ?err,
1358 "sqlite commit of a full cork failed"
1359 );
1360 handle_non_finalize_commit_error(self, &err);
1361 return Err(err);
1362 }
1363 };
1364 self.commit_total.fetch_add(1, Ordering::Relaxed);
1365 if let Some(metrics) = &self.metrics {
1366 metrics.record_commit(request.dirty_pages.len() as u64);
1367 }
1368
1369 let mut state = self.state.write();
1370 state.committed_db_size_pages = outcome.db_size_pages;
1371 state.head_txid = outcome
1372 .head_txid
1373 .or_else(|| state.head_txid.map(|head_txid| head_txid.saturating_add(1)));
1374 for dirty_page in &request.dirty_pages {
1375 state.note_committed_page(&self.config, dirty_page.pgno, &dirty_page.bytes);
1376 }
1377 if let Some(corked) = state.corked.as_mut() {
1378 corked.clear();
1379 }
1380 Ok(())
1381 }
1382
1383 pub(crate) fn take_transaction_too_large(&self) -> Option<SqliteTransactionTooLargeError> {
1384 self.transaction_too_large.lock().take()
1385 }
1386
1387 fn set_storage_error(&self, error: SqliteStorageError) {
1389 self.set_last_error(error.to_string());
1390 *self.storage_error.lock() = Some(error);
1391 }
1392
1393 pub(crate) fn take_storage_error(&self) -> Option<SqliteStorageError> {
1394 self.storage_error.lock().take()
1395 }
1396
1397 fn discard_refused_transaction(&self) {
1401 let mut state = self.state.write();
1402 if !state.refused_write_pending {
1403 return;
1404 }
1405 tracing::debug!(
1406 actor_id = %self.actor_id,
1407 dirty_pages = state.write_buffer.dirty.len(),
1408 "discarding the pages of an abandoned sqlite transaction at close"
1409 );
1410 state.discard_uncommitted_pages();
1411 }
1412
1413 fn abandon_write_buffer(&self) {
1417 self.state.write().refused_write_pending = true;
1418 }
1419
1420 fn discard_open_transaction(&self) {
1423 let mut state = self.state.write();
1424 if state.write_buffer.dirty.is_empty()
1425 && !state.write_buffer.in_atomic_write
1426 && state.db_size_pages == state.committed_size_pages_before_transaction()
1427 {
1428 return;
1429 }
1430 tracing::debug!(
1431 actor_id = %self.actor_id,
1432 dirty_pages = state.write_buffer.dirty.len(),
1433 "discarding the pages of an uncommitted sqlite transaction at close"
1434 );
1435 state.discard_uncommitted_pages();
1436 }
1437
1438 fn discard_unsent_commits(&self) -> usize {
1443 let mut state = self.state.write();
1444 let dirty_pages = state.write_buffer.dirty.len();
1445 state.discard_uncommitted_pages();
1446 dirty_pages
1447 }
1448
1449
1450
1451 pub(crate) fn take_last_error(&self) -> Option<String> {
1452 self.last_error.lock().take()
1453 }
1454
1455 fn add_commit_phase_metrics(
1456 &self,
1457 request_build_ns: u64,
1458 transport_metrics: CommitTransportMetrics,
1459 state_update_ns: u64,
1460 total_ns: u64,
1461 ) {
1462 self.update_operation_profile(|profile| {
1463 profile.storage_ns = profile
1464 .storage_ns
1465 .saturating_add(transport_metrics.transport_ns);
1466 profile.commit_round_trips = profile.commit_round_trips.saturating_add(1);
1467 });
1468 if let Some(metrics) = &self.metrics {
1469 metrics.observe_commit_phases(
1470 request_build_ns,
1471 transport_metrics.serialize_ns,
1472 transport_metrics.transport_ns,
1473 state_update_ns,
1474 total_ns,
1475 );
1476 }
1477 self.commit_request_build_ns
1478 .fetch_add(request_build_ns, Ordering::Relaxed);
1479 self.commit_serialize_ns
1480 .fetch_add(transport_metrics.serialize_ns, Ordering::Relaxed);
1481 self.commit_transport_ns
1482 .fetch_add(transport_metrics.transport_ns, Ordering::Relaxed);
1483 self.commit_state_update_ns
1484 .fetch_add(state_update_ns, Ordering::Relaxed);
1485 self.commit_duration_ns_total
1486 .fetch_add(total_ns, Ordering::Relaxed);
1487 }
1488
1489 fn sqlite_vfs_metrics(&self) -> SqliteVfsMetricsSnapshot {
1490 let state = self.state.read();
1491
1492 SqliteVfsMetricsSnapshot {
1493 request_build_ns: self.commit_request_build_ns.load(Ordering::Relaxed),
1494 serialize_ns: self.commit_serialize_ns.load(Ordering::Relaxed),
1495 transport_ns: self.commit_transport_ns.load(Ordering::Relaxed),
1496 state_update_ns: self.commit_state_update_ns.load(Ordering::Relaxed),
1497 total_ns: self.commit_duration_ns_total.load(Ordering::Relaxed),
1498 commit_count: self.commit_total.load(Ordering::Relaxed),
1499 page_cache_entries: state
1500 .prefetch_buffer
1501 .entry_count()
1502 .saturating_add(state.committed_buffer.entry_count()),
1503 page_cache_weighted_size: state.prefetch_buffer.weighted_size(),
1504 page_cache_capacity_pages: self.config.page_budget.capacity_pages(),
1505 write_buffer_dirty_pages: state.write_buffer.dirty.len() as u64,
1506 db_size_pages: state.db_size_pages as u64,
1507 }
1508 }
1509
1510 fn block_on_buffered_commit(
1511 &self,
1512 request: BufferedCommitRequest,
1513 timeout: Option<Duration>,
1514 ) -> std::result::Result<
1515 CommitWait<(BufferedCommitOutcome, CommitTransportMetrics)>,
1516 CommitBufferError,
1517 > {
1518 let trace_pages =
1519 tracing::enabled!(target: "sqlite_prefetch::trace", tracing::Level::TRACE).then(|| {
1520 (
1521 request
1522 .dirty_pages
1523 .iter()
1524 .map(|page| page.pgno)
1525 .collect::<Vec<_>>(),
1526 request.new_db_size_pages,
1527 )
1528 });
1529 let attempt = trace_pages
1530 .as_ref()
1531 .map(|_| self.trace_commit_attempts.fetch_add(1, Ordering::Relaxed));
1532 let span = tracing::trace_span!(target: "sqlite_prefetch::trace", "native_sqlite_commit",
1533 stream_id = %format_args!("sqlite-{:p}", self), actor_id = %self.actor_id,
1534 generation = self.generation, commit_attempt = attempt,
1535 local_request_id = %format_args!("c{}", self.commit_total.load(Ordering::Relaxed)));
1536 let commit =
1537 commit_buffered_pages(&*self.transport, request, self.config.commit_page_cap())
1538 .instrument(span.clone());
1539 let result = if let Some(timeout) = timeout {
1540 match self
1541 .runtime
1542 .block_on(async { tokio::time::timeout(timeout, commit).await })
1543 {
1544 Ok(result) => result,
1545 Err(_) => {
1546 span.in_scope(|| tracing::trace!(target: "sqlite_prefetch::trace", event = "commit_result", outcome = "timeout"));
1547 return Ok(CommitWait::TimedOut);
1548 }
1549 }
1550 } else {
1551 self.runtime.block_on(commit)
1552 };
1553
1554 let _span = span.enter();
1555 tracing::trace!(target: "sqlite_prefetch::trace", event = "commit_result",
1556 outcome = if result.is_ok() { "acknowledged" } else { "error" });
1557
1558 if result.is_ok()
1559 && let Some((pages, size_pages)) = trace_pages
1560 {
1561 let request_id = self.commit_total.load(Ordering::Relaxed);
1562 for pgno in pages {
1563 tracing::trace!(target: "sqlite_prefetch::trace", event = "access", op = "write",
1564 stream_id = %format_args!("sqlite-{:p}", self),
1565 pgno, hit = false, request_id = %format_args!("c{request_id}"));
1566 }
1567 tracing::trace!(target: "sqlite_prefetch::trace", event = "commit",
1568 stream_id = %format_args!("sqlite-{:p}", self),
1569 request_id = %format_args!("c{request_id}"), size_pages);
1570 }
1571 result.map(CommitWait::Completed)
1572 }
1573
1574 fn round_trip_counts(&self) -> SqliteRoundTripCounts {
1575 SqliteRoundTripCounts {
1576 get_pages: self.resolve_pages_fetches.load(Ordering::Relaxed),
1577 commit: self.commit_total.load(Ordering::Relaxed),
1578 }
1579 }
1580
1581 fn page_size(&self) -> usize {
1582 self.state.read().page_size.max(DEFAULT_PAGE_SIZE)
1583 }
1584
1585 fn open_aux_file(&self, path: &str) -> Arc<AuxFileState> {
1586 let mut aux_files = self.aux_files.write();
1587 aux_files
1588 .entry(path.to_string())
1589 .or_insert_with(|| Arc::new(AuxFileState::default()))
1590 .clone()
1591 }
1592
1593 fn aux_file_exists(&self, path: &str) -> bool {
1594 self.aux_files.read().contains_key(path)
1595 }
1596
1597 fn read_aux_file(&self, path: &str) -> Option<Vec<u8>> {
1598 let state = self.aux_files.read().get(path).cloned()?;
1599 let bytes = state.bytes.lock().clone();
1600 Some(bytes)
1601 }
1602
1603 fn delete_aux_file(&self, path: &str) {
1604 self.aux_files.write().remove(path);
1605 }
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615 fn is_dead(&self) -> bool {
1616 self.state.read().dead
1617 }
1618
1619 fn mark_fatal(&self, message: String) {
1620 self.set_last_error(message.clone());
1621 self.state.write().dead = true;
1622 let mut fatal_error = self.fatal_error.write();
1623 if fatal_error.is_none() {
1624 *fatal_error = Some(message);
1625 }
1626 }
1627
1628 fn resolve_pages(
1631 &self,
1632 target_pgnos: &[u32],
1633 partial: bool,
1634 ) -> std::result::Result<HashMap<u32, Option<PageBytes>>, GetPagesError> {
1635 use std::sync::atomic::Ordering::Relaxed;
1636 let request_id = self.resolve_pages_total.fetch_add(1, Relaxed);
1637 self.update_operation_profile(|profile| {
1638 profile.sqlite_requested_pages = profile
1639 .sqlite_requested_pages
1640 .saturating_add(target_pgnos.len() as u64);
1641 });
1642 if let Some(metrics) = &self.metrics {
1643 metrics.record_resolve_pages(target_pgnos.len() as u64);
1644 }
1645
1646 let mut resolved = HashMap::new();
1647 let mut missing = Vec::new();
1648 let mut seen = HashSet::new();
1649 let mut prefetch_consumed = 0_u64;
1650
1651 {
1652 let state = self.state.read();
1653 if state.dead {
1654 return Err(GetPagesError::Other(
1655 "sqlite actor lost its fence".to_string(),
1656 ));
1657 }
1658
1659 for pgno in target_pgnos.iter().copied() {
1660 if !seen.insert(pgno) {
1661 continue;
1662 }
1663 if let Some(bytes) = state.write_buffer.dirty.get(&pgno) {
1664 resolved.insert(pgno, Some(bytes.clone()));
1665 continue;
1666 }
1667 if let Some(bytes) = state.corked_page(pgno) {
1668 resolved.insert(pgno, Some(bytes.clone()));
1669 continue;
1670 }
1671 if let Some((bytes, was_prefetched)) =
1672 state.take_buffered_page(&self.config, pgno, partial)
1673 {
1674 prefetch_consumed = prefetch_consumed.saturating_add(u64::from(was_prefetched));
1675 resolved.insert(pgno, Some(bytes));
1676 continue;
1677 }
1678 missing.push(pgno);
1679 }
1680 }
1681 if tracing::enabled!(target: "sqlite_prefetch::trace", tracing::Level::TRACE) {
1682 for pgno in target_pgnos {
1683 tracing::trace!(target: "sqlite_prefetch::trace", event = "access", op = "read",
1684 stream_id = %format_args!("sqlite-{:p}", self),
1685 pgno, hit = resolved.contains_key(pgno), request_id);
1686 }
1687 }
1688 if prefetch_consumed > 0 {
1689 self.prefetch_consumed_total
1690 .fetch_add(prefetch_consumed, Relaxed);
1691 self.update_operation_profile(|profile| {
1692 profile.prefetch_consumed_pages = profile
1693 .prefetch_consumed_pages
1694 .saturating_add(prefetch_consumed);
1695 });
1696 }
1697
1698 if missing.is_empty() {
1699 self.resolve_pages_cache_hits
1700 .fetch_add(target_pgnos.len() as u64, Relaxed);
1701 if let Some(metrics) = &self.metrics {
1702 metrics.record_resolve_cache_hits(target_pgnos.len() as u64);
1703 }
1704 self.update_operation_profile(|profile| {
1705 profile.cache_hit_pages = profile
1706 .cache_hit_pages
1707 .saturating_add(target_pgnos.len() as u64);
1708 });
1709 return Ok(resolved);
1710 }
1711 self.resolve_pages_cache_hits
1712 .fetch_add((seen.len() - missing.len()) as u64, Relaxed);
1713 if let Some(metrics) = &self.metrics {
1714 metrics.record_resolve_cache_hits((seen.len() - missing.len()) as u64);
1715 metrics.record_resolve_cache_misses(missing.len() as u64);
1716 }
1717 self.update_operation_profile(|profile| {
1718 profile.cache_hit_pages = profile
1719 .cache_hit_pages
1720 .saturating_add((seen.len() - missing.len()) as u64);
1721 profile.cache_miss_pages = profile
1722 .cache_miss_pages
1723 .saturating_add(missing.len() as u64);
1724 });
1725
1726 let (to_fetch, page_size, db_size_pages, expected_head_txid) = {
1727 let state = self.state.read();
1728 let to_fetch = missing.clone();
1730 (
1731 to_fetch,
1732 state.page_size.max(1),
1733 state.db_size_pages,
1734 state.head_txid,
1735 )
1736 };
1737
1738 {
1739 self.resolve_pages_fetches.fetch_add(1, Relaxed);
1740 self.pages_fetched_total
1741 .fetch_add(to_fetch.len() as u64, Relaxed);
1742 tracing::debug!(
1743 actor_id = %self.actor_id,
1744 generation = ?self.generation,
1745 requested_pages = ?target_pgnos,
1746 missing_pages = ?missing,
1747 total_fetch_pages = to_fetch.len(),
1748 total_fetch_bytes = to_fetch.len().saturating_mul(page_size),
1749 db_size_pages,
1750 expected_head_txid,
1751 "vfs get_pages fetch"
1752 );
1753 }
1754
1755 let trace_span = tracing::trace_span!(target: "sqlite_prefetch::trace", "native_sqlite_read",
1756 stream_id = %format_args!("sqlite-{:p}", self), actor_id = %self.actor_id,
1757 generation = self.generation, local_request_id = request_id);
1758 let get_pages_start = Instant::now();
1759 let response = self.runtime.block_on(
1763 self.transport
1764 .get_pages(protocol::SqliteGetPagesRequest {
1765 actor_id: self.actor_id.clone().into(),
1766 pgnos: to_fetch.clone(),
1767 expected_generation: None,
1768 expected_head_txid,
1769 })
1770 .instrument(trace_span.clone()),
1771 );
1772 let _trace_guard = trace_span.enter();
1775 let get_pages_duration_ns = get_pages_start.elapsed().as_nanos() as u64;
1776 if let Some(metrics) = &self.metrics {
1777 metrics.observe_get_pages_duration(get_pages_duration_ns);
1778 }
1779 let profile_active = self.operation_profile_active.load(Ordering::Relaxed);
1780 let ordinal = if profile_active {
1781 self.operation_profile
1782 .lock()
1783 .as_ref()
1784 .map_or(1, |profile| profile.get_pages_round_trips.saturating_add(1))
1785 } else {
1786 1
1787 };
1788 let response = match response {
1789 Ok(response) => response,
1790 Err(err) => {
1791 self.update_operation_profile(|profile| {
1792 profile.push_get_pages(
1793 SqliteGetPagesProfile {
1794 ordinal,
1795 duration_ns: get_pages_duration_ns,
1796 demand_requested: missing.len() as u64,
1797 ..Default::default()
1798 },
1799 self.profile_request_limit(),
1800 );
1801 });
1802 if let Some(error) = storage_timeout(&err) {
1803 return Err(GetPagesError::Storage(error));
1804 }
1805 return Err(GetPagesError::Other(err.to_string()));
1806 }
1807 };
1808
1809 match response {
1810 protocol::SqliteGetPagesResponse::SqliteGetPagesOk(ok) => {
1811 if profile_active {
1812 let requested = to_fetch.iter().copied().collect::<HashSet<_>>();
1813 let mut request_profile = SqliteGetPagesProfile {
1814 ordinal,
1815 duration_ns: get_pages_duration_ns,
1816 demand_requested: missing.len() as u64,
1817 success: true,
1818 ..Default::default()
1819 };
1820 let mut btree_pages = 0_u64;
1821 let mut non_btree_pages = 0_u64;
1822 for fetched in &ok.pages {
1823 if !requested.contains(&fetched.pgno) {
1824 request_profile.overflow_expansion_extra =
1825 request_profile.overflow_expansion_extra.saturating_add(1);
1826 }
1827 if let Some(bytes) = &fetched.bytes {
1828 request_profile.response_present =
1829 request_profile.response_present.saturating_add(1);
1830 request_profile.response_bytes = request_profile
1831 .response_bytes
1832 .saturating_add(bytes.len() as u64);
1833 match classify(fetched.pgno, bytes) {
1834 PageClass::Btree => btree_pages = btree_pages.saturating_add(1),
1835 PageClass::Overflow => {
1836 non_btree_pages = non_btree_pages.saturating_add(1)
1837 }
1838 }
1839 } else {
1840 request_profile.response_missing =
1841 request_profile.response_missing.saturating_add(1);
1842 }
1843 }
1844 self.update_operation_profile(|profile| {
1845 profile.btree_pages = profile.btree_pages.saturating_add(btree_pages);
1846 profile.non_btree_pages =
1847 profile.non_btree_pages.saturating_add(non_btree_pages);
1848 profile.push_get_pages(request_profile, self.profile_request_limit());
1849 });
1850 }
1851 let response_head_txid = ok.head_txid;
1852 if let Some(head_txid) = response_head_txid {
1853 self.state.write().head_txid = Some(head_txid);
1854 }
1855 let missing_pages = missing.iter().copied().collect::<HashSet<_>>();
1856 let prefetch_buffer = self.state.read().prefetch_buffer.clone();
1857 #[cfg(debug_assertions)]
1858 let mut returned_pgnos = HashSet::new();
1859 #[cfg(debug_assertions)]
1860 let mut returned_missing_pages = Vec::new();
1861 #[cfg(debug_assertions)]
1862 let mut returned_missing_in_range_pages = Vec::new();
1863 let mut server_prefetched = 0_u64;
1864 for (ordinal, fetched) in ok.pages.into_iter().enumerate() {
1865 tracing::trace!(target: "sqlite_prefetch::trace", event = "page_received",
1866 ordinal, pgno = fetched.pgno, present = fetched.bytes.is_some(),
1867 bytes = fetched.bytes.as_ref().map_or(0, |bytes| bytes.len()),
1868 demand = missing_pages.contains(&fetched.pgno));
1869 let delivered = fetched
1871 .bytes
1872 .as_ref()
1873 .and_then(|bytes| PageTrace::capture(ordinal, fetched.pgno, bytes.len()));
1874 #[cfg(debug_assertions)]
1875 {
1876 returned_pgnos.insert(fetched.pgno);
1877 if fetched.bytes.is_none() {
1878 returned_missing_pages.push(fetched.pgno);
1879 if fetched.pgno <= db_size_pages {
1880 returned_missing_in_range_pages.push(fetched.pgno);
1881 }
1882 }
1883 }
1884 let bytes = if fetched.bytes.is_none()
1885 && self.commit_total.load(Relaxed) == 0
1886 && missing_pages.contains(&fetched.pgno)
1887 && fetched.pgno == 1
1888 {
1889 self.state.write().head_txid = Some(response_head_txid.unwrap_or(0));
1892 Some(PageBytes::from(empty_db_page()))
1893 } else {
1894 fetched.bytes.map(PageBytes::from)
1895 };
1896 if let Some(bytes) = &bytes {
1897 let kind = if missing_pages.contains(&fetched.pgno) {
1900 if partial {
1901 PageCacheInsertKind::PartialTarget
1902 } else {
1903 PageCacheInsertKind::Target
1904 }
1905 } else {
1906 PageCacheInsertKind::Prefetch
1907 };
1908 if kind == PageCacheInsertKind::Prefetch {
1909 server_prefetched += 1;
1910 if should_cache_page(&self.config, kind, fetched.pgno) {
1911 prefetch_buffer.insert_prefetched_traced(
1912 fetched.pgno,
1913 bytes.clone(),
1914 delivered,
1915 );
1916 } else if let Some(trace) = delivered {
1917 trace.disposition("unused");
1918 }
1919 } else {
1920 if let Some(trace) = delivered {
1923 trace.disposition("consumed");
1924 }
1925 cache_page(
1926 &self.config,
1927 &prefetch_buffer,
1928 kind,
1929 fetched.pgno,
1930 bytes.clone(),
1931 );
1932 }
1933 }
1934 resolved.insert(fetched.pgno, bytes);
1935 }
1936 self.prefetch_pages_total
1937 .fetch_add(server_prefetched, Relaxed);
1938 if let Some(metrics) = &self.metrics {
1939 metrics.record_get_pages_request(
1940 to_fetch.len() as u64,
1941 server_prefetched,
1942 page_size as u64,
1943 );
1944 }
1945 #[cfg(debug_assertions)]
1946 {
1947 let absent_response_pages = to_fetch
1948 .iter()
1949 .copied()
1950 .filter(|pgno| !returned_pgnos.contains(pgno))
1951 .collect::<Vec<_>>();
1952 let absent_in_range_pages = absent_response_pages
1953 .iter()
1954 .copied()
1955 .filter(|pgno| *pgno <= db_size_pages)
1956 .collect::<Vec<_>>();
1957 if !returned_missing_in_range_pages.is_empty()
1958 || !absent_in_range_pages.is_empty()
1959 {
1960 tracing::warn!(
1961 actor_id = %self.actor_id,
1962 requested_pages = ?target_pgnos,
1963 missing_pages = ?missing,
1964 fetch_pages = ?to_fetch,
1965 db_size_pages,
1966 returned_missing_pages = ?returned_missing_pages,
1967 returned_missing_in_range_pages = ?returned_missing_in_range_pages,
1968 absent_response_pages = ?absent_response_pages,
1969 absent_in_range_pages = ?absent_in_range_pages,
1970 "sqlite get_pages returned missing pages within declared db size"
1971 );
1972 }
1973 }
1974 for pgno in missing {
1975 resolved.entry(pgno).or_insert(None);
1976 }
1977 Ok(resolved)
1978 }
1979 protocol::SqliteGetPagesResponse::SqliteErrorResponse(error) => {
1980 self.update_operation_profile(|profile| {
1981 profile.push_get_pages(
1982 SqliteGetPagesProfile {
1983 ordinal,
1984 duration_ns: get_pages_duration_ns,
1985 demand_requested: missing.len() as u64,
1986 ..Default::default()
1987 },
1988 self.profile_request_limit(),
1989 );
1990 });
1991 if self.commit_total.load(Relaxed) == 0
1992 && missing.contains(&1)
1993 && is_initial_main_page_missing(&error.message)
1994 {
1995 for pgno in missing {
1996 let bytes = if pgno == 1 {
1997 Some(PageBytes::from(empty_db_page()))
1998 } else {
1999 None
2000 };
2001 resolved.entry(pgno).or_insert(bytes);
2002 }
2003 return Ok(resolved);
2004 }
2005 if is_head_fence_mismatch_response(&error) {
2006 return Err(GetPagesError::FenceMismatch(error.message.to_string()));
2007 }
2008 if let Some(refusal) = storage_refusal(&error) {
2009 return Err(GetPagesError::Storage(refusal));
2010 }
2011 Err(GetPagesError::Other(error.message.to_string()))
2012 }
2013 }
2014 }
2015
2016 fn flush_dirty_pages(
2017 &self,
2018 ) -> std::result::Result<Option<BufferedCommitOutcome>, CommitBufferError> {
2019 match self.flush_dirty_pages_with_timeout(None)? {
2020 CommitWait::Completed(outcome) => Ok(outcome),
2021 CommitWait::TimedOut => Err(CommitBufferError::Other(
2022 "sqlite commit timed out".to_string(),
2023 )),
2024 }
2025 }
2026
2027 fn flush_dirty_pages_with_timeout(
2028 &self,
2029 timeout: Option<Duration>,
2030 ) -> std::result::Result<CommitWait<Option<BufferedCommitOutcome>>, CommitBufferError> {
2031 let total_start = Instant::now();
2032 let request_build_start = Instant::now();
2033 let request = {
2034 let mut state = self.state.write();
2035 if state.dead {
2036 return Err(CommitBufferError::Other(
2037 "sqlite actor lost its fence".to_string(),
2038 ));
2039 }
2040 if state.closing {
2041 state.discard_uncommitted_pages();
2042 return Ok(CommitWait::Completed(None));
2043 }
2044 if state.write_buffer.in_atomic_write
2045 || (state.write_buffer.dirty.is_empty()
2046 && state.db_size_pages == state.committed_db_size_pages)
2047 {
2048 return Ok(CommitWait::Completed(None));
2049 }
2050 match state.cork_dirty_pages(self.config.commit_page_cap()) {
2052 CorkOutcome::NotCorked => {}
2053 CorkOutcome::Corked => {
2054 state.refused_write_pending = false;
2055 return Ok(CommitWait::Completed(None));
2056 }
2057 CorkOutcome::Overflow => {
2058 drop(state);
2059 self.commit_cork_alone()?;
2060 let mut state = self.state.write();
2061 return match state.cork_dirty_pages(self.config.commit_page_cap()) {
2062 CorkOutcome::Corked | CorkOutcome::NotCorked => {
2063 state.refused_write_pending = false;
2064 Ok(CommitWait::Completed(None))
2065 }
2066 CorkOutcome::Overflow => Err(self.transaction_too_large_commit_error()),
2067 };
2068 }
2069 }
2070
2071 BufferedCommitRequest {
2072 actor_id: self.actor_id.clone(),
2073 new_db_size_pages: state.db_size_pages,
2074 expected_head_txid: state.head_txid,
2075 dirty_pages: state.write_buffer.snapshot(),
2076 }
2077 };
2078 let request_build_ns = request_build_start.elapsed().as_nanos() as u64;
2079 self.update_operation_profile(|profile| {
2080 profile.dirty_pages = profile
2081 .dirty_pages
2082 .saturating_add(request.dirty_pages.len() as u64);
2083 profile.dirty_bytes = profile.dirty_bytes.saturating_add(
2084 request
2085 .dirty_pages
2086 .iter()
2087 .map(|page| page.bytes.len() as u64)
2088 .sum::<u64>(),
2089 );
2090 });
2091
2092 let (outcome, transport_metrics) =
2093 match self.block_on_buffered_commit(request.clone(), timeout) {
2097 Ok(CommitWait::Completed(outcome)) => outcome,
2098 Ok(CommitWait::TimedOut) => return Ok(CommitWait::TimedOut),
2099 Err(err) => {
2100 tracing::error!(
2101 actor_id = %self.actor_id,
2102 new_db_size_pages = request.new_db_size_pages,
2103 dirty_pages = request.dirty_pages.len(),
2104 ?err,
2105 "sqlite flush commit failed"
2106 );
2107 handle_non_finalize_commit_error(self, &err);
2108 return Err(err);
2109 }
2110 };
2111 self.commit_total
2112 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2113 if let Some(metrics) = &self.metrics {
2114 metrics.record_commit(request.dirty_pages.len() as u64);
2115 }
2116 tracing::debug!(
2117 dirty_pages = request.dirty_pages.len(),
2118 path = ?outcome.path,
2119 requested_db_size_pages = request.new_db_size_pages,
2120 db_size_pages = outcome.db_size_pages,
2121 request_build_ns,
2122 serialize_ns = transport_metrics.serialize_ns,
2123 transport_ns = transport_metrics.transport_ns,
2124 "vfs commit complete (flush)"
2125 );
2126 #[cfg(debug_assertions)]
2127 {
2128 if outcome.db_size_pages != request.new_db_size_pages {
2129 tracing::warn!(
2130 actor_id = %self.actor_id,
2131 dirty_pages = request.dirty_pages.len(),
2132 path = ?outcome.path,
2133 requested_db_size_pages = request.new_db_size_pages,
2134 outcome_db_size_pages = outcome.db_size_pages,
2135 "sqlite flush commit returned db size different from request"
2136 );
2137 }
2138 }
2139 let state_update_start = Instant::now();
2140 let mut state = self.state.write();
2141 state.db_size_pages = outcome.db_size_pages;
2142 state.committed_db_size_pages = outcome.db_size_pages;
2143 state.head_txid = outcome
2144 .head_txid
2145 .or_else(|| state.head_txid.map(|head_txid| head_txid.saturating_add(1)));
2146 for dirty_page in &request.dirty_pages {
2147 state.note_committed_page(&self.config, dirty_page.pgno, &dirty_page.bytes);
2148 }
2149 state.write_buffer.dirty.clear();
2150 state.refused_write_pending = false;
2151 let state_update_ns = state_update_start.elapsed().as_nanos() as u64;
2152 self.add_commit_phase_metrics(
2153 request_build_ns,
2154 transport_metrics,
2155 state_update_ns,
2156 total_start.elapsed().as_nanos() as u64,
2157 );
2158 Ok(CommitWait::Completed(Some(outcome)))
2159 }
2160
2161 fn commit_atomic_write(&self) -> std::result::Result<(), CommitBufferError> {
2162 match self.commit_atomic_write_with_timeout(None)? {
2163 CommitWait::Completed(()) => Ok(()),
2164 CommitWait::TimedOut => Err(CommitBufferError::Other(
2165 "sqlite commit timed out".to_string(),
2166 )),
2167 }
2168 }
2169
2170 fn commit_atomic_write_with_timeout(
2171 &self,
2172 timeout: Option<Duration>,
2173 ) -> std::result::Result<CommitWait<()>, CommitBufferError> {
2174 let total_start = Instant::now();
2175 let request_build_start = Instant::now();
2176 let request = {
2177 let mut state = self.state.write();
2178 if state.dead {
2179 return Err(CommitBufferError::Other(
2180 "sqlite actor lost its fence".to_string(),
2181 ));
2182 }
2183 if !state.write_buffer.in_atomic_write {
2184 return Ok(CommitWait::Completed(()));
2185 }
2186 if state.closing {
2187 state.discard_uncommitted_pages();
2188 return Ok(CommitWait::Completed(()));
2189 }
2190 if state.write_buffer.dirty.is_empty()
2191 && state.db_size_pages == state.committed_db_size_pages
2192 {
2193 state.write_buffer.in_atomic_write = false;
2194 return Ok(CommitWait::Completed(()));
2195 }
2196 match state.cork_dirty_pages(self.config.commit_page_cap()) {
2197 CorkOutcome::NotCorked => {}
2198 CorkOutcome::Corked => {
2199 state.write_buffer.in_atomic_write = false;
2200 state.refused_write_pending = false;
2201 return Ok(CommitWait::Completed(()));
2202 }
2203 CorkOutcome::Overflow => {
2204 drop(state);
2205 self.commit_cork_alone()?;
2206 let mut state = self.state.write();
2207 return match state.cork_dirty_pages(self.config.commit_page_cap()) {
2208 CorkOutcome::Corked | CorkOutcome::NotCorked => {
2209 state.write_buffer.in_atomic_write = false;
2210 state.refused_write_pending = false;
2211 Ok(CommitWait::Completed(()))
2212 }
2213 CorkOutcome::Overflow => Err(self.transaction_too_large_commit_error()),
2214 };
2215 }
2216 }
2217
2218 BufferedCommitRequest {
2219 actor_id: self.actor_id.clone(),
2220 new_db_size_pages: state.db_size_pages,
2221 expected_head_txid: state.head_txid,
2222 dirty_pages: state.write_buffer.snapshot(),
2223 }
2224 };
2225 let request_build_ns = request_build_start.elapsed().as_nanos() as u64;
2226 self.update_operation_profile(|profile| {
2227 profile.dirty_pages = profile
2228 .dirty_pages
2229 .saturating_add(request.dirty_pages.len() as u64);
2230 profile.dirty_bytes = profile.dirty_bytes.saturating_add(
2231 request
2232 .dirty_pages
2233 .iter()
2234 .map(|page| page.bytes.len() as u64)
2235 .sum::<u64>(),
2236 );
2237 });
2238
2239 let (outcome, transport_metrics) =
2240 match self.block_on_buffered_commit(request.clone(), timeout) {
2241 Ok(CommitWait::Completed(outcome)) => outcome,
2242 Ok(CommitWait::TimedOut) => return Ok(CommitWait::TimedOut),
2243 Err(err) => {
2244 tracing::error!(
2245 actor_id = %self.actor_id,
2246 new_db_size_pages = request.new_db_size_pages,
2247 dirty_pages = request.dirty_pages.len(),
2248 ?err,
2249 "sqlite atomic commit failed"
2250 );
2251 handle_non_finalize_commit_error(self, &err);
2252 return Err(err);
2253 }
2254 };
2255 self.commit_total
2256 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2257 if let Some(metrics) = &self.metrics {
2258 metrics.record_commit(request.dirty_pages.len() as u64);
2259 }
2260 tracing::debug!(
2261 dirty_pages = request.dirty_pages.len(),
2262 path = ?outcome.path,
2263 requested_db_size_pages = request.new_db_size_pages,
2264 db_size_pages = outcome.db_size_pages,
2265 request_build_ns,
2266 serialize_ns = transport_metrics.serialize_ns,
2267 transport_ns = transport_metrics.transport_ns,
2268 "vfs commit complete (atomic)"
2269 );
2270 #[cfg(debug_assertions)]
2271 {
2272 if outcome.db_size_pages != request.new_db_size_pages {
2273 tracing::warn!(
2274 actor_id = %self.actor_id,
2275 dirty_pages = request.dirty_pages.len(),
2276 path = ?outcome.path,
2277 requested_db_size_pages = request.new_db_size_pages,
2278 outcome_db_size_pages = outcome.db_size_pages,
2279 "sqlite atomic commit returned db size different from request"
2280 );
2281 }
2282 }
2283 self.clear_last_error();
2284 let state_update_start = Instant::now();
2285 let mut state = self.state.write();
2286 state.db_size_pages = outcome.db_size_pages;
2287 state.committed_db_size_pages = outcome.db_size_pages;
2288 state.head_txid = outcome
2289 .head_txid
2290 .or_else(|| state.head_txid.map(|head_txid| head_txid.saturating_add(1)));
2291 for dirty_page in &request.dirty_pages {
2292 state.note_committed_page(&self.config, dirty_page.pgno, &dirty_page.bytes);
2293 }
2294 state.write_buffer.dirty.clear();
2295 state.write_buffer.in_atomic_write = false;
2296 state.refused_write_pending = false;
2297 let state_update_ns = state_update_start.elapsed().as_nanos() as u64;
2298 self.add_commit_phase_metrics(
2299 request_build_ns,
2300 transport_metrics,
2301 state_update_ns,
2302 total_start.elapsed().as_nanos() as u64,
2303 );
2304 Ok(CommitWait::Completed(()))
2305 }
2306
2307 fn truncate_main_file(&self, size: sqlite3_int64) -> bool {
2316 let page_size = self.page_size() as i64;
2317 let truncated_pages = ((size + page_size - 1) / page_size) as u32;
2318 let mut state = self.state.write();
2319 state.db_size_pages = truncated_pages;
2320 state
2321 .write_buffer
2322 .dirty
2323 .retain(|pgno, _| *pgno <= truncated_pages);
2324 let between_transactions =
2325 !state.write_buffer.in_atomic_write && state.write_buffer.dirty.is_empty();
2326 if let Some(corked) = state.corked.as_mut() {
2327 corked.retain(|pgno, _| *pgno <= truncated_pages);
2328 }
2329 if state.corked.is_some() && between_transactions {
2331 state.corked_db_size_pages = truncated_pages;
2332 }
2333 state.invalidate_page_buffers();
2334 state.corked.is_none()
2336 && !state.write_buffer.in_atomic_write
2337 && state.write_buffer.dirty.is_empty()
2338 && state.db_size_pages != state.committed_db_size_pages
2339 }
2340
2341 fn cork(&self) {
2345 let mut state = self.state.write();
2346 if state.corked.is_none() {
2347 state.corked = Some(BTreeMap::new());
2348 state.corked_db_size_pages = state.committed_db_size_pages;
2352 }
2353 }
2354
2355 fn uncork(&self) -> std::result::Result<(), CommitBufferError> {
2360 {
2361 let mut state = self.state.write();
2362 if state.corked.is_none() {
2363 return Ok(());
2364 }
2365 if state.write_buffer.in_atomic_write || !state.write_buffer.dirty.is_empty() {
2366 return Err(CommitBufferError::Other(
2367 "cannot uncork sqlite commits inside a transaction".to_string(),
2368 ));
2369 }
2370 state.write_buffer.dirty = state.corked.take().unwrap_or_default();
2371 }
2372 self.flush_dirty_pages().map(|_| ())
2373 }
2374}
2375
2376impl Drop for VfsContext {
2377 fn drop(&mut self) {
2378 self.state.write().invalidate_page_buffers();
2379 self.report_prefetch_unused();
2380 let state = self.state.read();
2381 let page_cache_entries = state
2382 .prefetch_buffer
2383 .entry_count()
2384 .saturating_add(state.committed_buffer.entry_count());
2385 let page_cache_weighted_size = state.prefetch_buffer.weighted_size();
2386 tracing::debug!(
2387 actor_id = %self.actor_id,
2388 generation = ?self.generation,
2389 resolve_pages_calls = self.resolve_pages_total.load(Ordering::Relaxed),
2390 resolve_pages_cache_hits = self.resolve_pages_cache_hits.load(Ordering::Relaxed),
2391 get_pages_round_trips = self.resolve_pages_fetches.load(Ordering::Relaxed),
2392 pages_fetched_total = self.pages_fetched_total.load(Ordering::Relaxed),
2393 prefetch_pages_total = self.prefetch_pages_total.load(Ordering::Relaxed),
2394 prefetch_consumed_total = self.prefetch_consumed_total.load(Ordering::Relaxed),
2395 commit_count = self.commit_total.load(Ordering::Relaxed),
2396 db_size_pages = state.db_size_pages,
2397 head_txid = state.head_txid,
2398 page_cache_entries,
2399 page_cache_weighted_size,
2400 page_cache_capacity_pages = self.config.page_budget.capacity_pages(),
2401 "sqlite vfs close summary"
2402 );
2403 }
2404}
2405
2406fn cleanup_batch_atomic_probe(db: *mut sqlite3) {
2407 if let Err(err) = sqlite_exec(db, "DROP TABLE IF EXISTS __rivet_batch_probe;") {
2408 tracing::warn!(%err, "failed to clean up sqlite batch atomic probe table");
2409 }
2410}
2411
2412fn assert_batch_atomic_probe(db: *mut sqlite3, vfs: &SqliteVfs) -> std::result::Result<(), String> {
2413 let commit_atomic_before = vfs.commit_atomic_count();
2414 let probe_sql = "\
2415 BEGIN IMMEDIATE;\
2416 CREATE TABLE IF NOT EXISTS __rivet_batch_probe(x INTEGER);\
2417 INSERT INTO __rivet_batch_probe VALUES(1);\
2418 DELETE FROM __rivet_batch_probe;\
2419 DROP TABLE IF EXISTS __rivet_batch_probe;\
2420 COMMIT;\
2421 ";
2422
2423 if let Err(err) = sqlite_exec(db, probe_sql) {
2424 let last_error = vfs.clone_last_error();
2425 tracing::error!(
2426 %err,
2427 last_error = ?last_error,
2428 commit_atomic_before,
2429 "sqlite batch atomic probe failed"
2430 );
2431 cleanup_batch_atomic_probe(db);
2432 if let Some(last_error) = last_error {
2433 return Err(format!(
2434 "batch atomic probe failed: {err}; vfs last_error: {last_error}"
2435 ));
2436 }
2437 return Err(format!("batch atomic probe failed: {err}"));
2438 }
2439
2440 let commit_atomic_after = vfs.commit_atomic_count();
2441 if commit_atomic_after == commit_atomic_before {
2442 tracing::error!(
2443 commit_atomic_before,
2444 commit_atomic_after,
2445 last_error = ?vfs.clone_last_error(),
2446 "batch atomic writes not active for sqlite, SQLITE_ENABLE_BATCH_ATOMIC_WRITE may be missing"
2447 );
2448 cleanup_batch_atomic_probe(db);
2449 return Err(
2450 "batch atomic writes not active for sqlite, SQLITE_ENABLE_BATCH_ATOMIC_WRITE may be missing"
2451 .to_string(),
2452 );
2453 }
2454
2455 Ok(())
2456}
2457
2458struct BatchAtomicProbeTransport;
2464
2465#[async_trait]
2466impl SqliteTransport for BatchAtomicProbeTransport {
2467 async fn get_pages(
2468 &self,
2469 request: protocol::SqliteGetPagesRequest,
2470 ) -> Result<protocol::SqliteGetPagesResponse> {
2471 Ok(protocol::SqliteGetPagesResponse::SqliteGetPagesOk(
2472 protocol::SqliteGetPagesOk {
2473 pages: request
2474 .pgnos
2475 .into_iter()
2476 .map(|pgno| protocol::SqliteFetchedPage { pgno, bytes: None })
2477 .collect(),
2478 head_txid: Some(0),
2479 },
2480 ))
2481 }
2482
2483 async fn commit(
2484 &self,
2485 _request: protocol::SqliteCommitRequest,
2486 ) -> Result<protocol::SqliteCommitResponse> {
2487 Ok(protocol::SqliteCommitResponse::SqliteCommitOk(
2488 protocol::SqliteCommitOk { head_txid: None },
2489 ))
2490 }
2491
2492 async fn commit_stage_begin(
2493 &self,
2494 _request: protocol::SqliteCommitStageBeginRequest,
2495 ) -> Result<protocol::SqliteCommitStageBeginResponse> {
2496 anyhow::bail!("batch-atomic startup probe unexpectedly attempted a staged commit")
2497 }
2498
2499 async fn commit_stage_segment(
2500 &self,
2501 _request: protocol::SqliteCommitStageSegmentRequest,
2502 ) -> Result<protocol::SqliteCommitStageSegmentResponse> {
2503 anyhow::bail!("batch-atomic startup probe unexpectedly attempted a staged commit")
2504 }
2505
2506 async fn commit_finalize(
2507 &self,
2508 _request: protocol::SqliteCommitFinalizeRequest,
2509 ) -> Result<protocol::SqliteCommitFinalizeResponse> {
2510 anyhow::bail!("batch-atomic startup probe unexpectedly attempted a staged commit")
2511 }
2512}
2513
2514fn verify_batch_atomic_writes_at_process_startup_blocking(
2515 runtime: Handle,
2516 config: VfsConfig,
2517) -> std::result::Result<(), String> {
2518 let probe_id = NEXT_TEMP_AUX_ID.fetch_add(1, Ordering::Relaxed);
2519 let file_name = format!("batch-atomic-startup-probe-{probe_id}.db");
2520 let vfs = SqliteVfs::register_with_transport_and_initial_pages(
2521 &format!("batch-atomic-startup-probe-{probe_id}"),
2522 Arc::new(BatchAtomicProbeTransport),
2523 file_name.clone(),
2524 runtime,
2525 config,
2526 InitialPages::default(),
2527 None,
2528 )?;
2529 let connection = open_connection(
2530 Arc::new(vfs),
2531 &file_name,
2532 SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
2533 )?;
2534 configure_connection_for_database(connection.as_ptr(), &connection._vfs, &file_name)
2535 .map_err(|error| error.to_string())?;
2536 assert_batch_atomic_probe(connection.as_ptr(), &connection._vfs)
2537}
2538
2539pub async fn verify_batch_atomic_writes_at_process_startup() -> Result<()> {
2542 verify_batch_atomic_writes_at_process_startup_with_config(VfsConfig::default()).await
2543}
2544
2545async fn verify_batch_atomic_writes_at_process_startup_with_config(
2546 config: VfsConfig,
2547) -> Result<()> {
2548 let runtime = Handle::try_current().map_err(|err| {
2549 anyhow::anyhow!("batch-atomic startup probe requires a Tokio runtime: {err}")
2550 })?;
2551 let result = tokio::task::spawn_blocking(move || {
2552 verify_batch_atomic_writes_at_process_startup_blocking(runtime, config)
2553 })
2554 .await
2555 .map_err(|err| anyhow::anyhow!("batch-atomic startup probe task failed: {err}"))?;
2556 result.map_err(anyhow::Error::msg)
2557}
2558
2559fn handle_non_finalize_commit_error(ctx: &VfsContext, err: &CommitBufferError) {
2560 match err {
2561 CommitBufferError::FenceMismatch(message) => ctx.mark_fatal(message.clone()),
2562 CommitBufferError::Indeterminate(operation) => {
2563 ctx.mark_indeterminate(operation.clone());
2564 }
2565 CommitBufferError::Storage(error) => ctx.set_storage_error(error.clone()),
2566 CommitBufferError::Other(message) => ctx.set_last_error(message.clone()),
2567 }
2568}
2569
2570fn handle_finalize_fence_error(ctx: &VfsContext, err: &CommitBufferError) {
2571 if let CommitBufferError::FenceMismatch(reason) = err {
2572 ctx.mark_fatal(reason.clone());
2573 }
2574}
2575
2576
2577
2578pub(crate) async fn fetch_initial_pages_for_registration(
2579 transport: SqliteTransportHandle,
2580 actor_id: &str,
2581 generation: u64,
2582 _config: &VfsConfig,
2583) -> std::result::Result<InitialPages, String> {
2584 fetch_initial_pages(transport, actor_id.to_string(), generation, 1).await
2585}
2586
2587async fn fetch_initial_pages(
2588 transport: SqliteTransportHandle,
2589 actor_id: String,
2590 generation: u64,
2591 page_count: u32,
2592) -> std::result::Result<InitialPages, String> {
2593 tracing::debug!(
2594 actor_id = %actor_id,
2595 generation,
2596 page_count,
2597 "sqlite initial page preload request"
2598 );
2599
2600 let request_actor_id = actor_id.clone();
2601 let response = transport
2602 .get_pages(protocol::SqliteGetPagesRequest {
2603 actor_id: request_actor_id.into(),
2604 pgnos: (1..=page_count).collect(),
2605 expected_generation: None,
2606 expected_head_txid: None,
2607 })
2608 .await;
2609
2610 match response {
2611 Ok(protocol::SqliteGetPagesResponse::SqliteGetPagesOk(ok)) => {
2612 let head_txid = ok.head_txid;
2613 let pages: Vec<_> = ok
2614 .pages
2615 .into_iter()
2616 .filter_map(|page| page.bytes.map(|bytes| (page.pgno, bytes.into())))
2617 .collect();
2618 tracing::debug!(
2619 actor_id = %actor_id,
2620 generation,
2621 page_count,
2622 loaded_pages = pages.len(),
2623 head_txid,
2624 "sqlite initial page preload result"
2625 );
2626 Ok(InitialPages {
2627 pages,
2628 head_txid,
2629 requested_page_count: page_count,
2630 })
2631 }
2632 Ok(protocol::SqliteGetPagesResponse::SqliteErrorResponse(error)) => {
2633 if !is_initial_main_page_missing(&error.message) {
2634 return Err(format!(
2635 "sqlite initial page fetch failed: {}",
2636 error.message
2637 ));
2638 }
2639 tracing::info!(
2640 actor_id = %actor_id,
2641 generation,
2642 page_count,
2643 error = %error.message,
2644 "sqlite initial page fetch did not find persisted data"
2645 );
2646 Ok(InitialPages {
2647 pages: Vec::new(),
2648 head_txid: Some(0),
2649 requested_page_count: page_count,
2650 })
2651 }
2652 Err(err) => Err(format!("sqlite initial page fetch failed: {err}")),
2653 }
2654}
2655
2656fn is_initial_main_page_missing(message: &str) -> bool {
2657 message.contains("sqlite database was not found in this bucket branch")
2658 || message.contains("sqlite meta missing for get_pages")
2659 || message == "actor does not exist"
2660}
2661
2662fn next_temp_aux_path() -> String {
2663 format!(
2664 "{TEMP_AUX_PATH_PREFIX}-{}",
2665 NEXT_TEMP_AUX_ID.fetch_add(1, Ordering::Relaxed)
2666 )
2667}
2668
2669unsafe fn get_aux_state(file: &VfsFile) -> Option<&AuxFileHandle> {
2670 (!file.aux.is_null()).then(|| &*file.aux)
2671}
2672
2673const COMMIT_STAGE_THRESHOLD_PAGES: usize = 320;
2684
2685async fn commit_buffered_pages(
2686 transport: &dyn SqliteTransport,
2687 request: BufferedCommitRequest,
2688 max_dirty_pages: usize,
2689) -> std::result::Result<(BufferedCommitOutcome, CommitTransportMetrics), CommitBufferError> {
2690 if request.dirty_pages.len() > max_dirty_pages {
2694 return Err(CommitBufferError::Other(format!(
2695 "commit of {} dirty pages exceeds the {} page maximum",
2696 request.dirty_pages.len(),
2697 max_dirty_pages,
2698 )));
2699 }
2700
2701 if request.dirty_pages.len() > COMMIT_STAGE_THRESHOLD_PAGES {
2702 return commit_staged_pages(transport, request).await;
2703 }
2704
2705 let mut metrics = CommitTransportMetrics::default();
2706 let serialize_start = Instant::now();
2707 let BufferedCommitRequest {
2708 actor_id,
2709 new_db_size_pages,
2710 dirty_pages,
2711 expected_head_txid,
2712 } = request;
2713 let commit_request = protocol::SqliteCommitRequest {
2714 actor_id: actor_id.into(),
2715 dirty_pages: dirty_pages
2716 .into_iter()
2717 .map(|page| protocol::SqliteDirtyPage {
2718 pgno: page.pgno,
2719 bytes: page.bytes.into_bytes(),
2720 })
2721 .collect(),
2722 db_size_pages: new_db_size_pages,
2723 now_ms: sqlite_now_ms().map_err(|err| CommitBufferError::Other(err.to_string()))?,
2724 expected_generation: None,
2725 expected_head_txid,
2726 };
2727 metrics.serialize_ns += serialize_start.elapsed().as_nanos() as u64;
2728 let transport_start = Instant::now();
2729 match transport
2730 .commit(commit_request)
2731 .await
2732 .map_err(commit_transport_error)?
2733 {
2734 protocol::SqliteCommitResponse::SqliteCommitOk(ok) => {
2735 metrics.transport_ns += transport_start.elapsed().as_nanos() as u64;
2736 Ok((
2737 BufferedCommitOutcome {
2738 path: CommitPath::Fast,
2739 db_size_pages: new_db_size_pages,
2740 head_txid: ok.head_txid,
2741 },
2742 metrics,
2743 ))
2744 }
2745 protocol::SqliteCommitResponse::SqliteErrorResponse(error) => {
2746 if is_head_fence_mismatch_response(&error) {
2747 Err(CommitBufferError::FenceMismatch(error.message.to_string()))
2748 } else if let Some(refusal) = storage_refusal(&error) {
2749 Err(CommitBufferError::Storage(refusal))
2750 } else {
2751 Err(CommitBufferError::Other(error.message.to_string()))
2752 }
2753 }
2754 }
2755}
2756
2757async fn commit_staged_pages(
2764 transport: &dyn SqliteTransport,
2765 request: BufferedCommitRequest,
2766) -> std::result::Result<(BufferedCommitOutcome, CommitTransportMetrics), CommitBufferError> {
2767 let mut metrics = CommitTransportMetrics::default();
2768 let serialize_start = Instant::now();
2769 let mut dirty_pages = request.dirty_pages;
2770 dirty_pages.sort_by_key(|page| page.pgno);
2773 metrics.serialize_ns += serialize_start.elapsed().as_nanos() as u64;
2774
2775 let transport_start = Instant::now();
2776 let txid = match transport
2777 .commit_stage_begin(protocol::SqliteCommitStageBeginRequest {
2778 actor_id: request.actor_id.clone().into(),
2779 expected_generation: None,
2780 expected_head_txid: request.expected_head_txid,
2781 })
2782 .await
2783 .map_err(commit_transport_error)?
2784 {
2785 protocol::SqliteCommitStageBeginResponse::SqliteCommitStageBeginOk(ok) => ok.txid,
2786 protocol::SqliteCommitStageBeginResponse::SqliteErrorResponse(error) => {
2787 return Err(staged_commit_error(error));
2788 }
2789 };
2790
2791 let segments = sqlite_types::cut_page_segments(&dirty_pages, |page| page.pgno);
2792 let segment_first_pgnos = segments.iter().map(|(first_pgno, _)| *first_pgno).collect();
2793 let responses = stream::iter(segments)
2796 .map(|(first_pgno, segment)| {
2797 let dirty_pages = segment
2798 .iter()
2799 .map(|page| protocol::SqliteDirtyPage {
2800 pgno: page.pgno,
2801 bytes: page.bytes.clone().into_bytes(),
2802 })
2803 .collect();
2804 let actor_id = request.actor_id.clone();
2805 async move {
2806 match transport
2807 .commit_stage_segment(protocol::SqliteCommitStageSegmentRequest {
2808 actor_id: actor_id.into(),
2809 expected_generation: None,
2810 txid,
2811 first_pgno,
2812 dirty_pages,
2813 })
2814 .await
2815 .map_err(commit_transport_error)?
2816 {
2817 protocol::SqliteCommitStageSegmentResponse::SqliteCommitStageSegmentOk(_) => {
2818 Ok(())
2819 }
2820 protocol::SqliteCommitStageSegmentResponse::SqliteErrorResponse(error) => {
2821 Err(staged_commit_error(error))
2822 }
2823 }
2824 }
2825 })
2826 .buffered(STAGE_PIPELINE_DEPTH)
2827 .collect::<Vec<_>>()
2828 .await;
2829 for response in responses {
2830 response?;
2831 }
2832
2833 let head_txid = match transport
2834 .commit_finalize(protocol::SqliteCommitFinalizeRequest {
2835 actor_id: request.actor_id.clone().into(),
2836 expected_generation: None,
2837 txid,
2838 new_db_size_pages: request.new_db_size_pages,
2839 now_ms: sqlite_now_ms().map_err(|err| CommitBufferError::Other(err.to_string()))?,
2840 segment_first_pgnos,
2841 })
2842 .await
2843 .map_err(commit_transport_error)?
2844 {
2845 protocol::SqliteCommitFinalizeResponse::SqliteCommitFinalizeOk(ok) => ok.head_txid,
2846 protocol::SqliteCommitFinalizeResponse::SqliteErrorResponse(error) => {
2847 return Err(staged_commit_error(error));
2848 }
2849 };
2850 metrics.transport_ns += transport_start.elapsed().as_nanos() as u64;
2851
2852 Ok((
2853 BufferedCommitOutcome {
2854 path: CommitPath::Staged,
2855 db_size_pages: request.new_db_size_pages,
2856 head_txid,
2857 },
2858 metrics,
2859 ))
2860}
2861
2862fn commit_transport_error(error: anyhow::Error) -> CommitBufferError {
2863 if let Some(timeout) = storage_timeout(&error) {
2864 return CommitBufferError::Storage(timeout);
2865 }
2866 match error.downcast::<SqliteCommitIndeterminateError>() {
2867 Ok(error) => CommitBufferError::Indeterminate(error.0),
2868 Err(error) => CommitBufferError::Other(error.to_string()),
2869 }
2870}
2871
2872fn staged_commit_error(error: protocol::SqliteErrorResponse) -> CommitBufferError {
2873 if is_head_fence_mismatch_response(&error) {
2874 CommitBufferError::FenceMismatch(error.message.to_string())
2875 } else if let Some(refusal) = storage_refusal(&error) {
2876 CommitBufferError::Storage(refusal)
2877 } else {
2878 CommitBufferError::Other(error.message.to_string())
2879 }
2880}
2881
2882fn is_head_fence_mismatch_response(error: &protocol::SqliteErrorResponse) -> bool {
2883 is_head_fence_mismatch(&error.group, &error.code)
2884}
2885
2886unsafe fn get_file(p: *mut sqlite3_file) -> &'static mut VfsFile {
2887 &mut *(p as *mut VfsFile)
2888}
2889
2890unsafe fn get_vfs_ctx(p: *mut sqlite3_vfs) -> &'static VfsContext {
2891 &*((*p).pAppData as *const VfsContext)
2892}
2893
2894fn sqlite_error_message(db: *mut sqlite3) -> String {
2895 unsafe {
2896 if db.is_null() {
2897 "unknown sqlite error".to_string()
2898 } else {
2899 CStr::from_ptr(sqlite3_errmsg(db))
2900 .to_string_lossy()
2901 .into_owned()
2902 }
2903 }
2904}
2905
2906pub(crate) fn sqlite_exec(db: *mut sqlite3, sql: &str) -> std::result::Result<(), String> {
2907 let c_sql = CString::new(sql).map_err(|err| err.to_string())?;
2908 let rc = unsafe { sqlite3_exec(db, c_sql.as_ptr(), None, ptr::null_mut(), ptr::null_mut()) };
2909 if rc != SQLITE_OK {
2910 return Err(format!(
2911 "`{sql}` failed with code {rc}: {}",
2912 sqlite_error_message(db)
2913 ));
2914 }
2915 Ok(())
2916}
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936fn page_span(offset: i64, length: usize, page_size: usize) -> std::result::Result<Vec<u32>, ()> {
2937 if offset < 0 {
2938 return Err(());
2939 }
2940 if length == 0 {
2941 return Ok(Vec::new());
2942 }
2943
2944 let start = offset as usize / page_size + 1;
2945 let end = (offset as usize + length - 1) / page_size + 1;
2946 Ok((start as u32..=end as u32).collect())
2947}
2948
2949unsafe extern "C" fn io_close(p_file: *mut sqlite3_file) -> c_int {
2950 vfs_catch_unwind!(SQLITE_IOERR, {
2951 if p_file.is_null() {
2952 return SQLITE_OK;
2953 }
2954 let file = get_file(p_file);
2955 let result = if !file.aux.is_null() {
2956 let aux = Box::from_raw(file.aux);
2957 if aux.delete_on_close {
2958 let ctx = &*file.ctx;
2959 ctx.delete_aux_file(&aux.path);
2960 }
2961 file.aux = ptr::null_mut();
2962 Ok(())
2963 } else {
2964 let ctx = &*file.ctx;
2965
2966 ctx.discard_refused_transaction();
2967 let should_flush = {
2968 let state = ctx.state.read();
2969 state.write_buffer.in_atomic_write
2970 || !state.write_buffer.dirty.is_empty()
2971 || state.db_size_pages != state.committed_db_size_pages
2972 || state.corked.is_some()
2973 };
2974 if should_flush {
2975 if ctx.state.read().write_buffer.in_atomic_write {
2976 ctx.commit_atomic_write().map(|_| ())
2977 } else {
2978 ctx.uncork()
2979 .and_then(|()| ctx.flush_dirty_pages().map(|_| ()))
2980 }
2981 } else {
2982 Ok(())
2983 }
2984 };
2985 file.base.pMethods = ptr::null();
2986 match result {
2987 Ok(()) => SQLITE_OK,
2988 Err(err) => {
2989 let ctx = &*file.ctx;
2990 handle_finalize_fence_error(ctx, &err);
2991 SQLITE_IOERR
2992 }
2993 }
2994 })
2995}
2996
2997unsafe extern "C" fn io_read(
2998 p_file: *mut sqlite3_file,
2999 p_buf: *mut c_void,
3000 i_amt: c_int,
3001 i_offset: sqlite3_int64,
3002) -> c_int {
3003 vfs_catch_unwind!(SQLITE_IOERR_READ, {
3004 if i_amt <= 0 {
3005 return SQLITE_OK;
3006 }
3007
3008 let file = get_file(p_file);
3009 if let Some(aux) = get_aux_state(file) {
3010 if i_offset < 0 {
3011 return SQLITE_IOERR_READ;
3012 }
3013
3014 let offset = i_offset as usize;
3015 let requested = i_amt as usize;
3016 let buf = slice::from_raw_parts_mut(p_buf.cast::<u8>(), requested);
3017 buf.fill(0);
3018
3019 let bytes = aux.state.bytes.lock();
3020 if offset >= bytes.len() {
3021 return SQLITE_IOERR_SHORT_READ;
3022 }
3023
3024 let copy_len = requested.min(bytes.len() - offset);
3025 buf[..copy_len].copy_from_slice(&bytes[offset..offset + copy_len]);
3026 return if copy_len < requested {
3027 SQLITE_IOERR_SHORT_READ
3028 } else {
3029 SQLITE_OK
3030 };
3031 }
3032
3033 let ctx = &*file.ctx;
3034 if ctx.is_dead() {
3035 return SQLITE_IOERR_READ;
3036 }
3037
3038 let buf = slice::from_raw_parts_mut(p_buf.cast::<u8>(), i_amt as usize);
3039 let requested_pages = match page_span(i_offset, i_amt as usize, ctx.page_size()) {
3040 Ok(pages) => pages,
3041 Err(_) => return SQLITE_IOERR_READ,
3042 };
3043 let page_size = ctx.page_size();
3044 let (file_size, db_size_pages) = {
3045 let state = ctx.state.read();
3046 (
3047 state.db_size_pages as usize * state.page_size,
3048 state.db_size_pages,
3049 )
3050 };
3051
3052 let partial = i_offset as usize % page_size != 0 || i_amt as usize % page_size != 0;
3053 let resolved = match ctx.resolve_pages(&requested_pages, partial) {
3054 Ok(pages) => pages,
3055 Err(GetPagesError::FenceMismatch(message)) => {
3056 tracing::error!(
3057 actor_id = %ctx.actor_id,
3058 requested_pages = ?requested_pages,
3059 error = %message,
3060 "sqlite xRead hit fatal sqlite error"
3061 );
3062 ctx.mark_fatal(message);
3063 return SQLITE_IOERR_READ;
3064 }
3065 Err(GetPagesError::Storage(error)) => {
3066 tracing::warn!(
3067 actor_id = %ctx.actor_id,
3068 requested_pages = ?requested_pages,
3069 %error,
3070 "sqlite xRead failed without changing storage; the statement fails with a typed storage error"
3071 );
3072 ctx.set_storage_error(error);
3073 return SQLITE_IOERR_READ;
3074 }
3075 Err(GetPagesError::Other(message)) => {
3076 tracing::error!(
3077 actor_id = %ctx.actor_id,
3078 requested_pages = ?requested_pages,
3079 error = %message,
3080 "sqlite xRead failed to resolve pages"
3081 );
3082 ctx.set_last_error(message);
3083 return SQLITE_IOERR_READ;
3084 }
3085 };
3086 ctx.clear_last_error();
3087
3088 #[cfg(debug_assertions)]
3089 {
3090 let missing_in_range_pages = requested_pages
3091 .iter()
3092 .copied()
3093 .filter(|pgno| *pgno <= db_size_pages)
3094 .filter(|pgno| !matches!(resolved.get(pgno), Some(Some(_))))
3095 .collect::<Vec<_>>();
3096 if !missing_in_range_pages.is_empty() {
3097 tracing::warn!(
3098 actor_id = %ctx.actor_id,
3099 offset = i_offset,
3100 amount = i_amt,
3101 page_size,
3102 db_size_pages,
3103 file_size,
3104 requested_pages = ?requested_pages,
3105 missing_in_range_pages = ?missing_in_range_pages,
3106 "sqlite xRead would zero-fill pages within declared db size"
3107 );
3108 }
3109 }
3110
3111 buf.fill(0);
3112 for pgno in requested_pages.iter().copied() {
3113 let Some(Some(bytes)) = resolved.get(&pgno) else {
3114 continue;
3115 };
3116 let page_start = (pgno as usize - 1) * page_size;
3117 let copy_start = page_start.max(i_offset as usize);
3118 let copy_end = (page_start + page_size).min(i_offset as usize + i_amt as usize);
3119 if copy_start >= copy_end {
3120 continue;
3121 }
3122 let page_offset = copy_start - page_start;
3123 let dest_offset = copy_start - i_offset as usize;
3124 let copy_len = copy_end - copy_start;
3125 buf[dest_offset..dest_offset + copy_len]
3126 .copy_from_slice(&bytes[page_offset..page_offset + copy_len]);
3127 }
3128
3129 if i_offset as usize + i_amt as usize > file_size {
3130 return SQLITE_IOERR_SHORT_READ;
3131 }
3132
3133 SQLITE_OK
3134 })
3135}
3136
3137fn journal_records_added(offset: usize, len: usize, file_len: usize, page_size: usize) -> usize {
3146 if len == page_size {
3147 return usize::from(offset % DEFAULT_PAGE_SIZE != 0);
3148 }
3149 if len == 4 {
3150 return 0;
3151 }
3152 let appended = (offset + len).saturating_sub(file_len.max(offset));
3153 appended.div_ceil(page_size + 8)
3154}
3155
3156unsafe extern "C" fn io_write(
3157 p_file: *mut sqlite3_file,
3158 p_buf: *const c_void,
3159 i_amt: c_int,
3160 i_offset: sqlite3_int64,
3161) -> c_int {
3162 vfs_catch_unwind!(SQLITE_IOERR_WRITE, {
3163 if i_amt <= 0 {
3164 return SQLITE_OK;
3165 }
3166
3167 let file = get_file(p_file);
3168 if let Some(aux) = get_aux_state(file) {
3169 if i_offset < 0 {
3170 return SQLITE_IOERR_WRITE;
3171 }
3172
3173 let offset = i_offset as usize;
3174 let source = slice::from_raw_parts(p_buf.cast::<u8>(), i_amt as usize);
3175 let end = offset + source.len();
3176 let mut bytes = aux.state.bytes.lock();
3177 if aux.is_main_journal {
3178 let ctx = &*file.ctx;
3179 let added =
3180 journal_records_added(offset, source.len(), bytes.len(), ctx.page_size());
3181 let records = aux.state.journal_records.load(Ordering::Relaxed);
3182 if added > 0 && records + added > ctx.config.commit_page_cap() {
3183 drop(bytes);
3184 return ctx.refuse_oversize_transaction();
3185 }
3186 aux.state
3187 .journal_records
3188 .store(records + added, Ordering::Relaxed);
3189 }
3190 if bytes.len() < end {
3191 bytes.resize(end, 0);
3192 }
3193 bytes[offset..end].copy_from_slice(source);
3194 return SQLITE_OK;
3195 }
3196
3197 let ctx = &*file.ctx;
3198 if ctx.is_dead() {
3199 return SQLITE_IOERR_WRITE;
3200 }
3201
3202 let page_size = ctx.page_size();
3203 let source = slice::from_raw_parts(p_buf.cast::<u8>(), i_amt as usize);
3204 let target_pages = match page_span(i_offset, i_amt as usize, page_size) {
3205 Ok(pages) => pages,
3206 Err(_) => return SQLITE_IOERR_WRITE,
3207 };
3208
3209 {
3213 let state = ctx.state.read();
3214 let new_pages = target_pages
3215 .iter()
3216 .filter(|pgno| !state.write_buffer.dirty.contains_key(pgno))
3217 .count();
3218 if state.write_buffer.dirty.len() + new_pages > ctx.config.commit_page_cap() {
3219 drop(state);
3220 ctx.state.write().refused_write_pending = true;
3221 return ctx.refuse_oversize_transaction();
3222 }
3223 }
3224
3225 let offset = i_offset as usize;
3230 let amt = i_amt as usize;
3231 let is_aligned_full_page = offset % page_size == 0 && amt % page_size == 0;
3232
3233 let mut resolved = HashMap::new();
3234 if !is_aligned_full_page {
3235 let (db_size_pages, pages_to_resolve): (u32, Vec<u32>) = {
3236 let state = ctx.state.read();
3237 let known_max = state.db_size_pages;
3238 (
3239 known_max,
3240 target_pages
3241 .iter()
3242 .copied()
3243 .filter(|pgno| {
3244 *pgno <= known_max
3245 && !state.write_buffer.dirty.contains_key(pgno)
3246 && state.corked_page(*pgno).is_none()
3247 })
3248 .collect(),
3249 )
3250 };
3251
3252 if !pages_to_resolve.is_empty() {
3253 resolved = match ctx.resolve_pages(&pages_to_resolve, false) {
3254 Ok(pages) => pages,
3255 Err(GetPagesError::FenceMismatch(message)) => {
3256 ctx.mark_fatal(message);
3257 return SQLITE_IOERR_WRITE;
3258 }
3259 Err(GetPagesError::Storage(error)) => {
3260 ctx.set_storage_error(error);
3261 return SQLITE_IOERR_WRITE;
3262 }
3263 Err(GetPagesError::Other(message)) => {
3264 ctx.set_last_error(message);
3265 return SQLITE_IOERR_WRITE;
3266 }
3267 };
3268 }
3269 for pgno in &target_pages {
3270 if *pgno > db_size_pages {
3271 resolved.entry(*pgno).or_insert(None);
3272 }
3273 }
3274 #[cfg(debug_assertions)]
3275 {
3276 let missing_existing_pages = target_pages
3277 .iter()
3278 .copied()
3279 .filter(|pgno| *pgno <= db_size_pages)
3280 .filter(|pgno| pages_to_resolve.contains(pgno))
3281 .filter(|pgno| !matches!(resolved.get(pgno), Some(Some(_))))
3282 .collect::<Vec<_>>();
3283 if !missing_existing_pages.is_empty() {
3284 tracing::warn!(
3285 actor_id = %ctx.actor_id,
3286 offset = i_offset,
3287 amount = i_amt,
3288 page_size,
3289 db_size_pages,
3290 target_pages = ?target_pages,
3291 missing_existing_pages = ?missing_existing_pages,
3292 "sqlite xWrite partial update would synthesize existing pages from zeros"
3293 );
3294 }
3295 }
3296 }
3297
3298 let mut state = ctx.state.write();
3299 for pgno in target_pages {
3300 let page_start = (pgno as usize - 1) * page_size;
3301 let patch_start = page_start.max(offset);
3302 let patch_end = (page_start + page_size).min(offset + amt);
3303 let Some(copy_len) = patch_end.checked_sub(patch_start) else {
3304 continue;
3305 };
3306 if copy_len == 0 {
3307 continue;
3308 }
3309
3310 let page_offset = patch_start - page_start;
3311 let source_offset = patch_start - offset;
3312 if is_aligned_full_page {
3313 state.write_buffer.dirty.insert(
3314 pgno,
3315 source[source_offset..source_offset + copy_len]
3316 .to_vec()
3317 .into(),
3318 );
3319 continue;
3320 }
3321
3322 let mut page = state
3323 .write_buffer
3324 .dirty
3325 .get_mut(&pgno)
3326 .map(std::mem::take)
3327 .map(PageBytes::into_vec)
3328 .unwrap_or_else(|| {
3329 resolved
3330 .remove(&pgno)
3331 .flatten()
3332 .map(PageBytes::into_vec)
3333 .unwrap_or_else(|| vec![0; page_size])
3334 });
3335 if page.len() < page_size {
3336 page.resize(page_size, 0);
3337 }
3338 page[page_offset..page_offset + copy_len]
3339 .copy_from_slice(&source[source_offset..source_offset + copy_len]);
3340 state.write_buffer.dirty.insert(pgno, page.into());
3341 }
3342
3343 let end_page = ((offset + amt) + page_size - 1) / page_size;
3344 state.db_size_pages = state.db_size_pages.max(end_page as u32);
3345
3346 ctx.clear_last_error();
3347 SQLITE_OK
3348 })
3349}
3350
3351unsafe extern "C" fn io_truncate(p_file: *mut sqlite3_file, size: sqlite3_int64) -> c_int {
3352 vfs_catch_unwind!(SQLITE_IOERR_TRUNCATE, {
3353 if size < 0 {
3354 return SQLITE_IOERR_TRUNCATE;
3355 }
3356 let file = get_file(p_file);
3357 if let Some(aux) = get_aux_state(file) {
3358 let mut bytes = aux.state.bytes.lock();
3359 bytes.truncate(size as usize);
3360 bytes.shrink_to_fit();
3361 if size == 0 {
3362 aux.state.journal_records.store(0, Ordering::Relaxed);
3363 }
3364 return SQLITE_OK;
3365 }
3366 let ctx = &*file.ctx;
3367 if !ctx.truncate_main_file(size) {
3368 return SQLITE_OK;
3369 }
3370 match ctx.flush_dirty_pages() {
3371 Ok(_) => SQLITE_OK,
3372 Err(err) => {
3373 tracing::error!(
3374 actor_id = %ctx.actor_id,
3375 last_error = ?ctx.clone_last_error(),
3376 ?err,
3377 "sqlite truncate commit failed"
3378 );
3379 handle_finalize_fence_error(ctx, &err);
3380 ctx.abandon_write_buffer();
3381 SQLITE_IOERR_TRUNCATE
3382 }
3383 }
3384 })
3385}
3386
3387unsafe extern "C" fn io_sync(p_file: *mut sqlite3_file, _flags: c_int) -> c_int {
3391 vfs_catch_unwind!(SQLITE_IOERR_FSYNC, {
3392 let file = get_file(p_file);
3393 if get_aux_state(file).is_some() {
3394 return SQLITE_OK;
3395 }
3396 let ctx = &*file.ctx;
3397 if let Some(message) = ctx.take_transient_commit_error() {
3398 ctx.set_last_error(message);
3399 ctx.abandon_write_buffer();
3400 return SQLITE_IOERR_FSYNC;
3401 }
3402 match ctx.flush_dirty_pages() {
3403 Ok(_) => SQLITE_OK,
3404 Err(err) => {
3405 tracing::error!(
3406 actor_id = %ctx.actor_id,
3407 last_error = ?ctx.clone_last_error(),
3408 ?err,
3409 "sqlite sync failed"
3410 );
3411 handle_finalize_fence_error(ctx, &err);
3412 ctx.abandon_write_buffer();
3413 SQLITE_IOERR_FSYNC
3414 }
3415 }
3416 })
3417}
3418
3419unsafe extern "C" fn io_file_size(p_file: *mut sqlite3_file, p_size: *mut sqlite3_int64) -> c_int {
3420 vfs_catch_unwind!(SQLITE_IOERR_FSTAT, {
3421 let file = get_file(p_file);
3422 if let Some(aux) = get_aux_state(file) {
3423 *p_size = aux.state.bytes.lock().len() as sqlite3_int64;
3424 return SQLITE_OK;
3425 }
3426 let ctx = &*file.ctx;
3427 let state = ctx.state.read();
3428 *p_size = (state.db_size_pages as usize * state.page_size) as sqlite3_int64;
3429 SQLITE_OK
3430 })
3431}
3432
3433unsafe extern "C" fn io_lock(_p_file: *mut sqlite3_file, _level: c_int) -> c_int {
3440 vfs_catch_unwind!(SQLITE_IOERR_LOCK, SQLITE_OK)
3441}
3442
3443unsafe extern "C" fn io_unlock(_p_file: *mut sqlite3_file, _level: c_int) -> c_int {
3444 vfs_catch_unwind!(SQLITE_IOERR_UNLOCK, SQLITE_OK)
3445}
3446
3447unsafe extern "C" fn io_check_reserved_lock(
3448 _p_file: *mut sqlite3_file,
3449 p_res_out: *mut c_int,
3450) -> c_int {
3451 vfs_catch_unwind!(SQLITE_IOERR, {
3452 *p_res_out = 0;
3453 SQLITE_OK
3454 })
3455}
3456
3457unsafe extern "C" fn io_file_control(
3458 p_file: *mut sqlite3_file,
3459 op: c_int,
3460 p_arg: *mut c_void,
3461) -> c_int {
3462 vfs_catch_unwind!(SQLITE_IOERR, {
3463 let file = get_file(p_file);
3464 if get_aux_state(file).is_some() {
3465 return SQLITE_NOTFOUND;
3466 }
3467 let ctx = &*file.ctx;
3468
3469 match op {
3470 SQLITE_FCNTL_BEGIN_ATOMIC_WRITE => {
3471 let mut state = ctx.state.write();
3472 state.write_buffer.in_atomic_write = true;
3473 state.write_buffer.saved_db_size = state.db_size_pages;
3474 state.write_buffer.dirty.clear();
3475 state.refused_write_pending = false;
3476 SQLITE_OK
3477 }
3478 SQLITE_FCNTL_COMMIT_ATOMIC_WRITE => match ctx.commit_atomic_write() {
3479 Ok(()) => {
3480 ctx.commit_atomic_count.fetch_add(1, Ordering::Relaxed);
3481 SQLITE_OK
3482 }
3483 Err(err) => {
3484 tracing::error!(
3485 actor_id = %ctx.actor_id,
3486 last_error = ?ctx.clone_last_error(),
3487 ?err,
3488 "sqlite atomic write file control failed"
3489 );
3490 match &err {
3491 CommitBufferError::Other(message) => {
3492 ctx.defer_transient_commit_error(message.clone());
3493 }
3494 CommitBufferError::Storage(error) => {
3495 ctx.defer_transient_commit_error(error.to_string());
3496 }
3497 CommitBufferError::FenceMismatch(_)
3498 | CommitBufferError::Indeterminate(_) => {}
3499 }
3500 handle_finalize_fence_error(ctx, &err);
3501 SQLITE_IOERR
3502 }
3503 },
3504 SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE => {
3505 let mut state = ctx.state.write();
3506 state.write_buffer.dirty.clear();
3507 state.write_buffer.in_atomic_write = false;
3508 state.db_size_pages = state.write_buffer.saved_db_size;
3509 state.refused_write_pending = false;
3510 SQLITE_OK
3511 }
3512 SQLITE_FCNTL_OVERWRITE => {
3515 if p_arg.is_null() {
3516 return SQLITE_NOTFOUND;
3517 }
3518 let bytes = *(p_arg as *const i64);
3519 let pages = (bytes.max(0) as usize).div_ceil(ctx.page_size());
3520 if pages > ctx.config.commit_page_cap() {
3521 ctx.refuse_oversize_transaction()
3522 } else {
3523 SQLITE_NOTFOUND
3524 }
3525 }
3526 FCNTL_COMMIT_PAGE_CAP => {
3527 if p_arg.is_null() {
3528 return SQLITE_MISUSE;
3529 }
3530 *(p_arg as *mut i64) = ctx.config.commit_page_cap() as i64;
3531 SQLITE_OK
3532 }
3533 _ => SQLITE_NOTFOUND,
3534 }
3535 })
3536}
3537
3538unsafe extern "C" fn io_sector_size(_p_file: *mut sqlite3_file) -> c_int {
3539 vfs_catch_unwind!(DEFAULT_PAGE_SIZE as c_int, DEFAULT_PAGE_SIZE as c_int)
3540}
3541
3542unsafe extern "C" fn io_device_characteristics(p_file: *mut sqlite3_file) -> c_int {
3543 vfs_catch_unwind!(0, {
3544 let file = get_file(p_file);
3545 if get_aux_state(file).is_some() {
3546 0
3547 } else {
3548
3549 SQLITE_IOCAP_BATCH_ATOMIC
3550 }
3551 })
3552}
3553
3554unsafe extern "C" fn vfs_open(
3555 p_vfs: *mut sqlite3_vfs,
3556 z_name: *const c_char,
3557 p_file: *mut sqlite3_file,
3558 flags: c_int,
3559 p_out_flags: *mut c_int,
3560) -> c_int {
3561 vfs_catch_unwind!(SQLITE_CANTOPEN, {
3562 let ctx = get_vfs_ctx(p_vfs);
3563 let delete_on_close = (flags & SQLITE_OPEN_DELETEONCLOSE) != 0;
3564 let path = if z_name.is_null() {
3565 if delete_on_close {
3566 next_temp_aux_path()
3567 } else {
3568 return SQLITE_CANTOPEN;
3569 }
3570 } else {
3571 match CStr::from_ptr(z_name).to_str() {
3572 Ok(path) => path.to_string(),
3573 Err(_) => return SQLITE_CANTOPEN,
3574 }
3575 };
3576 let is_main =
3577 path == ctx.actor_id && !delete_on_close && (flags & SQLITE_OPEN_MAIN_DB) != 0;
3578
3579
3580
3581 let base = sqlite3_file {
3582 pMethods: ctx.io_methods.as_ref(),
3583 };
3584
3585 let aux = if is_main {
3586 ptr::null_mut()
3587 } else {
3588 Box::into_raw(Box::new(AuxFileHandle {
3589 is_main_journal: (flags & SQLITE_OPEN_MAIN_JOURNAL) != 0
3590 && path.strip_suffix("-journal") == Some(ctx.actor_id.as_str()),
3591 path: path.clone(),
3592 state: ctx.open_aux_file(&path),
3593 delete_on_close,
3594 }))
3595 };
3596 ptr::write(
3597 p_file.cast::<VfsFile>(),
3598 VfsFile {
3599 base,
3600 ctx: ctx as *const VfsContext,
3601 aux,
3602 },
3603 );
3604
3605 if !p_out_flags.is_null() {
3606 *p_out_flags = flags;
3607 }
3608
3609 SQLITE_OK
3610 })
3611}
3612
3613unsafe extern "C" fn vfs_delete(
3614 p_vfs: *mut sqlite3_vfs,
3615 z_name: *const c_char,
3616 _sync_dir: c_int,
3617) -> c_int {
3618 vfs_catch_unwind!(SQLITE_IOERR_DELETE, {
3619 if z_name.is_null() {
3620 return SQLITE_OK;
3621 }
3622
3623 let ctx = get_vfs_ctx(p_vfs);
3624 let path = match CStr::from_ptr(z_name).to_str() {
3625 Ok(path) => path,
3626 Err(_) => return SQLITE_OK,
3627 };
3628 if path == ctx.actor_id {
3629 ctx.set_last_error("main database deletion is unsupported".to_string());
3631 return SQLITE_IOERR_DELETE;
3632 } else {
3633
3634 ctx.delete_aux_file(path);
3635 }
3636 SQLITE_OK
3637 })
3638}
3639
3640unsafe extern "C" fn vfs_access(
3641 p_vfs: *mut sqlite3_vfs,
3642 z_name: *const c_char,
3643 _flags: c_int,
3644 p_res_out: *mut c_int,
3645) -> c_int {
3646 vfs_catch_unwind!(SQLITE_IOERR_ACCESS, {
3647 if z_name.is_null() {
3648 *p_res_out = 0;
3649 return SQLITE_OK;
3650 }
3651
3652 let ctx = get_vfs_ctx(p_vfs);
3653 let path = match CStr::from_ptr(z_name).to_str() {
3654 Ok(path) => path,
3655 Err(_) => {
3656 *p_res_out = 0;
3657 return SQLITE_OK;
3658 }
3659 };
3660
3661 *p_res_out = if path == ctx.actor_id || ctx.aux_file_exists(path) {
3662 1
3663 } else {
3664 0
3665 };
3666 SQLITE_OK
3667 })
3668}
3669
3670unsafe extern "C" fn vfs_full_pathname(
3671 _p_vfs: *mut sqlite3_vfs,
3672 z_name: *const c_char,
3673 n_out: c_int,
3674 z_out: *mut c_char,
3675) -> c_int {
3676 vfs_catch_unwind!(SQLITE_IOERR, {
3677 if z_name.is_null() || z_out.is_null() || n_out <= 0 {
3678 return SQLITE_IOERR;
3679 }
3680
3681 let name = CStr::from_ptr(z_name);
3682 let bytes = name.to_bytes_with_nul();
3683 if bytes.len() >= n_out as usize {
3684 return SQLITE_IOERR;
3685 }
3686
3687 ptr::copy_nonoverlapping(bytes.as_ptr().cast::<c_char>(), z_out, bytes.len());
3688 SQLITE_OK
3689 })
3690}
3691
3692unsafe extern "C" fn vfs_randomness(
3693 _p_vfs: *mut sqlite3_vfs,
3694 n_byte: c_int,
3695 z_out: *mut c_char,
3696) -> c_int {
3697 vfs_catch_unwind!(0, {
3698 let buf = slice::from_raw_parts_mut(z_out.cast::<u8>(), n_byte as usize);
3699 match getrandom::getrandom(buf) {
3700 Ok(()) => n_byte,
3701 Err(_) => 0,
3702 }
3703 })
3704}
3705
3706unsafe extern "C" fn vfs_sleep(_p_vfs: *mut sqlite3_vfs, microseconds: c_int) -> c_int {
3707 vfs_catch_unwind!(0, {
3708 std::thread::sleep(std::time::Duration::from_micros(microseconds as u64));
3709 microseconds
3710 })
3711}
3712
3713unsafe extern "C" fn vfs_current_time(_p_vfs: *mut sqlite3_vfs, p_time_out: *mut f64) -> c_int {
3714 vfs_catch_unwind!(SQLITE_IOERR, {
3715 let now = std::time::SystemTime::now()
3716 .duration_since(std::time::UNIX_EPOCH)
3717 .unwrap_or_default();
3718 *p_time_out = 2440587.5 + (now.as_secs_f64() / 86400.0);
3719 SQLITE_OK
3720 })
3721}
3722
3723unsafe extern "C" fn vfs_get_last_error(
3724 p_vfs: *mut sqlite3_vfs,
3725 n_byte: c_int,
3726 z_err_msg: *mut c_char,
3727) -> c_int {
3728 vfs_catch_unwind!(SQLITE_IOERR, {
3729 if n_byte <= 0 || z_err_msg.is_null() {
3730 return 0;
3731 }
3732
3733 let ctx = get_vfs_ctx(p_vfs);
3734 let Some(message) = ctx.clone_last_error() else {
3735 *z_err_msg = 0;
3736 return 0;
3737 };
3738
3739 let bytes = message.as_bytes();
3740 let max_len = (n_byte as usize).saturating_sub(1);
3741 let copy_len = bytes.len().min(max_len);
3742 let dst = z_err_msg.cast::<u8>();
3743 ptr::copy_nonoverlapping(bytes.as_ptr(), dst, copy_len);
3744 *dst.add(copy_len) = 0;
3745 0
3746 })
3747}
3748
3749impl SqliteVfs {
3750 pub(crate) fn take_last_error(&self) -> Option<String> {
3751 self.ctx.take_last_error()
3752 }
3753
3754 pub(crate) fn take_indeterminate_result(&self) -> Option<String> {
3755 self.ctx.take_indeterminate_result()
3756 }
3757
3758 pub(crate) fn take_transaction_too_large(&self) -> Option<SqliteTransactionTooLargeError> {
3759 self.ctx.take_transaction_too_large()
3760 }
3761
3762 pub(crate) fn take_storage_error(&self) -> Option<SqliteStorageError> {
3763 self.ctx.take_storage_error()
3764 }
3765
3766 fn clone_last_error(&self) -> Option<String> {
3767 self.ctx.clone_last_error()
3768 }
3769
3770 pub fn clone_fatal_error(&self) -> Option<String> {
3771 self.ctx.clone_fatal_error()
3772 }
3773
3774 pub(crate) fn read_aux_file(&self, path: &str) -> Option<Vec<u8>> {
3778 self.ctx.read_aux_file(path)
3779 }
3780
3781 pub(crate) fn delete_aux_file(&self, path: &str) {
3782 self.ctx.delete_aux_file(path);
3783 }
3784
3785 pub(crate) fn sqlite_vfs_metrics(&self) -> SqliteVfsMetricsSnapshot {
3786 self.ctx.sqlite_vfs_metrics()
3787 }
3788
3789
3790
3791
3792
3793 pub(crate) fn register_with_transport_and_initial_pages(
3794 name: &str,
3795 transport: SqliteTransportHandle,
3796 actor_id: String,
3797 runtime: Handle,
3798 config: VfsConfig,
3799 initial_pages: InitialPages,
3800 metrics: Option<Arc<dyn SqliteVfsMetrics>>,
3801 ) -> std::result::Result<Self, String> {
3802 let mut io_methods: sqlite3_io_methods = unsafe { std::mem::zeroed() };
3803 io_methods.iVersion = 1;
3804 io_methods.xClose = Some(io_close);
3805 io_methods.xRead = Some(io_read);
3806 io_methods.xWrite = Some(io_write);
3807 io_methods.xTruncate = Some(io_truncate);
3808 io_methods.xSync = Some(io_sync);
3809 io_methods.xFileSize = Some(io_file_size);
3810 io_methods.xLock = Some(io_lock);
3811 io_methods.xUnlock = Some(io_unlock);
3812 io_methods.xCheckReservedLock = Some(io_check_reserved_lock);
3813 io_methods.xFileControl = Some(io_file_control);
3814 io_methods.xSectorSize = Some(io_sector_size);
3815 io_methods.xDeviceCharacteristics = Some(io_device_characteristics);
3816
3817 let generation = name
3818 .rsplit_once("-g")
3819 .and_then(|(_, generation)| generation.parse::<u64>().ok());
3820 let mut ctx = Box::new(VfsContext::new(
3821 actor_id,
3822 generation,
3823 runtime,
3824 transport,
3825 config,
3826 io_methods,
3827 initial_pages,
3828 metrics,
3829 )?);
3830 tracing::trace!(target: "sqlite_prefetch::trace", event = "open",
3831 stream_id = %format_args!("sqlite-{:p}", &*ctx), actor_id = %ctx.actor_id, generation = ctx.generation);
3832 let ctx_ptr = (&mut *ctx) as *mut VfsContext;
3833 let name_cstring = CString::new(name).map_err(|err| err.to_string())?;
3834
3835 let mut vfs: sqlite3_vfs = unsafe { std::mem::zeroed() };
3836 vfs.iVersion = 1;
3837 vfs.szOsFile = std::mem::size_of::<VfsFile>() as c_int;
3838 vfs.mxPathname = MAX_PATHNAME;
3839 vfs.zName = name_cstring.as_ptr();
3840 vfs.pAppData = ctx_ptr.cast::<c_void>();
3841 vfs.xOpen = Some(vfs_open);
3842 vfs.xDelete = Some(vfs_delete);
3843 vfs.xAccess = Some(vfs_access);
3844 vfs.xFullPathname = Some(vfs_full_pathname);
3845 vfs.xRandomness = Some(vfs_randomness);
3846 vfs.xSleep = Some(vfs_sleep);
3847 vfs.xCurrentTime = Some(vfs_current_time);
3848 vfs.xGetLastError = Some(vfs_get_last_error);
3849
3850 let registration = SqliteVfsRegistration::register(vfs)?;
3851
3852 Ok(Self {
3853 _registration: registration,
3854 _name: name_cstring,
3855 ctx,
3856 })
3857 }
3858
3859 pub fn name_ptr(&self) -> *const c_char {
3860 self._name.as_ptr()
3861 }
3862
3863 pub fn cached_page_bytes(&self) -> u64 {
3866 let state = self.ctx.state.read();
3867 state
3868 .prefetch_buffer
3869 .entry_count()
3870 .saturating_add(state.committed_buffer.entry_count())
3871 .saturating_mul(state.page_size as u64)
3872 }
3873
3874 pub fn idle_release(&self) -> Option<std::time::Duration> {
3876 match self.ctx.config.idle_release_ms {
3877 0 => None,
3878 ms => Some(std::time::Duration::from_millis(ms)),
3879 }
3880 }
3881
3882
3883
3884 fn ctx(&self) -> &VfsContext {
3885 &self.ctx
3886 }
3887
3888 fn commit_atomic_count(&self) -> u64 {
3889 self.ctx.commit_atomic_count.load(Ordering::Relaxed)
3890 }
3891}
3892
3893impl SqliteVfsRegistration {
3894 fn register(vfs: sqlite3_vfs) -> std::result::Result<Self, String> {
3895 let vfs_ptr = Box::into_raw(Box::new(vfs));
3896 let rc = unsafe { sqlite3_vfs_register(vfs_ptr, 0) };
3897 if rc != SQLITE_OK {
3898 unsafe {
3899 drop(Box::from_raw(vfs_ptr));
3900 }
3901 return Err(format!("sqlite3_vfs_register failed with code {rc}"));
3902 }
3903
3904 Ok(Self { vfs_ptr })
3905 }
3906}
3907
3908impl Drop for SqliteVfsRegistration {
3909 fn drop(&mut self) {
3910 unsafe {
3911 sqlite3_vfs_unregister(self.vfs_ptr);
3912 drop(Box::from_raw(self.vfs_ptr));
3913 }
3914 }
3915}
3916
3917impl NativeDatabase {
3918 pub fn as_ptr(&self) -> *mut sqlite3 {
3919 self.db
3920 }
3921
3922 pub fn take_last_kv_error(&self) -> Option<String> {
3923 self._vfs.take_last_error()
3924 }
3925
3926 pub fn sqlite_vfs_metrics(&self) -> SqliteVfsMetricsSnapshot {
3927 self._vfs.ctx.sqlite_vfs_metrics()
3928 }
3929
3930 pub fn round_trip_counts(&self) -> SqliteRoundTripCounts {
3931 self._vfs.ctx.round_trip_counts()
3932 }
3933
3934 pub(crate) fn begin_operation_profile(&self) -> SqliteOperationProfileGuard<'_> {
3935 self._vfs.ctx.begin_operation_profile();
3936 SqliteOperationProfileGuard {
3937 ctx: &self._vfs.ctx,
3938 active: true,
3939 }
3940 }
3941
3942 pub fn cached_page_bytes(&self) -> u64 {
3944 self._vfs.cached_page_bytes()
3945 }
3946
3947 pub fn cork_commits(&self) {
3955 self._vfs.ctx().cork();
3956 }
3957
3958 pub fn uncork_commits(&self) -> Result<()> {
3965 let autocommit = unsafe { sqlite3_get_autocommit(self.as_ptr()) };
3967 if autocommit == 0 {
3968 anyhow::bail!("cannot uncork sqlite commits inside a transaction");
3969 }
3970 self._vfs
3971 .ctx()
3972 .uncork()
3973 .map_err(|error| anyhow::anyhow!("uncork sqlite commits: {error:?}"))
3974 }
3975
3976 pub fn release_idle_memory(&self) -> bool {
3977 {
3978 let state = self._vfs.ctx.state.read();
3979 if state.write_buffer.in_atomic_write
3980 || !state.write_buffer.dirty.is_empty()
3981 || state.corked.is_some()
3982 {
3983 return false;
3984 }
3985 }
3986 unsafe {
3987 sqlite3_db_release_memory(self.db);
3988 }
3989 self._vfs.ctx.state.write().invalidate_page_buffers();
3990 self._vfs.ctx.report_prefetch_unused();
3991 true
3992 }
3993}
3994
3995impl Drop for NativeDatabase {
3996 fn drop(&mut self) {
3997 if !self.db.is_null() {
3998 let ctx = self._vfs.ctx();
3999 ctx.discard_refused_transaction();
4000 if write_transaction_open(self.db) {
4003 ctx.discard_open_transaction();
4004 }
4005 {
4007 let mut state = ctx.state.write();
4008 if let Some(mut corked) = state.corked.take() {
4009 corked.append(&mut state.write_buffer.dirty);
4010 state.write_buffer.dirty = corked;
4011 }
4012 }
4013 let should_flush = {
4014 let state = ctx.state.read();
4015 state.write_buffer.in_atomic_write
4016 || !state.write_buffer.dirty.is_empty()
4017 || state.db_size_pages != state.committed_db_size_pages
4018 };
4019 if should_flush {
4020 let result = if ctx.state.read().write_buffer.in_atomic_write {
4021 ctx.commit_atomic_write_with_timeout(Some(NATIVE_DATABASE_DROP_FLUSH_TIMEOUT))
4022 } else {
4023 ctx.flush_dirty_pages_with_timeout(Some(NATIVE_DATABASE_DROP_FLUSH_TIMEOUT))
4024 .map(|wait| match wait {
4025 CommitWait::Completed(_) => CommitWait::Completed(()),
4026 CommitWait::TimedOut => CommitWait::TimedOut,
4027 })
4028 };
4029 match result {
4034 Ok(CommitWait::Completed(())) => {}
4035 Ok(CommitWait::TimedOut) => {
4036 let discarded_pages = ctx.discard_unsent_commits();
4037 tracing::error!(
4038 actor_id = %ctx.actor_id,
4039 timeout_ms = NATIVE_DATABASE_DROP_FLUSH_TIMEOUT.as_millis(),
4040 discarded_pages,
4041 "timed out flushing sqlite database before close, discarding its unsent commits"
4042 );
4043 }
4044 Err(err) => {
4045 handle_non_finalize_commit_error(ctx, &err);
4046 let discarded_pages = ctx.discard_unsent_commits();
4047 tracing::error!(
4048 actor_id = %ctx.actor_id,
4049 ?err,
4050 discarded_pages,
4051 "failed to flush sqlite database before close, discarding its unsent commits"
4052 );
4053 }
4054 }
4055 }
4056
4057 ctx.state.write().closing = true;
4063 let finalized_statements = finalize_outstanding_statements(self.db);
4064 if finalized_statements > 0 {
4065 tracing::warn!(
4066 actor_id = %ctx.actor_id,
4067 finalized_statements,
4068 "finalized statements left prepared on a closing sqlite database"
4069 );
4070 }
4071 let rc = unsafe { sqlite3_close_v2(self.db) };
4072 if rc != SQLITE_OK {
4073 tracing::warn!(
4074 rc,
4075 error = sqlite_error_message(self.db),
4076 "failed to close sqlite database"
4077 );
4078 }
4079 self.db = ptr::null_mut();
4080 }
4081 }
4082}
4083
4084fn finalize_outstanding_statements(db: *mut sqlite3) -> usize {
4091 let mut finalized = 0;
4092 loop {
4093 let stmt = unsafe { sqlite3_next_stmt(db, ptr::null_mut()) };
4096 if stmt.is_null() {
4097 break;
4098 }
4099 unsafe {
4100 sqlite3_finalize(stmt);
4101 }
4102 finalized += 1;
4103 }
4104 finalized
4105}
4106
4107fn write_transaction_open(db: *mut sqlite3) -> bool {
4110 let autocommit = unsafe { sqlite3_get_autocommit(db) };
4112 let txn_state = unsafe { sqlite3_txn_state(db, ptr::null()) };
4113 autocommit == 0 || txn_state == SQLITE_TXN_WRITE
4114}
4115
4116pub fn open_database(vfs: SqliteVfs, file_name: &str) -> Result<NativeDatabase> {
4117 open_connection(
4118 Arc::new(vfs),
4119 file_name,
4120 SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
4121 )
4122 .map_err(anyhow::Error::msg)
4123 .and_then(|connection| {
4124 configure_connection_for_database(connection.as_ptr(), &connection._vfs, file_name)?;
4125 Ok(connection)
4126 })
4127}
4128
4129pub fn open_connection(
4130 vfs: NativeVfsHandle,
4131 file_name: &str,
4132 flags: c_int,
4133) -> std::result::Result<NativeConnection, String> {
4134 let c_name = CString::new(file_name).map_err(|err| err.to_string())?;
4135 let mut db: *mut sqlite3 = ptr::null_mut();
4136
4137 let rc = unsafe { sqlite3_open_v2(c_name.as_ptr(), &mut db, flags, vfs.name_ptr()) };
4138 if rc != SQLITE_OK {
4139 let message = sqlite_error_message(db);
4140 tracing::error!(
4141 file_name,
4142 rc,
4143 %message,
4144 last_error = ?vfs.clone_last_error(),
4145 "failed to open sqlite database with custom VFS"
4146 );
4147 if !db.is_null() {
4148 unsafe {
4149 sqlite3_close(db);
4150 }
4151 }
4152 return Err(format!("sqlite3_open_v2 failed with code {rc}: {message}"));
4153 }
4154
4155 Ok(NativeDatabase { db, _vfs: vfs })
4156}
4157
4158#[derive(rivet_error::RivetError)]
4159#[error(
4160 "sqlite",
4161 "invalid_compile_options",
4162 "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\"."
4163)]
4164struct SqliteInvalidCompileOptions;
4165
4166pub fn configure_connection_for_database(
4167 db: *mut sqlite3,
4168 vfs: &SqliteVfs,
4169 file_name: &str,
4170) -> Result<()> {
4171 let batch_atomic = unsafe { sqlite3_compileoption_used(c"ENABLE_BATCH_ATOMIC_WRITE".as_ptr()) };
4173 let direct_overflow = unsafe { sqlite3_compileoption_used(c"DIRECT_OVERFLOW_READ".as_ptr()) };
4174 if batch_atomic == 0 || direct_overflow != 0 {
4175 return Err(SqliteInvalidCompileOptions.build());
4176 }
4177
4178 let cache_size_kib = sqlite_optimization_flags().pager_cache_size_kib;
4184 let cache_size_pragma = format!("PRAGMA cache_size = -{cache_size_kib};");
4185
4186 let pragmas = [
4187 "PRAGMA page_size = 4096;",
4188 "PRAGMA journal_mode = DELETE;",
4189 "PRAGMA synchronous = NORMAL;",
4190 "PRAGMA temp_store = MEMORY;",
4191 "PRAGMA auto_vacuum = NONE;",
4192 "PRAGMA locking_mode = EXCLUSIVE;",
4193 "PRAGMA journal_size_limit = 0;",
4196 cache_size_pragma.as_str(),
4197 ];
4198 for pragma in &pragmas {
4199 if let Err(err) = sqlite_exec(db, pragma) {
4200 tracing::error!(
4201 file_name,
4202 pragma,
4203 %err,
4204 last_error = ?vfs.clone_last_error(),
4205 "failed to configure sqlite database"
4206 );
4207 return Err(anyhow::Error::msg(err));
4208 }
4209 }
4210
4211 let rc = unsafe { sqlite3_set_authorizer(db, Some(authorize_journal_mode), ptr::null_mut()) };
4212 if rc != SQLITE_OK {
4213 anyhow::bail!("failed to install the sqlite authorizer: {rc}");
4214 }
4215
4216 Ok(())
4217}
4218
4219extern "C" fn authorize_journal_mode(
4228 _context: *mut c_void,
4229 action: c_int,
4230 first: *const c_char,
4231 second: *const c_char,
4232 _database: *const c_char,
4233 _trigger: *const c_char,
4234) -> c_int {
4235 if action != SQLITE_PRAGMA || first.is_null() || second.is_null() {
4236 return SQLITE_OK;
4237 }
4238 let name = unsafe { CStr::from_ptr(first) }.to_string_lossy();
4239 if name.eq_ignore_ascii_case("journal_size_limit") {
4242 return SQLITE_DENY;
4243 }
4244 let value = unsafe { CStr::from_ptr(second) }.to_string_lossy();
4245 let value = value.trim_matches(|c| matches!(c, '"' | '\''));
4246 if name.eq_ignore_ascii_case("synchronous") {
4247 let syncs_every_commit = [
4248 "normal", "full", "extra", "1", "2", "3", "on", "yes", "true",
4249 ]
4250 .iter()
4251 .any(|level| value.eq_ignore_ascii_case(level));
4252 return if syncs_every_commit {
4253 SQLITE_OK
4254 } else {
4255 SQLITE_DENY
4256 };
4257 }
4258 if !name.eq_ignore_ascii_case("journal_mode") {
4259 return SQLITE_OK;
4260 }
4261 if value.eq_ignore_ascii_case("delete") {
4262 SQLITE_OK
4263 } else {
4264 SQLITE_DENY
4265 }
4266}