# Runtime の再利用と Daemon

> 単一 CLI コマンドの Runtime ライフサイクルから、daemon が Worker Runtime pool を管理して読み込み済み Unit を呼び出し間で再利用する仕組みを学びます。

- Human documentation: [https://docs.univer.ai/ja-JP/ai/runtime-architecture](https://docs.univer.ai/ja-JP/ai/runtime-architecture)

- Agent Markdown: [https://docs.univer.ai/ja-JP/ai/runtime-architecture.md](https://docs.univer.ai/ja-JP/ai/runtime-architecture.md)

- Requested language: `ja-JP`

- Content language: `ja-JP`

- Documentation version: `1.1.0`

- Source: [runtime-architecture.ja-JP.mdx](https://github.com/dream-num/documentation/blob/dev/content/ai/runtime-architecture.ja-JP.mdx)

---

CLI で重いのは通常、引数解析ではなく Unit[^document-unit] を読み書きできるライブ Univer にする処理です。Univer 作成、プラグイン導入、共同編集バックエンドの開始、チェックポイント読み込み、変更セット適用、数式計算の完了待ちが必要です。

本章は 2 つの Runtime[^live-runtime] 機能を扱います。

* `@univer-cli/univer-collaboration-runtime`：現在のプロセスで Unit を読み込み操作します。
* `@univer-cli/univer-collaboration-runtime-pool`：Runtime を Worker に配置し、Unit の識別情報で貸与・再利用・破棄します。

Worker Runtime を複数の CLI 呼び出し間で維持するには、長寿命の所有者が必要です。ここではローカル daemon が担当します。

## 3 つのライフサイクル

再利用を考える前に、次のオブジェクトの寿命を区別します。

| オブジェクト         | 作成時点              | 終了時点                                                  | 所有するもの                                     |
| -------------- | ----------------- | ----------------------------------------------------- | ------------------------------------------ |
| CLI プロセス       | コマンド実行ごと          | 結果出力後                                                 | 引数、1 回のリクエスト、出力形式                          |
| Daemon プロセス    | 初回リクエストまたは直接起動    | 停止、更新による再起動、クラッシュ                                     | Runtime pool、業務ハンドラー、ローカル socket           |
| Worker Runtime | pool が初めて key に遭遇 | TTL/LRU[^runtime-cache] 削除、無効化、daemon 終了、Worker クラッシュ | 1 Worker、1 バックエンドハンドル、1 読み込み済み Unit、共同編集状態 |

**CLI は 1 回呼び出し、daemon が pool を所有し、pool が Worker を所有し、各 Worker が Runtime を所有します。**

```mermaid
flowchart LR
  CLI1[CLI 呼び出し 1<br/>短命] -->|JSON リクエスト| D[Daemon<br/>長寿命の所有者]
  CLI2[CLI 呼び出し 2<br/>短命] -->|JSON リクエスト| D
  CLIN[CLI 呼び出し N<br/>短命] -->|JSON リクエスト| D

  D --> P[Collaboration Runtime pool]
  P -->|key: Unit A| WA[Worker A]
  P -->|key: Unit B| WB[Worker B]
  WA --> RA[Runtime A<br/>Unit A 読み込み済み]
  WB --> RB[Runtime B<br/>Unit B 読み込み済み]
  RA --> BA[Backend A]
  RB --> BB[Backend B]
  BA --> DS[(共同編集サーバー<br/>または別データソース)]
  BB --> DS
```

daemon が転送するのはアプリケーション定義の JSON リクエストだけです。スナップショット、変更セット、ブロックは Worker 内のバックエンドが直接読み、CLI や daemon を通りません。

## 手順 1：コマンドごとに直接 Runtime を読み込む

最も単純な実装は Commander action 内で `@univer-cli/univer-collaboration-runtime` を作り、操作完了後に閉じます。

```ts
const runtime = await runtimeFactory.load(unitId, unitType);

try {
  const pulled = await runtime.pull();
  if (pulled.status === "conflict") throw new Error(pulled.conflict.message);

  const execution = await runtime.execute({ mode: "write", code });
  let commit = await runtime.commit();
  if (commit.status === "pull-required") {
    const repulled = await runtime.pull();
    if (repulled.status === "conflict") throw new Error(repulled.conflict.message);
    commit = await runtime.commit();
  }
  return { execution, commit };
} finally {
  await runtime.close();
}
```

`runtimeFactory` は `createUniverCollaborationRuntimeFactory()` で作成します。読み込みから終了まで Runtime は常に 1 Unit に結び付き、現在のリビジョン、保留 mutation、待機中の変更セット、競合状態を保持します。呼び出し側は最終コミット状態も確認します。完了を意味するのは `confirmed` と `nothing-to-commit` だけです。

`load()` は単なるファイル読み取りではなく、順に次を行います。

1. アプリケーション提供の共同編集バックエンドを開く；
2. Headless Univer を作り共同編集機能を導入；
3. UnitData または Snapshot チェックポイントを読み込む；
4. チェックポイントに含まれる変更セットを再生；
5. チェックポイントから最新リモートリビジョンまでの差分を取得・再生；
6. 導入済み数式モジュールの最新結果が適用されるまで待つ；
7. `execute()`、`pull()`、`commit()` が使える Runtime を返す。

単発呼び出し全体は次のとおりです。

```mermaid
sequenceDiagram
  autonumber
  actor User as ユーザー
  participant CLI as CLI プロセス
  participant RT as Collaboration Runtime
  participant U as Headless Univer
  participant B as Backend

  User->>CLI: コマンド実行
  CLI->>CLI: Node.js 起動、モジュール読み込み、引数解析
  CLI->>RT: factory.load(unitId, unitType)
  RT->>B: 開始してチェックポイント・変更を読む
  RT->>U: Univer 作成、プラグイン導入、Unit 読み込み・再生
  RT->>U: 数式の準備を待つ
  RT-->>CLI: Runtime 準備完了
  CLI->>RT: pull()
  CLI->>RT: execute()
  opt 書き込み操作
    CLI->>RT: commit()
    RT->>B: 変更セット送信
  end
  CLI->>RT: close()
  RT->>B: バックエンドハンドルを閉じる
  RT->>U: Univer を破棄
  CLI-->>User: 結果出力して終了
```

所有関係が明確で失敗後に状態を残さないため、低頻度コマンドや単発タスクに向きます。一方、次の CLI は新たな Node.js プロセスを起動して全読み込みを繰り返し、以前の Univer と Unit 状態を再利用できません。

## 手順 2：Worker pool に Runtime を置く

`@univer-cli/univer-collaboration-runtime-pool` は常駐 key ごとに別の Worker プロセスと Runtime を維持します。アプリケーションが Worker 入口を提供し、バックエンド、認証情報、Univer はすべて Worker 内で作成します。

```ts title="collaboration-worker.ts"
import { defineUniverCollaborationRuntimeWorker } from "@univer-cli/univer-collaboration-runtime-pool";

export default defineUniverCollaborationRuntimeWorker({
  async createRuntime(init: { unitId: string; unitType: number }) {
    return createRuntimeFromApplicationInit(init);
  },
});
```

Worker 入口はビルド済み JavaScript ESM ファイルを指す必要があります。`init` はコールド作成時のみ送られ、構造化複製に対応している必要があります。

Runtime 用の RPC 層を別途設計する必要はありません。lease が `getState()`、`execute()`、`fetch()`、`pull()`、`commit()`、`exportUnitData()` などの公開 API を代理します。

```ts
import { createUniverCollaborationRuntimePool } from "@univer-cli/univer-collaboration-runtime-pool";

const pool = createUniverCollaborationRuntimePool({
  entry: new URL("./collaboration-worker.js", import.meta.url),
});

const lease = await pool.acquire({
  key: targetKey,
  init: { unitId, unitType },
});

try {
  await lease.pull();
  const result = await lease.execute({ mode: "read", code });
  console.log(result.value);
} catch (error) {
  await lease.invalidate();
  throw error;
} finally {
  await lease.release();
}
```

`key` は再利用対象 Runtime の識別子で、単なる検索引数ではありません。同じ key は同じ Unit、共同編集状態の連鎖、互換性のある初期化意味論を表す必要があります。通常の Unit と Worktree 下書きは、`unitId` が同じでも別 key にします。

pool の重要な動作は 4 つです。

* 同じ key は同時に 1 lease のみ。並行要求は FIFO で待ち、同じライブ状態への同時変更を防ぎます。
* `release()` は信頼できる Runtime をアイドルキャッシュへ戻し、次の同じ key で再利用します。
* `invalidate()` は状態が信頼できないことを示します。Worker と Runtime を破棄し、次回はコールド読み込みします。
* アイドル Runtime は TTL/LRU で削除できます。タイムアウト、Worker クラッシュ、プロトコルエラーも現在のインスタンスを無効化します。

ただし pool 自体を短命な CLI 内に作ると、コマンド間では再利用できません。

```mermaid
flowchart LR
  subgraph C1[CLI 呼び出し 1]
    P1[Pool 1] --> W1[Worker A] --> R1[Runtime A]
  end

  X1[CLI 終了] -.->|pool が閉じ Worker も終了| C1

  subgraph C2[CLI 呼び出し 2]
    P2[新 Pool 2] --> W2[新 Worker A] --> R2[Runtime A 再読み込み]
  end

  X1 --> C2
```

Worker はプロセス隔離、排他的 lease、障害時終了を提供します。CLI 間で再利用するには、単一コマンドより長寿命の pool 所有者が必要です。

## 手順 3：Daemon を長寿命の所有者にする

daemon は必要時に起動するローカル Node.js プロセスです。CLI は Unix socket または Windows 名前付きパイプで呼びます。CLI 終了では daemon は終わらないため、pool、Worker、アイドル Runtime を維持できます。

daemon 入口で pool を一度作成し、各業務ハンドラーで lease を取得します。

```ts title="daemon-entry.ts"
import { createDaemonServer, DAEMON_SOCKET_ENV } from "@univer-cli/daemon";
import { createUniverCollaborationRuntimePool } from "@univer-cli/univer-collaboration-runtime-pool";

const socketPath = process.env[DAEMON_SOCKET_ENV];
if (!socketPath) throw new Error(`${DAEMON_SOCKET_ENV} is required`);

const pool = createUniverCollaborationRuntimePool({
  entry: new URL("./collaboration-worker.js", import.meta.url),
  cache: { idleTtlMs: 5 * 60_000, maxEntries: 20 },
});

const server = createDaemonServer({
  identity: { id: "my-cli", version: "1.0.0" },
  socketPath,
  onShutdown: () => pool.close(),
});

server.handle("unit.execute", async (payload) => {
  const input = parseExecuteRequest(payload);
  const lease = await pool.acquire({
    key: input.targetKey,
    init: { unitId: input.unitId, unitType: input.unitType },
  });

  try {
    const pulled = await lease.pull();
    if (pulled.status === "conflict") throw new Error(pulled.conflict.message);

    const execution =
      input.mode === "read"
        ? await lease.execute({ mode: "read", code: input.code })
        : await lease.execute({ mode: "write", code: input.code });
    const commit = input.mode === "write" ? await commitWithPull(lease) : null;
    return { commitStatus: commit?.status ?? null, value: execution.value };
  } catch (error) {
    await lease.invalidate();
    throw error;
  } finally {
    await lease.release();
  }
});

await server.listen();
```

`parseExecuteRequest()` と `targetKey` 規則はアプリケーションが定義します。daemon はローカル転送とライフサイクル管理だけを提供し、Unit、Runtime、共同編集プロトコルを理解しません。daemon 終了時は `pool.close()` を呼び、アイドル・実行中 Worker の最終所有者を明確にします。例の `commitWithPull()` は `pull-required` 後の pull/commit と最終状態検査を行います。保守的に、未処理エラーはすべて現在の lease を無効化します。

通常の CLI は 1 つの業務リクエストを送るだけになります。

```ts
import { createDaemonClient } from "@univer-cli/daemon";

const client = createDaemonClient({
  entry: new URL("./daemon-entry.js", import.meta.url),
  identity: { id: "my-cli", version: "1.0.0" },
  socketPath,
});

const result = await client.request("unit.execute", {
  targetKey,
  unitId,
  unitType,
  mode,
  code,
});
```

socket がなければ、クライアントは設定済み daemon 入口を起動し、ID とプロトコルのハンドシェイクを待ってから送信します。互換 daemon が既にあれば再利用します。

## 手順 4：初回と後続リクエストの比較

初回 key アクセスはコールド経路です。daemon 起動が必要な場合があり、pool は Worker と Runtime を作ります。

```mermaid
sequenceDiagram
  autonumber
  participant CLI as 最初の CLI 呼び出し
  participant D as Daemon
  participant P as Runtime pool
  participant W as Worker A
  participant R as Runtime A
  participant B as Backend

  CLI->>D: unit.execute(target A)
  Note over CLI,D: socket がなければ daemon を起動
  D->>P: acquire(key A, init)
  P->>W: Worker 作成と入口読み込み
  W->>R: createRuntime(init)
  R->>B: バックエンド開始、チェックポイント・変更読み取り
  R->>R: Univer 作成、Unit 読み込み、数式待ち
  R-->>P: Runtime 準備完了
  P-->>D: Lease A
  D->>R: pull → execute → commit
  D->>P: release()
  Note over P,R: Worker A と Runtime A はアイドルキャッシュに残る
  D-->>CLI: JSON 結果
  Note over CLI: CLI 終了, daemon・Worker は存続
```

同じ key の後続アクセスはホット経路です。CLI プロセスは再起動しますが、重い Runtime 読み込みは繰り返しません。

```mermaid
sequenceDiagram
  autonumber
  participant CLI as 後続 CLI 呼び出し
  participant D as 実行中の Daemon
  participant P as Runtime pool
  participant R as 読み込み済み Runtime A

  CLI->>D: unit.execute(target A)
  D->>P: acquire(key A, init)
  P-->>D: キャッシュ命中、Lease A を返す
  D->>R: pull()
  Note over R: アイドル中のリモート変更を同期
  D->>R: execute()
  opt 書き込み操作
    D->>R: commit()
  end
  D->>P: release()
  D-->>CLI: JSON 結果
  Note over CLI,R: CLI 終了, Runtime A は存続
```

再利用するのは作成・読み込み済みのライブ Runtime です。共同編集の整合性処理は省略されません。通常は lease 取得後も `pull()`、`commit()` 後の最終状態確認、`pull-required`、`retry`、`unknown`、`conflict` の処理が必要です。

## 減るコストと残るコスト

| コスト                       | 毎回直接読み込み  | Daemon + Worker pool |
| ------------------------- | --------- | -------------------- |
| CLI 起動、引数解析、出力            | 毎回        | 毎回                   |
| ローカル socket JSON 往復       | なし        | 毎回                   |
| Daemon 起動・モジュール読み込み       | なし        | 初回または再起動後            |
| Worker 起動・入口読み込み          | Worker なし | key ごとのコールド作成        |
| Univer 作成・プラグイン導入         | 毎回        | key ごとのコールド作成        |
| チェックポイント・初期変更再生・数式待ち      | 毎回        | key ごとのコールド作成        |
| アイドル中の変更を `pull()`        | 毎回        | 通常は毎回必要              |
| `execute()` と `commit()`  | 毎回        | 毎回                   |
| アイドルメモリ、バックエンドハンドル、Worker | CLI 終了で解放 | 削除・無効化・daemon 終了まで保持 |

再利用は常駐メモリと Worker 数を使い、後続呼び出しの起動遅延を減らします。すべての Unit を無期限に保持せず、pool イベントと実測遅延で `idleTtlMs`、`maxEntries`、開始・操作タイムアウトを選びます。

## 並行処理、障害、終了

動作は 4 つの規則にまとめられます。

1. **同じ key の並行要求は待機します。** 1 Runtime は同時に 1 lease だけに属し、交錯した呼び出しによる状態変更を防ぎます。
2. **異なる key は別 Worker で並行実行できます。** 常駐 key ごとに別 Worker と Runtime を割り当てます。
3. **信頼できる状態だけ再利用します。** 操作成功後、ローカル mutation を解消して `release()`。タイムアウト、クラッシュ、プロトコルエラー、状態不明時は `invalidate()` を呼びます。
4. **終了責任は下へ伝わります。** CLI が 1 リクエストを終え、daemon 終了が `pool.close()` を呼び、pool が Worker を終了して Runtime とバックエンドを閉じます。

単発遅延を許容できる低頻度コマンドには `@univer-cli/univer-collaboration-runtime` の直接利用が最も簡単です。反復読み込みが主なコストと実測で分かり、常駐プロセスとメモリを許容できる場合だけ daemon と `@univer-cli/univer-collaboration-runtime-pool` を組み合わせます。

全パッケージは[パッケージ一覧](https://docs.univer.ai/ja-JP/ai/packages.md)を参照してください。

[^document-unit]: Unit は、ワークブックや文書などの文書インスタンスです。ワークシートはワークブックの一部であり、独立した Unit ではありません。

[^live-runtime]: ここでの Runtime は、文書を読み込んだ Univer インスタンスと Unit の操作に必要なリソースです。Node.js の実行ファイルや保存済みのスナップショットではありません。

[^runtime-cache]: TTL は設定した寿命に達したアイドル Runtime を、LRU は容量制限時に最も長く未使用の Runtime を破棄します。解放するのは実行リソースで、永続化された Unit は削除しません。
