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
jsonbawaan secara default mengizinkan non-standard token (menghasilkan{"val": NaN}). Saat payload ini dikonsumsi oleh parser JSON yang ketat (seperti library Goencoding/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
nullatau string"NaN". Tanpa batas retry (Dead-Letter Queue), antrean tersumbat total. - Agregasi Metrik Rusak: Nilai
NaNbersifat menular. Semua operasi matematika lanjutan (sepertisum += val) pada downstream data pipeline akan menghasilkanNaN, 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 dataChecklist 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.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!