Nilai NaN (Not-a-Number) yang lolos ke sistem antrean atau caching layer menyebabkan fenomena NaN poisoning. Masalah ini bermula dari komputasi floating-point tidak valid (seperti pembagian dengan nol) yang masuk ke proses serialisasi data, lalu merusak integritas state di Redis, memicu crash loop pada worker consumer, dan mengacaukan metrik agregasi downstream.

Akar Masalah: IEEE 754 dan Inkonsistensi Serialisasi

Representasi floating-point standar IEEE 754 mendefinisikan status khusus seperti NaN, +Infinity, dan -Infinity. Masalah muncul saat serializer data memetakan nilai-nilai ini ke format transport teks maupun biner.

1. Spesifikasi JSON (RFC 8259)

Spesifikasi JSON secara eksplisit melarang representasi literal NaN atau Infinity. Parser bawaan bahasa pemrograman menangani pelanggaran ini secara berbeda:

  • JavaScript / Node.js: JSON.stringify({ val: NaN }) mengubah nilai menjadi {"val": null}. Tipe data berubah drastis dari number ke null tanpa memicu runtime error saat serialisasi.
  • Python: Modul json bawaan secara default mengizinkan non-standard token (menghasilkan {"val": NaN}). Saat payload ini dikonsumsi oleh parser JSON yang ketat (seperti library Go encoding/json), deserialisasi langsung gagal fatal dengan error parsing sintaks.

2. MessagePack (MsgPack)

MsgPack mendukung float IEEE 754 secara biner. Nilai NaN tetap lolos sebagai payload float yang valid, tetapi menyebarkan state tidak valid ke seluruh service downstream yang mengharapkan nilai riil.

Dampak NaN Poisoning pada Arsitektur Terdistribusi

Ketika nilai numerik korup tersimpan di caching layer atau broker antrean, kerusakan merambat ke beberapa komponen utama:

  • Cache State Corruption: Operasi matematika di Redis (seperti HINCRBYFLOAT) mengembalikan error jika berhadapan dengan data non-representable, menyebabkan cache miss berulang atau error internal runtime.
  • Worker Crash Loop (Poison Message): Worker mengambil message dari antrean, mengeksekusi kalkulasi berbasis asumsi tipe data number, lalu melempar runtime exception saat menerima null atau string "NaN". Tanpa batas retry (Dead-Letter Queue), antrean tersumbat total.
  • Agregasi Metrik Rusak: Nilai NaN bersifat menular. Semua operasi matematika lanjutan (seperti sum += val) pada downstream data pipeline akan menghasilkan NaN, merusak dashboard analytics dan sistem alerting alert thresholds.

Strategi Validasi Boundary Sebelum Serialisasi

Pencegahan paling efektif adalah validasi di boundary producer sebelum data masuk ke Redis atau RabbitMQ/Kafka. Nilai harus memenuhi kriteria isFinite.

// TypeScript: Schema validation menggunakan Zod sebelum publish ke queue
import { z } from 'zod';

const MetricsPayloadSchema = z.object({
  jobId: z.string().uuid(),
  // Cegah NaN, Infinity, dan -Infinity
  ratio: z.number().refine((val) => Number.isFinite(val), {
    message: "Value must be a finite number",
  }),
  latencyMs: z.number().nonnegative().finite(),
});

type MetricsPayload = z.infer<typeof MetricsPayloadSchema>;

function enqueueMetric(payload: unknown): void {
  const result = MetricsPayloadSchema.safeParse(payload);
  
  if (!result.success) {
    // Log error, drop payload, atau kirim ke dead-letter queue khusus
    console.error("Boundary validation failed:", result.error.flatten());
    throw new TypeError("Invalid numeric payload rejected at boundary");
  }

  // Aman untuk di-serialize ke JSON/MsgPack
  queueProducer.send(JSON.stringify(result.data));
}

Pola Sanitasi dan Deserialisasi pada Worker

Meskipun producer telah divalidasi, worker consumer harus memiliki mekanisme penanganan defensif terhadap data legacy atau data corrupt dari Redis/Queue. Fallback eksplisit memastikan worker tidak masuk ke kondisi unhandled rejection.

// Python Worker: Parser defensif untuk payload antrean
import json
import math
from typing import Any, Dict

def parse_and_sanitize(raw_payload: str) -> Dict[str, Any]:
    # parse_constant memastikan token non-standar (NaN, Infinity) terdeteksi
    def handle_invalid_constants(val: str) -> None:
        raise ValueError(f"Invalid float token encountered: {val}")

    try:
        data = json.loads(raw_payload, parse_constant=handle_invalid_constants)
    except (ValueError, json.JSONDecodeError) as err:
        # Pindahkan langsung ke Dead Letter Queue (DLQ)
        raise PermanentMessageFailure(f"Corrupt payload rejected: {err}")

    # Sanitasi fallback untuk tipe null hasil JSON.stringify JS
    ratio = data.get("ratio")
    if ratio is None or not isinstance(ratio, (int, float)) or math.isnan(ratio):
        # Terapkan fallback value yang aman untuk domain model
        data["ratio"] = 0.0

    return data

Checklist Pencegahan NaN Poisoning

  • Gunakan validasi Number.isFinite() (JS/TS), math.isfinite() (Python), atau !math.IsNaN() (Go) pada setiap kalkulasi pembagian atau float conversion sebelum data masuk ke DTO.
  • Konfigurasikan parser JSON producer agar menolak representasi float non-standar secara fail-fast.
  • Terapkan Dead-Letter Queue (DLQ) dengan batas retry maksimum (misal 3 kali) agar worker tidak terjebak crash loop tak terbatas jika deserialisasi gagal.
  • Gunakan tipe data fixed-point (misalnya integer cents untuk mata uang) untuk menghindari karakteristik representasi float sama sekali.