Rebuild Hanya untuk Pemulihan: Menjaga Ledger Tetap Sehat
Insiden rebuild yang mengubah 20 baris sehat jadi 77 UNKNOWN memaksa kontrak baru: alat pemulihan hanya mengisi kekosongan, tidak pernah menimpa baris sehat.
Ringkasan
Gara-gara prinsip "raw is truth", alat rebuild malah merusak data sehat karena parser ikut baca berkas sub-tab yang bukan data detail. Kontraknya pun diubah jadi recovery-only: baris sehat dipertahankan, sub-tab diabaikan, dan ketemu UNKNOWN berarti nggak nulis apa-apa sama sekali. Karena raw-nya append-only, ledger yang sehat cukup dilengkapi, bukan ditimpa ulang.
Dua puluh baris sehat di ledger menjadi tujuh puluh tujuh baris, lima puluh tujuh di antaranya UNKNOWN. Itu pemandangan di terminal saat saya menjalankan rebuild ledger portal putusan pengadilan pada proyek DemandScope. Recovery-nya sederhana: git checkout ke berkas ledger, dan dua puluh baris sehat kembali. Tapi insiden itu meninggalkan pertanyaan yang lebih penting: kok bisa alat yang tugasnya memelihara data justru yang merusaknya?
Penyebabnya bersih dari misteri. Parser detail ikut membaca berkas sub-tab yang punya akhiran khusus, padahal berkas itu bukan data detail. Docstring fungsi lama justru melegitimasi kelalaian itu: raw is truth, data mentah adalah kebenaran, jadi parse ulang penuh seharusnya aman. Asumsinya: selama sumber tidak berubah, membangun ulang hasilnya tidak mungkin salah.
"Raw is truth" Tidak Cukup
Prinsip raw-is-truth memang benar soal hierarki kebenaran, tapi ia diam-diam melisensikan sesuatu yang berbahaya: kebebasan menulis ulang apa pun selama "dari raw". Faktanya, ketahanan berkas data ada batasnya. Berkas basis data seperti SQLite memang tahan korupsi karena transaksi yang tertulis sebelumnya dirollback otomatis saat crash, tapi pada akhirnya itu tetap berkas biasa: proses apa pun bisa menimpanya dan pustaka tidak bisa membela diri [1].
Praktik vendor mengonfirmasi kehati-hatian ini. pg_resetwal di PostgreSQL didokumentasikan hanya sebagai jalan terakhir saat server menolak start karena korupsi, mewajibkan flag -f eksplisit, dan setelah dijalankan pengguna disuruh segera dump dan restore tanpa operasi modifikasi data sebelum dump [2]. Perintah .recover di CLI SQLite juga kalem: "pulihkan sebanyak mungkin data dari basis data yang korup", bahasa best-effort, bukan jaminan [3]. Alat pemulihan boleh keliru; yang membedakan, alat yang baik dirancang untuk membatasi kerusakan yang bisa ia timbulkan.
Kontrak Baru: Recovery-Only
Saya menulis ulang kontrak rebuild dari rebuild = parse ulang semuanya menjadi recovery-only. Empat penjaganya:
- Baris sehat dipertahankan verbatim, dicocokkan lewat nomor perkara.
- Yang ditambahkan hanya baris yang memang belum ada di ledger.
- Berkas sub-tab diabaikan sepenuhnya.
- Kalau ada baris baru yang terbaca UNKNOWN, tidak ada satu byte pun yang ditulis. All-or-nothing.
def rebuild_ledger(raw_dir, current_ledger):
new_rows = []
for f in raw_files(raw_dir):
if f.name.endswith("-tab.txt"):
continue # sub-tab files are not detail rows
row = parse_detail(f.read_text())
if row.status == "UNKNOWN":
abort("nothing written") # all-or-nothing
if not ledger_has(current_ledger, row.case_no):
new_rows.append(row)
current_ledger.extend(new_rows) # healthy rows stay verbatim
Perubahan terbesar sebenarnya bukan di kode, melainkan di definisi kapan alat ini boleh dipanggil. Ia bukan lagi tombol "segarkan" untuk dipencet tiap kali iseng. Ia alat pemulihan, dan dipanggil hanya saat ada kekosongan yang terverifikasi.
Mengapa Append-Only Jadi Fondasinya
Kontrak recovery-only jalan karena berkas mentahnya tidak pernah berubah. Raw file itu append-only: sesi panen hanya menambah, tidak pernah mengedit lampau. Pola ini standar di sistem log terdistribusi: Kafka menyimpan event secara durable dan tidak menghapusnya setelah dikonsumsi, masa retensinya ditentukan lewat konfigurasi per-topik [4]. Dengan raw yang stabil, ledger yang sehat tidak butuh "diperbarui"; ia cuma butuh dilengkapi.
Dua detail teknis menopang sisanya. Untuk ledger yang berformat SQLite, idempotensi penulisan hanya dijamin UPSERT yang dipicu constraint keunikan, dan itu berarti kunci unik harus dideklarasikan eksplisit di skema [5]. Dan mode jurnal yang mengatur cara transaksi ditulis tetap pada default DELETE kecuali alasan kuat muncul [6], satu variabel penuh kejutan yang sengaja tidak saya sentuh di tengah perbaikan.
Sisa insiden itu kini jadi penjaga permanen: rebuild hanya boleh mengisi kekosongan, tidak pernah menimpa yang sehat. Ledger yang sudah benar bukan pekerjaan rumah alat pemulihan.
## Sumber [1] How To Corrupt An SQLite Database File, sqlite.org [2] pg_resetwal, PostgreSQL Documentation [3] Command Line Shell For SQLite (.recover), sqlite.org [4] Apache Kafka Documentation [5] UPSERT, sqlite.org [6] PRAGMA Statements, sqlite.org