Biasanya tim hanya menindaklanjuti error yang aktif mengganggu pengguna. Tetapi bagaimana jika backend menghasilkan silent failure—proses tetap selesai tanpa error pengguna, namun data kritis tidak tercatat? Artikel ini menjelaskan langsung bagaimana tim mendeteksi dan menanggapi bug tersebut, dengan contoh log, metrik, hingga langkah perbaikan.

Gejala Awal: Data Summaries yang Tertunda

Tim observability e-commerce melihat bahwa laporan analitik harian mengalami keterlambatan 20-30 menit, padahal tidak ada alert error. Pengguna dashboard menganggap latency data sebagai delay normal. Namun metrik batch job menunjukkan peningkatan durasi dan jumlah batch kosong pada endpoint /api/summary/daily.

  • Log job: batch worker mencatat "batch processed" tanpa error, tapi duration naik dari 2s menjadi 18s.
  • Metric: processed_records per batch turun ke 0 selama spike.
  • Trace request: trace traces berulang ditandai dengan db.transaction() yang tertahan.

Karena pengguna tidak merasakan kegagalan eksplisit, masalah nyaris diabaikan sampai tim SRE melihat backlog metrics dan memulai investigasi.

Diagnosa Langkah demi Langkah

1. Correlate Metrics dengan Log

Langkah awal menghubungkan metrik kosong dengan log worker dari job scheduler. Mereka menandai ID batch yang kosong, lalu menelusuri trace. Trace menunjukkan bahwa proses mengunci tabel user_activity selama 15 detik sebelum akhirnya timeout pada layer ORM.

2. Trace Distribusi Dependensi

Tracing OpenTelemetry menunjukkan bahwa setiap job memanggil service ingestion lain yang menulis ke Kafka. Delay terjadi saat menunggu acknowledgement: timeout Kafka 10 detik, tapi service ingestion tidak mengembalikan respons karena proses mereka menunggu dependent Redis cache lock.

3. Hipotesis dan Verifikasi

Hipotesis: cache lock tidak dilepas karena lock.release() tidak terpanggil saat terjadi exception di layer business logic. Untuk memverifikasi, tim menambahkan logging ekstra pada path cacheService.acquireLock() dan finally block release().

Pada pengujian lokal, logger menunjukkan bahwa release() hanya dipanggil saat validasi payload berhasil, sedangkan saat payload kosong (kasus retesting webhook) exception di-throw sebelum finally dijalankan karena return di tengah flow. Platform menggunakan return di try block sebelum finally dapat berjalan.

Root Cause: Cache Lock yang Tidak Pernah Dirilis

Masalah sebenarnya bukan Kafka atau database, melainkan lock redis yang tidak dilepas. Karena proses job memanggil dependent service yang menunggu lock, seluruh pipeline stuck dan worker tetap lapor sukses tanpa memproses data apa pun.

Berikut snippet relevan dari service:

try {
    const lock = await cacheService.acquireLock(batchId);
    const payload = await fetchPayload(batchId);
    if (!payload) {
        logger.warn('payload kosong', { batchId });
        return; // lock tidak pernah dirilis
    }
    processBatch(payload);
} finally {
    await cacheService.releaseLock(batchId);
}

Masalahnya, return di dalam try menghentikan eksekusi sebelum finally menyelesaikan release karena pemanggilan terjadi dalam async function yang tidak menangani promise return dengan benar. Akibatnya, lock tetap terkunci, dan pengiriman payload berikutnya terhambat.

Solusi dan Preventif

Perbaikan Jangka Pendek

  • Patch release lock: Tambahkan await cacheService.releaseLock(batchId) sebelum return apabila payload kosong. Verifikasi dengan integration test.
  • Alert lock duration: Tambahkan histogram metric untuk waktu lock sehingga alert dipicu bila lock > 5 detik.
  • Low-priority job retry: Tambahkan retry async worker pada lock timeout untuk mencegah batch queue stagnan.

Preventif

  • Gunakan try/finally yang benar: Pastikan tidak ada return di dalam try tanpa melakukan cleanup eksplisit. Jika perlu early return, gunakan let success = false dan lakukan cleanup di finally.
  • Pasang tracing lock: Tambahkan custom span pada cache lock untuk memudahkan deteksi deadlock.
  • Fail-fast validation: Validasi payload lebih awal sebelum mengakuisisi lock.

Perbaikan ini menutup silent failure dengan gaya fail-fast dan observabilitas tambahan sehingga tim bisa merespons sebelum pengguna menyadari gangguan.

Kesimpulan dan Catatan Praktis

  • Catat gejala minor: Data kosong, durasi meningkat, atau throughput berkurang bisa jadi tanda silent failure.
  • Trace dependencies: Tool tracing (OpenTelemetry, Jaeger) perlu dipasang sejak awal untuk melacak path yang tersembunyi.
  • Laporkan state internal: Logging lock acquisition/release memudahkan memvalidasi asumsi.
  • Terapkan cleanup eksplisit: Gunakan finally yang benar dan hindari return di dalamnya tanpa cleanup.

Tim bisa mencegah silent failure dengan menggabungkan observability, logging yang tegas, dan kontrak kode yang menjamin cleanup. Bug ini nyaris tak terlihat, tapi dengan pendekatan sistematis, ia bisa dideteksi dan ditangani sebelum merusak kepercayaan pengguna.