Pendahuluan dan Gejala Awal

Masalah utama yang dihadapi adalah job cron yang berjalan menggunakan CodeIgniter 3 tiba-tiba berhenti merespons, mengalami retry terus-menerus, dan memunculkan log timeout dalam waktu singkat. Saat job dijalankan dari server scheduler, respons HTTP berhasil tapi proses internal tidak selesai karena bagian yang bergantung pada sesi atau lock tidak dapat menulis data baru. Dalam dua paragraf pertama, intinya: cron terus dijalankan dan job gagal menyelesaikan karena lock session macet.

Monitoring awal menunjukkan bahwa job dipicu setiap menit, tapi tidak ada output dashboard aplikasi untuk menyatakan bahwa job selesai. Log yang ditulis cron mencantumkan pesan timeout dari model session CodeIgniter setelah beberapa detik, dan sistem monitoring mendeteksi kegagalan terus menerus sehingga job otomatis di-retry berulang kali.

Monitoring dan Skrip Diagnostik

Langkah awal yang dilakukan adalah memperkuat observabilitas: menambahkan logging tambahan dan skrip monitoring ringan yang membandingkan jumlah job yang sedang berjalan dengan yang selesai. Skrip cron sederhana di server menyuntikkan curl ke endpoint internal untuk memicu job dan mencatat response header serta body. File log mencatat timestamp, job_id, serta status sesi.

Contoh skrip monitoring (dipanggil dari /etc/cron.d):

#!/bin/bash
LOGFILE=/var/log/cron_job_debug.log
RESPONSE=$(curl -s -w "%{http_code} %{time_total}" -o /tmp/cron_response.txt http://localhost/job/cronTask)
STATUS=$(cat /tmp/cron_response.txt | head -n 1)
printf "%s %s %s\n" "$(date +%FT%T)" "$RESPONSE" "$STATUS" >> "$LOGFILE"

Skrip ini mencatat response code, total waktu dan bagian awal body, lalu log mencari pola "Session lock" atau pesan timeout untuk mengidentifikasi kapan lock tidak dilepas.

Investigasi Root Cause

Profiling job menunjukkan bahwa CI Session digunakan untuk menyimpan status job, walaupun job dijalankan via CLI/cron. CodeIgniter 3 secara default menggunakan session berbasis file, sehingga setiap request akan mengunci file session. Cron job yang memanggil controller melalui HTTP mengakibatkan file session ter-lock, dan ketika job mencoba menulis session lain (misalnya untuk progress), ia menunggu lock dilepas. Namun pada kondisi retry sebelumnya, lock tidak pernah dilepas karena proses terhenti di bagian lain.

Selain itu, log menunjukkan bahwa job memulai ulang curl asynchronous ke endpoint yang sama (implementasi cronTask memanggil kembali dirinya untuk batching data). Ini menyebabkan deadlock karena dua request berbeda mencoba mengunci session yang sama secara bersamaan. Debugging menggunakan strace pada proses PHP menunjukkan bahwa ada *waiting on file descriptor* untuk file session.

Perbaikan Praktis: Lock dan Session Management

Langkah pertama: Nonaktifkan sesi untuk cron

Karena cron job tidak butuh sesi pengguna, cukup lepas dependency pada $this->session. Pada controller job, hapus pemanggilan session, atau buat flag yang menghindari pemuatan library session ketika dijalankan via CLI/cron:

public function cronTask()
{
    if ($this->input->is_cli_request() || isset($_SERVER['CRON_TRIGGER'])) {
        $this->session->sess_use_database = false;
        // disable session library secara manual
    }
    // logika job
}

Namun jika pahit menghapus seluruh session tidak mungkin (karena job memerlukan state), alternatifnya adalah mengubah konfigurasi session menjadi menggunakan database/Redis sehingga lock tidak seketat file.

Langkah kedua: Implementasikan lock eksplisit di DB

Daripada mengandalkan session lock internal, buat table sederhana cron_locks dengan kolom job_name, locked_at. Job memeriksa table sebelum mulai:

public function acquireLock($jobName)
{
    $now = time();
    $sql = "INSERT INTO cron_locks (job_name, locked_at) VALUES (?, ?)";
    if ($this->db->query($sql, [$jobName, $now])) {
        return true;
    }
    $sql = "UPDATE cron_locks SET locked_at = ? WHERE job_name = ? AND locked_at < ?";
    return $this->db->query($sql, [$now, $jobName, $now - 600]);
}

Dengan pendekatan ini, job cron tidak bergantung pada session file. Pastikan setiap job memanggil releaseLock() dalam blok try/finally agar lock dilepas bahkan saat exception.

Langkah ketiga: Retry dan Timeout management

Retry otomatis tetap diperlukan. Tambahkan lapisan timeout di job: jika lock tidak berhasil diambil dalam beberapa detik, log pesan dan keluar dengan kode khusus sehingga scheduler bisa menunda.

Gunakan log:
FAILED TO ACQUIRE LOCK untuk membedakan kegagalan lock dari error lainnya. Tambahkan konfigurasi microtime start-stop untuk melihat durasi job dan lapisan CI_Profiler jika perlu.

Verifikasi dan Mitigasi Regresi

Verifikasi dilakukan dengan menjalankan job secara manual pada environment staging dan memeriksa dua hal:

  • Log: Pastikan tidak ada pesan session lock. Job harus mencetak status "lock acquired" dan selesai tanpa exception.
  • Monitoring: Lihat grafik retry; setelah perbaikan, job tidak lagi memicu retry dalam periode jam. Jika ada, log harus menunjukkan alasan.

Regresi dicegah dengan menambahkan unit test/integration test untuk lock: simulasi dua proses cron berjalan bersamaan, pastikan yang kedua mendapat false dari acquireLock dan tidak memengaruhi database.

Tips Observabilitas dan Pencegahan Kembali

1. Aktifkan log level INFO untuk job cron sehingga menunjukan lifecycle (start, lock acquire, finish).
2. Gunakan monitoring metric (misalnya Prometheus atau adat) untuk melacak job duration dan jumlah retry yang gagal karena lock.
3. Sediakan endpoint health check yang mengonfirmasi database lock table dapat diakses.

Dengan observabilitas yang memadai dan lock eksplisit, job cron lambat laun kembali stabil tanpa bergantung pada session handler internal CI.