Layanan render Lottie dinamis berbasis headless browser atau canvas worker (seperti diffusionstudio/lottie) memproses modifikasi JSON Bodymovin secara instan. Kesalahan injeksi nilai dinamis—seperti warna di luar rentang kanal 0-1, teks string tanpa escaping, atau mutasi keyframe yang memecah struktur layer—sering memicu runtime crash pada worker rendering.
Ketika worker crash tanpa penanganan boundary yang disiplin, backend umumnya mengembalikan respons HTTP generic 500 Internal Server Error. Upstream service atau sistem webhook pengirim otomatis menganggap kegagalan ini sebagai kesalahan transien (transient error) lalu melakukan retry berulang kali. Akibatnya, server render terjebak dalam retry loop tak berujung yang menghabiskan CPU dan antrean rendering (thundering herd problem).
Klasifikasi Error: Deterministic vs. Transient
Pencegahan retry loop bertumpu pada ketegasan klasifikasi error di level API gateway atau HTTP controller:
- Deterministic Error: Error yang dihasilkan oleh data kiriman klien yang cacat (payload malformed, ID template tidak ditemukan, properti transformasi tidak valid). Permintaan yang sama akan selalu menghasilkan error yang sama. Handler wajib mengembalikan HTTP
400 Bad Requestatau422 Unprocessable Content. Consumer webhook harus segera menghentikan retry dan menandai job sebagai failed permanen. - Transient Error: Error yang disebabkan oleh gangguan infrastruktur sesaat (antrean Redis overload, memory limit worker tercapai, worker timeout). Handler wajib mengembalikan HTTP
429 Too Many Requestsatau503 Service Unavailabledisertai headerRetry-After. Pada kondisi ini, webhook diizinkan melakukan retry dengan exponential backoff.
Boundary Validation Menggunakan JSON Schema / Zod
Validasi dilakukan di HTTP boundary sebelum job dimasukkan ke dalam antrean (Redis/BullMQ). Payload modifikasi dinamis Lottie harus dibatasi secara eksplisit, bukan menerima raw Bodymovin JSON sembarangan.
import { z } from 'zod';
// Kontrak payload modifikasi Lottie dinamis
export const RenderLottieJobSchema = z.object({
templateId: z.string().uuid(),
// Batasi hanya modifikasi layer yang diizinkan (slot-based replacement)
modifications: z.object({
texts: z.record(
z.string().max(64), // Layer ID
z.string().max(200) // Text content
).optional(),
colors: z.record(
z.string().max(64), // Color identifier
// Format color Bodymovin: [R, G, B, A] dengan nilai 0.0 - 1.0
z.tuple([
z.number().min(0).max(1),
z.number().min(0).max(1),
z.number().min(0).max(1),
z.number().min(0).max(1).optional()
])
).optional()
}).strict(),
output: z.object({
format: z.enum(['mp4', 'webm', 'gif']),
fps: z.union([z.literal(30), z.literal(60)]).default(30),
width: z.number().int().min(100).max(1920).default(1080),
height: z.number().int().min(100).max(1920).default(1080)
}).strict()
}).strict();
export type RenderLottieJob = z.infer<typeof RenderLottieJobSchema>;
// ponytail: validasi sebatas text dan color slotting. Tambahkan validator path interpolasi jika perlu deformasi vektor kompleks.
[code] → skipped: path mutation validator, add when dynamic bezier shape injection is needed.
Penggunaan .strict() menolak penambahan property asing yang berisiko merusak struktur layer internal saat proses merge di runtime worker.
Penerapan Idempotency-Key pada Operasi CPU-Bound
Rendering animasi merupakan operasi komputasi berat. Jika request jaringan mengalami timeout di sisi klien padahal server telah menerima job, klien akan mencoba mengirim ulang request yang sama. Gunakan header Idempotency-Key untuk mencegah duplicate compute.
Alur Idempotency Engine:
- Klien mengirim header
Idempotency-Key: <uuid/hash>. - Handler memeriksa cache/key-value store (misal: Redis) menggunakan atomisitas (atomic set NX).
- Jika key berstatus
PROCESSING, kembalikan HTTP409 Conflictatau HTTP202 Accepteddengan status tracking URL yang sama. - Jika key berstatus
COMPLETED, segera kembalikan output tersimpan dari cache tanpa mendelegasikan ke worker. - Jika key belum ada, set status
PROCESSINGdengan TTL (misal: 300 detik), lalu masukkan ke worker queue.
Verifikasi Alur Validasi dan Idempotensi
Gunakan script pengujian berbasis native assertions berikut untuk memverifikasi bahwa HTTP boundary menolak payload malformed dengan HTTP 422 dan mencegah duplicate execution via Idempotency Key.
import assert from 'node:assert/strict';
import { RenderLottieJobSchema } from './schema';
// Mock storage untuk idempotency key
const idempotencyStore = new Map<string, { status: string; result?: unknown }>();
async function handleRenderRequest(headers: Record<string, string>, body: unknown) {
const idempotencyKey = headers['idempotency-key'];
if (!idempotencyKey) {
return { status: 400, error: 'Missing Idempotency-Key header' };
}
// 1. Verifikasi Idempotency State
const cached = idempotencyStore.get(idempotencyKey);
if (cached) {
if (cached.status === 'PROCESSING') {
return { status: 409, error: 'Job already in progress' };
}
return { status: 200, data: cached.result };
}
// 2. Strict Boundary Validation (Deterministic check)
const parseResult = RenderLottieJobSchema.safeParse(body);
if (!parseResult.success) {
// Return 422 agar consumer / webhook menghentikan retry
return {
status: 422,
error: 'Unprocessable Content',
details: parseResult.error.flatten()
};
}
// 3. Register job ke antrean
idempotencyStore.set(idempotencyKey, { status: 'PROCESSING' });
// Simulasi selesai proses render worker
const renderOutput = { url: 'https://cdn.example.com/renders/sample.mp4' };
idempotencyStore.set(idempotencyKey, { status: 'COMPLETED', result: renderOutput });
return { status: 201, data: renderOutput };
}
// Runnable Self-Check Test
async function runTests() {
const key = 'req_id_unique_123';
// Test 1: Payload malformed (Kanal warna > 1.0) -> Harus 422
const invalidBody = {
templateId: '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d',
modifications: {
colors: { 'primary_bg': [255, 0, 0] } // Error: Bodymovin butuh 0.0 - 1.0
},
output: { format: 'mp4' }
};
const res1 = await handleRenderRequest({ 'idempotency-key': 'req_malformed' }, invalidBody);
assert.equal(res1.status, 422, 'Payload salah harus menghasilkan 422');
// Test 2: Payload valid pertama kali -> Harus 201
const validBody = {
templateId: '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d',
modifications: {
colors: { 'primary_bg': [1.0, 0, 0] }
},
output: { format: 'mp4', fps: 30 }
};
const res2 = await handleRenderRequest({ 'idempotency-key': key }, validBody);
assert.equal(res2.status, 201, 'First request harus 201 Created');
// Test 3: Replay request yang sama -> Harus 200 via cache
const res3 = await handleRenderRequest({ 'idempotency-key': key }, validBody);
assert.equal(res3.status, 200, 'Replayed request harus 200 OK dari cache');
assert.deepEqual(res3.data, { url: 'https://cdn.example.com/renders/sample.mp4' });
console.log('Semua assertion berhasil lolos.');
}
runTests();
// ponytail: in-memory mock map. Ganti dengan Redis SET key val NX EX saat deploy multi-instance.
[code] → skipped: Redis atomic lock integration, add when horizontally scaling across multiple render nodes.
Rangkuman Operasional
Mencegah pemborosan compute pada rendering Lottie membutuhkan kontrak API berlapis. Pisahkan penanganan deterministic validation langsung di HTTP level sebelum request masuk ke worker queue. Terapkan HTTP 422 untuk schema failure agar retry loop terhenti, dan kunci resource compute dengan Idempotency-Key untuk mengeliminasi duplikasi pekerjaan.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!