# 通知設計書 47-ファイルシステム時刻ずれ警告

## 概要

本ドキュメントは、Julia の FileWatching.Pidfile モジュールにおけるファイルシステム時刻スキュー検出警告（`@warn "filesystem time skew detected"`）の通知設計について記載する。

### 本通知の処理概要

pidfile のロック機構において、pidfile の mtime（更新時刻）とシステム時刻の間に負の大きな差異（時刻スキュー）が検出された場合に警告を発行する通知である。

**業務上の目的・背景**：pidfile はプロセス間の排他制御に使用されるロック機構であり、ファイルの mtime を基にロックの有効期限（`stale_age`）を判定する。NFS や CIFS などのネットワークファイルシステム、または仮想マシンのスナップショット復元時に、ファイルシステムのクロックとローカルシステムクロックの間にずれが生じることがある。この場合、ロックの有効期限判定が正しく機能しなくなるため、ユーザーにファイルシステムの時刻スキューを警告し、ロック処理の信頼性が低下している可能性を伝える目的で本通知が発行される。

**通知の送信タイミング**：`stale_pidfile` 関数内で、pidfile の age（`time() - mtime`）が `-stale_age` より小さい場合（つまりファイルの mtime が現在時刻より `stale_age` 以上未来になっている場合）に送信される。

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

**通知内容の概要**：ファイルシステムの時刻スキューが検出されたこと、および対象のpidfileのパスを通知する。

**期待されるアクション**：ユーザーはファイルシステムとローカルシステムの時刻同期設定を確認する。NFS環境ではNTPの設定を見直す。

## 通知種別

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

## 送信仕様

### 基本情報

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

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

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

## 通知テンプレート

### メール通知の場合

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

### 本文テンプレート

```
┌ Warning: filesystem time skew detected
│   path = "<pidfileのパス>"
└ @ FileWatching.Pidfile stdlib/FileWatching/src/pidfile.jl:198
```

### 添付ファイル

該当なし

## テンプレート変数

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

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| API呼び出し | `open_exclusive()` 内での `stale_pidfile()` 呼び出し | `age < -stale_age`（ファイルの mtime がシステム時刻より stale_age 以上未来） | ファイルシステム時刻が未来方向にずれている場合 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| `stale_age == 0` | stale_age が無効化されている場合は `stale_pidfile` 自体が呼ばれない |
| 正常な時刻 | age >= -stale_age の場合は警告なし |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A["mkpidlock() / open_exclusive() 呼び出し"] --> B["tryopen_exclusive() 試行"]
    B -->|"失敗（ファイル存在）"| C{"stale_age > 0?"}
    C -->|No| D["待機またはエラー"]
    C -->|Yes| E["stale_pidfile() 呼び出し"]
    E --> F["parse_pidfile() でpid/hostname/age取得"]
    F --> G{"age < -stale_age?"}
    G -->|Yes| H["@warn 'filesystem time skew detected' 出力"]
    G -->|No| I["stale判定を続行"]
    H --> I
    I --> J{"ファイルは stale?"}
    J -->|Yes| K["pidfile 削除試行"]
    J -->|No| L["待機"]
```

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

### 参照テーブル一覧

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

### 更新テーブル一覧

該当なし

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| 時刻スキュー | ファイルシステムとシステムクロックのずれ | 警告を出力し処理を続行 |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | なし（本通知自体のリトライ） |
| リトライ間隔 | なし |
| リトライ対象エラー | なし |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 上限 | 特に制限なし（stale_pidfile が呼ばれるたびに条件判定） |

### 配信時間帯

制限なし

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

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

## 備考

- `stale_age` のデフォルト値は 0（無効化）であり、推奨値は通常の処理完了時間の3〜5倍
- `refresh` パラメータにより、ロック保持中に mtime を定期的に更新することで stale 判定を防ぐ
- `age` は `time() - mtime(io)` で計算される。`age < -stale_age` は mtime が未来方向に `stale_age` 以上ずれていることを意味する

---

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

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

### 推奨読解順序

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

pidfile ロック機構の基本構造を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | pidfile.jl | `stdlib/FileWatching/src/pidfile.jl` | `LockMonitor` 構造体（57-86行目） |
| 1-2 | pidfile.jl | `stdlib/FileWatching/src/pidfile.jl` | `parse_pidfile` 関数（149-171行目）、pidfile フォーマット（pid + hostname） |

**読解のコツ**: `LockMonitor` は `mutable struct` で finalizer が登録されている。`update` フィールドの Timer が mtime を定期更新する。

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

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

**主要処理フロー**:
1. **197行目**: `parse_pidfile(path)` で pid, hostname, age を取得
2. **198行目**: `age < -stale_age` なら `@warn "filesystem time skew detected"` を発行（本通知）
3. **199行目**: `refresh == 0` の場合は `longer_factor = 25`、それ以外は `5`
4. **200-201行目**: `age > stale_age` かつ `age > stale_age * longer_factor` または PID が無効なら `true` を返す

#### Step 3: open_exclusive からの呼び出しフロー

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | pidfile.jl | `stdlib/FileWatching/src/pidfile.jl` | `open_exclusive` 関数内の `stale_pidfile` 呼び出し（246-248行目、297-301行目） |

**主要処理フロー**:
- **246-248行目**: `wait=false` モードでの stale 判定と削除試行
- **297-301行目**: `wait=true` モードでの stale 判定と削除試行

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

```
mkpidlock(at, pid; stale_age, refresh, ...)   [pidfile.jl:62]
    │
    └─ open_exclusive(at; stale_age, refresh, ...) [pidfile.jl:236]
           │
           ├─ tryopen_exclusive(path, mode)         [pidfile.jl:214]
           │
           └─ stale_pidfile(path, stale_age, refresh) [pidfile.jl:196]
                  │
                  ├─ parse_pidfile(path)              [pidfile.jl:159]
                  │      └─ open → read → parse pid/hostname
                  │
                  ├─ age < -stale_age → @warn        [pidfile.jl:198: 本通知]
                  │
                  └─ isvalidpid(hostname, pid)        [pidfile.jl:178]
```

### データフロー図

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

pidfile ─────────────▶ parse_pidfile()
    (pid, hostname)        │
                           ▼
    mtime(io) ─────▶ age = time() - mtime
                           │
                           ▼
stale_age ─────────▶ age < -stale_age チェック
                           │
                    Yes → @warn ─────────────────▶ ログ出力 (stderr)
```

### 関連ファイル一覧

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