Let it crash

Filosofi let it crash dan batas-batas penerapannya.

Daftar isi

Let it crash adalah filosofi penanganan kegagalan pada BEAM yang membiarkan process yang gagal berhenti, lalu menyerahkan pemulihannya kepada supervisor. Topik ini membahas alasan filosofi itu bekerja pada process yang terisolasi, mendemokan pekerja yang berhenti karena error lalu dihidupkan ulang oleh supervisor, serta menetapkan batas penerapannya, yaitu jenis kegagalan yang layak diantisipasi dan dikembalikan sebagai data. Setelah topik ini dibaca, keputusan antara membiarkan process berhenti dan menangkap kegagalan di tempatnya dapat diambil dengan dasar yang jelas.

Prinsip let it crash#

Isolasi process pada BEAM memberi setiap process state miliknya sendiri, tanpa memori yang digunakan bersama dengan process lain. Ketika sebuah process berhenti karena error, yang rusak hanya state milik process itu, sedangkan process lain tetap bekerja seperti biasa. Kegagalan menjadi peristiwa lokal yang tidak menyebar secara diam-diam ke bagian lain dari program.

Kerusakan yang terbatas itu mengubah sikap kode terhadap error. Menangkap error di tempat kejadian sering kali tidak berguna, karena state pada saat itu sudah tidak dapat dipercaya dan keputusan terbaik adalah menghentikan process yang bersangkutan. Filosofi let it crash menyatakan sikap itu secara langsung, yaitu biarkan process yang gagal berhenti tanpa penjaga buatan di dalam kodenya, dan jangan berusaha memperbaiki state yang mungkin sudah rusak.

Pemulihan diserahkan kepada supervisor yang menghidupkan ulang process anak dari awal. State yang berjalan kembali adalah state awal yang baru dibangun, sehingga kondisinya diketahui bersih dan eksekusi tidak dilanjutkan dari state yang setengah rusak. Supervisor tersusun bertingkat membentuk pohon supervision, sehingga apabila supervisor itu sendiri gagal, kegagalan merambat naik ke supervisor di atasnya. Mekanisme child spec dan strategi restart yang mendukung pemulihan sudah dibahas pada modul Inti OTP, sedangkan topik ini menekankan filosofi di balik mekanisme tersebut.

Pekerja yang berhenti lalu dipulihkan#

Skrip berikut memperagakan siklus gagal dan pulih dalam satu berkas. Pekerja berupa process sederhana yang menerima dua perintah dari mailboxnya, yaitu :kirim yang ditangani seperti biasa dan :pecah yang menimbulkan exception. Tidak ada try atau rescue di dalam pekerja, sehingga exception menghentikannya seketika. Supervisor dengan strategi :one_for_one mengawasi pekerja, sedangkan process utama bertindak sebagai pemantik sekaligus pencatat yang menghitung setiap kali pekerja menyala:

elixir pemantik.exs
# pemantik.exs — pekerja yang berhenti lalu dihidupkan ulang supervisor :one_for_one
Process.register(self(), :utama)

defmodule Pekerja do
  # pekerja berupa process sederhana tanpa penanganan error apa pun
  def start_link(_arg) do
    {:ok, spawn_link(&loop/0)}
  end

  defp loop do
    Process.register(self(), :pekerja)
    send(:utama, :menyala)
    menerima()
  end

  defp menerima do
    receive do
      :kirim ->
        IO.puts("pekerja menangani perintah :kirim")
        menerima()

      :pecah ->
        # kegagalan tidak tertangani: process berhenti, tidak ada penjaga di dalamnya
        raise "perintah :pecah tidak dapat ditangani"
    end
  end
end

anak = %{id: Pekerja, start: {Pekerja, :start_link, [[]]}}
{:ok, _} = Supervisor.start_link([anak], strategy: :one_for_one, name: :pengawas)

cetak_menyala = fn jumlah ->
  receive do
    :menyala ->
      jumlah_baru = jumlah + 1
      IO.puts("pekerja menyala, percobaan ke-#{jumlah_baru}")
      jumlah_baru
  after
    5000 ->
      IO.puts("pekerja tidak menyala")
      jumlah
  end
end

# pekerja pertama menyala bersama supervisor
jumlah = cetak_menyala.(0)

# perintah wajar ditangani
send(:pekerja, :kirim)

# perintah yang membuat pekerja berhenti
send(:pekerja, :pecah)

# supervisor menghidupkan ulang pekerja
jumlah = cetak_menyala.(jumlah)
IO.puts("supervisor melakukan #{jumlah - 1} restart")

# pekerja pengganti kembali melayani perintah
send(:pekerja, :kirim)
text Keluaran
pekerja menyala, percobaan ke-1
pekerja menangani perintah :kirim
pekerja menyala, percobaan ke-2
supervisor melakukan 1 restart
pekerja menangani perintah :kirim

Skrip dapat dijalankan langsung setelah berkasnya disimpan:

bash
# menjalankan skrip pemantik
elixir pemantik.exs
text Keluaran
pekerja menyala, percobaan ke-1
pekerja menangani perintah :kirim
pekerja menyala, percobaan ke-2
supervisor melakukan 1 restart
pekerja menangani perintah :kirim

Urutan pada keluaran memperlihatkan siklus yang lengkap. Pekerja pertama menyala bersama supervisor dan menangani perintah :kirim. Perintah :pecah menghentikan pekerja tanpa ada yang menangkap exceptionnya. Supervisor kemudian menghidupkan pekerja pengganti. Penghitung menyala yang berpindah dari satu ke dua adalah bukti restart yang bersih, sedangkan baris terakhir menunjukkan pekerja pengganti kembali menangani perintah dengan state yang baru dibangun. Pekerja pada skrip sengaja ditulis sebagai process sederhana agar kejadiannya terlihat tanpa lapisan tambahan, dan process GenServer yang melempar error di dalam callback-nya dihidupkan ulang dengan cara yang persis sama.

Batas let it crash: kegagalan yang diantisipasi#

Let it crash ditujukan untuk kegagalan yang tidak diantisipasi, seperti bug logika atau kondisi luar biasa yang membuat kelanjutan eksekusi tidak bermakna. Kegagalan yang diantisipasi menempati posisi yang berbeda. Input pengguna yang salah, data yang tidak lengkap, dan nilai di luar jangkauan adalah alur normal sebuah program, bukan kejadian luar biasa. Untuk kegagalan yang diantisipasi ini, hasil berbentuk {:error, alasan} lebih tepat daripada crash yang menghentikan process.

Alasannya dapat diurai dengan mudah. Crash tidak membantu kegagalan yang seharusnya ditanggapi secara teratur, karena supervisor menghidupkan ulang process tanpa mengubah kondisi yang menyebabkan kegagalan itu. Input yang sama datang lagi pada process pengganti, kegagalan yang sama terulang, dan siklusnya berujung pada supervisor yang menyerah. Sebaliknya, pemanggil yang menerima {:error, alasan} bebas menentukan tindak lanjutnya secara tenang, misalnya menampilkan pesan yang jelas atau meminta input baru.

Fungsi pengurai durasi berikut menggambarkan pembagiannya. Teks yang tidak dapat diurai adalah kegagalan yang diantisipasi, sehingga setiap kemungkinan dikembalikan sebagai nilai:

elixir Contoh: kegagalan yang diantisipasi dikembalikan sebagai nilai
defmodule Durasi do
  # kegagalan yang diantisipasi dikembalikan sebagai nilai, bukan crash
  def uraian(teks) do
    case Integer.parse(teks) do
      {detik, ""} -> {:ok, detik}
      {_detik, sisa} -> {:error, {:sisa_tidak_terurai, sisa}}
      :error -> {:error, :bukan_bilangan}
    end
  end
end

IO.inspect(Durasi.uraian("120"), label: "\"120\"")
IO.inspect(Durasi.uraian("120 detik"), label: "\"120 detik\"")
IO.inspect(Durasi.uraian("cepat"), label: "\"cepat\"")
text Keluaran
"120": {:ok, 120}
"120 detik": {:error, {:sisa_tidak_terurai, " detik"}}
"cepat": {:error, :bukan_bilangan}

Ketiga hasil pada keluaran adalah data yang wajar dan tidak ada satu pun exception yang dilemparkan. Pemanggil uraian/1 cukup mencocokkan pola hasilnya, misalnya menampilkan peringatan apabila sisa teksnya tidak terurai. Durasi yang berhasil diuraikan berlanjut sebagai nilai {:ok, detik} yang dapat langsung digunakan pada perhitungan berikutnya.

Kesalahan umum#

Kesalahan pertama adalah membungkus seluruh isi process dengan try dan rescue demi menjaga program tetap hidup. Penangkapan yang terlalu luas meniadakan isolasi, karena state yang sudah tidak dapat dipercaya terus digunakan dan kegagalan yang seharusnya terlihat justru terkubur. Pengecualian boleh diberikan pada blok try yang sempit di sisi pemanggil, misalnya untuk satu panggilan fungsi tertentu, karena yang meniadakan isolasi adalah menelan seluruh kegagalan di dalam process yang menyimpan state. Perbaikannya mengikuti filosofi topik ini, yaitu biarkan process berhenti, serahkan pemulihan pada supervisor, dan simpan data yang harus selamat di tempat lain, misalnya tabel :ets atau penyimpanan luar.

Kesalahan kedua adalah menganggap restart memulihkan pekerjaan yang sedang berjalan. Process pengganti memulai dari state awal, sehingga pekerjaan setengah jalan hilang dan tidak dilanjutkan secara otomatis. Pembagian yang lazim adalah menyimpan hasil penting di luar process, misalnya pada basis data, dan menjadikan isi process sebagai state sementara yang boleh dibangun ulang kapan pun. Sebelum mengandalkan pemulihan otomatis, pastikan pekerjaan yang dilalui process dapat diulang dengan aman, misalnya dengan menandai progres pada penyimpanan luar sebelum langkah berisiko dijalankan.

Ringkasan#

  • Isolasi process membuat kegagalan bersifat lokal, sehingga membiarkan process yang gagal berhenti tidak merusak process lain.
  • Supervisor menghidupkan ulang process anak ke state awal yang bersih, dan siklus pulihnya terlihat sebagai penghitung menyala yang bertambah.
  • Let it crash tepat untuk kegagalan yang tidak diantisipasi, sedangkan kegagalan yang diantisipasi seperti input yang salah dikembalikan sebagai {:error, alasan}.
  • Restart tidak memulihkan pekerjaan setengah jalan, sehingga pekerjaan yang penting harus dirancang agar dapat diulang dengan aman.

Referensi#