# バッチ設計書 2-batch-sending-service-job

## 概要

本ドキュメントは、Ghostプラットフォームにおけるニュースレターメール一括送信バッチ「batch-sending-service-job」の設計仕様を記載したものです。このバッチは、ニュースレターメールをバッチ単位で効率的に送信し、大規模な購読者リストへの配信を実現します。

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

**業務上の目的・背景**：Ghostはニュースレター配信プラットフォームとして、数千〜数万人の購読者に対してメールを送信する必要があります。単一のAPIコールで全メールを送信することは技術的に不可能であり、また送信サービス（Mailgun）の制限にも抵触するため、メールをバッチ（最大1,000件）に分割して並列送信する仕組みが必要です。このバッチは、メール送信の信頼性・スケーラビリティを確保し、配信失敗時のリトライも行います。

**バッチの実行タイミング**：投稿の「公開」または「予約公開」アクション時にトリガーされます。メールが「pending」または「failed」状態の場合に実行されます。

**主要な処理内容**：
1. メールモデルのステータスを「pending」から「submitting」にロック取得付きで更新
2. 対象ニュースレターとセグメントに基づいて購読者を取得
3. 購読者を最大1,000件のバッチに分割し、email_batchesテーブルに登録
4. 最大並列度2でバッチを順次送信（Mailgun API経由）
5. 各バッチの送信結果をemail_batchesテーブルに記録
6. 全バッチ完了後、メールのステータスを「submitted」に更新

**前後の処理との関連**：投稿公開APIからトリガーされ、送信後はemail-analytics-fetch-latestバッチでイベントが収集されます。

**影響範囲**：emails、email_batches、email_recipientsテーブルが更新されます。購読者へのメール配信に直接影響します。

## バッチ種別

通知配信 / データ処理

## 実行スケジュール

| 項目 | 内容 |
|-----|------|
| 実行頻度 | 随時（イベント駆動） |
| 実行時刻 | 投稿公開時 |
| 実行曜日 | - |
| 実行日 | - |
| トリガー | 投稿公開/予約公開アクション |

## 実行条件

### 前提条件

| 条件 | 説明 |
|-----|------|
| メールステータス | emailsテーブルのstatusが「pending」または「failed」であること |
| ニュースレター設定 | 対象投稿に関連付けられたニュースレターが存在すること |
| 購読者存在 | 対象セグメントに1件以上の購読者が存在すること |
| Mailgun設定 | Mailgun APIキーとドメインが設定されていること |

### 実行可否判定

- `updateStatusLock()`でメールステータスを「submitting」に更新できた場合のみ実行
- 既に「submitting」「submitted」の場合はスキップ

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | デフォルト値 | 説明 |
|-------------|-----|-----|-------------|------|
| emailId | string | Yes | - | 送信対象のメールID |

### 入力データソース

| データソース | 形式 | 説明 |
|-------------|------|------|
| emails テーブル | DB | 送信対象メール情報 |
| newsletters テーブル | DB | ニュースレター設定 |
| posts テーブル | DB | 投稿コンテンツ |
| members テーブル | DB | 購読者リスト |

## 出力仕様

### 出力データ

| 出力先 | 形式 | 説明 |
|-------|------|------|
| emails テーブル | DB | メールステータス（submitted/failed）、submitted_at、エラー情報を更新 |
| email_batches テーブル | DB | バッチ情報（status、provider_id、エラー情報）を記録 |
| email_recipients テーブル | DB | 受信者情報、processed_atを記録 |
| Mailgun API | REST API | メール送信リクエスト |

### 出力ファイル仕様

ファイル出力なし（データベース・外部API）

## 処理フロー

### 処理シーケンス

```
1. emailJob開始
   └─ emailIdでメールを取得

2. updateStatusLock
   └─ メールステータスをpending/failed → submittingに更新（FOR UPDATE）

3. sendEmail開始
   └─ newsletter, postのリレーションを取得

4. getBatches
   └─ 既存バッチがあれば取得

5. createBatches（バッチがない場合）
   └─ セグメントごとに購読者を取得（1000件ずつ）
   └─ email_batchesテーブルにバッチ登録
   └─ email_recipientsテーブルに受信者登録

6. sendBatches
   └─ 配信デッドライン計算
   └─ 最大並列度2でバッチ送信

7. sendBatch（各バッチ）
   └─ バッチステータスをsubmittingに更新
   └─ getBatchMembersで受信者取得
   └─ sendingService.sendでMailgun API呼び出し
   └─ バッチステータスをsubmitted/failedに更新
   └─ email_recipients.processed_atを更新

8. メールステータス更新
   └─ 全バッチ成功: submitted、submitted_at設定
   └─ 一部失敗: failed、エラーメッセージ設定
```

### フローチャート

```mermaid
flowchart TD
    A[emailJob開始] --> B{ステータスロック取得}
    B -->|失敗| C[スキップ]
    B -->|成功| D[newsletter/post取得]
    D --> E{既存バッチあり?}
    E -->|Yes| F[既存バッチ使用]
    E -->|No| G[createBatches]
    G --> H[セグメントごとに購読者取得]
    H --> I[バッチ・受信者登録]
    F --> J[sendBatches]
    I --> J
    J --> K[並列度2でバッチ送信]
    K --> L{全バッチ成功?}
    L -->|Yes| M[status=submitted]
    L -->|No| N[status=failed]
    M --> O[バッチ終了]
    N --> O
    C --> O
```

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

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

| 処理 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| ステータスロック | emails | UPDATE | status→submitting（FOR UPDATE） |
| バッチ作成 | email_batches | INSERT | バッチ情報の登録 |
| 受信者登録 | email_recipients | INSERT | 受信者情報の登録 |
| バッチ送信結果 | email_batches | UPDATE | status, provider_id, error情報 |
| 受信者処理完了 | email_recipients | UPDATE | processed_at |
| メール完了 | emails | UPDATE | status, submitted_at, error |

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

#### emails

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | status | submitting → submitted/failed | トランザクション内 |
| UPDATE | submitted_at | 現在日時 | 成功時のみ |
| UPDATE | error | エラーメッセージ | 失敗時のみ |
| UPDATE | email_count | 実際の受信者数 | カウント不整合時 |

#### email_batches

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | email_id, member_segment, status, fallback_sending_domain | バッチ情報 | |
| UPDATE | status | pending → submitting → submitted/failed | |
| UPDATE | provider_id | Mailgunからのレスポンス | 成功時 |
| UPDATE | error_status_code, error_message, error_data | エラー詳細 | 失敗時 |

#### email_recipients

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | id, email_id, member_id, batch_id, member_uuid, member_email, member_name | 受信者情報 | |
| UPDATE | processed_at | 現在日時 | 送信試行後 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| BULK_EMAIL_SEND_FAILED | 送信エラー | Mailgun API呼び出し失敗 | リトライ後、失敗としてマーク |
| - | 部分失敗 | 一部バッチのみ失敗 | 「partially sent」エラーメッセージ |
| - | DBエラー | データベース接続失敗 | リトライ（最大10回、最大10分） |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数（送信前DB） | 最大10回 |
| リトライ最大時間（送信前DB） | 最大10分 |
| リトライ間隔（送信前DB） | 2秒（倍増） |
| リトライ回数（送信後DB） | 最大20回 |
| リトライ最大時間（送信後DB） | 最大30分 |
| リトライ回数（Mailgun API） | 最大6回 |
| リトライ間隔（Mailgun API） | 10秒 |

### 障害時対応

- DB接続エラー: retryDb()で自動リトライ（指数バックオフ）
- Mailgun APIエラー: 503の場合は最大30回リトライ
- 部分失敗時: failedバッチのみ再送可能（メールステータスをfailedに設定）
- Sentryへのエラー報告（設定時）

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

| 項目 | 内容 |
|-----|------|
| トランザクション範囲 | バッチ作成時（email_batches + email_recipients） |
| コミットタイミング | バッチ単位 |
| ロールバック条件 | バッチ作成時のエラー |

## パフォーマンス要件

| 項目 | 内容 |
|-----|------|
| 想定処理件数 | 1バッチあたり最大1,000件 |
| 最大並列度 | 2 |
| バッチサイズ | sendingService.getMaximumRecipients()で決定 |

## 排他制御

- `updateStatusLock()`による楽観的ロック
- メールステータスが「pending」または「failed」の場合のみ処理開始
- FOR UPDATEによるDB行ロック

## ログ出力

| ログ種別 | 出力タイミング | 出力内容 |
|---------|--------------|---------|
| 開始ログ | emailJob開始時 | `Starting email job for email ${emailId}` |
| バッチ作成ログ | バッチ作成時 | `Creating batches for email ${email.id}` |
| 送信ログ | バッチ送信時 | `Sending batch ${batch.id} for email ${email.id}` |
| リトライログ | リトライ時 | `[BULK_EMAIL_DB_RETRY]` プレフィックス付き |
| エラーログ | エラー発生時 | EmailError with context |

## 監視・アラート

| 監視項目 | 閾値 | アラート先 |
|---------|-----|----------|
| 送信失敗 | 1件以上 | Sentry（設定時） |
| email_count不整合 | 1%以上の誤差 | Sentry（設定時） |

## 備考

- ドメインウォーミング機能により、カスタムドメインとフォールバックドメインを使い分け可能
- セグメント機能により、特定条件の購読者のみにメールを送信可能
- deliveryTimeを設定することで、配信時刻を分散可能
- EmailBodyCacheにより、同一セグメントへの重複レンダリングを回避
