あるシステムで以下のような処理を行っている
- Firebase の Storage にファイルをアップロード
- そのファイルを Functions でダウンロードして色々処理
- 加工した情報を Firestore に保存
Firestore への保存処理は Functions でadmin.firestore().collection('...').doc(fileName).set(...)
のようにset
関数を使って行っている
この Firestore への保存処理が成功する時と失敗する時があった
Functions が原因の可能性が高いと思ったが、エラーも出ておらず、成功する時と失敗する時があるのでちゃんとした再現が難しく、調査に難航した
Firebase は Spark プラン(無料のやつ)
Firebase プロジェクトのリージョンはnam5 (us-central)
Firestore のリージョンはnam5 (us-central)
やったこと、やろうとしたこと
- 何かプランの制限にかかっていないか確認
- 試しに Functions のリージョンを変えてみる
- Firebase リソースのリージョンをすべて東京にする
- Firestore へ反映されていなかったらリトライする
何かプランの制限にかかっていないか確認
以下を確認
https://firebase.google.com/docs/functions/quotas
とくに大丈夫そう
試しに Functions のリージョンを変えてみる
Functions はデフォルトではus-central1
で実行される
東京リージョンに変えて実行してみた
functions .region('asia-northeast1') .storage.object() .onFinalize(...)
Functions のダッシュボード上で以下に変わっていればオッケー
なぜか Functions のリージョンを東京に変えたら失敗しなくなったっぽい(何回か試しただけだけど)
でも Storage も Firestore もリージョンはnam5 (us-central)
なので、Functions の実行環境として 1 番近いのはus-central1
である
この検証結果は忘れることにした
Firebase リソースのリージョンをすべて東京にする
Firebase プロジェクトのリージョンは後から変更できない
変えるならば新規プロジェクトを作り、全データ移行および設定し直しをする必要がある
かなり面倒だからやめた。。
ちなみに Firebase プロジェクトをasia-northeast1
リージョンに変更しても、Firebase の主要リソースはすべて利用できる
また、自分が調べた限りasia-northeast1
リージョンで不利になるのは以下だけ
- Firebase Hosting で Functions を使って動的コンテンツを配信するのは
us-central1
のみをサポート - Realtime Database は常に
us-central1
になる
参考
https://firebase.google.com/docs/functions/locations?hl=ja#background
Firestore へ反映されていなかったらリトライする
結局、失敗したらリトライ処理をするプログラムを書くことにした
const doSet = async (): Promise<void> => { admin .firestore() .collection('...') .doc(fileName) .set(...) await new Promise(resolve => setTimeout(resolve, 120000)) // Sleep for 2 minutes return admin .firestore() .collection('...') .doc(fileName) .get() .then(doc => { if (!doc.exists) return Promise.reject() const data = doc.data() return newData === data ? Promise.resolve() : Promise.reject() }) .catch(() => Promise.reject()) }
けっこう省略してるが、doSet
関数は以下の処理を行っている
admin.firestore().collection('...').doc(fileName).set(...)
で Firestore のドキュメントを登録/更新- 2 分スリープ
admin.firestore().collection('...').doc(fileName).get()
で Firestore のドキュメントを取得- 取得したドキュメントをチェック
- ドキュメントが存在しない、または更新されていない場合は
Promise.reject()
を返す - ドキュメントが登録/更新されている場合は
Promise.resolve()
を返す
- ドキュメントが存在しない、または更新されていない場合は
doSet
は以下のように呼ぶ
doSet() .catch(doSet) .catch(doSet) .catch(() => console.error(`Document not updates`))
最大 3 回doSet
をコールする
3 回目のコールでdoSet
がPromise.reject()
を返せば、エラーログを出して処理終了
いずれかのdoSet
でPromise.resolve()
を返せば、キャッチされないので処理終了
本当は Functions でエラー時にアラートを仕込めたらよいが Firebase コンソール上ではできなそう
GCP の Stackdriver Logging(有料)を使って色々設定すればできるかも
また、注意点として、Functions のデフォルトのタイムアウト時間は 60 秒なので以下のように変更しておく
functions .runWith({ timeoutSeconds: 540 }) .storage.object() .onFinalize(...)
540 秒(9 分)が最大値
Functions のダッシュボード上で以下に変わっていればオッケー
また、runWith
オプションで CPU のコア数やメモリも増やせる
参考
https://firebase.google.com/docs/functions/manage-functions?hl=ja#set_timeout_and_memory_allocation
さいごに
Firebase で最初にプロジェクトを作ったとき、ちゃんと東京リージョンにしておけば、今回の問題は起きなかったかもしれない
Functions のプログラム全体は以下
https://github.com/kurosame/glossary/blob/master/functions/src/index.ts