Firestoreのオフライン対応をしている場合、Chromeのキャッシュ削除で、データが部分的に取得できなくなる問題
はじめに
Firestoreの知見を持ってる方、何かご存知のことがあれば、気軽にコメントいただければ幸いです。
前提
- FirebaseのJavascript SDKを使い、WEBアプリ(sushiwork.com)を開発している
- Firestoreの
firestore.enablePersistence
を使って、オフライン対応をしている - ブラウザはChromeを使う
- Chrome以外で同じことが発生するかはまだ試していない
問題の再現方法
- ログインして、WEBアプリを使う
- Chromeの設定画面を開き、「プライバシーとセキュリティ」→「閲覧履歴データの削除」を開き、期間を1時間以内に設定し、データを削除する
問題の内容
- Firestoreに存在するはずのドキュメントが取得できない。取得してもundefinedが返却される
- セキュリティールールに問題はない。キャッシュ削除前は正常に取得できている
const user = await firestore
.collection("users")
.doc(userUid)
.get();
上記のようなごく簡単なクエリ。Chromeでキャッシュ削除後は、user.data()にundefinedが入る。user.existsにfalseが入る。
調査した結果分かったこと
- Firestoreでオフライン対応をオンにすると、ブラウザのIndexedDBにオフライン用のデータが保存される
- IndexedDBに「firestore/[DEFAULT/{service_name}/main]」という名前のデータベースがあり、「remoteDocuments」というテーブルに実データが保存されている
- Chromeで直近1時間分のキャッシュを削除すると、remoteDocuments内のデータの一部のデータだけ削除される
- 全部のドキュメントが削除されるのではなく、一部のドキュメントだけ削除される。何かの時間で判定されるっぽい
- 削除されるドキュメントも、完全になくなるのではなく、各ドキュメントのValueのdocumentがnullに変更され、noDocumentの値がnullから値が追加される
- その削除されたドキュメントを取得しようとすると、undefinedが返ってきていた
展開しているのが削除されてしまったドキュメント(document: null)で、展開していないのが存在するドキュメント(document: {...})です。削除されてしまったドキュメントも、Chromeでキャッシュ削除する前は、document: {...}の状態でした。
試行錯誤した内容
リロード
- IndexedDBの値は保持されるので、何も変わらない
Chromeのdevツールで、IndexedDBの中身を全部削除
- リロード後、元通りになり、正常に動く
Cookieとサイトデータを削除
- Chromeの設定画面で特定ドメインのデータを削除できるので、使っているドメインを検索して、その中の「データベース ストレージ」を削除すると、正常に動く
sourceを使う
const user = await firestore
.collection("users")
.doc(userUid)
.get({ source: "server" });
とすれば、必ずサーバーからデータを取得するので、データを取得できるかと思ったが、取得できなかった。
オフライン対応を外す
firestore.enablePersistence
のコードをコメントアウトしてから、リロードすると、正常に動いた。
IndexedDBの中身をコードから削除する
firebase.initializeApp()
した直後に、以下のコードを入れると、IndexedDBの中身が初期化され、正常に動いた。
firebase
.firestore()
.clearPersistence()
.catch(error => {
console.error("Could not enable persistence:", error.code);
});
どうしたいのか
- [最善] Chromeのキャッシュ削除後でも、正常にサーバからデータを取得できる
- [次点] Chromeのキャッシュ削除後などで、データが正常に取得できる状態ではなくなったということを検知する。検知したら、ユーザーにアラートを出し、リロードしてもらう。その時に、clearPersistence()を呼んで、IndexedDBを削除する
その他の懸念点
Chromeのキャッシュ削除以外にも、この挙動が発生するのではないか
firestore.settings({
cacheSizeBytes: 10485760, // 10MB
});
として、オフラインデータのキャッシュサイズを10MBに制限しているが、このキャッシュサイズをオーバーして、不要なデータがガーベッジコレクションされた場合に、同じことが発生しないか懸念がある。
実際、あるはずのデータが存在しないというエラーログが出ているのを、ごくたまにsentryが補足している。もちろん全部、Chromeのキャッシュ削除が原因だったということもありうる。
clearPersistenceは、テストを助けるために準備されているメソッド
Note: clearPersistence() is primarily intended to help write reliable tests that use Cloud Firestore. It uses an efficient mechanism for dropping existing data but does not attempt to securely overwrite or otherwise make cached data unrecoverable. For applications that are sensitive to the disclosure of cached data in between user sessions, we strongly recommend not enabling persistence at all.
と書いてあるので、テスト以外の目的に使うのは、あまり推奨されていない。
対応策の案
ユーザーに手動でキャッシュを削除してもらう
- リロードしても直らないエラーに遭遇した場合には、サービスのドメイン内のキャッシュデータを削除するように案内する
- 無難だが、なるべくユーザーの負担なく解決したい
- その他の不具合対策としても有効なので、これはユーザーに告知しようと思う
リロード時にIndexedDBのデータをクリアしてしまう
- 全てのリロード時にデータをクリアしてしまうと、キャッシュされたデータを有効に使えない
- 問題が発生した時にだけ、データをクリアできると良い
参考URL
- clearPersistence が追加された経緯
- clearPersistenceの使用例
- sourceオプション
エラーが発生した際に、ユーザーに手動でキャッシュを削除してもらう案内を書いた。
一応、実装案を考えたが、対応するかどうかは、もう少し様子を見る。
usersコレクションばかりで発生するのが気になっていて、何か別の原因があるのかもしれないから。
実装案
- あるはずのデータが存在しない場合に発生したエラーをcatchして、vuexとlocalstorageにデータ不整合が発生しているというフラグを入れておく
- vuexにデータ不整合のフラグが存在したら、ユーザーにリロードを促すポップアップを、画面の隅に表示する
- リロード時に、localstorageにデータ不整合のフラグが存在したら、clearPersistence()して、IndexedDBを初期化する。
- localstorageのデータ不整合のフラグを削除する。
補足
- vuexのデータはリロードすると初期化されてしまうので、localstorageも使用している
- localstorageよりいい手段があれば、それを使う
該当エラーが発生していたユーザーに連絡をし、エラー発生前にブラウザのキャッシュをクリアしたかを聞いてみたが、クリアしていないという回答をもらった。
ブラウザのキャッシュをクリアした時以外にも、エラー発生のトリガー条件があるようだ。
firestore.settings({
cacheSizeBytes: 10485760, // 10MB
});
の設定をしているので、キャッシュサイズが10MBを超えた時、データがクリアされた時にエラーが発生してしまうのかもしれない。
cacheSizeBytes: firebase.firestore.CACHE_SIZE_UNLIMITED
に設定すれば解決するかもしれないが、キャッシュを無制限に増やすのは、あまりやりたくない。
「N日ごとに、IndexedDBを初期化する」という対策もいいかもしれない。
下記のコードを、firebase.initializeAppの直後に追加し、リロード後に必ずIndexedDBを初期化するようにしてみた。
firebase
.firestore()
.clearPersistence()
.catch(error => {
console.error("Could not enable persistence:", error.code);
});
しかし、同様のエラーは継続して発生したので、問題解決にならなかった。
また、新たに下記のエラーが発生した。
The client has already been terminated.
効果がないことが分かったので、clearPersistence()の変更はロールバックした。
キャッシュの容量制限を無制限に変更してみたところ、エラーが発生しなくなった。
firestore.settings({
//cacheSizeBytes: 10485760, // 10MB
cacheSizeBytes: firebase.firestore.CACHE_SIZE_UNLIMITED,
});
キャッシュの容量制限が原因かもしれない。
もう少し様子を見てみる。
その後、エラーが発生しなくなっていたが、また同じようなエラーが発生し始めた。
結局、キャッシュの問題ではなかった!
import { getAuth, onAuthStateChanged } from "firebase/auth";
const auth = getAuth();
onAuthStateChanged(auth, function(user) {
if (user) {
// ごくたまにuidを取得できないことがある
console.log(auth.currentUser.uid);
// OK
console.log(user.uid);
}
});
onAuthStateChanged
の中で、 auth.currentUser.uid
ではなく、 user.uid
を使うようにしてからエラーが発生しなくなった。
const user = await firestore
.collection("users")
.doc(userUid)
.get();
に渡していた userUid
に値が入ってないケースがあり、usersドキュメントを取得できないことがあっただけだった。
一つ前のコメントの対応をした後でも、Chromeでキャッシュ削除後に、ドキュメントが取得できなくなる不具合は継続して発生した。
下記の対応を追加した。
- ドキュメントが取得できなくなる不具合が発生した時に、ローカルストレージに値を格納する
- 自動リロードをかける、もしくはユーザーにリロードしてもらう
- リロード時に、ローカルストレージの特定のキーに値が入っていたら、IndexedDBを空にする
initializeApp()、initializeFirestore()をした後に、下記のコードを追加した。
if (localStorage.getItem("clearIndexedDb")) {
clearIndexedDbPersistence(db)
.then(() => {
localStorage.removeItem("clearIndexedDb");
})
.catch(error => {
// eslint-disable-next-line no-console
console.error("Could not enable persistence:", error.code);
});
}
clearIndexedDbPersistence
をこういう使い方で使うことは推奨されていないが、不具合を自動的に修正するにはこれしか手段がなかった。他の手段があれば、ぜひ教えて欲しいです。