åæãšæ³šæäºé
ãã®èšäºã§ã¯ä»¥äžãåæãšããŠããŸãã
- ããŒã¿ããŒã¹ã«ã¯ Supabase ã® PostgreSQL ã䜿çšããŠããŸãã
- ã¯ãŒã«ãŒã¯ Next.js 16.4.2 ã®å éš API ã§ãVercel ã®é¢æ°ãšããŠåãããŠããŸãã
- ãã¥ãŒã«ã¯ pgmq ãªã©ã®æ¡åŒµæ©èœã䜿ãããPostgreSQL ã®è¡šãšé¢æ°ã ãã§å®è£ ããŸããæ¢åã®ãšã¯ã¹ããŒãåŠçãšåã圢ã«ãããããã£ãããã§ãã
- äžçŽã®ãšã³ãžãã¢ã®æ¹ãæ³å®ããSQL ãš TypeScript ã®åºæ¬çãªç¥èãããããšãåæãšããŸãã
ãã®èšäºã®ãŽãŒã«
衚ãšé¢æ°ã ãã§ããªãŒã¹ïŒåŠçã®æã¡æéïŒãäžä»£çªå·ãããã¯ãªãïŒå詊è¡ã®ééãå»¶ã°ãä»çµã¿ïŒãåãããžã§ããã¥ãŒãäœããŸãããããŠãè€æ°ã®ã¯ãŒã«ãŒãåæã«åããŠããåããžã§ããäºéã«åŠçããå®å šã«åŠçã§ãããšãããŸã§ããŽãŒã«ãšããŸãã
ä»åã®é¡æã¯ãåä¿¡ããã¡ãŒã«ã®æ·»ä»ãã¡ã€ã«ãåãã«è¡ãåŠçã§ãããã®åŠçããWebhook ã®åä¿¡åŠçãšã¯å¥ã®ãžã§ããšããŠåãåºããŸããã¯ãŒã«ãŒã¯ cron ãšãWebhook ã®åŸã«å®è¡ããã after() ã®äž¡æ¹ããèµ·åãããŸãããã®ãããåããžã§ãã2ã€ã®å®è¡ãåæã«æ±ããªãããã«ããå¿
èŠããããŸãã
æ§ææŠèŠ
å šäœã®æ§æã¯ä»¥äžã®ãšããã§ãã
ã¯ãŒã«ãŒã¯ claim_document_jobs 颿°ã§ãžã§ããåãåºããŸãããã®åŸã¯ãå»¶é·ã»å®äºã»å€±æã»æ»ãã®å颿°ãããžã§ãã® ID ãšäžä»£çªå·ã€ãã§åŒã³ãŸãã
ãã®ä»çµã¿ã®èŠç¹ã¯ä»¥äžã®4ã€ã§ãã
FOR UPDATE SKIP LOCKEDã§ãžã§ããåãåºããŸãã- ãªãŒã¹ïŒæéïŒã§ãã¯ãŒã«ãŒãéäžã§èœã¡ããžã§ããæŸãçŽããããã«ããŸãã
- äžä»£çªå·ïŒ
generationïŒã§ãå€ãã¯ãŒã«ãŒã®çµæãæšãŠãŸãã - 詊è¡åæ°ãšããã¯ãªãã§ã倱æãããžã§ããééã空ããŠå詊è¡ããŸãã
ãžã§ãçšã®è¡šãäœæãã
ãŸãããžã§ãã管çãã 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ã€ãŸã§
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åãã«ããŸãã
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 ã ãã«éå®ããŸãã
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 ã«ãããããåãäžä»£ã®ãšãã ãæŽæ°ããŸãã
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 ã䜿ã£ãã¹ã±ãžã¥ãŒã«å®è¡ã«ã€ããŠã¯ã以äžã®èšäºã§è§£èª¬ããŠããŸãã
ãŸãã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 ã«ãããšãäºéã«å®è¡ãããŠãçµæãå¢ããŸããã
è€æ°ã®ã¯ãŒã«ãŒãåæã«åãç°å¢ã§ããžã§ããäºéã«åŠçããªãä»çµã¿ãäœãããæ¹ã«ãšã£ãŠåèã«ãªãã°å¹žãã§ãã