# バッチ設計書 9-Storage::ReconcileJob

## 概要

本ドキュメントは、ストレージ使用量の整合性確認・修復処理を実行するバックグラウンドジョブ `Storage::ReconcileJob` の設計仕様を記載したものである。

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

このバッチは、実際のファイルストレージ使用量とレジャー（Storage::Entry）の記録値を比較し、差異がある場合に調整エントリを追加して整合性を修復する処理を行う。ストレージ追跡の信頼性を担保するための重要な機能を担う。

**業務上の目的・背景**：fizzyのストレージ追跡はレジャー方式を採用しており、各ファイル操作を差分として記録している。しかし、システム障害やバグにより、レジャーと実際のストレージ使用量に乖離が生じる可能性がある。本ジョブは、実際のファイルサイズを計算し、レジャーとの差異を検出・修復することで、ストレージ使用量の正確性を維持する。

**バッチの実行タイミング**：定期的なメンテナンス処理として実行、または手動でトリガーされる。

**主要な処理内容**：
1. オーナー（AccountまたはBoard）を受け取り、整合性確認を開始
2. スキャン前のエントリカーソル（最大ID）を記録
3. 実際のファイルストレージ使用量を計算
4. スキャン後のエントリカーソルを記録し、一貫性を確認
5. カーソルが一致しない場合（スキャン中に書き込みがあった場合）はリトライ
6. レジャー合計と実際の使用量の差分を計算
7. 差分がある場合は調整エントリを追加

**前後の処理との関連**：`Storage::MaterializeJob`がエントリの集約を担当し、本ジョブが整合性の確認・修復を担当する。両ジョブは補完的な関係にある。

**影響範囲**：`storage_entries`テーブル（調整エントリ追加）

## バッチ種別

データクレンジング / 整合性確認

## 実行スケジュール

| 項目 | 内容 |
|-----|------|
| 実行頻度 | 定期（日次/週次）または随時 |
| 実行時刻 | メンテナンス時間帯 |
| 実行曜日 | 設定に依存 |
| 実行日 | 設定に依存 |
| トリガー | スケジューラー/手動 |

## 実行条件

### 前提条件

| 条件 | 説明 |
|-----|------|
| オーナーの存在 | 有効なAccount/Boardレコードが存在すること |

### 実行可否判定

- `ActiveJob::DeserializationError`が発生した場合（オーナーが削除済みの場合など）はジョブを破棄する
- スキャン中にエントリが追加された場合は`ReconcileAborted`例外でリトライ

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | デフォルト値 | 説明 |
|-------------|-----|-----|-------------|------|
| owner | Account/Board | Yes | - | 整合性確認対象のオーナーオブジェクト |

### 入力データソース

| データソース | 形式 | 説明 |
|-------------|------|------|
| storage_entries | DB | レジャーエントリ |
| ファイルストレージ | ファイル | 実際のファイルサイズ |

## 出力仕様

### 出力データ

| 出力先 | 形式 | 説明 |
|-------|------|------|
| storage_entries | DB | 調整エントリの追加（差分がある場合） |

### 出力ファイル仕様

ファイル出力なし

## 処理フロー

### 処理シーケンス

```
1. ジョブ開始
   └─ owner.reconcile_storageメソッドを呼び出し
2. スキャン前カーソル取得
   └─ cursor_before = storage_entries.maximum(:id)
3. 実ファイルサイズ計算
   └─ real_bytes = calculate_real_storage_bytes
4. スキャン後カーソル取得
   └─ cursor_after = storage_entries.maximum(:id)
5. カーソル一致確認
   └─ cursor_before != cursor_after の場合は中止
6. レジャー合計計算
   └─ ledger_bytes = storage_entries.sum(:delta)
7. 差分計算
   └─ diff = real_bytes - ledger_bytes
8. 調整エントリ追加（差分がある場合）
   └─ Storage::Entry.record(operation: "reconcile", delta: diff)
9. ジョブ終了
```

### フローチャート

```mermaid
flowchart TD
    A[バッチ開始] --> B[スキャン前カーソル取得]
    B --> C[実ファイルサイズ計算]
    C --> D[スキャン後カーソル取得]
    D --> E{カーソル一致?}
    E -->|No| F[ReconcileAborted例外]
    F --> G[リトライ]
    E -->|Yes| H[レジャー合計計算]
    H --> I[差分計算]
    I --> J{差分あり?}
    J -->|No| K[成功終了]
    J -->|Yes| L[調整エントリ追加]
    L --> M[ログ出力]
    M --> K
    G --> A
```

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

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

| 処理 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 参照 | storage_entries | SELECT | 最大ID取得、合計計算 |
| 追加 | storage_entries | INSERT | 調整エントリの追加 |

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

#### storage_entries

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | id | MAX(id) | カーソル取得 |
| SELECT | delta | SUM(delta) WHERE id <= cursor | レジャー合計 |
| INSERT | operation | "reconcile" | 調整エントリ |
| INSERT | delta | real_bytes - ledger_bytes | 差分値 |
| INSERT | recordable | nil | 特定レコードに紐づかない |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| DeserializationError | シリアライズエラー | オーナーが削除済み | ジョブを破棄 |
| ReconcileAborted | 一貫性エラー | スキャン中にエントリ追加 | 1分後にリトライ |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | 3回 |
| リトライ間隔 | 1分 |
| リトライ対象エラー | ReconcileAborted |

### 障害時対応

3回のリトライ後も失敗する場合は、ジョブキューから除外される。手動での整合性確認が必要となる場合がある。

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

| 項目 | 内容 |
|-----|------|
| トランザクション範囲 | 各操作は個別 |
| コミットタイミング | 調整エントリ追加時 |
| ロールバック条件 | 例外発生時 |

## パフォーマンス要件

| 項目 | 内容 |
|-----|------|
| 想定処理件数 | ファイル数に依存 |
| 目標処理時間 | ファイル数に依存 |
| メモリ使用量上限 | ファイル数に依存 |

## 排他制御

- 同時実行制限: オーナーごとに1
- `limits_concurrency to: 1, key: ->(owner) { owner }`によりオーナー単位で同時実行を制限
- Two-cursorアプローチにより、スキャン中の書き込みを検出

## ログ出力

| ログ種別 | 出力タイミング | 出力内容 |
|---------|--------------|---------|
| 開始ログ | ジョブ開始時 | ActiveJob標準ログ |
| 警告ログ | スキャン中止時 | `[Storage] Reconcile aborted for ...` |
| 情報ログ | 調整実行時 | `[Storage] Reconcile ...: adjusting by N bytes` |
| 終了ログ | ジョブ終了時 | ActiveJob標準ログ |

## 監視・アラート

| 監視項目 | 閾値 | アラート先 |
|---------|-----|----------|
| ジョブ失敗率 | 異常な増加 | ジョブ監視システム |
| 調整回数 | 頻繁な調整発生 | ジョブ監視システム |
| 調整量 | 大きな差分 | ジョブ監視システム |

## 備考

- キュー名: `backend`
- 同時実行制限: オーナーごとに1（`limits_concurrency`）
- リトライ: 3回、1分間隔（`ReconcileAborted`のみ）
- Two-cursorアプローチ:
  - スキャン前後でエントリの最大IDを取得
  - IDが異なる場合、スキャン中にエントリが追加されたと判断
  - 正確な差分計算のため、リトライして再スキャン
- 調整エントリ:
  - operation: "reconcile"
  - recordable: nil（特定のファイルに紐づかない）
  - delta: 実際のサイズ - レジャー合計
- `calculate_real_storage_bytes`はサブクラス（Account/Board）で実装が必要
