Jadwalkan workflow
Job tertunda, pembatalan, dependensi DAG, kepemilikan retry, dan rekonsiliasi. Cara menjalankan pekerjaan multi-langkah yang bisa dipulihkan.
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.
- Rancang penjadwalan langkah 3 dan 4 tanpa memindai tabel akun setiap menit.
- Pengguna memverifikasi pada jam ke-23. Apa yang terjadi pada job jam ke-24, dan bagaimana memastikan tidak ada pengingat atau penghapusan yang salah?
- Apa yang diselidiki rekonsiliasi di sini?
Lihat pembahasan
- Saat akun dibuat, buat dua job tertunda:
pengingat-{akun}pada +24 jam danhapus-{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 berindeksjalan_pada. Sebar dengan jitter beberapa menit agar tidak menumpuk pada puncak pendaftaran. - Job tetap jalan, tetapi memeriksa keadaan: kirim pengingat hanya jika
status = 'belum_verifikasi', hapus hanya denganDELETE ... 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 kuncipengingat-{akun}agar retry tidak mengirim dua email. - Akun
belum_verifikasiyang 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
- M10.02 Tentukan perilaku pengiriman mendasari idempotensi tiap langkah.
- M10.05 Publikasikan event dengan andal menunjukkan cara memicu langkah berikutnya tanpa kehilangan event.
- M10.03 Kendalikan beban consumer relevan untuk lonjakan job yang jatuh tempo bersamaan.
- Pemulihan dan degradasi terkontrol dibahas lebih lanjut di M16.
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.