Skip to content

Performer

ジョブの中身はPerformerを継承したクラスとして書きます

基本の形

ts
import { Performer, type JobContext } from 'tsumugi/performer';

class SendMail extends Performer<{ to: string; subject: string }, void, {}, Env> {
  async perform(payload: { to: string; subject: string }, ctx: JobContext): Promise<void> {
    await fetch('https://api.example.com/mail', {
      method: 'POST',
      body: JSON.stringify(payload),
      signal: ctx.signal,
    });
  }
}

型引数は順に、ペイロード、戻り値、必須キーの宣言、Envです

bindingはWorkerEntrypointと同じくコンストラクタで受け取るので、this.envから参照できます

登録簿

binding名とperformerの対応はdefineTsumugiperformersに書きます ここ1箇所に書けば、wranglerのservice bindingも型引数の手書きも不要です

ts
const tsumugi = defineTsumugi<Env>({
  performers: { MAIL: SendMail, CHARGE: ChargeCard },
  // ...
});

実行文脈

performの第2引数にJobContextが渡ります

フィールド内容
jobId<binding>#<shard>:<localId>の形のジョブID
attempt1始まりの試行回数
idempotencyKeyジョブ単位で安定する値、再実行でも同じ
signalタイムアウト時にabortされるAbortSignal

at-least-onceでは同じジョブが2回実行され得るので、外部への副作用はidempotencyKeyで冪等にしておきます

signalは協調的な中断の要求にすぎません 応じないperformerは実行を継続するので、中断させたい処理には自分で渡す必要があります

失敗の伝えかた

throwすれば失敗として扱われ、Durable Objectがリトライの要否を決めます 戻り値を返せば成功です

ts
class ChargeCard extends Performer<{ customerId: string; amountJpy: number }, void, { concurrencyKey: true }, Env> {
  async perform(payload: { customerId: string; amountJpy: number }): Promise<void> {
    const res = await this.env.PAYMENT.charge(payload);
    if (!res.ok) throw new Error(`決済に失敗: ${res.status}`);
  }
}

投げた例外のメッセージは試行履歴に残り、ダッシュボードの詳細画面で確認できます 本文は2,000字で打ち切られ、1ジョブあたり20件まで保持されます

キーを必須にする

第3型引数に{ concurrencyKey: true }{ uniqueKey: true }を書くと、そのperformerへの投入時にキーの指定が必須になります

ts
class ChargeCard extends Performer<Payload, void, { concurrencyKey: true }, Env> {}

キーの導出をperformer側の関数に任せない理由は、Durable Objectとperformerが別isolateだからです Durable Objectの中でユーザーの関数は実行できないので、呼び出し側が文字列として渡します Durable Objectは不透明なキーとして扱うだけなので、追加のRPCもレイテンシも発生しません

現状の制約

この必須化が働くのはJobQueue<M>型を経由して呼び出す場合だけです enqueue(env, input)createClient()が受け取るのはEnqueueInputで、bindingstringpayloadunknownのため、キーの渡し忘れも型エラーになりません

JobQueue<M>は型定義としてexportされていますが、この型を返すランタイムAPIは現時点でありません 型による強制を効かせるには、利用側でJobQueue<M>に適合するラッパーを自分で用意する必要があります

別Workerに置く

performerはservice binding越しの別Workerにも置けます その場合はRemotePerformerを継承します

ts
// 相手側のWorker
import { RemotePerformer, type RemoteJobContext } from 'tsumugi/performer';

export class SendMail extends RemotePerformer<{ to: string; subject: string }, void, {}, Env> {
  async perform(payload: { to: string; subject: string }, ctx: RemoteJobContext): Promise<void> {
    // ...
  }
}

// WorkerEntrypointの名前付きexportに加えてdefaultも要る
export default {
  async fetch(): Promise<Response> {
    return new Response('performer only', { status: 404 });
  },
} satisfies ExportedHandler<Env>;

呼び出し側の登録簿には、クラスの代わりにremote()を置きます

ts
import { remote } from 'tsumugi';

const performers = { HELLO: Hello, MAIL: remote<SendMail>('MAIL_SERVICE') };

wrangler.jsoncではentrypointにクラス名を指定します

jsonc
"services": [
  { "binding": "MAIL_SERVICE", "service": "my-mailer", "entrypoint": "SendMail" },
],

ローカルのperformerとリモートのperformerは同じ登録簿に混在できます

リモートでの制約

RPCの引数はAbortSignalに対応していないので、RemoteJobContextにはsignalがありません タイムアウトは呼び出し側が待機を打ち切るだけで、リモート側の処理は継続します

中断が必要な処理はローカルに配置してください

テストする

tsumugi/testingからスケジューラとバックオフの純粋関数を取れます 時刻も乱数も引数で受け取るので、fake timersなしで検証できます

ts
import { nextAttempt, schedule } from 'tsumugi/testing';