Urutan task sudah terkunci; sekarang kita menjawab pertanyaan kapan task itu berjalan. Scheduling di Airflow sering disalahpahami sebagai “jalankan pada jam X”, padahal sebenarnya berbicara tentang periode data: setiap DAG run bertanggung jawab atas satu rentang waktu tertentu, dan jam jalan hanyalah akibat dari rentang itu. Bagian ini membedah argumen-argumen pada dekorator @dag yang mengatur semuanya, lalu mensimulasikan interval harian dengan fungsi murni di src/ agar semantiknya bisa kita buktikan tanpa Airflow.
Cron, Zona Waktu, dan start_date #
Enam baris berikut di dalam dekorator @dag mengatur seluruh kebijakan penjadwalan:
dag_id=DAG_ID,
description="ETL harian pesanan TokoKita: ekstrak, bersihkan, agregasi, muat",
schedule="30 2 * * *", # setiap hari 02:30 WIB
start_date=pendulum.datetime(2026, 11, 1, tz="Asia/Jakarta"),
catchup=True, # run yang terlewat sejak start_date diproses ulang
max_active_runs=1, # state.json tunggal: jangan menimpanya secara paralelschedule="30 2 * * *" adalah cron harian pukul 02:30 — jam yang sama dengan proyek ETL terjadwal sebelumnya, dipilih karena unggahan partisi dari sistem kasir selesai sekitar pukul dua pagi. Airflow menerima beberapa bentuk nilai schedule, dan pilihannya memengaruhi bentuk data interval:
| Nilai schedule | Makna | Data interval |
|---|---|---|
"30 2 * * *" (cron) | Harian pukul 02:30 | Berbatas cron: [02:30, 02:30) |
"@daily" | Tengah malam setiap hari | [00:00, 00:00) kalender |
None | Tidak dijadwalkan; run dipicu manual | Tidak berlaku |
| Timetable kustom | Aturan interval canggih (mis. per jam kerja) | Ditentukan timetable |
start_date memakai pendulum dengan zona Asia/Jakarta. Tanpa zona waktu eksplisit, jadwal mengikuti zona server — lingkungan pengembangan dan produksi bisa saja berbeda, dan run “pukul 02:30” bergeser diam-diam. Zona WIB dipilih tanpa perhitungan daylight saving karena Indonesia tidak mengenalnya; untuk wilayah yang mengenal, zona bernama tetap menjadi satu-satunya cara benar. Argumen catchup=True mengizinkan scheduler membuat run untuk periode yang terlewat sejak start_date (dibedah pada Bagian 7), dan max_active_runs=1 memaksa satu DAG run aktif pada satu waktu karena state kita satu berkas JSON — dua run paralel akan saling menimpa catatannya.
Data Interval dan ds #
Inilah bagian yang paling sering membingungkan. Untuk cron harian, scheduler membagi waktu menjadi interval berbatas cron: interval pertama dimulai pada cron tick pertama setelah start_date, dan setiap interval berakhir pada cron tick berikutnya. Run untuk sebuah interval dibuat hanya setelah interval berakhir:
Tiga akibat penting dari skema ini:
- Run selalu berjalan setelah datanya lengkap. Run untuk partisi 2026-11-02 berjalan dini hari 03 November, setelah seluruh hari 02 November (dalam arti batas cron) berlalu.
dsadalah bagian tanggal dari logical date. Taskekstrakmenerimadshasil injeksi konteks, sehinggapesanan_2026-11-02.csvdibaca oleh run yang memang bertanggung jawab atas 02 November — bukan oleh run tanggal 03.- Interval tidak harus tengah malam. Karena cron kita berbunyi 02:30, intervalnya berbatas 02:30 ke 02:30, bukan 00:00 ke 24:00. Partisi kita tetap per hari kalender, dan itu aman: label partisi (
ds) tidak berubah, hanya batas intervalnya yang digeser.
| Logical date | Data interval | Waktu run berjalan | ds | Partisi yang dibaca |
|---|---|---|---|---|
| 02 Nov, 02:30 | 02 Nov 02:30 → 03 Nov 02:30 | 03 Nov, 02:30 | 2026-11-02 | pesanan_2026-11-02.csv |
| 03 Nov, 02:30 | 03 Nov 02:30 → 04 Nov 02:30 | 04 Nov, 02:30 | 2026-11-03 | pesanan_2026-11-03.csv |
| 04 Nov, 02:30 | 04 Nov 02:30 → 05 Nov 02:30 | 05 Nov, 02:30 | 2026-11-04 | pesanan_2026-11-04.csv |
catchup dan max_active_runs dalam Praktik #
Perilaku kedua argumen ini paling mudah dipahami lewat skenario. Begitu DAG dideploy pada 05 November dengan start_date 01 November dan catchup=True, scheduler membuat run untuk setiap interval yang sudah berakhir: partisi 01, 02, 03, dan 04 November — persis skenario demonstrasi backfill pada Bagian 7. Dengan catchup=False, hanya interval terbaru yang dijalankan dan empat partisi awal dibiarkan bagi jalur lain (misalnya pemuatan historis sekali jalan).
max_active_runs=1 membatasi eksekusi paralel DAG run. Run hasil catchup diproses satu per satu sesuai urutan logical date — dengan muat yang menulis state.json tunggal, serialisasi ini bukan pemborosan melainkan syarat kebenaran. Konsekuensinya, catchup untuk ratusan hari akan mengantri lama; pada skala seperti itu task harus dirancang paralel-aman (state per partisi, bukan satu berkas) sebelum batas ini dinaikkan.
Tiga kesalahan umum yang layak diantisipasi sejak awal:
start_datedinamis sepertidatetime.now()membuat batas catchup bergeser setiap scheduler mengurai ulang berkas DAG; selalu pakai tanggal tetap.- Mengira
dsadalah “kemarin” —dsadalah logical date run, bukan selisih otomatis dari jam sekarang; untuk pipeline yang mengolah data H-1, posisikan start_date dan cron agar interval H-1 memang menjadi tanggung jawab run tersebut (persis desain kita). - Zona waktu implisit —
start_datetanpa zona waktu mengikuti server; dua lingkungan bisa menjalankan “jam yang sama” pada waktu yang berbeda.
Ketiganya sudah diantisipasi oleh desain kita: start_date ditulis sebagai tanggal tetap, cron dan start_date diposisikan agar ds tepat menjadi partisi H-1, dan zona waktu dinyatakan eksplisit Asia/Jakarta. Kontrak ini juga dikunci pengujian test_argumen_dag_lengkap pada tests/test_dag_structure.py yang memeriksa ketiga nilai tersebut pada berkas DAG.
Simulasi Interval Secara Lokal #
Semantik interval bisa kita buktikan tanpa Airflow karena intinya hanyalah deretan tanggal. Fungsi interval_terjadwal di src/state.py menghasilkan daftar partisi pada sebuah rentang:
def interval_terjadwal(mulai: str, selesai: str) -> list[str]:
"""Seluruh tanggal pada rentang [mulai..selesai] (inklusif).
Ini simulasi lokal berbasis hari kalender dari data interval Airflow,
yang aslinya mengikuti batas cron (lihat Bagian 5 proyek ini).
"""
awal = date.fromisoformat(mulai)
akhir = date.fromisoformat(selesai)
if akhir < awal:
raise ValueError(f"rentang terbalik: {mulai} > {selesai}")
hasil = []
while awal <= akhir:
hasil.append(awal.isoformat())
awal += timedelta(days=1)
return hasilFungsi ini menolak rentang terbalik secara eksplisit (raise ValueError pada baris 101-102) karena rentang terbalik pada backfill adalah bug pemanggil, bukan kondisi yang bisa didiamkan. Skrip demonstrasi kecil — disiapkan di luar bundel proyek — memanggil interval_terjadwal untuk rentang 02 sampai 04 November, lalu mencetak partisi yang harus tersedia beserta ilustrasi run harian mengikuti batas cron 02:30. Ini perhitungan Python murni, bukan keluaran Airflow:
Output yang Dihasilkan:
partisi yang harus tersedia di zona raw:
pesanan_2026-11-02.csv
pesanan_2026-11-03.csv
pesanan_2026-11-04.csv
ilustrasi run harian untuk schedule '30 2 * * *':
ds=2026-11-02 data interval [2026-11-02T02:30:00+07:00 .. 2026-11-03T02:30:00+07:00) berjalan 2026-11-03T02:30:00+07:00
ds=2026-11-03 data interval [2026-11-03T02:30:00+07:00 .. 2026-11-04T02:30:00+07:00) berjalan 2026-11-04T02:30:00+07:00
ds=2026-11-04 data interval [2026-11-04T02:30:00+07:00 .. 2026-11-05T02:30:00+07:00) berjalan 2026-11-05T02:30:00+07:00Deret interval_terjadwal mengikuti hari kalender sedangkan interval Airflow yang sebenarnya berbatas 02:30 — perbedaan yang tidak mengubah label partisi, cukup diingat saat membaca tabel run.
Kolom berjalan pada ilustrasi menegaskan lagi aturan emasnya: run sebuah interval selalu berjalan setelah intervalnya berakhir. Perilaku yang sama dipegang oleh pengujian test_interval_terjadwal_inklusif dan test_interval_terjadwal_menolak_rentang_terbalik pada tests/test_state.py, sehingga perubahan pada fungsi ini — dan akibatnya pada seluruh perhitungan backfill — tidak lolos diam-diam.
Dengan jadwal dan interval yang jelas, kita siap menghadapi kenyataan operasional: kegagalan sesaat di tengah malam. Kebijakan retry dan timeout pada Bagian 6 adalah jawabannya. Semantik penjadwalan yang lebih luas (cron mingguan, timetables kustom) dibahas pada chapter Scheduling, dan rujukan resmi ada di dokumentasi Apache Airflow .