# バッチ設計書 40-messenger:failed:retry

## 概要

本ドキュメントは、Symfony Messengerコンポーネントが提供する `messenger:failed:retry` コマンドのバッチ設計書である。失敗トランスポートに蓄積されたメッセージをリトライ（再処理）する運用管理コマンドの仕様を定義する。

### 本バッチの処理概要

`messenger:failed:retry` コマンドは、失敗トランスポートに保存されている処理失敗メッセージを再度メッセージバスにディスパッチし、ハンドラによる処理を再試行するコマンドである。

**業務上の目的・背景**：非同期メッセージ処理で失敗したメッセージは、一時的な障害（DB接続エラー、外部APIのタイムアウト等）が原因であることが多い。障害が解消された後にメッセージをリトライすることで、手動での再処理や再投入を行わずに正常処理を完了させることができる。本コマンドは対話的なリトライ操作と一括リトライの両方に対応し、運用者が柔軟に失敗メッセージを管理できるようにする。

**バッチの実行タイミング**：随時実行（手動）。障害解消後のリトライ作業や定期的な失敗メッセージ処理時に使用する。

**主要な処理内容**：
1. 失敗トランスポートの選択
2. 対象メッセージの決定（ID指定 / 対話モード）
3. メッセージの表示とリトライ/削除/スキップの選択
4. Workerインスタンスによるメッセージの再処理
5. シグナルハンドリング（SIGTERM/SIGINT/SIGQUIT/SIGALRM）

**前後の処理との関連**：`messenger:failed:show` で確認した失敗メッセージのIDを指定してリトライする。`messenger:consume` で処理に失敗し失敗トランスポートに移動されたメッセージが対象。リトライしたくないメッセージは `messenger:failed:remove` で削除する。

**影響範囲**：失敗トランスポートからメッセージを取得してメッセージバスに再ディスパッチする。成功すればメッセージは失敗トランスポートから削除される。ハンドラの実行内容に応じてデータベース操作やAPI呼び出し等が再実行される。

## バッチ種別

運用管理（失敗メッセージリトライ）

## 実行スケジュール

| 項目 | 内容 |
|-----|------|
| 実行頻度 | 随時 |
| 実行時刻 | 任意 |
| 実行曜日 | 任意 |
| 実行日 | 任意 |
| トリガー | 手動 |

## 実行条件

### 前提条件

| 条件 | 説明 |
|-----|------|
| 失敗トランスポート | 少なくとも1つの失敗トランスポートが設定されていること |
| メッセージバス | MessageBusInterfaceが設定されていること |
| イベントディスパッチャ | EventDispatcherInterfaceが利用可能であること |

### 実行可否判定

失敗トランスポートが設定され利用可能であれば実行可能。ID指定でリトライする場合はListableReceiverInterfaceの実装が必要。非対話モードではID指定が必須。

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | デフォルト値 | 説明 |
|-------------|-----|-----|-------------|------|
| id | string[] | No | なし | リトライするメッセージのID（複数指定可能） |
| --force | bool | No | false | 確認なしで強制リトライ |
| --transport | string | No | choose（デフォルトトランスポート） | 使用する失敗トランスポート名 |
| --keepalive | int | No | 5 | キープアライブ間隔（秒） |

### 入力データソース

| データソース | 形式 | 説明 |
|-------------|------|------|
| 失敗トランスポート | ReceiverInterface / ListableReceiverInterface | リトライ対象のメッセージ取得 |

## 出力仕様

### 出力データ

| 出力先 | 形式 | 説明 |
|-------|------|------|
| STDOUT | テキスト | メッセージ詳細・リトライ結果 |
| STDERR | テキスト | 操作ガイダンス・ログ情報 |

### 出力ファイル仕様

ファイル出力は行わない。

| 項目 | 内容 |
|-----|------|
| ファイル名 | なし（標準出力） |
| 出力先 | STDOUT |
| 文字コード | UTF-8 |
| 区切り文字 | なし |

## 処理フロー

### 処理シーケンス

```
1. コマンド初期化
   ├─ --keepaliveオプション処理（アラーム間隔設定）
   └─ StopWorkerOnMessageLimitListener(1)登録
2. 失敗トランスポート選択
   ├─ --transport指定時: 指定トランスポート
   └─ デフォルト: グローバル失敗トランスポート
3. レシーバー取得・保留メッセージ数表示
4. モード判定
   ├─ id引数あり: retrySpecificIds() で指定メッセージをリトライ
   └─ id引数なし（対話モード）: runInteractive() で1件ずつ確認
5a. ID指定リトライ (retrySpecificIds)
   ├─ ListableReceiverInterface実装チェック
   ├─ 各IDに対してfind()でメッセージ取得
   ├─ SingleMessageReceiverでラップ
   └─ runWorker()で再処理
5b. 対話モード (runInteractive)
   ├─ ListableReceiver対応時: all(1)で1件ずつ取得
   │   ├─ メッセージ詳細表示
   │   ├─ retry/delete/skip の選択
   │   └─ retry選択時: Worker経由で再処理
   └─ 非対応時: runWorker()でget()経由で取得
       ├─ WorkerMessageReceivedEventリスナーで対話処理
       ├─ retry選択時: メッセージをハンドル
       ├─ delete選択時: receiver->reject()
       └─ skip選択時: WorkerMessageSkipEvent発行
6. シグナルハンドリング
   ├─ SIGALRM: キープアライブ処理
   └─ SIGTERM/SIGINT/SIGQUIT: Worker停止（forceExit制御あり）
7. 完了メッセージ出力
```

### フローチャート

```mermaid
flowchart TD
    A[バッチ開始] --> B[失敗トランスポート選択]
    B --> C[レシーバー取得]
    C --> D{id引数あり?}
    D -->|あり| E[ID指定リトライ]
    D -->|なし| F{対話モード?}
    F -->|非対話| G[RuntimeException]
    F -->|対話| H[対話モードリトライ]
    E --> I[find でメッセージ取得]
    I --> J[SingleMessageReceiver でラップ]
    J --> K[runWorker で再処理]
    H --> L{ListableReceiver?}
    L -->|対応| M[all 1件ずつ取得]
    L -->|非対応| N[Worker get 経由]
    M --> O[メッセージ詳細表示]
    O --> P{retry/delete/skip?}
    P -->|retry| Q[Worker で再処理]
    P -->|delete| R[receiver reject]
    P -->|skip| S[スキップ]
    Q --> T{次のメッセージ?}
    R --> T
    S --> T
    T -->|あり| M
    T -->|なし| U[完了メッセージ]
    K --> U
    N --> U
    U --> V[バッチ終了]
```

## データベース操作仕様

### 操作別データベース影響一覧

本コマンド自体のデータベース操作に加え、リトライされたハンドラが行うデータベース操作が再実行される。

| 処理 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| メッセージ取得 | messenger_messages（Doctrine使用時） | SELECT | 失敗メッセージの取得 |
| メッセージAck（リトライ成功時） | messenger_messages（Doctrine使用時） | DELETE | 処理完了メッセージを削除 |
| メッセージReject（delete選択時） | messenger_messages（Doctrine使用時） | DELETE | 拒否メッセージを削除 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | RuntimeException | ID指定時に失敗トランスポートがListableReceiverInterface非実装 | 対応するトランスポートを使用する |
| - | RuntimeException | 指定IDのメッセージが見つからない | 正しいIDを指定する |
| - | RuntimeException | 非対話モードでID未指定 | IDを指定して実行する |
| - | RuntimeException | MessageDecodingFailedStampが付与されたメッセージのリトライ | showまたはremoveのみ可能 |
| - | InvalidArgumentException | 失敗トランスポートが存在しない | 正しいトランスポート名を指定する |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | 手動操作のため回数制限なし |
| リトライ間隔 | 手動操作のため間隔制限なし |
| リトライ対象エラー | 全ての失敗メッセージ（デコード失敗を除く） |

### 障害時対応

リトライが再度失敗した場合は、メッセージは再び失敗トランスポートに移動される。根本原因を解決してから再度リトライする。

## トランザクション仕様

| 項目 | 内容 |
|-----|------|
| トランザクション範囲 | メッセージ単位 |
| コミットタイミング | メッセージ処理完了時 |
| ロールバック条件 | ハンドラで例外発生時 |

## パフォーマンス要件

| 項目 | 内容 |
|-----|------|
| 想定処理件数 | 失敗メッセージ数に依存 |
| 目標処理時間 | メッセージハンドラの処理時間に依存 |
| メモリ使用量上限 | 特に制限なし |

## 排他制御

StopWorkerOnMessageLimitListener(1)により、Worker は1メッセージ処理後に停止する。対話モードでは1件ずつ処理するため、複数メッセージの同時処理は発生しない。

## ログ出力

| ログ種別 | 出力タイミング | 出力内容 |
|---------|--------------|---------|
| 案内ログ | コマンド開始時 | 「Quit this command with CONTROL-C.」 |
| メッセージ詳細 | 各メッセージ処理時 | メッセージクラス、ID、エラー情報、メッセージ履歴 |
| シグナルログ | シグナル受信時 | 受信シグナル名・トランスポート名 |
| キープアライブログ | SIGALRM受信時 | キープアライブリクエスト送信 |
| 成功ログ | 全メッセージ処理完了時 | 「All done!」 / 「All failed messages have been handled or removed!」 |

## 監視・アラート

| 監視項目 | 閾値 | アラート先 |
|---------|-----|----------|
| - | - | - |

手動実行のコマンドのため、監視・アラートは不要。

## 備考

- 基盤実装は `src/Symfony/Component/Messenger/Command/FailedMessagesRetryCommand.php` に存在する
- AbstractFailedMessagesCommandを継承し、SignalableCommandInterfaceを実装する
- 対話モードでは retry / delete / skip の3つの選択肢が提示される（--force指定時はretryが自動選択）
- MessageDecodingFailedStampが付与されたメッセージはリトライ不可（showまたはremoveのみ）
- ListableReceiverInterface実装のトランスポートでは、find()で個別取得するため一時的なAckを回避できる
- 非ListableReceiverの場合は、get()経由で取得するためトランスポートによっては一時的なAckが発生する可能性がある
- skip選択時はWorkerMessageSkipEventが発行される
- 全メッセージのリトライは `messenger:consume {failureTransportName}` で行うことも可能（案内メッセージ出力あり）
- キープアライブ機能により、長時間のリトライ処理中もトランスポート接続が維持される
- forceExit制御: 対話的な選択中（choice画面表示中）にシグナルを受信した場合はプロセスが即座に終了する
