โ† Semua pembelajaran / Astro Nol โ†’ Portal Berita
Fase 10 ยท Deploy di AWS

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);
AkibatContoh nyata
Berjalan sebanyak jumlah taskNewsletter terkirim 12 kali ke tiap pembaca
Berjalan pada waktu yang berbedaTiap task memulai timer saat ia start
Hilang saat deployTimer di-reset; tugas terlewat
Bersaing dengan trafik pembacaPekerjaan 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

TugasJadwalKalau terlewat
Sapu pesanan menggantungTiap 15 menitPembayaran tidak tercatat sampai berikutnya
Tagih langganan jatuh tempoHarian, 02.00 WIBPendapatan tertunda sehari
Rekonsiliasi keuanganHarian, 06.00 WIBSelisih ditemukan sehari lebih lambat
Ingatkan kartu akan kedaluwarsaHarianPenagihan gagal yang bisa dicegah
Bersihkan sesi kedaluwarsaHarian, 03.00 WIBTabel membengkak
Sanitasi HTML batchTiap jam, sampai habisTidak apa-apa โ€” bertahap
Rekonsiliasi indeks pencarianHarianHasil pencarian menyimpang
Perbarui sitemap arsipHarianโ€”

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.