# Menaklukkan Race Condition pada Distributed Cron Job di Kubernetes

> Diterbitkan oleh Literally Web ID pada 20 Oct 2026
> Sumber: https://literally.web.id/articles/menaklukkan-race-condition-pada-distributed-cron-job-di-kubernetes

---

Dalam 15 menit ke depan, kita akan merakit sebuah sistem pertahanan yang solid di atas klaster Kubernetes untuk menghentikan malapetaka eksekusi ganda pada *cron job* terdistribusi kita, sebuah skenario horor yang sering bikin jantungan saat tagihan Redis atau database jebol akibat *race condition*.

## 1. Menu Kita Hari Ini & Alat yang Perlu Disiapkan
Pernahkah Anda mendeploy backend Go atau Node.js ke Kubernetes dengan replika minimal 3, lalu tiba-tiba menyadari bahwa *cron job* bawaan Kubernetes (`CronJob` resource) mengeksekusi tugas penagihan atau pengiriman email sebanyak tiga kali secara bersamaan? Itu terjadi karena K8s *controller* memicu *job* pada setiap pod secara independen tanpa koordinasi lintas node.

Hari ini kita tidak akan bergantung pada keberuntungan. Kita akan meracik implementasi *Distributed Lock* menggunakan Redis (Redis Redlock / SETNX pattern) yang diintegrasikan langsung ke dalam *background worker* aplikasi kita. Siapkan terminal Anda, pastikan `kubectl` terhubung ke klaster uji coba, pastikan Redis cluster atau instance tunggal siap diakses, dan mari kita mulai meracik.

> [!IMPORTANT]
> Jangan pernah mengandalkan waktu lokal server (*system clock*) untuk sinkronisasi *cron job* di lingkungan *cloud* modern. *Drift time* sekecil apapun bisa memicu celah *race condition*.

## 2. Peta Jalan Singkat: Apa yang Sedang Kita Rakit
Sebelum tangan kita kotor dengan kode, mari petakan perjalanan 15 menit kita:
1. **Fondasi & Environment**: Menyiapkan koneksi Redis dan dependensi *worker* aplikasi.
2. **Konfigurasi Inti**: Menuliskan logika *locking* berbasis Redis dengan *atomic operation* (`SET key value NX PX milliseconds`).
3. **Keamanan & Optimasi**: Menangani *deadlock* menggunakan *auto-expiration* (TTL) dan penanganan kegagalan *graceful shutdown*.
4. **Verifikasi & Tes Rasa**: Melakukan *scaling* pod secara agresif dan melihat bagaimana sistem kita menolak eksekusi ganda dengan anggun.

Dari pemahaman arsitektur ini, mari kita langsung terjun ke langkah pertama untuk menyiapkan lingkungan kerja.

## 3. Langkah 1: Mempersiapkan Fondasi & Environment
Kita mulai dengan memastikan aplikasi backend kita (dalam contoh ini kita pakai Go, namun konsepnya identik untuk Node.js/PHP) dapat berbicara lancar dengan Redis. Buat file `main.go` dan masukkan inisialisasi koneksi Redis dasar.

```go
package main

import (
	"context"
	"fmt"
	"log"
	"github.com/go-redis/redis/v8"
)

var ctx = context.Background()

func main() {
	// Menghubungkan worker ke Redis instance
	rdb := redis.NewClient(&redis.Options{
		Addr:     "redis-master.default.svc.cluster.local:6379",
		Password: "", // gunakan password di production
		DB:       0,
	})

	_, err := rdb.Ping(ctx).Result()
	if err != nil {
		log.Fatalf("Gagal terhubung ke Redis: %v", err)
	}
	fmt.Println("Berhasil terhubung ke Redis, siap mengunci cron job!")
}
```

Jalankan dan lihat respons terminal Anda. Koneksi yang stabil ke Redis adalah kunci utama sebelum kita masuk ke logika *locking* yang sesungguhnya.

## 4. Langkah 2: Meracik Konfigurasi Inti Tanpa Basa-basi
Sekarang kita implementasikan algoritma *distributed lock* menggunakan perintah `SET` dengan argumen `NX` (hanya set jika belum ada) dan `PX` (expired dalam milidetik) untuk mencegah *deadlock* jika pod kita tiba-tiba *crash* di tengah jalan.

```go
package main

import (
	"context"
	"fmt"
	"time"
	"github.com/go-redis/redis/v8"
)

func AcquireLock(ctx context.Context, rdb *redis.Client, lockKey string, lockVal string, expiration time.Duration) (bool, error) {
	// SET lockKey lockVal NX PX expiration
	succes, err := rdb.SetNX(ctx, lockKey, lockVal, expiration).Result()
	return succes, err
}

func ReleaseLock(ctx context.Context, rdb *redis.Client, lockKey string, lockVal string) error {
	// Gunakan Lua script untuk memastikan kita hanya menghapus lock milik kita sendiri
	script := `
		if redis.call("get", KEYS[1]) == ARGV[1] then
		    return redis.call("del", KEYS[1])
		else
		    return 0
		end
	`
	res, err := rdb.Eval(ctx, script, []string{lockKey}, lockVal).Result()
	if err != nil {
		return err
	}
	if res == int64(0) {
		return fmt.Errorf("gagal merilis lock: kunci sudah kedaluwarsa atau diambil proses lain")
	}
	return nil
}
```

> [!TIP]
> Selalu gunakan Lua script saat menghapus *lock* di Redis. Ini menghindari kondisi di mana *worker* Anda menghapus *lock* milik proses lain yang sudah mengambil alih setelah TTL Anda habis.

## 5. Langkah 3: Sentuhan Akhir Keamanan & Optimasi
Gabungkan fungsi *lock* tersebut ke dalam *cron handler* aplikasi kita. Di Kubernetes, meskipun deployment Anda memiliki 10 replika pod yang semuanya terpanggil oleh *cron schedule* secara bersamaan, hanya satu pod yang akan berhasil mendapatkan *lock* di Redis.

```go
func RunCronJob(ctx context.Context, rdb *redis.Client) {
	lockKey := "cron:lock:daily-report"
	lockVal := "pod-instance-" + fmt.Sprint(time.Now().UnixNano())
	// Lock berlaku selama 30 detik, cukup untuk eksekusi awal
	ttl := 30 * time.Second

	acquired, err := AcquireLock(ctx, rdb, lockKey, lockVal, ttl)
	if err != nil || !acquired {
		fmt.Println("Cron job sedang dikerjakan oleh pod lain. Melewati eksekusi.")
		return
	}

	defer func() {
		err := ReleaseLock(ctx, rdb, lockKey, lockVal)
		if err != nil {
			fmt.Printf("Peringatan: %v\n", err)
		}
	}()

	fmt.Println("Lock didapat! Menjalankan tugas kritikal cron job...")
	// Letakkan logika bisnis utama di sini
	time.Sleep(5 * time.Second)
	fmt.Println("Cron job selesai dengan sukses.")
}
```

Dari konfigurasi di atas, mari kita lakukan uji coba langsung ke klaster untuk melihat hasilnya beraksi di lapangan.

## 6. Tes Rasa: Memastikan Semuanya Berjalan Sempurna
Deploy aplikasi worker kita ke Kubernetes dengan *replicas: 3*. Paksa pemanggilan *cron job* secara bersamaan. Amati log dari ketiga pod tersebut menggunakan perintah berikut:

```bash
kubectl logs -l app=cron-worker --tail=50 -f
```

Anda akan melihat pemandangan yang memuaskan: tepat satu pod mencetak log `"Lock didapat! Menjalankan tugas kritikal..."`, sementara dua pod lainnya dengan tenang mencetak log `"Cron job sedang dikerjakan oleh pod lain. Melewati eksekusi."` tanpa ada error atau konflik data di database utama.

## 7. Pertolongan Pertama: Jika Terjadi Kendala Kecil
- **Masalah**: Pod mati mendadak saat memegang *lock*, membuat *cron job* berikutnya tertahan selama TTL penuh.
  - **Solusi**: Pastikan nilai TTL disesuaikan secara rasional dengan durasi maksimal eksekusi tugas Anda, atau implementasikan *lock extension* (heartbeat goroutine) jika tugas memakan waktu sangat lama.
- **Masalah**: Redis mengalami *failover* saat *lock* aktif.
  - **Solusi**: Gunakan Redis Sentinel atau Redis Cluster dengan konfigurasi Redlock yang memadai untuk menjamin ketersediaan tinggi (*high availability*).