7 menit

Pola cron + database: pekerjaan latar terjadwal tanpa antrean

Pelajari pola cron + database untuk menjalankan background jobs terjadwal dengan retry, locking, dan idempotensi — tanpa perlu menyiapkan sistem antrean penuh.

Pola cron + database: pekerjaan latar terjadwal tanpa antrean

Masalah: pekerjaan terjadwal tanpa infrastruktur tambahan

Kebanyakan aplikasi butuh pekerjaan yang berjalan nanti atau terjadwal: mengirim email follow-up, menjalankan pengecekan billing malam hari, membersihkan record lama, membangun ulang laporan, atau menyegarkan cache.

Awalnya sering terasa menggoda untuk menambahkan sistem antrean penuh karena terasa seperti cara “benar” untuk mengerjakan background jobs. Tapi antrean menambah bagian yang bergerak: layanan lain untuk dijalankan, dipantau, di-deploy, dan di-debug. Untuk tim kecil (atau pendiri tunggal), beban ekstra itu bisa memperlambat Anda.

Jadi pertanyaannya: bagaimana menjalankan pekerjaan terjadwal secara andal tanpa menambah infrastruktur?

Upaya pertama yang umum sederhana: tambahkan entri cron yang memanggil endpoint, lalu endpoint itu mengerjakan tugasnya. Cara ini berhasil sampai tidak lagi. Begitu Anda memiliki lebih dari satu server, deploy di waktu yang salah, atau job yang berjalan lebih lama dari perkiraan, Anda mulai melihat kegagalan yang membingungkan.

Pekerjaan terjadwal biasanya gagal dalam beberapa cara yang bisa diprediksi:

  • Double runs: dua server menjalankan tugas yang sama, sehingga faktur digenerate dua kali atau email terkirim dua kali.
  • Lost runs: panggilan cron gagal saat deploy dan tidak ada yang menyadarinya sampai pengguna mengeluh.
  • Silent failures: job error sekali, lalu tak pernah dijalankan lagi karena tidak ada rencana retry.
  • Partial work: job crash di tengah jalan dan meninggalkan data dalam keadaan aneh.
  • No audit trail: Anda tidak bisa menjawab “kapan terakhir dijalankan?” atau “apa yang terjadi tadi malam?”.

Pola cron + database adalah jalan tengah. Anda masih menggunakan cron untuk “membangunkan” worker sesuai jadwal, tapi Anda menyimpan intent job dan state job di database sehingga sistem bisa mengoordinasikan, me-retry, dan merekam apa yang terjadi.

Ini cocok ketika Anda sudah memiliki satu database (sering PostgreSQL), beberapa tipe job, dan menginginkan perilaku yang dapat diprediksi dengan pekerjaan operasi minimal. Ini juga pilihan alami untuk aplikasi yang dibangun cepat pada stack modern (misalnya React + Go + PostgreSQL).

Ini tidak cocok jika Anda butuh throughput sangat tinggi, job yang berjalan lama dan harus men-stream progres, ordering ketat di banyak tipe job, atau fan-out berat (ribuan sub-task per menit). Dalam kasus itu, antrean nyata dan worker khusus biasanya lebih menguntungkan.

Ide inti dengan bahasa sederhana

Pola cron + database menjalankan pekerjaan background terjadwal tanpa menjalankan sistem antrean penuh. Anda tetap menggunakan cron (atau scheduler apa pun), tapi cron tidak memilih apa yang akan dijalankan. Cron hanya membangunkan worker dengan frekuensi tertentu (sekali per menit umum). Database yang memutuskan kerja mana yang jatuh tempo dan memastikan hanya satu worker yang mengambil setiap job.

Bayangkan seperti daftar tugas bersama di papan tulis. Cron adalah orang yang masuk ke ruangan setiap menit dan berkata, “Ada yang perlu dikerjakan sekarang?” Database adalah papan tulis yang menunjukkan apa yang jatuh tempo, apa yang sudah diambil, dan apa yang selesai.

Komponennya sederhana:

  • Pemicu scheduler tunggal berjalan sering.
  • Tabel jobs menyimpan “apa” dan “kapan” (waktu jatuh tempo), juga status dan jumlah percobaan.
  • Satu atau beberapa worker mem-poll tabel, mengklaim job, dan mengerjakannya.
  • Pengklaiman menggunakan lock di database sehingga dua worker tidak bisa mengambil baris yang sama.
  • Database tetap sumber kebenaran tentang apa yang dijalankan, apa yang gagal, dan apa yang harus di-retry.

Contoh: Anda ingin mengirim pengingat faktur setiap pagi, menyegarkan cache setiap 10 menit, dan membersihkan sesi lama setiap malam. Alih-alih tiga perintah cron terpisah (masing-masing dengan mode overlap dan kegagalan), Anda menyimpan entri job di satu tempat. Cron memulai proses worker yang sama. Worker menanyakan Postgres, “Apa yang jatuh tempo sekarang?” dan Postgres menjawab dengan melegalkan worker untuk secara aman mengklaim tepat satu job pada satu waktu.

Ini bisa diskalakan secara bertahap. Anda bisa mulai dengan satu worker di satu server. Nanti, jalankan lima worker di beberapa server. Kontrak tetap sama: tabel adalah kontrak.

Perubahan pola pikirnya sederhana: cron hanya panggilan bangun. Database adalah pengatur lalu lintas yang memutuskan apa yang boleh berjalan, mencatat apa yang terjadi, dan memberi Anda riwayat yang jelas ketika sesuatu salah.

Mendesain tabel jobs (skema praktis)

Pola ini bekerja paling baik ketika database Anda menjadi sumber kebenaran untuk apa yang harus dijalankan, kapan harus dijalankan, dan apa yang terjadi terakhir kali. Skema tidak rumit, tapi detail kecil (field lock dan indeks yang tepat) membuat perbedaan besar saat beban bertambah.

Satu tabel atau dua?

Dua pendekatan umum:

  • Satu tabel gabungan ketika Anda hanya peduli pada state terbaru setiap job (sederhana, lebih sedikit join).
  • Dua tabel ketika Anda ingin pemisahan bersih antara “apa job ini” dan “setiap kali job dijalankan” (riwayat lebih baik, debugging lebih mudah).

Jika Anda sering men-debug kegagalan, simpan riwayat. Jika ingin setup paling kecil, mulai dengan satu tabel dan tambahkan riwayat nanti.

Skema praktis (versi dua-tabel)

Berikut layout yang ramah PostgreSQL. Jika Anda membangun di Go dengan PostgreSQL, kolom-kolom ini bisa dipetakan dengan rapi ke struct.

-- 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()
);

Beberapa detail yang menghemat rasa sakit nanti:

  • Jaga job_type sebagai string pendek yang bisa Anda route (mis. send_invoice_emails).
  • Simpan payload sebagai jsonb supaya bisa berkembang tanpa migrasi.
  • run_at adalah “waktu jatuh tempo berikutnya”. Cron (atau skrip scheduler) mengaturnya, worker mengonsumsinya.
  • locked_by dan locked_until memungkinkan worker mengklaim job tanpa saling menginjak.
  • last_error sebaiknya singkat dan mudah dibaca. Simpan stack trace di tempat lain bila perlu.

Indeks yang Anda butuhkan

Tanpa indeks, worker akan memindai terlalu banyak baris. Mulai dengan:

  • Indeks untuk menemukan kerja yang jatuh tempo cepat: (status, run_at)
  • Indeks untuk membantu mendeteksi lock yang kedaluwarsa: (locked_until)
  • Opsional: indeks parsial untuk pekerjaan aktif saja (misalnya status di queued dan failed)

Ini menjaga query “temukan job berikutnya yang bisa dijalankan” cepat walau tabel tumbuh.

Locking dan mengklaim job dengan aman

Tujuannya sederhana: banyak worker boleh berjalan, tapi hanya satu yang boleh mengambil job tertentu. Jika dua worker memproses baris yang sama, Anda mendapatkan email ganda, biaya ganda, atau data berantakan.

Pendekatan aman adalah memperlakukan klaim job seperti “lease”. Worker menandai job sebagai terkunci untuk jangka waktu pendek. Jika worker crash, lease berakhir dan worker lain bisa mengambilnya. Itulah fungsi locked_until.

Pakai lease agar crash tidak memblokir pekerjaan selamanya

Tanpa lease, worker bisa mengunci job dan tidak pernah membuka (process killed, server reboot, deploy bermasalah). Dengan locked_until, job tersedia lagi saat waktu berlalu.

Aturan umum: job dapat diklaim saat locked_until bernilai NULL atau locked_until <= now().

Klaim job dengan satu update atomik

Detail penting: klaim job harus dilakukan dalam satu pernyataan (atau satu transaksi). Anda ingin database menjadi hakim.

Berikut pola PostgreSQL umum: pilih satu job yang jatuh tempo, kunci, dan kembalikan ke worker. (Contoh ini memakai satu tabel jobs; ide yang sama berlaku jika Anda mengklaim dari job_runs.)

WITH next_job AS (
  SELECT id
  FROM jobs
  WHERE status = 'queued'
    AND run_at <= now()
    AND (locked_until IS NULL OR locked_until <= 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.*;

Mengapa ini bekerja:

  • FOR UPDATE SKIP LOCKED memungkinkan banyak worker bersaing tanpa saling memblokir.
  • Lease di-set saat klaim, sehingga worker lain mengabaikannya sampai kedaluwarsa.
  • RETURNING menyerahkan baris ke worker yang menang kompetisi.

Berapa lama lease harusnya, dan bagaimana memperbaruinya?

Set lease lebih lama dari durasi normal run, tapi cukup pendek agar crash pulih cepat. Jika kebanyakan job selesai dalam 10 detik, lease 2 menit sudah memadai.

Untuk tugas panjang, perpanjang lease saat bekerja (heartbeat). Pendekatan sederhana: setiap 30 detik, perpanjang locked_until jika Anda masih punya job itu.

  • Panjang lease: 5x sampai 20x waktu job tipikal Anda
  • Interval heartbeat: 1/4 sampai 1/2 dari lease
  • Pembaruan renewal harus menyertakan WHERE id = $job_id AND locked_by = $worker_id

Kondisi terakhir itu penting. Ia mencegah worker memperpanjang lease pada job yang tidak lagi dimilikinya.

Retry dan backoff yang bisa diprediksi

Earn credits while you build
Buat konten atau referensikan rekan dan dapatkan kredit untuk melanjutkan pembangunan di Koder.ai.

Retry adalah tempat pola ini terasa tenang atau berubah jadi berantakan. Tujuannya sederhana: ketika job gagal, coba lagi nanti dengan cara yang bisa Anda jelaskan, ukur, dan hentikan.

Mulailah dengan membuat status job eksplisit dan terbatas: queued, running, succeeded, failed, dead. Dalam praktiknya, tim biasanya memakai failed untuk berarti “gagal tapi akan retry” dan dead untuk “gagal dan kami menyerah”. Satu perbedaan itu mencegah loop tak berujung.

Penghitungan percobaan adalah pengaman kedua. Simpan attempts (berapa kali dicoba) dan max_attempts (berapa kali diperbolehkan). Saat worker menangkap error, ia harus:

  • menambah attempts
  • set state ke failed jika attempts < max_attempts, jika tidak dead
  • hitung run_at untuk percobaan berikutnya (hanya untuk failed)

Backoff adalah aturan yang menentukan run_at berikutnya. Pilih satu, dokumentasikan, dan konsisten:

  • Fixed delay: selalu tunggu 1 menit
  • Exponential: 1m, 2m, 4m, 8m
  • Exponential dengan cap: eksponensial tapi tidak lebih dari mis. 30m
  • Tambahkan jitter: acak sedikit supaya job tidak semua retry pada detik yang sama

Jitter penting saat dependency turun lalu kembali. Tanpa jitter, ratusan job bisa retry bersamaan dan gagal lagi.

Simpan detail error yang cukup untuk membuat kegagalan terlihat dan bisa di-debug. Anda tidak perlu sistem logging penuh, tapi butuh hal dasar:

  • last_error (pesan singkat yang aman ditampilkan di admin)
  • error_code atau error_type (membantu pengelompokan)
  • failed_at dan next_run_at
  • opsional last_stack (hanya jika Anda bisa mengontrol ukurannya)

Aturan konkret yang sering bekerja: tandai job dead setelah 10 percobaan, dan gunakan backoff eksponensial dengan jitter. Itu menjaga kegagalan sementara terus dicoba, tapi menghentikan job rusak menghabiskan CPU selamanya.

Idempotensi: mencegah duplikat meskipun job diulang

Idempotensi berarti job Anda bisa dijalankan dua kali dan tetap menghasilkan hasil akhir yang sama. Dalam pola ini, itu penting karena baris yang sama mungkin diambil lagi setelah crash, timeout, atau retry. Jika job Anda adalah “kirim email faktur”, menjalankannya dua kali tidak aman.

Cara praktis memikirkannya: bagi setiap job menjadi (1) melakukan pekerjaan dan (2) menerapkan efek. Anda ingin efek terjadi sekali, meskipun pekerjaan dicoba beberapa kali.

Gunakan idempotency key yang terkait event bisnis

Idempotency key harus berasal dari apa yang direpresentasikan job, bukan dari percobaan worker. Kunci yang baik stabil dan mudah dijelaskan, seperti invoice_id, user_id + day, atau report_name + report_date. Jika dua percobaan job merujuk pada event dunia nyata yang sama, mereka harus berbagi kunci yang sama.

Contoh: “Generate daily sales report for 2026-01-14” bisa memakai sales_report:2026-01-14. “Charge invoice 812” bisa memakai invoice_charge:812.

Tegakkan “hanya sekali” dengan constraint database

Pengaman paling sederhana adalah membiarkan PostgreSQL menolak duplikat. Simpan idempotency key di tempat yang bisa diindeks, lalu tambahkan unique constraint.

-- 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;

Ini mencegah dua baris dengan kunci yang sama ada pada saat bersamaan. Jika desain Anda mengizinkan banyak baris (untuk riwayat), letakkan keunikan pada tabel “effects” terpisah, mis. sent_emails(idempotency_key) atau payments(idempotency_key).

Efek samping umum yang perlu dilindungi:

  • Email: buat baris sent_emails dengan kunci unik sebelum mengirim, atau rekam provider message id setelah dikirim.
  • Webhook: simpan delivered_webhooks(event_id) dan lewati jika sudah ada.
  • Pembayaran: selalu gunakan fitur idempotency provider pembayaran plus kunci unik di database Anda.
  • Penulisan file: tulis dengan nama sementara, lalu ganti nama, atau simpan record file_generated yang ter-key oleh (type, date).

Jika Anda membangun di stack berbasis Postgres (mis. backend Go + PostgreSQL), pemeriksaan keunikan ini cepat dan mudah ditempatkan dekat data. Inti idenya sederhana: retry itu normal, duplikat adalah hal yang bisa dicegah.

Langkah demi langkah: bangun worker dan scheduler minimal

Add a jobs admin screen
Hasilkan halaman internal kecil untuk memeriksa failed dan dead jobs dengan aman.

Pilih runtime yang sederhana dan tetap pada itu. Tujuan pola cron + database adalah mengurangi bagian yang bergerak, jadi proses kecil (Go, Node, atau Python) yang berbicara ke PostgreSQL biasanya cukup.

Bangun dalam lima langkah kecil

  1. Buat tabel dan indeks. Tambahkan tabel jobs (plus tabel lookup yang Anda inginkan nantinya), lalu indeks run_at, dan indeks yang membantu worker menemukan job tersedia dengan cepat (mis. pada (status, run_at)).

  2. Tulis fungsi enqueue kecil. Aplikasi Anda harus memasukkan baris dengan run_at di-set ke now atau waktu di masa depan. Jaga payload kecil dan prediktabel (ID dan tipe job, bukan blob besar).

INSERT INTO jobs (type, payload, status, run_at, attempts, max_attempts)
VALUES ($1, $2::jsonb, 'queued', $3, 0, 10);
  1. Implementasikan claim loop. Jalankan dalam transaksi. Pilih beberapa job yang jatuh tempo, kunci agar worker lain melewatinya, dan set menjadi running dalam transaksi yang sama.
WITH picked AS (
  SELECT id
  FROM jobs
  WHERE status = 'queued' AND run_at <= 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 *;
  1. Proses dan finalisasikan. Untuk setiap job yang diklaim, lakukan pekerjaan, lalu update ke done dengan finished_at. Jika gagal, rekam pesan error dan pindahkan kembali ke queued dengan run_at baru (backoff). Buat update finalisasi kecil dan selalu jalankan, bahkan saat proses hendak mati.

  2. Tambahkan aturan retry yang bisa dijelaskan. Pakai formula sederhana seperti run_at = now() + (attempts^2) * interval '10 seconds', dan hentikan setelah max_attempts dengan set status = 'dead'.

Tambahkan visibilitas dasar

Anda tidak perlu dashboard penuh di hari pertama, tapi butuh cukup agar melihat masalah.

  • Log satu baris per job: claimed, succeeded, failed, retried, dead.
  • Buat query atau view admin sederhana untuk “dead jobs” dan “old running jobs”.
  • Alert pada jumlah (mis. lebih dari N dead jobs dalam jam terakhir).

Jika Anda sudah di stack Go + PostgreSQL, ini berkorespondensi langsung ke satu binary worker plus cron.

Contoh realistis yang bisa Anda salin

Bayangkan SaaS kecil dengan dua pekerjaan terjadwal:

  • Pembersihan malam yang menghapus sesi kadaluarsa dan file temporer lama.
  • Email “laporan aktivitas Anda” mingguan yang dikirim ke setiap pengguna setiap Senin pagi.

Jaga sederhana: satu tabel PostgreSQL untuk menampung jobs, dan satu worker yang berjalan setiap menit (dipicu cron). Worker mengklaim job yang jatuh tempo, menjalankannya, dan mencatat sukses atau gagal.

Apa yang di-enqueue, dan kapan

Anda bisa enqueue job dari beberapa tempat:

  • Harian jam 02:00: enqueue satu job cleanup_nightly untuk “hari ini”.
  • Saat signup: enqueue job send_weekly_report untuk monday berikutnya pengguna.
  • Setelah event (mis. “user klik Export report”): enqueue job send_weekly_report yang dijalankan segera untuk rentang tanggal tertentu.

Payload cukup minimum yang dibutuhkan worker. Jaga kecil agar mudah di-retry.

{
  "type": "send_weekly_report",
  "payload": {
    "user_id": 12345,
    "date_range": {
      "from": "2026-01-01",
      "to": "2026-01-07"
    }
  }
}

Bagaimana idempotensi mencegah pengiriman ganda

Worker bisa crash pada momen terburuk: tepat setelah mengirim email, tapi sebelum menandai job sebagai “done”. Saat restart, ia mungkin mengambil job yang sama lagi.

Untuk menghentikan pengiriman ganda, berikan pekerjaan kunci deduplikasi alami dan simpan tempat database bisa menegakkannya. Untuk laporan mingguan, kunci yang baik adalah (user_id, week_start_date). Sebelum mengirim, worker mencatat “saya akan mengirim laporan X”. Jika catatan itu sudah ada, ia melewati pengiriman.

Ini bisa sesederhana tabel sent_reports dengan constraint unik pada (user_id, week_start_date), atau idempotency_key unik pada job itu sendiri.

Seperti apa kegagalan dan bagaimana pulih

Misalnya provider email Anda timeout. Job gagal, sehingga worker:

  • menambah attempts
  • menyimpan pesan error untuk debugging
  • menjadwalkan coba berikutnya dengan backoff (mis. +1 min, +5 min, +30 min, +2 jam)

Jika terus gagal melewati batas (mis. 10 attempts), tandai sebagai “dead” dan hentikan retry. Job akan sukses sekali, atau ia retry menurut jadwal yang jelas, dan idempotensi membuat retry aman.

Kesalahan umum dan jebakan

Build a Postgres job worker
Jelaskan tabel jobs dan loop worker Anda dalam chat lalu hasilkan kerangka aplikasi yang bisa dijalankan.

Pola cron + database sederhana, tapi kesalahan kecil bisa membuatnya menghasilkan duplikat, pekerjaan stuck, atau lonjakan beban. Mayoritas masalah muncul setelah crash pertama, deploy, atau spike lalu lintas.

Kesalahan yang menyebabkan duplikat atau job terjebak

Kebanyakan insiden nyata berasal dari beberapa jebakan:

  • Menjalankan job yang sama dari beberapa entri cron tanpa lease. Jika dua server berdetik pada menit yang sama, keduanya bisa mengklaim pekerjaan yang sama kecuali langkah klaim Anda atomik dan meng-set lock (atau lease) dalam transaksi database yang sama.
  • Mengabaikan locked_until. Jika worker crash setelah mengklaim job, baris itu bisa tetap “in progress” selamanya. Timestamp lease memungkinkan worker lain mengambilnya nanti.
  • Retry instan saat gagal. Ketika API down, retry instan membuat lonjakan, menghabiskan rate limit, dan terus gagal dalam loop rapat. Selalu jadwalkan percobaan berikutnya ke masa depan.
  • Menganggap “at least once” sebagai “exactly once”. Job bisa berjalan dua kali (timeouts, restart worker, masalah jaringan). Jika menjalankan dua kali berbahaya, buat side effect aman untuk diulang.
  • Menyimpan payload besar di baris job. JSON besar menggelembungkan tabel, memperlambat indeks, dan membuat locking lebih berat. Simpan referensi (mis. user_id, invoice_id, atau key file) dan ambil sisanya saat menjalankan.

Contoh: Anda mengirim email faktur mingguan. Jika worker timeout setelah mengirim tapi sebelum menandai job selesai, job yang sama mungkin di-retry dan mengirim email duplikat. Itu normal untuk pola ini kecuali Anda menambahkan pengaman (mis. merekam event “email sent” unik berdasarkan invoice id).

Gotcha yang kurang jelas

Hindari mencampur penjadwalan dan eksekusi dalam transaksi panjang. Jika Anda memegang transaksi terbuka saat melakukan panggilan jaringan, Anda memperpanjang lock lebih lama dari yang diperlukan dan memblokir worker lain.

Perhatikan perbedaan jam antar mesin. Gunakan waktu database (NOW() di PostgreSQL) sebagai sumber kebenaran untuk run_at dan locked_until, bukan jam server aplikasi.

Tetapkan runtime maksimum yang jelas. Jika job bisa memakan 30 menit, buat lease lebih panjang dari itu, dan perpanjang jika perlu. Kalau tidak, worker lain bisa mengambilnya di tengah jalannya.

Jaga tabel job tetap sehat. Jika baris selesai menumpuk selamanya, query melambat dan kontensi lock meningkat. Pilih aturan retensi sederhana (arsipkan atau hapus row lama) sebelum tabel menjadi sangat besar.

Checklist cepat dan langkah berikutnya

Checklist cepat

Sebelum Anda mengirimkan pola ini ke produksi, periksa hal dasar. Kelalaian kecil di sini biasanya berujung pada job terjebak, duplikat mengejutkan, atau worker yang membombardir database.

  • Tabel jobs Anda punya yang penting: run_at, status, attempts, locked_until, dan max_attempts (plus last_error atau serupa agar Anda bisa melihat apa yang terjadi).
  • Setiap job bisa dijalankan dua kali tanpa bahaya. Jika tidak yakin, tambahkan idempotency key atau aturan keunikan di sekitar side effect (mis. satu invoice per invoice_id).
  • Ada tempat jelas untuk mengamati kegagalan dan memutuskan tindakan: lihat failed jobs, jalankan ulang job, atau tandai sebagai dead saat harus berhenti.
  • Timeout lease Anda masuk akal untuk pekerjaan. Harus cukup panjang untuk run normal, tapi cukup pendek agar worker crash tidak memblokir progress selama berjam-jam.
  • Backoff retry bisa diprediksi. Ia harus memperlambat kegagalan berulang, dan berhenti setelah max_attempts.

Jika semua ini benar, pola cron + database biasanya cukup stabil untuk beban nyata.

Langkah berikutnya

Setelah checklist beres, fokus pada operasi sehari-hari.

  • Tambahkan dua aksi admin kecil: “retry now” (set run_at = now() dan bersihkan lock) dan “cancel” (pindah ke status terminal). Ini menghemat waktu saat insiden.
  • Buat worker log satu baris per job: jenis job, id job, nomor attempt, dan hasil. Tambahkan alert pada peningkatan jumlah kegagalan.
  • Load test dengan spike realistis: banyak job dijadwalkan pada menit yang sama. Jika klaim job melambat, tambahkan indeks yang tepat (sering pada status, run_at).

Jika Anda ingin membangun setup semacam ini dengan cepat, Koder.ai (koder.ai) dapat membantu Anda dari skema ke aplikasi Go + PostgreSQL yang dideploy dengan lebih sedikit wiring manual, sementara Anda fokus pada aturan locking, retries, dan idempotensi.

Jika kemudian Anda melebihi kapasitas setup ini, Anda tetap telah mempelajari lifecycle job dengan jelas, dan ide-ide yang sama ini mudah dipetakan ke sistem antrean penuh.

Pertanyaan umum

Kapan saya sebaiknya menggunakan cron dengan database untuk pekerjaan latar belakang?

Gunakan pola ini saat Anda sudah memiliki database, hanya memiliki sejumlah kecil jenis pekerjaan, dan ingin menjalankan pekerjaan terjadwal tanpa mengoperasikan antrean terpisah. Pola ini cocok untuk tugas seperti pengingat email, pembersihan, laporan, dan penyegaran cache.

Apa fungsi cron dalam pengaturan ini?

Cron hanya perlu memulai worker secara berkala, sering kali sekali per menit. Database menentukan pekerjaan mana yang jatuh tempo, mencatat statusnya, dan memungkinkan satu worker mengklaim setiap pekerjaan.

Kolom apa saja yang perlu ada dalam tabel pekerjaan?

Simpan jenis pekerjaan, payload JSON kecil, waktu jatuh tempo, status, jumlah percobaan, batas percobaan ulang, pemilik kunci, waktu kedaluwarsa kunci, dan pesan kesalahan singkat. Kolom-kolom tersebut memberi worker informasi yang cukup untuk menjalankan dan memulihkan pekerjaan dengan aman.

Bagaimana beberapa worker menghindari menjalankan pekerjaan yang sama?

Klaim pekerjaan dalam satu transaksi database dengan penguncian baris, seperti PostgreSQL FOR UPDATE SKIP LOCKED. Perbarui baris menjadi sedang berjalan dan tetapkan masa sewanya sebelum worker memulai tugas sebenarnya.

Mengapa pekerjaan latar belakang memerlukan batas waktu masa sewa?

Masa sewa adalah kunci sementara yang dicatat dengan locked_until. Jika worker mengalami crash, masa sewa berakhir dan worker lain dapat mengambil pekerjaan tersebut, sehingga tidak macet selamanya.

Bagaimana pekerjaan yang gagal sebaiknya dicoba ulang?

Coba lagi nanti dengan aturan backoff yang jelas, misalnya 1 menit, 2 menit, 4 menit, lalu jeda terbatas dengan sedikit variasi acak. Setelah jumlah percobaan yang diizinkan tercapai, tandai pekerjaan sebagai mati agar tidak terus menghabiskan waktu worker.

Apa itu idempotensi, dan mengapa hal itu penting?

Buat efek samping aman untuk diulang. Misalnya, gunakan kunci idempotensi yang stabil seperti ID faktur atau ID pengguna ditambah tanggal laporan, lalu terapkan keunikan di PostgreSQL atau pada penyedia eksternal.

Apakah pola ini menjamin pemrosesan tepat satu kali?

Tidak. Worker dapat mengalami crash setelah mengirim email atau memanggil penyedia pembayaran, tetapi sebelum menyimpan status berhasil. Rancang untuk pemrosesan setidaknya sekali, lalu gunakan idempotensi untuk mencegah efek duplikat.

Haruskah worker menahan transaksi database saat menjalankan pekerjaan?

Jaga transaksi database tetap singkat. Klaim pekerjaan, commit, lakukan panggilan jaringan di luar transaksi, lalu simpan keberhasilan atau kegagalan dalam pembaruan terpisah. Transaksi panjang menahan kunci dan memperlambat worker lain.

Bagaimana cara memantau pekerjaan cron dan database?

Lacak pekerjaan mati dan pekerjaan lama yang masih berjalan, catat setiap klaim dan hasilnya, lalu buat peringatan saat kegagalan meningkat. Tambahkan tindakan admin sederhana untuk mencoba ulang pekerjaan sekarang, membatalkannya, atau memeriksa kesalahan terbarunya.

Related posts