Skip to content

Commit 9d72e25

Browse files
committed
fix(sqlite-native): tidy gated read cache behavior
1 parent 0f0530c commit 9d72e25

1 file changed

Lines changed: 46 additions & 27 deletions

File tree

  • rivetkit-typescript/packages/sqlite-native/src

rivetkit-typescript/packages/sqlite-native/src/vfs.rs

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use std::ffi::{c_char, c_int, c_void, CStr, CString};
88
use std::ptr;
99
use std::slice;
1010
use std::sync::atomic::{AtomicU64, Ordering};
11-
use std::sync::Arc;
11+
use std::sync::{Arc, OnceLock};
1212

1313
use libsqlite3_sys::*;
1414
use tokio::runtime::Handle;
@@ -54,6 +54,9 @@ const MAX_PATHNAME: c_int = 64;
5454
/// Maximum number of keys accepted by a single KV put or delete request.
5555
const KV_MAX_BATCH_KEYS: usize = 128;
5656

57+
/// Opt-in flag for the native read cache. Disabled by default to match the WASM VFS.
58+
const READ_CACHE_ENV_VAR: &str = "RIVETKIT_SQLITE_NATIVE_READ_CACHE";
59+
5760
/// First 108 bytes of a valid empty page-1 SQLite database.
5861
///
5962
/// This must match `HEADER_PREFIX` in
@@ -100,6 +103,16 @@ fn is_valid_file_size(size: i64) -> bool {
100103
size >= 0 && (size as u64) <= kv::MAX_FILE_SIZE
101104
}
102105

106+
fn read_cache_enabled() -> bool {
107+
static READ_CACHE_ENABLED: OnceLock<bool> = OnceLock::new();
108+
109+
*READ_CACHE_ENABLED.get_or_init(|| {
110+
std::env::var(READ_CACHE_ENV_VAR)
111+
.map(|value| matches!(value.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
112+
.unwrap_or(false)
113+
})
114+
}
115+
103116
// MARK: VFS Metrics
104117

105118
/// Per-VFS-callback operation metrics for diagnosing native vs WASM performance.
@@ -139,6 +152,7 @@ struct VfsContext {
139152
kv: Arc<dyn SqliteKv>,
140153
actor_id: String,
141154
main_file_name: String,
155+
read_cache_enabled: bool,
142156
rt_handle: Handle,
143157
io_methods: Box<sqlite3_io_methods>,
144158
vfs_metrics: Arc<VfsMetrics>,
@@ -279,16 +293,16 @@ struct KvFileState {
279293
/// Read cache: maps chunk keys to their data. Populated on KV gets,
280294
/// updated on writes, cleared on truncate/delete. This avoids
281295
/// redundant KV round-trips for pages SQLite reads multiple times.
282-
read_cache: HashMap<Vec<u8>, Vec<u8>>,
296+
read_cache: Option<HashMap<Vec<u8>, Vec<u8>>>,
283297
}
284298

285299
impl KvFileState {
286-
fn new() -> Self {
300+
fn new(read_cache_enabled: bool) -> Self {
287301
Self {
288302
batch_mode: false,
289303
dirty_buffer: BTreeMap::new(),
290304
saved_file_size: 0,
291-
read_cache: HashMap::new(),
305+
read_cache: read_cache_enabled.then(HashMap::new),
292306
}
293307
}
294308
}
@@ -420,9 +434,11 @@ unsafe extern "C" fn kv_io_read(
420434
}
421435
// Check read cache.
422436
let key = kv::get_chunk_key(file.file_tag, chunk_idx as u32);
423-
if let Some(cached) = state.read_cache.get(key.as_slice()) {
424-
buffered_chunks.insert(chunk_idx, cached.clone());
425-
continue;
437+
if let Some(read_cache) = state.read_cache.as_ref() {
438+
if let Some(cached) = read_cache.get(key.as_slice()) {
439+
buffered_chunks.insert(chunk_idx, cached.clone());
440+
continue;
441+
}
426442
}
427443
chunk_keys_to_fetch.push(key.to_vec());
428444
}
@@ -435,10 +451,11 @@ unsafe extern "C" fn kv_io_read(
435451
} else {
436452
match ctx.kv_get(chunk_keys_to_fetch) {
437453
Ok(resp) => {
438-
// Populate read cache with fetched values.
439-
for (key, value) in resp.keys.iter().zip(resp.values.iter()) {
440-
if !value.is_empty() {
441-
state.read_cache.insert(key.clone(), value.clone());
454+
if let Some(read_cache) = state.read_cache.as_mut() {
455+
for (key, value) in resp.keys.iter().zip(resp.values.iter()) {
456+
if !value.is_empty() {
457+
read_cache.insert(key.clone(), value.clone());
458+
}
442459
}
443460
}
444461
resp
@@ -645,13 +662,12 @@ unsafe extern "C" fn kv_io_write(
645662
entries_to_write.push((file.meta_key.to_vec(), encode_file_meta(file.size)));
646663
}
647664

648-
// Update read cache with the entries we're about to write.
649-
{
650-
let state = get_file_state(file.state);
665+
if let Some(read_cache) = get_file_state(file.state).read_cache.as_mut() {
651666
for (key, value) in &entries_to_write {
652-
// Only cache chunk keys, not metadata.
667+
// Only cache chunk keys here. Metadata keys are read on open/access
668+
// and should not be mixed into the per-page cache.
653669
if key.len() == 8 {
654-
state.read_cache.insert(key.clone(), value.clone());
670+
read_cache.insert(key.clone(), value.clone());
655671
}
656672
}
657673
}
@@ -702,15 +718,15 @@ unsafe extern "C" fn kv_io_truncate(p_file: *mut sqlite3_file, size: sqlite3_int
702718
return SQLITE_OK;
703719
}
704720

705-
// Invalidate read cache entries for truncated chunks.
706-
{
707-
let state = get_file_state(file.state);
721+
if let Some(read_cache) = get_file_state(file.state).read_cache.as_mut() {
708722
let truncate_from_chunk = if size == 0 {
709723
0u32
710724
} else {
711725
(size as u32 / kv::CHUNK_SIZE as u32) + 1
712726
};
713-
state.read_cache.retain(|key, _| {
727+
// The read cache stores only chunk keys. Keep entries strictly before
728+
// the truncation boundary so reads cannot serve bytes from removed chunks.
729+
read_cache.retain(|key, _| {
714730
// Chunk keys are 8 bytes: [prefix, version, CHUNK_PREFIX, file_tag, idx_be32]
715731
if key.len() == 8 && key[3] == file.file_tag {
716732
let chunk_idx = u32::from_be_bytes([key[4], key[5], key[6], key[7]]);
@@ -901,12 +917,14 @@ unsafe extern "C" fn kv_io_file_control(
901917

902918
// Move dirty buffer entries into the read cache so subsequent
903919
// reads can serve them without a KV round-trip.
904-
let flushed: Vec<_> = std::mem::take(&mut state.dirty_buffer)
905-
.into_iter()
906-
.collect();
907-
for (chunk_index, data) in flushed {
908-
let key = kv::get_chunk_key(file.file_tag, chunk_index);
909-
state.read_cache.insert(key.to_vec(), data);
920+
let flushed: Vec<_> = std::mem::take(&mut state.dirty_buffer).into_iter().collect();
921+
if let Some(read_cache) = state.read_cache.as_mut() {
922+
// Only chunk pages belong in the read cache. The metadata write above
923+
// still goes through KV, but should not be cached as a page.
924+
for (chunk_index, data) in flushed {
925+
let key = kv::get_chunk_key(file.file_tag, chunk_index);
926+
read_cache.insert(key.to_vec(), data);
927+
}
910928
}
911929
file.meta_dirty = false;
912930
state.batch_mode = false;
@@ -1010,7 +1028,7 @@ unsafe extern "C" fn kv_vfs_open(
10101028
return SQLITE_CANTOPEN;
10111029
};
10121030

1013-
let state = Box::into_raw(Box::new(KvFileState::new()));
1031+
let state = Box::into_raw(Box::new(KvFileState::new(ctx.read_cache_enabled)));
10141032
let base = sqlite3_file {
10151033
pMethods: ctx.io_methods.as_ref() as *const sqlite3_io_methods,
10161034
};
@@ -1205,6 +1223,7 @@ impl KvVfs {
12051223
kv,
12061224
actor_id: actor_id.clone(),
12071225
main_file_name: actor_id,
1226+
read_cache_enabled: read_cache_enabled(),
12081227
rt_handle,
12091228
io_methods: Box::new(io_methods),
12101229
vfs_metrics,

0 commit comments

Comments
 (0)