# Manajemen Background Job Skala Enterprise dengan Redis dan Supervisor

> Diterbitkan oleh Literally Web ID pada 20 Oct 2026
> Sumber: https://literally.web.id/articles/manajemen-background-job-skala-enterprise-dengan-redis-dan-supervisor

---

## 1. Menu Kita Hari Ini & Alat yang Perlu Disiapkan

Dalam 15 menit ke depan, kita akan merakit sistem pemrosesan background job kelas produksi dari nol sampai berjalan mulus. Lupakan sejenak skrip cron job rapuh yang dijalankan tiap menit atau request HTTP timeout karena backend Anda terpaksa mengolah file PDF 50 MB secara synchronous.

Bayangkan dapur restoran bintang lima saat jam makan malam: pesanan masuk tidak boleh membuat koki pingsan, dan tiket pesanan tidak boleh hilang tertiup angin. Hari ini, Redis bertindak sebagai papan tiket pesanan yang super cepat dan tahan banting, sementara Supervisor adalah manajer operasional dapur yang memastikan para koki (proses worker) selalu siaga di posnya masing-masing.

Sebelum kita menyalakan kompor, pastikan bahan-bahan berikut sudah tersedia di meja kerja Linux Anda (Ubuntu 22.04/24.04 atau Debian 12):

*   **Server Linux Berbasis Debian/Ubuntu** dengan akses `sudo`.
*   **Redis Server** (versi 6.2 ke atas agar kita bisa memanfaatkan perintah atomic modern seperti `BLMOVE`).
*   **Supervisor Daemon** (`supervisor`), pengawas proses tingkat OS.
*   **Runtime Backend**: PHP CLI (v8.1+) dengan ekstensi `ext-redis` dan `pcntl`, atau Node.js (v18+) sesuai bahasa kesukaan Anda. Pada resep ini, kita racik worker PHP tangguh yang dilengkapi signal handling, lalu kita intip padanannya di Node.js.

Mari kita pasang perkakas dasarnya terlebih dahulu:

```bash
# Perbarui index paket dan pasang Redis serta Supervisor
sudo apt update && sudo apt install -y redis-server supervisor php-cli php-redis

# Pastikan Redis langsung aktif dan menyala saat boot
sudo systemctl enable --now redis-server

# Periksa respons denyut jantung Redis
redis-cli ping
# Output yang diharapkan: PONG
```

Jika terminal Anda membalas dengan `PONG`, fondasi dapur sudah menyala. Sekarang, mari kita pahami cetak biru aliran data yang akan kita bangun agar tidak ada satu pun job yang raib di tengah jalan.

---

## 2. Peta Jalan Singkat: Apa yang Sedang Kita Rakit

Banyak tim pemula menganggap background job sesederhana perintah `LPUSH` ke Redis lalu di sisi worker diambil dengan `RPOP`. Pendekatan itu bagaikan melempar piring keramik ke udara: begitu worker crash atau kehabisan memori tepat setelah eksekusi `RPOP`, data job lenyap selamanya dari muka bumi.

Di level enterprise, kita menerapkan pola **Reliable Queue Pattern** dengan mekanisme **Dead-Letter Queue (DLQ)**. Alurnya terbagi menjadi tiga zona pertahanan:

1.  **Ingestion Zone (Queue Utama)**: Aplikasi web mendorong payload JSON ke dalam queue Redis (`queue:default`) menggunakan `LPUSH`.
2.  **Processing Zone (Worker Pool)**: Supervisor mengeksekusi kumpulan worker paralel. Worker memindahkan job secara atomic dari `queue:default` ke `queue:processing` menggunakan perintah `BLMOVE` (atau `BRPOPLPUSH` pada Redis lama). Jika worker mati mendadak di tengah eksekusi, job masih tersimpan rapi di daftar processing.
3.  **Quarantine Zone (DLQ)**: Apabila job gagal diproses setelah melewati batas maksimal percobaan (misal: 3 kali retry), worker memindahkan job ke `queue:failed` (DLQ) lengkap dengan metadata error, jejak stack trace, dan timestamp.

```
+-------------+     LPUSH     +---------------+     BLMOVE     +------------------+
| Web App /   | ------------> | queue:default | -------------> | queue:processing |
| API Gateway |               +---------------+                +------------------+
+-------------+                                                         |
                                                                        v
                                                              +-------------------+
                                                              | Worker Supervisor |
                                                              +-------------------+
                                                                 /             \
                                                       Sukses   /               \ Gagal >= 3x
                                                               v                 v
                                                          +---------+      +--------------+
                                                          | LREM    |      | queue:failed |
                                                          | (Hapus) |      | (DLQ)        |
                                                          +---------+      +--------------+
```

Dengan alur kerja yang sudah terpetakan jelas di kepala, saatnya kita menyiapkan fondasi sistem operasi dan skrip worker yang siap menghadapi skenario terburuk.

---

## 3. Langkah 1: Mempersiapkan Fondasi & Environment

Kita mulai dengan menata Redis agar tidak bersikap seperti cache murni yang mudah lupa ketika RAM penuh. Redis untuk queue wajib memprioritaskan durabilitas data.

Buka file konfigurasi Redis di `/etc/redis/redis.conf` dan sesuaikan parameter berikut:

```ini
# Nonaktifkan eviction volatil agar Redis menolak data baru daripada menghapus antrean yang ada
maxmemory-policy noeviction

# Aktifkan Append Only File (AOF) untuk meminimalisir data loss saat server mati tiba-tiba
appendonly yes
appendfsync everysec
```

Muat ulang konfigurasi Redis tanpa memutus koneksi yang sedang aktif:

```bash
sudo systemctl restart redis-server
```

> [!IMPORTANT]
> Pengaturan `maxmemory-policy noeviction` adalah perimeter pengaman krusial. Jika RAM server habis, Redis akan mengembalikan error ke producer alih-alih secara diam-diam membuang job lama Anda seperti halnya mekanisme cache LRU.

Sekarang, mari kita buat direktori kerja dan sebuah worker PHP tangguh yang dilengkapi penanganan sinyal UNIX (`SIGTERM`, `SIGINT`) untuk proses *graceful shutdown*:

```bash
sudo mkdir -p /var/www/enterprise-queue
cd /var/www/enterprise-queue
```

Buat file worker bernama `worker.php`:

```php
<?php
declare(ticks=1);

// Inisialisasi koneksi Redis
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

$sourceQueue     = 'queue:default';
$processingQueue = 'queue:processing';
$failedQueue     = 'queue:failed';
$maxRetries      = 3;
$shouldKeepRunning = true;

// Tangkap sinyal terminasi dari Supervisor untuk graceful shutdown
$signalHandler = function ($signal) use (&$shouldKeepRunning) {
    echo "[!] Sinyal diterima ({$signal}). Menyelesaikan job berjalan sebelum undur diri...\n";
    $shouldKeepRunning = false;
};

pcntl_signal(SIGTERM, $signalHandler);
pcntl_signal(SIGINT, $signalHandler);

echo "[*] Worker enterprise siap melayani pesanan antrean...\n";

while ($shouldKeepRunning) {
    // Pindahkan job secara atomic: tahan koneksi maksimal 3 detik jika antrean kosong
    $jobRaw = $redis->blmove($sourceQueue, $processingQueue, 'RIGHT', 'LEFT', 3);

    if (!$jobRaw) {
        // Timeout tercapai tanpa ada job, putar loop kembali
        continue;
    }

    $job = json_decode($jobRaw, true);
    $attempts = $job['attempts'] ?? 0;

    echo "[+] Memproses Job ID: {$job['id']} (Percobaan ke-{$attempts})\n";

    try {
        // Simulasi logika bisnis: jika payload berisi 'trigger_crash', kita paksa gagal
        if (!empty($job['payload']['trigger_crash'])) {
            throw new Exception("Koneksi gateway pihak ketiga timeout!");
        }

        // Logika pekerjaan normal Anda diletakkan di sini (kirim email, invoice PDF, dll.)
        usleep(200000); // Simulasi kerja selama 200ms

        // Sukses: Bersihkan job dari antrean processing
        $redis->lrem($processingQueue, $jobRaw, 1);
        echo "[v] Job ID: {$job['id']} sukses dieksekusi.\n";
    } catch (Throwable $e) {
        echo "[-] Gagal memproses Job ID: {$job['id']}: " . $e->getMessage() . "\n";
        
        // Cabut job dari processing queue
        $redis->lrem($processingQueue, $jobRaw, 1);
        
        $attempts++;
        $job['attempts'] = $attempts;
        $job['last_error'] = $e->getMessage();
        $job['failed_at'] = date('Y-m-d H:i:s');

        if ($attempts >= $maxRetries) {
            // Lempar ke Dead-Letter Queue (DLQ)
            echo "[x] Kuota retry terlampaui. Mengasingkan Job ID: {$job['id']} ke DLQ ({$failedQueue})\n";
            $redis->lpush($failedQueue, json_encode($job));
        } else {
            // Dorong kembali ke queue utama untuk dicoba lagi nanti
            echo "[r] Mengembalikan Job ID: {$job['id']} ke queue utama untuk retry\n";
            $redis->lpush($sourceQueue, json_encode($job));
        }
    }
}

echo "[*] Worker pamit undur diri dengan tertib.\n";
exit(0);
```

Skrip koki worker kita sudah mengerti sopan santun: ia tahu cara retry, tahu kapan harus membuang sampah berbahaya ke DLQ, dan tahu cara menuntaskan tugas sebelum dimatikan. Pertanyaannya: siapa yang menjaga agar koki ini terus bekerja 24/7 dan melipatgandakan tenaganya saat antrean membludak? Di sinilah Supervisor mengambil kendali.

---

## 4. Langkah 2: Meracik Konfigurasi Inti Tanpa Basa-basi

Menjalankan worker menggunakan screen atau terminal biasa adalah tiket menuju bencana di lingkungan produksi. Supervisor bertindak sebagai daemon init tingkat proses yang mengawasi worker kita bagaikan elang: mati satu, bangkitkan lagi dalam hitungan milidetik.

Mari kita racik berkas konfigurasi Supervisor di `/etc/supervisor/conf.d/queue-worker.conf`:

```ini
[program:queue-worker]
; Perintah eksekusi worker PHP kita
command=php /var/www/enterprise-queue/worker.php
process_name=%(program_name)s_%(process_num)02d

; Jalankan 4 proses worker paralel secara bersamaan
numprocs=4

; Mulai otomatis saat OS boot dan restart otomatis jika proses mati/crash
autostart=true
autorestart=true

; Jalankan di bawah user non-root demi keamanan
user=www-data

; Waktu tunggu graceful shutdown sebelum dikirim sinyal SIGKILL paksa
stopwaitsecs=15
stopasgroup=true
killasgroup=true

; Logging stdout dan stderr ke berkas terpisah
stdout_logfile=/var/log/supervisor/queue-worker.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=5
stderr_logfile=/var/log/supervisor/queue-worker-error.log
stderr_logfile_maxbytes=20MB
stderr_logfile_backups=5
```

Perhatikan baik-baik pengaturan `stopasgroup=true` dan `killasgroup=true`. Ini menjamin bahwa jika worker Anda melahirkan child process (misal mengeksekusi binary `ffmpeg` atau `pdftoppm`), seluruh rantai proses anak akan ikut dibersihkan saat Supervisor mematikan worker, menghindari kemunculan *zombie process* yang memakan RAM secara sembunyi-sembunyi.

Sekarang perintahkan Supervisor untuk membaca konfigurasi baru dan menghidupkan armada worker:

```bash
# Beri izin kepemilikan direktori kerja kepada user www-data
sudo chown -R www-data:www-data /var/www/enterprise-queue

# Perintahkan Supervisor membaca file konfigurasi baru di conf.d
sudo supervisorctl reread

# Update dan jalankan proses baru
sudo supervisorctl update

# Intip status armada worker kita
sudo supervisorctl status
```

Jika segalanya berjalan presisi, terminal Anda akan menampilkan pemandangan yang memuaskan seperti ini:

```text
queue-worker:queue-worker_00   RUNNING   pid 14205, uptime 0:00:03
queue-worker:queue-worker_01   RUNNING   pid 14206, uptime 0:00:03
queue-worker:queue-worker_02   RUNNING   pid 14207, uptime 0:00:03
queue-worker:queue-worker_03   RUNNING   pid 14208, uptime 0:00:03
```

Empat koki pekerja sudah bersiap di posnya masing-masing. Namun di skala jutaan job per hari, memori bocor (memory leak) dan saturasi disk adalah musuh laten yang siap melumpuhkan server. Mari kita pasang perisai pengamannya di langkah berikutnya.

---

## 5. Langkah 3: Sentuhan Akhir Keamanan & Optimasi

Pada aplikasi berbasis skrip panjang (long-running script) seperti worker antrean, kebocoran memori mikro pada framework atau library pihak ketiga adalah keniscayaan. Jika dibiarkan menyala berhari-hari tanpa daur ulang, satu proses worker bisa membengkak dari 30 MB menjadi 1.5 GB dan memicu *Linux Out-Of-Memory (OOM) Killer* menembak jatuh Redis Anda.

### Trik 1: Batasi Siklus Hidup Worker (Max-Jobs Recycling)
Salah satu standar emas industri adalah memprogram worker agar secara sukarela mengakhiri hidupnya setelah memproses sejumlah job tertentu (misal: 1.000 job) atau saat memori melampaui ambang batas. Supervisor kemudian akan menyalakan instans proses yang segar dalam 50 milidetik.

Tambahkan cuplikan proteksi memori ini di dalam loop `worker.php` Anda:

```php
// Sisipkan di dalam while loop worker.php setelah job selesai diproses:
$processedCount++;
$memoryUsageMB = memory_get_usage(true) / 1024 / 1024;

if ($processedCount >= 1000 || $memoryUsageMB > 128) {
    echo "[!] Batas memori atau kuota job tercapai ({$memoryUsageMB} MB). Merestart diri secara teratur...\n";
    $shouldKeepRunning = false;
}
```

> [!TIP]
> Jika Anda menggunakan ekosistem framework seperti Laravel (`php artisan queue:work --max-jobs=1000 --memory=128`) atau Node.js BullMQ, parameter ini sudah terintegrasi bawaan. Pastikan selalu memasang flag batas memori tersebut di konfigurasi Supervisor Anda.

### Trik 2: Pemantauan Health Check DLQ Otomatis
Jangan biarkan antrean gagal (`queue:failed`) menjadi kuburan bisu. Buat cron job sederhana untuk memantau apakah ada lonjakan anomali pada DLQ yang menandakan adanya bug baru di rilis produksi atau API pihak ketiga yang sedang down total:

```bash
# Skrip bash one-liner kilat untuk mengecek panjang DLQ
DLQ_COUNT=$(redis-cli llen queue:failed)
if [ "$DLQ_COUNT" -gt 50 ]; then
    echo "[WARNING] Bahaya! Ada $DLQ_COUNT job tersangkut di Dead-Letter Queue!" 
    # Kirim webhook notifikasi ke Slack / Discord / PagerDuty di sini
fi
```

Semua filter pengaman sudah terpasang rapi. Sekarang tibalah saat yang paling memuaskan: mari kita uji ketahanan sistem ini dengan muatan job nyata.

---

## 6. Tes Rasa: Memastikan Semuanya Berjalan Sempurna

Saatnya menguji apakah keempat worker kita benar-benar mampu melahap ribuan job dan apakah job beracun akan masuk ke karantina DLQ tanpa merusak antrean lainnya.

Buat skrip produsen job kilat bernama `producer.php` di direktori yang sama:

```php
<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

echo "[*] Menyuntikkan 20 job normal dan 2 job beracun ke dalam antrean...\n";

// Suntikkan 20 job valid
for ($i = 1; $i <= 20; $i++) {
    $payload = [
        'id' => 'job_norm_' . $i,
        'payload' => ['task' => 'Kirim Invoice #' . $i],
        'attempts' => 0,
        'created_at' => time()
    ];
    $redis->lpush('queue:default', json_encode($payload));
}

// Suntikkan 2 job beracun (poison pill) untuk menguji keandalan DLQ
for ($j = 1; $j <= 2; $j++) {
    $toxicPayload = [
        'id' => 'job_toxic_' . $j,
        'payload' => ['task' => 'Eksekusi Rusak #' . $j, 'trigger_crash' => true],
        'attempts' => 0,
        'created_at' => time()
    ];
    $redis->lpush('queue:default', json_encode($toxicPayload));
}

echo "[v] Seluruh job berhasil disuntikkan!\n";
```

Buka dua jendela terminal secara berdampingan:

**Terminal 1 (Pantau Log Worker secara Real-Time):**
```bash
sudo tail -f /var/log/supervisor/queue-worker.log
```

**Terminal 2 (Jalankan Producer dan Cek Status Redis):**
```bash
php /var/www/enterprise-queue/producer.php
```

Perhatikan respons di Terminal 1: keempat worker akan langsung berebut mengambil payload secara paralel. Anda akan melihat job-job normal selesai dalam sekejap dengan tanda `[v]`, sementara dua job beracun akan dicoba ulang sebanyak 3 kali sebelum akhirnya muncul pesan:

```text
[-] Gagal memproses Job ID: job_toxic_1: Koneksi gateway pihak ketiga timeout!
[r] Mengembalikan Job ID: job_toxic_1 ke queue utama untuk retry
...
[x] Kuota retry terlampaui. Mengasingkan Job ID: job_toxic_1 ke DLQ (queue:failed)
```

Sekarang periksa kondisi akhir antrean Redis Anda langsung via CLI:

```bash
# Periksa apakah queue utama sudah bersih tuntas
redis-cli llen queue:default
# Output yang diharapkan: 0

# Periksa apakah processing queue juga bersih (tidak ada job nyangkut)
redis-cli llen queue:processing
# Output yang diharapkan: 0

# Periksa apakah 2 job beracun tersimpan aman di DLQ untuk diinvestigasi engineer
redis-cli llen queue:failed
# Output yang diharapkan: 2
```

Jika angka-angka tersebut cocok, selamat! Arsitektur background job Anda telah lulus uji ketahanan enterprise.

---

## 7. Pertolongan Pertama: Jika Terjadi Kendala Kecil

Di dunia nyata, rintangan kecil kadang muncul di luar skenario ideal. Berikut adalah diagnosis cepat untuk masalah umum yang sering dihadapi di lapangan:

### Kendala 1: Status Supervisor `FATAL` atau `BACKOFF`
Saat menjalankan `supervisorctl status`, Anda melihat tulisan merah `FATAL (Exited too quickly)`.  
*   **Penyebab Utama**: Izin akses file (permission denied) atau binary PHP tidak ditemukan di path default.  
*   **Solusi Cepat**: Intip isi error log spesifik di `/var/log/supervisor/queue-worker-error.log`. Pastikan user `www-data` memiliki izin baca/tulis pada folder kerja (`chown -R www-data:www-data /var/www/enterprise-queue`), dan gunakan path absolut ke PHP binary di file konfigurasi Supervisor (misal: `/usr/bin/php` alih-alih hanya `php`).

### Kendala 2: Job Tertahan Selamanya di `queue:processing`
Ini terjadi jika server Linux mendadak mati listrik (hard reboot) saat worker sedang mengeksekusi job, sehingga job belum sempat dibersihkan atau dipindah ke DLQ.  
*   **Solusi Cepat**: Jalankan skrip *re-queue reconciliation* kecil saat sistem startup yang bertugas memindahkan sisa payload dari `queue:processing` kembali ke `queue:default` jika job ID tersebut sudah terdiam lebih dari durasi timeout yang wajar.

> [!WARNING]
> Jangan pernah menjalankan perintah `FLUSHDB` atau `FLUSHALL` di server Redis produksi tanpa memisahkan logical database atau instance port yang berbeda antara data cache situs dan data message queue!