STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

Webhook の重複処理実行を回避してシステムの信頼性を高める

こんにちは、STORES ブランドアプリ や STORES ロイヤリティ の開発をしている ta-chibana です。 STORES では複数のサービスが通信しあうことで実現される機能が多くありますが、STORES で開発されたプロダクト同士の連携に限らず、Shopify などの外部サービスと連携する機能も存在します。

本記事では STORES での Shopify 連携で実装した Webhook の重複処理実行を回避する仕組みについて紹介します。 なお、STORES での Shopify 連携においては shopify_app gem を利用しているため、その前提での実装例となります。

Webhook は重複するもの

Webhook による連携処理を実装する際、同一のイベントを受信した場合の考慮が必要です。 Shopify のドキュメントによると、Shopify の Webhook システムは重複送信を最小限に抑えるよう設計されていますが、完全に排除することはできません。 そのため、イベントを受信する側が重複実行をしないための制御、または複数回実行されても冪等となるような実装をすることで重複したイベントの受信に備える必要があります。

Shopify のドキュメントでは以下の3つのステップで重複回避を実装することを推奨しています。

  1. ヘッダーからイベントIDを取得: X-Shopify-Event-Idヘッダーからイベントの一意識別子を取得する
  2. 永続化ストレージで重複チェック: 既に受信済みの Webhook かどうかを確認する
  3. 重複の場合は処理をスキップ: 同じ Webhook が受信済みの場合は処理をスキップする

重複実行を回避する

まず、Webhook イベントの重複を制御するためのテーブル定義を作成します。

class CreateShopifyWebhookEvents < ActiveRecord::Migration[8.0]
  def change
    create_table :shopify_webhook_events do |t|
      t.string :webhook_identifier, null: false, index: { unique: true }
      t.string :topic, null: false, comment: 'e.g. orders/create, orders/updated, etc.'
      t.datetime :triggered_at, comment: 'イベントの発生日時'

      t.timestamps
    end
  end
end

次に、同一の Webhook を受信した際に重複実行を回避する仕組みを実装します。

module Shopify::WebhookTrackable
  extend ActiveSupport::Concern

  class_methods do
    def handle(data:)
      ApplicationRecord.transaction do
        Shopify::WebhookEvent.create!(
          webhook_identifier: data.webhook_id, # X-Shopify-Event-Id の値が取得できる
          topic: data.topic,
          triggered_at: data.body['updated_at'],
        )
        super # ここで各イベントに対応したジョブが enqueue される
      end
    rescue ActiveRecord::RecordNotUnique
      Rails.logger.warn("Skipped processing Shopify webhook event. webhook_id: #{data.webhook_id}, shop: #{data.shop}, topic: #{data.topic}")
    end
  end
end

webhook_identifier カラムのユニーク制約により、同じ Webhook を受信した際は ActiveRecord::RecordNotUnique が発生し、重複した Webhook のジョブの enqueue が回避されます。 この module を各 Webhook ハンドラーで include することで重複回避が機能します。

class Shopify::OrdersCreateJob < ApplicationJob
  include Shopify::WebhookTrackable  # 重複実行回避機能を有効化

  # ... 処理内容
end

得られたもの

この実装により以下の効果が得られました。

  • 重複処理によるデータの不整合を防ぎ、システムの信頼性を向上させました
  • Webhook の重複処理による予期しない動作を防いだり検知不要なアラートを抑制したりと運用の安定性を向上させました
  • 各 Webhook ハンドラーで統一された重複回避機能により、処理の一貫性を保証しています

ただし、この実装はあくまで同一の Webhook に対する重複処理が行われないようにするための制御であり、enqueue された各ジョブが処理中に中断しリトライされた場合についての冪等性担保は行うことができません。 そのため、各イベントのハンドラーの冪等性担保のための実装はそれぞれで行う必要がある点には注意が必要です。 各ハンドラーの処理を冪等にすることでシステムの信頼性をより高めることができ、有事の際にはそのイベントが処理済みかどうかに関わらず「とりあえずリトライ!」としても問題なくなるため、運用面でもメリットが大きいと考えます。

おわりに

DB 制約を活用した重複実行を回避する仕組みについて紹介しました。 この実装により、Webhook の重複処理による問題を効果的に回避し、システムの信頼性を向上させることができました。

この方法は Shopify との連携に限らずとも利用可能なものなので、サービス間連携の実装の際に参考にしていただけると幸いです。 また、近しい話題として以下の記事も非常に参考になるのでおすすめです。

product.st.inc

今後もこのような信頼性向上の取り組みを継続し、より安定したシステムの構築に取り組んでいきます。