# 機能設計書 38-Scheduler

## 概要

本ドキュメントは、Symfony Schedulerコンポーネントの機能設計を記述する。SchedulerコンポーネントはSymfony Messengerを活用したタスクスケジューリング機能を提供し、cron式やインターバル指定による定期的なメッセージ実行を実現する。

### 本機能の処理概要

Schedulerコンポーネントは、RecurringMessage（定期メッセージ）をScheduleに登録し、トリガー条件（cron式、周期的な間隔）に基づいてメッセージを生成・ディスパッチする仕組みを提供する。Messenger Workerと統合することで、Messengerのインフラ（トランスポート、ミドルウェア、リトライ等）をそのまま活用した定期タスク実行が可能となる。

**業務上の目的・背景**：アプリケーション運用においてレポート生成、データクリーンアップ、外部システムとの同期、キャッシュ更新等の定期タスクは不可欠である。従来はOSのcrontabで管理していたが、SchedulerコンポーネントによりPHPコードベースでスケジュール定義が可能となり、バージョン管理、テスト、動的変更が容易になる。

**機能の利用シーン**：日次/週次/月次のレポート生成、定期的なデータベースクリーンアップ、外部APIデータの定期同期、一時ファイルの定期削除、ヘルスチェック、メール送信のスケジュール実行など、定期実行が必要なあらゆるバックグラウンドタスクで使用される。

**主要な処理内容**：
1. RecurringMessageの定義（every, cron, trigger メソッド）
2. Scheduleへのメッセージ登録と管理（add, remove, clear）
3. トリガーの評価（CronExpressionTrigger, PeriodicalTrigger, JitterTrigger）
4. MessageGeneratorによるスケジュールに基づくメッセージ生成
5. Scheduler::run()による直接実行ループ（Messenger非使用時）
6. Messengerトランスポートとしての統合（SchedulerTransport）
7. 状態管理（stateful: キャッシュベースの実行状態の永続化）
8. ロック機構（Schedule::lock: 排他制御）
9. イベントシステム（PreRunEvent, PostRunEvent, FailureEvent）
10. Jitter機能（実行タイミングへのランダムな遅延付加）

**関連システム・外部連携**：Messengerコンポーネント（メッセージバスとトランスポート）、Lockコンポーネント（排他制御）、Cacheコンポーネント（実行状態の永続化）、Clockコンポーネント（時刻管理・テスト用モック）、EventDispatcherコンポーネント（実行前後のイベント通知）と連携する。

**権限による制御**：スケジュール自体には権限制御はない。ワーカープロセスの実行権限に依存する。

## 関連画面

本機能はバックグラウンドタスクであり、直接関連する画面はない。

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| - | - | - | 画面関連なし |

## 機能種別

タスクスケジューリング処理

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| frequency (every) | string\|int\|\DateInterval | Yes | 実行間隔。秒数、ISO 8601期間、DateInterval | 有効な期間指定であること |
| expression (cron) | string | Yes | cron式（例: "0 * * * *"） | 有効なcron式であること |
| message | object | Yes | ディスパッチするメッセージオブジェクト | オブジェクト型であること |
| from | string\|\DateTimeImmutable\|null | No | スケジュール開始日時 | - |
| until | string\|\DateTimeImmutable | No | スケジュール終了日時（デフォルト: 3000-01-01） | - |
| timezone (cron) | \DateTimeZone\|string\|null | No | タイムゾーン | - |
| jitter (withJitter) | int | No | 最大ジッター秒数（デフォルト: 60） | 正の整数 |

### 入力データソース

- PHPコードによるスケジュール定義（ScheduleProviderInterface実装）
- サービスコンテナからのスケジュール登録

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| messages | iterable | トリガー条件を満たしたメッセージの反復子 |

### 出力先

- Scheduler::run()によるハンドラー直接実行
- Messengerトランスポート経由でのメッセージバスディスパッチ

## 処理フロー

### 処理シーケンス

```
1. Schedule の構築
   ├─ RecurringMessage::every() / cron() / trigger() でメッセージ定義
   ├─ Schedule::add() で登録
   ├─ Schedule::lock() でロック設定（オプション）
   └─ Schedule::stateful() で状態永続化設定（オプション）
2. Scheduler::run() による実行ループ
   ├─ generators（MessageGenerator）のループ
   │   ├─ MessageGenerator::getMessages() でトリガー評価
   │   │   └─ TriggerInterface::getNextRunDate() で次回実行日時取得
   │   ├─ PreRunEvent のディスパッチ
   │   ├─ ハンドラー実行
   │   ├─ PostRunEvent のディスパッチ
   │   └─ FailureEvent のディスパッチ（エラー時）
   └─ メッセージなしの場合はsleep
3. 停止
   └─ Scheduler::stop() で shouldStop フラグをtrue
```

### フローチャート

```mermaid
flowchart TD
    A[Scheduler::run] --> B[実行ループ開始]
    B --> C[generators ループ]
    C --> D[MessageGenerator::getMessages]
    D --> E{メッセージあり?}
    E -->|Yes| F{Dispatcher設定?}
    F -->|Yes| G[PreRunEvent ディスパッチ]
    G --> H{キャンセル?}
    H -->|No| I[ハンドラー実行]
    H -->|Yes| C
    I --> J[PostRunEvent ディスパッチ]
    F -->|No| K[ハンドラー直接実行]
    E -->|No| L{shouldStop?}
    J --> C
    K --> C
    L -->|No| M[sleep]
    M --> B
    L -->|Yes| N[終了]
    I -.->|例外| O[FailureEvent]
    O --> P{shouldIgnore?}
    P -->|Yes| C
    P -->|No| Q[例外再スロー]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-38-01 | 周期的トリガー | every()で指定された間隔ごとにメッセージを生成する | RecurringMessage::every使用時 |
| BR-38-02 | Cronトリガー | cron式の評価結果に基づいてメッセージを生成する | RecurringMessage::cron使用時 |
| BR-38-03 | ハッシュドCron | cron式に`#`を含む場合、メッセージのStringable表現をハッシュして時刻を決定する | cron式に`#`含む場合 |
| BR-38-04 | Jitter | 実行タイミングにランダムな遅延（最大60秒）を付加してサンダリングハード問題を緩和する | withJitter使用時 |
| BR-38-05 | 状態永続化 | CacheInterfaceを使用してスケジュールの実行状態を永続化し、再起動時に未実行タスクを回復する | stateful設定時 |
| BR-38-06 | 排他制御 | LockInterfaceを使用して同一スケジュールの並行実行を防止する | lock設定時 |
| BR-38-07 | 最終ミス実行のみ処理 | processOnlyLastMissedRunが有効な場合、未実行タスクの中で最後のもののみ処理する | processOnlyLastMissedRun有効時 |
| BR-38-08 | 重複メッセージ検出 | 同一IDのRecurringMessageを追加するとLogicExceptionをスローする | 同一IDメッセージの重複追加時 |
| BR-38-09 | 動的スケジュール変更 | add/remove/clearでスケジュールを動的に変更でき、変更時はrestartフラグが設定される | 実行中のスケジュール変更時 |

### 計算ロジック

- RecurringMessage ID: `hash('crc32c', provider::class + provider::getId() + trigger::class + (string)trigger)` の結合ハッシュ
- スリープ時間: `options['sleep'] - 1e6 * (now - start)` で実行時間を差し引いた残りスリープ

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

本機能自体はデータベース操作を行わない。ただしMessengerのDoctrineトランスポート経由で間接的にDB操作が発生する場合がある。

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

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

該当なし。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | LogicException | 重複するRecurringMessage IDの追加 | メッセージIDを確認する |
| - | InvalidArgumentException | cron式に`#`を使用するがメッセージがStringableでない | メッセージにStringableインターフェースを実装する |
| - | FailureEvent | ハンドラー実行中の例外 | FailureEventリスナーでshouldIgnoreを設定するか、ハンドラーを修正する |

### リトライ仕様

- Scheduler自体にはリトライ機構はない
- Messenger経由で実行する場合は、Messengerのリトライ戦略が適用される

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

トランザクション管理は行わない。

## パフォーマンス要件

- ポーリング間隔（sleep設定）によりCPU消費を制御（デフォルト: 1秒）
- 実行時間を差し引いたスリープ計算により、正確なインターバルを維持
- Jitter機能により、多数のスケジュールタスクが同時刻に集中することを緩和

## セキュリティ考慮事項

- スケジュール定義はPHPコードで行うため、コード改ざんに対する保護が必要
- ワーカープロセスの実行権限を最小限に設定する

## 備考

- SchedulerコンポーネントはSymfony 6.3で導入された
- Messenger TransportとしてSchedulerTransportを提供することで、messenger:consumeコマンドでスケジュールメッセージを処理できる
- ClockInterfaceを使用しているため、テスト時にMockClockを注入してスケジュール動作をテストできる

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

スケジュールとトリガーのデータ構造を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | RecurringMessage.php | `src/Symfony/Component/Scheduler/RecurringMessage.php` | 定期メッセージのファクトリ。every, cron, triggerの3つの生成メソッド |
| 1-2 | Schedule.php | `src/Symfony/Component/Scheduler/Schedule.php` | スケジュール定義。メッセージの追加・削除・状態管理 |
| 1-3 | ScheduleProviderInterface.php | `src/Symfony/Component/Scheduler/ScheduleProviderInterface.php` | スケジュールプロバイダーインターフェース |

**読解のコツ**: RecurringMessageはprivateコンストラクタを持ち、every()/cron()/trigger()のスタティックファクトリメソッドでのみインスタンス化される。

#### Step 2: エントリーポイントを理解する

Schedulerクラスの実行ループを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | Scheduler.php | `src/Symfony/Component/Scheduler/Scheduler.php` | スケジューラー本体。run()メソッドが実行ループ |

**主要処理フロー**:
1. **34-43行目**: コンストラクタ。handlers, schedules, clock, dispatcherの受け取り
2. **45-53行目**: `addSchedule()` / `addMessageGenerator()` - スケジュールの動的追加
3. **61-107行目**: `run()` - メインループ。generators反復、PreRunEvent/PostRunEvent/FailureEvent、スリープ計算
4. **109-112行目**: `stop()` - shouldStopフラグの設定

#### Step 3: トリガーを理解する

各トリガーの実装を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | TriggerInterface.php | `src/Symfony/Component/Scheduler/Trigger/TriggerInterface.php` | トリガーインターフェース |
| 3-2 | PeriodicalTrigger.php | `src/Symfony/Component/Scheduler/Trigger/PeriodicalTrigger.php` | 周期的トリガー |
| 3-3 | CronExpressionTrigger.php | `src/Symfony/Component/Scheduler/Trigger/CronExpressionTrigger.php` | Cron式トリガー |
| 3-4 | JitterTrigger.php | `src/Symfony/Component/Scheduler/Trigger/JitterTrigger.php` | ジッタートリガー（デコレータ） |

#### Step 4: メッセージジェネレーターを理解する

スケジュールからメッセージを生成する仕組みを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | MessageGenerator.php | `src/Symfony/Component/Scheduler/Generator/MessageGenerator.php` | スケジュールに基づくメッセージ生成器 |
| 4-2 | MessageContext.php | `src/Symfony/Component/Scheduler/Generator/MessageContext.php` | メッセージ実行コンテキスト |

### プログラム呼び出し階層図

```
Scheduler::run()
    |
    +-- MessageGenerator::getMessages()
    |       |
    |       +-- Schedule::getRecurringMessages()
    |       +-- RecurringMessage::getTrigger()
    |       |       +-- TriggerInterface::getNextRunDate()
    |       |           +-- PeriodicalTrigger / CronExpressionTrigger / JitterTrigger
    |       |
    |       +-- RecurringMessage::getMessages()
    |               +-- MessageProviderInterface::getMessages()
    |
    +-- EventDispatcher::dispatch()
    |       +-- PreRunEvent
    |       +-- PostRunEvent
    |       +-- FailureEvent
    |
    +-- handlers[$message::class]($message)
    |
    +-- Clock::sleep() [メッセージなし時]

Schedule
    +-- add(RecurringMessage)
    +-- remove(RecurringMessage)
    +-- lock(LockInterface)
    +-- stateful(CacheInterface)
    +-- before/after/onFailure(callable)
```

### データフロー図

```
[入力]                        [処理]                              [出力]

RecurringMessage定義 ────> Schedule::add()
(every/cron/trigger)         |
                              v
                        Scheduler::run()
                              |
                        MessageGenerator::getMessages()
                              |
                        TriggerInterface::getNextRunDate()
                              |
                        ┌─────┴─────┐
                        |           |
                  条件一致       条件不一致
                        |           |
                  メッセージ    sleep
                  生成            |
                        |       次のループ
                  Handler実行
                        |
                  PostRunEvent ──────> 処理完了
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| Scheduler.php | `src/Symfony/Component/Scheduler/Scheduler.php` | ソース | スケジューラー本体 |
| Schedule.php | `src/Symfony/Component/Scheduler/Schedule.php` | ソース | スケジュール定義 |
| RecurringMessage.php | `src/Symfony/Component/Scheduler/RecurringMessage.php` | ソース | 定期メッセージ定義 |
| ScheduleProviderInterface.php | `src/Symfony/Component/Scheduler/ScheduleProviderInterface.php` | ソース | スケジュールプロバイダー |
| MessageGenerator.php | `src/Symfony/Component/Scheduler/Generator/MessageGenerator.php` | ソース | メッセージ生成器 |
| MessageContext.php | `src/Symfony/Component/Scheduler/Generator/MessageContext.php` | ソース | メッセージコンテキスト |
| TriggerInterface.php | `src/Symfony/Component/Scheduler/Trigger/TriggerInterface.php` | ソース | トリガーインターフェース |
| PeriodicalTrigger.php | `src/Symfony/Component/Scheduler/Trigger/PeriodicalTrigger.php` | ソース | 周期的トリガー |
| CronExpressionTrigger.php | `src/Symfony/Component/Scheduler/Trigger/CronExpressionTrigger.php` | ソース | Cron式トリガー |
| JitterTrigger.php | `src/Symfony/Component/Scheduler/Trigger/JitterTrigger.php` | ソース | ジッタートリガー |
| Messenger/ | `src/Symfony/Component/Scheduler/Messenger/` | ソース | Messenger統合 |
| Event/ | `src/Symfony/Component/Scheduler/Event/` | ソース | イベント群 |
