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 #
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:
retriesadalah jumlah percobaan ulang — total eksekusi sebuah task adalahretries + 1.execution_timeoutberlaku 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:
@task(execution_timeout=timedelta(minutes=10)) @task(retries=3, retry_delay=timedelta(minutes=2))Rasionya per task:
| Task | Override | Alasan |
|---|---|---|
transform | execution_timeout=10 menit | Membaca dan membersihkan partisi seharusnya cepat; berjalan lebih dari 10 menit menandakan sesuatu tidak beres, lebih baik gagal cepat daripada menggantung slot worker |
muat | retries=3, jeda 2 menit | Menulis berkas gold dan state paling mungkin terganggu hal sesaat (berkas terkunci, disk penuh sesaat), jadi percobaan lebih banyak dengan jeda lebih pendek |
ekstrak, laporan | tanpa override | Memakai 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:
| Besaran | Pertanyaan penentu | Nilai kita |
|---|---|---|
retries | Berapa kali gangguan sesaat biasanya pulih? | 2 (bawaan), 3 untuk muat yang paling sering tersendat berkas |
retry_delay | Berapa lama sumber gangguan biasanya reda? | 5 menit (bawaan), 2 menit untuk muat |
execution_timeout | Berapa 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_pesanan | Sifat | Apakah retry masuk akal |
|---|---|---|
FileNotFoundError — partisi belum diunggah | Sesaat: unggahan bisa telat menit atau jam | Ya, itulah alasan jeda 5 menit di ekstrak |
ValueError — kolom wajib hilang | Deterministik: berkas salah format | Tidak, setiap percobaan akan gagal sama |
ValueError — format tanggal salah | Deterministik: bug kode | Tidak, 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:
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:
| Mekanisme | Lokasi | Jaminan |
|---|---|---|
Gerbang sudah_sukses | state.py | Eksekusi ulang tidak mengerjakan ulang tanggal yang sudah sukses |
| Gold ditimpa penuh | muat_ringkasan | Menulis ulang selalu menghasilkan isi identik, bukan baris menumpuk |
| Catatan state per tanggal ditulis ulang | catat_sukses | Kunci 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:
== 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,98000Bacaan 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 .