Arsitektur proyek

Pohon supervision, context, dan LiveView sebagai lapisan arsitektur.

Daftar isi

Setelah proyek dipilih, arsitekturnya perlu ditetapkan sebelum fitur pertama ditulis, karena keputusan pada tahap ini mengikat seluruh pengerjaan berikutnya. Topik ini menyusun arsitektur dari tiga lapisan yang seluruhnya berasal dari konsep jalur ini, yaitu pohon supervision sebagai fondasi process, context sebagai batas antara lapisan web dan domain, serta LiveView sebagai lapisan antarmuka. Hasil akhirnya berupa satu skrip yang membangun pohon supervision proyek secara nyata lalu membuktikan bahwa setiap lapisannya bekerja.

Pohon supervision proyek#

Proyek web pada jalur ini menumbuhkan pohon supervision dengan pola yang tetap. Supervisor akar milik aplikasi mengawasi tiga bagian infrastruktur. Phoenix.PubSub menyediakan saluran pesan untuk broadcast lintas process, Registry menyediakan pencarian nama dengan kunci apa pun, sedangkan DynamicSupervisor mengawasi process yang dibuat saat runtime, misalnya satu process kamar setiap kali pengguna membuka kamar baru. Ketiga bagian infrastruktur itu dibuat sekali saat aplikasi dimulai, sedangkan process kamar muncul dan hilang mengikuti kebutuhan:

Aplikasi proyekstart/2Supervisor akarPhoenix.PubSubsaluran pesanRegistrypencarian nama kamarDynamicSupervisorkamar buatan runtimeKamar.Sesiprocess per kamar
Pohon supervision proyek beserta satu kamar buatan runtime

Pembagian peran pada diagram menjelaskan dua sifat pohon ini. Process infrastruktur bersifat permanen dan dihidupkan ulang oleh supervisor akar apabila mati, sedangkan process kamar bersifat sesaat dengan child spec restart: :temporary, sehingga kamar yang ditutup tidak dihidupkan ulang secara buta. Kegagalan satu kamar juga tidak pernah menjangkau kamar lain, karena masing-masing berdiri sebagai anak DynamicSupervisor yang diawasi terpisah. Banyak kamar tumbuh tanpa mengubah satu baris pun pada daftar anak supervisor akar, karena yang bertumbuh hanyalah isi DynamicSupervisor.

Batas context#

Context adalah modul yang menjadi pintu masuk domain, dan dalam proyek ini modul Kamar mengisi peran tersebut. Semua process kamar disembunyikan di balik fungsi Kamar.buka/1 untuk membuka kamar dan Kamar.ringkas/1 untuk membaca ringkasannya. Pemanggil dari lapisan web tidak perlu mengetahui bahwa di baliknya terdapat DynamicSupervisor, Registry, dan GenServer. LiveView hanya memanggil context, sehingga tidak ada satu baris pun kode halaman yang menyentuh process secara langsung.

Batas ini memberi tiga manfaat yang terasa sepanjang pengerjaan. Lapisan web dapat diubah, misalnya dari LiveView ke halaman biasa, tanpa menyentuh domain. Domain dapat diuji dengan ExUnit tanpa memulai endpoint dan tanpa koneksi browser. Kesalahan penggunaan infrastruktur juga tidak menyebar, karena hanya ada satu tempat yang berhak memanggil DynamicSupervisor.start_child/2. Sebaliknya, broadcast PubSub adalah urusan pengiriman pesan antar process, bukan logika domain, sehingga boleh diakses langsung dari lapisan web sebagai saluran pengiriman ke pelanggan.

Posisi LiveView pada arsitektur ini mengikuti batas yang sama. LiveView sendiri adalah process yang menyimpan state sekaligus merender HTML, sehingga statenya berupa data halaman dan bukan data domain. Fungsi mount/3 mengambil data awal melalui context, fungsi handle_info/2 menerima broadcast PubSub lalu memperbarui state halaman, dan setiap perubahan domain tetap kembali ke Kamar. Dengan susunan itu, process halaman lahir dan mati bersama pengunjungnya, sedangkan process kamar yang usianya lebih panjang tetap berada di bawah DynamicSupervisor.

Skrip utuh: pohon supervision kamar#

Skrip berikut membangun pohon pada diagram, membuka satu kamar melalui context, lalu membuktikan tiga hal sekaligus, yaitu isi pohon akar, kamar yang aktif dan terdaftar di Registry, serta broadcast PubSub yang sampai ke pelanggan topiknya. Jalankan dengan elixir pohon.exs setelah berkasnya disimpan:

elixir pohon.exs
# pohon.exs — pohon supervision proyek: PubSub, Registry, DynamicSupervisor, dan satu kamar
Mix.install([{:phoenix, "~> 1.8"}, {:jason, "~> 1.4"}])
Logger.configure(level: :warning)

defmodule Kamar.Sesi do
  use GenServer

  def start_link(nama) do
    GenServer.start_link(__MODULE__, nama, name: {:via, Registry, {:daftar_kamar, nama}})
  end

  # kamar buatan runtime tidak layak dihidupkan ulang secara buta
  def child_spec(nama), do: Map.merge(super(nama), %{restart: :temporary})

  @impl true
  def init(nama), do: {:ok, %{nama: nama, pesan: 0}}

  @impl true
  def handle_call(:ringkas, _dari, state),
    do: {:reply, "kamar #{state.nama}: #{state.pesan} pesan", state}
end

defmodule Kamar do
  # context: pintu domain menuju process kamar, tanpa menyentuh infrastruktur
  def buka(nama), do: DynamicSupervisor.start_child(:pengawas_kamar, {Kamar.Sesi, nama})

  def ringkas(nama), do: GenServer.call({:via, Registry, {:daftar_kamar, nama}}, :ringkas)
end

defmodule PohonKamar do
  use Application

  @impl true
  def start(_tipe, _arg) do
    anak = [
      {Phoenix.PubSub, name: :papan_pubsub},
      {Registry, keys: :unique, name: :daftar_kamar},
      {DynamicSupervisor, name: :pengawas_kamar}
    ]

    Supervisor.start_link(anak, strategy: :one_for_one, name: PohonKamar.Supervisor)
  end
end

{:ok, _} = PohonKamar.start(:normal, [])

# satu kamar dibuat saat runtime melalui context
{:ok, _} = Kamar.buka("ruang-baca")

anak = Supervisor.which_children(PohonKamar.Supervisor)

IO.inspect(
  Enum.sort(Enum.map(anak, fn {id, _pid, tipe, _modul} -> {id, tipe} end)),
  label: "isi pohon akar"
)

IO.puts("kamar aktif: #{DynamicSupervisor.count_children(:pengawas_kamar).workers}")
IO.puts("kamar terdaftar: #{match?([{_pid, _nilai}], Registry.lookup(:daftar_kamar, "ruang-baca"))}")
IO.puts(Kamar.ringkas("ruang-baca"))

# lapisan pengiriman pesan menerima broadcast PubSub pada topik kamar
Phoenix.PubSub.subscribe(:papan_pubsub, "kamar:ruang-baca")
Phoenix.PubSub.broadcast(:papan_pubsub, "kamar:ruang-baca", {:pesan, "halo"})

receive do
  {:pesan, isi} -> IO.inspect(isi, label: "broadcast diterima")
after
  3000 -> IO.puts("broadcast tidak diterima")
end
text Keluaran
isi pohon akar: [
  {Phoenix.PubSub.Supervisor, :supervisor},
  {:daftar_kamar, :supervisor},
  {:pengawas_kamar, :supervisor}
]
kamar aktif: 1
kamar terdaftar: true
kamar ruang-baca: 0 pesan
broadcast diterima: "halo"

Keluaran mengonfirmasi setiap lapisan pada gilirannya. Isi pohon akar menampilkan tiga anak bertipe :supervisor, dan nama anak yang tercetak adalah nama infrastrukturnya, yaitu supervisor milik PubSub, Registry dengan nama :daftar_kamar, serta DynamicSupervisor dengan nama :pengawas_kamar. Kamar aktif berjumlah satu, kuncinya terdaftar di Registry, dan ringkasan dari Kamar.ringkas/1 menunjukkan bahwa context menghubungi kamar tanpa menyentuh pid. Broadcast pada bagian akhir membuktikan saluran pesan siap digunakan LiveView, karena pelanggan topik kamar:ruang-baca menerima isi pesannya sebelum skrip berakhir.

Kesalahan umum#

Kesalahan pertama adalah menaruh logika domain di modul LiveView, misalnya memanggil DynamicSupervisor.start_child/2 langsung dari fungsi handle_event/3. Logika seperti itu tidak dapat diuji tanpa web dan tidak dapat digunakan dari lapisan lain. Perbaikannya adalah memindahkan logika itu ke context Kamar, sehingga yang dipanggil halaman hanyalah Kamar.buka/1.

Kesalahan kedua adalah menumpuk seluruh kamar pada satu GenServer tunggal. State banyak kamar di satu process membuat satu kamar yang lambat menunda semuanya, karena eksekusi GenServer bersifat satu demi satu, dan kegagalan process itu menjangkau seluruh kamar sekaligus. Pola satu process per kamar di bawah DynamicSupervisor menghilangkan kedua masalah itu.

Kesalahan ketiga adalah membuka kamar dengan memanggil Kamar.Sesi.start_link/1 langsung dari context. Process yang dimulai dengan cara itu tidak memiliki pengawas, sehingga process yang mati tidak pernah dipulihkan dan tidak pernah tercatat. Pembuatan process saat runtime harus selalu melewati DynamicSupervisor.start_child/2, persis seperti pada skrip utuh di atas.

Ringkasan#

  • Supervisor akar mengawasi tiga bagian infrastruktur permanen, yaitu Phoenix.PubSub, Registry, dan DynamicSupervisor, sedangkan process kamar lahir di bawah DynamicSupervisor saat runtime.
  • Context Kamar menjadi satu-satunya pintu domain, sehingga LiveView memanggil Kamar.buka/1 tanpa mengetahui process di baliknya.
  • Kamar menggunakan nama via Registry dan child spec restart: :temporary, sehingga dapat dicari tanpa pid dan tidak dihidupkan ulang secara buta.
  • Broadcast PubSub adalah urusan pengiriman pesan yang boleh diakses dari lapisan web, sedangkan logika domain tetap di dalam context.

Referensi#