Skip to content

Lock dicuri di tengah jalan: pelajaran fencing token

Adityo Guni Waluyo

Lock yang dicek sekali di awal cuma menjaga pintu. Fencing token yang divalidasi di setiap tulisan state, plus heartbeat lease, yang menjaga datanya.

Ringkasan

Cek lock sekali di awal doang nggak cukup, soalnya sesi lama bisa bangun lagi padahal lock-nya udah dicuri sesi lain. Solusinya fencing token: tiap tulis wajib bawa token yang naik terus, kalau udah basi langsung ditolak di sisi penyimpanan. Plus resume harus idempoten dan dites pakai kontrol negatif, biar guard-nya kebukti bisa gagal sebelum insiden beneran kejadian.

Malam di selftest R66: satu proses transcribe jalan dengan tiga window. Window satu dan dua selesai, sidecar-nya sudah menempel di disk. Lalu prosesnya di-kill paksa di tengah jalan. Lock yang ditinggalkan pelan-pelan kedaluwarsa, sesi kedua mengambil alih, dan resume-nya nggak mengulang kerja dari nol karena jendela yang sudah selesai dikenali dari sidecar dan dipakai ulang. Bagian yang paling saya suka justru skenario (C) dari test yang sama: lucuti satu komponen bernama heartbeat-renew dari salinan kernel, lalu tuntut supaya salinan itu mati dengan cara yang terprediksi pas lock-nya dicuri. Tebakan lama saya soal lock, cukup dicek sekali di awal proses, salah total.

Cek di pintu nggak cukup

Model lama yang enak dipercaya: lock file itu tiket masuk. Sesi yang sempat memegangnya di awal merasa berhak jalan sampai selesai. Masalahnya muncul pas job-nya panjang. etcd mendokumentasikan lease sebagai mekanisme deteksi liveness: cluster memberi TTL, lease kedaluwarsa kalau keepAlive nggak datang dalam periode itu, dan semua key yang terikat lease ikut terhapus saat ia kedaluwarsa [2]. Terjemahan bebasnya: kedaluwarsa itu perilaku normal, bukan exception, dan sesi kedua yang mengambil alih lock bekas kedaluwarsa itu sah.

Yang bikin tidak nyaman: sesi pertama belum tentu benar-benar mati. Di Linux, SIGKILL tidak bisa ditangkap, diblok, atau diabaikan [3], tapi proses juga bisa tersangkut lama tanpa mati, dan begitu bangun ia nggak punya cara alami untuk tahu bahwa dunia sudah berganti pemilik. Cleanup di akhir proses nggak bisa diandalkan, karena nggak ada jaminan proses sempat sampai ke sana. Satu-satunya tempat yang pasti dilewati setiap sesi adalah titik-titik di mana ia menulis.

Fencing token: tulisan yang telat ditolak

Pola yang menutup celah ini namanya fencing token. Kleppmann merangkumnya: setiap permintaan tulis harus membawa token, sebuah angka yang selalu naik setiap lock diambil, dan pihak penyimpanan menolak tulisan yang token-nya lebih kecil dari yang pernah diproses [1]. Kalau sesi lama bangun dan mencoba menulis pakai token lama, tulisan itu ditolak bukan karena keberuntungan, tapi karena pemeriksaannya dilakukan di sisi penyimpanan, di setiap tulisan.

Di kernel ingest, bentuk praktisnya sederhana: satu fungsi pemeriksa token dipanggil di setiap titik penulisan state, baik event, transkripsi, maupun media. Kalau token yang dipegang nggak sama dengan isi lock sekarang, berarti sesi ini dicuri di tengah jalan: proses langsung berhenti tanpa menulis apa pun, dengan alasan lock-stolen-mid-run. Sementara itu, sesi yang sah nggak perlu takut kedaluwarsa prematur, karena setiap event ikut memperpanjang lock, mirip keepAlive pada lease. Cek sekali di pintu memang lebih murah, tapi dia cuma menjaga pintu, bukan datanya.

Resume idempoten, dan guard yang harus bisa gagal

Dua pelajaran lain dari commit ini. Pertama, resume harus idempoten: plan_id dipakai lagi, jendela yang sudah selesai dikenali dari sidecar, dan run yang terbunuh nggak menulis event ganda. Ini syarat, bukan bonus. Kalau sesi kedua harus mengulang kerja dari nol, seluruh cerita di atas jadi jauh lebih mahal.

Kedua, kontrol negatif. Salinan kernel sengaja dibuat tanpa heartbeat-renew, lalu dimasukkan ke skenario pencurian lock. Hasil yang dituntut justru kegagalan: salinan itu harus mati dengan lock-stolen-mid-run. Guard yang belum pernah dilihat gagal itu belum selesai dites, dan kontrol negatif adalah cara termurah untuk melihatnya gagal tanpa menunggu insiden sungguhan. Saya mulai memandang pola ini sebagai paket minimum buat job apa pun yang nulis ke ledger bersama: lock ber-TTL, heartbeat, pemeriksa token di setiap tulisan. Mahalnya bukan di implementasi, tapi di kejadian pertama yang menunggu kalau pola ini ditunda.

Sumber

  1. How to do distributed locking — Martin Kleppmann
  2. etcd API documentation — Lease
  3. signal(7) — Linux man-pages

Artikel terkait