# バッチ設計書 8-Internal::RemoveStaleContactsJob

## 概要

本ドキュメントは、Chatwootアプリケーションにおける`Internal::RemoveStaleContactsJob`バッチの設計仕様を記載したものである。

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

このバッチは、識別情報（メール、電話番号、識別子）がなく、会話もない30日以上経過したコンタクトを削除するジョブである。ProcessStaleContactsJobから呼び出される子ジョブとして動作する。

**業務上の目的・背景**：Chatwootでは、ウェブサイトウィジェットやAPIを通じてコンタクトが作成されるが、識別情報が設定されず会話も発生しないコンタクトが蓄積することがある。これらの「匿名かつ非アクティブ」なコンタクトはビジネス価値がなく、データベースのストレージとパフォーマンスに悪影響を与えるため、定期的に削除する必要がある。

**バッチの実行タイミング**：ProcessStaleContactsJobから呼び出される子ジョブとして実行される。1〜10分のランダム遅延付きでキューイングされる。

**主要な処理内容**：
1. 対象アカウントの古いコンタクトを検索
2. バッチサイズ1,000件で分割処理
3. 関連するcontact_inboxesを先に削除
4. コンタクトを削除
5. 削除件数をログ出力

**前後の処理との関連**：Internal::ProcessStaleContactsJobから呼び出される子ジョブ。RemoveStaleContactsServiceを使用して実際の削除処理を行う。

**影響範囲**：contacts、contact_inboxesテーブルのレコード削除

## バッチ種別

データクリーンアップ / ハウスキーピング

## 実行スケジュール

| 項目 | 内容 |
|-----|------|
| 実行頻度 | 親ジョブから呼び出し（約5日に1回/アカウント） |
| 実行時刻 | - |
| 実行曜日 | - |
| 実行日 | - |
| トリガー | 親ジョブ（ProcessStaleContactsJob）からのperform_later |

## 実行条件

### 前提条件

| 条件 | 説明 |
|-----|------|
| アカウント存在 | 対象アカウントが存在すること |

### 実行可否判定

親ジョブから引数として渡されたアカウントに対して常に実行される。

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | デフォルト値 | 説明 |
|-------------|-----|-----|-------------|------|
| account | Account | Yes | - | 対象アカウントオブジェクト |
| batch_size | Integer | No | 1000 | 削除処理のバッチサイズ |

### 入力データソース

| データソース | 形式 | 説明 |
|-------------|------|------|
| contacts | DB | 対象アカウントのコンタクト（stale_without_conversationsスコープ） |
| contact_inboxes | DB | コンタクトに関連するインボックス関連 |

## 出力仕様

### 出力データ

| 出力先 | 形式 | 説明 |
|-------|------|------|
| contacts | DB | 対象レコードの削除 |
| contact_inboxes | DB | 関連レコードの削除 |
| ログ | テキスト | 削除件数の出力 |

### 出力ファイル仕様

ファイル出力なし

## 処理フロー

### 処理シーケンス

```
1. 削除対象コンタクトの取得
   └─ @account.contacts.stale_without_conversations(30.days.ago)
   └─ email, phone_number, identifierがすべてNULLまたは空
   └─ 会話がないコンタクト（.missing(:conversations)）
   └─ 30日以上前に作成されたコンタクト
2. 開始ログ出力
   └─ "Starting removal of stale contacts for account {id}"
3. バッチ削除処理
   └─ find_in_batches(batch_size: 1000)で分割
   └─ コンタクトIDリストを取得
   └─ ContactInbox.where(contact_id: contact_ids).delete_all
   └─ Contact.where(id: contact_ids).delete_all
   └─ 削除件数をログ出力
4. 完了（暗黙）
```

### フローチャート

```mermaid
flowchart TD
    A[バッチ開始] --> B[RemoveStaleContactsService呼び出し]
    B --> C[削除対象コンタクト検索]
    C --> D[開始ログ出力]
    D --> E[1000件ずつバッチ取得]
    E --> F{バッチあり?}
    F -->|Yes| G[コンタクトIDリスト取得]
    G --> H[ContactInbox削除]
    H --> I[Contact削除]
    I --> J[削除件数ログ出力]
    J --> E
    F -->|No| K[バッチ終了]
```

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

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

| 処理 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 削除対象検索 | contacts | SELECT | stale_without_conversationsスコープで検索 |
| 関連削除 | contact_inboxes | DELETE | コンタクトIDに関連するレコード削除 |
| コンタクト削除 | contacts | DELETE | 対象コンタクトの削除 |

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

#### contacts

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | id, email, phone_number, identifier, created_at | email IS NULL/空 AND phone_number IS NULL/空 AND identifier IS NULL/空 AND created_at < 30.days.ago AND 会話なし | batch_size: 1000 |
| DELETE | id | 検索結果のIDリスト | delete_allで一括削除 |

#### contact_inboxes

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| DELETE | contact_id | 削除対象コンタクトのIDリスト | delete_allで一括削除 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | ActiveRecord::StatementInvalid | DB接続エラー、デッドロック等 | Sidekiqリトライ機構に委譲 |
| - | StandardError | 予期しないエラー | Sidekiqリトライ機構に委譲 |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | Sidekiqのデフォルト設定に依存 |
| リトライ間隔 | Sidekiqのデフォルト設定に依存 |
| リトライ対象エラー | ジョブ実行時の例外全般 |

### 障害時対応

バッチ単位で削除処理を行うため、途中で失敗した場合は次回実行時に残りのレコードが処理される。30日の保持期間があるため、多少の遅延は問題にならない。

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

| 項目 | 内容 |
|-----|------|
| トランザクション範囲 | バッチ単位（明示的なトランザクションなし） |
| コミットタイミング | 各delete_all実行時に即時コミット |
| ロールバック条件 | 明示的なロールバック処理なし |

## パフォーマンス要件

| 項目 | 内容 |
|-----|------|
| 想定処理件数 | アカウントあたり数件〜数万件 |
| 目標処理時間 | アカウントあたり数秒〜数分 |
| メモリ使用量上限 | 最小限（find_in_batchesで分割処理） |

## 排他制御

同時実行の制御は特になし。ProcessStaleContactsJobによる分散処理と遅延キューイングにより、同一アカウントの同時処理は発生しにくい。

## ログ出力

| ログ種別 | 出力タイミング | 出力内容 |
|---------|--------------|---------|
| 情報ログ | 処理開始時 | `[Internal::RemoveStaleContactsService] Starting removal of stale contacts for account {id}` |
| 情報ログ | 各バッチ削除後 | `[Internal::RemoveStaleContactsService] Deleted {batch_size} contacts ({total} total) for account {id}` |

## 監視・アラート

| 監視項目 | 閾値 | アラート先 |
|---------|-----|----------|
| ジョブ失敗 | 連続失敗 | Sidekiq監視 |
| 削除件数 | 異常に多い場合 | 運用監視（任意） |

## 備考

- housekeepingキューを使用し、通常のジョブとは別のキューで処理される
- `delete_all`を使用しているため、ActiveRecordコールバックは実行されない
- 削除対象の判定条件:
  - email IS NULL または空文字列
  - phone_number IS NULL または空文字列
  - identifier IS NULL または空文字列
  - 会話がない（`.missing(:conversations)`）
  - created_at < 30.days.ago
- ContactInboxを先に削除してからContactを削除することで、外部キー制約違反を回避
- pattr_initializeマクロを使用してaccountを必須引数として初期化
