Retry yang Aman Dimulai dari State, Bukan Timeout
- Authors

- Name
- Samsul Hadi
- Threads
- @Threads


Satu job seharusnya memilih satu item dari queue. Setelah beberapa retry, empat item sudah berubah status.
Awalnya saya fokus mencari alasan job terus timeout. Saya mengira solusinya tinggal memperpanjang durasi atau menjalankan job sekali lagi. Pola sebenarnya baru terlihat ketika log percobaan dibandingkan dengan perubahan state pada queue: write pertama sudah berhasil sebelum timeout terjadi.
Worker memilih satu item berstatus queued, lalu mengubahnya agar dapat masuk ke tahap berikutnya. Ketika job dijalankan ulang, selector tidak kembali ke item yang sama karena statusnya sudah berubah. Ia memilih item berikutnya.
Setelah beberapa percobaan, empat item ikut berubah walaupun kebutuhan awalnya hanya satu slot. Queue kemudian diperiksa dan item tambahan dikembalikan ke status semula. Sejak itu saya tidak lagi melihat retry sebagai tombol โcoba lagiโ. Timeout adalah gejala. Masalah desainnya berada pada state dan identitas operasi.
Mengapa retry dapat memperbesar side effect
Bayangkan selector dengan aturan berikut:
1. Cari item pertama dengan status queued.
2. Ubah statusnya menjadi prompt_sent.
3. Jalankan proses berikutnya.
Alur tersebut terlihat benar ketika semua tahap berhasil dalam satu percobaan. Namun, ada jendela kegagalan setelah langkah kedua. Jika proses berhenti di sana, state sudah berubah tetapi caller belum menerima hasil yang meyakinkan.
Retry kemudian menjalankan kembali langkah pertama. Item sebelumnya tidak lagi memenuhi filter. Selector berpindah ke item berikutnya dan menghasilkan mutasi baru. Sistem tidak sedang melanjutkan operasi lama; ia diam-diam membuat operasi lain.
Inilah alasan response timeout tidak membuktikan bahwa tidak ada perubahan. Caller hanya mengetahui bahwa jawaban tidak diterima tepat waktu. Database, file queue, provider, atau service lain mungkin sudah menerima write.
Empat pertanyaan desain sebelum menambahkan retry
1. Apa identitas operasi ini?
Setiap pekerjaan yang dapat dicoba ulang membutuhkan identitas stabil. Bentuknya dapat berupa operation_id, job_id, atau idempotency key. Identitas itu harus tetap sama selama caller masih mencoba menyelesaikan pekerjaan yang sama.
Jika setiap retry membuat ID baru, worker tidak mempunyai cara membedakan pengulangan dari permintaan baru.
2. Kapan item dianggap sudah dimiliki worker?
Pemilihan dan perubahan status sebaiknya tidak menjadi dua keputusan longgar. Pada penyimpanan yang mendukung transaksi, claim item dapat dibuat atomik. Pada queue berbasis file atau sistem yang lebih sederhana, kita tetap membutuhkan mekanisme lock, lease, atau reservation yang mempunyai owner dan masa berlaku.
Tujuannya bukan membuat sistem rumit. Tujuannya memastikan dua percobaan tidak menganggap dua item berbeda sebagai jawaban untuk satu operasi.
3. Apa yang dilakukan retry ketika write pertama sudah terjadi?
Jawabannya sebaiknya bukan โpilih lagiโ. Worker perlu mencari hasil operasi yang sama terlebih dahulu. Jika operation ID sudah terhubung ke Item A, retry harus membaca atau melanjutkan Item A. Ia tidak boleh memilih Item B hanya karena status A berubah.
4. Bagaimana kita memastikan hasil akhirnya?
HTTP 200 saja tidak selalu cukup, apalagi timeout. Setelah mutasi penting, gunakan read-back yang sesuai: baca row yang diklaim, periksa external ID, validasi permalink, atau cocokkan status provider. Definition of done harus berdasarkan state yang benar-benar dapat diamati.
Pola implementasi yang lebih aman
Berikut bentuk konseptualnya:
receive(operation_id)
# Operasi ini mungkin sudah selesai pada percobaan sebelumnya
if result exists for operation_id:
return existing result
# Operasi belum selesai, tetapi item mungkin sudah pernah dipilih
reservation = find reservation by operation_id
if reservation does not exist:
reservation = atomically claim one queued item for operation_id
process reservation.item
save result under operation_id
read back saved result
return result
Dengan pola ini, retry tidak kembali mencari item queued dari awal. Ia mencari reservation milik operation_id yang sama.
Pola tersebut mempunyai beberapa sifat penting. Pertama, retry membawa operation_id yang sama. Kedua, hubungan antara operasi dan item disimpan. Ketiga, worker memeriksa hasil lama sebelum membuat side effect baru. Keempat, hasil dibaca kembali sebelum pekerjaan dianggap selesai.
Kita juga perlu menentukan transisi state yang eksplisit. Contohnya:
queued โ reserved โ processing โ completed
โโโโโโโโโโ failed_retryable
โโโโโโโโโโ failed_terminal
reserved mempunyai operation_id, worker_id, dan lease_expires_at. Apabila worker mati, sistem tidak langsung menganggap item bebas. Reconciler memeriksa lease dan bukti hasil sebelum memutuskan apakah pekerjaan dilanjutkan, dikembalikan ke queue, atau ditandai membutuhkan review.
Retry policy juga harus membedakan kegagalan sementara dan kegagalan permanen. Rate limit, gangguan jaringan, atau provider unavailable mungkin layak dicoba ulang. Payload invalid, hak akses ditolak, atau asset yang gagal quality gate tidak akan membaik hanya karena diulang lebih sering.
Empat pemeriksaan utama
Sebelum menyalakan retry otomatis, saya sekarang memeriksa empat hal:
- Di titik mana write pertama dapat terjadi?
- Apakah retry membawa operation ID yang sama?
- Dapatkah operasi menemukan item atau hasil dari percobaan sebelumnya?
- Bagaimana hasil akhirnya dibaca kembali dan diverifikasi?
Untuk production, pemeriksaan itu dilengkapi dengan claim atomik, batas retry, backoff, lease, reconciliation, dan log yang menghubungkan operasi dengan item. Jika empat jawaban utama masih kabur, menambah retry biasanya hanya membuat kegagalan menjadi lebih cepat dan lebih sulit ditelusuri.
Tidak semua sistem membutuhkan arsitektur yang sama
Transactional database, message broker, CSV queue, dan API eksternal mempunyai kemampuan berbeda. Queue kecil berbasis file mungkin cukup memakai lock, operation ledger, dan reconciler sederhana.
Untuk sistem yang lebih besar, pola lanjutan seperti transactional outbox, unique constraint, lease, atau deduplication store dapat dibutuhkan. Itu adalah lapisan lanjutan, bukan syarat agar pembaca memahami prinsip utamanya.
Idempotent juga tidak selalu berarti โtidak ada write keduaโ. Pada beberapa operasi, write boleh terjadi lagi selama hasil akhirnya ekuivalen dan tidak menciptakan efek bisnis tambahan. Yang penting adalah kontraknya jelas dan dapat diuji.
Pelajaran utamanya bukan bahwa semua timeout berbahaya atau semua worker harus dibangun dengan arsitektur besar. Pelajarannya lebih sederhana: retry selalu masuk ke dunia yang mungkin sudah berubah. Karena itu, desain retry harus dimulai dari state, identitas operasi, dan cara membuktikan hasil, bukan sekadar dari berapa kali proses boleh dicoba kembali.
Related Articles
Tulisan lain yang nyambung dengan topik ini.


Automation Butuh Sistem yang Bisa Dirawat
Catatan praktis tentang automation yang bukan hanya cepat, tapi juga punya gate, source of truth, wiring, dan jejak proses yang bisa dirawat.


Cara Menjaga Fokus Saat Kerja di Banyak Project Sekaligus
Catatan praktis tentang sistem kecil, kerja yang bisa diulang, dan cara mengubah pengalaman harian menjadi proses yang lebih rapi.


Cara Saya Bikin Sistem Konten Sederhana Supaya Ide Tidak Hilang dan Publish Lebih Rapi
Catatan praktis tentang cara merapikan ide, draft, aset, status, dan riwayat publish supaya proses konten tidak tercecer.
Comments
Komentar akan tampil setelah disetujui.