DeepSeek Harness: プラグインライフサイクル:Fiber 状態機械
最終更新:2026-08-31
Cordis の各プラグインは静的なコードブロックではなく、ライフサイクルを持つ生きた実体——Fiber です。Fiber 状態機械を理解することは、「ロード待ち」から「実行中」「グレースフル終了」までの全過程を理解することであり、堅牢なプラグインを書くための鍵です。
📋 前提知識:13-effect.md の完了、自動クリーンアップと ctx.effect() を理解していること
1. 学習内容
- Fiber 概念:プラグインの状態コンテナ
- ライフサイクル:pending → active → disposing → disposed
- Fiber の作成と破棄
- 親子 Fiber 関係
- エラー処理と状態ロールバック
- Fiber コンテキスト分離
2. Fiber 概念
(1) Fiber とは
Fiber は Cordis のプラグインランタイム状態の抽象化——各プラグインインスタンスは1つの Fiber オブジェクトに対応し、プラグインが現在どのライフサイクル段階にあるかを記録します。
interface Fiber {
id: string
name: string
state: FiberState
context: Context
parent: Fiber | null
children: Fiber[]
}
(2) なぜ Fiber が必要か
Fiber がなければ、プラグインの状態は「ロード済み」と「未ロード」の2つしかありません。しかし実際には:
- 依存関係が未準備の場合、プラグインは「失敗」ではなく「待機」すべき
- アンロード中、プラグインは単に消えるのではなく「クリーンアップ中」であるべき
- エラー発生時、プラグインは「死亡」ではなく「リトライ」が必要な場合がある
Fiber はこれらの中間状態のための明確な状態機械モデルを提供します。
▶ サンプル 3:
Fiber = プラグインの「魂」(状態情報)
Context = プラグインの「体」(リソースと環境)
各 Fiber は Context を保持し、Context のライフサイクルは Fiber にバインドされます。
3. ライフサイクル状態
▶ サンプル 1:
stateDiagram-v2
[*] --> pending: プラグイン登録
pending --> active: 依存関係準備完了 + apply 成功
pending --> errored: apply 失敗
active --> disposing: アンロード要求
errored --> active: リトライ成功
errored --> disposing: リトライ放棄
disposing --> disposed: クリーンアップ完了
(2) 状態の意味
| 状態 | 意味 | ctx 利用可能 | 可逆 |
|---|---|---|---|
| pending | 依存関係を待機中 | 制限あり | ✅ |
| active | 正常実行中 | 完全 | ✅ |
| disposing | クリーンアップ中 | 読み取り専用 | ❌ |
| disposed | 破棄済み | 利用不可 | ❌ |
| errored | 起動失敗 | 利用不可 | ✅ |
(3) pending 状態
プラグインが inject を宣言しているが依存関係が未登録の場合、Fiber は pending に入ります:
export const inject = ['tools']
// tools サービス未登録 → Fiber は pending
// tools 登録後 → Fiber は active に遷移し、apply を呼び出し
pending 中:
- プラグインの apply はまだ呼び出されていない
- ctx 上の依存サービスは利用不可
- 依存関係が準備でき次第、自動的に active に遷移
(4) active 状態
apply が正常に実行された後、Fiber は active に入ります:
export function apply(ctx: Context) {
// Fiber は現在 active
ctx.logger.info('I am alive!')
ctx.on('session/created', (s) => {
// すべてのサービスを正常に使用可能
})
}
active 中:
- 宣言されたすべてのサービスが利用可能
- 新しいリソースやリスナーを登録可能
- イベントやコマンドに応答可能
(5) disposing 状態
アンロード要求を受信後、Fiber は disposing に入り、リソースのクリーンアップを開始:
disposing プロセス:
1. 新しいリクエストの受け付けを停止
2. ctx.effect() クリーンアップ関数を逆順で実行
3. イベントリスナーを自動削除
4. タイマーをクリア
5. コマンドとサービスの登録を解除
(6) disposed 状態
すべてのリソースがクリーンアップされた後、Fiber は disposed に入ります:
- ctx は利用不可
- ctx への任意の呼び出しはエラーをスロー
- Fiber オブジェクトは監査とログのために保持
4. Fiber の作成と破棄
(1) 作成タイミング
Fiber はプラグインの登録時に自動的に作成されます:
// フレームワーク内部ロジック(疑似コード)
function registerPlugin(plugin: PluginDefinition) {
const fiber = new Fiber({
id: generateId(),
name: plugin.name,
state: hasDeps(plugin) ? 'pending' : 'active'
})
if (fiber.state === 'active') {
fiber.context = createContext(fiber)
plugin.apply(fiber.context)
}
}
(2) 破棄のトリガー
Fiber の破棄は以下のトリガーで発生:
| トリガー | 説明 |
|---|---|
ctx.dispose() |
アクティブアンロード |
| 親 Fiber の破棄 | カスケードアンロード |
| 設定変更による再ロード | 旧 Fiber 破棄、新 Fiber 作成 |
▶ サンプル 3:
graph TD
TRIGGER[アンロードトリガー] --> STOP[新しいリクエストを停止]
STOP --> CHILD[子 Fiber を破棄]
CHILD --> CLEAN[クリーンアップ関数を実行]
CLEAN --> DISPOSED[disposed とマーク]
5. 親子 Fiber 関係
(1) 階層構造
Fiber は親子関係をサポートし、ツリー構造を形成します:
graph TD
ROOT[Root Fiber<br/>dsh-core] --> A[Plugin A Fiber]
ROOT --> B[Plugin B Fiber]
A --> A1[Sub-plugin A1]
A --> A2[Sub-plugin A2]
(2) 親子関係の確立
ctx.plugin() で子 Fiber を作成:
export function apply(ctx: Context) {
ctx.plugin({
name: 'sub-plugin',
apply(subCtx: Context) {
subCtx.logger.info('I am a child fiber')
}
})
}
(3) カスケード破棄
親 Fiber が破棄されると、すべての子 Fiber が自動的に破棄されます:
Plugin A をアンロード:
→ 先に Sub-plugin A1 を破棄
→ 次に Sub-plugin A2 を破棄
→ 最後に Plugin A を破棄
この「子が先、親が後」の破棄順序により、依存関係が壊れません。
(4) スコープの継承
子 Fiber は親 Fiber の ctx を継承します:
// 親プラグインが登録したサービスは子プラグインからアクセス可能
export function apply(ctx: Context) {
ctx.provide('parent-service', { ... })
ctx.plugin({
name: 'child',
inject: ['parent-service'],
apply(childCtx) {
childCtx['parent-service'] // ✅ 親サービスにアクセス可能
}
})
}
6. エラー処理と状態ロールバック
(1) apply の失敗
apply が例外をスローすると、Fiber は errored 状態に入ります:
export function apply(ctx: Context) {
throw new Error('initialization failed')
// Fiber → errored
}
(2) 自動リトライ
Cordis は errored Fiber を自動的にリトライします:
1回目:apply() → Error スロー → errored
2回目:(1秒待機)apply() → Error スロー → errored
3回目:(2秒待機)apply() → Error スロー → errored
4回目:(4秒待機)apply() → 成功 → active
リトライ間隔は指数バックオフ:1s → 2s → 4s → 8s → ... → 最大 60s。
(3) リトライ戦略の設定
export const Config = Schema.object({
maxRetries: Schema.number().default(5).description('最大リトライ回数'),
retryInterval: Schema.number().default(1000).description('初期リトライ間隔(ms)')
})
(4) 手動リトライ
ctx.on('fiber/errored', (fiber) => {
ctx.logger.warn(`plugin ${fiber.name} errored, retrying...`)
fiber.retry()
})
(5) リトライ不可エラー
一部のエラーはリトライすべきではありません:
export function apply(ctx: Context) {
if (!process.env.REQUIRED_VAR) {
// 設定エラー、リトライは無意味
throw new NonRetryableError('REQUIRED_VAR is not set')
}
}
(6) クリーンアップフェーズのエラー
disposing フェーズ中のエラーは Fiber の disposed への遷移を妨げませんが、ログに記録されます:
[warn] cleanup error in plugin my-plugin:Connection already closed
[info] plugin my-plugin disposed (with 1 cleanup warnings)
7. Fiber コンテキスト分離
(1) 各 Fiber が独立した ctx を持つ
const fiberA = new Fiber({ name: 'plugin-a' })
const fiberB = new Fiber({ name: 'plugin-b' })
fiberA.context !== fiberB.context // true
(2) 分離の境界
| リソース | 分離 | 説明 |
|---|---|---|
| イベントリスナー | ✅ | 各 Fiber が独立して登録 |
| タイマー | ✅ | 各 Fiber が独立してクリーンアップ |
| コマンド | ⚠️ | グローバル共有、ただしスコープあり |
| サービス | ❌ | グローバル共有 |
| 設定 | ✅ | 各プラグインが独立 |
(3) サービス共有と分離のバランス
サービスはグローバル共有——これは Cordis の設計決定です。プラグイン A が登録したサービス A は、プラグイン B が inject で使用できます。分離が必要な場合はスコープ機構を使用(20-scope.md を参照)。
(4) 状態の照会
// Fiber 状態の照会
ctx.fiber.state // 'active'
ctx.fiber.id // 'fiber-abc-123'
ctx.fiber.parent // 親 Fiber または null
ctx.fiber.children // 子 Fiber[]
❓ よくある質問
ctx.plugin() でサブプラグインを登録する際、フレームワークが自動的に Fiber を作成します。typescript ctx.on('fiber/created', (fiber) => { ... }) ctx.on('fiber/active', (fiber) => { ... }) ctx.on('fiber/disposing', (fiber) => { ... }) ctx.on('fiber/disposed', (fiber) => { ... }) ctx.on('fiber/errored', (fiber) => { ... }) 📖 まとめ
- Fiber は Cordis のプラグインランタイム状態の抽象化;各プラグインインスタンスは1つの Fiber に対応
- ライフサイクル:pending(依存待ち)→ active(実行中)→ disposing(クリーンアップ中)→ disposed(破棄)
- 親子 Fiber は「子が先、親が後」の順序でカスケード破棄をサポート
- apply 失敗時、Fiber は errored に入り、指数バックオフで自動リトライ
- 各 Fiber は独立した Context を持つ;イベントリスナーとタイマーは分離、サービスはグローバル共有
- Fiber ライフサイクルイベントでプラグイン状態を監視
📝 練習問題
1. ⭐ 基礎:apply 内で現在の Fiber の状態と id を出力するプラグインを書いてください。起動し、ログで Fiber が active 状態であることを確認すること。
2. ⭐⭐ 応用:apply 内で意図的にエラーをスローするプラグインを書いてください(初期化失敗をシミュレート)。Fiber の errored 状態とリトライ動作を観察。その後エラーを修正し、Fiber が active に復旧することを確認。
3. ⭐⭐⭐ チャレンジ:親子 Fiber 構造を作成してください:親プラグインがサービスを登録、子プラグインが inject でそれを使用。親プラグインをアンロードし、子もカスケード的に破棄されることを確認。クリーンアップ関数で破棄順序を出力し、「子が先、親が後」であることを確認すること。