ข้ามไปยังเนื้อหาหลัก

ลอจิกการลองใหม่ของเวบฮุก

เรียนรู้วิธีการจัดการการลองใหม่ของเวบฮุก, ใช้ความสามารถในการเป็นปัญหาเดียวกันเพื่อจัดการการจัดส่งที่ซ้ำกัน และสร้างระบบการประมวลผลเวบฮุกที่เชื่อถือได้

ภาพรวม

การจัดส่งเวบฮุกไม่รับประกันในครั้งแรก ปัญหาเครือข่าย, ความล้มเหลวของเซิร์ฟเวอร์ หรือข้อผิดพลาดในการประมวลผลสามารถทำให้เกิดความล้มเหลว คำแนะนำนี้ครอบคลุม:

  • ลำดับการลองใหม่อัตโนมัติของ Omise และ exponential backoff
  • การใช้ความสามารถในการเป็นปัญหาเดียวกันเพื่อจัดการเหตุการณ์ที่ซ้ำกัน
  • การบันทึกและตรวจสอบการจัดส่งเวบฮุก
  • ขั้นตอนการลองใหม่ด้วยตนเอง
  • แนวทางปฏิบัติที่ดีสำหรับการจัดการความล้มเหลว

ตารางการลองใหม่

วิธีการลองใหม่เวบฮุก

เมื่อการจัดส่งเวบฮุกล้มเหลว (การตอบสนอง non-2xx หรือการหมดเวลา), Omise จะลองใหม่โดยอัตโนมัติด้วย exponential backoff:

ความพยายาม 1: ทันที (การจัดส่งเริ่มต้น)
ความพยายาม 2: หลังจาก 1 นาที
ความพยายาม 3: หลังจาก 5 นาที
ความพยายาม 4: หลังจาก 15 นาที
ความพยายาม 5: หลังจาก 1 ชั่วโมง
ความพยายาม 6: หลังจาก 3 ชั่วโมง
ความพยายาม 7: หลังจาก 6 ชั่วโมง
ความพยายาม 8: หลังจาก 12 ชั่วโมง
ความพยายาม 9: หลังจาก 24 ชั่วโมง
ความพยายาม 10: หลังจาก 48 ชั่วโมง
ความพยายามสุดท้าย: ทุกๆ 48 ชั่วโมงจนกระทั่ง 7 วันผ่านไป

พฤติกรรมการลองใหม่

ตัวบ่งชี้สำหรับการลองใหม่:

  • รหัสสถานะ HTTP 400-599 (ข้อผิดพลาดของไคลเอนต์และเซิร์ฟเวอร์)
  • Timeout เครือข่าย (ไม่มีการตอบสนองภายใน 10 วินาที)
  • ความล้มเหลวในการเชื่อมต่อ (ข้อผิดพลาด DNS, การปฏิเสธการเชื่อมต่อ)

เกณฑ์ความสำเร็จ:

  • รหัสสถานะ HTTP 200-299
  • การตอบสนองที่ได้รับภายใน 10 วินาที

ระยะเวลาการลองใหม่:

  • การลองใหม่ดำเนินต่อไปเป็นเวลา 7 วันนับจากความพยายามเริ่มต้น
  • หลังจาก 7 วัน, เวบฮุกจะถูกทำเครื่องหมายว่าล้มเหลวถาวร

ความเป็นปัญหาเดียวกัน

ความสามารถในการเป็นปัญหาเดียวกันช่วยให้การประมวลผลเวบฮุกหลายครั้งมีเอฟเฟกต์เดียวกับการประมวลผลครั้งเดียว นี่เป็นสิ่งสำคัญเพราะเวบฮุกสามารถจัดส่งหลายครั้งเนื่องจากการลองใหม่

เหตุใดความเป็นปัญหาเดียวกันจึงสำคัญ

สถานการณ์: การจัดส่งเวบฮุกล้มเหลวหลังจากประมวลผล แต่ก่อนการตอบสนอง

1. เวบฮุกได้รับ
2. ประจำถูกทำเครื่องหมายว่าจ่ายแล้ว ✓
3. อีเมลส่งให้ลูกค้า ✓
4. เซิร์ฟเวอร์ขัดข้องก่อนการตอบสนอง ✗
5. Omise ลองใหม่เวบฮุก
6. ไม่มีความสามารถในการเป็นปัญหาเดียวกัน:
- ประจำถูกทำเครื่องหมายว่าจ่ายแล้วอีกครั้ง (ซ้ำกัน)
- ลูกค้าได้รับอีเมลซ้ำ ✗
7. ด้วยความสามารถในการเป็นปัญหาเดียวกัน:
- จำหน่ายเหตุการณ์ที่ประมวลผลแล้ว
- ข้ามการประมวลผลที่ซ้ำกัน ✓

รูปแบบความสามารถในการเป็นปัญหาเดียวกัน

1. การติดตาม Event ID

// Node.js - การติดตาม Event ID ด้วย Redis
const redis = require('redis');
const client = redis.createClient();

async function processWebhookIdempotent(event) {
const eventId = event.id;
const processedKey = `webhook:processed:${eventId}`;

// ตรวจสอบว่าประมวลผลแล้ว
const isProcessed = await client.exists(processedKey);
if (isProcessed) {
console.log(`Event ${eventId} already processed, skipping`);
return { status: 'duplicate', eventId };
}

try {
// ประมวลผลเวบฮุก
await processWebhook(event);

// ทำเครื่องหมายเป็นประมวลผล (เก็บไว้ 7 วัน)
await client.setex(processedKey, 7 * 24 * 3600, JSON.stringify({
processed_at: new Date().toISOString(),
event_key: event.key
}));

return { status: 'success', eventId };
} catch (error) {
console.error(`Error processing event ${eventId}:`, error);
throw error;
}
}

2. ข้อจำกัดฐานข้อมูล

// Node.js - ความสามารถในการเป็นปัญหาเดียวกันที่ระดับฐานข้อมูลด้วย MongoDB
const mongoose = require('mongoose');

// โครงการเวบฮุกพร้อมข้อจำกัดแบบเฉพาะ
const WebhookEventSchema = new mongoose.Schema({
event_id: {
type: String,
required: true,
unique: true, // ป้องกันการประมวลผลที่ซ้ำกัน
index: true
},
event_key: { type: String, required: true },
processed_at: { type: Date, default: Date.now },
data: { type: mongoose.Schema.Types.Mixed },
status: {
type: String,
enum: ['processing', 'completed', 'failed'],
default: 'processing'
}
});

const WebhookEvent = mongoose.model('WebhookEvent', WebhookEventSchema);

async function processWebhookIdempotent(event) {
try {
// พยายามแทรกบันทึกเหตุการณ์
const webhookEvent = new WebhookEvent({
event_id: event.id,
event_key: event.key,
data: event.data
});

await webhookEvent.save();

} catch (error) {
// ข้อผิดพลาดคีย์ที่ซ้ำกัน - เหตุการณ์ประมวลผลแล้ว
if (error.code === 11000) {
console.log(`Event ${event.id} already processed`);
return { status: 'duplicate' };
}
throw error;
}

try {
// ประมวลผลเวบฮุก
await processWebhook(event);

// อัปเดตสถานะ
await WebhookEvent.updateOne(
{ event_id: event.id },
{ status: 'completed' }
);

return { status: 'success' };
} catch (error) {
// อัปเดตสถานะเป็นล้มเหลว
await WebhookEvent.updateOne(
{ event_id: event.id },
{ status: 'failed', error: error.message }
);
throw error;
}
}

การจัดการความล้มเหลว

การจัดการข้อผิดพลาดที่สง่างาม

ตอบสนองต่อเวบฮุกอย่างรวดเร็วแม้ว่าการประมวลผลจะล้มเหลว:

// Node.js - การจัดการข้อผิดพลาดที่สง่างาม
app.post('/webhooks/omise', async (req, res) => {
let event;

try {
// ตรวจสอบลายเซ็น
const signature = req.headers['x-omise-signature'];
if (!signature || !verifySignature(req.body, signature)) {
return res.status(401).json({ error: 'Invalid signature' });
}

event = JSON.parse(req.body.toString());

// ตอบสนองทันที (ก่อนการประมวลผล)
res.status(200).json({ received: true });

} catch (error) {
console.error('Webhook parsing error', error);
return res.status(400).json({ error: 'Invalid request' });
}

// ประมวลผลแบบไม่ซิงโครนัสด้วยการจัดการข้อผิดพลาด
processWebhookWithRetry(event)
.then(() => {
console.log('Webhook processed successfully');
})
.catch(error => {
console.error('Webhook processing failed', error);
// เข้าคิวสำหรับการตรวจสอบด้วยตนเอง
queueForManualReview(event, error);
});
});

คิวจดหมายที่ส่งไม่สำเร็จ

# Python - Dead letter queue ด้วย Redis
import redis
import json
from datetime import datetime

redis_client = redis.Redis(host='localhost', port=6379, decode_responses=True)

def queue_for_manual_review(event, error):
"""เพิ่มเวบฮุกที่ล้มเหลวลงในคิวตัวอักษรที่ตายแล้ว"""
failed_webhook = {
'event': event,
'error': str(error),
'failed_at': datetime.now().isoformat(),
'attempts': get_attempt_count(event['id'])
}

# เพิ่มลงในคิว (sorted set ตามเวลา)
redis_client.zadd(
'webhook:dead_letter_queue',
{json.dumps(failed_webhook): datetime.now().timestamp()}
)

# ส่งการแจ้งเตือนไปยังทีมการดำเนินการ
send_alert(f"Webhook {event['id']} failed after retries", failed_webhook)

def retry_failed_webhook(event_id):
"""ลองใหม่เวบฮุกที่ล้มเหลวด้วยตนเอง"""
items = redis_client.zrange('webhook:dead_letter_queue', 0, -1)

for item in items:
webhook = json.loads(item)
if webhook['event']['id'] == event_id:
try:
# ลองประมวลผลอีกครั้ง
process_webhook(webhook['event'])

# ลบออกจาก dead letter queue
redis_client.zrem('webhook:dead_letter_queue', item)

print(f"Successfully reprocessed webhook {event_id}")
return True
except Exception as e:
print(f"Retry failed: {e}")
return False

print(f"Webhook {event_id} not found in dead letter queue")
return False

การบันทึกและการตรวจสอบ

การบันทึกเวบฮุกที่ครอบคลุม

// Node.js - การบันทึกโครงสร้างด้วย Winston
const winston = require('winston');

const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({
filename: 'webhooks-error.log',
level: 'error'
}),
new winston.transports.File({
filename: 'webhooks-combined.log'
})
]
});

// บันทึกการตรวจสอบ
logger.info('webhook.received', {
event_id: event.id,
event_key: event.key,
livemode: event.livemode,
ip: req.ip,
received_at: new Date().toISOString()
});

// บันทึกการตรวจสอบลายเซ็น
logger.info('webhook.signature_verified', {
event_id: event.id,
signature_present: !!signature
});

// บันทึกการเริ่มต้นการประมวลผล
logger.info('webhook.processing_started', {
event_id: event.id,
event_key: event.key,
started_at: new Date().toISOString()
});

// บันทึกข้อผิดพลาด
logger.error('webhook.processing_failed', {
event_id: event.id,
event_key: event.key,
error: error.message,
stack: error.stack,
attempt: attemptCount,
duration_ms: Date.now() - startTime
});

การลองใหม่ด้วยตนเอง

การลองใหม่จากแดชบอร์ด

ใช้แดชบอร์ด Omise เพื่อลองใหม่เวบฮุกที่ล้มเหลว:

  1. นำทางไปยังส่วนเวบฮุก
  2. เลือกจุดสิ้นสุดเวบฮุกของคุณ
  3. ดู "Recent Deliveries"
  4. ค้นหาการจัดส่งที่ล้มเหลว
  5. คลิก "Resend"

การลองใหม่ด้วยตนเอง API

// Node.js - การลองใหม่ด้วยตนเองผ่าน API
const Omise = require('omise');
const omise = Omise({ secretKey: process.env.OMISE_SECRET_KEY });

async function retryFailedWebhooks() {
try {
// ดึงเหตุการณ์ล่าสุด
const events = await omise.events.list({ limit: 100 });

for (const event of events.data) {
// ตรวจสอบว่าเหตุการณ์ต้องการการประมวลผลซ้ำ
const needsRetry = await checkIfNeedsRetry(event.id);

if (needsRetry) {
console.log(`Retrying event ${event.id}`);

try {
await processWebhook(event);
console.log(`Successfully processed ${event.id}`);
} catch (error) {
console.error(`Failed to process ${event.id}:`, error);
}
}
}
} catch (error) {
console.error('Error retrieving events:', error);
}
}

async function checkIfNeedsRetry(eventId) {
// ตรวจสอบว่าเหตุการณ์ประมวลผลเรียบร้อยแล้ว
const processed = await db.webhookEvents.findOne({
event_id: eventId,
status: 'completed'
});

return !processed;
}

retryFailedWebhooks();

แนวทางปฏิบัติที่ดี

รายการตรวจสอบการประมวลผลเวบฮุก

  • ตอบภายใน 10 วินาที - ส่งกลับ 200 ทันที
  • ใช้ความสามารถในการเป็นปัญหาเดียวกัน - ติดตาม ID เหตุการณ์หรือข้อจำกัดฐานข้อมูล
  • ประมวลผลแบบไม่ซิงโครนัส - ใช้คิวหรือตัวเรียกใช้ในพื้นหลัง
  • บันทึกอย่างครอบคลุม - ติดตามเหตุการณ์เวบฮุกทั้งหมดและผลลัพธ์
  • ตรวจสอบเมตริก - ติดตามอัตราข้อผิดพลาด, ความล่าช้า, ซ้ำกัน
  • จัดการซ้ำกันอย่างสง่างาม - ไม่ถือว่าเป็นข้อผิดพลาด
  • ใช้ลอจิกการลองใหม่ - สำหรับความล้มเหลวชั่วคราวในการประมวลผล
  • ใช้ dead letter queue - สำหรับเวบฮุกที่ล้มเหลวหลังจากการลองใหม่สูงสุด
  • ตั้งค่าการแจ้งเตือน - สำหรับความล้มเหลวที่สำคัญหรืออัตราข้อผิดพลาดสูง
  • ทดสอบสถานการณ์ความล้มเหลว - ให้แน่ใจว่ามีการเสื่อมคุณภาพอย่างสง่างาม

ทรัพยากรที่เกี่ยวข้อง

ขั้นตอนถัดไป

  1. ใช้ความสามารถในการเป็นปัญหาเดียวกัน ในตัวจัดการเวบฮุก
  2. ตั้งค่า dead letter queue สำหรับเวบฮุกที่ล้มเหลว
  3. ใช้ลอจิกการลองใหม่ สำหรับความล้มเหลวชั่วคราว
  4. ตั้งค่าการตรวจสอบสุขภาพ สำหรับเซิร์ฟเวอร์การผลิต
  5. ตั้งค่าการตรวจสอบ สำหรับปัญหาเวบฮุก
  6. ทดสอบกรณีของความล้มเหลว ก่อนไปใช้งาน
  7. ตั้งค่าการแจ้งเตือน สำหรับรูปแบบข้อผิดพลาดที่ผิดปกติ

เสร็จแล้ว ไปไปใช้งาน ดูProduction Checklist

ต้องการความปลอดภัย ดูความปลอดภัยเวบฮุก