Skip to content

Launcher Dua Kali Bayar: Tiga Lapis Penjaga Sebelum Skrip Delegasi Menyala

Adityo Guni Waluyo

Skrip delegasi yang diluncurkan dua kali menghabiskan budget dua kali. Guard flock, cek artefak, dan pgrep menolak sebelum mulai.

Ringkasan

Ceritanya session Claude Code kelihatan macet padahal lagi mikir, jadi gue jalankan ulang dan dua proses makan budget bareng. Solusinya flock -n via file descriptor, plus cek result JSON lama dan pgrep -f buat deteksi proses kembar. Alasan penolakan dibedain lewat exit code 42, 43, 44 biar gampang debug.

Dua kali bayar untuk satu pekerjaan

Layar terminal diam. Kursor berkedip, tidak ada output baru dari delegasi Claude Code yang saya jalankan lima menit lalu. Refleks langsung ambil alih: tekan Ctrl+C, jalankan ulang perintah yang sama.

Ternyata keputusan mahal. Session pertama nggak macet, cuma lagi mikir keras. Karena saya jalankan ulang, muncul session kedua, dan dua proses paralel itu menggerus max-budget-usd yang sama bersamaan. Bukannya lebih cepat selesai, saya bayar dua kali untuk output yang sama.

Reaksi pertama saya dulu: "tinggal tambah lockfile biasa, atau cek PID lama pakai ps lalu pkill kalau ada yang nyangkut." Rapuh dua-duanya. Lockfile biasa meninggalkan mayat kalau skrip crash sebelum sempat menghapus file-nya. Dan pkill itu main tebak-tebakan: PID bisa didaur ulang, proses lama bisa saja sudah mati tapi state-nya membingungkan.

Saya butuh mekanisme yang menolak eksekusi sebelum apa pun terjadi, bukan membersihkan setelah tabrakan. Di commit 900f683 pada hermes/scripts/cc-launch.sh, guard-nya berdiri di atas tiga lapis pemeriksaan yang semua selesai sebelum perintah utama jalan.

Kunci yang tidak bisa antre

Intinya flock. Banyak yang pikir pasang flock saja cukup; justru flag-nya yang menentukan hidup mati skrip.

Pola di skrip: exec 9>"$LOCK" lalu flock -n 9. Kenapa wajib -n? Karena flock tanpa itu menunggu (blocking) selama lock dipegang proses lain. Dengan LOCK_NB, permintaan yang bentrok langsung gagal dan mengembalikan EWOULDBLOCK [1]. Tanpa -n, launcher kedua diam-diam mengantre di belakang proses lama, lalu menyala saat yang pertama selesai, dan kita kembali ke masalah budget yang sama.

Kenapa lock via file descriptor, bukan sekadar keberadaan file lock? Karena lock flock menempel pada open file description, bukan pada PID: fd duplikat berbagi lock yang sama, lock hilang saat semua fd tertutup, dan dia bertahan melewati execve [1]. Dengan exec 9>"$LOCK", fd 9 hidup selama proses launcher hidup. Jauh lebih aman daripada mengandalkan PID yang bisa didaur ulang OS.

Jangan ulangi pekerjaan yang sudah selesai

Sebelum menolak, skrip perlu tahu apakah pekerjaan lama sebenarnya sudah kelar. Lapis kedua memeriksa file .out dari eksekusi sebelumnya: kalau sudah berisi result JSON yang valid, launcher keluar dengan exit code 43. Kalau hasilnya sudah ada dan valid, menolak menjalankan ulang adalah keputusan paling hemat. Tidak ada alasan membayar token API dua kali untuk pertanyaan yang sama, dan ini yang bikin launcher bisa dipanggil berulang tanpa takut (idempoten).

Lapis ketiga menyambung ke situ: bagaimana kalau proses lama ternyata masih jalan, belum sempat menulis hasil? Di sini pgrep masuk. Default-nya, pgrep hanya mencocokkan pola pada nama proses; flag -f lah yang mencocokkan full command line [3], dan hanya dengan itu pgrep -f 'claude -p' bisa menemukan apa pun. Ketemu? Launcher keluar dengan exit code 44, proses kembar masih hidup.

Ada nuansa teknisnya. Sebagian developer tergoda memanipulasi process group lewat setpgid, tapi memanggilnya pada child yang sudah execve gagal dengan EACCES [2]. Race kecil seperti ini salah satu alasan semua pemeriksaan guard selesai sebelum child process dibuat, bukan sesudah.

Exit code yang bercerita

Hasil akhirnya, launcher dengan prinsip tegas: tolak sebelum mulai. Setiap alasan penolakan punya exit code sendiri:

exit 42  # lock sedang dipegang proses lain
exit 43  # result JSON sudah ada, pekerjaan selesai
exit 44  # proses kembar masih hidup

Kode keluar yang spesifik jauh lebih murah di-debug daripada membiarkan tabrakan lalu membersihkannya kemudian. Saya pilih skrip yang gagal dengan sopan dan jelas, daripada yang jalan tapi merusak state sistem.

Sumber

Artikel terkait