# 通知設計書 36-SparkListenerNodeUnblacklisted

## 概要

本ドキュメントは、Apache SparkにおけるSparkListenerNodeUnblacklistedイベント通知の設計について記述する。本イベントはノード（ホスト）がブラックリスト（除外リスト）から解除された際に発火する非推奨イベントであり、Spark 3.1.0以降はSparkListenerNodeUnexcludedに置き換えられている。

### 本通知の処理概要

SparkListenerNodeUnblacklistedは、以前除外されたノードが一定時間経過後に再有効化された際に発火するイベント通知である。

**業務上の目的・背景**：除外されたノードを永久に除外し続けると、クラスタのキャパシティが低下する。タイムアウト期間（デフォルト1時間）経過後にノードを再有効化し、再びExecutorの配置対象とする。一時的な障害が解消された可能性があるため、リソースの有効活用を図る。本通知はその再有効化をリスナーに通知する。

**通知の送信タイミング**：HealthTracker.applyExcludeOnFailureTimeoutメソッド内で、除外されたノードのexpiryTimeが現在時刻を過ぎた場合に発火する。

**通知の受信者**：LiveListenerBusに登録された全てのSparkListenerInterface実装。

**通知内容の概要**：再有効化されたノードのホストIDおよび再有効化時刻が含まれる。

**期待されるアクション**：Spark UIでノードの除外状態が解除されたことが反映される。運用者はノードが再びアクティブになったことを確認し、引き続きモニタリングを行う。

## 通知種別

アプリ内通知（Sparkイベントバスによるインプロセス非同期イベント配信）

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 非同期（LiveListenerBus経由） |
| 優先度 | 中 |
| リトライ | なし |

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

LiveListenerBusに登録された全てのSparkListenerInterface実装に対してブロードキャスト配信される。SparkListenerBus.doPostEventのパターンマッチにより`onNodeUnblacklisted`メソッドが呼び出される。

## 通知テンプレート

### イベントオブジェクト

| 項目 | 内容 |
|-----|------|
| イベントクラス | `SparkListenerNodeUnblacklisted` |
| パッケージ | `org.apache.spark.scheduler` |
| 親クラス | `SparkListenerEvent` |
| シリアライズ形式 | JSON（JsonProtocol経由） |

### イベントデータ構造

```scala
@deprecated("use SparkListenerNodeUnexcluded instead", "3.1.0")
case class SparkListenerNodeUnblacklisted(time: Long, hostId: String)
  extends SparkListenerEvent
```

### 添付ファイル

該当なし

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| time | 再有効化時刻（ミリ秒） | `clock.getTimeMillis()` (now) | Yes |
| hostId | 再有効化されたノードのホスト名 | nodeIdToExcludedExpiryTime.keys | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| 内部処理 | タイムアウト期限到達 | 除外ノードのexpiry時刻 < 現在時刻 | HealthTracker.applyExcludeOnFailureTimeout内で発火 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| タイムアウト未到達 | expiryTimeがまだ現在時刻を過ぎていない場合はイベントが発行されない |
| excludeOnFailure無効 | HealthTrackerが生成されない場合はイベント自体が発生しない |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[applyExcludeOnFailureTimeout呼び出し] --> B{現在時刻 > nextExpiryTime?}
    B -->|Yes| C[期限切れExecutor処理]
    B -->|No| Z[処理終了]
    C --> D[期限切れノードをフィルタ]
    D --> E{期限切れノードあり?}
    E -->|Yes| F[nodeIdToExcludedExpiryTimeから除去]
    E -->|No| Z
    F --> G[SparkListenerNodeUnblacklisted発行]
    F --> H[SparkListenerNodeUnexcluded発行]
    G --> I[LiveListenerBusでリスナーに配信]
    H --> I
    I --> J[_excludedNodeList更新]
    J --> Z
```

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

該当なし（インメモリのデータ構造を使用）

### インメモリデータ構造の更新

| データ構造 | 操作 | 説明 |
|-----------|------|------|
| nodeIdToExcludedExpiryTime | remove | 期限切れノードを除外リストから除去 |
| _excludedNodeList | set | 除外ノード集合を更新 |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| イベント配信失敗 | ListenerBusキュー溢れ | LiveListenerBusのドロップポリシーに従う |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | 0 |
| リトライ間隔 | N/A |
| リトライ対象エラー | N/A |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | 制限なし |
| 1日あたり上限 | 制限なし |

### 配信時間帯

制限なし

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

- 本イベントはクラスタ内部の情報のみを含み、個人情報は含まない

## 備考

- 本イベントは`@deprecated`であり、SparkListenerNodeUnexcludedが推奨される
- Executor解除とノード解除は独立してタイムアウト管理される
- applyExcludeOnFailureTimeout内では、まずExecutorの解除処理が行われ、その後ノードの解除処理が行われる

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | SparkListener.scala | `core/src/main/scala/org/apache/spark/scheduler/SparkListener.scala` | 211-213行目: SparkListenerNodeUnblacklistedのcase class定義。timeとhostIdのみ |

**読解のコツ**: 除外イベント（Blacklisted）はexecutorFailuresを持つが、解除イベント（Unblacklisted）はhostIdのみ。

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | HealthTracker.scala | `core/src/main/scala/org/apache/spark/scheduler/HealthTracker.scala` | 128-143行目: applyExcludeOnFailureTimeout内のノード解除処理部分 |

**主要処理フロー**:
1. **128行目**: `nodeIdToExcludedExpiryTime.filter(_._2 < now)`で期限切れノードを抽出
2. **133-134行目**: ノードを`nodeIdToExcludedExpiryTime`から除去
3. **136行目**: `listenerBus.post(SparkListenerNodeUnblacklisted(now, node))`でイベント発行
4. **137行目**: `listenerBus.post(SparkListenerNodeUnexcluded(now, node))`も同時発行
5. **139行目**: `_excludedNodeList.set(nodeIdToExcludedExpiryTime.keySet.toSet)`で除外ノード集合を更新

#### Step 3: イベント配信メカニズムを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | SparkListenerBus.scala | `core/src/main/scala/org/apache/spark/scheduler/SparkListenerBus.scala` | 76-77行目: doPostEvent内でonNodeUnblacklistedにルーティング |

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

```
TaskSchedulerImpl (定期的なスケジューリングサイクル)
    |
    +-- HealthTracker.applyExcludeOnFailureTimeout()
            |
            +-- (Executor解除処理)
            |
            +-- nodeIdToExcludedExpiryTime.filter(期限切れ)
            +-- nodeIdToExcludedExpiryTime.remove(node)
            +-- listenerBus.post(SparkListenerNodeUnblacklisted)
            +-- listenerBus.post(SparkListenerNodeUnexcluded)
            +-- _excludedNodeList.set(更新)
            +-- updateNextExpiryTime()
```

### データフロー図

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

現在時刻(now)             HealthTracker                    SparkListenerNodeUnblacklisted
除外ノードリスト     ---> .applyExcludeOnFailureTimeout()   イベント
(hostId,                  タイムアウト判定・除去               |
 expiryTime)                                                v
                                                     LiveListenerBus
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| SparkListener.scala | `core/src/main/scala/org/apache/spark/scheduler/SparkListener.scala` | ソース | イベントクラス定義 |
| SparkListenerBus.scala | `core/src/main/scala/org/apache/spark/scheduler/SparkListenerBus.scala` | ソース | イベントディスパッチ |
| HealthTracker.scala | `core/src/main/scala/org/apache/spark/scheduler/HealthTracker.scala` | ソース | タイムアウト処理・イベント発行元 |
| AppStatusListener.scala | `core/src/main/scala/org/apache/spark/status/AppStatusListener.scala` | ソース | UI向けイベント受信 |
| EventLoggingListener.scala | `core/src/main/scala/org/apache/spark/scheduler/EventLoggingListener.scala` | ソース | イベントログへの永続化 |
