Cron Kirim Dua Kali? Pasang Kontrak Async Idempoten
Loop cron yang crash mengirim ulang prompt yang sama. Kontrak async kirim-then-poll plus dua file state bikin generate chat Qwen aman dieksekusi ulang.
Ringkasan
Cron yang crash setelah prompt terkirim bikin artikel sama terkirim dua kali dan kuota gratis hangus. Solusinya cuma dua file kosong sebagai idempotency key: penanda prompt sudah terkirim dan penanda hasil sudah selesai, plus kontrak exit code untuk status antrian hingga selesai. Cron itu mesin retry alami, jadi selama state-nya bisa dicek, message broker nggak perlu.
Pagi itu saya cek log cron artikel dan nemu kejadian yang bikin bias: dua chat Qwen dengan prompt yang sama persis. Loop sebelumnya crash di tengah jalan setelah prompt terkirim, lalu cycle berikutnya jalan lagi dan menganggap tugas itu belum ada. Hasilnya, satu artikel, dua kali kirim, kuota free chat.qwen.ai hangus separuh tanpa artikel tambahan. Di pipeline otomatis adityo.web.id, kejadian kayak gini numpuk diam-diam: kegagalan kecil yang kelihatan sepele, tapi makan jatah tiap cycle.
Awalnya saya kira solusinya harus keluar dari skrip biasa. Insting pertama waktu itu: pasang message broker, bikin antrean beneran, simpan job di Redis, lengkap. Wajar, soalnya hampir semua tutorial orkestrasi asinkron bermula dari sana. Tapi begitu saya bedah masalahnya, kebutuhannya jauh lebih ramping: cuma butuh jawaban atas dua pertanyaan. Apakah prompt ini sudah pernah terkirim, dan apakah hasilnya sudah ada. Dua pertanyaan itu bisa dijawab dua file kosong.
Kirim dulu, tanya hasil kemudian
Bentuk kontraknya saya pinjam dari pola klasik dunia web. Dokumentasi Azure Architecture Center njelasin pola asynchronous request-reply begini: client mengirim request untuk memicu operasi yang berjalan lama, lalu API membalas secepatnya dengan HTTP 202 Accepted sebagai tanda permintaan diterima, disertai referensi lokasi yang bisa di-poll untuk mengecek hasil [7]. Pipeline saya memainkan peran yang sama, hanya saja semua komponennya hidup di sisi client karena chat.qwen.ai tidak menyediakan API job asinkron. Peran 202 diperankan exit code 42: prompt sudah masuk, jawaban dijamin jalan, bukan gagal. Peran header Location diperankan file sidecar .md.url yang nyimpen alamat chat, dan cara mengeceknya tinggal panggil ulang generate yang sama.
Kontrak exit code-nya cuma empat. Exit 42 berarti queued, exit 3 berarti masih diproses, exit 0 berarti hasil sudah bisa ditarik, dan exit 1 satu-satunya alasan beneran ganti strategi. Jarak antar pengecekan saya kunci di sekitar 60 detik. Arahnya sama dengan header Retry-After yang direkomendasikan pada pola di atas: server bilang ke client kapan boleh nanya lagi, supaya client nggak menagih status terus-terusan [7]. Selisihnya, skrip saya yang menentukan jadwalnya sendiri.
Dua file sebagai idempotency key
Idempotensinya seperti ini: generate yang dijalankan ulang akan melihat .md.url sudah ada, lalu memilih mengecek hasil daripada mengirim ulang prompt. Kalau file hasil .md juga sudah ada, seluruh proses berhenti sebagai no-op. AWS docs mendefinisikan metode idempoten sebagai request yang efek beberapa permintaan identiknya sama dengan efek satu permintaan [8], dan MDN menambahkan bahwa client boleh mengulang request idempoten dengan aman saat ragu request pertama sampai atau tidak [9]. Persis kondisi cron yang crash: nggak ada yang tahu prompt sempat diproses atau nggak, jadi panggilan ulang harus aman apa pun hasilnya.
Referensi lapangan yang saya pakai patokannya datang dari Stripe. API mereka mendukung idempotency key supaya request bisa diulang dengan aman tanpa risiko operasi yang sama dieksekusi dua kali [11], dan hasil request pertama disimpan per key, jadi pengulangan dengan key yang sama mengembalikan hasil yang sama [11]. Dua file itu bekerja di posisi yang sama: .md.url sebagai kunci "sudah terkirim" dan .md sebagai kunci "sudah selesai". Sengaja nggak pakai basis data; file kosong nggak butuh migrasi dan isinya nggak mungkin selisih.
Yang bikin pendekatan ini masuk akal, cron sendiri adalah mesin retry alami. Daemon cron memeriksa semua crontab dan menjalankan job yang jatuh di menit berjalan [10], jadi job yang crash memang akan keeksekusi ulang; yang bisa saya kendalikan cuma memastikan eksekusi ulang itu tidak membayar dua kali. RFC 9110 menegaskan arah yang sama: client sebaiknya tidak mengulang otomatis request non-idempoten tanpa cara mengetahui request asli benar-benar belum dieksekusi [8]. Daripada mengandalkan keberuntungan, saya bikin pengulangannya jadi tidak berbahaya.
Buat saya pelajarannya bukan soal Qwen, tapi soal kontrak. Selama cron yang saya tulis punya state yang bisa ditanya "apakah kamu sudah jalan", dia nggak butuh infrastruktur antrean. Sekali kontrak async dan idempotensi terpasang di sisi client, ganti engine, ganti jadwal, atau crash di tengah cycle nggak lagi berarti biaya tambahan. Saya sudah pakai pola serupa untuk poll jawaban baru di chat lama tanpa dapat yang basi, dan untuk fallback router9 saat chat gratis bermasalah; kontrak kali ini yang nutup sisa lubangnya, biaya paling mahal di pipeline kecil tetap pesanan yang kekirim dua kali.
Sumber: