Tantangan Sinkronisasi Status Katalog Karya Seni
Sinkronisasi status karya seni (seperti status ketersediaan, reservasi kurator, atau perubahan harga lelang) menuntut konsistensi tinggi. Pengiriman payload webhook melalui jaringan publik memiliki dua masalah utama: kegagalan transmisi yang memicu retry otomatis (duplikasi data), dan latensi rute jaringan yang menyebabkan urutan kedatangan payload tidak berurutan (out-of-order delivery).
Jika webhook pembaruan status SOLD tiba sebelum webhook RESERVED akibat rute jaringan yang lambat, sistem penerima berisiko membatalkan penjualan yang sah jika tidak memvalidasi versi entitas secara ketat.
Arsitektur Idempotensi Menggunakan Redis TTL
Untuk mencegah eksekusi ganda akibat retry webhook penyedia eksternal, consumer harus memanfaatkan header Idempotency-Key unik yang dikirimkan oleh producer.
- Atomic Lock: Simpan kunci ke dalam Redis menggunakan instruksi atomic
SET key status NX EX ttl. - Status Pending vs Success: Tandai nilai sebagai
PROCESSINGsaat proses mutasi berlangsung. Ubah status menjadiCOMPLETEDbeserta hash hasil respons setelah mutasi database berhasil. - Cache Retention: Gunakan TTL moderat (misalnya 86400 detik / 24 jam) untuk menolak eksekusi ulang payload dengan kunci yang sama dan langsung mengembalikan status HTTP yang sesuai tanpa mutasi database.
Menangani Out-of-Order Delivery: Entity Versioning vs Sequence Number
Penyedia webhook tidak menjamin paket HTTP tiba sesuai urutan waktu pembuatan. Dua pendekatan utama digunakan untuk memitigasi anomali ini:
1. Monotonic Sequence Number
Producer menyertakan kolom integer sequence_id yang naik secara berkala per entitas. Consumer membandingkan nilai incoming sequence dengan sequence terakhir di database lokal:
UPDATE artworks
SET status = $1, last_sequence = $2, updated_at = NOW()
WHERE id = $3 AND last_sequence < $2;Jika baris terpengaruh (affected rows) bernilai 0 dan last_sequence lokal lebih besar, payload merupakan data usang dan harus diabaikan tanpa melempar status error.
2. Entity Versioning Berbasis Timestamp ISO8601
Jika generator sequence sentral tidak tersedia pada platform multi-region, gunakan timestamp modifikasi tingkat milidetik (misalnya 2026-03-31T08:14:20.123Z) yang disematkan langsung pada payload karya seni. Validasi dijalankan secara atomik pada level database engine.
Implementasi Kode Webhook Consumer (Node.js & Express)
Berikut implementasi endpoint consumer minimal dengan verifikasi signature HMAC SHA-256, penguncian atomic Redis, dan pencegahan update out-of-order.
import crypto from 'node:crypto';
import express from 'express';
import Redis from 'ioredis';
import { Pool } from 'pg';
const app = express();
const redis = new Redis(process.env.REDIS_URL);
const db = new Pool({ connectionString: process.env.DATABASE_URL });
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
// Tangkap raw body untuk kalkulasi signature
app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf; } }));
function verifyHmacSignature(rawBody, signatureHeader) {
if (!signatureHeader) return false;
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
const digest = 'sha256=' + hmac.update(rawBody).digest('hex');
return crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(signatureHeader));
}
app.post('/api/webhooks/artworks', async (req, res) => {
const signature = req.headers['x-signature-sha256'];
const idempotencyKey = req.headers['idempotency-key'];
if (!verifyHmacSignature(req.rawBody, signature)) {
return res.status(401).json({ error: 'Invalid signature' });
}
if (!idempotencyKey) {
return res.status(400).json({ error: 'Missing Idempotency-Key header' });
}
const redisLockKey = `idempotency:artwork:${idempotencyKey}`;
const acquired = await redis.set(redisLockKey, 'PROCESSING', 'EX', 86400, 'NX');
if (!acquired) {
const status = await redis.get(redisLockKey);
if (status === 'PROCESSING') {
return res.status(409).json({ message: 'Request currently being processed' });
}
return res.status(200).json({ message: 'Payload already processed' });
}
const { artwork_id, status, updated_at } = req.body;
try {
// Mutasi status hanya jika updated_at incoming lebih baru dari versi database
const query = `
UPDATE artworks
SET status = $1, version_timestamp = $2
WHERE id = $3 AND version_timestamp < $2::timestamptz
RETURNING id;
`;
const result = await db.query(query, [status, updated_at, artwork_id]);
if (result.rowCount === 0) {
// Check apakah entitas memang ada (out-of-order) atau tidak ditemukan
const exists = await db.query('SELECT version_timestamp FROM artworks WHERE id = $1', [artwork_id]);
if (exists.rowCount > 0) {
// Payload basi/out-of-order diabaikan secara aman
await redis.set(redisLockKey, 'STALE', 'EX', 86400);
return res.status(200).json({ message: 'Stale update skipped' });
}
await redis.del(redisLockKey);
return res.status(404).json({ error: 'Artwork not found' });
}
await redis.set(redisLockKey, 'COMPLETED', 'EX', 86400);
return res.status(200).json({ message: 'Artwork status updated' });
} catch (err) {
await redis.del(redisLockKey); // Buka lock agar producer dapat melakukan retry valid
console.error('Webhook error:', err);
return res.status(500).json({ error: 'Internal processing failure' });
}
});
// ponytail: gunakan worker queue (BullMQ/RabbitMQ) jika proses penyesuaian katalog melibatkan transisi aset media
app.listen(3000, () => console.log('Webhook consumer online'));Panduan HTTP Status Response Code
Respons yang tepat memberi instruksi eksplisit kepada webhook dispatcher:
- 200 OK: Mutasi berhasil diterapkan, atau payload duplikat/basi telah diidentifikasi dan sengaja diabaikan. Ini menghentikan siklus retry dari pengirim.
- 400 Bad Request: Struktur payload rusak atau header wajib (seperti
Idempotency-Key) hilang. Dispatcher tidak perlu mengirim ulang payload yang sama tanpa revisi kode. - 401 Unauthorized: Kegagalan verifikasi HMAC SHA-256. Mencegah manipulasi pihak ketiga.
- 409 Conflict: Kunci idempotensi sedang aktif dieksekusi secara konkuren. Pengirim disarankan melakukan retry dengan jeda backoff singkat.
- 500 Internal Server Error: Terjadi kendala basis data internal. Lock Redis harus dihapus agar siklus retry webhook berikutnya dapat mengeksekusi ulang permintaan.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!