@@ -14,7 +14,7 @@ use std::fs::OpenOptions;
1414use std:: future:: Future ;
1515#[ cfg( unix) ]
1616use std:: os:: unix:: fs:: OpenOptionsExt ;
17- use std:: path:: PathBuf ;
17+ use std:: path:: { Path , PathBuf } ;
1818use std:: sync:: atomic:: { AtomicI64 , AtomicU64 , Ordering } ;
1919use std:: sync:: { Arc , Mutex } ;
2020
@@ -23,7 +23,7 @@ use lightning::util::persist::{
2323 KVStore , MigratableKVStore , PageToken , PaginatedKVStore , PaginatedListResponse ,
2424} ;
2525use lightning_types:: string:: PrintableString ;
26- use rusqlite:: { named_params, Connection } ;
26+ use rusqlite:: { named_params, Connection , DatabaseName } ;
2727
2828use crate :: io:: utils:: { check_namespace_key_validity, create_dir_all_private} ;
2929
@@ -69,7 +69,30 @@ impl SqliteStore {
6969 pub fn new (
7070 data_dir : PathBuf , db_file_name : Option < String > , kv_table_name : Option < String > ,
7171 ) -> io:: Result < Self > {
72- let inner = Arc :: new ( SqliteStoreInner :: new ( data_dir, db_file_name, kv_table_name) ?) ;
72+ Self :: new_with_backup ( data_dir, db_file_name, kv_table_name, None )
73+ }
74+
75+ /// Constructs a new [`SqliteStore`], continuously replicating to `backup_path`.
76+ ///
77+ /// After opening (including schema setup/migration) and after every successful
78+ /// [`KVStore::write`] / [`KVStore::remove`], the primary database is copied to `backup_path`
79+ /// via SQLite's [Online Backup API]. A failed replica update fails the persist.
80+ ///
81+ /// `backup_path` must be a filesystem path to a database *file* (parent directories are
82+ /// created if missing). SQLite's `:memory:` database name and `file:` URI filenames are not
83+ /// supported. The replica must not be the same file as the primary database.
84+ ///
85+ /// The replica is a complete SQLite database and may later be opened as a primary
86+ /// [`SqliteStore`]. It is a hot-spare file, not a second live node: do not open it with
87+ /// another store while this one is running.
88+ ///
89+ /// [Online Backup API]: https://www.sqlite.org/backup.html
90+ pub fn new_with_backup (
91+ data_dir : PathBuf , db_file_name : Option < String > , kv_table_name : Option < String > ,
92+ backup_path : Option < PathBuf > ,
93+ ) -> io:: Result < Self > {
94+ let inner =
95+ Arc :: new ( SqliteStoreInner :: new ( data_dir, db_file_name, kv_table_name, backup_path) ?) ;
7396
7497 let next_write_version = AtomicU64 :: new ( 1 ) ;
7598 Ok ( Self { inner, next_write_version } )
@@ -230,25 +253,66 @@ struct SqliteStoreInner {
230253 connection : Arc < Mutex < Connection > > ,
231254 data_dir : PathBuf ,
232255 kv_table_name : String ,
256+ backup_path : Option < PathBuf > ,
233257 write_version_locks : Mutex < HashMap < String , Arc < Mutex < u64 > > > > ,
234258 next_sort_order : AtomicI64 ,
235259}
236260
261+ fn backup_connection ( src : & Connection , backup_path : & Path ) -> io:: Result < ( ) > {
262+ src. backup ( DatabaseName :: Main , backup_path, None ) . map_err ( |e| {
263+ let msg = format ! ( "Failed to backup SQLite database to {}: {}" , backup_path. display( ) , e) ;
264+ io:: Error :: new ( io:: ErrorKind :: Other , msg)
265+ } )
266+ }
267+
268+ fn reject_sqlite_uri_path ( path : & Path , name : & str ) -> io:: Result < ( ) > {
269+ let path_str = path. to_string_lossy ( ) ;
270+ if name == ":memory:" || name. starts_with ( "file:" ) || path_str. starts_with ( "file:" ) {
271+ return Err ( io:: Error :: new (
272+ io:: ErrorKind :: InvalidInput ,
273+ "SQLite :memory: and file: database names are not supported" ,
274+ ) ) ;
275+ }
276+ Ok ( ( ) )
277+ }
278+
279+ #[ cfg( unix) ]
280+ fn ensure_private_sqlite_file ( path : & Path ) -> io:: Result < ( ) > {
281+ match OpenOptions :: new ( ) . create_new ( true ) . write ( true ) . mode ( 0o600 ) . open ( path) {
282+ Ok ( _) => Ok ( ( ) ) ,
283+ Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: AlreadyExists => Ok ( ( ) ) ,
284+ Err ( e) => {
285+ let msg = format ! ( "Failed to create database file {}: {}" , path. display( ) , e) ;
286+ Err ( io:: Error :: new ( io:: ErrorKind :: Other , msg) )
287+ } ,
288+ }
289+ }
290+
237291impl SqliteStoreInner {
238292 fn new (
239293 data_dir : PathBuf , db_file_name : Option < String > , kv_table_name : Option < String > ,
294+ backup_path : Option < PathBuf > ,
240295 ) -> io:: Result < Self > {
241296 let db_file_name = db_file_name. unwrap_or ( DEFAULT_SQLITE_DB_FILE_NAME . to_string ( ) ) ;
242297 let mut db_file_path = data_dir. clone ( ) ;
243298 db_file_path. push ( & db_file_name) ;
244- if db_file_name == ":memory:"
245- || db_file_name. starts_with ( "file:" )
246- || db_file_path. to_string_lossy ( ) . starts_with ( "file:" )
247- {
248- return Err ( io:: Error :: new (
249- io:: ErrorKind :: InvalidInput ,
250- "SQLite :memory: and file: database names are not supported" ,
251- ) ) ;
299+ reject_sqlite_uri_path ( & db_file_path, & db_file_name) ?;
300+ if let Some ( backup_path) = backup_path. as_ref ( ) {
301+ let backup_name = backup_path. file_name ( ) . and_then ( |n| n. to_str ( ) ) . unwrap_or_default ( ) ;
302+ reject_sqlite_uri_path ( backup_path, backup_name) ?;
303+ if backup_path. is_dir ( ) {
304+ let msg = format ! (
305+ "SQLite backup path must be a database file, not a directory: {}" ,
306+ backup_path. display( )
307+ ) ;
308+ return Err ( io:: Error :: new ( io:: ErrorKind :: InvalidInput , msg) ) ;
309+ }
310+ if backup_path == & db_file_path {
311+ return Err ( io:: Error :: new (
312+ io:: ErrorKind :: InvalidInput ,
313+ "SQLite backup path must differ from the primary database file" ,
314+ ) ) ;
315+ }
252316 }
253317 let kv_table_name = kv_table_name. unwrap_or ( DEFAULT_KV_TABLE_NAME . to_string ( ) ) ;
254318
@@ -261,15 +325,7 @@ impl SqliteStoreInner {
261325 io:: Error :: new ( io:: ErrorKind :: Other , msg)
262326 } ) ?;
263327 #[ cfg( unix) ]
264- match OpenOptions :: new ( ) . create_new ( true ) . write ( true ) . mode ( 0o600 ) . open ( & db_file_path) {
265- Ok ( _) => { } ,
266- Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: AlreadyExists => { } ,
267- Err ( e) => {
268- let msg =
269- format ! ( "Failed to create database file {}: {}" , db_file_path. display( ) , e) ;
270- return Err ( io:: Error :: new ( io:: ErrorKind :: Other , msg) ) ;
271- } ,
272- }
328+ ensure_private_sqlite_file ( & db_file_path) ?;
273329
274330 let mut connection = Connection :: open ( db_file_path. clone ( ) ) . map_err ( |e| {
275331 let msg =
@@ -351,9 +407,41 @@ impl SqliteStoreInner {
351407 } ) ?;
352408 let next_sort_order = AtomicI64 :: new ( max_sort_order + 1 ) ;
353409
410+ if let Some ( backup_path) = backup_path. as_ref ( ) {
411+ if let Some ( parent) = backup_path. parent ( ) {
412+ if !parent. as_os_str ( ) . is_empty ( ) {
413+ create_dir_all_private ( parent) . map_err ( |e| {
414+ let msg = format ! (
415+ "Failed to create SQLite backup directory {}: {}" ,
416+ parent. display( ) ,
417+ e
418+ ) ;
419+ io:: Error :: new ( io:: ErrorKind :: Other , msg)
420+ } ) ?;
421+ }
422+ }
423+ #[ cfg( unix) ]
424+ ensure_private_sqlite_file ( backup_path) ?;
425+ backup_connection ( & connection, backup_path) ?;
426+ }
427+
354428 let connection = Arc :: new ( Mutex :: new ( connection) ) ;
355429 let write_version_locks = Mutex :: new ( HashMap :: new ( ) ) ;
356- Ok ( Self { connection, data_dir, kv_table_name, write_version_locks, next_sort_order } )
430+ Ok ( Self {
431+ connection,
432+ data_dir,
433+ kv_table_name,
434+ backup_path,
435+ write_version_locks,
436+ next_sort_order,
437+ } )
438+ }
439+
440+ fn backup_to_replica ( & self , connection : & Connection ) -> io:: Result < ( ) > {
441+ if let Some ( backup_path) = self . backup_path . as_ref ( ) {
442+ backup_connection ( connection, backup_path) ?;
443+ }
444+ Ok ( ( ) )
357445 }
358446
359447 fn get_inner_lock_ref ( & self , locking_key : String ) -> Arc < Mutex < u64 > > {
@@ -449,7 +537,9 @@ impl SqliteStoreInner {
449537 e
450538 ) ;
451539 io:: Error :: new ( io:: ErrorKind :: Other , msg)
452- } )
540+ } ) ?;
541+ drop ( stmt) ;
542+ self . backup_to_replica ( & locked_conn)
453543 } )
454544 }
455545
@@ -484,7 +574,8 @@ impl SqliteStoreInner {
484574 ) ;
485575 io:: Error :: new ( io:: ErrorKind :: Other , msg)
486576 } ) ?;
487- Ok ( ( ) )
577+ drop ( stmt) ;
578+ self . backup_to_replica ( & locked_conn)
488579 } )
489580 }
490581
@@ -1134,6 +1225,94 @@ mod tests {
11341225 assert_eq ! ( response. keys, vec![ "key_c" , "key_b" , "key_a" ] ) ;
11351226 }
11361227 }
1228+
1229+ #[ tokio:: test]
1230+ async fn test_sqlite_store_continuous_backup ( ) {
1231+ let mut temp_path = random_storage_path ( ) ;
1232+ temp_path. push ( "test_sqlite_store_continuous_backup" ) ;
1233+ let backup_dir = temp_path. join ( "backup" ) ;
1234+ let backup_path = backup_dir. join ( "replica.sqlite" ) ;
1235+ let db_file_name = "test_db" . to_string ( ) ;
1236+ let kv_table_name = "test_table" . to_string ( ) ;
1237+
1238+ let primary_namespace = "test_ns" ;
1239+ let secondary_namespace = "test_sub" ;
1240+ let key = "test_key" ;
1241+ let value = vec ! [ 7u8 ; 16 ] ;
1242+
1243+ {
1244+ let store = SqliteStore :: new_with_backup (
1245+ temp_path. clone ( ) ,
1246+ Some ( db_file_name. clone ( ) ) ,
1247+ Some ( kv_table_name. clone ( ) ) ,
1248+ Some ( backup_path. clone ( ) ) ,
1249+ )
1250+ . unwrap ( ) ;
1251+
1252+ KVStore :: write ( & store, primary_namespace, secondary_namespace, key, value. clone ( ) )
1253+ . await
1254+ . unwrap ( ) ;
1255+
1256+ std:: mem:: forget ( store) ;
1257+ }
1258+
1259+ let restored = SqliteStore :: new (
1260+ backup_dir. clone ( ) ,
1261+ Some ( "replica.sqlite" . to_string ( ) ) ,
1262+ Some ( kv_table_name) ,
1263+ )
1264+ . unwrap ( ) ;
1265+ let restored_value =
1266+ KVStore :: read ( & restored, primary_namespace, secondary_namespace, key) . await . unwrap ( ) ;
1267+ assert_eq ! ( restored_value, value) ;
1268+
1269+ std:: mem:: forget ( restored) ;
1270+
1271+ {
1272+ let store = SqliteStore :: new_with_backup (
1273+ temp_path. clone ( ) ,
1274+ Some ( db_file_name) ,
1275+ Some ( "test_table" . to_string ( ) ) ,
1276+ Some ( backup_path. clone ( ) ) ,
1277+ )
1278+ . unwrap ( ) ;
1279+ KVStore :: remove ( & store, primary_namespace, secondary_namespace, key, false )
1280+ . await
1281+ . unwrap ( ) ;
1282+ std:: mem:: forget ( store) ;
1283+ }
1284+
1285+ let restored = SqliteStore :: new (
1286+ backup_dir. clone ( ) ,
1287+ Some ( "replica.sqlite" . to_string ( ) ) ,
1288+ Some ( "test_table" . to_string ( ) ) ,
1289+ )
1290+ . unwrap ( ) ;
1291+ let missing = KVStore :: read ( & restored, primary_namespace, secondary_namespace, key)
1292+ . await
1293+ . unwrap_err ( ) ;
1294+ assert_eq ! ( missing. kind( ) , io:: ErrorKind :: NotFound ) ;
1295+
1296+ std:: mem:: forget ( restored) ;
1297+ let _ = fs:: remove_dir_all ( & temp_path) ;
1298+ }
1299+
1300+ #[ tokio:: test]
1301+ async fn test_sqlite_store_backup_rejects_primary_path ( ) {
1302+ let mut temp_path = random_storage_path ( ) ;
1303+ temp_path. push ( "test_sqlite_store_backup_same_path" ) ;
1304+ let backup_path = temp_path. join ( "test_db" ) ;
1305+ let err = match SqliteStore :: new_with_backup (
1306+ temp_path,
1307+ Some ( "test_db" . to_string ( ) ) ,
1308+ Some ( "test_table" . to_string ( ) ) ,
1309+ Some ( backup_path) ,
1310+ ) {
1311+ Ok ( _) => panic ! ( "expected backup path coinciding with the primary database to fail" ) ,
1312+ Err ( e) => e,
1313+ } ;
1314+ assert_eq ! ( err. kind( ) , io:: ErrorKind :: InvalidInput ) ;
1315+ }
11371316}
11381317
11391318#[ cfg( ldk_bench) ]
0 commit comments