Penanganan error

Pola penanganan error di pipeline, meliputi percobaan ulang dan dead-letter.

Daftar isi

Topik ini membahas dua pola penanganan error pada pipeline batch, yaitu percobaan ulang untuk kegagalan sementara dan dead-letter untuk baris yang tidak dapat diselamatkan. Aturan pakai keduanya diuraikan terlebih dahulu, kemudian satu skrip demonstrasi menampilkan keduanya pada satu pekerjaan muat. Setelah membacanya, pipeline dapat gagal dengan cara yang terkendali dan meninggalkan jejak yang dapat ditindaklanjuti.

Dua kelas kegagalan#

Kegagalan pada pipeline terbagi dua kelas yang penanganannya berbeda, yaitu kegagalan sementara yang pulih apabila dicoba lagi dan kegagalan permanen yang pasti terulang bila masukannya sama. Kegagalan sementara berasal dari lingkungan, misalnya gudang yang sedang sibuk, koneksi jaringan yang terputus, atau kunci tabel yang belum lepas. Kegagalan permanen berasal dari data dan kode, misalnya baris dengan angka rusak, skema sumber yang bergeser, atau kunci yang kembar.

KelasContohPola penangananAkhir yang diharapkan
Sementaragudang sibuk, timeout jaringanpercobaan ulang berbataspekerjaan berhasil pada percobaan berikutnya
Permanenbaris cacat, skema bergeserdead-letter lalu lanjutbaris buruk tercatat, baris baik tetap terkirim

Kebalikan dari kedua pola itu sama-sama merugikan. Kegagalan sementara yang dibiarkan tanpa percobaan ulang mematikan pekerjaan karena gangguan yang akan pulih sendiri, sedangkan baris buruk yang dicoba terus-menerus membuang seluruh jendela pemrosesan untuk satu baris yang tidak akan pernah berhasil. Memisahkan kedua kelasnya sejak awal membuat keputusan penanganannya tidak lagi subjektif.

Percobaan ulang dengan batas dan backoff#

Percobaan ulang (retry) mengulang operasi yang gagal dengan batas jumlah percobaan dan jeda antarpercobaan yang memanjang, pola yang dikenal sebagai backoff. Batasnya wajib ada, karena percobaan tanpa akhir pada kegagalan permanen membuat pekerjaan menggantung selamanya sambil terus membayar komputasinya. Jeda yang memanjang, misalnya berlipat dua pada setiap percobaan, memberi waktu bagi sistem yang sibuk untuk pulih tanpa menambah bebannya.

Orkestrator menyediakan pola ini sebagai pengaturan, dan pada Airflow setiap task dapat diberi jumlah percobaan ulang beserta jeda antarpercobaannya lewat konfigurasi default_args.

python Pengaturan percobaan ulang pada task Airflow
# tidak-dijalankan
from datetime import timedelta

default_args = {
    "retries": 3,
    "retry_delay": timedelta(minutes=5),
}

Nilai pada contoh itu memberi tiga kesempatan bagi task yang gagal, dengan jeda lima menit di antara percobaannya. Percobaan ulang hanya aman diberikan pada pekerjaan yang idempoten, karena task yang diulang akan menulis ulang hasil runnya yang gagal. Pada pipeline yang sudah idempoten, pengaturan ini membuat gangguan kecil pulih tanpa campur tangan, sedangkan kegagalan yang sungguh permanen tetap berhenti setelah batasnya dan menyalakan peringatan.

Dead-letter untuk baris yang tidak dapat diselamatkan#

Dead-letter adalah tempat penyimpanan khusus untuk baris yang gagal diproses, sebagaimana antrian surat yang tidak dapat terkirim, sehingga baris itu tidak ikut menghentikan baris lain. Pada pipeline berbasis berkas, bentuk yang paling mudah dipakai adalah berkas JSON Lines, yaitu satu objek JSON per baris, karena berkasnya cukup ditambah tanpa perlu dibaca ulang. Setiap catatan memuat alasan penolakan, baris asli yang utuh, dan penanda run agar barisnya dapat ditelusuri kembali.

Tempat ini baru berguna bila ditinjau. Triage berkala yang membaca catatannya menjawab tiga hal, yaitu pola baris buruk yang berulang, skema sumber yang bergeser, dan baris yang layak dimuat ulang setelah diperbaiki di sumbernya. Jumlah baris pada dead-letter juga layak menjadi angka yang dipantau, karena lonjakan mendadak biasanya berarti ada perubahan di sumber, bukan kebetulan semata.

Demonstrasi percobaan ulang dan dead-letter#

Skrip berikut memuat empat baris peminjaman buku dengan satu baris cacat ke gudang yang dua permintaan pertamanya sengaja ditiru menolak koneksi. Validasi baris berjalan lebih dulu untuk memisahkan baris yang lolos dari baris yang masuk dead-letter, kemudian barisan yang lolos dimuat dengan percobaan ulang berbatas. Dead-letter lama dihapus di awal skrip, sehingga setiap run meninggalkan catatan yang bersih.

python muat-dengan-retry.py
"""Pola penanganan error saat muat: percobaan ulang dan dead-letter."""

import json
import time
from pathlib import Path

DEAD_LETTER = Path("dead_letter.jsonl")
PERCOABAAN_MAKS = 3
JEDA_DETIK = 0.1

BARISAN = [
    {"id": "P001", "tanggal": "2026-10-01", "jumlah_buku": 2},
    {"id": "P002", "tanggal": "2026-10-01", "jumlah_buku": 1},
    {"id": "P003", "tanggal": "2026-10-02", "jumlah_buku": "banyak"},
    {"id": "P004", "tanggal": "2026-10-03", "jumlah_buku": 3},
]


class GudangSibuk(Exception):
    """Kegagalan sementara yang masih memungkinkan percobaan ulang."""


kirim_gagal_hingga = 2
jumlah_kirim = 0


def kirim_ke_gudang(barisan: list[dict]) -> None:
    """Simulasi koneksi muat yang dua permintaan pertamanya ditolak."""
    global jumlah_kirim
    jumlah_kirim += 1
    if jumlah_kirim <= kirim_gagal_hingga:
        raise GudangSibuk("gudang menolak koneksi")


def muat_dengan_percobaan_ulang(barisan: list[dict]) -> int:
    """Coba muat sampai batas percobaan dengan jeda antarpercobaan."""
    for percobaan in range(1, PERCOABAAN_MAKS + 1):
        try:
            kirim_ke_gudang(barisan)
            return percobaan
        except GudangSibuk:
            print(f"  percobaan {percobaan} gagal: gudang menolak koneksi")
            time.sleep(JEDA_DETIK)
    raise RuntimeError(f"muat gagal setelah {PERCOABAAN_MAKS} percobaan")


def tulis_dead_letter(baris: dict, alasan: str) -> None:
    """Catat satu baris buruk ke berkas dead-letter berformat JSON Lines."""
    catatan = {"id": baris["id"], "alasan": alasan, "baris_asli": baris}
    with DEAD_LETTER.open("a", encoding="utf-8") as berkas:
        berkas.write(json.dumps(catatan, ensure_ascii=False) + "\n")


# persiapan: hapus dead-letter lama agar setiap run bermula dari keadaan nol
DEAD_LETTER.unlink(missing_ok=True)

lolos: list[dict] = []
print("Validasi baris:")
for baris in BARISAN:
    try:
        baris["jumlah_buku"] = int(baris["jumlah_buku"])
    except (TypeError, ValueError):
        tulis_dead_letter(baris, "jumlah_buku bukan angka")
        print(f"  {baris['id']} ditolak, dicatat ke dead_letter.jsonl")
    else:
        lolos.append(baris)
        print(f"  {baris['id']} lolos")

print("Muat barisan yang lolos:")
percobaan = muat_dengan_percobaan_ulang(lolos)
print(f"  terkirim {len(lolos)} baris pada percobaan ke-{percobaan}")
print("Isi dead_letter.jsonl:")
print(DEAD_LETTER.read_text(encoding="utf-8"), end="")
text Keluaran
Validasi baris:
  P001 lolos
  P002 lolos
  P003 ditolak, dicatat ke dead_letter.jsonl
  P004 lolos
Muat barisan yang lolos:
  percobaan 1 gagal: gudang menolak koneksi
  percobaan 2 gagal: gudang menolak koneksi
  terkirim 3 baris pada percobaan ke-3
Isi dead_letter.jsonl:
{"id": "P003", "alasan": "jumlah_buku bukan angka", "baris_asli": {"id": "P003", "tanggal": "2026-10-02", "jumlah_buku": "banyak"}}

Baris P003 yang jumlah bukunya tertulis sebagai kata tidak pernah mencoba menembus gudang, karena kesalahannya permanen dan penanganan yang tepat adalah pencatatan. Sebaliknya, gangguan koneksi yang ditiru berlaku untuk seluruh muatan, sehingga percobaan ulang menuntaskannya pada percobaan ketiga. Berkas dead_letter.jsonl muncul di direktori kerja dan berisi satu catatan lengkap dengan alasan serta baris aslinya, siap dibaca pada triage berikutnya. Perhatikan bahwa kedua pola bekerja pada lapisan yang berbeda, yaitu percobaan ulang pada keseluruhan pekerjaan dan dead-letter pada tingkat baris.

Kesalahan umum#

  1. Percobaan ulang tanpa batas. Penyebabnya, harapan bahwa kegagalan berikutnya pasti pulih. Akibatnya, pekerjaan menggantung selamanya pada kegagalan permanen sambil terus menghabiskan komputasi. Perbaikannya, tetapkan batas percobaan yang jelas dan panjangkan jeda antarpercobaannya, lalu gagalkan pekerjaan secara eksplisit setelah batasnya tercapai.
  2. Menelan error dengan blok except yang kosong. Penyebabnya, keinginan agar pipeline terus berjalan apa pun yang terjadi. Akibatnya, kegagalan tidak terlihat dan baris hilang diam-diam tanpa catatan. Perbaikannya, setiap penangkapan error harus menghasilkan sesuatu yang terlihat, baik catatan dead-letter, pencatatan log, atau penghentian pekerjaan.
  3. Dead-letter tanpa alasan dan konteks. Penyebabnya, hanya isi barisnya yang disimpan. Akibatnya, triage tidak dapat memutuskan apa pun karena tidak tahu mengapa baris itu ditolak. Perbaikannya, simpan alasan penolakan, baris asli, dan penanda run pada setiap catatan.

Ringkasan#

  • Kegagalan sementara ditangani percobaan ulang berbatas dengan jeda yang memanjang, sedangkan baris yang gagal permanen ditangani dead-letter.
  • Percobaan ulang hanya aman pada pekerjaan yang idempoten, dan dead-letter hanya berguna bila catatannya ditinjau serta dipantau.
  • Kedua pola bekerja pada lapisan yang berbeda, yaitu percobaan ulang pada keseluruhan pekerjaan dan dead-letter pada tingkat baris.

Referensi#