Automated cleanup worker bertugas membersihkan data akun atau lisensi inaktif untuk efisiensi penyimpanan dan kepatuhan regulasi. Namun, kesalahan logika sederhana pada kueri retensi dapat mengubah rutinitas pembersihan menjadi bencana operasional: penghapusan massal data aktif secara prematur. Artikel ini membahas anatomi insiden deletion spike, instrumen observasi metrik, serta penerapan runtime kill switch terdistribusi tanpa restart pod.

Akar Masalah: Bug Parsing Timestamp dan Zero-Value Fallback

Penyebab umum kegagalan pada background purge worker adalah asumsi format string waktu saat parsing nilai last_active_at. Pertimbangkan skenario perubahan format database dari RFC3339 ke format standar SQL YYYY-MM-DD HH:MM:SS tanpa penyesuaian parser worker:

  • Parser gagal memproses format baru dan mengembalikan error yang diabaikan atau jatuh ke fallback zero-value (misalnya 0001-01-01T00:00:00Z di Go atau epoch 0 di Python).
  • Evaluasi logika retensi mengecek apakah last_active_at < NOW() - INTERVAL 90 DAY.
  • Karena zero-value bernilai ribuan tahun di masa lalu, setiap record yang dievaluasi langsung memenuhi kualifikasi penghapusan. Worker mengeksekusi bulk deletion ke seluruh database secara instan.

Observasi: Metrik Prometheus dan Alerting Rule

Deteksi dini memerlukan metrik granular pada siklus eksekusi worker. Jangan hanya memantau durasi eksekusi; pantau jumlah baris yang dihapus per satuan waktu.

// Definisi metrik Prometheus di worker (Go)
var (
	purgeDeletedTotal = promauto.NewCounterVec(
		prometheus.CounterOpts{
			Name: "purge_records_deleted_total",
			Help: "Jumlah akumulatif record yang dihapus oleh worker inaktivitas",
		},
		[]string{"entity_type", "status"},
	)
)

Tambahkan PrometheusRule untuk mendeteksi anomali laju penghapusan. Jika laju penghapusan melebihi ambang batas toleransi historis, alert tingkat kritis harus langsung dipicu:

groups:
- name: inactivity_purge_alerts
  rules:
  - alert: InactivityPurgeDeletionSpike
    expr: sum(rate(purge_records_deleted_total{status="success"}[2m])) > 50
    for: 1m
    labels:
      severity: critical
      team: platform-data
    annotations:
      summary: "Deletion spike terdeteksi pada worker purge inaktivitas"
      description: "Worker menghapus {{ $value }} record/detik selama 1 menit terakhir. Ambang batas aman adalah 50/detik."

Implementasi Worker Guard Clause & Redis Kill Switch

Mengubah environment variable atau menghentikan pod via kubectl scale deployment worker --replicas=0 membutuhkan waktu akses cluster dan propagasi deployment (30–90 detik). Pada kecepatan eksekusi tinggi, puluhan ribu baris data bisa hilang dalam rentang waktu tersebut.

Gunakan dynamic flag berbasis Redis sebagai emergency kill switch yang diuji pada setiap iterasi batch.

package main

import (
	"context"
	"errors"
	"log"
	"time"

	"github.com/redis/go-redis/v9"
)

type PurgeWorker struct {
	rdb            *redis.Client
	maxBatchLimit  int
}

var ErrKillSwitchActivated = errors.New("kill switch aktif: eksekusi purge dihentikan")
var ErrBatchAnomalyLimit   = errors.New("jumlah kandidat penghapusan melebihi batas pengaman")

func (w *PurgeWorker) ProcessBatch(ctx context.Context, candidateIDs []int64) error {
	// 1. Guard Clause: Cek status emergency stop dari Redis
	halt, err := w.rdb.Get(ctx, "config:kill_switch:purge_inactive").Bool()
	if err == nil && halt {
		return ErrKillSwitchActivated
	}

	// 2. Circuit Breaker: Batasi ukuran penghapusan per siklus
	if len(candidateIDs) > w.maxBatchLimit {
		log.Printf("Anomaly: %d kandidat ditemukan, melebihi batas aman %d", len(candidateIDs), w.maxBatchLimit)
		return ErrBatchAnomalyLimit
	}

	// 3. Eksekusi penghapusan bertahap
	for _, id := range candidateIDs {
		// Evaluasi ulang kill switch per chunk kecil jika batch berukuran menengah
		if err := w.deleteRecord(ctx, id); err != nil {
			return err
		}
	}
	return nil
}

func (w *PurgeWorker) deleteRecord(ctx context.Context, id int64) error {
	// Implementasi query penghapusan database di sini
	return nil
}

Untuk menghentikan seluruh worker dalam hitungan milidetik, operator hanya perlu mengeksekusi perintah CLI berikut ke instance Redis target:

redis-cli SET config:kill_switch:purge_inactive true

Mitigasi Struktural: Soft-Delete dengan Grace Period 30 Hari

Penghapusan fisik (DELETE FROM ...) secara langsung pada job background membawa risiko pemulihan yang sangat mahal (harus restore point-in-time dari snapshot database). Terapkan strategi transisi status berfase:

  1. Fase Identifikasi: Worker menandai entitas inaktif dengan status pending_purge dan mencatat purge_scheduled_at = NOW() + INTERVAL 30 DAY.
  2. Fase Restriksi Akses: Akun dinonaktifkan di lapisan aplikasi. Pengguna menerima notifikasi peringatan. Jika pengguna login kembali, flag pending_purge dicabut.
  3. Fase Hard Purge: Job terpisah hanya menghapus record dengan kondisi purge_scheduled_at <= NOW() dan status = 'pending_purge'.

Skema dua tahap ini memberikan waktu reaksi mitigasi hingga 30 hari tanpa risiko kehilangan data permanen seketika jika terjadi anomali parser.

SOP Penanganan Insiden & Postmortem Checklist

Langkah Tanggap Darurat

  1. Hentikan Worker Seketika: Aktifkan Redis kill switch (SET config:kill_switch:purge_inactive true). Jika koneksi Redis bermasalah, segera turunkan replika pod worker ke 0.
  2. Identifikasi Rentang Dampak: Jalankan kueri audit untuk menghitung record yang terhapus sejak waktu rilis deployment worker bermasalah.
  3. Rollback Aplikasi: Kembalikan versi deployment worker ke commit stabil sebelumnya via pipeline CI/CD.
  4. Restorasi Data: Jika menggunakan soft-delete, balikkan status data melalui query UPDATE ... SET status = 'active' WHERE status = 'pending_purge'. Jika data terhapus permanen, lakukan Point-in-Time Recovery (PITR) database ke tabel staging terpisah untuk rekonsiliasi.

Postmortem Action Items

  • Dry-Run Mode: Mewajibkan flag dry-run pada staging sebelum rilis production untuk memverifikasi rasio data yang terdampak kueri pembersihan.
  • Hard Batch Threshold: Tambahkan hard limit mutlak pada query level (misalnya LIMIT 500) agar worker menolak memproses data jika jumlah kandidat melonjak drastis dalam satu siklus.
  • Testing Parsing Timestamp: Tambahkan unit test eksplisit untuk menangani string kosong, null, format waktu invalid, serta verifikasi bahwa zero-value waktu memicu error fatal, bukan eksekusi query default.