# 機能設計書 59-アウトボックス

## 概要

本ドキュメントは、Ghostのアウトボックス機能に関する設計を記述します。この機能は、非同期で処理が必要なイベント（メンバー登録時のウェルカムメール送信など）を一時的に保存し、バックグラウンドジョブで順次処理するためのメッセージキュー機能です。

### 本機能の処理概要

**業務上の目的・背景**：メンバー登録時のウェルカムメール送信など、即時応答が不要な処理をリクエスト処理から分離することで、ユーザー体験を向上させます。また、失敗時のリトライ機能により、一時的な障害からの回復を実現します。

**機能の利用シーン**：新規メンバー登録時のウェルカムメール送信、外部サービスへの通知送信など、信頼性が求められる非同期処理で活用されます。

**主要な処理内容**：
1. イベントのアウトボックステーブルへの保存
2. 定期ジョブによるPENDINGエントリの取得
3. イベントタイプに応じたハンドラー実行
4. 成功時の削除、失敗時のリトライ管理
5. 最大リトライ回数超過時のFAILED状態への遷移

**関連システム・外部連携**：
- Member Welcome Email Service: ウェルカムメール送信
- Domain Events: イベント発行
- Scheduled Job: 定期実行

**権限による制御**：アウトボックス処理はサーバー内部で自動実行されるため、ユーザー権限は不要です。

## 関連画面

本機能はバックグラウンド処理のため、直接関連する画面はありません。

## 機能種別

非同期処理 / メッセージキュー / リトライ機構

## 入力仕様

### 入力パラメータ

#### アウトボックスエントリ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| event_type | string | Yes | イベント種別 | MemberCreatedEvent等 |
| payload | JSON | Yes | イベントデータ | JSON形式 |
| status | string | Yes | 処理状態 | PENDING/PROCESSING/COMPLETED/FAILED |
| retry_count | number | No | リトライ回数 | デフォルト0 |

### 入力データソース

- Domain Events: MemberCreatedEvent
- データベース: `outbox`

## 出力仕様

### 出力データ

#### 処理結果

| 項目名 | 型 | 説明 |
|--------|-----|------|
| processed | number | 成功件数 |
| failed | number | 失敗件数 |

### 出力先

- データベース: `outbox` (status更新/削除)
- データベース: `automated_email_recipients` (送信記録)
- ログ: 処理結果

## 処理フロー

### 処理シーケンス

```
1. アウトボックス初期化
   └─ OutboxServiceWrapper.init()
   └─ ジョブスケジュール登録
   └─ StartOutboxProcessingEvent購読

2. 定期ジョブ実行
   └─ startProcessing()
   └─ 処理中フラグチェック

3. エントリ取得
   └─ fetchPendingEntries()
   └─ PENDING状態のエントリを取得
   └─ PROCESSING状態に更新（トランザクション内）

4. エントリ処理
   └─ processEntries()
   └─ ハンドラー呼び出し
   └─ 成功時: エントリ削除
   └─ 失敗時: リトライカウント更新

5. 結果ログ
   └─ 処理件数・失敗件数を出力
```

### フローチャート

```mermaid
flowchart TD
    A[定期ジョブ/イベント] --> B{処理中?}
    B -->|Yes| C[スキップ]
    B -->|No| D[処理中フラグON]
    D --> E[fetchPendingEntries]
    E --> F{エントリあり?}
    F -->|No| G[完了]
    F -->|Yes| H[processEntry]
    H --> I{ハンドラー存在?}
    I -->|No| J[失敗記録]
    I -->|Yes| K[ハンドラー実行]
    K --> L{成功?}
    L -->|Yes| M[エントリ削除]
    L -->|No| N{リトライ上限?}
    N -->|Yes| O[FAILED更新]
    N -->|No| P[PENDING更新]
    M --> Q[次のエントリ]
    J --> Q
    O --> Q
    P --> Q
    Q --> F
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-59-001 | 最大リトライ | 最大リトライ回数を超えた場合はFAILED状態に遷移 | リトライ時 |
| BR-59-002 | バッチサイズ | 1ジョブで処理するエントリ数の上限 | BATCH_SIZE定数 |
| BR-59-003 | 最大エントリ数 | 1ジョブで処理する総エントリ数の上限 | MAX_ENTRIES_PER_JOB定数 |
| BR-59-004 | 排他制御 | 処理中フラグで同時実行を防止 | ジョブ開始時 |
| BR-59-005 | ウェルカムメール機能フラグ | labs.welcomeEmailsが有効な場合のみ処理 | 処理開始時 |

### 計算ロジック

**リトライ判定**:
```javascript
newStatus = (retryCount + 1) <= MAX_RETRIES ? PENDING : FAILED
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| エントリ取得 | outbox | SELECT | PENDING状態のエントリ取得 |
| 処理中更新 | outbox | UPDATE | status=PROCESSING |
| 成功削除 | outbox | DELETE | 処理完了エントリ削除 |
| 失敗更新 | outbox | UPDATE | retry_count++, status更新 |
| 送信記録 | automated_email_recipients | INSERT | メール送信記録 |

### テーブル別操作詳細

#### outbox

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | * | event_type, status=PENDING, last_retry_at条件 | forUpdate()でロック |
| UPDATE | status | PROCESSING | 取得時に即時更新 |
| DELETE | - | id指定 | 成功時に削除 |
| UPDATE | status, retry_count, last_retry_at, message | リトライ情報 | 失敗時 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | Warn | ハンドラーが存在しない | 失敗記録してスキップ |
| - | Error | ペイロード解析失敗 | 失敗記録してスキップ |
| - | Error | ハンドラー実行失敗 | リトライ更新 |
| - | Error | エントリ削除失敗 | COMPLETED状態に更新 |

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

エントリ取得時にトランザクション内でSELECT FOR UPDATEとUPDATEを実行し、同一エントリの重複処理を防止します。

## パフォーマンス要件

- バッチ処理時間: ログに出力（entries/sec）
- 1ジョブあたりの処理上限: MAX_ENTRIES_PER_JOB

## セキュリティ考慮事項

- ペイロードにはメンバー情報が含まれるため、ログ出力時は注意
- エラーメッセージは最大2000文字に制限

## 備考

- 現在はMemberCreatedEventのみサポート（ウェルカムメール送信）
- labs.welcomeEmailsフラグで有効化

---

## コードリーディングガイド

### 推奨読解順序

#### Step 1: サービス初期化を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | index.js | `ghost/core/core/server/services/outbox/index.js` | init(), startProcessing() |

#### Step 2: ジョブ処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | process-outbox.js | `ghost/core/core/server/services/outbox/jobs/lib/process-outbox.js` | processOutbox() |
| 2-2 | process-entries.js | `ghost/core/core/server/services/outbox/jobs/lib/process-entries.js` | processEntry(), リトライ処理 |

#### Step 3: ハンドラーを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | member-created.js | `ghost/core/core/server/services/outbox/handlers/member-created.js` | handle() |

**主要処理フロー**:
- **index.js 9-23行目**: `init()` - サービス初期化
- **index.js 25-40行目**: `startProcessing()` - 処理開始
- **process-outbox.js 10-39行目**: `fetchPendingEntries()` - エントリ取得
- **process-outbox.js 41-88行目**: `processOutbox()` - メイン処理
- **process-entries.js 43-77行目**: `processEntry()` - 個別処理
- **process-entries.js 17-31行目**: `updateFailedEntry()` - 失敗更新

### プログラム呼び出し階層図

```
Boot / Scheduled Job
    │
    └─ OutboxServiceWrapper.init()
           │
           ├─ jobs.scheduleOutboxJob()
           │
           └─ domainEvents.subscribe(StartOutboxProcessingEvent)
                  │
                  └─ startProcessing()
                         │
                         └─ processOutbox()
                                │
                                ├─ memberWelcomeEmailService.api.loadMemberWelcomeEmails()
                                │
                                └─ [Loop] fetchPendingEntries()
                                       │
                                       └─ processEntries()
                                              │
                                              └─ processEntry()
                                                     │
                                                     ├─ handler.handle()
                                                     │      └─ memberCreatedHandler
                                                     │             └─ welcomeEmailService.send()
                                                     │
                                                     └─ deleteProcessedEntry() / updateFailedEntry()
```

### データフロー図

```
┌─────────────────┐     ┌───────────────────┐     ┌───────────────────┐
│  Domain Event   │────>│  outbox table     │<────│  Scheduled Job    │
│  (insert)       │     │  (message queue)  │     │  (processOutbox)  │
└─────────────────┘     └───────────────────┘     └───────────────────┘
                                 │
                                 ▼
                        ┌───────────────────┐
                        │  Event Handler    │
                        │  (member-created) │
                        └───────────────────┘
                                 │
                    ┌────────────┴────────────┐
                    ▼                         ▼
        ┌───────────────────┐     ┌───────────────────┐
        │  Welcome Email    │     │  Tracking DB      │
        │  (send)           │     │  (recipients)     │
        └───────────────────┘     └───────────────────┘
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| index.js | `ghost/core/core/server/services/outbox/index.js` | ソース | サービスラッパー |
| process-outbox.js | `ghost/core/core/server/services/outbox/jobs/lib/process-outbox.js` | ソース | メイン処理 |
| process-entries.js | `ghost/core/core/server/services/outbox/jobs/lib/process-entries.js` | ソース | エントリ処理 |
| constants.js | `ghost/core/core/server/services/outbox/jobs/lib/constants.js` | ソース | 定数定義 |
| member-created.js | `ghost/core/core/server/services/outbox/handlers/member-created.js` | ソース | メンバー作成ハンドラー |
| start-outbox-processing-event.js | `ghost/core/core/server/services/outbox/events/start-outbox-processing-event.js` | ソース | 処理開始イベント |
| outbox-job.js | `ghost/core/core/server/services/outbox/jobs/outbox-job.js` | ソース | ジョブ定義 |
