gen_server

Callback gen_server untuk server generik: handle_call, handle_cast, dan state yang mengalir.

Daftar isi

Topik ini memperkenalkan gen_server, behaviour yang menggantikan loop receive rakitan dengan kerangka bawaan: antrean pesan, pencocokan, penanganan waktu habis, dan pelaporan error sudah diurus OTP, sementara program hanya mengisi callback yang benar-benar spesifik. Setelah membaca, layanan antrean tiket berbasis gen_server dapat dibangun dan dikonsumsi dari shell.

Dari loop rakitan ke behaviour#

Loop proses pada modul Proses adalah bentuk paling mentah dari server: receive dengan pola, balasan ke pengirim, dan rekursi state. Bentuk itu bekerja, tetapi setiap program menulis ulang hal yang sama, dan kesalahan halus mudah lolos, misalnya pesan sistem yang tidak ditangani atau balasan yang tercampur. Gen_server memindahkan seluruh loop itu ke pustaka bawaan. Program mendeklarasikan -behaviour(gen_server). lalu mengisi callback: init/1 untuk state awal, handle_call/3 untuk permintaan yang menunggu balasan, dan handle_cast/2 untuk pesan sekali kirim. Atribut behaviour juga membuat compiler memeriksa kelengkapan callback dan memberi peringatan bila ada yang hilang.

Modul antrean tiket#

Modul berikut menggantikan contoh antrean tiket pada modul Proses dengan gen_server. Bagian atas modul adalah API publik, bagian bawah adalah callback:

erlang tiket.erl
-module(tiket).
-behaviour(gen_server).
-export([start_link/0, masuk/1, ambil/0]).
-export([init/1, handle_call/3, handle_cast/2]).

start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

masuk(No) -> gen_server:cast(?MODULE, {masuk, No}).

ambil() -> gen_server:call(?MODULE, ambil).

init([]) -> {ok, []}.

handle_call(ambil, _Dari, []) -> {reply, {error, kosong}, []};
handle_call(ambil, _Dari, [No | Sisa]) -> {reply, {ok, No}, Sisa}.

handle_cast({masuk, No}, Antrian) -> {noreply, Antrian ++ [No]}.

start_link/0 memulai proses sekaligus mendaftarkannya dengan nama atom dari modul, sehingga pemanggil cukup menyebut ?MODULE. init/1 mengembalikan state awal, yaitu list kosong. Pembaruan antrean memakai cast karena pemanggil tidak butuh balasan, sedangkan pengambilan memakai call karena nomor tiketnya adalah balasan yang ditunggu.

Menjalankan dan mengonsumsi layanan#

Sesi berikut memulai server, mengisi antrean, lalu mengosongkannya:

erlang Mengonsumsi server tiket
Erlang/OTP 29 [erts-17.1.1] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit:ns]

Eshell V17.1.1 (press Ctrl+G to abort, type help(). for help)
1> c(tiket).
{ok,tiket}
2> tiket:start_link().
{ok,<0.92.0>}
3> tiket:masuk(1).
ok
4> tiket:masuk(2).
ok
5> tiket:ambil().
{ok,1}
6> tiket:ambil().
{ok,2}
7> tiket:ambil().
{error,kosong}

Perbandingan dengan loop rakitan menunjukkan apa yang hilang dari kode: tidak ada receive, tidak ada Dari ! ... manual, dan tidak ada rekursi state, karena state mengalir sebagai argumen ketiga callback dan nilai kembaliannya menentukan state berikutnya. Tuple {reply, Nilai, State} mengirim balasan sekaligus menyimpan state, sedangkan {noreply, State} hanya menyimpan. Nama terdaftar membuat API tidak perlu membawa PID, dan pemanggilan gen_server:call yang sudah membawa penanda referensi membebaskan program dari masalah balasan yang tercampur yang dibedah pada topik PID dan mailbox.

Introspeksi dengan sys#

Semua behaviour OTP mewarisi fasilitas introspeksi modul sys, dan yang paling sering dipakai adalah sys:get_state/1 untuk membaca state proses yang berjalan tanpa mengganggunya:

erlang Membaca state server tiket
1> c(tiket).
{ok,tiket}
2> tiket:start_link().
{ok,<0.92.0>}
3> tiket:masuk(5).
ok
4> sys:get_state(tiket).
[5]

Nilai kembaliannya persis state yang dipegang server, yaitu list yang berisi satu nomor tiket. Fasilitas ini bekerja karena gen_server membalas pesan sistem OTP, dan itulah keuntungan yang tidak dimiliki loop rakitan pada modul Proses: server buatan sendiri tidak menjawab pesan sistem, sehingga observer dan alat debug tidak dapat membacanya. Karena itu pula gen_server dirancang mewarisi keseluruhan keluarga fasilitas tersebut sejak awal.

call, cast, dan handle_info#

Tiga jalur pesan gen_server memiliki pasangannya masing-masing. call memetakan ke handle_call/3 dan menunggu balasan dengan batas waktu bawaan lima detik, sehingga server yang mati membuat pemanggil menerima error, bukan menggantung selamanya. cast memetakan ke handle_cast/2 dan kembali segera tanpa balasan. Pesan mentah yang dikirim dengan ! langsung ke proses server masuk ke handle_info/2, callback opsional yang lazim dideklarasikan untuk menangkap sisa pesan atau peristiwa sistem. Bila callback itu tidak dideklarasikan, pesan mentah yang tiba dicatat di log dengan teks Undefined handle_info dan server tetap berjalan. Dengan tiga pintu itu, seluruh lalu lintas proses biasa memiliki tempat yang tetap dan terdokumentasi.

Kesalahan umum#

  • Menaruh pekerjaan berat di handle_call tanpa pertimbangan. Selama callback berjalan, server tidak membaca pesan berikutnya, sehingga antrean menumpuk. Pekerjaan panjang membutuhkan desain khusus, misalnya dibagi menjadi langkah kecil.
  • Mengandalkan call untuk pemberitahuan. Call memaksa pemanggil menunggu balasan. Pemberitahuan searah memakai cast, dan pemakaian call yang salah membuat pengirim ikut menderita saat penerima sibuk.
  • Menggunakan antrean Antrian ++ [No] pada antrean raksasa. Operator ++ menyalin sisi kirinya setiap kali ditambahkan. Untuk antrean panjang, gunakan pola akumulator terbalik pada modul Rekursi dan list atau struktur data yang lebih tepat.
  • Menulis loop receive sendiri di dalam callback. Pekerjaan antrean sudah diurus gen_server, dan receive manual di dalam callback memindahkan pesan dari antrean yang diurus OTP ke antrean yang tidak terlihat alat debug, sehingga sistem kehilangan kendali atasnya.

Ringkasan#

  • gen_server menggantikan loop receive rakitan. Program mengisi init/1, handle_call/3, dan handle_cast/2, sementara OTP mengurus antrean dan pelaporannya.
  • start_link dengan opsi {local, Nama} memulai proses dan mendaftarkannya sekaligus.
  • {reply, Nilai, State} menentukan balasan dan state berikutnya, sedangkan {noreply, State} hanya menyimpan state. Pesan mentah masuk ke handle_info/2.
  • call menunggu balasan dengan batas waktu bawaan, sehingga server yang mati terlihat sebagai error di pemanggil, bukan penggantungan tanpa tanda.

Referensi#