Model inferensi generatif 3D seperti TripoSplat membutuhkan waktu pemrosesan puluhan detik hingga beberapa menit untuk menghasilkan representasi 3D Gaussian Splatting dari sebuah gambar. Menjalankan proses komputasi ini secara sinkron melalui HTTP request standar memicu kegagalan sistemik: reverse proxy gateway timeout (HTTP 504), pool koneksi backend yang habis, dan crash pipeline. Pendekatan sinkron harus diganti dengan sistem asinkron berbasis event dan webhook yang andal.

Arsitektur Job Ticket: Menghentikan Polling Berlebih

Pola Job Ticket memisahkan fase penerimaan permintaan dari eksekusi model pada worker GPU. Ketika klien mengirimkan payload berupa gambar acuan, API Gateway memvalidasi input, menerbitkan job_id dengan status queued ke message broker (misalnya RabbitMQ atau Redis Stream), lalu langsung mengembalikan respon HTTP 202 Accepted.

Pipeline ini mencegah beban berlebih akibat short polling berulang dari klien yang menghabiskan bandwidth dan koneksi database. Worker GPU mengambil job dari antrean, mengeksekusi rekonstruksi 3D, menyimpan output artifact (seperti file .ply atau .splat) ke Object Storage (S3/GCS), dan memicu pengiriman webhook callback.

Mencegah Eksekusi Ganda dengan Idempotency Key

Komputasi GPU berbasis VRAM berbiaya tinggi. Masalah jaringan sering memicu pengiriman ulang pesan dari broker (at-least-once delivery) atau klien menekan tombol submit berkali-kali. Worker GPU harus mengecek idempotency key secara atomik sebelum mengalokasikan resource.

import redis
import json

r = redis.Redis(host='localhost', port=6379, db=0)

def claim_gpu_job(job_id: str, idempotency_key: str, ttl_seconds: int = 3600) -> bool:
    cache_key = f"idempotency:{idempotency_key}"
    # atomic SET NX EX: hanya sukses jika key belum ada
    acquired = r.set(cache_key, json.dumps({"status": "PROCESSING", "job_id": job_id}), nx=True, ex=ttl_seconds)
    return bool(acquired)

def execute_triposplat_pipeline(job_id: str, idempotency_key: str):
    if not claim_gpu_job(job_id, idempotency_key):
        print(f"Job {job_id} diabaikan: Idempotency key {idempotency_key} sedang atau sudah diproses.")
        return
    
    try:
        # Eksekusi rekonstruksi 3D (TripoSplat)
        artifact_url = run_triposplat_inference(job_id)
        r.set(f"idempotency:{idempotency_key}", json.dumps({"status": "COMPLETED", "artifact_url": artifact_url}))
        dispatch_webhook(job_id, "job.completed", {"artifact_url": artifact_url})
    except Exception as exc:
        r.set(f"idempotency:{idempotency_key}", json.dumps({"status": "FAILED", "error": str(exc)}))
        dispatch_webhook(job_id, "job.failed", {"error": str(exc)})

Kontrak Payload Webhook

Penyedia webhook harus menetapkan payload terstruktur agar konsumen dapat membedakan status keberhasilan dan kegagalan secara deterministik, lengkap dengan timestamp dan payload signature berbasis HMAC untuk validasi keamanan.

{
  "event": "3d_reconstruction.completed",
  "id": "evt_9b1deb4d3b7d4",
  "timestamp": 1711929600,
  "data": {
    "job_id": "job_tripo_8849201",
    "model": "triposplat-v1",
    "compute_time_ms": 48210,
    "artifacts": {
      "gaussian_ply": "https://storage.example.com/outputs/job_tripo_8849201.ply",
      "rendered_preview": "https://storage.example.com/outputs/job_tripo_8849201.png"
    }
  }
}

Jika terjadi Out of Memory (OOM) pada VRAM CUDA atau degradasi mesh, kirimkan event kegagalan eksplisit daripada membiarkan koneksi menggantung:

{
  "event": "3d_reconstruction.failed",
  "id": "evt_9b1deb4d3b7e8",
  "timestamp": 1711929645,
  "data": {
    "job_id": "job_tripo_8849201",
    "error_code": "CUDA_OUT_OF_MEMORY",
    "message": "Resolusi input melebihi alokasi tensor point cloud.",
    "retryable": false
  }
}

Webhook Retry: Exponential Backoff dengan Full Jitter

Saat endpoint penerima mengalami transient error (HTTP 500, 502, 503, 504, atau network timeout), pengiriman ulang webhook tanpa jeda terukur memicu efek thundering herd yang memperburuk beban server klien. Solusinya adalah algoritma Exponential Backoff dengan Full Jitter.

Formula jeda waktu pengiriman:

T_sleep = random(0, min(T_max, T_base * 2 ^ attempt))
import time
import random
import requests

def deliver_webhook_with_retry(endpoint_url: str, payload: dict, max_retries: int = 5):
    base_backoff = 2.0  # detik
    max_backoff = 60.0  # detik
    
    for attempt in range(max_retries):
        try:
            response = requests.post(endpoint_url, json=payload, timeout=10.0)
            if response.status_code in (200, 201, 202, 204):
                return True
            
            # Kode status 4xx selain 408/429 mengindikasikan bug pada payload/klien, batalkan retry
            if 400 <= response.status_code < 500 and response.status_code not in (408, 429):
                print(f"Pengiriman gagal permanen: HTTP {response.status_code}")
                return False
        except requests.RequestException:
            pass  # Masalah konektivitas atau timeout, lanjut retry
        
        # Hitung delay exponential dengan full jitter
        ceiling = min(max_backoff, base_backoff * (2 ** attempt))
        sleep_duration = random.uniform(0, ceiling)
        
        print(f"Retry {attempt + 1}/{max_retries} tertunda selama {sleep_duration:.2f} detik...")
        time.sleep(sleep_duration)
    
    # Simpan ke Dead Letter Queue (DLQ) jika batas percobaan habis
    push_to_dlq(endpoint_url, payload)
    return False

Dead-Letter Queue dan Observabilitas

Jika webhook tetap gagal terkirim setelah seluruh upaya retry habis, data event dipindahkan ke Dead-Letter Queue (DLQ). Mekanisme ini memastikan log kegagalan tetap tersimpan secara persisten, memungkinkan re-drive payload secara manual setelah endpoint konsumen normal kembali, serta memantau kesehatan webhook melalui alert metrik.