🧭

Firestore から Cloud Functions をトリガーするときには、無限ループに気を付けよう!

2022/06/17に公開

こんにちは。地図パズル製作所の都島です。地図パズル製作所ではログイン機能、ユーザ管理機能を絶賛実装中です。何回か前の記事でも同じことを言っていたじゃないか、と思われるかもしれませんが、、、そうなんです!まだできていないんです!!Firebase は得意だと思っていたんですが、分かっていないことがいろいろありまして、、まだ実装中です。プログラマは勉強することが尽きないですね。

では、本題に入る前に、いつも通り地図パズル製作所の宣伝です。

地図パズル製作所では地図パズルで遊べるウェブサイトを作っています。(OGP画像が表示されるようになりました!!)

https://chizu-puzzle.com

※地理院地図を加工して利用しています。

最近もどんどん新しいパズルが追加されていて、先日は東大本郷キャンパスの地図パズルを追加しました。Zenn を使っていらっしゃる方には、東大生、東大卒業生の方も多そうなイメージです。ぜひやってみてください!

https://chizu-puzzle.com/puzzles/tokyo_daigaku_hongo_campus01/

※OpenStreetMap を加工して利用しています。

では、本題に入りましょう!

Firestore + Cloud Functions で無限ループになってしまうソースコード

では、始めに無限ループになってしまうソースコードをお見せしましょう。こんな感じです。

export const onUpdateUser = functions 
  .region("asia-northeast1") 
  .firestore.document("users/{userId}") 
  .onUpdate(async (change, context) => { 
    const data = change.after.data(); 
    const result = await check(data.handle); 
    await db 
      .collection("users") 
      .doc(context.params.userId) 
      .update({ status: result, version: data.version + 1 }); 
  }); 

どこにでもありそうな Cloud Functions のソースコードですね。Firestore の users コレクションが更新されたときに、ハンドルネームをチェックして、チェック結果とバージョンを users コレクションに設定する、という感じです。これを Firestore Emulator で実行してみると、こんな感じになりました。

i  functions: Beginning execution of "onUpdateUser" 
i  functions: Finished "onUpdateUser" in ~1s 
i  functions: Beginning execution of "onUpdateUser" 
i  functions: Finished "onUpdateUser" in ~1s 
i  functions: Beginning execution of "onUpdateUser" 
i  functions: Finished "onUpdateUser" in ~1s 
i  functions: Beginning execution of "onUpdateUser" 
i  functions: Finished "onUpdateUser" in ~1s 
i  functions: Beginning execution of "onUpdateUser" 
i  functions: Finished "onUpdateUser" in ~1s 

はい、無限ループになってますね。いやー、恐ろしいですね。。こんなソースコードを間違って本番環境にデプロイでもしてしまったら、月末の請求がどうなることやら。。。

ではまず、なぜ無限ループになるのか解説していきたいと思います!

なぜ無限ループになるの?

実は、Cloud Functinos から Firestore に書き込みがあった時も、バックグラウンド関数がトリガーするんです。つまり、

クライアントから users コレクションを更新

バックグラウンド関数が起動

バックグラウンド関数から users コレクションを更新

バックグラウンド関数が起動

(無限ループ)

となってしまうわけですね。

ちなみに、これは、Firestore からバックグラウンド関数を起動する場合だけに発生するわけではないようです。例えば、Cloud Storage の画像ファイルが更新されたときに、バックグラウンド関数で画像圧縮して再度画像を更新する、とした場合も無限ループになります。このことは、他の方が記事を書いてらっしゃいますので、見てみてください。

https://note.com/shogoyamada/n/n4df03447d079

無限ループを回避するには?

無限ループを回避する方法を一つ紹介します。こんな感じにしたら無限ループは回避できます。

export const onUpdateUser = functions 
  .region("asia-northeast1") 
  .firestore.document("users/{userId}") 
  .onUpdate(async (change, context) => { 
    const data = change.after.data(); 
    const result = await check(data.handle); 
    const docSnapshot = await db 
      .doc(`userStatuses/${context.params.userId}`) 
      .get(); 
    const statusData = docSnapshot.data();
    await db
      .collection("userStatuses")
      .doc(context.params.userId)
      .set({
        status: result,
        version: statusData == null ? 1 : statusData.version + 1,
      });
  });  });

バックグラウンド関数を起動するコレクションとバックグランド関数から書き込むコレクションを分けました。エミュレータで実行してみるとこうなりました。

i  functions: Finished "onUpdateUser" in ~1s
i  functions: Beginning execution of "onUpdateUser"

ということで、無限ループは回避できました。他にいい方法がありましたら、コメントで教えてください!

ちなみに、バックグラウンド関数は書き込み前と書き込み後が同じ値だった場合は起動しないようです。なので、最初のソースコードでも、version がなければ、起動前と起動後の status が同じであれば無限ループには陥らないようです。でも、無駄に複数回バックグラウンド関数が起動しますし、危険なのでやめたほうが良さそうです。

終わりに

ということで、今日は Firestore + Cloud Functions で無限ループにならないようにする方法を説明しました。これからも地図パズル製作所をよろしくお願いします!

ツイッターもよろしくお願いします!

https://twitter.com/chizu_puzzle

アメブロもやってます!

https://ameblo.jp/chizu-puzzle

Discussion