@@ -8,7 +8,7 @@ use std::ffi::{c_char, c_int, c_void, CStr, CString};
88use std:: ptr;
99use std:: slice;
1010use std:: sync:: atomic:: { AtomicU64 , Ordering } ;
11- use std:: sync:: Arc ;
11+ use std:: sync:: { Arc , OnceLock } ;
1212
1313use libsqlite3_sys:: * ;
1414use 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.
5555const 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
285299impl 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