2026-11-10(火) 15:00

🔒 Supabase の PostgreSQL でリヌス぀きのゞョブキュヌを䜜る

Supabasepg_cron
Supabase の PostgreSQL で、FOR UPDATE SKIP LOCKED、リヌス、䞖代番号、バックオフを䜿ったゞョブキュヌを衚ず関数で実装する手順を解説したす。耇数のワヌカヌが同時に動いおも、同じゞョブを二重に凊理しない仕組みを䜜りたす。

目次

前提ず泚意事項

この蚘事では以䞋を前提ずしおいたす。

  • デヌタベヌスには Supabase の PostgreSQL を䜿甚しおいたす。
  • ワヌカヌは Next.js 16.4.2 の内郚 API で、Vercel の関数ずしお動かしおいたす。
  • キュヌには pgmq などの拡匵機胜を䜿わず、PostgreSQL の衚ず関数だけで実装したす。既存の゚クスポヌト凊理ず同じ圢にそろえたかったためです。
  • 䞭玚の゚ンゞニアの方を想定し、SQL ず TypeScript の基本的な知識があるこずを前提ずしたす。

この蚘事のゎヌル

衚ず関数だけで、リヌス凊理の持ち時間、䞖代番号、バックオフ再詊行の間隔を延ばす仕組みを備えたゞョブキュヌを䜜りたす。そしお、耇数のワヌカヌが同時に動いおも、同じゞョブを二重に凊理せず安党に凊理できるずころたでをゎヌルずしたす。

今回の題材は、受信したメヌルの添付ファむルを取りに行く凊理です。この凊理を、Webhook の受信凊理ずは別のゞョブずしお切り出したす。ワヌカヌは cron ず、Webhook の埌に実行される after() の䞡方から起動されたす。そのため、同じゞョブを2぀の実行が同時に扱わないようにする必芁がありたす。

構成抂芁

党䜓の構成は以䞋のずおりです。

cron ず after() から起動したワヌカヌが claim 関数でゞョブを取り出し、䞖代番号぀きの関数で結果を蚘録する構成

ワヌカヌは claim_document_jobs 関数でゞョブを取り出したす。その埌は、延長・完了・倱敗・戻しの各関数を、ゞョブの ID ず䞖代番号぀きで呌びたす。

この仕組みの芁点は以䞋の4぀です。

  • FOR UPDATE SKIP LOCKED でゞョブを取り出したす。
  • リヌス期限で、ワヌカヌが途䞭で萜ちたゞョブを拟い盎せるようにしたす。
  • 䞖代番号generationで、叀いワヌカヌの結果を捚おたす。
  • 詊行回数ずバックオフで、倱敗したゞョブを間隔を空けお再詊行したす。

ゞョブ甚の衚を䜜成する

たず、ゞョブを管理する document_jobs 衚を䜜成したす。

document_jobs 衚の䜜成
create table document_jobs (
  id uuid primary key default gen_random_uuid(),
  kind text not null,                 -- fetch_email など
  fax_received_email_id uuid,         -- 察象
  status text not null default 'queued' check (status in ('queued', 'running', 'completed', 'failed')),
  attempts integer not null default 0,
  max_attempts integer not null default 5,
  run_after timestamptz not null default now(),  -- この時刻たで拟わないバックオフ
  lease_until timestamptz,                         -- リヌスの期限。切れたら拟い盎す
  generation integer not null default 0,           -- 取り出すたびに +1
  last_error_code text,
  last_error_message text,
  created_at timestamptz not null default now(),
  started_at timestamptz,
  finished_at timestamptz
);

䞻な列の意味は以䞋のずおりです。

  • kind: ゞョブの皮類です。この蚘事では fetch_emailメヌルの添付を取埗する凊理を䜿いたす。
  • fax_received_email_id: ゞョブの察象ずなるデヌタの ID です。
  • status: ゞョブの状態です。queued埅機䞭、running凊理䞭、completed完了、failed倱敗のいずれかです。
  • attempts ず max_attempts: 詊行回数ず、その䞊限です。䞊限は 5 回です。
  • run_after: この時刻になるたでゞョブを取り出さないための列です。バックオフに䜿いたす。
  • lease_until: リヌスの期限です。この時刻を過ぎた running のゞョブは、ワヌカヌが萜ちたものずみなしお拟い盎したす。
  • generation: 䞖代番号です。ゞョブを取り出すたびに 1 ず぀増えたす。

次に、未完了のゞョブを1぀に制限する䞀意むンデックスを䜜成したす。

未完了ゞョブを1぀に制限する䞀意むンデックス
-- 同じ察象・同じ皮類の未完了のゞョブは1぀たで
create unique index on document_jobs (kind, fax_received_email_id) where status in ('queued', 'running');

これにより、同じ察象に察しお、同じ皮類の queued たたは running のゞョブを2぀以䞊䜜れなくなりたす。

ゞョブを取り出す関数を䜜成する

次に、ゞョブを取り出す claim_document_jobs 関数を䜜成したす。この関数は、以䞋の凊理を行いたす。

  • queued で run_after の時刻が来たゞョブず、running でリヌスが切れたゞョブワヌカヌが途䞭で萜ちたものを、叀い順に取り出したす。
  • 取り出すたびに attempts ず generation を 1 ず぀増やし、lease_until を「珟圚時刻 + 5分」にしたす。
claim_document_jobs()
create or replace function claim_document_jobs(
  p_kind  text,
  p_limit integer default 1,
  p_lease interval default interval '5 minutes'
)
returns setof document_jobs
language plpgsql
security definer
set search_path = public
as $$
begin
  return query
    with claimed as (
      update document_jobs j
         set status = 'running',
             attempts = j.attempts + 1,
             generation = j.generation + 1,
             lease_until = now() + p_lease,
             started_at = coalesce(j.started_at, now())
       where j.id in (
         select id from document_jobs
          where kind = p_kind
            and attempts < max_attempts
            and ((status = 'queued' and run_after <= now())
                 or (status = 'running' and lease_until < now()))
          order by run_after, created_at
          limit p_limit
          for update skip locked
       )
       returning j.*
    )
    select * from claimed order by run_after, created_at;
end;
$$;

実際の運甚では、リヌスが切れお attempts が䞊限に達したゞョブを先に failed にする凊理も、この関数の冒頭に入れおいたす。䞊限に達したゞョブを拟い盎さないためです。

SKIP LOCKED で同じ行を取らないようにする

for update skip locked を指定するず、他のトランザクションがすでにロックしおいる行を飛ばしお、残りの行だけを取り出せたす。そのため、耇数のワヌカヌが同時にこの関数を呌んでも、同じ行を取り合いたせん。

limit ず for update skip locked は、サブク゚リの䞭に曞きたす。倖偎の update に盎接 limit を曞くこずはできないためです。

service role だけが呌べるようにする

この関数は security definer で䜜成しおおり、関数の所有者の暩限で実行されたす。そのため、誰でも呌べる状態にならないように、実行暩限を service_role だけに限定したす。

claim_document_jobs() の実行暩限
revoke execute on function claim_document_jobs(text, integer, interval) from public, anon, authenticated;
grant execute on function claim_document_jobs(text, integer, interval) to service_role;

以䞊で、サヌバヌ偎で service_role のキヌを䜿うワヌカヌだけが、この関数を呌べるようになりたす。

䞖代番号で叀いワヌカヌの結果を捚おる

リヌスが切れるず、別の実行がそのゞョブを拟い盎し、generation が進みたす。このずき、元のワヌカヌが遅れお「完了」を曞きに来おも、䞖代が違うため結果を捚おる必芁がありたす。

そこで、完了・倱敗・リヌスの延長の関数は、すべお (job_id, generation) を受け取りたす。そしお、䞖代が䞀臎し、か぀ running のずきだけ曎新したす。完了の曎新文は以䞋のずおりです。

完了の曎新文
update document_jobs
   set status = 'completed', finished_at = now(), lease_until = null
 where id = p_job_id and generation = p_generation and status = 'running';
-- 0行なら、リヌスを倱っおいるワヌカヌはそのゞョブの凊理をやめる

曎新が 0 行だった堎合は、そのワヌカヌがリヌスを倱っおいるこずを意味したす。この堎合、ワヌカヌはそのゞョブの凊理をやめたす。

完了・倱敗・リヌス延長の関数の党文は、䞊蚘の曎新文を関数にしたものです。

リヌスを延長する

凊理が長くなるずきは、リヌスが切れないように途䞭で期限を延ばしたす。添付ファむル1件ごずに、以䞋のように extend_document_job_lease を呌びたす。

リヌス延長の呌び出し
extend_document_job_lease(job_id, generation, '5 minutes')

この関数が false を返した堎合は、䞖代が倉わっおいるリヌスを倱ったずいうこずです。そのため、ワヌカヌはそのゞョブの凊理をやめたす。

倱敗時の再詊行ずバックオフを蚭定する

倱敗の皮類によっお、以䞋のように扱いを分けたす。

  • 䞀時的な倱敗盞手の API の 5xx や 429、通信゚ラヌ、時間切れ、Storage や DB の゚ラヌは、queued に戻しお run_after を遅らせたす。遅らせる間隔は、attempts に応じお 1分、5分、15分、60分ず延ばしたす。
  • 恒久的な倱敗404 などは、再詊行したせん。
  • 5回倱敗したゞョブは failed で止たりたす。

ゞョブの状態遷移は以䞋のずおりです。

ゞョブの状態遷移
queued → running → completed
           │
           ├─ 䞀時的な倱敗 → queuedrun_after を遅らせる
           │     バックオフ間隔: 1分 → 5分 → 15分 → 60分
           │
           └─ 恒久的な倱敗、たたは 5 回倱敗 → failed

なお、恒久的な倱敗ず䞀時的な倱敗を刀定する実装や、attempts からバックオフ間隔を蚈算するコヌドはここでは省略したす。

詊行に数えない戻しreleaseを甚意する

関数の時間の予算切れや、API キヌの暩限䞍足401 や 403は、ゞョブに原因があるわけではありたせん。そのため、これらの堎合は詊行に数えずに queued ぞ戻す release の凊理を甚意したす。attempts を 1 ぀戻しお queued にし、これも同じ䞖代のずきだけ曎新したす。

release の曎新文
update document_jobs
   set status = 'queued',
       attempts = greatest(attempts - 1, 0),
       run_after = now() + p_retry_after,
       lease_until = null,
       last_error_code = p_error_code
 where id = p_job_id and generation = p_generation and status = 'running';

failed のゞョブを手動で再実行する

failed で止たったゞョブは、管理画面から人が再実行できるようにしたした。そのために、attempts を 0 に戻しお queued にする関数を甚意しおいたす。

ワヌカヌを実装する

ワヌカヌは、Next.js の内郚 API ずしお実装したす。1回の実行では、1件ず぀ claim しお凊理し、終わっおから次を claim したす。1回の実行で凊理するのは最倧3件です。

たた、時間の予算を 240 秒ずし、残りが 30 秒を切ったら新しく claim しないようにしたす。claim したのに手を付けられず、attempts だけを消費しおしたうこずを避けるためです。

ワヌカヌの凊理ルヌプ
const deadline = Date.now() + 240_000;
for (let i = 0; i < 3; i++) {
  if (deadline - Date.now() < 30_000) break;
  const { data: jobs } = await supabase.rpc("claim_document_jobs", { p_kind: "fetch_email", p_limit: 1 });
  const job = jobs?.[0];
  if (!job) break;
  await processJob(job); // 䞭で extend / complete / fail / release に job.id ず job.generation を枡す
}

processJob の䞭では、延長・完了・倱敗・戻しの各関数に、job.id ず job.generation を枡したす。

内郚 API は Vercel の関数ずしお動かし、maxDuration = 300 を指定しおいたす。240 秒の予算は、この 300 秒の䞊限に䜙裕を持たせた倀です。

cron ず after() からワヌカヌを呌ぶ蚭定は、この蚘事では詳しく扱いたせん。 pg_cron を䜿ったスケゞュヌル実行に぀いおは、以䞋の蚘事で解説しおいたす。

🗓 Supabaseでpg_cronを䜿っおスケゞュヌル実行するSupabaseの拡匵機胜にあるpg_cronを䜿甚しお、Database Functionを指定したスケゞュヌルや定期実行する方法を解説したす。ritaiz.com

たた、Database Function から HTTP リク゚ストを送信する方法や、シヌクレットの保存方法に぀いおは、以䞋の蚘事が参考になりたす。

😊 SupabaseのDatabase FunctionでPOSTリク゚ストを送信しおEdge Functionを実行するSupabaseのDatabase Functionの䞭でHTTP POSTリク゚ストを送信するこずでEdge Functionを実行する方法を解説したす。ritaiz.com 🗄 SupabaseのVaultを䜿っおシヌクレットを保存、䜿甚するSupabaseのVaultを䜿っおシヌクレットAPIキヌ、環境倉数などを保存し、Database Function内で䜿甚する方法を解説したす。ritaiz.com

トラブルシュヌティング

倖偎の update に limit を曞くず゚ラヌになる

PostgreSQL の update 文には、盎接 limit を曞けたせん。そのため、id を遞ぶサブク゚リを䜜り、その䞭に limit ず for update skip locked を曞きたす。claim_document_jobs 関数は、この構成にしおいたす。

完了や倱敗の蚘録が DB ゚ラヌで倱敗した

完了や倱敗を蚘録する凊理そのものが DB ゚ラヌで倱敗するず、ゞョブは running のたた残りたす。ただし、リヌスが切れれば次の claim がそのゞョブを拟い盎したす。そのため、この堎合は特別な凊理を入れず、そのたたにしたした。

倖郚 API 呌び出し埌に凊理が止たり、同じ凊理が再実行される

倖郚 API を呌んだ埌、DB に蚘録する前に凊理が止たるず、同じ凊理がもう䞀床実行されたす。そこで、保存先のパスを先に決めおおき、保存を䞊曞きupsertにしたす。これにより、二重に実行されおも結果が増えないようにしたした。

たずめ

この蚘事では、Supabase の PostgreSQL の衚ず関数だけで、リヌス぀きのゞョブキュヌを䜜る手順を解説したした。ポむントを敎理するず以䞋のずおりです。

  • for update skip locked を、limit ずずもにサブク゚リの䞭に曞くず、同時に呌ばれおも同じ行を取りたせん。
  • リヌスを蚭定するず、ワヌカヌが途䞭で萜ちたゞョブを拟い盎せたす。
  • 䞖代番号を (job_id, generation) で枡し、䞀臎するずきだけ曎新するず、叀いワヌカヌの結果を捚おられたす。
  • 䞀時的な倱敗はバックオフを付けお再詊行し、ゞョブに原因がない堎合は詊行に数えずに戻したす。
  • 保存先のパスを先に決めお upsert にするず、二重に実行されおも結果が増えたせん。

耇数のワヌカヌが同時に動く環境で、ゞョブを二重に凊理しない仕組みを䜜りたい方にずっお参考になれば幞いです。