Pipeline malam hari berjalan tanpa pengawasan, dan dunia tengah malam penuh kejadian sesaat: berkas partisi telat beberapa menit, jaringan tersendat, proses pihak ketiga lambat. Kebijakan retry dan timeout adalah cara DAG merespons kejadian itu tanpa membangunkan siapa pun. Pada bagian ini kita memasang kebijakan bawaan di default_args, menggantinya untuk task yang berbeda kebutuhannya, lalu membuktikan bahwa kebijakan tersebut aman dipasang — karena task kita idempoten, mencoba ulang tidak pernah mendouble-kan hasil.

Kebijakan Bawaan lewat default_args #

python dags/etl_pesanan.py — baris 49-54
    default_args={
        "owner": "tim-data-tokokita",
        "retries": 2,
        "retry_delay": timedelta(minutes=5),
        "execution_timeout": timedelta(minutes=15),
    },

Setiap task di dalam DAG menerima kebijakan ini: paling banyak dua percobaan ulang, berjarak lima menit, dan setiap percobaan dibatasi 15 menit. Perilaku Airflow-nya lugas: bila sebuah task mengangkat galat, scheduler menandai percobaan itu gagal, menjadwalkan percobaan berikutnya setelah retry_delay, dan bila seluruh percobaan habis task berstatus failed — status yang kemudian memicu alarm_kegagalan kita lewat trigger_rule="one_failed". Riwayat setiap percobaan (attempt 1, 2, 3) tercatat pada log task dan tampil di UI; itu deskripsi perilaku yang diharapkan, bukan keluaran yang kita jalankan di sini.

Tiga catatan tentang interaksi antar argumen:

  • retries adalah jumlah percobaan ulang — total eksekusi sebuah task adalah retries + 1.
  • execution_timeout berlaku per percobaan, bukan untuk seluruh rangkaian percobaan; task yang selalu habis 15 menit tetap mendapat dua kesempatan berikutnya.
  • Jeda bisa dibuat menanjak dengan retry_exponential_backoff (fitur Airflow yang tidak kita pakai di sini); jeda tetap lima menit sudah cukup untuk pola “partisi telat diunggah”.

Override per Task #

Sebagian task butuh kebijakan berbeda, dan dekorator @task menerima argumen yang sama dengan default_args untuk menimpanya:

python dags/etl_pesanan.py — baris 68
    @task(execution_timeout=timedelta(minutes=10))
python dags/etl_pesanan.py — baris 86
    @task(retries=3, retry_delay=timedelta(minutes=2))

Rasionya per task:

TaskOverrideAlasan
transformexecution_timeout=10 menitMembaca dan membersihkan partisi seharusnya cepat; berjalan lebih dari 10 menit menandakan sesuatu tidak beres, lebih baik gagal cepat daripada menggantung slot worker
muatretries=3, jeda 2 menitMenulis berkas gold dan state paling mungkin terganggu hal sesaat (berkas terkunci, disk penuh sesaat), jadi percobaan lebih banyak dengan jeda lebih pendek
ekstrak, laporantanpa overrideMemakai bawaan: 2 retry, jeda 5 menit

Aturan pembacanya sederhana: nilai eksplisit pada dekorator task selalu menang atas default_args, dan default_args selalu menang atas nilai bawaan Airflow. Kebijakan yang sama untuk semua task cukup ditulis satu kali; pengecualian ditulis di tempat pengecualiannya.

Berapa nilai yang tepat? Tidak ada magic number — angka ditarik dari dua kuantitas nyata:

BesaranPertanyaan penentuNilai kita
retriesBerapa kali gangguan sesaat biasanya pulih?2 (bawaan), 3 untuk muat yang paling sering tersendat berkas
retry_delayBerapa lama sumber gangguan biasanya reda?5 menit (bawaan), 2 menit untuk muat
execution_timeoutBerapa lama task sehat boleh berjalan?15 menit (bawaan), 10 menit untuk transform

Pola berpikirnya: timeout ditarik dari durasi eksekusi sehat ditambah margin, bukan dari perasaan; retry dan jeda ditarik dari pola gangguan yang pernah terjadi. Setelah beberapa minggu berjalan, riwayat durasi task di UI menjadi sumber angka yang lebih baik daripada dugaan awal.

Galat yang Layak Dicoba Ulang #

Retry hanya berguna untuk kegagalan yang mungkin pulih dengan sendirinya. Jenis galat yang dibangkitkan src/tasks.py membedakan dua situasi itu dengan jelas:

Galat dari baca_pesananSifatApakah retry masuk akal
FileNotFoundError — partisi belum diunggahSesaat: unggahan bisa telat menit atau jamYa, itulah alasan jeda 5 menit di ekstrak
ValueError — kolom wajib hilangDeterministik: berkas salah formatTidak, setiap percobaan akan gagal sama
ValueError — format tanggal salahDeterministik: bug kodeTidak, perbaikan ada di kode

Prasyarat Retry: Idempotensi #

Retry berarti task yang sama berjalan lebih dari sekali untuk satu run, sehingga hasilnya harus aman bila dihitung ulang. Gerbangnya ada di jalankan_etl:

python src/tasks.py — baris 243-249
    state = state_etl.baca_state(state_path)
    if state_etl.sudah_sukses(state, tanggal) and not force:
        return _hasil_dari_info(state["terolah"][tanggal], tanggal, "dilewati", state["watermark"])
    ekstrak = baca_pesanan(raw_dir, tanggal)
    bersih = bersihkan_pesanan(ekstrak)
    ringkasan = agregasi_harian(bersih.valid, tanggal)
    berkas_gold = muat_ringkasan(gold_dir, tanggal, ringkasan)

Dua baris pertama adalah gerbang sudah_sukses: tanggal yang sudah tercatat sukses langsung dilewati tanpa menyentuh berkas, dan hanya force=True yang membuka pemrosesan ulang. Idempotensi penuh ditopang tiga mekanisme yang saling melengkapi:

MekanismeLokasiJaminan
Gerbang sudah_suksesstate.pyEksekusi ulang tidak mengerjakan ulang tanggal yang sudah sukses
Gold ditimpa penuhmuat_ringkasanMenulis ulang selalu menghasilkan isi identik, bukan baris menumpuk
Catatan state per tanggal ditulis ulangcatat_suksesKunci terolah[tanggal] diganti, tidak berlipat

Ketiganya kita buktikan dengan eksekusi nyata — skrip demonstrasi di luar bundel proyek menumbuhkan partisi 2026-11-02 pada direktori sementara, lalu memanggil jalankan_etl tiga kali berturut-turut:

Output yang Dihasilkan:

text
== eksekusi pertama: partisi 2026-11-02 ==
[2026-11-02] sukses: 7 baris mentah -> 4 valid, 2 ditolak, 1 duplikat; 3 kota, omzet Rp843,000 -> summary_harian_2026-11-02.csv

== eksekusi kedua tanpa force ==
[2026-11-02] dilewati - sudah sukses sebelumnya (valid 4, omzet Rp843,000, watermark 2026-11-02)

== eksekusi ketiga dengan force=True ==
[2026-11-02] sukses: 7 baris mentah -> 4 valid, 2 ditolak, 1 duplikat; 3 kota, omzet Rp843,000 -> summary_harian_2026-11-02.csv

== isi berkas gold setelah eksekusi ==
tanggal,kota,jumlah_pesanan,omzet
2026-11-02,Bandung,1,175000
2026-11-02,Jakarta,2,570000
2026-11-02,Surabaya,1,98000

Bacaan keluarannya: eksekusi pertama memproses tujuh baris mentah menjadi empat baris valid; eksekusi kedua tanpa force langsung dilewati karena state sudah mencatat sukses; eksekusi ketiga dengan force=True memproses ulang dan menghasilkan berkas gold dengan isi identik — persis skenario “retry setelah keberhasilan yang tidak sempat tercatat”. Perilaku ini juga dikunci oleh pengujian test_jalankan_etl_idempoten_dilewati dan test_jalankan_etl_force_memproses_ulang pada tests/test_tasks.py, sehingga setiap perubahan pada gerbang state langsung terdeteksi.

Dengan kebijakan retry, timeout, dan jaminan idempotensi di tempatnya, satu pertanyaan besar masih terbuka: bagaimana bila yang terlewat bukan satu task, melainkan beberapa hari penuh eksekusi? Jawabannya adalah backfill, pokok bahasan Bagian 7. Teori kegagalan dan percobaan ulang dibahas lebih mendalam pada chapter Retries dan Timeouts serta Idempotency dan Retries, dan daftar lengkap argumen task ada di dokumentasi Apache Airflow .