@@ -27,6 +27,8 @@ pub enum LeadershipRole {
2727 Follower ,
2828}
2929
30+ const MAX_DB_WORKER_RESPAWNS : u32 = 3 ;
31+
3032pub struct WorkerConfig {
3133 pub db_name : String ,
3234 pub follower_timeout_ms : f64 ,
@@ -135,6 +137,7 @@ pub struct CoordinatorState {
135137 db_pending : Rc < RefCell < HashMap < u32 , DbRequestOrigin > > > ,
136138 pub follower_pending : Rc < RefCell < HashMap < String , u32 > > > ,
137139 pub next_db_request_id : Rc < RefCell < u32 > > ,
140+ db_worker_restart_attempts : Rc < Cell < u32 > > ,
138141}
139142
140143pub struct DbWorkerState {
@@ -167,6 +170,7 @@ impl CoordinatorState {
167170 db_pending : Rc :: new ( RefCell :: new ( HashMap :: new ( ) ) ) ,
168171 follower_pending : Rc :: new ( RefCell :: new ( HashMap :: new ( ) ) ) ,
169172 next_db_request_id : Rc :: new ( RefCell :: new ( 1 ) ) ,
173+ db_worker_restart_attempts : Rc :: new ( Cell :: new ( 0 ) ) ,
170174 } ) )
171175 }
172176
@@ -296,8 +300,6 @@ impl CoordinatorState {
296300 }
297301
298302 fn spawn_db_worker ( self : & Rc < Self > ) -> Result < ( ) , JsValue > {
299- let db_name_encoded =
300- serde_json:: to_string ( & self . db_name ) . unwrap_or_else ( |_| "\" unknown\" " . to_string ( ) ) ;
301303 let body_val = Reflect :: get (
302304 & js_sys:: global ( ) ,
303305 & JsValue :: from_str ( "__SQLITE_EMBEDDED_WORKER" ) ,
@@ -311,33 +313,44 @@ impl CoordinatorState {
311313 let body = body_val
312314 . as_string ( )
313315 . ok_or_else ( || JsValue :: from_str ( "Embedded worker source is missing" ) ) ?;
314- let preamble = format ! (
316+ let preamble = self . build_worker_preamble ( ) ;
317+ let worker = Self :: create_worker_from_script ( & preamble, & body) ?;
318+
319+ let state = Rc :: clone ( self ) ;
320+ let handler = Closure :: wrap ( Box :: new ( move |event : MessageEvent | {
321+ state. handle_db_worker_event ( event) ;
322+ } ) as Box < dyn FnMut ( MessageEvent ) > ) ;
323+ worker. set_onmessage ( Some ( handler. as_ref ( ) . unchecked_ref ( ) ) ) ;
324+ handler. forget ( ) ;
325+
326+ self . db_worker . borrow_mut ( ) . replace ( worker) ;
327+ Ok ( ( ) )
328+ }
329+
330+ fn build_worker_preamble ( & self ) -> String {
331+ let db_name_encoded =
332+ serde_json:: to_string ( & self . db_name ) . unwrap_or_else ( |_| "\" unknown\" " . to_string ( ) ) ;
333+ // __SQLITE_DB_ONLY=true runs the embedded worker in DB-only mode, separating coordinator work from DB tasks.
334+ format ! (
315335 "self.__SQLITE_DB_ONLY = true;\n self.__SQLITE_DB_NAME = {};\n self.__SQLITE_FOLLOWER_TIMEOUT_MS = {};\n self.__SQLITE_QUERY_TIMEOUT_MS = {};\n " ,
316336 db_name_encoded,
317337 self . follower_timeout_ms,
318338 self . query_timeout_ms,
319- ) ;
339+ )
340+ }
320341
342+ fn create_worker_from_script ( preamble : & str , body : & str ) -> Result < Worker , JsValue > {
321343 let parts = js_sys:: Array :: new ( ) ;
322- parts. push ( & JsValue :: from_str ( & preamble) ) ;
323- parts. push ( & JsValue :: from_str ( & body) ) ;
344+ parts. push ( & JsValue :: from_str ( preamble) ) ;
345+ parts. push ( & JsValue :: from_str ( body) ) ;
324346 let options = BlobPropertyBag :: new ( ) ;
325347 options. set_type ( "application/javascript" ) ;
326348 let blob = Blob :: new_with_str_sequence_and_options ( & parts, & options) ?;
327349 let url = Url :: create_object_url_with_blob ( & blob) ?;
328350
329351 let worker = Worker :: new ( & url) ?;
330352 Url :: revoke_object_url ( & url) ?;
331-
332- let state = Rc :: clone ( self ) ;
333- let handler = Closure :: wrap ( Box :: new ( move |event : MessageEvent | {
334- state. handle_db_worker_event ( event) ;
335- } ) as Box < dyn FnMut ( MessageEvent ) > ) ;
336- worker. set_onmessage ( Some ( handler. as_ref ( ) . unchecked_ref ( ) ) ) ;
337- handler. forget ( ) ;
338-
339- self . db_worker . borrow_mut ( ) . replace ( worker) ;
340- Ok ( ( ) )
353+ Ok ( worker)
341354 }
342355
343356 pub fn handle_db_worker_event ( self : & Rc < Self > , event : MessageEvent ) {
@@ -350,6 +363,7 @@ impl CoordinatorState {
350363 Ok ( MainThreadMessage :: WorkerReady ) => {
351364 * self . db_worker_ready . borrow_mut ( ) = true ;
352365 * self . leader_ready . borrow_mut ( ) = true ;
366+ self . db_worker_restart_attempts . set ( 0 ) ;
353367 let ready = ChannelMessage :: LeaderReady {
354368 leader_id : self . worker_id . clone ( ) ,
355369 } ;
@@ -377,6 +391,8 @@ impl CoordinatorState {
377391 * self . db_worker_ready . borrow_mut ( ) = false ;
378392 * self . leader_ready . borrow_mut ( ) = false ;
379393 * self . ready_signaled . borrow_mut ( ) = false ;
394+ let attempts = self . db_worker_restart_attempts . get ( ) . saturating_add ( 1 ) ;
395+ self . db_worker_restart_attempts . set ( attempts) ;
380396 if let Some ( worker) = self . db_worker . borrow_mut ( ) . take ( ) {
381397 worker. terminate ( ) ;
382398 }
@@ -385,6 +401,13 @@ impl CoordinatorState {
385401 for ( _, origin) in pending {
386402 self . fail_origin ( origin, error. clone ( ) ) ;
387403 }
404+ if attempts > MAX_DB_WORKER_RESPAWNS {
405+ let message = format ! (
406+ "DB worker restart limit reached (max {MAX_DB_WORKER_RESPAWNS}); leaving worker failed"
407+ ) ;
408+ let _ = send_worker_error_message ( & message) ;
409+ return ;
410+ }
388411 if let Err ( err) = self . spawn_db_worker ( ) {
389412 let _ = send_worker_error_message ( & js_value_to_string ( & err) ) ;
390413 }
@@ -1189,6 +1212,31 @@ mod tests {
11891212 ) ;
11901213 }
11911214
1215+ #[ wasm_bindgen_test]
1216+ fn db_worker_failure_stops_after_max_retries ( ) {
1217+ set_global_str ( "__SQLITE_DB_NAME" , "testdb-db-failure-limit" ) ;
1218+ set_global_num ( "__SQLITE_FOLLOWER_TIMEOUT_MS" , 50.0 ) ;
1219+ set_global_num ( "__SQLITE_QUERY_TIMEOUT_MS" , 50.0 ) ;
1220+ set_global_str ( "__SQLITE_EMBEDDED_WORKER" , "" ) ;
1221+
1222+ let cfg = worker_config_from_global ( ) . expect ( "config" ) ;
1223+ let state = CoordinatorState :: new ( cfg) . expect ( "state" ) ;
1224+ * state. db_worker_ready . borrow_mut ( ) = true ;
1225+ * state. leader_ready . borrow_mut ( ) = true ;
1226+ * state. ready_signaled . borrow_mut ( ) = true ;
1227+
1228+ state. db_worker_restart_attempts . set ( MAX_DB_WORKER_RESPAWNS ) ;
1229+ state. handle_db_worker_failure ( "still broken" . to_string ( ) ) ;
1230+
1231+ assert ! ( !* state. db_worker_ready. borrow( ) ) ;
1232+ assert ! ( !* state. ready_signaled. borrow( ) ) ;
1233+ assert_eq ! (
1234+ state. db_worker_restart_attempts. get( ) ,
1235+ MAX_DB_WORKER_RESPAWNS + 1
1236+ ) ;
1237+ assert ! ( state. db_worker. borrow( ) . is_none( ) ) ;
1238+ }
1239+
11921240 #[ wasm_bindgen_test( async ) ]
11931241 async fn db_worker_queue_serializes_requests ( ) {
11941242 let results = Rc :: new ( Array :: new ( ) ) ;
0 commit comments