DeepSeek Harness: プラグインライフサイクル:Fiber 状態機械

最終更新:2026-08-31

Cordis の各プラグインは静的なコードブロックではなく、ライフサイクルを持つ生きた実体——Fiber です。Fiber 状態機械を理解することは、「ロード待ち」から「実行中」「グレースフル終了」までの全過程を理解することであり、堅牢なプラグインを書くための鍵です。

💡 ヒント:Fiber の核心的価値は「観察可能なライフサイクル」——すべての状態遷移には明確なトリガ条件と観察可能な副作用があります。プラグインが今どの状態にあり、なぜそうなのかを常に把握できます。

📋 前提知識13-effect.md の完了、自動クリーンアップと ctx.effect() を理解していること

1. 学習内容

Fiber 状態機械


2. Fiber 概念

(1) Fiber とは

Fiber は Cordis のプラグインランタイム状態の抽象化——各プラグインインスタンスは1つの Fiber オブジェクトに対応し、プラグインが現在どのライフサイクル段階にあるかを記録します。

TYPESCRIPT
interface Fiber {
  id: string
  name: string
  state: FiberState
  context: Context
  parent: Fiber | null
  children: Fiber[]
}

(2) なぜ Fiber が必要か

Fiber がなければ、プラグインの状態は「ロード済み」と「未ロード」の2つしかありません。しかし実際には:

Fiber はこれらの中間状態のための明確な状態機械モデルを提供します。

▶ サンプル 3:

TEXT 📖 参照専用
Fiber = プラグインの「魂」(状態情報)
Context = プラグインの「体」(リソースと環境)

各 Fiber は Context を保持し、Context のライフサイクルは Fiber にバインドされます。


3. ライフサイクル状態

▶ サンプル 1:

100%
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 に入ります:

TYPESCRIPT
export const inject = ['tools']

// tools サービス未登録 → Fiber は pending
// tools 登録後 → Fiber は active に遷移し、apply を呼び出し

pending 中:

(4) active 状態

apply が正常に実行された後、Fiber は active に入ります:

TYPESCRIPT
export function apply(ctx: Context) {
  // Fiber は現在 active
  ctx.logger.info('I am alive!')
  
  ctx.on('session/created', (s) => {
    // すべてのサービスを正常に使用可能
  })
}

active 中:

(5) disposing 状態

アンロード要求を受信後、Fiber は disposing に入り、リソースのクリーンアップを開始:

TEXT 📖 参照専用
disposing プロセス:
1. 新しいリクエストの受け付けを停止
2. ctx.effect() クリーンアップ関数を逆順で実行
3. イベントリスナーを自動削除
4. タイマーをクリア
5. コマンドとサービスの登録を解除

(6) disposed 状態

すべてのリソースがクリーンアップされた後、Fiber は disposed に入ります:


4. Fiber の作成と破棄

(1) 作成タイミング

Fiber はプラグインの登録時に自動的に作成されます:

TYPESCRIPT
// フレームワーク内部ロジック(疑似コード)
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:

100%
graph TD
    TRIGGER[アンロードトリガー] --> STOP[新しいリクエストを停止]
    STOP --> CHILD[子 Fiber を破棄]
    CHILD --> CLEAN[クリーンアップ関数を実行]
    CLEAN --> DISPOSED[disposed とマーク]

5. 親子 Fiber 関係

(1) 階層構造

Fiber は親子関係をサポートし、ツリー構造を形成します:

100%
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 を作成:

TYPESCRIPT
export function apply(ctx: Context) {
  ctx.plugin({
    name: 'sub-plugin',
    apply(subCtx: Context) {
      subCtx.logger.info('I am a child fiber')
    }
  })
}

(3) カスケード破棄

親 Fiber が破棄されると、すべての子 Fiber が自動的に破棄されます:

TEXT 📖 参照専用
Plugin A をアンロード:
  → 先に Sub-plugin A1 を破棄
  → 次に Sub-plugin A2 を破棄
  → 最後に Plugin A を破棄

この「子が先、親が後」の破棄順序により、依存関係が壊れません。

(4) スコープの継承

子 Fiber は親 Fiber の ctx を継承します:

TYPESCRIPT
// 親プラグインが登録したサービスは子プラグインからアクセス可能
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 状態に入ります:

TYPESCRIPT
export function apply(ctx: Context) {
  throw new Error('initialization failed')
  // Fiber → errored
}

(2) 自動リトライ

Cordis は errored Fiber を自動的にリトライします:

TEXT 📖 参照専用
1回目:apply() → Error スロー → errored
2回目:(1秒待機)apply() → Error スロー → errored
3回目:(2秒待機)apply() → Error スロー → errored
4回目:(4秒待機)apply() → 成功 → active

リトライ間隔は指数バックオフ:1s → 2s → 4s → 8s → ... → 最大 60s。

(3) リトライ戦略の設定

TYPESCRIPT
export const Config = Schema.object({
  maxRetries: Schema.number().default(5).description('最大リトライ回数'),
  retryInterval: Schema.number().default(1000).description('初期リトライ間隔(ms)')
})

(4) 手動リトライ

TYPESCRIPT
ctx.on('fiber/errored', (fiber) => {
  ctx.logger.warn(`plugin ${fiber.name} errored, retrying...`)
  fiber.retry()
})

(5) リトライ不可エラー

一部のエラーはリトライすべきではありません:

TYPESCRIPT
export function apply(ctx: Context) {
  if (!process.env.REQUIRED_VAR) {
    // 設定エラー、リトライは無意味
    throw new NonRetryableError('REQUIRED_VAR is not set')
  }
}

(6) クリーンアップフェーズのエラー

disposing フェーズ中のエラーは Fiber の disposed への遷移を妨げませんが、ログに記録されます:

TEXT 📖 参照専用
[warn] cleanup error in plugin my-plugin:Connection already closed
[info] plugin my-plugin disposed (with 1 cleanup warnings)

7. Fiber コンテキスト分離

(1) 各 Fiber が独立した ctx を持つ

TYPESCRIPT
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) 状態の照会

TYPESCRIPT
// Fiber 状態の照会
ctx.fiber.state           // 'active'
ctx.fiber.id              // 'fiber-abc-123'
ctx.fiber.parent          // 親 Fiber または null
ctx.fiber.children        // 子 Fiber[]

❓ よくある質問

Q Fiber と Thread の関係は?
A 関係ありません。Fiber は OS スレッドではなく、Cordis の論理状態単位です。すべての Fiber は同じ Node.js イベントループ内で実行されます。
Q 手動で Fiber を作成できますか?
A 非推奨。ctx.plugin() でサブプラグインを登録する際、フレームワークが自動的に Fiber を作成します。
Q errored プラグインはリソースを消費しますか?
A いいえ。errored Fiber は ctx リソースを保持しません;状態追跡用の Fiber オブジェクト自体(最小メモリ)のみ保持されます。
Q 子 Fiber は親の破棄後も存続できますか?
A いいえ。親子関係は強い依存——親の破棄は常に子も破棄します。独立したライフサイクルが必要な場合は、親子関係を確立しないでください。
Q すべての Fiber 状態を監視するには?
A 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) => { ... })
Q 設定変更による Fiber 再構築時、旧クリーンアップ関数は実行されますか?
A はい。設定変更時、旧 Fiber は disposing → disposed の完全なフローを経て、すべてのクリーンアップ関数が実行されます。その後、新 Fiber が作成され pending/active に入ります。

📖 まとめ


📝 練習問題

1. ⭐ 基礎:apply 内で現在の Fiber の状態と id を出力するプラグインを書いてください。起動し、ログで Fiber が active 状態であることを確認すること。

2. ⭐⭐ 応用:apply 内で意図的にエラーをスローするプラグインを書いてください(初期化失敗をシミュレート)。Fiber の errored 状態とリトライ動作を観察。その後エラーを修正し、Fiber が active に復旧することを確認。

3. ⭐⭐⭐ チャレンジ:親子 Fiber 構造を作成してください:親プラグインがサービスを登録、子プラグインが inject でそれを使用。親プラグインをアンロードし、子もカスケード的に破棄されることを確認。クリーンアップ関数で破棄順序を出力し、「子が先、親が後」であることを確認すること。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%