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.
-- 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 mesinFungsi 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.
-- 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)))== 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.
# jalankan demonstrasi permukaan error
lua demo.lua== 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.
-- 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.
// 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_konfigurasimengangkat error terstruktur{ kode, pesan }untuk kandidat yang bukan table, sedangkanmuat_amanmembungkusnya denganpcalldan 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_loadfilelalulua_pcallagar kegagalan skrip dikembalikan sebagai status dan string pada stack, tidak pernah merusak proses host.
Referensi#
- Manual Referensi Lua 5.4 — Lua.org
— Bagian 2.3 tentang penanganan error dan bagian 4.4 tentang penanganan error di C menjadi rujukan pola
pcalldanlua_pcall. - Redis Lua API reference — Redis — Dokumentasi sandbox ketat pada penerjemah Lua tersemat Redis sebagai pembanding strategi pembatasan lapisan skrip.