Skip to content

Commit fd8c2f7

Browse files
committed
ai comment updates
1 parent 69d9b9b commit fd8c2f7

3 files changed

Lines changed: 66 additions & 17 deletions

File tree

packages/sqlite-web-core/src/coordination.rs

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ pub enum LeadershipRole {
2727
Follower,
2828
}
2929

30+
const MAX_DB_WORKER_RESPAWNS: u32 = 3;
31+
3032
pub 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

140143
pub 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;\nself.__SQLITE_DB_NAME = {};\nself.__SQLITE_FOLLOWER_TIMEOUT_MS = {};\nself.__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());

packages/sqlite-web/src/worker_template.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub fn generate_self_contained_worker(db_name: &str) -> String {
66
let encoded = serde_json::to_string(db_name).unwrap_or_else(|_| "\"unknown\"".to_string());
77
let embedded_body = serde_json::to_string(include_str!("embedded_worker.js"))
88
.unwrap_or_else(|_| "\"\"".to_string());
9+
// __SQLITE_EMBEDDED_WORKER stores the JSON-encoded embedded worker body (embedded_body) so the coordinator can spawn a separate DB worker (see coordination.rs:301-313); set when embedded-worker mode is used and consumers must JSON-decode before instantiating the worker.
910
let prefix = format!(
1011
"self.__SQLITE_DB_NAME = {};\nself.__SQLITE_FOLLOWER_TIMEOUT_MS = 5000.0;\nself.__SQLITE_QUERY_TIMEOUT_MS = 30000.0;\nself.__SQLITE_EMBEDDED_WORKER = {};\n",
1112
encoded, embedded_body

svelte-test/package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)