Agent dan Var

Menjalankan perubahan state asinkron dengan agent serta mengikat ulang var secara dinamis.

Daftar isi

Topik ini membahas agent untuk perubahan state asinkron dan var dinamis untuk binding yang berlaku sementara. Pembahasan mencakup send, await, dan deref pada agent, send-off untuk aksi blocking, validator serta pemulihan error, lalu binding dinamis dan pengantar with-redefs. Setelah topik ini selesai, pekerjaan yang mahal dapat dilepas ke latar belakang dan konfigurasi dapat diganti per lingkup.

Agent dan perubahan state asinkron#

Agent adalah identitas yang nilainya diganti secara asinkron. Fungsi send mengantrekan fungsi pengubah tanpa menunggu pemrosesannya selesai, dan aksi pada satu agent berjalan satu per satu menurut urutan pengirimannya, sehingga perilakunya menyerupai antrean loket yang dilayani satu petugas. Pembacaan nilai tetap dilakukan dengan deref atau @, sedangkan fungsi await menunggu seluruh aksi yang telah dikirim selesai diproses. Agent cocok untuk pembaruan yang mahal atau nonblocking, yaitu aksi yang tidak menahan thread pemanggil.

Contoh berikut mencatat dua nilai sensor suhu ke dalam satu agent.

clojure Contoh: mencatat nilai sensor dengan agent
(def log-sensor (agent []))

(send log-sensor conj {:kanal "suhu" :nilai 28})
(send log-sensor conj {:kanal "suhu" :nilai 29})
(await log-sensor)

(println "Jumlah catatan:" (count @log-sensor))
(prn @log-sensor)

(shutdown-agents)
text Keluaran
Jumlah catatan: 2
[{:kanal "suhu", :nilai 28} {:kanal "suhu", :nilai 29}]

Keluaran menunjukkan dua catatan yang tersusun sesuai urutan pengirimannya. Pemanggilan await menjamin pembacaan dilakukan setelah kedua aksi selesai, sehingga keluaran stabil tanpa penentuan waktu tunggu tertentu. Fungsi send sendiri kembali seketika setelah mengantrekan aksinya, sehingga thread utama bebas melakukan pekerjaan lain di antara dua pengiriman.

send-off, validator, dan pemulihan error#

Fungsi send menjalankan aksi pada thread pool tetap yang ukurannya sebanding dengan jumlah inti prosesor, sehingga cocok untuk aksi singkat. Aksi yang menahan thread lama, misalnya membaca berkas besar, disalurkan lewat send-off yang menggunakan thread pool terpisah agar pool utama tidak habis digunakan aksi blocking. Validator juga berlaku pada agent, dan opsi :validator pada pembuatan agent diperiksa terhadap setiap hasil aksi.

Aksi yang melempar exception atau menghasilkan nilai yang ditolak validator membuat agent masuk keadaan gagal. Dalam keadaan ini agent tidak lagi mengubah nilainya, error-nya dapat dibaca dengan agent-error, dan pemulihannya dilakukan dengan restart-agent yang menetapkan nilai baru. Contoh berikut menunjukkan penolakan validator beserta pemulihannya.

clojure Contoh: validator pada agent dan restart-agent
(def data (agent 10 :validator number?))

(println "Nilai awal:" @data)

; kirim aksi yang menghasilkan nilai tidak valid
(send data (fn [_] "bukan angka"))

; menunggu hingga error aksi tersedia, aksinya sendiri berjalan sangat cepat
(loop [n 0]
  (when (and (nil? (agent-error data)) (< n 10000000))
    (recur (inc n))))

(println "agent-error:" (.getMessage (agent-error data)))
(println "Nilai tetap:" @data)

(restart-agent data 0)
(println "Setelah restart:" @data)

(shutdown-agents)
text Keluaran
Nilai awal: 10
agent-error: Invalid reference state
Nilai tetap: 10
Setelah restart: 0

Aksi yang menghasilkan string ditolak validator, error tercatat sebagai Invalid reference state, dan nilai lama tetap 10. Fungsi restart-agent kemudian memasang nilai 0, dan setelah itu agent menerima aksi berikutnya seperti biasa. Pemisahan antara kegagalan dan pemulihan inilah yang membuat aksi yang gagal tidak menghilangkan nilai yang sudah sah.

Var dinamis dan binding#

Var yang dibuat dengan def berlaku sebagai penamaan tetap, tetapi var dapat diberi metadata ^:dynamic agar nilainya dapat diganti sementara. Penggantian sementara itu dilakukan dengan binding, yang menetapkan nilai var hanya dalam lingkup badannya dan hanya pada thread yang menjalankannya. Konvensi penamaan var dinamis menambahkan tanda bintang di awal dan akhir nama, misalnya *batas-antrean*, bentuk yang biasa disebut earmuffs.

clojure Contoh: binding dinamis batas antrean
(def ^:dynamic *batas-antrean* 100)

(defn muat-cukup? [n]
  (<= n *batas-antrean*))

(println "Batas 100, muatan 50:" (muat-cukup? 50))

(binding [*batas-antrean* 20]
  (println "Batas 20, muatan 50:" (muat-cukup? 50)))
text Keluaran
Batas 100, muatan 50: true
Batas 20, muatan 50: false

Pemeriksaan pertama menggunakan nilai bawaan 100, sedangkan pemeriksaan kedua berjalan dalam binding yang menetapkan batas 20 sehingga keluarannya menjadi false. Pola ini digunakan untuk konfigurasi yang perlu diganti per lingkup, misalnya batas maksimum antrean yang dilonggarkan pada lingkup pemrosesan berkas cadangan. Setelah badan binding berakhir, var kembali ke nilai bawaannya tanpa langkah pemulihan apa pun.

with-redefs untuk pengujian#

with-redefs adalah bentuk penggantian var yang paling ringkas untuk pengujian. Macro ini mengikat ulang satu atau beberapa var ke nilai sementara sepanjang lingkup badannya, tanpa menuntut var tersebut dinamis, sehingga fungsi yang dibungkusnya mengembalikan nilai tiruan pada saat pengujian. Bentuk ini lazim digunakan untuk membungkus fungsi yang bergantung pada waktu, keacakan, atau layanan luar yang tidak boleh dipanggil dalam pengujian.

Kekuatan sekaligus risikonya terletak pada cakupannya. Penggantian with-redefs berlaku global terhadap seluruh thread yang berjalan sepanjang lingkupnya, sehingga pengujian yang dijalankan paralel dapat saling menimpa pengikatan dan menghasilkan keluaran yang tidak dapat diprediksi. Aturan praktisnya adalah menggunakan with-redefs hanya pada pengujian yang berjalan tunggal, memilih var dinamis dengan binding untuk penggantian perilaku yang lebih permanen, dan menyatakan perilaku yang berbeda-beda sebagai argumen fungsi ketika strukturnya memungkinkan.

Kesalahan umum#

Membaca agent tanpa await ketika seluruh aksi perlu dipastikan selesai. Nilai yang dibaca dengan deref adalah nilai terkini, bukan nilai setelah seluruh aksi terproses, sehingga hasil dapat tertinggal dari yang diharapkan. Perbaikannya dengan memanggil await sebelum membaca, sebagaimana contoh pencatatan sensor di atas.

Melupakan shutdown-agents pada skrip yang menggunakan agent. JVM menunggu thread pool agent yang bukan daemon, sehingga program tampak menggantung setelah baris terakhirnya selesai. Perbaikannya dengan memanggil shutdown-agents di akhir skrip atau setelah seluruh aksi selesai.

Menjalankan binding pada var yang tidak dinamis. Binding hanya sah untuk var bermetadata ^:dynamic, dan pelanggarannya ditolak dengan pesan Can't dynamically bind non-dynamic var. Perbaikannya dengan menambahkan metadata ^:dynamic pada definisi var, atau meneruskan nilainya sebagai argumen fungsi bila penggantian per lingkup tidak diperlukan.

Ringkasan#

  • Agent adalah identitas yang diubah secara asinkron lewat send, dibaca dengan deref, dan disinkronkan dengan await.
  • send-off menampung aksi blocking pada thread pool terpisah, sedangkan validator dan restart-agent menjaga serta memulihkan keadaan agent yang gagal.
  • Var dinamis dengan ^:dynamic dan binding memberi nilai sementara per thread, cocok untuk konfigurasi seperti batas maksimum antrean.
  • with-redefs mengikat ulang var secara global sementara dan hanya layak digunakan pada pengujian yang berjalan tunggal.

Referensi#