🔫

Cloud Firestore トリガーが2回発火されるのを防ぐ

2020/12/02に公開

Firestore の書き込みによりトリガーされる Cloud Functions は1つのイベントごとに最低1回実行されますが、複数回実行されることがあります。

引用
Cloud Firestore トリガーのドキュメント
制約と保証より

イベントは必ず 1 回以上処理されますが、1 つのイベントで関数が複数回呼び出される場合があります。「正確に 1 回」のメカニズムに依存することは避け、べき等になるように関数を記述してください。

べき等とはなんぞや
Wikipediaより引用
冪等

大雑把に言って、ある操作を1回行っても複数回行っても結果が同じであることをいう概念である。

何回発火されても結果が同じになるということ。

なにか値を保存するとかなら何回発火されたところで問題ないのですが、e-mailを送信するとかだと同じ内容の e-mail が 2 通送信されてしまう、保存されている値をインクリメントするとかだと想定より多くインクリメントされてしまったりします。

じゃあ1イベントごとに1回以上発火されてしまうとマズい場合に多重発火をどう防げばいいのか。

Cloud Functions pro tips: Building idempotent functionsによるとイベントを識別するイベントIDを利用すればいいようです。
1イベントにつき複数回発火してしまった時に、イベントIDを利用して既に発火済みかどうかを確認して処理をストップすればいいってことになます。

というわけなので GO のクラウドファンクションをこんな感じに書きました。

func FooBar(ctx context.Context, e FirestoreEvent) error {
  ctxMetadata, _ := metadata.FromContext(ctx)
  triggered := wasTriggered(ctxMetadata.EventID)
	if !triggered {
	//多重発火してほしくない処理
	}

    return nil
}


func wasTriggered(eventId string) bool {
	opt := option.WithCredentialsFile(os.Getenv("SERVICE_ACCOUNT_JSON"))
	ctx := context.Background()
	app, err := firebase.NewApp(ctx, nil, opt)
	var triggered bool = false
	if err != nil {
		fmt.Println(fmt.Errorf("error initializing app: %v", err))
	}
	client, err := app.Firestore(ctx)
	if err != nil {
		fmt.Println(fmt.Errorf("error initializing client: %v", err))
	}
	ref := client.Collection("trigger_events").Doc(eventId)
	client.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error {
		doc, _ := tx.Get(ref)
		if doc.Exists() {
			log.Println("EventID:" + eventId + "has already triggered.")
			triggered = true
		}
		return nil
	})
	if !triggered {
		ref.Create(ctx, map[string]interface{}{})
	}
	defer client.Close()
	return triggered
}

Cloud Function がトリガーされた際、 FireStore に trigger_events というコレクションを用意して、イベントIDをドキュメントIDとしたドキュメントを生成します。
ドキュメントを生成する前にイベントIDがドキュメントIDとなっているドキュメントが既に存在していた場合は、イベントが既に一度実行されているので、処理を行いません。

Discussion