DynamicSupervisor dan Registry

Process yang dibuat saat runtime dan pencariannya.

Daftar isi

Supervisor pada topik sebelumnya menjaga daftar anak yang ditetapkan di awal, sedangkan banyak sistem membutuhkan process yang jumlahnya baru diketahui saat runtime, misalnya satu process per sesi pengguna atau per koneksi. DynamicSupervisor memenuhi kebutuhan itu dengan memisahkan pembuatan supervisor dari pembuatan anaknya, sehingga anak baru ditambahkan satu demi satu melalui start_child/2. Process dalam jumlah banyak itu juga membutuhkan pencarian yang lebih kaya daripada nama lokal, dan Registry mengisi kekosongan tersebut. Topik ini membahas keduanya sebagai satu pasangan, lengkap dengan skrip utuh pekerja bernama yang dibuat, dicari, lalu dimatikan.

Supervisor yang anaknya baru ada saat runtime#

DynamicSupervisor.start_link/1 memulai supervisor tanpa satu pun anak di dalam daftarnya. Anak ditambahkan belakangan melalui DynamicSupervisor.start_child/2, yang menerima child spec atau tuple {Module, arg} yang diubah menjadi child spec oleh modul anak itu sendiri. Setiap panggilan start_child/2 membuat process baru yang berdiri sendiri, dan process utama tetap bebas melakukan hal lain karena pembuatan pekerja tidak lagi terikat pada start supervisor.

DynamicSupervisor tetap menjalankan tugas pengawasan yang sama. Anak yang mati dihidupkan ulang sesuai nilai :restart pada child specnya, dan count_children/1 melaporkan jumlah pekerja yang sedang hidup. Bedanya terletak pada asal daftar anak, yaitu Supervisor biasa mengetahui seluruh anaknya sejak awal, sedangkan DynamicSupervisor menyusun daftarnya dari panggilan runtime. Untuk pekerja buatan runtime, nilai restart: :temporary sering menjadi pilihan yang wajar, karena process sekali pakai yang mati di tengah tugas biasanya tidak layak dihidupkan ulang secara buta.

Nilai balikan start_child/2 juga membawa informasi yang layak dibaca, bukan diabaikan. {:ok, pid} berarti pekerja baru benar-benar dimulai, sedangkan {:error, alasan} melaporkan kegagalan start tanpa melempar error, misalnya karena kunci Registry yang dituju sudah digunakan process lain pada registry dengan kunci unik. Perlakukan kedua bentuk itu sebagai hasil normal operasi, karena pembuatan pekerja runtime memang dapat gagal dan pemicunya biasanya berupa kondisi sistem, bukan bug pada kode.

Registry sebagai pencari process#

Registry menyediakan pencarian nama yang kuncinya boleh berupa term apa pun, bukan hanya atom. Registry dibuat dengan Registry.start_link/1 dan salah satu dari dua mode kunci. Mode keys: :unique menjamin satu kunci hanya dimiliki satu process, sehingga cocok untuk nama pekerja, sedangkan mode keys: :duplicate memperbolehkan banyak process berbagi kunci yang sama dan menjadi dasar pubsub, yaitu pola pesan satu-ke-banyak. Fungsi Registry.lookup/2 menerima kunci lalu mengembalikan daftar pasangan {pid, nilai}, dan pid di dalamnya dapat digunakan untuk mengirim sinyal ke process tersebut.

Nama Registry juga dapat dipasang langsung sebagai nama GenServer melalui bentuk via tuple, yaitu {:via, Registry, {nama_registry, kunci}}. Dengan nama semacam itu, seluruh fungsi GenServer.call/3, GenServer.cast/2, dan opsi name: pada start_link/3 menerima kunci Registry persis seperti menerima nama atom lokal. Selisihnya, kunci via tuple boleh berupa string atau tuple terkomposisi, sehingga pekerja dengan kunci “olah-1” dapat dicari dan dihubungi tanpa menangkap pidnya secara manual.

Mode keys: :duplicate memenuhi kebutuhan yang berlawanan, yaitu satu kunci yang dibagi banyak process. Setiap process yang mendaftarkan diri pada kunci yang sama akan muncul sebagai elemen tersendiri pada hasil Registry.lookup/2, dan mode inilah yang menjadi dasar pola broadcast pesan ke sekumpulan pelanggan pada framework seperti Phoenix. Pemilihan mode dilakukan sekali saat registry dibuat, sehingga keputusan unik atau duplikat harus diambil berdasarkan makna kuncinya, bukan berdasarkan kebutuhan sesaat.

Skrip utuh: pekerja bernama melalui Registry#

Skrip berikut memulai Registry dengan kunci unik dan DynamicSupervisor tanpa anak, lalu membuat dua pekerja satu per satu. Nama pekerja ditulis sebagai via tuple, sehingga GenServer.call/3 dapat menghubunginya melalui kunci yang sama. Pencarian melalui Registry.lookup/2 dibuktikan hanya dari bentuknya, karena pid yang dikembalikan berubah pada setiap eksekusi dan tidak layak dicetak:

elixir pekerja.exs
# pekerja.exs — DynamicSupervisor membuat process pekerja saat runtime, Registry menjadi pencarinya
defmodule Tunggu do
  def hingga(_kondisi, 0), do: :waktu_habis

  def hingga(kondisi, sisa_ms) when sisa_ms > 0 do
    if kondisi.() do
      :ok
    else
      Process.sleep(10)
      hingga(kondisi, sisa_ms - 10)
    end
  end
end

defmodule Pekerja do
  use GenServer

  def start_link(kunci) do
    # nama via tuple: terdaftar di Registry, bukan di tabel nama lokal
    GenServer.start_link(__MODULE__, kunci, name: {:via, Registry, {:daftar_pekerja, kunci}})
  end

  # pekerja buatan runtime tidak layak dihidupkan ulang otomatis
  def child_spec(arg) do
    Map.merge(super(arg), %{restart: :temporary})
  end

  @impl true
  def init(kunci) do
    {:ok, %{kunci: kunci, diproses: 0}}
  end

  @impl true
  def handle_call(:status, _from, state) do
    {:reply, "pekerja #{state.kunci} siap", state}
  end

  @impl true
  def handle_call({:olah, data}, _from, state) do
    {:reply, {:selesai, data}, %{state | diproses: state.diproses + 1}}
  end
end

{:ok, _} = Registry.start_link(keys: :unique, name: :daftar_pekerja)
{:ok, _} = DynamicSupervisor.start_link(name: :pengawas_dinamis)

# dua pekerja dibuat saat runtime, satu per satu
{:ok, _} = DynamicSupervisor.start_child(:pengawas_dinamis, {Pekerja, "olah-1"})
{:ok, _} = DynamicSupervisor.start_child(:pengawas_dinamis, {Pekerja, "olah-2"})

IO.puts(GenServer.call({:via, Registry, {:daftar_pekerja, "olah-1"}}, :status))
IO.inspect(GenServer.call({:via, Registry, {:daftar_pekerja, "olah-1"}}, {:olah, 42}),
  label: "hasil olah"
)

IO.puts(GenServer.call({:via, Registry, {:daftar_pekerja, "olah-2"}}, :status))

# lookup menghasilkan pid, cukup dibuktikan bentuknya tanpa dicetak
IO.puts("pekerja ditemukan: #{match?([{_pid, _nilai}], Registry.lookup(:daftar_pekerja, "olah-1"))}")
IO.puts("pekerja aktif: #{DynamicSupervisor.count_children(:pengawas_dinamis).workers}")

# matikan pekerja pertama, lalu amati Registry kehilangan kuncinya
{pid, _nilai} = hd(Registry.lookup(:daftar_pekerja, "olah-1"))
Process.exit(pid, :kill)
Tunggu.hingga(fn -> Registry.lookup(:daftar_pekerja, "olah-1") == [] end, 2000)

IO.puts("pekerja ditemukan: #{match?([{_pid, _nilai}], Registry.lookup(:daftar_pekerja, "olah-1"))}")
IO.puts("pekerja aktif: #{DynamicSupervisor.count_children(:pengawas_dinamis).workers}")
text Keluaran
pekerja olah-1 siap
hasil olah: {:selesai, 42}
pekerja olah-2 siap
pekerja ditemukan: true
pekerja aktif: 2
pekerja ditemukan: false
pekerja aktif: 1

Dua perubahan pada paruh akhir keluaran menunjukkan kerja sama kedua modul ini. Setelah pekerja pertama dibunuh, kunci “olah-1” hilang dari Registry dengan sendirinya, karena Registry melepas kunci process yang berhenti tanpa perlu pembersihan manual. Jumlah pekerja aktif ikut turun dari dua ke satu, yang berarti DynamicSupervisor mencatat kematian pekerja tersebut dan, sesuai child spec :temporary-nya, membiarkannya tetap mati. Angka yang dicetak hanyalah bidang workers, sedangkan count_children/1 melaporkan bidang lain seperti supervisors dan active untuk seluruh process yang sedang hidup.

Kesalahan umum#

Kesalahan pertama adalah memperlakukan hasil Registry.lookup/2 sebagai pid tunggal. Fungsi itu selalu mengembalikan daftar pasangan {pid, nilai}, meskipun registry-nya menggunakan kunci unik, sehingga pola pencocokannya berupa [{_pid, _nilai}] dan bukan pid langsung. Apabila pid tunggal memang dibutuhkan, ambil elemen pertama daftar setelah memastikan daftarnya tidak kosong.

Kesalahan kedua adalah mewarisi nilai restart default untuk pekerja buatan runtime. Tanpa penyesuaian, supervisor mengisi restart: :permanent dan pekerja yang gagal terus dihidupkan ulang, padahal tugasnya sudah kedaluwarsa bersama pemintanya. Tetapkan restart: :temporary pada child spec pekerja semacam ini agar kematian pekerja berarti berakhirnya tugas, dan nilai :transient bukan kompromi yang aman di sini karena exit abnormal pada pekerja runtime justru memicu restart.

Kesalahan ketiga adalah memanggil GenServer.call/3 ke via tuple yang kuncinya belum terdaftar, misalnya karena registry belum dimulai atau pekerjanya sudah mati. Pemanggil menerima exit :noproc, persis seperti pemanggilan ke nama lokal yang tidak ada. Perlakukan hasil lookup yang kosong sebagai sinyal bahwa pekerja tidak tersedia, bukan sebagai error yang perlu diulang segera.

Ringkasan#

  • DynamicSupervisor.start_link/1 memulai supervisor tanpa anak, dan setiap start_child/2 menambahkan process baru saat runtime lengkap dengan pengawasannya.
  • Registry menyediakan pencarian dengan kunci term apa pun, mode keys: :unique untuk nama tunggal dan keys: :duplicate untuk banyak pemilik kunci.
  • Nama via tuple {:via, Registry, {registry, kunci}} membuat kunci Registry dapat digunakan sebagai nama GenServer tanpa menyentuh pid.
  • Registry melepas kunci process yang berhenti secara otomatis, dan lookup yang kosong adalah sinyal resmi bahwa process tersebut tidak tersedia.

Referensi#