ניסיונות חוזרים ומניעת כפילויות
מה נשמר בניסיון חוזר, ואיך צרכן נמנע מלטפל באותו אירוע פעמיים.
בקצרה
- לוח ברירת המחדל הוא שמונה ניסיונות לאורך כ-21 שעות, ואפשר לשנות אותו לכל יעד.
- ניסיון חוזר שומר על אותו מזהה מסירה, ולכן אפשר לזהות אותו ולהתעלם ממנו בבטחה.
- למניעת כפילויות יש שני חצאים: המפתח בצד המפרסם, והבדיקה בצד הצרכן.
- 410 Gone עוצר מיד, כי הצרכן ביטל את ההרשמה. זה לא כשל.
| # | שעה | סטטוס | קוד | זמן תגובה | למה |
|---|---|---|---|---|---|
| 1 | 09:41:02 | נכשל | 503 | 1,204 ms | השירות לא זמין |
| 2 | 09:41:07 | נכשל | 503 | 980 ms | ניסיון חוזר אחרי 5 שניות |
| 3 | 09:41:37 | נכשל | timeout | 15,000 ms | ניסיון חוזר אחרי 30 שניות |
| 4 | 09:46:37 | נמסר | 200 | 142 ms | ניסיון חוזר אחרי 5 דקות |
אותו webhook-id בכל ניסיון, כך שהצרכן יכול לסנן כפילויות.
כל מערכת מסירה צריכה להחליט מה עושים כשהיעד לא עונה. מי שמוותר מיד מאבד אירועים בכל פריסה. מי שמנסה שוב מיד ובלי סוף מפגיז יעד שכבר מתקשה. התשובה היא לוח זמנים שמתחיל מהר ומתרחב, ולצדו צרכן שמוכן לקבל את אותו אירוע יותר מפעם אחת.
הלוח
| ניסיון | המתנה לפניו |
|---|---|
| 1 | אין, מיידי |
| 2 | 30 שניות |
| 3 | 2 דקות |
| 4 | 10 דקות |
| 5 | 30 דקות |
| 6 | 2 שעות |
| 7 | 6 שעות |
| 8 | 12 שעות |
בסך הכול כ-21 שעות, מספיק כדי לעבור פריסה, חידוש תעודה ורוב התקלות. המרווחים הקצרים בהתחלה תופסים את הכשלים הרגעיים, והארוכים בסוף נותנים לצוות זמן לתקן בלי שהיעד יוצף. יעד שצריך משהו אחר מקבל לוח משלו:
curl -X PATCH https://api.hookget.com/v1/endpoints/ep_… \
-H "authorization: Bearer $HOOKGET_KEY" \
-d '{"retry_schedule":[0,10,60,300],"max_attempts":4}'
מניעת כפילויות משני הצדדים
בצד המפרסם. שלחו מפתח ייחודיות (idempotency key) עם הפרסום, ואותו מפתח לעולם לא ייצור אירוע שני, לא משנה כמה פעמים התשתית שלכם עצמה שולחת את הבקשה מחדש. זה חשוב כי גם הקריאה שלכם ל-API יכולה ליפול באמצע, ואז אין לכם דרך לדעת אם האירוע נשמר. עם מפתח, פשוט שולחים שוב.
-d '{"type":"order.created","payload":{…},"idempotency_key":"ord_10241-created"}'
בצד הצרכן. webhook-id נשאר קבוע בכל ניסיון חוזר של אותה מסירה. שמרו אותו
והתעלמו מכל מה שכבר טיפלתם בו:
const id = headers['webhook-id'];
if (await seen(id)) return res.status(200).end(); // already handled
await handle(event);
await remember(id); // then remember it
res.status(200).end();
הסדר בקוד הזה מכוון. קודם בודקים אם המזהה כבר הופיע, ואם כן עונים 200 ולא עושים כלום, כי מבחינת השולח המסירה הצליחה. אחר כך מטפלים באירוע, ורק בסוף שומרים את המזהה, כדי שכשל באמצע הטיפול לא יסמן את האירוע כמטופל.
עונים מהר ועובדים אחר כך. צרכן שמעבד שלושים שניות לפני שהוא עונה יקבל ניסיון חוזר בזמן שהוא עדיין עובד, ואז יצטרך להתמודד עם כפילות גם מול העבודה החצי גמורה של עצמו.
מה עוצר את הלוח מוקדם
| תשובה | מה קורה |
|---|---|
| 2xx | נמסר. הניסיון נרשם ומונה הכשלים מתאפס |
| 410 Gone | היעד מושבת מיד, כי הוא הסיר את עצמו |
| 4xx (אחר) | ניסיון חוזר: 401 הוא בדרך כלל סוד חתימה שהוחלף, לא סירוב קבוע |
| 5xx, timeout או שגיאת חיבור | ניסיון חוזר לפי הלוח |
| הלוח נגמר | עובר לתור הכשלים (DLQ) ונשמר |
שאלות
כמה פעמים HookGet מנסה שוב מסירה שנכשלה?
בלוח ברירת המחדל יש שמונה ניסיונות, לאורך כ-21 שעות: הראשון מיידי, והמרווחים גדלים מ-30 שניות ועד 12 שעות. אפשר להגדיר לוח אחר לכל יעד, עם מרווחים ומספר ניסיונות משלו.
למה הצרכן שלי קיבל את אותו אירוע פעמיים?
כמעט תמיד בגלל ניסיון חוזר. היעד טיפל באירוע, אבל התשובה לא הגיעה בזמן, או שהחיבור נפל אחרי
העיבוד, ומבחינת השולח המסירה נכשלה. זה לא באג שצריך לתקן, אלא בדיוק המנגנון שמונע אובדן אירועים. הפתרון
הוא בצד המקבל: לשמור את webhook-id ולדלג על מה שכבר טופל.
מה ההבדל בין מפתח ייחודיות בפרסום לבין webhook-id?
מפתח הייחודיות (idempotency key) שומר על הצד שלכם: אותו מפתח בפרסום לעולם לא יוצר אירוע שני.
webhook-id שומר על הצרכן: הוא קבוע בכל ניסיון חוזר של מסירה, ולכן הצרכן יכול לזהות כפילות.
צריך את שניהם, כי כל אחד סוגר פער אחר.
למה 401 מקבל ניסיון חוזר ולא עוצר את הלוח?
כי 401 הוא בדרך כלל סוד חתימה שהוחלף בצד אחד ועוד לא בצד השני, ולא סירוב קבוע. אילו המסירה
נעצרה, תקלת תיאום של כמה דקות הייתה הופכת לאירועים חסרים. רק 410 Gone נחשב
להודעה ברורה שהיעד לא רוצה יותר מסירות.
להתחיל למסור וובהוקים היום
מפנים את הוובהוקים ל-HookGet ורואים את המסירה הראשונה מגיעה, חתומה, תוך פחות מדקה.
פתיחת חשבון חינם לנסות את בודק הוובהוקים החינמי
10,000 מסירות בחודש בחינם, בלי כרטיס אשראי. המסלול החינמי חוסם ולא מחייב, כך שניסיון לא יכול להסתיים בחשבונית.