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