למה אינטגרציה "עובדת" בדמו ונכשלת בשקט בייצור
בבדיקת קבלה סטנדרטית שולחים הודעה אחת, רואים שהיא הגיעה, ומאשרים. בייצור, אותה אינטגרציה מעבדת אלפי הודעות ביום, וחלקן ייכשלו - בגלל Timeout, נעילת שורה, הרשאה שפג תוקפה או שינוי סכימה במערכת השנייה. השאלה שקובעת את איכות הפתרון היא לא "האם האינטגרציה עובדת", אלא "מה קורה כשהיא לא עובדת, ומי שם לב".
רוב הכשלים היקרים שראיתי לא נבעו מבאג בקוד האינטגרציה עצמו, אלא מהיעדר שלוש יכולות: זיהוי שהודעה נכשלה, מנגנון שמנסה שוב בלי ליצור כפילות, ותהליך שמוודא שהמידע בשתי המערכות אכן תואם בסוף היום. בלעדיהן, כל אינטגרציה "עובדת" עד הרגע שבו מתגלה שהיא לא עבדה כבר שבועיים.
ארבע השכבות שמרכיבות טיפול נכון בשגיאות
| שכבה | מה היא פותרת | כשל טיפוסי בלעדיה |
|---|---|---|
| Idempotency | הרצה חוזרת של אותה הודעה לא יוצרת רשומה כפולה | הזמנה כפולה או תנועת מלאי כפולה אחרי Retry |
| Retry עם Backoff | כשל זמני (Timeout, Rate Limit) מתוקן אוטומטית | עומס רגעי הופך לתקלה קבועה |
| Dead Letter Queue | כשל שאינו זמני מסומן ולא נעלם בשקט | הודעה "נבלעת" והצדדים חושבים שהיא טופלה |
| Reconciliation עסקי | פערי נתונים שלא הצליחו להיכשל בבירור מתגלים | דוח חודשי מגלה פער שכבר קשה לשחזר את מקורו |
כל שכבה תלויה בקודמת. Retry בלי Idempotency יוצר כפילויות; Dead Letter בלי Reconciliation מסתיר את זה שגם הודעות שהצליחו "טכנית" לא בהכרח שיקפו את המצב העסקי הנכון.
Idempotency: המפתח שמונע כפילות
כל אינטגרציה שיכולה לקבל את אותה הודעה יותר מפעם אחת - וכמעט כל אינטגרציה כזו - זקוקה למפתח ייחודי חיצוני (External ID) שמזהה את האירוע, לא רק את הרשומה. ב-Salesforce המימוש הנפוץ הוא Upsert לפי שדה External ID עם Unique constraint, בשילוב טבלת לוג (Custom Object או Platform Event Log) שרושמת אילו מזהי אירועים כבר טופלו במלואם.
הטעות השכיחה: להסתפק ב-Upsert על הרשומה העסקית עצמה (למשל Order External ID) בלי לתעד שלבי ביניים. אם התהליך כולל גם עדכון מלאי במערכת חיצונית, Upsert על ההזמנה לא מונע קריאה כפולה לעדכון המלאי - כל תת-פעולה בעלת תופעת לוואי חיצונית (Side Effect) צריכה להיות Idempotent בפני עצמה, לא רק הרשומה הסופית.
Retry: מדיניות Backoff וסיווג שגיאות
לא כל שגיאה ראויה ל-Retry. יש להפריד מראש בין שלוש קטגוריות:
- שגיאות זמניות (Timeout, 503, Rate Limit) - מועמדות ל-Retry עם Exponential Backoff, כלומר המרווח בין ניסיונות גדל (למשל 30 שנ', 2 דק', 10 דק') כדי לא להחמיר עומס.
- שגיאות מבניות (שדה חובה חסר, הפרת Validation Rule, ערך לא חוקי) - לא יילכו ל-Retry, כי הן ייכשלו שוב באותה צורה. הן צריכות לעבור ישירות ל-Dead Letter.
- שגיאות הרשאה או קונפיגורציה (Token שפג, שינוי API Version) - דורשות התראה מיידית לצוות טכני, כי הן חוסמות את כל התור ולא רק הודעה בודדת.
ב-Salesforce, מימוש Retry נעשה בדרך כלל בשכבת ה-Middleware או ב-Apex Queueable/Batch עם מונה ניסיונות שמור על הרשומה עצמה. מספר ניסיונות סביר לרוב המקרים הוא 3-5 עם Backoff, לא Retry אינסופי - Retry ללא גבול הופך תקלה זמנית לעומס מתמשך על שתי המערכות.
Dead Letter Queue: איפה הודעות כושלות "חיות"
Dead Letter הוא לא רק מקום איחסון - הוא חוזה. כל הודעה שמגיעה אליו צריכה: מזהה אירוע מקורי, Payload מלא, סיבת כשל מסווגת, מספר ניסיונות שבוצעו, וזמן כניסה לתור. בלי המידע הזה, "טיפול" ב-Dead Letter הופך לניחוש.
שתי גישות נפוצות למימוש ב-Salesforce:
- Custom Object ייעודי (
Integration_Failed_Message__c) עם שדות מובנים ו-List View לפי סוג שגיאה - מתאים כשצריך שקיפות לצוות עסקי בתוך Salesforce עצמו. - תור חיצוני בשכבת ה-Middleware (למשל Dead Letter Exchange ב-MuleSoft/Boomi) - מתאים כשהצוות הטכני מנטר מחוץ ל-Salesforce ורוצה להימנע מעומס על ה-Org.
הבחירה תלויה במי שאמור לפעול על הכשל: אם זה בעל תהליך עסקי, הוא צריך לראות את זה בתוך Salesforce; אם זה צוות אינטגרציה טכני, עדיף בשכבה החיצונית.
Reconciliation עסקי: הבדיקה שחושפת את מה ש-Retry לא תפס
גם עם Idempotency ו-Retry מושלמים, יש כשלים ש"מצליחים" מבחינה טכנית אך יוצרים פער עסקי - למשל הודעה שהתקבלה ועובדה, אבל עם ערך שגוי שהתקבל ממקור נתונים לא מעודכן. Reconciliation הוא תהליך תקופתי (יומי, שעתי, לפי קצב האירועים) שמשווה ספירה או סכום מצטבר בין שתי המערכות - למשל מספר הזמנות שנוצרו ב-ERP מול מספר הזמנות שנוצרו ב-Salesforce לאותו יום - ומדגיש פערים לפני שהם הופכים לתקלת שירות ללקוח.
תהליך Reconciliation טוב אינו דורש בדיקת שדה-שדה של כל רשומה; מספיק Checksum או ספירה מצטברת שמאותתת מתי צריך לרדת לפירוט. ברוב הארגונים תדירות יומית מספיקה; בתהליכים כספיים או קריטיים (הזמנות, חיובים) נדרשת בדיקה תוך שעות ספורות.
Decision Framework: מתי כל שכבה חובה ומתי אפשר לוותר
| קריטריון | Idempotency חובה | Retry אוטומטי חובה | Dead Letter נפרד חובה | Reconciliation יומי חובה |
|---|---|---|---|---|
| האירוע יוצר תנועה כספית או מלאי | כן | כן | כן | כן |
| האירוע חד-כיווני, קריאה בלבד (Read) | לא קריטי | כן | לא | לא |
| נפח מעל 500 הודעות ביום | כן | כן | כן | מומלץ |
| שותף חיצוני ללא SLA זמינות גבוה | כן | כן, עם Backoff ארוך | כן | מומלץ |
| אינטגרציה בין שני אובייקטים לא-כספיים בנפח נמוך | מומלץ | מומלץ | לא הכרחי | לא |
הכלל שמנחה את הטבלה: ככל שלכשל יש השלכה כספית או בלתי הפיכה (משלוח, חיוב, עדכון מלאי), כל ארבע השכבות עוברות מ"רצוי" ל"חובה" - ללא קשר לנפח.
תרחיש לדוגמה: קמעונאית עם סנכרון הזמנות דו-כיווני
חברת קמעונאות עם 40 סניפים מפעילה Salesforce לניהול הזמנות B2B ו-ERP חיצוני למלאי וחשבוניות. האינטגרציה נבנתה במקור עם קריאת REST פשוטה: כשהזמנה נוצרת ב-Salesforce, קריאה סינכרונית יוצרת אותה ב-ERP. ללא Retry, ללא Dead Letter.
בתקופת עומס (Black Friday), ה-ERP החל להחזיר Timeout בכ-3% מהקריאות. בלי מנגנון Retry, ה-3% האלה פשוט "נעלמו" - ההזמנה נשארה ב-Salesforce בסטטוס "נשלח" בלי שה-ERP ידע עליה. תוך יומיים הצטברו כ-140 הזמנות שלא הגיעו לתהליך אריזה, והתגלו רק כשלקוחות התקשרו לברר איפה הסחורה.
הפתרון שנבנה בעקבות זה: שכבת Queueable ב-Apex שמנסה שוב עד 5 פעמים עם Backoff של 1/5/15/30/60 דקות; שדה ERP_Sync_Status__c עם ערכי Pending/Synced/Failed; Custom Object Integration_Failed_Message__c שמרכז כשלים סופיים עם כפתור "טפל מחדש" לצוות התפעול; ודוח Reconciliation יומי שמשווה ספירת הזמנות בין המערכות ושולח Slack Alert כשהפער עולה על אפס. זמן הגילוי של תקלה דומה ירד מיומיים לפחות משעה.
סיכונים נפוצים ופעולות מניעה
| סיכון | איך הוא נראה בפועל | פעולת מניעה |
|---|---|---|
| Retry אינסופי על שגיאה מבנית | אותה הודעה נכשלת שוב ושוב ומייצרת עומס | לסווג שגיאות מראש ולשלוח שגיאות מבניות ישר ל-Dead Letter |
| היעדר מפתח ייחודי לאירוע | Retry או קריאה כפולה יוצרים רשומה כפולה | External ID על האירוע, לא רק על הרשומה הסופית |
| Dead Letter בלי Owner | הודעות מצטברות ואף אחד לא סוגר אותן | לקבוע Owner ו-SLA טיפול לפי סוג אירוע, לא לפי מערכת |
| ניטור טכני בלבד (סטטוס API) | האינטגרציה "ירוקה" אך המידע העסקי לא תואם | להוסיף Reconciliation שמשווה תוצאה עסקית, לא רק קוד תגובה |
| Backoff קבוע וקצר מדי | ניסיונות חוזרים מחמירים עומס בזמן תקלה רחבה | Exponential Backoff עם תקרת ניסיונות מוגדרת |
Checklist לפני אישור עיצוב טיפול בשגיאות
- ☐ לכל אירוע יש מפתח ייחודי (External ID) שמונע כפילות בהרצה חוזרת
- ☐ שגיאות מסווגות מראש לזמניות/מבניות/הרשאה, עם טיפול שונה לכל סוג
- ☐ מדיניות Backoff מוגדרת עם מספר ניסיונות מקסימלי
- ☐ קיים Dead Letter נגיש עם Payload מלא וסיבת כשל
- ☐ יש Owner ו-SLA טיפול מוגדרים לכל סוג כשל
- ☐ קיים תהליך Reconciliation תקופתי שמשווה תוצאה עסקית בין המערכות
- ☐ התראות מגיעות לערוץ שבו מישהו קורא בפועל (לא רק ללוג)
- ☐ תרחיש בדיקה כולל הפסקת שירות של המערכת השנייה, לא רק Happy Path
איך זה מתחבר לשאר הארכיטקטורה
עיצוב טיפול בשגיאות לא עומד בפני עצמו - הוא נשען על שכבת הנתונים וההרשאות שמוגדרת במדריך ארכיטקטורת CRM, ועל ההחלטה אם הלוגיקה ממומשת ב-Flow או ב-Apex לפי Salesforce Flow או Apex. מודל ההרשאות שדרכו רכיבי האינטגרציה כותבים לנתונים חייב להיבדק מול מודל ההרשאות של Salesforce, כדי שמשתמש טכני של האינטגרציה לא יקבל גישה רחבה מדי. וכשנפח ההודעות גדל, שאלת ה-Retry נפגשת ישירות עם מגבלות ה-API שמפורטות בSalesforce API Limits.
סיכום
טיפול בשגיאות אינטגרציה אינו פיצ'ר שמוסיפים בסוף - הוא ההבדל בין מערכת שמתגלה כשבורה אחרי שלקוח התלונן, לבין מערכת שמתריעה על עצמה לפני שהנזק מצטבר. ארבע השכבות - Idempotency, Retry מסווג, Dead Letter עם Owner, ו-Reconciliation עסקי - לא דורשות פרויקט נפרד, אבל דורשות החלטה מפורשת בשלב התכנון, לפני שהאינטגרציה הראשונה עולה לייצור. ארגון שמזהיר את עצמו על 3% הודעות שנכשלות תוך שעה שונה מהותית מארגון שמגלה זאת מלקוח כועס.
