Tugas terjadwal & pekerjaan latar
Menaruh cron di dalam service web adalah kesalahan yang terlihat wajar dan berakibat mahal: dua belas task berarti dua belas kali pekerjaan yang sama.
Intisari
- Jangan pernah menjalankan cron di dalam proses web yang punya lebih dari satu task.
- EventBridge Scheduler memicu tugas ECS terpisah โ satu eksekusi, terjamin.
- Setiap tugas harus idempoten: ia akan berjalan dua kali suatu saat.
- Pasang kunci berbasis database untuk tugas yang benar-benar tidak boleh tumpang tindih.
- Pantau tugas yang tidak berjalan โ kegagalan senyap lebih umum daripada kegagalan berisik.
Kenapa bukan di dalam proses web
// JANGAN. Di 12 task, ini berjalan 12 kali.
setInterval(() => kirimNewsletter(), 60 * 60 * 1000);
| Akibat | Contoh nyata |
|---|---|
| Berjalan sebanyak jumlah task | Newsletter terkirim 12 kali ke tiap pembaca |
| Berjalan pada waktu yang berbeda | Tiap task memulai timer saat ia start |
| Hilang saat deploy | Timer di-reset; tugas terlewat |
| Bersaing dengan trafik pembaca | Pekerjaan berat memperlambat halaman |
Ini juga yang harus kamu matikan di CI3 saat cutover (Fase 9). Crontab CI3 yang masih hidup bersamaan dengan versi Astro-nya menghasilkan tepat masalah yang sama โ dan untuk penagihan langganan, itu berarti pelanggan ditagih dua kali.
Tugas ECS terjadwal
aws scheduler create-schedule \
--name portal-sapu-pesanan \
--schedule-expression "rate(15 minutes)" \
--schedule-expression-timezone "Asia/Jakarta" \
--flexible-time-window '{"Mode":"OFF"}' \
--target '{
"Arn": "arn:aws:ecs:ap-southeast-3:123456789012:cluster/portal",
"RoleArn": "arn:aws:iam::123456789012:role/portal-scheduler",
"EcsParameters": {
"TaskDefinitionArn": "arn:aws:ecs:...:task-definition/portal-tugas",
"LaunchType": "FARGATE",
"TaskCount": 1,
"NetworkConfiguration": {"awsvpcConfiguration": {
"Subnets": ["subnet-priv-a","subnet-priv-b"],
"SecurityGroups": ["sg-portal-web"],
"AssignPublicIp": "DISABLED"}}
},
"Input": "{\"containerOverrides\":[{\"name\":\"tugas\",\"command\":[\"node\",\"tugas.mjs\",\"sapu-pesanan\"]}]}",
"RetryPolicy": {"MaximumRetryAttempts": 2}
}'
// tugas.mjs โ satu titik masuk, banyak tugas
const TUGAS = {
"sapu-pesanan": sapuPesananMenggantung, // Fase 6
"tagih-jatuh-tempo": tagihYangJatuhTempo, // Fase 6
"rekonsiliasi": rekonsiliasiHarian, // Fase 6
"bersihkan-sesi": bersihkanSesiKedaluwarsa, // Fase 5
"sanitasi-batch": bersihkanBatch, // Fase 9
"indeks-cari": rekonsiliasiIndeks, // Fase 9
"kartu-kedaluwarsa": ingatkanKartuKedaluwarsa, // Fase 6
};
const nama = process.argv[2];
const fn = TUGAS[nama];
if (!fn) {
console.error(JSON.stringify({ level: "error", msg: "tugas tidak dikenal", nama }));
process.exit(1);
}
const t0 = Date.now();
try {
const hasil = await fn();
console.log(JSON.stringify({ level: "info", msg: "tugas selesai", nama,
ms: Date.now() - t0, hasil }));
process.exit(0);
} catch (e) {
console.error(JSON.stringify({ level: "error", msg: "tugas gagal", nama,
ms: Date.now() - t0, e: String(e) }));
process.exit(1);
} finally {
await Promise.allSettled([dbBaca.destroy(), dbTulis.destroy()]);
}
Jadwal untuk portalmu
| Tugas | Jadwal | Kalau terlewat |
|---|---|---|
| Sapu pesanan menggantung | Tiap 15 menit | Pembayaran tidak tercatat sampai berikutnya |
| Tagih langganan jatuh tempo | Harian, 02.00 WIB | Pendapatan tertunda sehari |
| Rekonsiliasi keuangan | Harian, 06.00 WIB | Selisih ditemukan sehari lebih lambat |
| Ingatkan kartu akan kedaluwarsa | Harian | Penagihan gagal yang bisa dicegah |
| Bersihkan sesi kedaluwarsa | Harian, 03.00 WIB | Tabel membengkak |
| Sanitasi HTML batch | Tiap jam, sampai habis | Tidak apa-apa โ bertahap |
| Rekonsiliasi indeks pencarian | Harian | Hasil pencarian menyimpang |
| Perbarui sitemap arsip | Harian | โ |
Jadwalkan tugas berat di jam sepi โ tapi ingat replica. Tugas yang menulis banyak (sanitasi batch, pembersihan sesi) akan menaikkan lag replica selama berjalan. Untuk portal Indonesia, jendela paling aman sekitar pukul 02.00โ05.00 WIB.
Idempotensi
// Tugas AKAN berjalan dua kali suatu saat:
// - EventBridge menjamin "at least once", bukan "exactly once"
// - RetryPolicy mengulang setelah kegagalan sementara
// - Seseorang menjalankannya manual sambil yang terjadwal jalan
export async function tagihYangJatuhTempo() {
// order_id deterministik = Midtrans menolak yang berulang (Fase 6)
const orderId = `RNW-${m.id}-${hariIniISO()}`;
// โฆ
}
export async function bersihkanSesiKedaluwarsa() {
// DELETE idempoten menurut sifatnya
let total = 0;
for (;;) {
const h = await dbTulis.deleteFrom("sesi")
.where("kedaluwarsa", "<", new Date()).limit(5000)
.executeTakeFirstOrThrow();
total += Number(h.numDeletedRows);
if (h.numDeletedRows < 5000n) break;
}
return { dihapus: total };
}
Kunci untuk tugas yang tidak boleh tumpang tindih
export async function denganKunci<T>(nama: string, detik: number, fn: () => Promise<T>) {
// GET_LOCK milik MySQL: kunci bernama tingkat sesi.
const { didapat } = await sql<{ didapat: number }>`
SELECT GET_LOCK(${nama}, 0) AS didapat
`.execute(dbTulis).then((r) => r.rows[0]!);
if (didapat !== 1) {
log("warn", "tugas dilewati, sudah berjalan", { nama });
return null;
}
try {
return await fn();
} finally {
await sql`SELECT RELEASE_LOCK(${nama})`.execute(dbTulis);
}
}
Argumen kedua 0 berarti "jangan menunggu" โ kalau kunci sudah dipegang, langsung kembali.
Untuk tugas terjadwal itu perilaku yang benar: kalau eksekusi sebelumnya masih berjalan, lewati saja
yang ini.
Pantau tugas yang TIDAK berjalan
aws cloudwatch put-metric-alarm \
--alarm-name portal-tugas-tagih-tidak-jalan \
--metric-name TugasSelesai --namespace Portal \
--dimensions Name=Tugas,Value=tagih-jatuh-tempo \
--statistic Sum --period 86400 --evaluation-periods 1 \
--threshold 1 --comparison-operator LessThanThreshold \
--treat-missing-data breaching \
--alarm-actions arn:aws:sns:ap-southeast-3:123456789012:portal-alarm
--treat-missing-data breaching adalah bagian yang menentukan. Kegagalan tugas
yang paling berbahaya bukan yang error โ itu terlihat di log. Yang berbahaya adalah tugas yang
tidak pernah dipicu: jadwal terhapus, IAM role berubah, task definition dihapus. Tanpa
pengaturan ini, tidak adanya data diperlakukan sebagai "baik-baik saja", dan penagihan langgananmu bisa
berhenti berminggu-minggu tanpa satu pun peringatan.
Latihan: buat task definition portal-tugas dan jadwal EventBridge untuk sapu
pesanan tiap 15 menit. Verifikasi ia berjalan tepat sekali dengan memeriksa log. Lalu jalankan tugas
yang sama secara manual sambil yang terjadwal berjalan, dan buktikan GET_LOCK membuat yang
kedua melewatinya. Terakhir, pasang alarm "tidak berjalan" dan uji dengan menonaktifkan jadwalnya
sehari.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.