Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

fiber による耐久実行

最終更新 Markdown で表示Agent セットアップ

Durable Object のエビクションを乗り越える作業を実行します。runFiber() はタスクを SQLite に登録し、実行中はエージェントを生存させ、stash() で中間状態をチェックポイントし、タスク途中でエージェントがエビクションされた場合は次回起動時に onFiberRecovered() を呼びます。

呼び出し側がバックグラウンド作業を耐久的に受け付け、すばやく返し、再試行を安全に重複排除し、後から状態を確認し、実行中のジョブをキャンセルする必要があるときは startFiber() を使います。

クイックスタート

import { Agent } from "agents";
import type { FiberRecoveryContext } from "agents";

class MyAgent extends Agent {
	async doWork() {
		await this.runFiber("my-task", async (ctx) => {
			const step1 = await expensiveOperation();
			ctx.stash({ step1 });

			const step2 = await anotherExpensiveOperation(step1);
			this.setState({ ...this.state, result: step2 });
		});
	}

	async onFiberRecovered(ctx: FiberRecoveryContext) {
		if (ctx.name !== "my-task") return;
		const snapshot = ctx.snapshot as { step1: unknown } | null;
		if (snapshot) {
			const step2 = await anotherExpensiveOperation(snapshot.step1);
			this.setState({ ...this.state, result: step2 });
		}
	}
}

fiber が存在する理由

Durable Objects がエビクションされる理由は 3 つあります。

  1. 非アクティブタイムアウト — 受信リクエストも開いている WebSocket もない状態が約 70〜140 秒続く
  2. コード更新 / ランタイム再起動 — 非決定的で、1 日あたり 1〜2 回
  3. アラームハンドラのタイムアウト — 15 分

作業の途中でエビクションが起きると、上流の HTTP 接続(LLM プロバイダー、API、データベース)は永続的に切れます。メモリ上の状態(ストリーミングバッファ、部分応答、ループカウンタ)は失われます。複数ターンのエージェントループは、進行位置を完全に失います。

keepAlive() はエビクションの可能性を下げます。runFiber() はエビクションを生き延びられるようにします。

エージェントから独立して動かし、ステップごとの再試行と複数ステップのオーケストレーションが必要な作業には、代わりに Workflows を使います。fiber は、エージェント自身の実行の一部である作業向けです。比較は 長時間稼働するエージェント: Workflows とエージェント内部パターン を参照してください。

keepAlive

30 秒ごとのアラームハートビートを作り、非アクティブタイマーをリセットしてアイドルエビクションを防ぎます。

class Agent {
	keepAlive(): Promise<() => void>;
	keepAliveWhile<T>(fn: () => Promise<T>): Promise<T>;
}

推奨は keepAliveWhile() です。非同期関数を実行し、完了または例外時にハートビートを自動で片付けます。

const result = await this.keepAliveWhile(async () => {
	return await slowAPICall();
});

手動制御では、keepAlive() は disposer を返します。終わったら必ず呼んでください。呼ばないとハートビートが無期限に続きます。

const dispose = await this.keepAlive();
try {
	await longWork();
} finally {
	dispose();
}

仕組み

keepAlive の参照が 1 つでも残っているあいだ、30 秒ごとにアラームが発火し、非アクティブタイマーをリセットします。すべての disposer が呼ばれるとアラームは止まり、DO は自然にアイドルになれます。

ハートビートは listSchedules() からは見えません。スケジュール行は作られません。独自のスケジュールとも衝突しません。アラームシステムは、すべてのスケジュールと keepAlive ハートビートを 1 つのアラーム枠で多重化します。

間隔の設定

デフォルトは 30 秒です。非アクティブタイムアウトは約 70〜140 秒なので、30 秒なら余裕があります。静的オプションで上書きできます。

class MyAgent extends Agent {
	static options = { keepAliveIntervalMs: 2_000 };
}

keepAlive と runFiber の使い分け

keepAlive はエビクションを防ぎますが、復旧はしません。ハートビートがあってもエージェントがエビクションされた場合(コード更新、アラームタイムアウト、リソース上限)、進行中の作業は失われます。

runFiber は内部で keepAlive を呼び、作業を SQLite に永続化して復旧できるようにします。やり直しが安価、またはチェックポイントが不要なときは keepAlive だけを使います。作業が高価で、中断した位置から再開したいときは runFiber を使います。

シナリオ 使うもの
遅い API 呼び出しを待つ keepAlive()
LLM 応答のストリーミング(AIChatAgent 経由) 自動(組み込み)
中間結果がある複数ステップの計算 runFiber()
10 分以上かかるバックグラウンド調査ループ stash() 付きの runFiber()
ちょうど 1 回だけ受け付ける必要のある webhook ジョブ startFiber()

runFiber

チェックポイントと復旧付きの耐久実行です。

class Agent {
	runFiber<T>(name: string, fn: (ctx: FiberContext) => Promise<T>): Promise<T>;
	startFiber(
		name: string,
		fn: (ctx: FiberContext) => Promise<void>,
		options?: StartFiberOptions,
	): Promise<StartFiberResult>;
	inspectFiber(fiberId: string): Promise<FiberInspection | null>;
	inspectFiberByKey(idempotencyKey: string): Promise<FiberInspection | null>;
	listFibers(options?: ListFibersOptions): Promise<FiberInspection[]>;
	cancelFiber(fiberId: string, reason?: string): Promise<boolean>;
	cancelFiberByKey(idempotencyKey: string, reason?: string): Promise<boolean>;
	deleteFibers(options?: DeleteFibersOptions): Promise<number>;
	resolveFiber(fiberId: string, result: FiberRecoveryResult): Promise<boolean>;
	stash(data: unknown): void;
	onFiberRecovered(
		ctx: FiberRecoveryContext,
	): Promise<void | FiberRecoveryResult>;
}

type FiberContext = {
	id: string;
	signal: AbortSignal;
	stash(data: unknown): void;
	snapshot: unknown | null;
};

type FiberStatus =
	| "pending"
	| "running"
	| "completed"
	| "aborted"
	| "interrupted"
	| "error";

type FiberRecoveryContext = {
	id: string;
	name: string;
	status?: FiberStatus;
	idempotencyKey?: string;
	metadata?: Record<string, unknown> | null;
	snapshot: unknown | null;
	createdAt: number;
	recoveryReason: "interrupted";
};

ライフサイクル

通常の実行

runFiber("work", fn)
  ├─ Persist recovery metadata
  ├─ keepAlive() — heartbeat starts
  ├─ Execute fn(ctx)
  │    ├─ ctx.stash(data) → persist snapshot
  │    ├─ ctx.stash(data) → persist snapshot
  │    └─ return result
  ├─ Delete recovery metadata
  ├─ keepAlive dispose — heartbeat stops
  └─ Return result to caller

エビクションと復旧

[DO evicted — all in-memory state lost]

  On next activation:
  ├─ Request/connection → onStart() → check for orphaned fibers  [primary path]
  │  OR
  ├─ Persisted alarm fires → housekeeping check                   [fallback path]

  Recovery:
  ├─ Load interrupted fibers from storage
  ├─ For each interrupted fiber:
  │    ├─ Parse snapshot from JSON
  │    ├─ Call onFiberRecovered(ctx)
  │    └─ Delete recovery metadata after successful recovery
  └─ If onFiberRecovered calls runFiber() again → new fiber, normal execution

どちらの復旧パスも同じフックを呼びます。アラーム経路は、受信クライアント接続がないバックグラウンドエージェントに重要です。永続化されたアラームがエージェント自身を起こします。

サブエージェント

fiber はサブエージェント内でも動きます。fiber 行とスナップショットはサブエージェント自身の SQLite データベースに保存され、onFiberRecovered() はサブエージェントを this として実行されます。

サブエージェントには独立したアラーム枠がないため、物理ハートビートはトップレベルの親が所有します。サブエージェントが fiber を始めると、親は復旧チェックを所有側のサブエージェントへ戻すためのメタデータを十分に追跡します。子にクライアント接続や受信 RPC がなくても同様です。

これにより復旧は子のローカルに保ちつつ、親が所有する単一の物理アラーム枠を維持します。復旧後の継続処理は facet 内から schedule() を使えます。親が物理アラームを所有し、コールバックを子へルーティングします。

実行中のエラー

fn(ctx) throws Error
  ├─ DELETE row from cf_agents_runs
  ├─ keepAlive dispose
  └─ Error propagates to caller (or logged if fire-and-forget)

自動再試行はありません。復旧ロジックは onFiberRecovered に置きます。スナップショットと、何が失敗したかの十分なコンテキストがそこにあります。

インラインと fire-and-forget

runFiber() は両方のパターンをサポートします。

// Inline — await the result
const result = await this.runFiber("work", async (ctx) => {
	return computeExpensiveThing();
});

// Fire-and-forget — caller does not wait
void this.runFiber("background", async (ctx) => {
	await longRunningProcess();
});

インラインの await 中に DO がエビクションされると、呼び出し側はいなくなります。復旧時は onFiberRecovered が発火しますが、元の呼び出し側へ結果を返せません。これはプロセス境界をまたぐ耐久実行の本質的な制約です。1 回の DO 寿命を超える可能性が高い長時間作業で、呼び出し側が状態レコードの保持、冪等な受付、キャンセルを必要とする場合は startFiber() を使います。

startFiber

呼び出し側がバックグラウンド作業を耐久的に受け付け、すばやく返し、再試行を安全に重複排除する必要があるときは startFiber() を使います。コールバック実行前に保持される fiber レコードを保存し、runFiber() と同じ keep-alive と復旧の仕組みでコールバックをバックグラウンド開始します。

const receipt = await this.startFiber(
	"reply-to-webhook",
	async (ctx) => {
		ctx.stash({ webhookId, threadId });
		await postReply(threadId);
	},
	{
		idempotencyKey: `webhook:${webhookId}`,
		metadata: { threadId },
	},
);

if (!receipt.accepted) {
	// This webhook was already accepted by an earlier delivery.
}

デフォルトでは、startFiber() は作業が耐久的に受け付けられたあとに返します。受け付け済み fiber が終端状態になるまで呼び出し側を開いたままにする場合は waitForCompletion: true を渡します。同じ冪等キーでの重複呼び出しは、可能なときはアクティブなメモリ上の実行に合流し、accepted: false 付きの保持状態を返します。

const result = await this.startFiber("reply-to-webhook", reply, {
	idempotencyKey: `webhook:${webhookId}`,
	waitForCompletion: true,
});

if (result.status === "error") {
	console.error(result.error);
}

startFiber() は耐久受付 API であり、値を返す API ではありません。管理対象 fiber の状態は返しますが、コールバックの結果は返しません。後から状態を見るには inspectFiber() または inspectFiberByKey() を使います。

const current = await this.inspectFiberByKey(`webhook:${webhookId}`);

if (current) {
	await this.cancelFiber(current.fiberId, "No longer needed");
}

await this.deleteFibers({
	status: ["completed", "error", "aborted"],
	settledBefore: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
});

デフォルトでは、deleteFibers() は確定した completederroraborted 行を削除します。interrupted 行は、その状態を明示しない限り削除しません。中断行は検査や手動解決が必要なことが多いためです。

キャンセルは協調的です。cancelFiber() は中止の終端状態を記録し、fiber が現在の isolate で実行中なら ctx.signal を中止します。コールバックは高コストな作業の前後と、目に見える副作用の前に ctx.signal.aborted を確認してください。waitForCompletion: true の呼び出し側は、台帳が aborted になると返します。非協調的なコールバックが現在の isolate で動き続けていても同様です。

fiber の途中で Durable Object がエビクションされると、保持レコードは interrupted になり、onFiberRecovered() は最後のチェックポイントを受け取ります。元のクロージャは自動では再生できません。再開するか、補償するか、検査のために残すかは、ctx.namectx.snapshot、メタデータで判断します。

方針の決定を記録するには、onFiberRecovered() から FiberRecoveryResult を返します。

async onFiberRecovered(ctx: FiberRecoveryContext) {
	if (ctx.name !== "reply-to-webhook") return;

	const snapshot = ctx.snapshot as { webhookId: string; threadId: string };
	await postRecoveryMessage(snapshot.threadId);

	return {
		status: "completed",
		snapshot: { ...snapshot, recovered: true },
	};
}

undefined を返すと、管理対象 fiber は interrupted のままです。例外を投げると interrupted のままで、検査用に復旧エラーを記録します。aborted などの終端済み管理対象 fiber は、古い実行行が残っていても再復旧しません。

復旧が onFiberRecovered() ではなく、後続の重複 webhook で起きた場合は、アプリケーション層の復旧が成功したあとに同じ結果形で resolveFiber() を使います。resolveFiber() が更新するのは、現在 interrupted の管理対象 fiber だけです。pending、running、またはすでに終端の行には false を返します。

stash によるチェックポイント

ctx.stash(data) は SQLite へ同期的に書き込みます。「保存すると決めた」と「保存された」のあいだに非同期の隙間はありません。stash() が返ったあとにエビクションが起きても、データは SQLite にあることが保証されます。

各呼び出しは前回のスナップショットを完全に置き換えます。マージではありません。復旧に必要な状態をすべて書いてください。

await this.runFiber("research", async (ctx) => {
	const steps = ["search", "analyze", "synthesize"];
	const completed: string[] = [];
	const results: Record<string, unknown> = {};

	for (const step of steps) {
		results[step] = await executeStep(step);
		completed.push(step);

		ctx.stash({
			completed,
			results,
			pendingSteps: steps.slice(completed.length),
		});
	}
});

this.stash と ctx.stash

どちらも同じことをします。ctx.stash() は fiber ID への直接クロージャを使います。this.stash()AsyncLocalStorage で現在実行中の fiber を探します。各 fiber の ALS コンテキストは独立なので、並行 fiber でも正しく動きます。

this.stash() は、ctx にアクセスできないネストした関数から呼ぶときに便利です。runFiber コールバックの外で呼ぶと例外を投げます。

復旧

中断した fiber を扱うには onFiberRecovered をオーバーライドします。デフォルト実装は警告をログし、行を削除します。

class ResearchAgent extends Agent {
	async onFiberRecovered(ctx: FiberRecoveryContext) {
		if (ctx.name !== "research") return;

		const snapshot = ctx.snapshot as {
			completed: string[];
			results: Record<string, unknown>;
			pendingSteps: string[];
		} | null;

		if (snapshot && snapshot.pendingSteps.length > 0) {
			void this.runFiber("research", async (fiberCtx) => {
				const { completed, results, pendingSteps } = snapshot;

				for (const step of pendingSteps) {
					results[step] = await this.executeStep(step);
					completed.push(step);

					fiberCtx.stash({
						completed,
						results,
						pendingSteps: pendingSteps.slice(pendingSteps.indexOf(step) + 1),
					});
				}
			});
		}
	}
}

要点は次のとおりです。

  • 元のラムダはもうありません。 復旧時に手元にあるのは namesnapshot だけです。ラムダはシリアライズできないので、復旧ロジックはフックに置く必要があります。
  • 管理対象外の runFiber() 行は、フックが正常に返ったあとに削除されます。 管理対象外の作業を続けたい場合は、フック内で再度 runFiber() を呼びます。新しい行が作られます。
  • 管理対象の startFiber() 行は保持されます。 中断した管理対象 fiber を completederroraborted、または引き続き interrupted にするには FiberRecoveryResult を返します。
  • 復旧の意味は自分で決めます。 最初から再試行する、チェックポイントから再開する、スキップしてユーザーに通知する、何もしない、のいずれでも構いません。フレームワークは戦略を強制しません。
  • フックが例外を投げると、行は(上限まで)残ります。 後続の起動またはアラーム走査が復旧を再試行します。一時的なストレージやスケジュールの失敗から守れます。作業を終端にして再試行したくないときは、アプリケーション層のエラーを自分で捕捉してください。常に例外を投げるフックは、バックオフ付きスケジュールで再試行されます(復旧アラームは指数遅延で上限 5 分なので、ビジーループにはなりません)。行が fiberRecoveryMaxAgeMs(デフォルト 24 時間)を超えると、fiber:recovery:skippedreason: "max_age_exceeded")イベント付きで破棄されます。fiberRecoveryMaxAgeMs: 0 にすると、そのような行を無期限に保持します。復旧は上限付きバックオフで再試行し続け、復旧不能な行があるあいだ Durable Object はアイドルエビクションしません。自分で検査または削除するつもりがない限り、有限の寿命を推奨します。管理対象の作業では、保持行は interrupted のままで、検査用に復旧エラーを記録します。

チャットの復旧

AIChatAgentThink は、LLM ストリーミング復旧に fiber を使います。すべてのチャットターンは自動で fiber に包まれます。フレームワークが内部の復旧経路を扱い、プロバイダー固有の戦略向けに onChatRecovery を公開します。詳細は 長時間稼働するエージェント: 中断した LLM ストリームの復旧 を参照してください。

並行 fiber

複数の fiber を同時に実行できます。それぞれが SQLite に独自の行とスナップショットを持ち、それぞれが独立して keepAlive() を呼びます(参照カウントなので、すべての fiber が完了するまで DO は生存します)。

void this.runFiber("fetch-data", async (ctx) => {
	/* ... */
});
void this.runFiber("process-queue", async (ctx) => {
	/* ... */
});

復旧時は、孤立したすべての行を走査し、それぞれに対して onFiberRecovered を呼びます。復旧フックでは ctx.name で fiber の種類を区別します。

ローカルでのテスト

wrangler dev では、fiber 復旧は本番と同じように動きます。SQLite とアラーム状態は、再起動のあいだディスクに残ります。

  1. エージェントを起動し、fiber(runFiber)を開始します
  2. wrangler プロセスを終了します(Ctrl-C または SIGKILL)
  3. wrangler を再起動します
  4. 復旧は自動で発火します。リクエストが来れば onStart() 経由、クライアントが接続しなければ永続化されたアラーム経由です

API リファレンス

runFiber(name, fn)

耐久 fiber を実行します。fiber は fn の実行前に SQLite へ登録され、完了(または例外)後に削除されます。そのあいだ keepAlive() が保持されます。

  • name — fiber の識別子。onFiberRecovered で種類を区別します。一意ではありません。複数の fiber が同じ名前を共有できます。
  • fnFiberContext を受け取る非同期関数。クロージャは自然に使えます(this とローカル変数がキャプチャされます)。
  • 戻り値fn が返した値。完了前に DO がエビクションされると戻り値は失われ、復旧はフック経由で行われます。

startFiber(name, fn, options)

保持されるバックグラウンド fiber を耐久的に受け付けます。返される StartFiberResult には、生成された fiberId、現在の status、任意の metadata、および accepted が含まれます。同じ冪等キーに既存 fiber が一致した場合、acceptedfalse です。

  • name — 管理対象 fiber の識別子。検査と復旧で使います。
  • fnFiberContext を受け取る非同期関数。関数の結果は保存されません。
  • options.idempotencyKey — 再試行の重複排除に使う、安定した外部キーです。
  • options.metadata — 保持行と一緒に保存する、JSON シリアライズ可能なデータです。
  • options.waitForCompletion — 返す前に終端状態を待ちます。

inspectFiber(fiberId) / inspectFiberByKey(idempotencyKey)

管理対象 fiber の保持状態行を返します。行がなければ null です。

listFibers(options)

保持されている管理対象 fiber を一覧します。status または name で絞り込み、limit で件数を上限できます。

cancelFiber(fiberId, reason) / cancelFiberByKey(idempotencyKey, reason)

管理対象 fiber を aborted にし、現在の isolate で実行中ならメモリ上の ctx.signal を中止します。fiber が存在しない、またはすでに終端なら false を返します。

resolveFiber(fiberId, result)

アプリケーション層の復旧が成功したあと、interrupted の管理対象 fiber を解決します。pending、running、またはすでに終端の行には false を返します。

deleteFibers(options)

保持されている管理対象 fiber 行を削除します。デフォルトでは、確定した completederroraborted 行が対象です。クリーンアップを絞るには statussettledBefore、または limit を渡します。

stash(data) / ctx.stash(data)

現在の fiber の状態をチェックポイントします。SQLite へ同期書き込みします。各呼び出しは前回のスナップショットを完全に置き換えます。data は JSON シリアライズ可能である必要があります。

onFiberRecovered(ctx)

エージェント再起動時に、孤立した fiber 行ごとに 1 回呼ばれます。復旧を実装するにはオーバーライドします。管理対象外の runFiber() 行は、このフックが正常に返ったあとに削除されます。復旧が例外を投げると、行は後続の走査のために残ります。一時障害で復旧ハンドルを失わないためです。管理対象の startFiber() 行は保持され、FiberRecoveryResult を返すことで解決できます。

  • ctx.id — 一意の fiber ID
  • ctx.namerunFiber() に渡した名前
  • ctx.status — 管理対象 fiber の保持状態
  • ctx.idempotencyKey — 管理対象 fiber の冪等キー(指定した場合)
  • ctx.metadata — 管理対象 fiber のメタデータ(指定した場合)
  • ctx.snapshot — 最後の stash() データ。stash() を一度も呼んでいなければ null
  • ctx.createdAtrunFiber() 開始時のエポックミリ秒。安全に再生するには古すぎる復旧をスキップするために Date.now() と比較します。
  • ctx.recoveryReason — 復旧が動いている理由。現在はエビクションまたは再起動復旧のため、常に "interrupted" です。

keepAlive()

30 秒のアラームハートビートを作ります。disposer 関数を返します。冪等です。disposer を複数回呼んでも安全です。

keepAliveWhile(fn)

DO を生存させたまま非同期関数を実行します。ハートビートは fn の前に始まり、完了または例外時に止まります。fn が返した値を返します。

関連

役に立ちましたか?