Menekan Publish Dua Kali Kini Hanya Mempublish Sekali
Tiga permintaan publish untuk aplikasi yang sama tiba di API kami dengan selang 84 milidetik, dan masing-masing memulai deploy penuh untuk aplikasi itu. Ketiganya berebut ke hosting yang sama, beberapa rilis yang dihasilkan gagal karena yang lain sudah lebih dulu sampai, dan satu-satunya tanda yang terlihat adalah email “release failed”. Aplikasinya sendiri akhirnya baik-baik saja. Sekarang, publish yang datang saat publish lain untuk aplikasi yang sama sedang berjalan tidak lagi memulai build kedua. Database yang memutuskan permintaan mana yang menang, dan yang lain diberi tahu dengan jujur: sebuah publish sudah berjalan.
Apa yang terjadi
Saat Anda mempublish aplikasi di Mythex, API mencatat sebuah deployment untuknya dan meminta orchestrator kami, yaitu layanan yang menjalankan build, untuk membangun dan merilisnya. Publish bisa dimulai dari tombol Publish, dari tab browser lain, atau dari agen saat Anda memintanya untuk mempublish. Double click, dua tab, atau permintaan agen yang datang dua kali bisa menghasilkan lebih dari satu publish untuk aplikasi yang sama hampir bersamaan.
Dalam kasus ini, tiga permintaan tiba dalam 84 md. Masing-masing menjalankan deploy penuh. Di platform hosting, rilis untuk aplikasi yang sama dibuat dalam detik yang sama, dan tiga di antaranya gagal karena yang lain sudah berjalan. Rilis yang terakhir macet, dan aplikasinya berfungsi setelahnya.
Itulah yang membuat kejadian ini mudah terlewat. Pengguna melihat satu aplikasi yang berfungsi. Kegagalannya hanya muncul sebagai email kegagalan rilis.
Mengapa tidak ada yang menghentikannya
Kode publish memang punya langkah yang memeriksa deploy yang sedang berjalan untuk proyek itu. Tetapi yang dilakukannya adalah menandai setiap deploy yang sedang berjalan sebagai gagal sebelum memulai, dengan alasan “digantikan oleh publish baru”. Langkah itu dimaksudkan untuk membersihkan build yang tertinggal dari proses yang crash.
Dalam praktiknya, publish kedua yang datang di tengah build membuat publish pertama dinyatakan mati, lalu memulai miliknya sendiri. Keduanya berjalan sampai selesai terhadap aplikasi yang sama. Tidak ada kunci; yang ada adalah langkah yang secara aktif membuka jalan bagi race.
Ada gejala kedua yang lebih aneh untuk publish pertama sebuah aplikasi. Karena belum ada catatan deployment untuk digunakan ulang, dua publish pertama yang bersamaan sama-sama mencoba menyisipkan satu catatan. Database punya unique index pada nama tautan aplikasi (slug-nya, seperti your-app di your-app.mythex.ai), sehingga penyisipan kedua gagal. API menerjemahkannya menjadi “nama tautan itu sudah dipakai”, yang menyarankan pengguna mengganti nama aplikasi yang, pada saat itu, sedang berhasil dipublish dengan nama tersebut.
Perbaikannya: klaim adalah kuncinya
Mengklaim catatan deployment sekarang adalah satu pembaruan database bersyarat: ambil baris ini dan tandai sebagai building hanya jika tidak ada yang sedang mempublishnya. Karena ini satu pernyataan, database memutuskan permintaan yang tiba dalam milidetik yang sama. Tepat satu dari mereka yang mengubah baris. Yang lain melihat bahwa nol baris berubah, berhenti, dan tidak pernah sampai ke orchestrator.
Kunci juga butuh jalan keluar saat pemegangnya mati. Jika API restart di tengah publish, baris akan tetap ditandai “building” selamanya dan aplikasi tidak bisa dipublish lagi. Karena itu, baris yang pemiliknya sudah berhenti mengirim heartbeat bisa diklaim ulang. Publish yang berjalan memperbarui baris setiap 30 detik, dan lima menit tanpa sinyal dianggap mati. Ambang itu berasal dari heartbeat, bukan tebakan berapa lama build seharusnya berjalan. Membangun Dockerfile milik pengguna bisa berlangsung lama secara sah, dan build yang lambat tetapi masih mengirim heartbeat tetap memegang klaimnya.
Langkah yang dulu menandai setiap deploy yang sedang berjalan sebagai gagal tidak lagi berjalan sebelum publish. Hanya baris yang ditinggalkan proses yang mati yang dibersihkan.
Memberi tahu yang kalah dengan jujur
Permintaan yang kalah sekarang mendapat jawaban yang jelas: publish_in_progress, bersama ID deployment yang sedang berjalan. Setiap tempat yang bisa memulai publish menanganinya:
- Workspace mengikuti build yang sedang berjalan, alih-alih menampilkan error di atasnya.
- Agen diberi tahu bahwa publish aplikasi ini sudah berjalan dan harus menunggu, alih-alih diberi tahu bahwa sesuatu gagal. Dulu, pemberitahuan gagal bisa membuatnya pergi “memperbaiki” Dockerfile dan mencoba lagi.
- Collision publish pertama pada nama tautan dicek: jika baris yang memegang nama itu adalah publish yang sedang berjalan milik proyek ini sendiri, jawabannya “sedang mempublish”, bukan “nama itu sudah dipakai”. Collision nyata dengan proyek lain tetap mendapat pesan ganti nama.
Cara kami mengujinya
Kami menulis skrip reproduksi yang menjalankan tiga publish dari satu aplikasi secara bersamaan terhadap database sungguhan dan orchestrator tiruan, lalu menghitung berapa deploy yang diminta dari orchestrator. Skrip ini mencakup publish pertama dan republish aplikasi yang sudah live. Sebelum perbaikan, hitungannya dua deploy. Sesudahnya, satu.
Skrip itu juga mencakup jalan keluarnya: ia menandai baris aplikasi sebagai building tanpa heartbeat selama sepuluh menit, lalu mempublish lagi, dan memeriksa bahwa publish baru berhasil mengklaim baris itu dan berjalan.
Unit test mengunci kunci itu pada dua hal yang harus dilakukannya: dari tiga republish bersamaan, tepat satu yang lolos dan dua lainnya diberi tahu bahwa publish sedang berjalan; dan build yang pemiliknya sudah mati bisa diklaim. Tes lain memastikan percobaan baru tidak membawa pesan error dari percobaan sebelumnya.
Apa yang kami pelajari
- Kode pembersihan bisa menjadi race yang menyamar. “Gagalkan apa pun yang sedang berjalan, lalu mulai” terdengar seperti kebersihan. Dengan dua pemanggil, itu berarti masing-masing membatalkan yang lain.
- Biarkan database yang memutuskan. Membaca lalu menulis meninggalkan celah bagi permintaan lain. Satu pembaruan bersyarat tidak.
- Setiap kunci butuh masa berlaku yang terikat pada kehidupan prosesnya. Tanpanya, crash berubah menjadi aplikasi yang macet.
- Pesan error adalah instruksi. “Nama itu sudah dipakai” menyuruh orang melakukan hal yang keliru. Pesan yang benar lebih sederhana dan memang benar.
Lebih banyak tentang publish ada di dokumentasi: cara mempublish. Dan perbaikan terkait: aplikasi yang sudah dipublish tetap aktif saat Anda mempublish ulang.