Cloudflare DurableObjects内のエラーを調べる
Cloudflare DurableObjectsでスローされるエラーは、retryable
とかあるので調べる。
最終的な目標は、Cloudflare Worker(Hono)側で正しいエラーハンドリングを行い、APIであれば正しいレスポンスコード・エラーメッセージを返す事とする。
まず適当にスローする
import { DurableObject } from "cloudflare:workers";
import { Env } from "hono";
export class Counter extends DurableObject {
constructor(
private readonly state: DurableObjectState,
readonly env: Env["Bindings"]
) {
super(state, env);
}
async throwError() {
throw new Error("This is intentional error");
}
}
Cloudflare Worker側でUnhandledだと、これは500
エラーで出てくる。
Cloudflare Worker内でエラーをキャッチすると、
{
remote: true
}
このリモートとは何なのか?
remote: true
で伝播する
コード起因のエラーはDurableObjectが伝播させるエラーは2種類あって、
An exception can be thrown within the user code which implements a Durable Object class. The resulting exception will have a .remote property set to True in this case.
記述したコード起因のエラーはremote: true
で判別される。
これに対して、コード起因以外のエラーがおそらくremote: false
で伝播されると思いきや、そんなこともないらしい。
An exception can be generated by Durable Object’s infrastructure. Some sources of infrastructure exceptions include: transient internal errors, sending too many requests to a single Durable Object, and too many requests being queued due to slow or excessive I/O (external API calls or storage operations) within an individual Durable Object. Some infrastructure exceptions may also have the .remote property set to True – for example, when the Durable Object exceeds its memory or CPU limits.
- 一時的エラー
- アクセス集中エラー
- 実行待ちキュー多すぎエラー
例えばこんなエラーがあるが、メモリ・CPUの制限超過でもremote: true
になるらしい。
Troubleshootingを漁る
Troubleshootingを漁って、どんなパラメータがエラーに含まれるのか調べる。
-
overloaded: true
: 実行キュー多すぎ、時間かかりすぎ等。負荷分散とかして減らそう -
retryable: true
: 一時的エラー起因。DurableObjectへの参照取得にリミットがあって、そのリミットに到達した時とか。(リミット自体はキャッシュの問題なのでリトライすればいいらしい)
retryable
の時のリトライ例
ここから引用しただけ。
// Retry behavior can be adjusted to fit your application.
let maxAttempts = 3;
let baseBackoffMs = 100;
let maxBackoffMs = 20000;
let attempt = 0;
while (true) {
// Try sending the request
try {
// Create a Durable Object stub for each attempt, because certain types of
// errors will break the Durable Object stub.
const doStub = env.ErrorThrowingObject.get(id);
const resp = await doStub.fetch("http://your-do/");
return Response.json(resp);
} catch (e: any) {
if (!e.retryable) {
// Failure was not a transient internal error, so don't retry.
break;
}
}
let backoffMs = Math.min(maxBackoffMs, baseBackoffMs * Math.random() * Math.pow(2, attempt));
attempt += 1;
if (attempt >= maxAttempts) {
// Reached max attempts, so don't retry.
break;
}
await scheduler.wait(backoffMs);
ユーザ起因のエラーに情報をもっと乗っけたい
ユーザ起因のエラーの場合にも、リトライを許可するかとか重複リクエストによるエラーとか色んな情報を乗っけてCloudflare Workerに返したくなる。
とりあえずJSON.stringify
でエラーメッセージを返すことにする。
async throwError() {
throw new Error(JSON.stringify({ message: "test throw", status: 500 }));
}
エラー型もRPCで伝播させる
とりあえず、こんな感じで書けたら嬉しいを書いてみた。
interface DurableObjectExceptionOptions extends ErrorOptions {
status?: StatusCode;
cause?: unknown;
message?: string;
}
export class DurableObjectException extends Error {
readonly status?: StatusCode;
constructor(options?: DurableObjectExceptionOptions) {
super(options?.message, { cause: options?.cause });
this.status = options?.status;
}
httpException() {
return new HTTPException(this.status || 500, { message: this.message });
}
}
export type DurableObjectResponse<T> = Promise<T | DurableObjectException>;
export class Counter extends DurableObject {
async throwCustomError(): DurableObjectResponse<void> {
try {
throw new DurableObjectException({
message: "This is custom error",
status: 503,
});
} catch (e) {
return new DurableObjectException({ cause: e });
}
}
}
const app = new OpenAPIHono();
const route = createRoute({
// 省略
});
app.openapi(route, async (c) => {
try {
const id = await c.env.COUNTER.idFromName("main");
const stub = await c.env.COUNTER.get(id);
const res = await stub.throwCustomError();
if (res instanceof DurableObjectException) {
throw res;
}
return c.json({ message: "unintetionally ignored throw" }, 200);
} catch (e: any) {
if (e instanceof HTTPException) {
throw e;
}
if (e instanceof DurableObjectException) {
throw e.httpException();
}
throw new HTTPException(500);
}
},
(result, c) => {
if (!result.success) {
return c.json({ code: 422, message: result.error.message }, 422);
}
});
スタックトレースをCloudflare Worker(Hono)側に伝播される方法が分からない
ES2022のErrorオブジェクトはcause
プロパティを持っており、これが再起関数になっていてスタックトレースを取れる(?)という認識。
なので、このErrorオブジェクトをそのままHono側に渡してあげたいが、渡し方がわからない。
そもそも大前提として、こうしたスタックトレースをDurableObjectの外に出してしまう事自体がバッドプラクティスなのか?
DurableObjectも1つのWorkerという扱いなので、Workerを跨ぐようなエラーの伝播の仕方はそもそも良くないという考え方をすべきなのかも。
という事で、DurableObject内でtry..catch
を行い握り潰して、neverthrowのResultのような形でエラーハンドリングを行う方針にする。