Runtime の再利用と Daemon

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

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

本章は 2 つの Runtime2 機能を扱います。

  • @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 Runtimepool が初めて key に遭遇TTL/LRU3 削除、無効化、daemon 終了、Worker クラッシュ1 Worker、1 バックエンドハンドル、1 読み込み済み Unit、共同編集状態

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

100%

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

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

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

TypeScript
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 を返す。

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

100%

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

手順 2:Worker pool に Runtime を置く

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

TypeScript
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 を代理します。

TypeScript
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 内に作ると、コマンド間では再利用できません。

100%

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

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

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

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

TypeScript
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 つの業務リクエストを送るだけになります。

TypeScript
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 を作ります。

100%

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

100%

再利用するのは作成・読み込み済みのライブ 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()毎回毎回
アイドルメモリ、バックエンドハンドル、WorkerCLI 終了で解放削除・無効化・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 を組み合わせます。

全パッケージはパッケージ一覧を参照してください。

Footnotes

  1. Unit は、ワークブックや文書などの文書インスタンスです。ワークシートはワークブックの一部であり、独立した Unit ではありません。 ↩

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

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

このガイドはいかがですか?

© 2026 DreamNum Co., Ltd.