150 lines
5.8 KiB
Rust
150 lines
5.8 KiB
Rust
//! The notify/listen mechanism's SQLite arm — the wake half
|
|
//! (`sqlite-engine-notify-listen`; `notify_tx` landed with the seam):
|
|
//!
|
|
//! - `notify` — the auto-commit path: acquire the writer slot, run
|
|
//! the substrate's `notify()` SQL function (one statement, one
|
|
//! auto-commit transaction), release. Commit-atomic by SQLite's
|
|
//! autocommit; rollback is impossible on this path (nothing holds
|
|
//! the statement back). No payload limit on SQLite — this engine
|
|
//! 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)
|
|
//! 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
|
|
//! may arrive for unrelated writes, may coalesce, or may repeat —
|
|
//! the receiver delivers `Wake { channel }` only, never payloads or
|
|
//! ids (ADR-008 §3); consumers are idempotent on wake.
|
|
//!
|
|
//! Close semantics (ADR-006, ADR-008 §3): watcher death
|
|
//! (`WatcherDeathGuard`) clears the subscriber feed, the bridge thread
|
|
//! exits, and the receiver sees the close — `recv() -> None`, terminal
|
|
//! (it never reopens); `try_recv` / `recv_timeout` carry
|
|
//! [`Error::Closed`], distinguishable from idle's `Ok(None)`;
|
|
//! `recv_timeout`'s timeout expiry without a wake is `Ok(None)` — the
|
|
//! documented idle arm, never a timeout signal.
|
|
//!
|
|
//! Unsubscribe is the receiver's own [`Drop`]: the substrate prunes
|
|
//! disconnected subscribers lazily (at the next fanout), so the
|
|
//! receiver unsubscribes eagerly on drop — a dropped subscriber leaves
|
|
//! the feed immediately and the bridge thread exits on the disconnect.
|
|
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
use std::time::Duration;
|
|
|
|
use serde_json::Value;
|
|
|
|
use alkstore::{BoxedFuture, Error, Result, Wake, WakeReceiver};
|
|
|
|
use crate::seam::{sqlite_error, with_writer};
|
|
use crate::substrate::{SharedUpdateWatcher, Writer};
|
|
|
|
/// The bridge's tokio-channel capacity. The substrate's 1-slot sync
|
|
/// feed stays the coalescing point; this buffer only smooths the
|
|
/// bridge-thread → consumer handoff.
|
|
const WAKE_CHANNEL_CAPACITY: usize = 16;
|
|
|
|
/// The auto-commit notify path (see the module docs). Name validation
|
|
/// fires at the entry point, before any round trip and before the
|
|
/// payload is touched (ADR-008 §4).
|
|
pub(crate) fn notify(
|
|
writer: Arc<Writer>,
|
|
channel: &str,
|
|
payload: Value,
|
|
) -> BoxedFuture<'static, Result<()>> {
|
|
if let Err(e) = alkstore::validate_shared_name(channel) {
|
|
return Box::pin(async move { Err(e) });
|
|
}
|
|
let channel = channel.to_string();
|
|
Box::pin(async move {
|
|
with_writer(writer, move |conn| {
|
|
let bytes = alkstore::encode_payload(&payload)?;
|
|
let text = String::from_utf8(bytes)
|
|
.map_err(|e| Error::Codec(format!("notify payload must be utf-8 text: {e}")))?;
|
|
conn.query_row(
|
|
"SELECT notify(?1, ?2)",
|
|
rusqlite::params![channel, text],
|
|
|_| Ok(()),
|
|
)
|
|
.map_err(sqlite_error)?;
|
|
Ok(())
|
|
})
|
|
.await
|
|
})
|
|
}
|
|
|
|
/// The listen path (see the module docs). Name validation fires at the
|
|
/// entry point; a listen against a closed store fails closed with
|
|
/// [`Error::Database`](alkstore::Error) (the engine-wide closed-store
|
|
/// posture — `begin_tx`/`notify` yield the same shape).
|
|
pub(crate) fn listen(
|
|
watcher: Arc<SharedUpdateWatcher>,
|
|
closed: Arc<AtomicBool>,
|
|
channel: &str,
|
|
) -> Result<Box<dyn WakeReceiver>> {
|
|
alkstore::validate_shared_name(channel)?;
|
|
if closed.load(Ordering::Acquire) {
|
|
return Err(Error::database(std::io::Error::other(
|
|
"the store is closed",
|
|
)));
|
|
}
|
|
let (id, rx) = watcher.subscribe();
|
|
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;
|
|
}
|
|
}
|
|
});
|
|
Ok(Box::new(SqliteWakeReceiver {
|
|
wake_rx,
|
|
watcher,
|
|
id,
|
|
}))
|
|
}
|
|
|
|
/// The consumer-visible wake receiver: the tokio side of the bridge.
|
|
/// Its [`Drop`] unsubscribes the substrate feed (the subscriber is
|
|
/// pruned eagerly — before the watcher's next fanout would).
|
|
struct SqliteWakeReceiver {
|
|
wake_rx: tokio::sync::mpsc::Receiver<Wake>,
|
|
watcher: Arc<SharedUpdateWatcher>,
|
|
id: u64,
|
|
}
|
|
|
|
impl Drop for SqliteWakeReceiver {
|
|
fn drop(&mut self) {
|
|
self.watcher.unsubscribe(self.id);
|
|
}
|
|
}
|
|
|
|
impl WakeReceiver for SqliteWakeReceiver {
|
|
fn recv<'a>(&'a mut self) -> BoxedFuture<'a, Option<Wake>> {
|
|
Box::pin(async move { self.wake_rx.recv().await })
|
|
}
|
|
|
|
fn try_recv(&mut self) -> Result<Option<Wake>> {
|
|
match self.wake_rx.try_recv() {
|
|
Ok(wake) => Ok(Some(wake)),
|
|
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => Ok(None),
|
|
Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => Err(Error::Closed),
|
|
}
|
|
}
|
|
|
|
fn recv_timeout<'a>(&'a mut self, timeout: Duration) -> BoxedFuture<'a, Result<Option<Wake>>> {
|
|
Box::pin(async move {
|
|
match tokio::time::timeout(timeout, self.wake_rx.recv()).await {
|
|
Ok(Some(wake)) => Ok(Some(wake)),
|
|
Ok(None) => Err(Error::Closed),
|
|
Err(_expired) => Ok(None),
|
|
}
|
|
})
|
|
}
|
|
}
|