Blog · Terbit
Server penjual mati semalam: enam dari tujuh percobaan webhook habis sebelum pagi
Dokumentasi menyebut angka yang terdengar lapang: endpoint yang tidak menjawab 2xx akan di-retry maksimal 7 kali dalam kisaran 33 jam. Angka itu benar, dan pembacaan yang paling sering diambil darinya salah. Tangga percobaannya tidak dibagi rata sepanjang 33 jam. Beratnya ada di depan: enam dari tujuh percobaan jatuh dalam 8 jam 36 menit pertama, dan percobaan ketujuh baru datang sekitar sehari kemudian. Sebuah server yang mati jam sepuluh malam dan baru dihidupkan jam tujuh pagi karena itu tidak menghabiskan sebagian jatah sebuah event, melainkan enam dari tujuhnya, sementara tidak ada seorang pun yang terjaga.
Tulisan ini soal apa yang dilakukan sesudahnya, dan apa yang mencegahnya lain kali. Untuk pertanyaan yang lebih dulu, yaitu apakah sebuah event pernah dikirim atau tidak, urutannya ada di cara memisahkan belum pernah dikirim dari ditolak.
Tangga percobaannya, dan kenapa beratnya di depan
Percobaan pertama dikirim segera setelah pembayaran berhasil. Setiap kegagalan membeli satu jeda, dan jedanya melebar:
| Percobaan | Jeda sejak kegagalan sebelumnya | Waktu sejak pembayaran masuk |
|---|---|---|
| 1 | langsung | 0 |
| 2 | 1 menit | 1 menit |
| 3 | 5 menit | 6 menit |
| 4 | 30 menit | 36 menit |
| 5 | 2 jam | 2 jam 36 menit |
| 6 | 6 jam | 8 jam 36 menit |
| 7 | 24 jam | 32 jam 36 menit |
Bentuk ini masuk akal untuk gangguan yang wajar: endpoint yang tersedak satu menit karena deploy akan menerima eventnya pada percobaan kedua, dan tidak ada yang perlu dikerjakan siapa pun. Yang tidak masuk akal adalah memperlakukan 33 jam itu sebagai bantalan tidur. Bantalan yang sesungguhnya adalah delapan setengah jam, dan sesudah itu tersisa satu percobaan tunggal yang datang keesokan harinya.
Gangguan semalam, dihitung
Sebuah toko menerima pembayaran sepanjang malam. Server penagihannya berhenti menjawab pukul 22.00 dan baru hidup lagi pukul 07.00, sembilan jam kemudian. Yang terjadi pada tiap event ternyata sangat berbeda menurut jam masuknya pembayaran:
- Pembayaran pukul 22.00, tepat saat gangguannya mulai, kehilangan keenam percobaan pertamanya berturut-turut pada pukul 22.00, 22.01, 22.06, 22.36, 00.36, dan 06.36. Ketika server hidup pukul 07.00, event itu sudah kehabisan enam dari tujuh jatahnya dan yang tersisa satu percobaan yang baru jatuh pukul 06.36 keesokan harinya. Statusnya masih Mencoba ulang dan bukan gagal, jadi tidak ada yang terlihat merah, tetapi pesanannya tertahan hampir sehari penuh kalau tidak ada yang bertindak.
- Pembayaran pukul 02.00 memakai lima percobaan selama gangguan, yang terakhir pukul 04.36, lalu percobaan keenamnya jatuh pukul 10.36 dan berhasil. Event seperti ini pulih sendiri beberapa jam setelah perbaikan, tanpa ada yang perlu dikerjakan siapa pun.
Dua event dari malam yang sama, dan nasibnya berlawanan hanya karena selisih empat jam. Yang membedakan bukan lamanya gangguan melainkan berapa banyak rung yang sempat dilewati sebelum perbaikan datang, dan karena rung keenam berada di jam ke-8 lewat 36 menit, garis pemisahnya jatuh persis di sana: event yang lebih tua dari itu saat server hidup hanya punya satu percobaan tersisa, dan percobaan itu datang besok.
Status Gagal permanen sendiri baru muncul pada gangguan yang melewati 32 jam 36 menit. Semalam tidak pernah sampai ke sana; akhir pekan panjang yang tidak ada yang menjaga bisa.
Pelajaran praktisnya satu: gangguan yang lewat tengah malam tidak selesai dengan menunggu tangga percobaannya bekerja. Untuk event yang sudah sampai rung terakhir, menunggu berarti menunggu sampai besok.
Server yang lambat membakar percobaan sama seperti server yang mati
Daftar lengkap hal yang dihitung sebagai penolakan ada di artikel diagnosis yang ditautkan di atas. Satu di antaranya perlu disebut ulang di sini karena justru menyerang pada malam yang sibuk, bukan pada malam yang mati: jawaban yang tidak selesai dalam sepuluh detik dicatat sebagai galat dan membakar satu percobaan, persis seperti koneksi yang ditolak.
Bentuk handler yang paling sering terkena adalah yang memproses pesanan lebih dulu dan baru menjawab belakangan. Pada beban normal handler itu selesai dalam ratusan milidetik dan tidak pernah terlihat bermasalah; pada lonjakan, saat basis datanya sedang antre, pekerjaannya berhasil tetapi jawabannya terlambat, sehingga pembayaran tercatat di kedua sisi sambil percobaannya tetap habis satu per satu. Bentuk yang tahan adalah urutan pendek: verifikasi tanda tangan, catat id eventnya, jawab 2xx, lalu kerjakan sisanya di luar permintaan itu. Tiga kesalahan yang membuat verifikasi tanda tangannya sia-sia dibahas di cara mengamankan endpoint webhook.
Pemeliharaan yang direncanakan punya jawaban yang berbeda
Kalau matinya server sudah diketahui sebelumnya, misalnya migrasi database yang dijadwalkan dini hari, membiarkan event menabrak endpoint yang mati adalah pilihan yang paling mahal. Ada jalan yang tidak membakar apa pun: matikan endpointnya dari pengaturan webhook sebelum pekerjaan dimulai. Endpoint yang berstatus nonaktif menahan eventnya tanpa menghabiskan jatah percobaan sampai dinyalakan lagi, jadi seluruh antrean malam itu berangkat utuh setelah pekerjaannya selesai, dengan tujuh percobaan yang masih penuh.
Perbedaannya besar dan sering terlewat: membiarkan endpoint mati menghabiskan enam percobaan dalam delapan jam, sementara mematikannya lebih dulu menghabiskan nol. Satu-satunya syaratnya adalah mengingat untuk menyalakannya kembali.
Gagal permanen bukan berarti hilang
Event yang seluruh percobaannya ditolak berhenti dengan status Gagal permanen, dan pada titik itu hanya tombol kirim ulang di log pengiriman yang menghidupkannya. Yang penting dipahami tentang tombol itu: kirim ulang memberi satu percobaan lagi, sekarang. Bukan tangga yang diulang dari awal, dan bukan pengiriman massal. Untuk satu event yang tertinggal, itu persis yang dibutuhkan. Untuk empat puluh event setelah gangguan semalam, menekan tombol empat puluh kali adalah pekerjaan yang salah bentuknya, dan tiap penekanan hanya berhasil kalau endpointnya sudah benar-benar sehat.
Karena itu urutannya penting: perbaiki endpoint lebih dulu, pastikan jalurnya hidup dengan tombol kirim event percobaan, baru sentuh event yang tertinggal. Kirim ulang ke endpoint yang masih setengah sehat membakar satu-satunya percobaan yang tersisa.
Menutup selisihnya dari sisi sendiri
Untuk gangguan yang menyisakan banyak event, pemulihan yang benar tidak berjalan dari log pengiriman melainkan dari basis data sendiri. Sisi penjual sudah tahu pesanan mana yang dibuat dan belum ditandai lunas, dan daftar itu yang menjadi pertanyaannya. Bacalah status tiap permintaan pembayaran yang menggantung lewat endpoint pembacaan satu transaksi memakai id yang sudah disimpan, lalu perlakukan hasilnya persis seperti hasil webhook: dedupe berdasarkan id event atau id pembayaran, pindahkan status pesanan dalam satu transaksi database, dan catat bahwa pembayaran itu sudah diproses.
Sapuan seperti ini perlu ada bahkan di integrasi yang webhook-nya tidak pernah bermasalah, karena ada keadaan yang memang tidak pernah dikirim sebagai event sama sekali. Kolom yang harus disimpan supaya sapuan itu mungkin dibahas di panduan menyimpan data pembayaran di sistem sendiri.
Membalas selain 2xx pada event yang sudah diproses justru memperburuk
Ada refleks yang terlihat rapi dan ternyata merugikan: handler yang menerima event dengan id yang sudah pernah diproses lalu menjawab 409 atau 500 karena menganggapnya duplikat. Pengiriman bersifat at-least-once, jadi event yang sama memang bisa datang lebih dari sekali dan membawa id yang sama, dan jawaban selain 2xx pada pengiriman kedua itu membakar satu percobaan untuk sesuatu yang sebenarnya sudah selesai. Pada malam pemulihan, percobaan itulah yang paling mahal. Jawaban yang benar untuk duplikat adalah 2xx tanpa mengerjakan apa pun.
Satu hal yang tidak perlu dipantau sendiri
Endpoint yang berhenti menjawab memicu satu pemberitahuan lewat surel untuk seluruh gangguan itu, bukan satu surel per event, dengan pengingat yang jarang selama gangguannya belum beres dan pemberitahuan pemulihan saat pengiriman berhasil lagi. Bentuk itu memang disengaja, karena satu endpoint mati yang menjatuhkan dua ratus pembayaran akan menjadi dua ratus surel yang tidak dibaca siapa pun. Konsekuensinya, surel itu berguna sebagai penanda waktu dan bukan sebagai pengganti jadwal jaga: ada jeda sebelum pesan pertama dikirim, dan surel yang dibaca pukul tujuh pagi tetap sampai setelah enam percobaan terbakar. Bagi integrasi yang gangguannya berbiaya tinggi, memasang peringatan sendiri yang menyala pada rung kelima, sekitar dua setengah jam, memberi ruang yang jauh lebih berguna daripada menunggu pemberitahuan pemulihan.
Kalau satu endpoint terasa terlalu rapuh untuk memikul semua sistem sekaligus, pilihan memecahnya beserta akibatnya pada anggaran percobaan dibahas di perbandingan satu endpoint webhook dan beberapa, dan seluruh kontrak pengirimannya ada di dokumentasi webhook.
Ringkasnya
- Tujuh percobaan, tetapi enam di antaranya habis dalam 8 jam 36 menit.
- Pemeliharaan yang direncanakan: matikan endpointnya lebih dulu, karena event yang tertahan tidak membakar percobaan.
- Gagal permanen masih bisa dihidupkan, satu percobaan per penekanan tombol kirim ulang, dan hanya sesudah endpointnya benar-benar sehat.
- Pemulihan berjumlah besar dikerjakan sebagai sapuan dari basis data sendiri, bukan dari log pengiriman.
- Duplikat dijawab 2xx tanpa mengerjakan apa pun.