# 通知設計書 48-古いpidfile削除試行警告

## 概要

本ドキュメントは、Julia の FileWatching.Pidfile モジュールにおける古い pidfile の削除試行警告（`@warn "attempting to remove probably stale pidfile"`）の通知設計について記載する。

### 本通知の処理概要

pidfile のロック取得処理において、既存の pidfile が古い（stale）と判定された場合、その削除を試行する前にユーザーに警告を発行する通知である。

**業務上の目的・背景**：pidfile ロック機構では、プロセスがクラッシュや異常終了した場合に pidfile が残留することがある。残留した pidfile は他のプロセスのロック取得を永久にブロックする原因となるため、一定時間（`stale_age`）を超過した pidfile は自動的に削除する仕組みが用意されている。ただし、正当なプロセスが長時間稼働しているだけの可能性もあるため、削除前に警告を発行してユーザーの注意を喚起する。

**通知の送信タイミング**：`open_exclusive` 関数内で `stale_pidfile()` が `true` を返し、pidfile の削除（`tryrmopenfile`）を試行する直前に送信される。`wait=false` モードと `wait=true` モードの両方で発行され得る。

**通知の受信者**：pidfile ロック機構を使用するプロセスを実行している開発者・運用者。

**通知内容の概要**：古いと思われる pidfile の削除を試行すること、および対象の pidfile のパスを通知する。

**期待されるアクション**：ユーザーは残留 pidfile の原因を調査する。プロセスのクラッシュが原因であれば根本原因を修正する。正当なプロセスが長時間稼働している場合は `stale_age` の値を調整するか、`refresh` パラメータで mtime を更新する仕組みを有効にする。

## 通知種別

ログ出力（`@warn` マクロによる標準エラー出力へのWarnレベルログ）

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 同期（`@warn` マクロによる即時ログ出力） |
| 優先度 | 中（Warn レベル） |
| リトライ | なし（ログ出力のため） |

### 送信先決定ロジック

Julia のログシステムに委譲される。デフォルトでは `stderr` に出力される。

## 通知テンプレート

### メール通知の場合

該当なし（ログ出力のみ）

### 本文テンプレート

```
┌ Warning: attempting to remove probably stale pidfile
│   path = "<pidfileのパス>"
└ @ FileWatching.Pidfile stdlib/FileWatching/src/pidfile.jl:248
```

### 添付ファイル

該当なし

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| `path` | pidfile のファイルパス | `open_exclusive` 関数の引数 | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| API呼び出し | `open_exclusive()` で `wait=false` モード | `stale_age > 0` かつ `stale_pidfile()` が `true` | 非待機モードでの stale 判定 |
| API呼び出し | `open_exclusive()` で `wait=true` モード | `stale_age > 0` かつ `stale_pidfile()` が `true`（ループ内） | 待機モードでの stale 判定 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| `stale_age == 0` | stale_age が無効化されている場合 |
| `stale_pidfile()` が `false` | pidfile が stale ではないと判定された場合 |
| 2回目以降（`wait=true` モード） | `stale_age -= stale_age` によって stale_age が0にリセットされるため、同一ロック取得処理内で最大1回 |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A["open_exclusive() 呼び出し"] --> B["tryopen_exclusive() 試行"]
    B -->|"失敗"| C{"wait モード?"}
    C -->|"wait=false"| D{"stale_age > 0?"}
    D -->|Yes| E["stale_pidfile() 呼び出し"]
    E -->|true| F["@warn 警告出力"]
    F --> G["tryrmopenfile() 削除試行"]
    G --> H["再度 tryopen_exclusive()"]
    C -->|"wait=true"| I["待機ループ"]
    I --> J{"stale_age > 0?"}
    J -->|Yes| K["stale_pidfile() 呼び出し"]
    K -->|true| L["stale_age をゼロに"]
    L --> M["@warn 警告出力"]
    M --> N["tryrmopenfile() 削除試行"]
```

## データベース参照・更新仕様

### 参照テーブル一覧

該当なし（ファイルシステムのみ使用）

### 更新テーブル一覧

該当なし

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| pidfile削除失敗 | 他プロセスがファイルを保持中、権限不足 | `tryrmopenfile` が例外をキャッチし `IOError` を返す |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | `wait=true` モードでは1回（stale_age がゼロにリセットされるため） |
| リトライ間隔 | `poll_interval`（デフォルト10秒） |
| リトライ対象エラー | pidfile の削除失敗 |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 上限 | 同一ロック取得処理内で最大2回（wait=false: 1回、wait=true のループ内: 1回） |

### 配信時間帯

制限なし

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

- ログメッセージに pidfile のフルパスが含まれるため、ファイルシステム構造の情報が漏洩する可能性がある

## 備考

- `wait=true` モードでは `stale_age -= stale_age`（300行目）によって stale_age がゼロにリセットされるため、同一ロック取得処理内で最大1回の削除試行となる
- Windows 環境では `tryrmopenfile` がファイル名変更（rename）後に削除する特殊処理を行う
- `stale_age` の推奨値は通常の処理完了時間の3〜5倍

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | pidfile.jl | `stdlib/FileWatching/src/pidfile.jl` | `LockMonitor` 構造体（57-86行目）と pidfile フォーマット |
| 1-2 | pidfile.jl | `stdlib/FileWatching/src/pidfile.jl` | `PidlockedError` 例外（223-225行目） |

**読解のコツ**: pidfile のフォーマットは `"$pid $(gethostname())"` の単純なテキスト形式。`stale_age` と `refresh` の関係が重要。

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | pidfile.jl | `stdlib/FileWatching/src/pidfile.jl` | `open_exclusive` 関数（236-305行目） |

**主要処理フロー**:
1. **243行目**: `tryopen_exclusive` で高速パス
2. **245-258行目**: `wait=false` モード。246-248行目で stale 判定と警告（本通知の発行箇所1）
3. **261-304行目**: `wait=true` モードのループ
4. **297-301行目**: ループ内での stale 判定と警告（本通知の発行箇所2）
5. **300行目**: `stale_age -= stale_age` で2回目以降の削除試行を防止

#### Step 3: stale 判定ロジックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | pidfile.jl | `stdlib/FileWatching/src/pidfile.jl` | `stale_pidfile` 関数（196-206行目） |
| 3-2 | pidfile.jl | `stdlib/FileWatching/src/pidfile.jl` | `isvalidpid` 関数（178-189行目） |
| 3-3 | pidfile.jl | `stdlib/FileWatching/src/pidfile.jl` | `tryrmopenfile` 関数（316-342行目） |

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

```
mkpidlock(at, pid; stale_age, ...)              [pidfile.jl:62]
    │
    └─ open_exclusive(at; stale_age, wait, ...)  [pidfile.jl:236]
           │
           ├─ tryopen_exclusive(path, mode)       [pidfile.jl:214]
           │
           ├─ [wait=false] stale_pidfile(...)      [pidfile.jl:246-247]
           │      └─ true → @warn                  [pidfile.jl:248: 本通知]
           │              └─ tryrmopenfile(path)   [pidfile.jl:249]
           │
           └─ [wait=true loop] stale_pidfile(...)  [pidfile.jl:297]
                  └─ true → stale_age = 0          [pidfile.jl:300]
                          → @warn                   [pidfile.jl:301: 本通知]
                          → tryrmopenfile(path)     [pidfile.jl:302]
```

### データフロー図

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

pidfile パス ─────────▶ parse_pidfile()
    pid/hostname/age        │
                            ▼
stale_age ────────────▶ stale 判定
                            │
                    true → @warn ──────────────▶ ログ出力 (stderr)
                            │
                    tryrmopenfile() ────────────▶ pidfile 削除
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| pidfile.jl | `stdlib/FileWatching/src/pidfile.jl` | ソース | 通知発行元、pidfile ロック機構全体のロジック |
