sqlite: the listen bridge takes the stream bridge's std-thread shape (review 004 item 8) — one dedicated named thread per subscription doing blocking_send, not a spawn_blocking task (past ~500 listeners the blocking-pool ceiling queued the whole engine's seams); spawn failure unsubscribes and fails closed with Error::Database, matching the stream bridge's posture; notify.rs + engine-sqlite.md docs aligned; task -> completed

This commit is contained in:
glm-5.3-flash committed 2026-10-11 10:50:24 +00:00
1 parent 0fadf0f224
commit acdef94058
3 files changed
+67 -12

No files matched your search

+24 -9
View File
@@ -9,9 +9,12 @@
//! never produces [`Error::PayloadTooLarge`](alkstore::Error) (ADR-016
//! §5); the payload crosses the notify table as the serde_json
//! serialization's UTF-8 text, the byte form ADR-020 §4 pins.
//! - `listen` — the watcher-fanout bridge: one `spawn_blocking` thread
//! per subscription (`engine-sqlite.md`'s mapping row) looping
//! `recv()` on the substrate's sync feed (1-slot — bursts coalesce)
//! - `listen` — the watcher-fanout bridge: one dedicated `std::thread`
//! per subscription (the stream bridge's aligned shape — review 004
//! item 8; not a tokio `spawn_blocking` per subscription, which
//! would queue the engine's seams behind parked bridges at tokio's
//! blocking-pool ceiling) looping `recv()` on the substrate's sync
//! feed (1-slot — bursts coalesce)
//! and `blocking_send`-ing [`Wake`] values into a tokio channel the
//! [`WakeReceiver`] owns. Wakes are coalesced hints (overtrigger on
//! purpose, ADR-006): the watcher fires on *any* commit, so a wake
@@ -88,6 +91,10 @@ pub(crate) fn notify(
/// against a store whose watcher has *died* (the db file was replaced)
/// fails closed the same way — a new subscription never enrolls onto a
/// dead feed to park forever (review 004 item 2's new-subscriber arm).
/// The bridge runs on a dedicated `std::thread` (the stream bridge's
/// shape — review 004 item 8); a thread-spawn failure unsubscribes and
/// fails closed with [`Error::Database`](alkstore::Error) (the stream
/// bridge's posture).
pub(crate) fn listen(
watcher: Arc<SharedUpdateWatcher>,
closed: Arc<AtomicBool>,
@@ -102,13 +109,21 @@ pub(crate) fn listen(
let (id, rx) = watcher.subscribe().map_err(database_error)?;
let (wake_tx, wake_rx) = tokio::sync::mpsc::channel(WAKE_CHANNEL_CAPACITY);
let channel = channel.to_string();
let _bridge = tokio::task::spawn_blocking(move || {
while let Ok(()) = rx.recv() {
if wake_tx.blocking_send(Wake::new(channel.clone())).is_err() {
break;
let spawned = std::thread::Builder::new()
.name(format!("alkstore-listen-bridge-{channel}"))
.spawn(move || {
while let Ok(()) = rx.recv() {
if wake_tx.blocking_send(Wake::new(channel.clone())).is_err() {
break;
}
}
}
});
});
if let Err(e) = spawned {
watcher.unsubscribe(id);
return Err(Error::database(std::io::Error::other(format!(
"failed to spawn listen bridge thread: {e}"
))));
}
Ok(Box::new(SqliteWakeReceiver {
wake_rx,
watcher,