---
title: スケジューラーのライフサイクル
description: alas.py のメインループ、タスク選定、例外の階層的リカバリ、スレッドモデル、キャッシュ無効化規約。
updated: 2026-10-20
---

# スケジューラーのライフサイクル

`alas.py` はランタイムの心臓部です。ゲーム操作自体は一切実装しておらず、「次のタスクを選出 → 設定をバインド → 実行 → 結果に応じた次回スケジューリング」のループ処理のみを担当します。

<SchedulerFlow />

## <DocIcon name="rocket" /> エントリポイント

| エントリ | 用途 |
| --- | --- |
| `uv run python alas.py [インスタンス名]` | CLI 起動。省略時は `ap`、不正な名前は終了コード 2 で即座に終了 |
| `AzurLaneAutoScript(config_name).loop()` | worker プロセスのメイン実行エントリ |
| `AzurLaneAutoScript.run(command)` | 単一タスクの直接実行（WebUI「即時実行」） |
| `config.task_call('Restart')` | タスク境界をまたぐ再起動処理の割り込み挿入 |
| `AzurLaneAutoScript.stop_event` | プロセス間停止シグナルプロキシ |

## <DocIcon name="refresh" /> メインループの処理ステップ

各実行サイクルは以下の6つのステップで固定されています：

1. **停止シグナルの検知**：`stop_event` がセットされている場合はループを抜け、正常な終了シーケンスへ移行。
2. **メンテナンス待機**：`checker.wait_until_available()` を呼び出し、メンテナンス終了の瞬間に設定をリロードして `Restart` を注入。
3. **定期再起動の判定**：直前の再起動から `RestartIntervalHours`（デフォルト4時間）が経過している場合、タスクの合間でエミュレーターを再起動。
4. **タスクの選出**：`get_next_task()` により実行対象タスクを決定。
5. **タスクの実行**：`device.config` を更新し、フリーズ・連打カウンタをクリアした上で、実行監視（ウォッチドッグ）を有効化して `run(command)` を呼び出し。
6. **リザルト精算**：失敗カウンタの更新、センシティブ判定、結果に応じたインターバル設定。

### タスク選定の内部挙動

`config.get_next()` は「期限が到来しており、かつ有効化されている」タスクの中から最も優先度の高いものを選択します。該当するタスクが存在しない場合は、待機キューの中から最も早く期限を迎えるタスクを選び、返却されるコピーの `next_run` にホarding時間を加算します。

両方のキューが空の場合は `RequestHumanTakeover` を送出し、少なくとも1つのタスクを有効化するよう促します。

待機時間が3時間を超え、かつ `CloseEmulatorDuringLongWait` が有効な場合はエミュレーターをシャットダウンします。プリヒート（事前起動）が有効な場合は逆に、予定時刻の数分前にあらかじめ起動してメイン画面で待機させます。

### wait_until と設定のホットリロード

```python
def wait_until(self, future):
    while True:
        if self.stop_event.is_set():
            exit(0)          # 親プロセスにより「更新終了」として分類
        if self.config.should_reload():
            return False     # 外側のループで config キャッシュを破棄し、タスクを再選定
        ...
```

待機中は5秒ごとに停止イベントと設定ファイルの更新日時（mtime）をチェックします。差分マージは行わず、**設定オブジェクト全体を丸ごと再構築**します。

## <DocIcon name="circle-check" /> run() の3値戻り値

| 戻り値 | 意味 | 失敗カウンタの扱い |
| --- | --- | --- |
| `True` | 正常終了（`TaskEnd` を含む） | ゼロクリア |
| `'recoverable'` | 失敗したが、現場で適切なリカバリを予約済み | カウント維持（加算しない） |
| `False` | 回復不能な失敗 | ＋1 加算 |

可観測なリカバリ処理が行われた場合に失敗カウンタを加算しない理由：失敗カウンタは「エミュレーター全体の強制再起動」という高コストなアクションのトリガーとして機能するため、すでに個別対策が講じられたエラーに対して二重のペナルティを課さないためです。

## <DocIcon name="circle-alert" /> 例外処理マトリクス

すべての例外ハンドラは、まず `_check_sensitive_exit()` を評価します：厳格再起動が有効かつ当該タスクがセンシティブ設定されている場合、エラー現場を保存して通知を送信し、即座に `exit(1)` でプロセスを停止します。

| 発生例外 | 自動復旧アクション | 返却値 |
| --- | --- | --- |
| `TaskEnd` | 正常終了とみなす | `True` |
| `GameNotRunningError` | `Restart` タスクをキューに注入 | `recoverable` |
| `GameStuckError` / `GameTooManyClickError` | 連続閾値到達時はエミュレーター再起動、未満時は10秒後にゲーム再起動 | `recoverable` |
| `GameBugError` | ゲームクライアントを再起動 | `recoverable` |
| `GamePageUnknownError` | サーバー稼働確認：正常ならゲーム再起動、メンテ中なら待機 | `recoverable` / `False` |
| `ScriptError` | 2回目までは `Restart` 注入、3回連続でプロセス停止 | `recoverable` / `exit(1)` |
| `EmulatorNotRunningError` | エミュレーター再起動 ＋ `Restart` 注入（リトライ間隔を拡大） | `recoverable` |
| `RequestHumanTakeover` | まずエミュレーター再起動による自己復旧を試行 | `recoverable` |
| `MindCalculatorScanError` | 原因とスクリーンショットを記録し、旧データを維持（再起動は抑止） | `False` |
| `AutoSearchSetError` | ゲームクライアントを再起動 | `recoverable` |
| その他の一般 `Exception` | 連続閾値到達時にエミュレーター再起動へエスカレーション | `recoverable` |

最外層の `except` は最後のセーフティネットとして機能し、ログ出力、エミュレーター再起動、`Restart` 注入を行った上で、指数バックオフ（20秒から最大300秒）を挟んで自動リトライします。

### プロセスが完全に終了する4つの経路

1. `ScriptError` が連続して3回発生した場合。
2. センシティブタスクでエラーが発生した場合。
3. `stop_event` がセットされた場合。
4. 設定ファイルやデバイスの初期化に失敗した場合。

これら以外の例外でプロセスを勝手に終了させてはなりません。24時間365日の連続稼働は「障害によってプロセスを安易に落とさない」原則の上に成り立っています。

## <DocIcon name="clock" /> スレッドモデル

| スレッド名 | 生成契機 | 責務と役割 |
| --- | --- | --- |
| メインスレッド | worker プロセス起動時 | `loop()` の実行およびタスクの逐次処理 |
| `alas-watchdog` | いずれかの監視機能が有効な場合 | 30秒ごとにタスクタイムアウトや定期再起動を検査 |
| `daily-summary-scheduler-<name>` | デイリー日報が有効な場合 | 1秒ごとに送信トリガー時刻を判定 |
| エミュレーター起動停止ワーカー | 各種起動・停止操作の実行時 | 600秒のハードタイムアウト付きで stop/start を非同期実行 |

重要な設計規約：

- **監視スレッドは `run()` の実行期間中のみアクティブ化**され、待機スリープ中に誤作動することはありません。
- **ウォッチドッグはスレッドを殺さず、エミュレーターを終了させます**：Python スレッドが ADB の C 拡張内でブロックされている場合、Python レイヤーから安全に割り込むことができないためです。
- **エミュレーターの起動停止は必ず `_emulator_op_with_timeout` を経由**させます。
- **日報スレッドは `self.config` を直接参照せず**、ディスク上の設定ファイルのリードオンスナップショットを読み取ります。

## <DocIcon name="drive" /> キャッシュとライフサイクル

| オブジェクト | キャッシュ種別 | 無効化（破棄）タイミング |
| --- | --- | --- |
| `config` / `device` / `checker` | `cached_property` | `del_cached_property`：タスク終了時、エラー復旧時、メンテ終了時、再起動時 |
| `failure_record` | メモリ上インスタンス変数 | タスク成功時にクリア、プロセス再起動時にクリア |

## <DocIcon name="wrench" /> デバッグ

| 調査手段 | 活用方法 |
| --- | --- |
| ログの確認 | `[AzurPilot]` を検索してスケジューラーの意思決定ログを追跡 |
| エラー現場の確認 | `log/error/<インスタンス名>/<タイムスタンプ>/` 配下のログと画像を分析 |
| 単一タスクのテスト | WebUI の「即時実行」から `run(command)` を単体実行 |
| 外部スケジューラー連携 | 起動コマンド、設定、ログ、プロセス終了の4つの規約を利用（下記参照） |

### 外部スケジューラー契約 {#外部调度器契约}

外部ツール（AUTO-MAS 等）から AzurPilot を連携制御するための契約仕様：

| 項目 | 連携規約 |
| --- | --- |
| 起動方法 | `uv run python alas.py <インスタンス名>`（作業ディレクトリはプロジェクトルート） |
| 設定管理 | `config/<インスタンス名>.json` を直接読み書き |
| ログ監視 | `log/<日付>_<インスタンス名>.txt`（「开始任务 / 结束任务」をタスク境界としてパース） |
| 停止方法 | プロセスツリー全体の強制終了（停止制御ファイル等は非提供） |

`module/logger.py` はインポート時に作業ディレクトリを自動的にプロジェクトルートへ切り替えるため、外部ランチャー側で cwd を厳密に指定する必要はありません。ただし、1つのインスタンス名につきプロセスは必ず1つに限定してください。
