Scheduled Task yang Aman Dijalankan Berulang
Kalau sudah cukup lama pegang sistem yang jalan 24 jam — apalagi di lingkungan klinik atau rumah sakit yang tidak pernah benar-benar libur — cepat atau lambat kita akan menemui satu kenyataan pahit: scheduled task itu tidak pernah dijamin jalan tepat sekali. Server bisa restart di tengah eksekusi, worker bisa di-deploy ulang, dua instance bisa kebetulan overlap, atau seseorang menjalankan command yang sama secara manual karena panik melihat data belum masuk. Kalau command itu tidak didesain untuk aman dijalankan berulang, hasilnya bisa data dobel, saldo salah hitung, atau notifikasi terkirim dua kali ke pasien. Idempotency bukan fitur tambahan, ia bagian dari desain scheduled task itu sendiri.
Kenapa "jalan sekali" itu asumsi yang rapuh
Scheduler seperti cron atau Laravel Task Scheduling pada dasarnya cuma menjamin memicu command pada waktu tertentu, bukan menjamin command itu selesai bersih tanpa gangguan. Di produksi, saya anggap ada tiga sumber duplikasi yang realistis: overlap karena eksekusi sebelumnya belum selesai saat jadwal berikutnya datang, retry otomatis dari supervisor proses atau queue worker setelah crash, dan yang paling sering — manusia menjalankan ulang command secara manual saat debugging. Kalau kita mendesain dengan asumsi "pasti cuma jalan sekali", ketiga skenario ini akan cepat atau lambat membongkar asumsi itu.
Pola dasar: cek-lalu-tulis dengan kunci yang jelas
Pola paling sederhana adalah memastikan efek sampingnya dikaitkan ke sebuah kunci unik yang mencerminkan "pekerjaan ini", bukan "waktu command dijalankan". Misalnya command harian yang menghasilkan rekap transaksi sebaiknya menyimpan baris dengan kunci unik berupa tanggal, bukan insert baru setiap kali dipanggil:
DailySummary::updateOrCreate(
['summary_date' => $date],
['total' => $total, 'generated_at' => now()]
);updateOrCreate atau upsert jauh lebih aman dibanding create polos, karena command yang jalan dua kali untuk tanggal yang sama akan menimpa baris yang sama, bukan menambah baris baru. Pola yang sama berlaku untuk pengiriman notifikasi: simpan status "sudah terkirim" di tabel, dan cek status itu sebelum mengirim, sehingga command boleh dipanggil ulang kapan saja tanpa mengirim dua kali.
Mencegah overlap di level scheduler dan job
Selain di level data, Laravel juga menyediakan mekanisme untuk mencegah eksekusi tumpang tindih di level proses. Di scheduler, ->withoutOverlapping() memastikan instance baru tidak mulai kalau instance sebelumnya dari command yang sama masih berjalan. Untuk queue job, ShouldBeUnique beserta konfigurasi uniqueId() memakai lock berbasis cache supaya job dengan identitas yang sama tidak diproses dua kali secara paralel:
class SyncPatientRecord implements ShouldQueue, ShouldBeUnique
{
public function uniqueId(): string
{
return $this->recordId;
}
public function uniqueFor(): int
{
return 3600;
}
}Perlu diingat, mekanisme lock ini melindungi dari eksekusi paralel, tapi tidak otomatis melindungi dari duplikasi data kalau logika di dalamnya tetap memakai create tanpa pengecekan. Lock dan idempotency data adalah dua lapis pertahanan yang saling melengkapi, bukan pengganti satu sama lain — sebagai ilustrasi, lock mencegah dua proses menulis bersamaan, sementara upsert memastikan penulisan itu sendiri aman diulang meski lock-nya gagal.
Hal yang sering terlewat
Beberapa jebakan yang menurut pengalaman saya paling sering luput: pertama, memakai increment ($model->increment('count')) di dalam command yang bisa dijalankan ulang — ini nyaris selalu salah kalau tidak dibungkus pengecekan status. Kedua, mengirim efek ke sistem eksternal (email, webhook, API pihak ketiga) tanpa idempotency key di sisi mereka juga, karena lock di sisi kita tidak melindungi dari retry yang dipicu oleh sisi lain. Ketiga, lupa membungkus rangkaian operasi dalam DB::transaction() sehingga command yang terputus di tengah jalan meninggalkan data setengah jadi yang justru lebih sulit dideteksi daripada duplikasi murni.
Takeaway
Anggap setiap scheduled command sebagai fungsi yang suatu saat pasti akan dipanggil lebih dari sekali — entah oleh sistem atau oleh diri sendiri saat debugging jam dua pagi. Desain dari awal dengan kunci unik yang bermakna, pakai updateOrCreate/upsert daripada create polos, kombinasikan withoutOverlapping() atau ShouldBeUnique di level proses dengan idempotency di level data, dan bungkus operasi majemuk dalam transaksi. Sedikit disiplin ini jauh lebih murah dibanding membersihkan data dobel setelah insiden terjadi.