😨

Cloud FunctionsのFirestoreトリガーで無限ループが発生した時のこと

2024/03/07に公開

概要

初めてCloud Functionsを起動させる部分で無限ループ(内部ではなくCloud Functions自体が無限起動)が起こったのでメモしました
意外と注意してても発生するまで気付けないので今回は忘れないように記事にしました

Firestoreと無限ループ

まずはFirestoreトリガーについてざっくり

Cloud Firestore トリガー
Cloud Functions を使用すると、クライアント コードを更新することなく、Cloud Firestore 内のイベントを処理できます。Cloud Firestore の変更は、ドキュメント スナップショット インターフェースまたは Admin SDK を使用して行うことができます。
一般的なライフサイクルの場合、Cloud Firestore の関数は次のように動作します。

  • 特定のドキュメントに変更が加えられるのを待ちます。
  • イベントが発生するとトリガーされ、そのタスクを実行します。
  • 指定されたドキュメントに保存されているデータのスナップショットを含むデータ オブジェクトを受け取ります。書き込みイベントまたは更新イベントの場合、データ オブジェクトには、トリガーとなるイベントの前後のデータ状態を表す 2 つのスナップショットが含まれています。
    Cloud Firestore トリガー

FirebaseのFirestore(NoSQL クラウド データベース)にデータが追加・変更・削除があった時に特定のCloud Functionsを発火させることができるというものです
その際Firestoreでいうコレクション(要はテーブルのようなもの)を指定して発火を制御することができます

例えばTasksというコレクションがあったとすれば、そのTasksコレクションにデータを追加すると1行レコードができます このレコードのことをFirestoreではドキュメントと呼びます
このドキュメントが追加・変更・削除された時にCloud Functionsを発火させることができるというものです
これを使うことで例えばTasksの中にドキュメントが追加された時にSlackなどにタスク追加の通知したり、変更された時は変更される前と後で違うコレクションにログを残したりできます

では早速ですがこのTasksコレクションの変更で発火するCloud Functionsが、もしTasksコレクションにドキュメントを追加するという機能だったらどうなるでしょうか

1. Tasksコレクションのドキュメントが追加される
2. Tasksコレクションにドキュメントが追加されたことをトリガーにCloud Functionsが発火
3. Cloud FunctionsによってTasksコレクションにドキュメントが追加される
4. 2が動く

上記のような機能を作ると無限ループが起きます
今回はこれが起こったわけではないですが、注意しなければ簡単に無限ループの可能性があるものということです

今回起こったこと

今回起こったのは上記のようなドキュメントの変更でCloud Functionsを動かす「Firestoreトリガー」と「外部ライブラリのイベントリスナー(Amazon Connect Streams)」が交互に作用して無限ループが起こったというものです
トリガーもイベントリスナーもどちらも何かしらの「イベント」に作用して動くものなので、それぞれ把握しておかないと勝手に交互に上書いたり動いたりしてしまうといった事象です

Amazon Connect Streamsとは

Amazon Connect StreamsとはWebアプリにAmazonConnectの通話機能や通話ステータス(待機、オフライン、受信可能など)をiframeで埋め込める機能とAPIのライブラリです
基本的にはよくある会社のカスタマーサポートセンターが使ったり、お金がかかりますが自分のサービス(Webサイトとか)で電話をかけたりすることができます

Amazon Connect自体はCCPと呼ばれるコールコントロールパネルで通話をしたり、通話中などの現在の通話ステータスの変更を行えます
Amazon Connect StreamsはこのCCPをWebアプリに埋め込むことができるので、現在自社のオペレーションツール(Webアプリ)に埋め込んで使っています

更に通話機能だけではなくステータスを使ってメンバーの皆の稼働状態を見れるようにしたりてるのでそれぞれのステータスの変更などをイベントリスナーで検知してFirestoreにログとして状態を残すことをしています

本題

今自社ではNuxtで作ったWebアプリを運用していますが、このオペレーションツールにログインしているユーザーのテーブルビューを用意しました
取ってきているコレクション名はUsersコレクションで、それぞれ名前やAmazon Connectのステータスを持っています
そして機能としてユーザーのAmazon ConnectのステータスをWebアプリ上で変更できるUIを追加することになりました

これはテーブルのステータスのセルをセレクトボックスにし、クリックしてステータス一覧から変えると、Amazon ConnectのAPIを使ってAmazon Connect側(CCP)のデータを書き換えにいくという仕様です
のでフロントには このセレクトボックスを変えたらAPI通信してね のコードを書けばいいだけなのですが、このログインしているユーザーのデータはFirestoreに保存されています(Usersコレクション)
Amazon Connect側のデータを書き換えただけでは次に読み込んだ時にFirestore側には何も反映されず、CCPではステータスが変わってるのにこのテーブルビューではFirestoreには何もしていないので変わってないことになってしまいます

ではどのようにFirestoreとAmazon Connect側のデータのステータスの整合性を合わせているかというと、まずこのセレクトボックスで表示されてるユーザーのステータスを変えるとFirestoreのUserコレクションのドキュメントに変更を行います
その後Userコレクションのドキュメント変更をトリガーにCloudFunctionが発火します(Firestore トリガー)
そしてこのCloudFunctionがAmazon ConnectのAPIを使って変えられたユーザーのIDとステータスを持ってAmazon Connect側のデータを書き換えにいく というような流れです
これによってFirestoreにあるUserデータのステータスも、Amazon Connect側のデータもちゃんと変わりますね

CCP本体のステータス変更

さてここまで見ていただいたらわかる通りFirestoreに保存されてるステータスとCCP本体のステータスは別物で、片方を変えたら片方も変えないと整合性が合わなくなってしまいます
上記の動作はフロントで変更→Firestoreを変更→CCP本体を変更と整合性を合わせにいく流れですが、逆にCCP本体を変更→Firestoreを変更も作らなければなりません

Amazon Connect Streamsはライブラリなのでページ起動時にCCPで何か変更された際のイベントリスナーを設置することができます
例えば以下はユーザーのステータスがCCP本体を操作して変わった時にフロントで発火するリスナーです

 connect.agent(function (agent) {
    agent.onStateChange(function (agentStateChange) {
       // CCP本体のステータスが変わった時に何かしら処理をする
    })
  })

よって上記のコードを使い、CCP本体のデータが変わった時にUserコレクションのドキュメントにCCP本体で変えたステータスを保存しに行きます

 connect.agent(function (agent) {
    agent.onStateChange(function (agentStateChange) {
       // CCP本体のステータスが変わったらFirestoreに保存しに行く
       updateFirestoreUserStatus(agentStateChange.newState)
    })
  })

これによりCCP本体→Firestoreの書き込みができ、Firestoreを書き換えたらCCP本体のステータスが変わり、CCP本体のステータスを変えたらFirestoreに書き込みを行うという、どちらのステータスも同じで整合性が合っている状態となるわけです

無限ループ

では長くなりましたがどのようにして無限ループが起こったのでしょうか(もう既に交互で動く関数があるので不安しかありませんが)
これはWebアプリ側テーブルビューの操作でユーザーのステータスを変更した後、CCP本体へのステータスを書き換える前に、CCPもしくはWebアプリのどちらかのステータスに変更を加えた時に無限ループが起こります

そうなんとこのWebアプリ側でユーザーのステータスを変更した後、CCP本体にステータスを書き換えにいく時間で3秒ほどタイムラグがあるんですね
その3秒 CCP本体へのステータスを書き換える前にもう1回Webアプリ側のテーブルのステータスを変更したり、CCP本体のステータスを変えてしまうと・・・・

  1. Webアプリ側でステータスを変更しUserドキュメントを書き換える
    Firestore: 稼働中→休憩
    CCP: 稼働中
     

  2. Userドキュメントが書き換わりトリガーCloudFunctionsが発火してAPIでCCP本体のデータを書き換えにいく
    Firestore: 休憩
    CCP: 稼働中→休憩

 

  1. そのタイムラグ途中でCCP本体のステータスを操作してステータスを変更する
    Firestore: 休憩
    CCP: 稼働中→オフライン
     

  2. 3.の動作によりCCP本体のステータスが変更された為、CCPイベントリスナーによってFirestoreのUserドキュメントが書き換わる
    Firestore: 休憩→オフライン
    CCP: オフライン
     

  1. 4.の動作によりFirestoreのUserドキュメントが書き換わったのでトリガーCloud Functionsが発火しCCP本体のデータを書き換えにいく
    Firestore: オフライン
    CCP: オフライン→オフライン(まだ書き変わらない)
     

  2. 2.のトリガーがここで終了し、CCPのステータスが書き換わる
    Firestore: オフライン
    CCP: オフライン→休憩

 

  1. 6.の動作によりCCPイベントリスナーによってFirestoreのUserドキュメントが書き換わる
    Firestore: オフライン→休憩
    CCP: 休憩

 

  1. 4.のトリガーがここで終了し、CCPのステータスが書き換わる
    Firestore: 休憩
    CCP: 休憩→オフライン
     

  2. 7.の動作によりトリガーCloud Functionsが発火しCCP本体のデータを書き換えにいく
    Firestore: 休憩
    CCP: 休憩 or オフライン→ 休憩
     

見てわかる通りFirestoreの書き換えによるトリガーとCCP本体のステータスが書き変わった時のイベントリスナーがはちゃめちゃに作用し、無限ループが起こります
どうしてこうなった

解決策(解決・・・?)

大前提としてFirestoreトリガーを辞めました(使いこなすにはまだ早かった・・・)
WebアプリからFirestoreのステータスを変えた後、追加でHTTPトリガーでAmazon ConnectのAPIを叩く構図に変更しました
これによりFiresotreの書き込みに反応するトリガーが無くなったので無限ループが起こらないようになりました

反省

今回はイベントとしてFirestoreに書き込んだ時とCCP本体のステータスを変えた時の二つのイベントが存在していました
この無限ループはWebアプリとCloud Functions、さらに外部システム(この場合はCCP)の間で発生する複雑なデータ同期の問題で、しっかりイベントを把握してないと起こってしまう問題です

それと今回はWebアプリを操作しながら開発していたのでステータスが荒ぶったのをすぐに認識できました
早急に止めたので大事には至らなかったのでが一歩誤れば大変なことになっていましたね・・・反省

まとめ

データ同期を行う際はイベントとイベントのトリガーが絡み合わないよう整理してから実装すべきでした
特に今回のようなFireStoreトリガーでCCP本体を変更する際にラグがあったので、イベントトリガーの途中でUI操作で起こり得る可能性大だったので要注意です
今後は私も気をつけて実装していこうと思い残した記事でした

CBcloud Tech Blog

Discussion