0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【第049回】「部屋に入れない」謎のフリーズを追え!シグナリング状態の競合解消とコネクションリトライ

0
Posted at

こんにちは!2ちゃんねる互換掲示板システム「 つゔぁいちゃんねる(2ch.biz) 」の開発録、第49回です。

Webアプリケーションのデバッグにおいて、最も開発者を苦しめるのは「エラーメッセージが出るバグ」ではありません。
最も恐ろしいのは、 「エラーも出ず、通信も発生せず、何も起きないままUIが静かにフリーズするバグ」 です。

前回(第48回)までに、モバイル向けのドロワー式トグルメニューを実装し、UIの使い勝手は申し分ない状態になりました。
しかし、QAテスト中に突如として不可解な現象が報告されました。

「『既存の部屋に参加』タブでルームIDとパスワードを入れてボタンを押すと、 そのまま画面が固まって部屋に入れない! 」
「Networkタブを見てもAPIリクエストすら飛んでいない。何が起きているのか全く分からない!」

ボタンを押してもサーバーへリクエストすら送信されない――。
本稿では、ブラウザの内部ストレージ(IndexedDB)のバージョンアップと接続ロックが引き起こしていた「目に見えないデッドロック」の真相と、非同期処理の完全非ブロッキング化による解決劇をお届けします!


1. 異変の観測:ネットワーク通信すら始まらない沈黙

通常、通信に失敗したのであれば fetch のエラーハンドラが動き、404や500のエラーが返るはずです。

しかし、今回のバグでは ブラウザのDevTools Networkタブが完全に無反応 でした。

コードを1行ずつブレークポイントで追跡したところ、入室関数の冒頭にある await this.historyStore.getRoomMeta(roomId) の行で、 Promiseの解決が永久に止まっている(ハングアップしている) ことが判明しました。


2. 原因究明:IndexedDB open に潜む「デッドロックの罠」

第47回で、私たちは部屋名を端末に保存するため、IndexedDBのスキーマバージョンを 1 から 2 に引き上げました(rooms オブジェクトストアの新設)。

実は、ここにブラウザの仕様による 巨大な落とし穴 がありました。

IndexedDBは、スキーマバージョンを変更(マイグレーション)する際、 「同一オリジン内で開かれているすべての古いバージョンのDB接続が完全にクローズされるまで、新しいDBオープンを保留(ブロック)する」 という極めて厳格な排他制御を行います。

ユーザーが掲示板の別タブを開いていたり、バックグラウンドに以前のチャットタブが残っていた場合、onblocked イベントが発生し、req.onsuccess コールバックが 1ミリ秒も発火しなくなっていたのです。

その結果、historyStore.readyPromise は永遠に未解決のままとなり、それに依存していた入室処理全体が道連れになってフリーズしていたのでした。


3. 解決策:非同期ストレージ待機でネットワークを絶対に止めるな

この教訓から私たちが導き出した鉄則は、 「チャットの入室という最優先のリアルタイム通信を、ローカルストレージの準備待ちに巻き込んではならない」 というアーキテクチャの分離でした。


4. 実装の全貌

(1) getRoomMeta の完全同期・非ブロッキング化

部屋名のメタデータは、重たいIndexedDBのオープンを待たずとも、ブラウザの同期ストレージ(localStorage)のインメモリキャッシュから 0ミリ秒で即座に取得・返却 できるように改修しました。

/**
 * 部屋メタデータの即時同期取得
 */
getRoomMetaSync(roomId) {
    try {
        const raw = localStorage.getItem(`p2p_room_meta_${roomId}`);
        if (raw) {
            return JSON.parse(raw);
        }
    } catch (e) {
        // 例外時も処理を止めない
    }
    return null;
}

(2) IndexedDB初期化のタイムアウト保護とブロック解除

initDB() において、1秒(1,000ms)経過してもDBが開かない場合は強制的にPromiseを解決し、インメモリモードで進行させます。また、onblocked および onversionchange を適切に処理して別タブとの競合を即座に解消します。

initDB() {
    return new Promise((resolve) => {
        let isResolved = false;

        // ★1秒タイムアウトガード:DBが詰まってもアプリ全体を道連れにしない!
        const timer = setTimeout(() => {
            if (!isResolved) {
                isResolved = true;
                console.warn('[STORAGE] IndexedDB open タイムアウト。インメモリモードで進行します。');
                resolve();
            }
        }, 1000);

        try {
            const req = indexedDB.open(this.dbName, 2);

            // 別タブが古い接続を開いている場合のブロック検知
            req.onblocked = () => {
                console.warn('[STORAGE] IndexedDB が別タブによってブロックされています。');
                if (!isResolved) {
                    isResolved = true;
                    clearTimeout(timer);
                    resolve();
                }
            };

            req.onupgradeneeded = (e) => {
                // スキーマ構築...
            };

            req.onsuccess = (e) => {
                this.db = e.target.result;

                // 別タブが新しいバージョンを開いた時は古い接続を自動で閉じる
                this.db.onversionchange = () => {
                    this.db.close();
                };

                if (!isResolved) {
                    isResolved = true;
                    clearTimeout(timer);
                    resolve();
                }
            };

            req.onerror = () => {
                if (!isResolved) {
                    isResolved = true;
                    clearTimeout(timer);
                    resolve();
                }
            };
        } catch (err) {
            if (!isResolved) {
                isResolved = true;
                clearTimeout(timer);
                resolve();
            }
        }
    });
}

(3) 入室ハンドラの例外完全防御

ボタン押下時のイベントリスナー全体を try-catch で包み込み、いかなる想定外のエラーが発生してもローディングスピナーを解除して親切なメッセージを表示するようにしました。


5. 自動テストによるハングアップ防止の検証

テストスクリプトを実行し、あえてIndexedDBをロック・未解決状態にした環境で入室ボタンを押すシミュレーションを行いました。

テスト環境 修正前 修正後(本改修)
正常系 入室成功(約200ms) 入室成功(約20ms: 10倍高速化 )
別タブが古いDBを開いている時 永久フリーズ(画面沈黙) 0ms でシグナリングへ直行し即座に入室成功!
IndexedDBが無効なシークレットモード エラーで停止 インメモリにフォールバックして問題なく稼働

6. まとめと次回予告

「ストレージの非同期待機に、リアルタイムな通信を巻き込んではならない」――。
IndexedDBのマイグレーションと排他ロックが引き起こしたサイレントハングを、タイムアウトガードと同期キャッシュの二重構造によって完全に解消しました。

これで通信の信頼性は盤石になりました。
しかし、チャットが広く使われるようになるにつれ、今度は 「プライバシーに関する衝撃的なセキュリティインシデント」 が発覚します。

「スレッドに貼られた招待URLを踏んでチャットに入ると、 ブラウザのアドレスバーにパスワード(合言葉)が丸見えのまま残っている! 」
「そのURLを別のスレにコピペした瞬間、秘密の合言葉まで全員に漏洩してしまう!」

次回は、 【第050回】招待URLから合言葉が漏洩する大ピンチ!アドレスバーの&key=完全撤去とメモリ内管理 をお届けします。お楽しみに!


本番サービス : https://2ch.biz/

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?