# バッチ設計書 6-SavedArticlesFetcher

## 概要

本ドキュメントは、Wikipedia iOSアプリにおける保存済み記事フェッチャー（SavedArticlesFetcher）のバッチ設計について記述します。

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

SavedArticlesFetcherは、ユーザーが保存した記事をオフライン閲覧用にダウンロードし、Spotlightインデックスへの登録も行うバッチ処理です。

**業務上の目的・背景**：ユーザーが記事を「保存」した場合、オフライン環境でも記事を閲覧できるようにする必要があります。これにより、ネットワーク接続がない場所（飛行機内、地下鉄など）でもWikipedia記事を読むことができます。また、Spotlight検索への登録により、iOS全体から保存記事を検索できるようになります。

**バッチの実行タイミング**：記事が保存されたとき、同期完了時、アプリ起動時に実行されます。

**主要な処理内容**：
1. 保存済み記事の一覧取得（savedDate != NULL かつ isDownloaded != YES）
2. 記事コンテンツのダウンロード（ArticleCacheController経由）
3. Spotlightインデックスへの登録
4. ダウンロード失敗時のリトライスケジュール（最大28日間隔）
5. 未保存のダウンロード済み記事の削除

**前後の処理との関連**：ReadingListsSyncOperationで同期された記事がダウンロード対象となります。DatabaseHousekeeperTaskのクリーンアップ対象と連携します。

**影響範囲**：WMFArticle、キャッシュストレージ、Spotlightインデックスに影響します。

## バッチ種別

データダウンロード / キャッシュ管理

## 実行スケジュール

| 項目 | 内容 |
|-----|------|
| 実行頻度 | イベント駆動 |
| 実行時刻 | 随時 |
| 実行曜日 | 毎日 |
| 実行日 | 毎日 |
| トリガー | WMFArticleUpdated通知 / syncDidFinish通知 |

## 実行条件

### 前提条件

| 条件 | 説明 |
|-----|------|
| SavedArticlesFetcherがstart済み | isRunningがtrue |
| ダウンロード対象記事が存在 | savedDate != NULL かつ isDownloaded != YES |
| リトライ期間が経過 | downloadRetryDate == NULL または downloadRetryDate < 現在日時 |

### 実行可否判定

start()メソッドで監視が開始され、WMFArticleUpdatedまたはsyncDidFinish通知を受信すると処理が実行されます。500ミリ秒のデバウンス処理により、連続した通知は統合されます。

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | デフォルト値 | 説明 |
|-------------|-----|-----|-------------|------|
| dataStore | MWKDataStore | Yes | - | データストア参照 |

### 入力データソース

| データソース | 形式 | 説明 |
|-------------|------|------|
| CoreData (WMFArticle) | DB | 保存済み記事情報 |
| Wikipedia API | REST API | 記事コンテンツ |
| ArticleCacheController | キャッシュ | キャッシュ管理 |

## 出力仕様

### 出力データ

| 出力先 | 形式 | 説明 |
|-------|------|------|
| CoreData (WMFArticle) | DB | isDownloaded更新、エラー情報 |
| ArticleCache | ファイル | キャッシュされた記事コンテンツ |
| Spotlight Index | iOS Index | 検索可能な記事情報 |

### 出力ファイル仕様

| 項目 | 内容 |
|-----|------|
| ファイル名 | ArticleCacheController管理 |
| 出力先 | アプリキャッシュディレクトリ |
| 文字コード | UTF-8 |
| 形式 | HTML/JSON/画像 |

## 処理フロー

### 処理シーケンス

```
1. start()呼び出し
   └─ isRunning = true、NotificationCenter監視開始
2. WMFArticleUpdated/syncDidFinish通知受信
   └─ update()メソッド呼び出し
3. デバウンス処理（500ms）
   └─ cancelPreviousPerformRequests + perform afterDelay
4. _update()実行
   └─ isUpdatingチェック、バックグラウンドタスク開始
5. ダウンロード対象記事取得
   └─ fetchRequest with articlesToFetchPredicate
6. 記事ダウンロード
   └─ articleCacheController.add(url:groupKey:)
7. ダウンロード結果処理
   └─ 成功: isDownloaded = true、Spotlight登録
   └─ 失敗: リトライスケジュール設定
8. 未保存記事の削除チェック
   └─ savedDate == NULL && isDownloaded == YES
9. 記事キャッシュ削除
   └─ articleCacheController.remove(groupKey:)
10. 次の記事処理
    └─ 100msディレイ後に再帰呼び出し
11. 完了
    └─ バックグラウンドタスク終了
```

### フローチャート

```mermaid
flowchart TD
    A[start] --> B[通知監視開始]
    B --> C[WMFArticleUpdated受信]
    C --> D[update呼び出し]
    D --> E[500msデバウンス]
    E --> F[_update実行]
    F --> G{isUpdating?}
    G -->|Yes| H[countOfFetchesInProcess更新して終了]
    G -->|No| I[バックグラウンドタスク開始]
    I --> J[ダウンロード対象記事取得]
    J --> K{記事あり?}
    K -->|Yes| L[articleCacheController.add]
    K -->|No| M[削除対象記事取得]
    L --> N{ダウンロード成功?}
    N -->|Yes| O[isDownloaded = true]
    N -->|No| P[リトライスケジュール設定]
    O --> Q[Spotlight登録]
    P --> R[100ms後に_update再実行]
    Q --> R
    M --> S{削除対象あり?}
    S -->|Yes| T[articleCacheController.remove]
    S -->|No| U[バックグラウンドタスク終了]
    T --> V[isDownloaded = false]
    V --> R
```

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

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

| 処理 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 対象取得 | WMFArticle | SELECT | ダウンロード対象記事の取得 |
| ダウンロード完了 | WMFArticle | UPDATE | isDownloadedフラグ更新 |
| ダウンロード失敗 | WMFArticle | UPDATE | error、downloadAttemptCount、downloadRetryDate更新 |
| キャッシュ削除 | WMFArticle | UPDATE | isDownloadedをfalseに更新 |

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

#### WMFArticle

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | - | savedDate != NULL AND isDownloaded != YES AND (downloadRetryDate == NULL OR downloadRetryDate < now) | ダウンロード対象 |
| UPDATE | isDownloaded | true | ダウンロード成功時 |
| UPDATE | isDownloaded | false | キャッシュ削除時 |
| UPDATE | error | .apiFailed / .saveToDiskFailed | エラー発生時 |
| UPDATE | downloadAttemptCount | +1 | リトライ時にインクリメント |
| UPDATE | downloadRetryDate | 現在日時 + リトライ間隔 | 次回リトライ日時 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| .apiFailed | WMFArticleError | API呼び出し失敗 | リトライスケジュール設定 |
| .saveToDiskFailed | WMFArticleError | ディスク容量不足 | 処理停止、通知送信 |
| NSURLErrorTimedOut | URLError | タイムアウト | 処理停止 |
| NSURLErrorNotConnectedToInternet | URLError | ネットワーク未接続 | 処理停止 |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | 5回（指数バックオフ） |
| リトライ間隔 | 30秒、15分、7.5時間、9.4日、28日 |
| リトライ対象エラー | API失敗、一部ネットワークエラー |

### 障害時対応

ディスク容量不足の場合はSavedArticlesFetcher.saveToDiskDidFail通知を送信してフェッチを停止します。ネットワーク切断の場合も停止し、接続復帰後の次回更新で再開されます。

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

| 項目 | 内容 |
|-----|------|
| トランザクション範囲 | 記事単位 |
| コミットタイミング | 各記事処理完了時 |
| ロールバック条件 | CoreData保存エラー時 |

## パフォーマンス要件

| 項目 | 内容 |
|-----|------|
| 想定処理件数 | 1記事ずつ順次処理 |
| 目標処理時間 | 記事サイズに依存 |
| メモリ使用量上限 | 1記事分のキャッシュデータ |

## 排他制御

- isUpdatingフラグで重複実行を防止
- UIBackgroundTaskIdentifierでバックグラウンド実行を保護
- cancelAllRequests()で実行中タスクをキャンセル可能

## ログ出力

| ログ種別 | 出力タイミング | 出力内容 |
|---------|--------------|---------|
| エラーログ | フェッチエラー時 | "Error fetching next article to download: \(error)" |
| エラーログ | カウントエラー時 | "Error counting number of article to be downloaded: \(error)" |
| デバッグログ | ダウンロード成功時 | "Successfully saved all items for \(articleKey)" |
| エラーログ | ダウンロード失敗時 | "Failed saving items for \(articleKey): \(error)" |
| エラーログ | 保存エラー時 | "Error saving after saved articles fetch: \(error)" |

## 監視・アラート

| 監視項目 | 閾値 | アラート先 |
|---------|-----|----------|
| ディスク容量不足 | エラー発生時 | NotificationCenter (saveToDiskDidFail) |

## 備考

- progressプロパティでダウンロード進捗を追跡可能
- WMFSavedPageSpotlightManagerでSpotlight連携
- バックグラウンドタスク名は"SavedArticlesFetch"
- expirationHandlerでバックグラウンドタイムアウト時に安全に停止
- リトライ間隔は指数バックオフ：30秒 -> 15分 -> 7.5時間 -> 9.4日 -> 28日
