Tantangan Retry Ganda pada API Vision AI
Inferensi Computer Vision (seperti deteksi penyakit tanaman pada GrassDx) memerlukan beban komputasi GPU yang tinggi dan transfer payload gambar berukuran besar. Masalah utama muncul ketika client mengalami network timeout saat transmisi HTTP belum selesai atau saat model masih memproses inferensi.
Ketika client melakukan retry otomatis, permintaan yang sama dapat dieksekusi berkali-kali. Dampaknya meliputi:
- Pemborosan Biaya Komputasi GPU: Model inferensi memproses tensor gambar identik lebih dari satu kali.
- Race Condition Database: Record diagnosa baru dibuat ganda untuk objek fisik yang sama.
- Exhaustion Worker Pool: Antrean job inferensi penuh oleh task redundan, meningkatkan latensi sistem secara keseluruhan.
Solusi standar industri untuk masalah ini adalah menerapkan Idempotency Key menggunakan spesifikasi IETF draft, dipadukan dengan validasi fingerprint hash payload dan status tracking terdistribusi.
Arsitektur Kontrak API dan Spesifikasi Header IETF
Client mengirimkan token idempotensi unik pada header HTTP. Standar draft IETF menggunakan header Idempotency-Key. Server menggunakan key ini bersama SHA-256 dari biner gambar untuk memverifikasi keaslian request.
Spesifikasi Header HTTP Request
POST /v1/diagnostics/grass HTTP/1.1
Host: api.grassdx.internal
Idempotency-Key: 7b84a9e2-51c3-4d6d-bf22-e4231b519cb8
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryXYZ
X-Signature: t=1710576000,v1=a8f5c...Skema Payload Request (Multipart Form)
image: File biner citra daun (format JPEG/PNG).crop_type: String (contoh:bermudagrass).callback_url: URL endpoint webhook client.
Response Task Awal (HTTP 202 Accepted)
Untuk menghindari client blocking dan timeout HTTP pada inferensi berdurasi panjang, endpoint mengembalikan status 202 Accepted segera setelah task dijadwalkan.
HTTP/1.1 202 Accepted
Content-Type: application/json
Idempotency-Key: 7b84a9e2-51c3-4d6d-bf22-e4231b519cb8
{
"task_id": "tsk_9021841284",
"status": "PENDING",
"created_at": "2025-05-10T08:30:00Z",
"fingerprint": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}State Machine Task dan Concurrency Control di Redis
Sistem backend mengelola siklus hidup task di Redis menggunakan tiga state: PENDING, PROCESSING, dan COMPLETED. Distributed lock menggunakan SET resource_name my_random_value NX EX max_lock_time digunakan untuk mencegah eksekusi konkuren.
Transisi State Task
- PENDING: Task tercatat, lock berhasil diperoleh, job dimasukkan ke queue GPU.
- PROCESSING: GPU worker mengambil task dari queue dan mulai memproses model segmentasi/klasifikasi.
- COMPLETED: Inferensi selesai, hasil disimpan permanen, webhook dipicu, data cache idempotensi diperbarui dengan hasil akhir.
Pseudocode Penanganan Request & Race Condition
import hashlib
import json
def handle_diagnosis_request(request):
idempotency_key = request.headers.get("Idempotency-Key")
if not idempotency_key:
return {"error": "Missing Idempotency-Key header"}, 400
# 1. Hitung SHA-256 dari biner gambar untuk payload integrity check
image_bytes = request.files["image"].read()
current_fingerprint = hashlib.sha256(image_bytes).hexdigest()
redis_key = f"idempotency:{idempotency_key}"
lock_key = f"lock:{idempotency_key}"
# 2. Cek apakah key sudah pernah diproses sebelumnya
existing_record = redis_client.get(redis_key)
if existing_record:
record = json.loads(existing_record)
# Tolak jika key sama digunakan untuk payload berbeda
if record["fingerprint"] != current_fingerprint:
return {"error": "Idempotency-Key conflict: payload mismatch"}, 422
# Kembalikan response tersimpan sesuai status saat ini
if record["status"] in ["PENDING", "PROCESSING"]:
return record["response_payload"], 202
elif record["status"] == "COMPLETED":
return record["response_payload"], 200
# 3. Dapatkan distributed lock (TTL 60 detik)
acquired = redis_client.set(lock_key, "locked", nx=True, ex=60)
if not acquired:
# Conflict race-condition: request paralel kedua saat request pertama sedang acquire
return {"error": "Concurrent request in progress, retry shortly"}, 409
try:
task_id = generate_ulid()
initial_response = {
"task_id": task_id,
"status": "PENDING",
"fingerprint": current_fingerprint
}
# 4. Simpan state awal dengan TTL (contoh: 24 jam = 86400 detik)
task_state = {
"fingerprint": current_fingerprint,
"status": "PENDING",
"response_payload": initial_response
}
redis_client.set(redis_key, json.dumps(task_state), ex=86400)
# 5. Push ke queue worker AI
queue_payload = {
"task_id": task_id,
"idempotency_key": idempotency_key,
"image_bytes": image_bytes,
"callback_url": request.form.get("callback_url")
}
gpu_task_queue.push(queue_payload)
return initial_response, 202
finally:
# Lepas lock distributed
redis_client.delete(lock_key)Pengiriman Hasil Inferensi via Webhook Terverifikasi HMAC
Setelah worker GPU menyelesaikan eksekusi model, server mengirimkan HTTP POST callback ke callback_url client. Untuk mencegah man-in-the-middle (MITM) dan memastikan keaslian callback, server menandatangani body payload menggunakan HMAC-SHA256.
Format Payload Callback
{
"event": "diagnosis.completed",
"task_id": "tsk_9021841284",
"idempotency_key": "7b84a9e2-51c3-4d6d-bf22-e4231b519cb8",
"data": {
"disease_detected": "Bipolaris cynodontis (Leaf Blight)",
"confidence": 0.942,
"severity": "MODERATE",
"bounding_box": {
"x": 120,
"y": 85,
"width": 340,
"height": 410
}
},
"processed_at": 1710576045
}Header Autentikasi Webhook
Header webhook menyertakan timestamp unix dan signature hexadecimal:
X-Signature: t=1710576045,v1=e7c8e9d50672b1d3d63d66420f5c6b6d510bf8a08a3d5821f57f58b09f428c0bVerifikasi Signature pada Sisi Client (Python)
import hmac
import hashlib
import time
def verify_webhook(raw_payload: bytes, signature_header: str, secret_key: str) -> bool:
parts = dict(x.split("=") for x in signature_header.split(","))
timestamp = parts.get("t")
received_sig = parts.get("v1")
if not timestamp or not received_sig:
return False
# Toleransi timestamp 5 menit (300 detik) untuk cegah replay attacks
if abs(time.time() - int(timestamp)) > 300:
return False
signed_payload = f"{timestamp}.".encode("utf-8") + raw_payload
computed_sig = hmac.new(
secret_key.encode("utf-8"),
signed_payload,
hashlib.sha256
).hexdigest()
# Mitigasi timing attack dengan compare_digest
return hmac.compare_digest(computed_sig, received_sig)Troubleshooting dan Edge Cases
- Payload Mismatch (HTTP 422): Terjadi jika client mengirim ulang
Idempotency-Keyyang sama namun dengan file gambar atau parameter deteksi berbeda. Server wajib memvalidasi hash SHA-256 dan menolak request dengan status422 Unprocessable Entity. - Worker Crash Saat PROCESSING: Jika GPU worker mengalami Out-of-Memory (OOM), task dapat tertahan di state
PROCESSING. Atasi dengan menerapkan Dead Man's Switch atau worker heartbeat TTL pada Redis. Jika TTL habis tanpa update, ubah status menjadiFAILEDdan izinkan retry. - Client Polling Fallback: Selalu sediakan endpoint
GET /v1/diagnostics/grass/{idempotency_key}bagi client yang tidak memiliki public IP untuk webhook callback.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!