@@ -13,6 +13,32 @@ use redb::{Database, ReadableTable, TableDefinition, TableError};
1313use std:: collections:: HashMap ;
1414use std:: path:: { Path , PathBuf } ;
1515use std:: sync:: Arc ;
16+ use std:: time:: Duration ;
17+
18+ /// Maximum number of attempts to open the database when another process holds
19+ /// the lock. Each concurrent `smolvm` CLI invocation (e.g., parallel
20+ /// `machine start` calls) opens the database exclusively via redb's file lock.
21+ /// Without retry, the second process fails immediately with
22+ /// "Database already open. Cannot acquire lock."
23+ ///
24+ /// With 10 retries and exponential backoff (50ms initial, 1s cap), the total
25+ /// wait before giving up is ~5 seconds — enough for any normal CLI operation
26+ /// to release the lock.
27+ const DB_OPEN_MAX_RETRIES : u32 = 10 ;
28+
29+ /// Initial backoff delay between database open retries.
30+ /// Starts short (10ms) since typical DB operations complete in ~1-2ms.
31+ /// Doubles on each attempt: 10ms → 20ms → 40ms → 80ms → 160ms → 320ms → 640ms → 1000ms (capped).
32+ const DB_OPEN_INITIAL_BACKOFF : Duration = Duration :: from_millis ( 10 ) ;
33+
34+ /// Maximum backoff delay between retries. Prevents excessive wait on any
35+ /// single retry when the backoff would otherwise grow beyond this.
36+ const DB_OPEN_MAX_BACKOFF : Duration = Duration :: from_secs ( 1 ) ;
37+
38+ /// Check if a redb error indicates another process holds the database lock.
39+ fn is_lock_contention ( e : & redb:: DatabaseError ) -> bool {
40+ matches ! ( e, redb:: DatabaseError :: DatabaseAlreadyOpen )
41+ }
1642
1743/// Table for storing VM records (name -> JSON-serialized VmRecord).
1844const VMS_TABLE : TableDefinition < & str , & [ u8 ] > = TableDefinition :: new ( "vms" ) ;
@@ -56,17 +82,53 @@ impl std::fmt::Debug for SmolvmDb {
5682
5783impl SmolvmDb {
5884 /// Run a closure with the cached database handle, opening it on first use.
85+ ///
86+ /// If another process holds the database lock, retries with exponential
87+ /// backoff rather than failing immediately. This allows concurrent CLI
88+ /// commands (e.g., parallel `machine start` calls) to succeed.
5989 fn with_db < T , F > ( & self , f : F ) -> Result < T >
6090 where
6191 F : FnOnce ( & Database ) -> Result < T > ,
6292 {
6393 let mut guard = self . handle . lock ( ) ;
6494 if guard. is_none ( ) {
65- * guard = Some ( Database :: create ( & self . path ) . db_err ( "open database" ) ?) ;
95+ * guard = Some ( Self :: open_with_retry ( & self . path ) ?) ;
6696 }
6797 f ( guard. as_ref ( ) . unwrap ( ) )
6898 }
6999
100+ /// Open the database file, retrying with exponential backoff on lock contention.
101+ ///
102+ /// redb uses an exclusive file lock — only one process can have the database
103+ /// open at a time. When multiple CLI commands run concurrently (e.g., parallel
104+ /// `machine start` calls), the second process retries until the first releases
105+ /// the lock. The API server avoids this entirely by holding a single long-lived
106+ /// database connection.
107+ fn open_with_retry ( path : & Path ) -> Result < Database > {
108+ let mut backoff = DB_OPEN_INITIAL_BACKOFF ;
109+ for attempt in 0 ..=DB_OPEN_MAX_RETRIES {
110+ match Database :: create ( path) {
111+ Ok ( db) => return Ok ( db) ,
112+ Err ( e) if attempt < DB_OPEN_MAX_RETRIES && is_lock_contention ( & e) => {
113+ tracing:: debug!(
114+ attempt = attempt + 1 ,
115+ max = DB_OPEN_MAX_RETRIES ,
116+ backoff_ms = backoff. as_millis( ) ,
117+ "database locked by another process, retrying"
118+ ) ;
119+ std:: thread:: sleep ( backoff) ;
120+ backoff = std:: cmp:: min ( backoff * 2 , DB_OPEN_MAX_BACKOFF ) ;
121+ }
122+ Err ( e) => {
123+ return Err ( Error :: database_unavailable ( format ! ( "open database: {}" , e) ) ) ;
124+ }
125+ }
126+ }
127+ // All retries exhausted — the loop always returns on the last iteration
128+ // (attempt == DB_OPEN_MAX_RETRIES falls through to the Err arm).
129+ unreachable ! ( )
130+ }
131+
70132 /// Open the database at the default location.
71133 ///
72134 /// Default path: `~/Library/Application Support/smolvm/server/smolvm.redb` (macOS)
0 commit comments