メインコンテンツへスキップ

ウェブフック 再試行 ロジック

Omise ウェブフック の自動再試行スケジュール、べき等パターンの実装、失敗処理、監視についての完全なガイド。

概要

Omise は自動的 にウェブフック を再試行 します。エンドポイント がタイムアウト または 5xx エラー を返す場合、ウェブフック は指数 バックオフ で再試行 されます。

再試行 スケジュール

Omise は以下の指数 バックオフ スケジュール を使用 してウェブフック を再試行 します:

試行待機時間説明
11時間最初の再試行
22時間
34時間
48時間
512時間
624時間
724時間
824時間
924時間
1024時間最終試行 (最大3日)

べき等 パターン

重複イベント を安全に処理 するため、べき等 パターン を実装 する必要があります。

パターン 1: イベント ID トラッキング

Redis またはデータベース でイベント ID をトラッキング:

// Node.js - Redis を使用 したべき等 処理
const redis = require('redis');
const client = redis.createClient();

async function handleWebhook(event) {
const eventId = event.id;
const key = `webhook:${eventId}`;

// すでに処理済み か どうかを確認
const exists = await client.exists(key);
if (exists) {
console.log(`Duplicate event: ${eventId}`);
return;
}

// イベント を処理
try {
await processEvent(event);

// Redis にセット (24時間後に期限切れ)
await client.setex(key, 86400, '1');
} catch (error) {
console.error(`Error processing event: ${error.message}`);
// 処理済み としてマーク しない - 再試行 を許可
throw error;
}
}

パターン 2: データベース 制約

データベース 内 の一意 の制約 を使用:

# Python - SQLAlchemy を使用 したべき等 処理
from sqlalchemy import Column, String, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

Base = declarative_base()

class ProcessedWebhook(Base):
__tablename__ = 'processed_webhooks'
event_id = Column(String, primary_key=True, unique=True)
created_at = Column(DateTime, default=datetime.utcnow)

def handle_webhook(event):
session = Session()

try:
# すでに処理済み か どうかを確認
existing = session.query(ProcessedWebhook).filter_by(
event_id=event['id']
).first()

if existing:
print(f"Duplicate event: {event['id']}")
return

# イベント を処理
process_event(event)

# 記録 を追加
webhook_record = ProcessedWebhook(event_id=event['id'])
session.add(webhook_record)
session.commit()
except IntegrityError:
# 別 のプロセス が既に処理済み
session.rollback()
print(f"Event already processed: {event['id']}")
finally:
session.close()

パターン 3: ビジネスロジック べき等性

ビジネスロジック のべき等性 を確保:

# Ruby - Rails でのべき等 処理
def handle_charge_complete(charge)
charge_id = charge['id']

# すでに処理済み か どうかを確認 (データ から)
existing_order = Order.find_by(omise_charge_id: charge_id)
if existing_order
puts "Charge already processed: #{charge_id}"
return
end

# チャージ を処理
Order.create!(
omise_charge_id: charge_id,
amount: charge['amount'],
status: 'completed'
)

# 確認メール を送信
send_confirmation_email(existing_order.customer)
end

メッセージキュー での実装

本番環境 での信頼性 の高い処理:

Bull (Node.js)

// Node.js - Bull を使用 したウェブフック 処理
const Queue = require('bull');
const webhookQueue = new Queue('webhooks', {
redis: { host: 'localhost', port: 6379 }
});

// ウェブフック エンドポイント
app.post('/webhooks/omise', async (req, res) => {
// シグネチャ を検証
if (!verifySignature(req)) {
return res.status(401).json({ error: 'Invalid signature' });
}

// キュー に追加
await webhookQueue.add(req.body, {
jobId: req.body.id, // べき等キー として使用
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000
},
removeOnComplete: true
});

// 迅速に レスポンス
res.json({ received: true });
});

// キュー プロセッサー
webhookQueue.process(async (job) => {
const event = job.data;
console.log(`Processing webhook: ${event.key}`);

// べき等 をチェック
const processed = await isEventProcessed(event.id);
if (processed) {
console.log(`Event already processed: ${event.id}`);
return;
}

// イベント を処理
await processEvent(event);

// 記録 を追加
await markEventAsProcessed(event.id);
});

Celery (Python)

# Python - Celery を使用 したウェブフック 処理
from celery import Celery
from flask import Flask, request, jsonify

app = Flask(__name__)
celery = Celery(app.name, broker='redis://localhost:6379')

@app.route('/webhooks/omise', methods=['POST'])
def handle_webhook():
# シグネチャ を検証
if not verify_signature(request):
return jsonify({'error': 'Invalid signature'}), 401

# タスク を キュー に追加
event = request.json
process_webhook.delay(event, event_id=event['id'])

# 迅速に レスポンス
return jsonify({'received': True}), 200

@celery.task(bind=True, max_retries=3)
def process_webhook(self, event):
try:
# べき等 をチェック
if is_event_processed(event['id']):
print(f"Event already processed: {event['id']}")
return

# イベント を処理
handle_event(event)

# 記録 を追加
mark_event_as_processed(event['id'])
except Exception as e:
# 指数 バックオフ で再試行
raise self.retry(exc=e, countdown=2 ** self.request.retries)

Sidekiq (Ruby)

# Ruby - Sidekiq を使用 したウェブフック 処理
class WebhookJob
include Sidekiq::Worker
sidekiq_options retries: 3

def perform(event_json)
event = JSON.parse(event_json)

# べき等 をチェック
if event_processed?(event['id'])
puts "Event already processed: #{event['id']}"
return
end

# イベント を処理
handle_event(event)

# 記録 を追加
mark_event_as_processed(event['id'])
end

private

def event_processed?(event_id)
ProcessedWebhook.exists?(event_id: event_id)
end

def mark_event_as_processed(event_id)
ProcessedWebhook.create!(event_id: event_id)
end
end

# Rails コントローラー
class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token

def omise
# シグネチャ を検証
unless verify_signature(request)
return render json: { error: 'Invalid signature' }, status: 401
end

# ジョブ を キューに追加
WebhookJob.perform_async(request.body.read)

# 迅速に レスポンス
render json: { received: true }
end
end

失敗処理

デッドレターキュー

処理 に失敗したイベント を処理:

// Node.js - デッドレターキュー を使用 した処理
const webhookQueue = new Queue('webhooks');
const dlq = new Queue('webhooks-dlq');

// 失敗 イベント をデッドレターキュー に移動
webhookQueue.on('failed', (job, err) => {
console.error(`Job ${job.id} failed:`, err.message);
dlq.add(job.data, {
originalJobId: job.id,
failureReason: err.message,
failureTime: new Date()
});
});

// デッドレターキュー を手動で処理
dlq.process(async (job) => {
console.log(`Processing failed event: ${job.data.originalJobId}`);
// 管理者に通知を送信、ログを記録など
});

ログ と監視

ウェブフック メトリクス

// Prometheus メトリクス
const prometheus = require('prom-client');

const webhookCounter = new prometheus.Counter({
name: 'webhook_events_total',
help: 'Total webhook events received',
labelNames: ['event_type', 'status']
});

const webhookDuration = new prometheus.Histogram({
name: 'webhook_processing_seconds',
help: 'Time to process webhook',
labelNames: ['event_type']
});

app.post('/webhooks/omise', async (req, res) => {
const startTime = Date.now();
const event = req.body;

try {
await processEvent(event);
webhookCounter.inc({
event_type: event.key,
status: 'success'
});
} catch (error) {
webhookCounter.inc({
event_type: event.key,
status: 'error'
});
} finally {
webhookDuration.observe(
{ event_type: event.key },
(Date.now() - startTime) / 1000
);
}

res.json({ received: true });
});

手動再試行

ダッシュボード から

  1. ダッシュボード に移動
  2. 設定 > ウェブフック を開く
  3. エンドポイント を選択
  4. 最近の配信 をビュー
  5. 失敗したイベント を見つけて 再送信 をクリック

API 経由

# ウェブフック を再トリガー
curl https://api.omise.co/webhooks/events/{event_id}/resend \
-u skey_test_YOUR_KEY: \
-X POST

ベストプラクティス

1. 必ずべき等 を実装

すべてのウェブフック ハンドラー でべき等 を確保:

async function handleWebhook(event) {
// イベント が既に処理済み か どうかをチェック
const eventId = event.id;
const exists = await checkIfProcessed(eventId);
if (exists) return;

// イベント を処理
await processEvent(event);

// 処理済み としてマーク
await markAsProcessed(eventId);
}

2. 迅速に レスポンス

ウェブフック エンドポイント は 10秒以内 にレスポンス する必要があります:

@app.route('/webhooks/omise', methods=['POST'])
def handle_webhook():
# 迅速に ストレージ にキュー を作成
event = request.json
webhook_queue.put(event)

# 迅速に レスポンス
return jsonify({'received': True}), 200

# バックグラウンド で処理
def process_webhooks():
while True:
event = webhook_queue.get()
handle_event(event)

3. デッドレターキュー を使用

処理 に失敗したイベント をトラッキング:

webhookQueue.on('failed', async (job, err) => {
// デッドレターキュー に移動
await deadLetterQueue.add({
originalEvent: job.data,
error: err.message,
failedAt: new Date()
});

// 管理者 に通知
notifyAdmins(`Webhook processing failed: ${err.message}`);
});

トラブルシューティング

イベント が重複 して処理 される

原因: べき等 が実装 されていない

解決策: すべてのウェブフック ハンドラー でべき等 を実装

タイムアウト

原因: ウェブフック ハンドラー が遅すぎる

解決策: キュー を使用 して非同期 で処理

関連リソース

次のステップ

  1. べき等 パターン を選択
  2. メッセージキュー をセットアップ
  3. デッドレターキュー を実装
  4. メトリクス と監視 をセットアップ
  5. 手動再試行 プロセス をテスト