Permukaan Error

Strategi penanganan kegagalan di dalam sandbox skrip proyek.

Daftar isi

Topik ini menutup permukaan error proyek, yaitu seluruh jalur kegagalan yang dapat dicapai skrip konfigurasi beserta cara mesin dan host menanganinya. Tujuannya satu: kandidat konfigurasi yang bermasalah tidak boleh membuat host Pencatat berhenti. Strateginya bertingkat, yaitu validasi yang mengumpulkan pelanggaran, error terstruktur untuk kegagalan yang tidak dapat ditafsirkan, dan pelaksanaan terlindungi di tepi lapisan skrip.

Kategori kegagalan#

Kegagalan pada lapisan skrip terbagi atas empat kategori yang penanganannya berbeda. Kategori pertama adalah nilai yang tipenya salah, misalnya tema.warna yang berisi bilangan alih-alih string. Kategori kedua adalah nilai di luar batas, misalnya batas_panjang yang bernilai negatif. Kedua kategori ini tidak menghentikan apa pun, karena mesin melaporkannya pada daftar pelanggaran lalu menggunakan nilai bawaan. Kategori ketiga adalah kandidat yang tidak dapat ditafsirkan sama sekali, misalnya nilai nil atau string yang dikirim alih-alih table konfigurasi. Kategori ini mengangkat error terstruktur, karena tidak ada bagian konfigurasi yang dapat diresolusi. Kategori keempat adalah berkas konfigurasi yang rusak secara sintaks, sehingga pemuat Lua menolaknya sebelum kode berjalan, dan kegagalan ini hanya dapat ditangkap oleh pemanggil, bukan oleh mesin.

Pemisahan kategori menentukan siapa yang bertanggung jawab. Pelanggaran validasi adalah urusan mesin, sedangkan pengangkatan dan penangkapan error adalah kerja sama antara mesin dan host. Host yang baik tidak pernah memanggil mesin tanpa pelindung, sebagaimana kode produksi tidak pernah memanggil fungsi luar tanpa menyiapkan penanganan kegagalannya.

Error terstruktur dan muat_aman#

Mesin versi terbaru menambahkan dua mekanisme. Pertama, muat_konfigurasi mengangkat error berbentuk table dengan kode dan pesan, alih-alih string mentah, bila kandidatnya bukan table. Bentuk table membuat penerima dapat memeriksa kode kegagalannya secara programatik tanpa mengurai teks. Kedua, fungsi baru muat_aman membungkus muat_konfigurasi dengan pcall dan menormalisasi hasilnya menjadi pasangan status dan hasil, sehingga pemanggil tidak pernah menyentuh mekanisme pcall secara langsung.

lua mesin.lua
-- mesin.lua
-- Inti mesin konfigurasi aplikasi Pencatat.
-- Versi ini menambahkan muat_aman, yaitu pelaksanaan terlindungi di tepi
-- lapisan skrip. Perilaku muat_konfigurasi dan tampilkan tidak berubah.

local mesin = {}

-- Skema konfigurasi, yaitu tiga grup kunci sah beserta tipe, default, dan batas.
local SKEMA = {
  tema = {
    nama        = { tipe = "string",  default = "gelap" },
    warna       = { tipe = "string",  default = "#1e1e2e" },
    ukuran_teks = { tipe = "integer", default = 14, minimum = 8, maksimum = 32 },
  },
  aturan = {
    batas_panjang = { tipe = "integer", default = 1000, minimum = 1, maksimum = 100000 },
    pola_berkas   = { tipe = "string",  default = "catatan%d+" },
  },
  konten = {
    label = { tipe = "string", default = "Catatan" },
  },
}

-- Nama tipe yang ditampilkan pada pesan pelanggaran.
local function nama_tipe(tipe)
  if tipe == "integer" then
    return "bilangan bulat"
  end
  return tipe
end

-- Nama tipe aktual dari sebuah nilai untuk keperluan pesan pelanggaran.
local function nama_tipe_nilai(nilai)
  local t = type(nilai)
  if t == "number" then
    return math.type(nilai) == "integer" and "bilangan bulat" or "bilangan pecahan"
  end
  return t
end

-- Memeriksa kesesuaian tipe nilai terhadap aturan skemanya.
local function tipe_sesuai(aturan, nilai)
  if aturan.tipe == "integer" then
    return type(nilai) == "number" and math.type(nilai) == "integer"
  end
  return type(nilai) == aturan.tipe
end

-- Memvalidasi satu nilai dan mengembalikan teks pelanggaran atau nil.
local function validasi_kunci(jalur, aturan, nilai)
  if not tipe_sesuai(aturan, nilai) then
    return string.format(
      "TIPE_SALAH: kunci %s seharusnya bertipe %s, tetapi bertipe %s",
      jalur, nama_tipe(aturan.tipe), nama_tipe_nilai(nilai)
    )
  end
  if aturan.tipe == "integer" and (nilai < aturan.minimum or nilai > aturan.maksimum) then
    return string.format(
      "NILAI_DI_LUAR_BATAS: kunci %s seharusnya bernilai antara %d dan %d",
      jalur, aturan.minimum, aturan.maksimum
    )
  end
  return nil
end

-- Menggabungkan nilai default skema dengan kandidat konfigurasi.
-- Kandidat tidak pernah dimodifikasi, hasilnya selalu table baru.
-- Kandidat yang bukan table mengangkat error terstruktur dengan level 0.
function mesin.muat_konfigurasi(kandidat)
  if type(kandidat) ~= "table" then
    error({ kode = "KANDIDAT_TIDAK_SAH",
      pesan = string.format(
        "kandidat konfigurasi seharusnya table, tetapi bertipe %s", type(kandidat)
      ) }, 0)
  end
  local final = {}
  local pelanggaran = {}
  for nama_grup, skema_grup in pairs(SKEMA) do
    final[nama_grup] = {}
    local grup = kandidat[nama_grup]
    if grup ~= nil and type(grup) ~= "table" then
      table.insert(pelanggaran, string.format(
        "TIPE_SALAH: grup %s seharusnya table, tetapi bertipe %s",
        nama_grup, nama_tipe_nilai(grup)))
      grup = nil
    end
    for nama_kunci, aturan in pairs(skema_grup) do
      local nilai = nil
      if grup ~= nil then
        nilai = grup[nama_kunci]
      end
      if nilai == nil then
        final[nama_grup][nama_kunci] = aturan.default
      else
        local temuan = validasi_kunci(nama_grup .. "." .. nama_kunci, aturan, nilai)
        if temuan == nil then
          final[nama_grup][nama_kunci] = nilai
        else
          table.insert(pelanggaran, temuan)
          final[nama_grup][nama_kunci] = aturan.default
        end
      end
    end
  end
  for nama_grup, grup in pairs(kandidat) do
    if SKEMA[nama_grup] == nil then
      table.insert(pelanggaran, string.format(
        "KUNCI_TAK_DIKENAL: grup %s tidak dikenal", nama_grup))
    elseif type(grup) == "table" then
      for nama_kunci in pairs(grup) do
        if SKEMA[nama_grup][nama_kunci] == nil then
          table.insert(pelanggaran, string.format(
            "KUNCI_TAK_DIKENAL: kunci %s.%s tidak dikenal", nama_grup, nama_kunci))
        end
      end
    end
  end
  table.sort(pelanggaran)
  return final, pelanggaran
end

-- Membungkus muat_konfigurasi dengan pcall agar error tidak menyebar ke host.
-- Bila sukses, hasil berupa table konfigurasi final, sedangkan bila gagal
-- hasil berupa table error terstruktur { kode = "...", pesan = "..." }.
function mesin.muat_aman(kandidat)
  local ok, hasil = pcall(mesin.muat_konfigurasi, kandidat)
  if ok then
    return true, hasil
  end
  if type(hasil) == "table" and hasil.kode ~= nil then
    return false, hasil
  end
  return false, { kode = "ERROR_TAK_TERDUGA", pesan = tostring(hasil) }
end

-- Mencetak laporan ringkas konfigurasi final dengan urutan yang tetap.
function mesin.tampilkan(final)
  print(string.format("%-22s %s", "tema.nama", final.tema.nama))
  print(string.format("%-22s %s", "tema.warna", final.tema.warna))
  print(string.format("%-22s %s", "tema.ukuran_teks", final.tema.ukuran_teks))
  print(string.format("%-22s %s", "aturan.batas_panjang", final.aturan.batas_panjang))
  print(string.format("%-22s %s", "aturan.pola_berkas", final.aturan.pola_berkas))
  print(string.format("%-22s %s", "konten.label", final.konten.label))
end

return mesin

Fungsi muat_aman mengembalikan true bersama table konfigurasi final bila pemuatan berhasil, dan false bersama objek error bila gagal. Objek error dari muat_konfigurasi diteruskan apa adanya karena sudah terstruktur, sedangkan error tak terduga dibungkus dengan kode ERROR_TAK_TERDUGA agar bentuk nilai baliknya tetap seragam. Penyeragaman bentuk ini penting di sisi host, karena kode penanganannya tidak perlu meneka-neka bentuk kegagalan yang datang.

Demonstrasi penanganan kegagalan#

Berkas demo.lua memperagakan tiga skenario, yaitu kandidat dengan pelanggaran validasi, kandidat yang bukan table, dan berkas konfigurasi yang rusak sintaksnya.

lua demo.lua
-- demo.lua
-- Demonstrasi permukaan error yang ditangani rapi di tepi lapisan skrip.

local mesin = dofile("mesin.lua")

-- Pelanggaran validasi dilaporkan, dan nilai yang melanggar digantikan default.
print("== kandidat dengan pelanggaran ==")
local ok, final, pelanggaran = pcall(mesin.muat_konfigurasi, {
  tema = { warna = 7 },
  aturan = { batas_panjang = -5 },
  konten = { label = "Catatan Harian", pintasan = { simpan = "Ctrl+S" } },
})
print(string.format("status : %s", tostring(ok)))
for _, temuan in ipairs(pelanggaran) do
  print("  " .. temuan)
end
mesin.tampilkan(final)

-- Kandidat yang tidak dapat ditafsirkan ditangani muat_aman.
print("== kandidat bukan table ==")
local sukses, hasil = mesin.muat_aman("bukan table")
print(string.format("sukses : %s", tostring(sukses)))
print(string.format("kode   : %s", hasil.kode))
print(string.format("pesan  : %s", hasil.pesan))

-- Berkas yang rusak sintaks dimuat dengan dofile di dalam pcall.
print("== berkas konfigurasi rusak ==")
local muat, err = pcall(dofile, "konfigurasi-rusak.lua")
print(string.format("sukses : %s", tostring(muat)))
print(string.format("pesan  : %s",
  tostring(err):match(":%d+: (.+)$") or tostring(err)))
text Keluaran
== kandidat dengan pelanggaran ==
status : true
  KUNCI_TAK_DIKENAL: kunci konten.pintasan tidak dikenal
  NILAI_DI_LUAR_BATAS: kunci aturan.batas_panjang seharusnya bernilai antara 1 dan 100000
  TIPE_SALAH: kunci tema.warna seharusnya bertipe string, tetapi bertipe bilangan bulat
tema.nama              gelap
tema.warna             #1e1e2e
tema.ukuran_teks       14
aturan.batas_panjang   1000
aturan.pola_berkas     catatan%d+
konten.label           Catatan Harian
== kandidat bukan table ==
sukses : false
kode   : KANDIDAT_TIDAK_SAH
pesan  : kandidat konfigurasi seharusnya table, tetapi bertipe string
== berkas konfigurasi rusak ==
sukses : false
pesan  : unexpected symbol near <eof>

Skrip dijalankan dengan perintah berikut, dan keluarannya persis seperti blok Keluaran di bawahnya.

bash
# jalankan demonstrasi permukaan error
lua demo.lua
text Keluaran
== kandidat dengan pelanggaran ==
status : true
  KUNCI_TAK_DIKENAL: kunci konten.pintasan tidak dikenal
  NILAI_DI_LUAR_BATAS: kunci aturan.batas_panjang seharusnya bernilai antara 1 dan 100000
  TIPE_SALAH: kunci tema.warna seharusnya bertipe string, tetapi bertipe bilangan bulat
tema.nama              gelap
tema.warna             #1e1e2e
tema.ukuran_teks       14
aturan.batas_panjang   1000
aturan.pola_berkas     catatan%d+
konten.label           Catatan Harian
== kandidat bukan table ==
sukses : false
kode   : KANDIDAT_TIDAK_SAH
pesan  : kandidat konfigurasi seharusnya table, tetapi bertipe string
== berkas konfigurasi rusak ==
sukses : false
pesan  : unexpected symbol near <eof>

Skenario pertama menunjukkan sifat fail-safe mesin. Kandidat memuat tiga pelanggaran sekaligus, tetapi pemanggilan tetap berstatus true, pelanggaran terurut berdasarkan kodenya, dan table final terisi nilai bawaan untuk setiap kunci yang bermasalah. Skenario kedua memperlihatkan muat_aman mengembalikan status false bersama objek error yang kodenya dapat diperiksa. Skenario ketiga membuktikan bahwa kegagalan sintaks juga tertangkap rapi, karena dofile dipanggil di dalam pcall dan prefiks posisi berkasnya dibuang agar pesannya bersih.

Berkas konfigurasi yang rusak#

Berkas berikut sengaja dibuat tidak sah, yaitu table pembukanya tidak pernah ditutup. Berkas semacam ini tidak pernah dapat dijalankan sendiri, sehingga potongannya diberi penanda tanpa eksekusi dan kegagalannya hanya terlihat saat dimuat pemuat.

lua konfigurasi-rusak.lua
-- tidak-dijalankan
-- Berkas konfigurasi yang sengaja dibuat rusak secara sintaks untuk
-- demonstrasi penanganan kegagalan pemuatan. Tabel berikut tidak pernah
-- ditutup, sehingga pemuat Lua menolak berkas ini pada tahap kompilasi.
return {
  tema = {
    nama = "gelap",

Kegagalan pada berkas ini terjadi pada tahap kompilasi, sebelum satu pun pernyataan dijalankan, sehingga tidak ada validasi mesin yang sempat bekerja. Kasus ini mengingatkan bahwa pemuatan berkas data tetap merupakan operasi yang dapat gagal, dan penangkapannya menjadi tanggung jawab pemanggil dofile, bukan tanggung jawab mesin.

Penanganan kegagalan di sisi host#

Host C menjalankan skrip dengan lua_pcall alih-alih luaL_dofile yang polos, sehingga error dari lapisan skrip dikembalikan sebagai status dan tidak pernah menyeberangi batas proses. Potongan berikut menggambarkan pola tersebut pada level model, karena kompilasinya memerlukan kepala lua.h dan pustaka Lua yang tidak tersedia di lingkungan latihan.

c Contoh: pelaksanaan terlindungi pada host C
// tidak-dijalankan
/* muat dan jalankan skrip konfigurasi secara terlindungi */
if (luaL_loadfile(L, "konfigurasi.lua") != LUA_OK) {
    fprintf(stderr, "skrip gagal dimuat: %s\n", lua_tostring(L, -1));
    lua_pop(L, 1);
    return 1;
}
if (lua_pcall(L, 0, 0, 0) != LUA_OK) {
    fprintf(stderr, "skrip gagal berjalan: %s\n", lua_tostring(L, -1));
    lua_pop(L, 1);
    return 1;
}

Pola luaL_loadfile lalu lua_pcall memisahkan dua tahap yang kegagalannya berbeda, yaitu kompilasi dan pelaksanaan. Error yang muncul pada kedua tahap berada di puncak stack sebagai string, sehingga host membacanya dengan lua_tostring dan melepasnya dengan lua_pop. Pembanding ekosistemnya jelas: Redis menjalankan skrip Lua di dalam sandbox ketat yang meniadakan pustaka berbahaya dan menangkap seluruh kegagalan skrip sebagai pesan error, bukan sebagai kerusakan server.

Ringkasan#

  • Kegagalan lapisan skrip dibagi atas empat kategori, yaitu tipe salah, nilai di luar batas, kandidat yang tidak dapat ditafsirkan, dan berkas yang rusak sintaksnya, dan setiap kategori memiliki penanganan berbeda.
  • muat_konfigurasi mengangkat error terstruktur { kode, pesan } untuk kandidat yang bukan table, sedangkan muat_aman membungkusnya dengan pcall dan menyeragamkan bentuk nilai baliknya.
  • Pelanggaran validasi tidak menghentikan pemuatan, karena mesin mengumpulkannya pada daftar terurut dan menggunakan nilai bawaan untuk setiap kunci yang bermasalah.
  • Host C menggunakan luaL_loadfile lalu lua_pcall agar kegagalan skrip dikembalikan sebagai status dan string pada stack, tidak pernah merusak proses host.

Referensi#