Prepare-Challenge パターン設計書 v0.2
概要
外部システムとの非同期処理を確実に完了させるためのデザインパターンです。
「準備(Prepare)」と「挑戦(Challenge)」の2つのフェーズ、そしてコールバックによる結果処理を組み合わせることで、外部システムの状態を考慮しつつ、確実な処理完了を目指します。
解決する問題
- 外部システムの処理能力や状態に左右される非同期処理を確実に完了させたい
- 処理の完了を保証しつつ、外部システムに過度な負荷をかけたくない
- 処理の状態を追跡可能にしたい
- コールバックを用いた非同期処理の結果に応じて適切なリトライ制御を行いたい
パターンの構成要素
1. Prepare(準備)フェーズ
外部システムが処理を受け付けられる状態かを確認します。
- 目的: 処理実行の準備が整っているかの判断
-
判断基準の例:
- システムの負荷状態
- 同時実行数の制限
- 前回の処理完了からの経過時間
- 依存する他の処理の完了状態
2. Challenge(挑戦)フェーズ
実際の処理を実行します。
- 目的: 処理の実行と初期状態の記録
-
実行内容:
- 外部APIの呼び出し
- 処理の受付記録
- 一意なRequestIDの発行
3. Callback(コールバック)フェーズ
外部システムからの処理結果を受け取り、適切な後続処理を実行します。
- 目的: 処理結果の受信と適切なアクション実行
-
実行内容:
- 結果の永続化
- リトライ判定
- 必要に応じた再キュー
4. ChallengeResult(挑戦結果)の記録
処理の結果を永続化します。
- 目的: 処理の追跡可能性の確保
-
記録内容:
- RequestID
- 処理のジョブID
- 実行時刻
- 処理状態(処理中/成功/失敗)
- エラー情報(発生時)
- リトライ回数
シーケンス図
実装例(TypeScript)
interface PrepareChallenger<T, R> {
prepare(): Promise<boolean>;
challenge(): Promise<string>; // RequestIDを返す
logChallengeStart(requestId: string): Promise<void>;
getRetryCount(): number;
incrementRetryCount(): void;
}
interface CallbackHandler<T> {
handleCallback(result: T): Promise<void>;
shouldRetry(error: Error): boolean;
logCallbackResult(result: T): Promise<void>;
}
class OrderProcessingChallenger implements PrepareChallenger<void, string>, CallbackHandler<OrderResult> {
private retryCount = 0;
private readonly maxRetries = 3;
async prepare(): Promise<boolean> {
const status = await externalSystem.checkStatus();
return status.canAcceptNewJobs;
}
async challenge(): Promise<string> {
const requestId = generateRequestId();
await externalSystem.processOrder(this.orderData, {
callbackUrl: `${CALLBACK_BASE_URL}/${requestId}`
});
return requestId;
}
async logChallengeStart(requestId: string): Promise<void> {
await db.challenges.insert({
requestId,
timestamp: new Date(),
status: 'processing',
retryCount: this.retryCount
});
}
async handleCallback(result: OrderResult): Promise<void> {
await this.logCallbackResult(result);
if (!result.success && this.shouldRetry(result.error)) {
await this.requeueForRetry();
}
}
shouldRetry(error: Error): boolean {
if (this.retryCount >= this.maxRetries) return false;
// リトライ可能なエラーの判定
return [
'TEMPORARY_UNAVAILABLE',
'RESOURCE_CONFLICT',
'NETWORK_ERROR'
].includes(error.code);
}
async logCallbackResult(result: OrderResult): Promise<void> {
await db.challenges.update({
requestId: result.requestId,
status: result.success ? 'succeeded' : 'failed',
error: result.error,
completedAt: new Date()
});
}
getRetryCount(): number {
return this.retryCount;
}
incrementRetryCount(): void {
this.retryCount++;
}
private async requeueForRetry(): Promise<void> {
this.incrementRetryCount();
await sqs.sendMessage({
QueueUrl: QUEUE_URL,
MessageBody: JSON.stringify({
...this.orderData,
retryCount: this.retryCount
})
});
}
}
// SQSメッセージハンドラー
async function handleMessage(challenger: PrepareChallenger<any, any>) {
if (challenger.getRetryCount() >= maxRetries) {
throw new Error('最大リトライ回数超過');
}
const isPrepared = await challenger.prepare();
if (!isPrepared) {
await requeueMessage();
return;
}
try {
const requestId = await challenger.challenge();
await challenger.logChallengeStart(requestId);
} catch (error) {
challenger.incrementRetryCount();
throw error; // SQSが自動でリトライ
}
}
// コールバックハンドラー
async function handleCallback(
handler: CallbackHandler<any>,
result: any
) {
await handler.handleCallback(result);
}
パターン適用のメリット
-
処理の確実性
- 外部システムの状態を考慮した実行制御
- コールバックベースの結果確認
- きめ細かいリトライ制御
-
システムの保護
- 外部システムへの負荷制御
- 処理の分散化
- エラーの適切な分類と処理
-
運用性
- 処理状態の可視化
- 障害時の追跡容易性
- リトライポリシーの柔軟な設定
-
拡張性
- 新しい処理タイプへの容易な適用
- 処理フローのカスタマイズ容易性
- コールバック処理の柔軟な実装
適用シーン
-
注文処理システム
- 受注登録
- 在庫引当
- 出荷指示
-
バッチ処理システム
- データ同期
- レポート生成
- マスタ更新
-
外部連携処理
- API連携
- ファイル転送
- メッセージング
実装時の注意点
-
タイムアウト設定
- Prepareフェーズの適切なタイムアウト設定
- Challengeフェーズの実行時間考慮
- コールバック待ち時間の設定
-
リトライ戦略
- リトライ可能なエラーの明確な定義
- 適切なリトライ回数設定
- エクスポネンシャルバックオフの考慮
-
デッドレター対策
- 最大リトライ回数超過時の処理
- エラー通知の実装
- 永続的なエラーの適切な判断
-
監視設計
- 処理の成功/失敗率の監視
- 処理時間の監視
- キュー長の監視
- コールバック受信率の監視