# バッチ設計書 58-ActiveStorage::MirrorJob

## 概要

本ドキュメントは、直接アップロードされたBlobの非同期ミラーリングを行うActiveJobクラスについて記載する。

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

このジョブは、プライマリストレージサービスにアップロードされたファイルを、設定されたミラーサービスに非同期でコピーするためのActiveJobクラスである。MirrorServiceを使用している場合に、ファイルの冗長化やバックアップを実現する。

**業務上の目的・背景**：大規模なアプリケーションでは、ストレージの冗長化や地理的分散が必要になる場合がある。ファイルアップロード時にすべてのミラーに同期的にコピーすると、レスポンス時間が長くなる。本ジョブにより、プライマリへのアップロードを高速に完了させつつ、バックグラウンドでミラーへのコピーを行うことで、可用性と性能の両立が可能になる。

**バッチの実行タイミング**：MirrorServiceを使用している場合、ファイルがプライマリにアップロードされた後にエンキューされる。具体的にはmirror_laterメソッド呼び出し時。

**主要な処理内容**：
1. ファイルキーとチェックサムを引数として受け取る
2. プライマリサービスからファイルを取得
3. 各ミラーサービスに対して、ファイルが存在しなければコピー
4. チェックサムの検証を行いながらコピー

**前後の処理との関連**：Attachmentのafter_create_commitコールバックでmirror_blob_laterが呼び出され、本ジョブがエンキューされる。

**影響範囲**：ミラーストレージサービスへのファイル追加。データベースへの影響なし。

## バッチ種別

データ複製（ストレージミラーリング）

## 実行スケジュール

| 項目 | 内容 |
|-----|------|
| 実行頻度 | 随時（イベントドリブン） |
| 実行時刻 | - |
| 実行曜日 | - |
| 実行日 | - |
| トリガー | MirrorService#mirror_later呼び出し時 |

## 実行条件

### 前提条件

| 条件 | 説明 |
|-----|------|
| MirrorService設定 | ストレージサービスがMirrorServiceとして設定されていること |
| プライマリファイル存在 | プライマリサービスにファイルが存在すること |
| ミラーサービス接続 | 各ミラーサービスへの接続が可能であること |

### 実行可否判定

特別な実行可否判定なし。サービスがMirrorServiceでない場合は、service.try(:mirror, ...)によりnilが返され何も起こらない。

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | デフォルト値 | 説明 |
|-------------|-----|-----|-------------|------|
| key | String | Yes | - | ファイルのキー（識別子） |
| checksum | String | Yes | - | ファイルのMD5チェックサム（キーワード引数） |

### 入力データソース

| データソース | 形式 | 説明 |
|-------------|------|------|
| プライマリストレージ | バイナリ | コピー元のファイル実体 |

## 出力仕様

### 出力データ

| 出力先 | 形式 | 説明 |
|-------|------|------|
| ミラーストレージサービス | バイナリ | コピーされたファイル |

### 出力ファイル仕様

| 項目 | 内容 |
|-----|------|
| ファイル名 | 元のkey（プライマリと同一） |
| 出力先 | 設定されたすべてのミラーサービス |
| 文字コード | - |
| 区切り文字 | - |

## 処理フロー

### 処理シーケンス

```
1. ジョブ受信
   └─ keyとchecksumをデシリアライズ
2. サービス確認
   └─ ActiveStorage::Blob.service.try(:mirror, ...)でMirrorServiceか確認
3. ミラー対象確認
   └─ 各ミラーサービスでファイルの存在確認
4. プライマリからダウンロード
   └─ ファイルが存在しないミラーがある場合、プライマリからファイル取得
5. ミラーへアップロード
   └─ チェックサム検証しながら各ミラーにアップロード
```

### フローチャート

```mermaid
flowchart TD
    A[ジョブ開始] --> B{MirrorService?}
    B -->|No| C[何もせず終了]
    B -->|Yes| D[mirror実行]
    D --> E{ファイル未存在のミラーあり?}
    E -->|No| F[ジョブ完了]
    E -->|Yes| G[プライマリからファイル取得]
    G --> H{FileNotFoundError?}
    H -->|Yes| I[discard_on: ジョブ破棄]
    H -->|No| J[各ミラーにアップロード]
    J --> K{IntegrityError?}
    K -->|Yes| L[retry_on: 最大10回]
    K -->|No| F
    L --> G
```

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

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

本ジョブはデータベースを操作しない。

| 処理 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| - | - | - | データベース操作なし |

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

直接のテーブル操作なし

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | ActiveStorage::FileNotFoundError | プライマリにファイルが存在しない | discard_on: ジョブを破棄 |
| - | ActiveStorage::IntegrityError | チェックサム不一致 | retry_on: 最大10回リトライ |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | 最大10回 |
| リトライ間隔 | polynomially_longer（多項式的に増加） |
| リトライ対象エラー | ActiveStorage::IntegrityError |

### 障害時対応

1. FileNotFoundError発生時：プライマリからファイルが削除されている。Blobが削除済みの可能性あり、正常な動作としてジョブは破棄される
2. IntegrityError発生時：ファイル転送中の破損。リトライで解消されることが多い

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

| 項目 | 内容 |
|-----|------|
| トランザクション範囲 | なし（データベース操作なし） |
| コミットタイミング | - |
| ロールバック条件 | - |

## パフォーマンス要件

| 項目 | 内容 |
|-----|------|
| 想定処理件数 | 1件/ジョブ |
| 目標処理時間 | ファイルサイズ・ミラー数・ネットワーク速度に依存 |
| メモリ使用量上限 | ファイルサイズに依存（ストリーミング処理の場合は軽減） |

## 排他制御

同一ファイルに対する複数のMirrorJobは並列実行可能。各ミラーへのアップロードは冪等（既に存在するファイルは更新されない）。

## ログ出力

| ログ種別 | 出力タイミング | 出力内容 |
|---------|--------------|---------|
| ジョブログ | ジョブ開始/終了時 | ActiveJob標準のログ出力 |
| インストルメンテーション | mirror実行時 | key, checksumを含むイベント |

## 監視・アラート

| 監視項目 | 閾値 | アラート先 |
|---------|-----|----------|
| リトライ回数 | 10回超過時 | アプリケーションログ監視 |
| キュー滞留 | 運用ポリシーに依存 | キュー監視システム |

## 備考

- キュー名: ActiveStorage.queues[:mirror]（デフォルト: active_storage_mirror）
- MirrorServiceは以下の構成で設定：
  ```yaml
  mirror:
    service: Mirror
    primary: local
    mirrors:
      - amazon
      - google
  ```
- 直接アップロード時、プライマリへのアップロード完了後にミラーリングがエンキューされる
- 既にミラーにファイルが存在する場合はスキップされる（exist?チェック）
- service.try(:mirror, ...)を使用しているため、MirrorService以外では何も起こらない
