Pengenalan
Jika endpoint webhook Anda gagal merespons atau mengembalikan status code non-2xx, CSKU AI akan secara otomatis mencoba mengirim ulang webhook.
Mekanisme retry memastikan webhook Anda terkirim meskipun terjadi masalah sementara.
Konfigurasi Retry
| Parameter | Nilai |
|---|
| Maksimum Retry | 3 kali |
| Jeda Retry | 30 detik antar percobaan |
| Metode Retry | Exponential backoff |
Proses Retry
Request Awal → Gagal
↓
Tunggu 30 detik
↓
Retry 1 → Gagal
↓
Tunggu 30 detik
↓
Retry 2 → Gagal
↓
Tunggu 30 detik
↓
Retry 3 → Gagal
↓
Tandai sebagai kegagalan permanen
Setelah 3 percobaan yang gagal, webhook ditandai sebagai failed_permanent dan tidak akan di-retry lagi.
Memicu Retry
Webhook di-retry ketika:
Server Anda tidak merespons dalam periode timeout.
Server Anda mengembalikan status code di luar rentang 200-299.
Koneksi gagal karena masalah jaringan.
Server Anda mengembalikan status 500 atau error serupa.
Respons Sukses
Webhook dianggap berhasil terkirim ketika server Anda mengembalikan:
Konten body respons tidak penting. Hanya status code yang dipertimbangkan.
Kegagalan Permanen
Setelah 3 percobaan yang gagal, webhook ditandai sebagai kegagalan permanen dan tidak akan di-retry. Anda dapat:
- Memeriksa webhook yang gagal di dashboard CSKU AI Anda
- Query tabel
tbl_resend_webhook untuk percobaan yang gagal
- Mengirim ulang webhook secara manual jika diperlukan
Idempotensi
Kritis: Endpoint webhook Anda harus idempoten. Event yang sama mungkin diterima beberapa kali.
Mengapa Idempotensi Penting
Karena retry, webhook Anda mungkin menerima event yang sama beberapa kali. Tanpa idempotensi, Anda bisa:
- Memproses pesan yang sama dua kali
- Mengirim notifikasi duplikat
- Membuat record database duplikat
- Menagih pelanggan dua kali
Mengimplementasikan Idempotensi
Menggunakan Message ID
const processedEvents = new Set();
app.post('/webhook', async (req, res) => {
const { event, message } = req.body;
// Buat identifier event unik
const eventId = `${event}_${message.id}`;
// Periksa apakah sudah diproses
if (processedEvents.has(eventId)) {
console.log('Event duplikat, melewati');
return res.status(200).send('OK');
}
try {
// Proses event
await handleEvent(event, message);
// Tandai sebagai sudah diproses
processedEvents.add(eventId);
// Opsional: Persist ke database
await db.processedEvents.create({ event_id: eventId });
res.status(200).send('OK');
} catch (error) {
console.error('Error memproses webhook:', error);
res.status(500).send('Error');
}
});
Menggunakan Database untuk Persistensi
app.post('/webhook', async (req, res) => {
const { event, message } = req.body;
const eventId = `${event}_${message.id}`;
try {
// Periksa database untuk event yang ada
const existing = await db.processedEvents.findOne({ event_id: eventId });
if (existing) {
console.log('Event sudah diproses, melewati');
return res.status(200).send('OK');
}
// Proses event
await handleEvent(event, message);
// Simpan sebagai sudah diproses
await db.processedEvents.create({ event_id: eventId, processed_at: new Date() });
res.status(200).send('OK');
} catch (error) {
console.error('Error memproses webhook:', error);
// Jangan kembalikan 200 pada error untuk memicu retry
res.status(500).send('Error');
}
});
Membersihkan Event yang Diproses
// Pembersihan in-memory (batasi ke 10,000 event)
if (processedEvents.size > 10000) {
const eventsArray = Array.from(processedEvents);
processedEvents.clear();
eventsArray.slice(5000).forEach(id => processedEvents.add(id));
}
// Pembersihan database (hapus event lebih dari 7 hari)
await db.processedEvents.deleteMany({
processed_at: { $lt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) }
});
Praktik Terbaik untuk Retry
Proses webhook dengan cepat dan respons dalam 5 detik untuk menghindari timeout.
Kembalikan 200 pada Sukses
Selalu kembalikan HTTP 200 setelah berhasil memproses webhook.
Kembalikan 500 pada Error Pemrosesan
Kembalikan status 5xx untuk memicu retry jika Anda mengalami error pemrosesan sementara.
Log pengiriman webhook yang berhasil maupun gagal untuk debugging.
Gunakan exponential backoff jika Anda melakukan retry operasi dalam handler webhook Anda.
Contoh: Handler Retry Komprehensif
const processedEvents = new Map(); // Simpan eventId dengan timestamp
app.post('/webhook', async (req, res) => {
const { event, message } = req.body;
const eventId = `${event}_${message.id}`;
const now = Date.now();
// Periksa duplikat (dalam 1 jam)
const existing = processedEvents.get(eventId);
if (existing && (now - existing.timestamp) < 3600000) {
console.log('Event duplikat:', eventId);
return res.status(200).send('OK');
}
try {
// Proses event
await handleEvent(event, message);
// Tandai sebagai sudah diproses
processedEvents.set(eventId, { timestamp: now });
// Bersihkan event lama (lebih dari 24 jam)
const hourAgo = now - 86400000;
for (const [id, data] of processedEvents.entries()) {
if (data.timestamp < hourAgo) {
processedEvents.delete(id);
}
}
res.status(200).send('OK');
} catch (error) {
console.error('Error pemrosesan webhook:', error);
// Tentukan apakah retry harus terjadi
if (isTemporaryError(error)) {
// Kembalikan 500 untuk memicu retry
res.status(500).send('Error sementara');
} else {
// Kembalikan 200 untuk menghindari retry (kegagalan idempoten)
res.status(200).send('OK');
}
}
});
function isTemporaryError(error) {
// Retry pada masalah koneksi database, error jaringan, dll.
return (
error.code === 'ECONNREFUSED' ||
error.code === 'ETIMEDOUT' ||
error.name === 'DatabaseConnectionError'
);
}
Monitoring Retry
Lacak Metrik Retry
const metrics = {
attempts: 0,
successes: 0,
failures: 0,
duplicates: 0
};
app.post('/webhook', async (req, res) => {
metrics.attempts++;
const { event, message } = req.body;
const eventId = `${event}_${message.id}`;
// Periksa duplikat
if (isDuplicate(eventId)) {
metrics.duplicates++;
return res.status(200).send('OK');
}
try {
await handleEvent(event, message);
metrics.successes++;
// Log metrik setiap 100 request
if (metrics.attempts % 100 === 0) {
console.log('Metrik webhook:', metrics);
}
res.status(200).send('OK');
} catch (error) {
metrics.failures++;
console.error('Error webhook:', error);
res.status(500).send('Error');
}
});
Contoh Output Metrik
Metrik webhook: {
attempts: 100,
successes: 95,
failures: 3,
duplicates: 2
}
Jumlah duplikat yang tinggi mungkin menandakan webhook Anda memproses lambat, menyebabkan retry.
Troubleshooting Retry
Duplikat Berlebihan
Gejala: Jumlah event duplikat yang tinggi
Solusi:
- Periksa waktu pemrosesan webhook (harus < 5 detik)
- Optimalkan query database
- Gunakan pemrosesan async untuk operasi berat
- Periksa resource server (CPU, memory)
Kegagalan Permanen
Gejala: Semua webhook ditandai sebagai kegagalan permanen
Solusi:
- Verifikasi URL webhook benar dan dapat diakses
- Periksa autentikasi (secret key)
- Uji endpoint dengan curl
- Tinjau log server untuk error
- Pastikan firewall mengizinkan koneksi masuk
Kegagalan Intermiten
Gejala: Beberapa webhook berhasil, yang lain gagal
Solusi:
- Implementasikan penanganan error yang tepat
- Gunakan exponential backoff untuk retry
- Periksa stabilitas koneksi database
- Monitor penggunaan resource server
- Implementasikan health check
Langkah Selanjutnya