Supervisor dan GenServer yang sudah dibahas selama ini berdiri sendiri dalam skrip, padahal di proyek nyata keduanya dirakit menjadi satu unit yang dimulai dan dihentikan secara utuh. Unit itu disebut aplikasi (application), yaitu komponen OTP yang memiliki satu titik masuk berupa fungsi start/2, dan dari titik itu seluruh pohon supervision ditumbuhkan. Topik ini membahas kontrak start/2, pendaftaran aplikasi di mix.exs, serta satu skrip utuh yang memulai aplikasi dua tingkat dan memeriksa isi pohonnya dengan Supervisor.which_children/1.
Aplikasi sebagai unit start dan stop#
Aplikasi OTP bukan aplikasi dalam arti program seluler, melainkan unit kode terkecil yang BEAM mulai dan hentikan sebagai satu kesatuan. Modul aplikasi dibuat dengan use Application, dan BEAM memanggil start/2 miliknya pada saat aplikasi dimulai. Fungsi ini wajib mengembalikan {:ok, pid} yang berupa pid akar pohon supervision, dan BEAM mencatat process itu sebagai bagian dari siklus hidup aplikasi. Saat aplikasi dihentikan, seluruh pohon di bawah akarnya dihentikan berurutan, sehingga tidak ada process yang tertinggal tanpa pengawas.
Pendaftaran dilakukan di mix.exs melalui fungsi application/0. Kunci mod menghubungkan nama aplikasi dengan modul aplikasinya, dan inilah yang membuat aplikasi otomatis dimulai ketika proyek berjalan:
# tidak-dijalankan
def application do
[
extra_applications: [:logger],
mod: {AplikasiPersediaan, []}
]
endElemen kedua pada tuple kunci mod adalah argumen yang diteruskan ke start/2, sehingga konfigurasi awal dapat dikirim dari sini. Hubungan antara modul dan aplikasinya juga dapat ditelusuri dari arah sebaliknya, yaitu Application.get_application/1 menerima nama modul dan mengembalikan nama aplikasi yang memiliki modul tersebut. Dalam proyek Mix, panggilan Application.get_application(Persediaan) mengembalikan nama proyek sebagai atom, dan nilai itu berguna untuk membaca konfigurasi milik aplikasi sendiri.
start/2 membangun pohon supervision#
Isi start/2 pada praktiknya selalu satu pola, yaitu menyusun daftar anak lalu memanggil Supervisor.start_link/2. Daftar itu menetapkan urutan start, karena start anak pertama selesai sebelum anak berikutnya dimulai, dan urutan yang sama menjadi dasar strategi :rest_for_one pada topik Supervisor. Dua tingkat pengawasan terbentuk secara alami ketika salah satu anak adalah supervisor lain, misalnya Task.Supervisor, sehingga pohonnya menumbuhkan cabang baru alih-alih menumpuk seluruh process pada satu akar:
Pohon pada diagram menjelaskan pembagian peran yang lazim. GenServer Persediaan memegang state yang harus terus hidup, sedangkan Task.Supervisor menyiapkan cabang khusus untuk pekerjaan sesaat yang anak-anaknya muncul dan hilang sepanjang runtime. Apabila Persediaan mati, supervisor akar menghidupkannya kembali tanpa menyentuh cabang tugas, dan kegagalan satu task tidak pernah menjangkau supervisor akar karena tertahan di cabangnya.
Skrip utuh: aplikasi dua tingkat#
Skrip berikut membangun aplikasi pada diagram tersebut dan memulainya dengan memanggil start/2 secara langsung. Argumen :normal pada pemanggilan itu adalah tipe start yang sama dengan yang dikirim BEAM ketika aplikasi dimulai secara biasa. Dalam proyek Mix, pemanggilan ini dilakukan otomatis saat aplikasi dimulai, sedangkan dalam skrip pemanggilannya dituliskan agar pohonnya tetap terbentuk. Supervisor.which_children/1 kemudian memeriksa isi pohon, dan yang dicetak hanyalah pasangan id dan tipe anak, karena pid yang dikembalikan bersamanya berubah pada setiap eksekusi:
# aplikasi.exs — start/2 sebuah aplikasi membangun pohon supervision dua tingkat
defmodule Persediaan do
use GenServer
def start_link(_arg) do
GenServer.start_link(__MODULE__, %{}, name: :persediaan)
end
@impl true
def init(_arg), do: {:ok, %{buku: 10}}
@impl true
def handle_call(:ringkas, _from, state) do
{:reply, "stok buku: #{state.buku}", state}
end
end
defmodule AplikasiPersediaan do
use Application
@impl true
def start(_tipe, _arg) do
anak_tingkat_dua = [
Persediaan,
{Task.Supervisor, name: :pengawas_tugas}
]
Supervisor.start_link(anak_tingkat_dua, strategy: :one_for_one, name: AplikasiPersediaan.Supervisor)
end
end
# aplikasi dimulai dengan cara yang sama seperti saat BEAM memanggilnya
{:ok, _} = AplikasiPersediaan.start(:normal, [])
anak = Supervisor.which_children(AplikasiPersediaan.Supervisor)
IO.inspect(Enum.map(anak, fn {id, _pid, tipe, _modul} -> {id, tipe} end), label: "isi pohon")
IO.puts("jumlah anak: #{length(anak)}")
IO.puts(GenServer.call(:persediaan, :ringkas))
IO.puts("pengawas tugas terdaftar: #{is_pid(Process.whereis(:pengawas_tugas))}")isi pohon: [{:pengawas_tugas, :supervisor}, {Persediaan, :worker}]
jumlah anak: 2
stok buku: 10
pengawas tugas terdaftar: trueKeluaran mengonfirmasi dua hal sekaligus. Anak pertama pada daftar start berupa GenServer yang dilaporkan bertipe :worker, sedangkan anak kedua berupa supervisor tugas yang dilaporkan bertipe :supervisor, dan urutan pelaporan which_children/1 tidak menjamin urutan start. Konfirmasi lainnya bersifat fungsional, yaitu GenServer yang lahir di dalam start/2 langsung dapat dihubungi melalui namanya, dan pengawas tugas juga sudah terdaftar sebelum skrip utama berlanjut. Apabila aplikasi ini kelak dihentikan, stop/1 milik modul aplikasinya dipanggil dan seluruh pohon di bawahnya berhenti berurutan.
Kesalahan umum#
Kesalahan pertama adalah memulai process penting di luar start/2, misalnya memanggil Persediaan.start_link/1 dari kode lain setelah aplikasi berjalan. Process semacam itu tidak tercatat pada pohon, sehingga matinya tidak pernah diketahui dan tidak pernah dipulihkan. Perbaikannya adalah mencantumkan semua process jangka panjang pada daftar anak di start/2, lalu menyediakan DynamicSupervisor untuk process yang memang baru ada saat runtime.
Kesalahan kedua adalah menempatkan pekerjaan yang lambat di dalam start/2. Fungsi ini dijalankan oleh application controller, dan seluruh tahapan start aplikasi menunggu sampai start/2 kembali, sehingga koneksi lambat atau inisialisasi berat di dalamnya menunda aplikasi secara keseluruhan. Pindahkan pekerjaan semacam itu ke process anak yang diawasi, atau siapkan statenya secara bertahap setelah pohon berdiri.
Kesalahan ketiga adalah menganggap aplikasi sebagai pengganti modul. Aplikasi adalah unit start dan stop pada BEAM, sedangkan batas logika kode tetap berada di tingkat modul dan fungsinya. Banyak proyek nyata hidup dengan satu aplikasi saja, dan pemisahan menjadi beberapa aplikasi dilakukan hanya ketika unit start, konfigurasi, atau dependensinya memang perlu berbeda.
Ringkasan#
- Aplikasi OTP adalah unit yang BEAM mulai dan hentikan secara utuh, dengan titik masuk
start/2yang wajib mengembalikan{:ok, pid}akar pohon supervision. - Kunci
modpadaapplication/0dimix.exsmenghubungkan aplikasi dengan modul aplikasinya, danApplication.get_application/1menelusuri hubungan itu dari arah sebaliknya. - Isi
start/2berupa daftar anak untukSupervisor.start_link/2, dan supervisor lain di dalam daftar itu membentuk tingkat pengawasan baru. Supervisor.which_children/1memeriksa isi pohon dengan pasangan id dan tipe anak, sedangkan pekerjaan lambat tidak boleh ditaruh distart/2.
Referensi#
- Modul Application — Hexdocs Elixir
— dokumentasi siklus hidup aplikasi,
start/2, danget_application/1. - Applications — Erlang Design Principles — prinsip aplikasi dan application controller pada level Erlang.
- Modul Supervisor — Hexdocs Elixir
— bentuk daftar anak dan
which_children/1yang digunakan skrip utuh.