Skip to content

fix: detect half-open WebSocket connections via read-idle watchdog#22

Merged
hitalin merged 1 commit into
mainfrom
fix/ws-halfopen-watchdog
Jun 17, 2026
Merged

fix: detect half-open WebSocket connections via read-idle watchdog#22
hitalin merged 1 commit into
mainfrom
fix/ws-halfopen-watchdog

Conversation

@hitalin

@hitalin hitalin commented Jun 17, 2026

Copy link
Copy Markdown
Owner

概要

Android で WebSocket 常時接続が切れる問題(hitalin/notedeck#506 / #507)の土台修正。設計の詳細は hitalin/notedeck#640 §B / §E。

根本原因

ws_loop の read 側に死活検知が無く、半開(ゾンビ)接続を検知できなかった:

  • 30s 周期で client ping は送るが、その応答(Pong)を監視していない(_ => {} で握り潰し)
  • read idle timeout が無く read.next() が無限ブロック → サーバー送信が静かに止まっても OS の TCP 再送タイムアウト(分オーダー)まで顕在化しない

ブラウザ/PWA は内蔵 WS が死活管理を肩代わりするため目立たないが、生 WS を自前管理する notecli では Android(WiFi 省電力 / CGNAT)で顕在化していた。

変更(src/streaming.rs のみ)

  • read-idle watchdog(§B): select ループに 4 本目の arm を追加。Some(Ok(_))(data / server Ping / 自分の ping への Pong)= あらゆる inbound フレームで 90s 締切をリセットし、elapse したら WsExitReason::Disconnected を返して既存の指数バックオフ再接続に合流。catch-all _ => {} は維持。
    • WS_READ_IDLE_TIMEOUT = 90sWS_PING_INTERVAL = 30s の 3 倍。Pong 2 回欠落まで許容)。
    • 安全性は「標準 Misskey(ws ライブラリ autoPong)が client ping に Pong を返す」前提に依存。Some(Ok(_)) 全般でリセットするため、通常トラフィックでも延命でき誤切断に強い。
  • Equal Jitter(§E): 再接続バックオフ sleep を [backoff/2, backoff] にランダム化(backoff_secs は書き戻さない)。複数アカウントの同時再接続を脱同期しつつ、fast-fail での hot retry loop を floor で防ぐ。

テスト

  • jitter が常に [backoff/2, backoff] 内(1000 サンプル × 6 段、from_secs_f64 panic 回避も担保)
  • WS_READ_IDLE_TIMEOUT >= WS_PING_INTERVAL * 3 の不変条件
  • 既存 164 テストに回帰なし

ローカルで CI 同条件(--no-default-features)の build / clippy -D warnings / test を確認済み。

注意

  • 半開接続のランタイム再現検証(機内モード/NIC down)と実機 Android(Doze)は別途。机上検証では「機構が追加されたこと」までを担保。
  • このマージ単体では notedeck の挙動は変わらない(notedeck 側の rev pin bump で初めて反映)。#507 の grace(notedeck 側)と同一リリースに揃えること(notedeck#640 §M)。

Refs hitalin/notedeck#506, hitalin/notedeck#507

Add a read-idle watchdog to the streaming WS loop: any inbound frame
(data, a server Ping, or the Pong replying to our 30s keepalive ping)
resets a 90s deadline; if it elapses the socket is treated as half-open
(a zombie connection where the OS still reports ESTABLISHED but the peer
is silently gone) and a reconnect is forced. This closes the gap where a
silently-dropped connection — common on mobile/Android with WiFi
power-save or CGNAT — was never detected, because the read side blocked
forever and pongs were swallowed by a catch-all arm.

Also jitter the reconnect backoff (Equal Jitter, [backoff/2, backoff]) to
de-sync reconnects across accounts while keeping a floor, so a
fast-failing connect can't spin into a hot retry loop.

Refs hitalin/notedeck#506, hitalin/notedeck#507

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@hitalin hitalin self-assigned this Jun 17, 2026
@hitalin hitalin merged commit a04ea9e into main Jun 17, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant