Lompat ke konten
20 menit bacamateri 60 dari 96

Jadwalkan workflow

Job tertunda, pembatalan, dependensi DAG, kepemilikan retry, dan rekonsiliasi. Cara menjalankan pekerjaan multi-langkah yang bisa dipulihkan.

Ditinjau 26 Oktober 2026

Pertanyaan

Bagaimana menjalankan pekerjaan yang punya beberapa langkah, jeda waktu, dan kemungkinan batal, sehingga setelah crash kita tahu persis harus lanjut dari mana?

Setelah baca ini, kamu bisa merancang job tertunda yang bisa dibatalkan, menggambar dependensi sebagai DAG, menetapkan satu pemilik retry per langkah, dan menambahkan rekonsiliasi sebagai jaring pengaman.

Prasyarat dan contoh

Kamu sudah memahami ack dan idempotensi (M10.02), outbox (M10.05), dan beban consumer (M10.03).

Sebuah marketplace punya aturan: pesanan yang belum dibayar dalam 30 menit dibatalkan dan stok dikembalikan. Pesanan dibuat 10:00. Tim membuat cron tiap menit yang memindai tabel pesanan dengan status = 'menunggu' AND dibuat < now() - 30 menit. Pada 200.000 pesanan menunggu, pindaian ini menyentuh tabel besar tiap menit. Lalu muncul kasus: pelanggan membayar pada 10:29:59 tepat saat cron membatalkan. Pesanan dibatalkan padahal sudah dibayar.

Dua masalah di situ: mekanisme penjadwalan (pindai vs jadwal per item) dan koordinasi keputusan (balapan antara bayar dan batal).

Mekanisme

Job tertunda. Alih-alih memindai, buat satu job per pesanan dengan waktu jalan 10:30:

jadwal(job_id="batal-8812", jalan_pada=10:30, payload={pesanan:8812})

Penyimpanan: tabel jadwal berindeks jalan_pada, sorted set, atau fitur delay milik broker. Worker mengambil job yang jalan_pada <= now() dengan klaim (lease) supaya tidak diambil dua kali. Presisi realistisnya detik, bukan milidetik, dan jalan tepat waktu tidak dijamin saat sistem sibuk atau mati. Job tertunda berarti "paling cepat pada waktu itu".

Job harus memeriksa keadaan, bukan percaya jadwal. Saat job berjalan, ia membaca status terkini dan memutuskan:

UPDATE pesanan SET status='dibatalkan'
 WHERE id=8812 AND status='menunggu'

Jika sudah dibayar, update menyentuh 0 baris, job selesai tanpa efek. Pembatalan eksplisit jadi sederhana: tidak perlu menghapus job dari jadwal, cukup biarkan ia menemukan bahwa tidak ada yang perlu dikerjakan (lebih tahan balapan daripada mencoba menghapus job). Menghapus job tetap berguna untuk menghemat ruang, tapi bukan jaminan kebenaran.

Dependensi DAG. Workflow dengan banyak langkah digambar sebagai graf berarah tanpa siklus:

validasi → (tagih, cadangkan stok) → kirim_struk → tandai_selesai

Langkah jalan setelah semua prasyaratnya sukses. tagih dan cadangkan stok bisa paralel. Dua cara menyimpan keadaan: tabel status langkah per instance workflow (jelas dan bisa dikueri), atau event yang dipicu langkah sebelumnya (longgar, tapi sulit melihat gambaran keseluruhan). Untuk alur panjang dengan kompensasi, simpan keadaan eksplisit.

Kepemilikan retry. Tentukan satu lapisan yang melakukan retry untuk tiap langkah. Kalau klien, gateway, workflow engine, dan worker semuanya retry 3 kali, satu kegagalan menjadi 3 × 3 × 3 × 3 = 81 percobaan. Aturan umum: retry dimiliki lapisan terdekat yang tahu apakah kegagalan itu sementara, dengan batas, backoff, dan jitter. Lapisan lain hanya meneruskan error. Tiap langkah tetap idempoten dengan kunci seperti workflow_id:langkah.

Kompensasi. Langkah yang sudah berhasil dan tidak bisa di-rollback oleh transaksi (tagihan, email) dibatalkan dengan langkah kebalikan (refund, email koreksi). Kompensasi juga bisa gagal, jadi ia perlu retry dan idempotensi yang sama.

Rekonsiliasi. Jadwal dan event bisa hilang, tertunda, atau salah. Sediakan pass periodik yang membandingkan keadaan yang seharusnya dengan yang sebenarnya, misalnya tiap 15 menit mencari pesanan menunggu yang berumur > 45 menit (seharusnya sudah dibatalkan 15 menit lalu). Ini bukan pengganti job tertunda, melainkan jaring pengaman. Karena hanya mencari anomali, indeks sempit pada status, dibuat membuatnya murah, dan jumlah temuan menjadi metrik kesehatan.

Saat rancangan ini gagal

  • Cron memindai tabel besar. Pindaian tiap menit pada 200.000 baris menaikkan beban baca dan menumpuk saat pindaian sebelumnya belum selesai. Beberapa instance cron mengeksekusi pekerjaan yang sama. Gunakan kunci, atau jadwal per item.
  • Zona waktu dan DST. Job "setiap hari 02:30" tidak ada atau jalan dua kali pada hari pergantian waktu musim panas di beberapa zona. Simpan waktu dalam UTC dan tentukan perilaku eksplisit untuk jadwal berbasis waktu lokal.
  • Penumpukan pada titik bulat. 50.000 job dijadwalkan pada pukul 00:00 jalan bersamaan dan menjenuhkan dependensi. Sebar dengan jitter atau batasi laju.
  • Pembatalan yang berlomba. Menghapus job bersamaan dengan job yang sudah diambil worker. Perubahan keadaan harus kondisional di sumber kebenaran, seperti WHERE status='menunggu'.
  • Retry bertingkat. Lihat hitungan 81 percobaan tadi: dependensi yang sedang lemah dihantam paling keras tepat saat ia butuh waktu pulih.
  • Workflow tanpa timeout. Langkah menunggu callback pihak luar yang tidak pernah datang. Instance menggantung berhari-hari. Beri batas waktu per langkah dan jalur gagal yang jelas.
  • Keadaan hanya di memori worker. Worker crash, tidak ada yang tahu langkah mana yang selesai. Simpan kemajuan di penyimpanan tahan lama sebelum melanjutkan.

Contoh tandingan: "workflow engine menangani semuanya". Engine yang baik menyimpan kemajuan dan menjadwalkan ulang, tapi efek langkah ke sistem luar tetap perlu idempotensi dan kompensasi yang kamu rancang sendiri.

Latihan

Alur onboarding: (1) buat akun, (2) kirim email verifikasi, (3) jika tidak diverifikasi dalam 24 jam, kirim pengingat, (4) jika tidak diverifikasi dalam 72 jam, hapus akun. Ada 400.000 akun baru per hari.

  1. Rancang penjadwalan langkah 3 dan 4 tanpa memindai tabel akun setiap menit.
  2. Pengguna memverifikasi pada jam ke-23. Apa yang terjadi pada job jam ke-24, dan bagaimana memastikan tidak ada pengingat atau penghapusan yang salah?
  3. Apa yang diselidiki rekonsiliasi di sini?
Lihat pembahasan
  1. Saat akun dibuat, buat dua job tertunda: pengingat-{akun} pada +24 jam dan hapus-{akun} pada +72 jam, dalam transaksi yang sama dengan pembuatan akun (atau lewat outbox, M10.05). 400.000 akun/hari berarti 800.000 job per hari, rata-rata sekitar 9 job/detik. Itu beban kecil untuk tabel berindeks jalan_pada. Sebar dengan jitter beberapa menit agar tidak menumpuk pada puncak pendaftaran.
  2. Job tetap jalan, tetapi memeriksa keadaan: kirim pengingat hanya jika status = 'belum_verifikasi', hapus hanya dengan DELETE ... WHERE id=? AND status='belum_verifikasi'. Akun yang sudah verifikasi membuat kedua job selesai tanpa efek. Tidak perlu membatalkan job secara eksplisit. Pengingat juga idempoten dengan kunci pengingat-{akun} agar retry tidak mengirim dua email.
  3. Akun belum_verifikasi yang umurnya > 73 jam (hapus gagal atau job hilang) dan akun yang umurnya > 25 jam tanpa catatan pengingat terkirim. Query-nya sempit, jalan tiap 15 menit, dan jumlah temuan dipantau. Nilai di atas nol yang konsisten berarti bug di job utama.

Terkait

Catatan

  • Asumsi: penyimpanan job mendukung klaim atomik dan indeks pada waktu jalan. Presisi dan perilaku persisnya bergantung produk.
  • Hitungan 81 percobaan = 3^4, asumsi tiap lapisan retry 3 kali pada tiap percobaan lapisan atas.
  • Yang nggak dibahas: pemilihan engine workflow tertentu, desain saga lengkap, dan penjadwalan lintas wilayah.
  • Terakhir ditinjau: 26 Oktober 2026.

Isi · System Design