fix(websocket): lossless EOF signal + pending sweep (WS-02, CON-02)

- Replace the axum/tungstenite pump paths' Notify-based read-EOF signal
  with a retained tokio watch channel: a late subscriber (monitor
  spawned after session setup, or pump EOF before the receiver is
  taken) still observes EOF (WS-02).
- from_wss drop monitor: on EOF (or session close) fail all pendings
  retryable, then keep sweeping the pending map every 1 s — calls
  registered after the initial fail_all (the forgotten-session import
  path) resolve instead of hanging (CON-02).
- Tests: drop-during-registration race variants (forget + held
  session) and a post-EOF registration resolved via the sweep; the
  existing no-hang test stays green.

cargo test (219), cargo test --features wss (231, 3x for flake check),
cargo clippy --all-targets -- -D warnings, cargo fmt --check
This commit is contained in:
2026-08-29 09:36:34 +00:00
parent 5ff88756eb
commit a9ac6f6cbd
4 changed files with 223 additions and 42 deletions
+1 -21
View File
@@ -95,33 +95,13 @@ impl WsPumps {
/// EOF signaled at any point — including before the receiver is
/// taken or the observer starts awaiting — is still observed, and
/// may be observed repeatedly. Used by `from_wss`'s
/// connection-drop monitor (ADR-070); await it with
/// [`wait_for_eof`].
/// connection-drop monitor (ADR-070).
#[cfg(feature = "wss")]
pub(crate) fn read_eof(&self) -> tokio::sync::watch::Receiver<bool> {
self.read_eof.subscribe()
}
}
/// Resolves once the given WS read-EOF watch receiver observes `true`.
///
/// Lossless by construction: a `watch::Receiver` retains the latest
/// value, so an EOF signaled before this call (or before the receiver
/// existed) is still observed — the property a one-shot `Notify` lacked
/// (WS-02). Returns early (treated as EOF) if the sender half is
/// dropped, e.g. the read pump was aborted.
#[cfg(feature = "wss")]
pub(crate) async fn wait_for_eof(rx: &mut tokio::sync::watch::Receiver<bool>) {
loop {
if *rx.borrow_and_update() {
return;
}
if rx.changed().await.is_err() {
return;
}
}
}
/// Split a `WebSocket` into the byte stream + the pump tasks. The
/// adapter is the single seam between axum's WS and alkcall's
/// byte-oriented channels machinery; shared with `from_wss`.