定期ジョブでは「普段はすぐ処理したいが、深夜や早朝に発生したものだけ朝まで待たせたい」という要件があります。

専用のスケジューラを増やさなくても、キューテーブルにsend_afterのような実行可能時刻を1列持たせるだけで、かなり単純に実装できます。

1、キューへ実行可能時刻を持たせる

基本となる列は次のようなものです。

CREATE TABLE job_queue (
    no           BIGSERIAL PRIMARY KEY,
    payload      TEXT NOT NULL,
    created_at   TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    send_after   TIMESTAMP NOT NULL,
    completed_at TIMESTAMP
);

send_afterは「この時刻以降なら処理してよい」という意味にします。

2、登録時に即時か遅延かだけ決める

たとえば、00:00から06:29までに発生したジョブは06:30まで待ち、それ以外はすぐ実行するとします。

IF now_local::time < TIME '06:30:00' THEN
    scheduled_at := date_trunc('day', now_local)
                  + INTERVAL '6 hours 30 minutes';
ELSE
    scheduled_at := now_local;
END IF;

ここでやっているのは、複雑なスケジュール管理ではなく、レコードへ実行可能時刻を書き込むだけです。

3、workerはsend_afterを過ぎたものだけ読む

cronはこれまで通り一定間隔で動かします。worker側では、現在時刻を過ぎたものだけ取得します。

SELECT no, payload
  FROM job_queue
 WHERE completed_at IS NULL
   AND send_after <= CURRENT_TIMESTAMP
 ORDER BY send_after, no
 LIMIT 1;

この方法なら、早朝専用のcronを別に増やす必要はありません。

4、ORDER BYはsend_afterを先にする

遅延対象のレコードが古い連番を持っている場合、単純にORDER BY noだけで取得すると、まだ処理できないレコードが先頭に居座って後続処理を塞ぐ設計になりがちです。

取得条件でsend_after <= nowを入れたうえで、ORDER BY send_after, noにすると分かりやすくなります。

5、タイムゾーンを明示する

サーバーOS、DB、アプリケーションでタイムゾーン設定が異なる可能性がある場合、深夜帯の判定は特に注意が必要です。

PostgreSQLなら、業務上の基準時刻を明示して計算しておくと事故を減らせます。

CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Tokyo'

UTCで保存する設計でも構いませんが、「どのタイムゾーンを基準に06:30とするのか」はコード上ではっきりさせます。

6、送信先や設定は実行直前に確認する

通知処理の場合、キューへ積んだ時点と実際の送信時点でユーザー設定が変わることがあります。

早朝にイベントが発生し、数時間後に送信するのであれば、宛先の展開や配信可否の最終確認は送信直前に行うほうが自然です。

7、1列追加するだけで運用を変えやすい

send_after方式の良いところは、処理する側をほとんど変えずに「今すぐ」「少し待つ」を同じキューで扱えることです。

将来ルールが変わっても、登録時の時刻計算を変更するだけで対応しやすく、専用cronを増やし続ける構成より見通しを保ちやすくなります。