Cron + Database パターン:キューを使わないバックグラウンドジョブ
フルなキューを導入せずに、リトライ、ロック、冪等性を備えたスケジュール済みバックグラウンドジョブを実行するための cron + database パターンを学びます。

問題:追加インフラなしでスケジュール処理を行う
ほとんどのアプリは、後で実行する仕事や定期的に動かす処理が必要です:フォローアップメールの送信、夜間の請求チェック、古いレコードのクリーンアップ、レポートの再生成、キャッシュの更新など。
初期段階では、背景処理は「キューを導入するのが正解」と感じて、フルなキューシステムを入れたくなります。しかしキューは別のサービスを運用・監視・デプロイ・デバッグする必要があり、特に小さなチームや創業者1人だと余計な負担になります。
そこで本当に問うべきは:追加のインフラを立てずに、どうやって信頼性のあるスケジュール処理を実行するか、です。
よくある最初の試みは単純です:cronでエンドポイントを叩き、そのエンドポイントが仕事を実行する。うまくいくこともありますが、うまくいかなくなると厄介です。サーバーが複数台になったり、デプロイのタイミングが悪かったり、ジョブが予想より長くかかると、混乱した障害が出始めます。
スケジュールされた仕事はたいてい、次のような分かりやすい失敗で壊れます:
- 二重実行:2台のサーバーが同じタスクを両方実行してしまい、請求書が2重に生成されたりメールが重複送信されたりする。
- 実行漏れ:デプロイ中にcron呼び出しが失敗して、ユーザーからの不満で初めて気づく。
- 無音の失敗:ジョブが一度エラーを起こして以降、再試行の仕組みがないため二度と実行されない。
- 部分的な作業完了:ジョブが途中でクラッシュしてデータが中途半端な状態に残る。
- 監査証跡がない:最後にいつ実行されたか、昨夜何が起きたか答えられない。
cron + database パターンはその中間の道です。cronで「起床」だけを行い、ジョブの意図と状態はデータベースに保存します。データベースが調整、再試行、実行履歴の記録を担います。
データベースが既に1つ(多くは PostgreSQL)あり、ジョブの種類が少なく、最低限の運用で予測可能な挙動が欲しい場合に適しています。React + Go + PostgreSQL のようなモダンなスタックで素早く構築したアプリにも自然に合います。
逆に、非常に高いスループットが必要な場合、進行状況をストリームする長時間ジョブ、数多くのジョブタイプ間で厳密な順序付けが必要な場合、あるいは大量のファンアウト(毎分何千というサブタスク)が必要な場合は、専用のキューとワーカーの方が向きます。
コアアイデア(平易に)
cron + database パターンは、フルなキューシステムを立てずにスケジュール処理を実行する方法です。cron(または任意のスケジューラ)は使いますが、cronが何を実行するかは決めません。cronは頻繁に(分毎など)ワーカーを起こすだけで、データベースがどの仕事が期限切れかを判断し、1つだけを確実に取り出します。
ホワイトボードに張られた共有のチェックリストのように考えてください。cronは毎分部屋に入って「今やるべきことある?」と聞く人で、データベースは何が期限で、誰が取ったか、何が終わったかを示すホワイトボードです。
主要な要素は単純です:
- 単一のスケジューラトリガーが頻繁に実行される。
- ジョブを表すテーブルが「何をいつやるか(due time)」、ステータス、試行回数を保持する。
- 1つ以上のワーカーがテーブルをポーリングし、ジョブをクレームして作業を実行する。
- クレームはデータベースのロック(リース)で行い、複数のワーカーが同じ行を取れないようにする。
- データベースが何が実行されたか、失敗したか、再試行すべきかの真実の源になる。
例:毎朝請求リマインダーを送る、キャッシュを10分ごとに更新する、古いセッションを夜間にクリーンアップする。個別のcronコマンドを3つ作る代わりに、ジョブを1か所に保存します。cronは同じワーカープロセスを起動し、ワーカーがPostgresに「今実行すべきものは?」と尋ね、Postgresが1件ずつ安全にジョブをクレームさせます。
この方式は段階的にスケールします。最初は1台のサーバー上で1つのワーカーから始められ、後で複数サーバーに5つのワーカーを走らせても契約(テーブル)が同じままです。
マインドセットの切り替えは簡単です:cronは目覚ましでしかなく、データベースが交通整理をして実行の可否を決め、何が起きたかの履歴を残す役目です。
ジョブテーブルの設計(実用スキーマ)
このパターンは、データベースを「いつ何を実行すべきか」「最後に何が起きたか」の真実の源にすることで最も効果を発揮します。スキーマ自体は派手ではありませんが、ロック用フィールドや適切なインデックスといった小さな配慮が負荷増加時に大きな差を生みます。
1テーブルにするか2テーブルにするか
よくある2つのアプローチ:
- 1つの統合テーブル:各ジョブの最新状態だけ気にする場合(シンプルで結合が少ない)。
- 2つのテーブル:ジョブの定義(what)と実行履歴(each run)を分離する場合(履歴が残り、デバッグが楽)。
頻繁に失敗をデバッグする見込みがあるなら履歴を残しましょう。最小構成で始めたいなら1テーブルで始め、あとで履歴を追加するのも良いです。
実用的なスキーマ(2テーブル版)
以下はPostgreSQL向けのレイアウトです。GoでPostgreSQLを使う場合、これらの列は構造体に素直にマッピングできます。
-- What should exist (the definition)
create table job_definitions (
id bigserial primary key,
job_type text not null,
payload jsonb not null default '{}'::jsonb,
schedule text, -- optional: cron-like text if you store it
max_attempts int not null default 5,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
-- What should run (each run / attempt group)
create table job_runs (
id bigserial primary key,
definition_id bigint references job_definitions(id),
job_type text not null,
payload jsonb not null default '{}'::jsonb,
run_at timestamptz not null,
status text not null, -- queued | running | succeeded | failed | dead
attempts int not null default 0,
max_attempts int not null default 5,
locked_by text,
locked_until timestamptz,
last_error text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
いくつか後で助けになる細かな点:
- job_type は
send_invoice_emailsのように短い文字列にしてルーティングしやすくする。 - payload は
jsonbにして、マイグレーションなしで柔軟に進化できるようにする。 - run_at は「次に実行すべき時刻」。cron(あるいはスケジューラ)が設定し、ワーカーが消費する。
- locked_by と locked_until はワーカーが互いに干渉しないようにジョブをクレームするために使う。
- last_error は管理画面で見せられる短く分かりやすいメッセージを入れる。スタックトレースは別に保管してもよい。
欲しいインデックス
インデックスがないとワーカーが不要なスキャンをすることになります。最低限:
- 期限切れの仕事を速く見つけるためのインデックス:
(status, run_at) - 期限切れリースを検出するためのインデックス:
(locked_until) - 任意:アクティブな作業だけに効く部分インデックス(例:
statusがqueuedやfailedの場合)
これらでテーブルが大きくなっても「次に実行可能なジョブを探す」クエリが速くなります。
安全にロックしてジョブをクレームする
目標は単純:複数のワーカーが走っても特定のジョブを1つしか取らないようにすること。複数のワーカーが同じ行を処理すると、二重メールや二重課金、データの混乱が起きます。
安全な方法は、ジョブのクレームを「リース(lease)」として扱うことです。ワーカーは短い間だけジョブをロックします。ワーカーがクラッシュしたらリースが切れて別のワーカーがそのジョブを取れます。これが locked_until の役目です。
クラッシュで永遠にブロックしないようリースを使う
リースがないと、ワーカーがジョブをロックしたままアンロックされない(プロセスが殺された、サーバー再起動、デプロイ失敗など)ことがあります。locked_until を使えば時間経過でジョブが再び利用可能になります。
典型ルールは、locked_until が NULL か locked_until \u003c= now() のときにジョブをクレームできる、というものです。
ジョブのクレームは1つの原子更新で行う
重要な点は、ジョブのクレームを単一のステートメント(またはトランザクション)で行うことです。データベースに審判を任せたいからです。
以下はPostgreSQLでよく使われるパターン:期限切れのジョブを1件選んでロックし、ワーカーに返す(この例は単一の jobs テーブルを使っていますが、job_runs でも同様です)。
WITH next_job AS (
SELECT id
FROM jobs
WHERE status = 'queued'
AND run_at \u003c= now()
AND (locked_until IS NULL OR locked_until \u003c= now())
ORDER BY run_at ASC
LIMIT 1
FOR UPDATE SKIP LOCKED
)
UPDATE jobs j
SET status = 'running',
locked_until = now() + interval '2 minutes',
locked_by = $1,
attempts = attempts + 1,
updated_at = now()
FROM next_job
WHERE j.id = next_job.id
RETURNING j.*;
なぜこれが機能するか:
FOR UPDATE SKIP LOCKEDにより複数ワーカーが競合してもお互いを待ち合わせずに進める。- クレーム時にリースが設定されるので他のワーカーはその間無視する。
RETURNINGでそのレースに勝ったワーカーに行が渡される。
リースはどれくらいにし、どう更新するか?
リースは通常の実行時間より長めに、しかしクラッシュからの回復が速くなる程度に短めに設定します。多くのジョブが10秒で終わるなら2分のリースで十分です。
長時間かかるタスクは、処理中にリースを延長(ハートビート)してください。単純な方法:30秒ごとに locked_until を延長する。
- リース長:典型的なジョブ時間の5倍〜20倍
- ハートビート間隔:リースの1/4〜1/2
- 更新時は
WHERE id = $job_id AND locked_by = $worker_idを付ける
最後の条件が重要です。自分が所有していないジョブのリースを他人が伸ばすのを防ぎます。
予測可能に動くリトライとバックオフ
リトライは、このパターンが落ち着いて見えるか、騒がしい混乱になるかを決めるポイントです。目標はシンプル:ジョブが失敗したら、後で分かりやすく、測定可能で、止められる形で再試行すること。
まずジョブ状態を明示的かつ有限にします:queued, running, succeeded, failed, dead。実務では failed を「失敗したが再試行する」、dead を「失敗して再試行を諦める」と使い分けるのが一般的です。これにより無限ループを防ぎます。
試行回数のカウントも重要です。attempts(試行回数)と max_attempts(許容最大試行回数)を保存します。ワーカーがエラーをキャッチしたら:
attemptsを増やすattempts \u003c max_attemptsの場合はfailedに設定、さもなければdeadにする- 次の試行用に
run_atを計算して設定(failedの場合のみ)
バックオフは次の run_at を決めるルールです。1つ選んで文書化し、一貫して運用しましょう:
- 固定遅延:常に1分待つ
- 指数関数的:1m, 2m, 4m, 8m
- 上限つき指数:指数で増えるが最大30分などで止める
- ジッターを加える:少しランダム化して全ジョブが同じ秒に再試行しないようにする
依存サービスが落ちて復帰したときにジッターが重要になります。ジッターがないと大量のジョブが一斉に再試行してまた失敗します。
失敗を可視化してデバッグできるだけのエラー情報は必須です。フルなロギングシステムは要らなくても最低限:
last_error(管理画面で見せられる短いメッセージ)error_codeやerror_type(グルーピングに便利)failed_atとnext_run_at- 任意で
last_stack(サイズを制御できるなら)
実用的なルール:10回の試行で dead にし、バックオフはジッター付き指数にする、という方針はトランジェントな失敗を再試行させつつ、壊れたジョブが永遠にCPUを浪費するのを防ぎます。
冪等性:ジョブが繰り返されても重複を防ぐ
冪等性とは、ジョブを2回実行しても最終的に同じ結果になることを意味します。このパターンでは、同じ行がクラッシュやタイムアウト、再試行で何度も拾われる可能性があるので重要です。例えば「請求書メールを送る」ジョブは2回実行すると問題になります。
実践的な考え方は、各ジョブを (1) 作業を行うフェーズ と (2) 効果を適用するフェーズ に分けることです。効果(メール送信や支払いなど)は一度だけ行われるようにします。
ビジネスイベントに紐づく冪等キーを使う
冪等キーはワーカーの試行ごとに変わるものではなく、ジョブが表すビジネスイベントから導きます。良いキーは安定して説明しやすいものです:invoice_id、user_id + day、report_name + report_date など。同じ実世界イベントを指すジョブ試行は同じキーを持つべきです。
例:「2026-01-14 の日次売上レポート」は sales_report:2026-01-14、「請求書 812 の課金」は invoice_charge:812 のようなキーにできます。
データベース制約で「一度だけ」を保証する
最も単純な防護は PostgreSQL に重複を拒否させることです。冪等キーをインデックス可能な場所に保存し、一意制約を付けます。
-- Example: ensure one logical job/effect per business key
ALTER TABLE jobs
ADD COLUMN idempotency_key text;
CREATE UNIQUE INDEX jobs_idempotency_key_uniq
ON jobs (idempotency_key)
WHERE idempotency_key IS NOT NULL;
この制約により、同じキーを持つ行が同時に複数存在することを防げます。もし履歴用に複数行を許す設計なら、効果を記録する別テーブル(例:sent_emails(idempotency_key) や payments(idempotency_key))に一意制約を置きます。
保護すべき一般的な副作用:
- メール:送信前に
sent_emailsテーブルにユニークキーで行を作る、あるいは送信後にプロバイダのメッセージIDを記録する。 - Webhook:
delivered_webhooks(event_id)を保存して既にあるならスキップする。 - 支払い:支払いプロバイダの冪等機能を使いつつ、自分のデータベースでも一意キーを持つ。
- ファイル書き出し:一時名で書いてからリネームする、または
(type, date)でキー付けしたfile_generatedレコードを使う。
Postgresを使ったスタック(例:Go + PostgreSQL)なら、これらの一意チェックは高速でデータに近い場所に置けます。要点は簡単:再試行は普通のこと、重複は制御する、です。
ステップバイステップ:最小ワーカーとスケジューラを作る
退屈なランタイムを1つ決めてそれに固執してください。cron + database パターンは動くパーツを減らすことが目的なので、PostgreSQLに接続する小さなGo、Node、Pythonのプロセスで十分なことが多いです。
5つの小さなステップで作る
-
テーブルとインデックスを作る。
jobsテーブル(とあとで使う参照テーブル)を作り、run_atにインデックスを付け、ワーカーが利用可能なジョブを速く見つけられるよう(status, run_at)のようなインデックスを追加する。 -
小さなenqueue関数を書く。 アプリは
run_atをnowか将来時刻にして行を挿入する。payloadは小さく(IDやジョブタイプだけ)して、巨大な BLOB を入れない。
INSERT INTO jobs (type, payload, status, run_at, attempts, max_attempts)
VALUES ($1, $2::jsonb, 'queued', $3, 0, 10);
- クレームループを実装する。 トランザクション内で動かす。期限切れのジョブを数件選び、他のワーカーがスキップするようロックして、同じトランザクションで
runningにする。
WITH picked AS (
SELECT id
FROM jobs
WHERE status = 'queued' AND run_at \u003c= now()
ORDER BY run_at
FOR UPDATE SKIP LOCKED
LIMIT 10
)
UPDATE jobs
SET status = 'running', started_at = now()
WHERE id IN (SELECT id FROM picked)
RETURNING *;
-
処理と最終化。 クレームした各ジョブについて作業を実行し、成功なら
doneとしてfinished_atを更新する。失敗したらエラーメッセージを記録してqueuedに戻し(バックオフ付きでrun_atを更新)、max_attemptsを超えたらdeadにする。最終化更新は小さく保ち、プロセスが終了する際にも必ず実行する。 -
説明できるリトライ規則を追加。 例えば
run_at = now() + (attempts^2) * interval '10 seconds'のような単純な式を使い、max_attemptsを超えたらstatus = 'dead'にする。
基本的な可視化を追加
初日からフルダッシュボードは要りませんが、問題に気づくための最低限は必要です。
- ジョブごとに1行ログを残す:クレーム、成功、失敗、再試行、dead。
- 「dead jobs」や「古い running ジョブ」を見る単純な管理ビューを作る。
- 増加する失敗数をアラートする。
Go + PostgreSQL のスタックなら、単一のワーカーバイナリと cron で素直にマッピングできます。
そのまま使える現実的な例
小さなSaaSアプリを想像してください。スケジュール処理は2つだけ:
- 夜間のクリーンアップ(期限切れセッションやテンポラリファイルの削除)。
- 毎週月曜朝にユーザーごとに送る「活動レポート」メール。
シンプルに保つ:ジョブを保持する1つのPostgreSQLテーブルと、1分ごとに起動されるワーカー(cronでトリガー)を用意します。ワーカーは期限切れのジョブをクレームして実行し、成功か失敗かを記録します。
何をいつエンキューするか
ジョブは複数の場所からエンキューできます:
- 毎日02:00に1つの
cleanup_nightlyジョブをその日のためにエンキュー。 - サインアップ時に、そのユーザーの次の月曜に
send_weekly_reportをエンキュー。 - イベント後(例:「ユーザーがレポートをExportした」)に、特定の期間について即時に
send_weekly_reportをエンキュー。
payload はワーカーに最低限必要な情報だけにしておくと、再試行が楽になります。
{
"type": "send_weekly_report",
"payload": {
"user_id": 12345,
"date_range": {
"from": "2026-01-01",
"to": "2026-01-07"
}
}
}
冪等性で重複送信を防ぐ方法
ワーカーが最悪のタイミングでクラッシュすることはあり得ます:メールを送った直後に落ちて、ジョブを「done」にマークする前に再起動すると、同じジョブをもう一度処理するかもしれません。
二重送信を防ぐには、業務的に自然な重複排除キーを与え、データベースでそれを強制する場所に保存します。週次レポートならキーは (user_id, week_start_date) が良いでしょう。送信前に「報告書Xを送る」と記録し、そのレコードが既にあれば送信をスキップします。
これは sent_reports テーブルに (user_id, week_start_date) の一意制約を付けるか、ジョブ自体に一意の idempotency_key を持たせるだけで実現できます。
障害の例と回復の流れ
メールプロバイダがタイムアウトしたとします。ワーカーは:
attemptsを増やす- デバッグ用にエラーメッセージを保存する
- バックオフで次回をスケジュールする(例:+1分、+5分、+30分、+2時間)
もし規定回数(例:10回)を超えても失敗し続けるなら、そのジョブは dead にして再試行を止めます。ジョブは一度成功するか、明確なスケジュールで再試行され、冪等性があれば再試行は安全です。
よくある間違いと落とし穴
cron + database パターンは単純ですが、小さなミスが重複、スタック、想定外の負荷を招きます。最初のクラッシュ、デプロイ、トラフィックスパイクで問題が顕在化します。
重複やスタックを招くミス
現場で起きるインシデントの多くは次の罠から来ます:
- 同じジョブを複数のcronエントリから起動しているのにリースがない。複数サーバーが同じ分に起動すると、クレーム手順が原子的にロックを設定しない限り両方が実行してしまう。
locked_untilを使わない。ワーカーがクレームした後にクラッシュするとその行が永遠に「進行中」になりうる。リースのタイムスタンプがあれば他のワーカーが安全に回収できる。- 失敗時に即時再試行する。APIが落ちていると即時再試行がスパイクを生み、レート制限に引っかかって失敗し続ける。必ず次回を未来にスケジュールする。
- 「少なくとも一度(at least once)」を「ちょうど一度(exactly once)」と扱う。タイムアウトやワーカー再起動でジョブは2回実行される可能性がある。2回実行すると問題になる処理には冪等性を付ける。
- ジョブ行に大きなペイロードを保存する。巨大なJSONはテーブルを膨らませ、インデックスを遅くし、ロックの負荷を増やす。
user_idやinvoice_idのような参照を保存し、実行時に必要なデータを取得する。
例:週次請求メールを送るとき、送信後にジョブを done にマークする前にタイムアウトすると、同じジョブが再実行され二重送信になる。これはこのパターンでは普通に起こり得るため、メール送信の一意チェック(例えば invoice_id に基づく email_sent レコード)を入れる必要がある。
あまり明白でない落とし穴
スケジューリングと実行を同じ長時間トランザクションで混ぜないでください。ネットワーク呼び出し中にトランザクションを開いたままにすると、ロックが長く保持され他のワーカーがブロックされます。
マシン間の時計差にも注意。run_at や locked_until の基準時刻はアプリサーバの時計ではなくデータベース時刻(PostgreSQLの NOW())を使う。
最大実行時間を明確に設定する。ジョブが30分かかるならリースはそれより長くし、必要なら更新する。さもないと別のワーカーが途中でそのジョブを拾ってしまう。
ジョブテーブルを健全に保つ。完了したジョブが永遠に溜まるとクエリが遅くなりロック競合が増える。古い行をアーカイブまたは削除する単純な保持ルールを決めておく。
クイックチェックリストと次の一手
クイックチェックリスト
本番投入前に次を確認してください。ここを怠るとスタックや重複、DBに負荷をかけることになります。
- ジョブテーブルに必須項目がある:
run_at,status,attempts,locked_until,max_attempts(およびlast_error等で何が起きたか見えるように)。 - 各ジョブは2回実行されても安全である。確信が持てないなら冪等キーや副作用に対する一意ルールを追加する(例:invoice_idごとに1つ)。
- 障害を観察して対処する場所がある:失敗ジョブを見て再実行したり、ジョブを
deadにしたりできる。 - リース(ロック)タイムアウトが作業に対して妥当である。通常の実行より長めだが、クラッシュが進捗を阻害しない程度に短い。
- 再試行バックオフは予測可能で、繰り返しの失敗を抑制し
max_attemptsを超えたら停止する。
これらが満たされていれば、cron + database パターンは実用的なワークロードに十分耐えられます。
次の一手
チェックリストが整ったら、日常運用に注力します。
- 「今すぐ再試行」(
run_at = now()にしてロックをクリア)と「キャンセル」(ターミナルステータスに移す)の2つの管理操作を追加する。インシデント時に役立ちます。 - ワーカーがジョブごとに1行ログを出すようにする:ジョブタイプ、ジョブID、試行回数、結果。失敗数の増加でアラートを上げる。
- 実際のスパイクを想定した負荷テストを行う:同じ分に大量のジョブがスケジュールされる場合を再現する。クレームが遅くなるなら正しいインデックス(たいていは
status, run_at)を追加する。
このようなセットアップを素早く作りたいなら、Koder.ai (koder.ai) はスキーマからデプロイ済みの Go + PostgreSQL アプリまで、ロッキング、リトライ、冪等性のルールに注力しながら手助けできます。
将来的にこの構成を使い続けられなくなっても、ジョブライフサイクルについての知見は残り、同じ考え方はフルなキューシステムにも自然に移行できます。
よくある質問
バックグラウンドジョブで cron とデータベースを使うのはどのような場合ですか?
すでにデータベースがあり、ジョブの種類がそれほど多くなく、別途キューを運用せずに定期実行したい場合に、このパターンを使います。メールのリマインダー、クリーンアップ、レポート、キャッシュ更新といったタスクに向いています。
この構成で cron は何をしますか?
Cron は、通常は1分ごとなど、一定間隔でワーカーを起動する役割だけを担います。実行期限を迎えたジョブの判定、状態の記録、各ジョブを1つのワーカーだけが取得できるようにする処理は、データベースが担います。
jobs テーブルにはどのような項目を含めるべきですか?
ジョブの種類、小さな JSON ペイロード、実行予定時刻、ステータス、試行回数、再試行上限、ロック所有者、ロックの有効期限、短いエラーメッセージを保存します。これらの項目があれば、ワーカーは安全にジョブを実行し、障害から復旧できます。
複数のワーカーは同じジョブを実行しないよう、どう防ぎますか?
PostgreSQL の FOR UPDATE SKIP LOCKED のような行ロックを使い、1つのデータベーストランザクション内でジョブを取得します。ワーカーが実際のタスクを始める前に、行の状態を実行中に更新し、リースを設定します。
バックグラウンドジョブにリースのタイムアウトが必要なのはなぜですか?
リースは locked_until に記録する一時的なロックです。ワーカーがクラッシュしてもリースが期限切れになり、ジョブが永久に止まるのではなく、別のワーカーが引き継げます。
失敗したジョブはどのように再試行すべきですか?
1分後、2分後、4分後、その後は少しランダム性を加えた上限付きの遅延といった、明確なバックオフルールで後から再試行します。許可された試行回数を超えたら、ジョブを dead としてマークし、ワーカーの時間を使い続けないようにします。
冪等性とは何ですか? また、なぜ重要ですか?
副作用を繰り返しても安全な設計にします。たとえば、請求書 ID やユーザー ID とレポート日を組み合わせたものなど、安定した冪等性キーを使い、PostgreSQL または外部プロバイダー側で一意性を保証します。
このパターンは厳密に1回だけ処理されることを保証しますか?
いいえ。メール送信や決済プロバイダーの呼び出し後、成功を保存する前にワーカーがクラッシュすることがあります。少なくとも1回は処理される前提で設計し、冪等性によって重複した副作用を防ぎます。
ワーカーはジョブの実行中もデータベーストランザクションを保持すべきですか?
データベーストランザクションは短く保ちます。ジョブを取得してコミットし、トランザクションの外でネットワーク呼び出しを行い、その後に別の更新で成功または失敗を保存します。長いトランザクションはロックを保持し、他のワーカーを遅らせます。
cron とデータベースジョブはどのように監視しますか?
dead になったジョブと長時間実行中のジョブを追跡し、各取得と結果を記録し、失敗が増えたらアラートを出します。ジョブをすぐに再試行する、キャンセルする、最新のエラーを確認するといったシンプルな管理操作も追加します。