ניסיון חוזר של פונקציות אסינכרוניות

במסמך הזה מוסבר איך אפשר לבקש מפונקציות ברקע אסינכרוניות (לא HTTP) לבצע ניסיון חוזר במקרה של כשל.

למה פונקציות מבוססות-אירועים לא מצליחות להסתיים

במקרים נדירים, יכול להיות שפונקציה תצא מוקדם מדי בגלל שגיאה פנימית, ובאופן כללי יכול להיות שהמערכת תנסה להפעיל אותה מחדש באופן אוטומטי או שלא.

בדרך כלל, פונקציה מבוססת-אירועים עלולה להיכשל בהשלמה מוצלחת בגלל שגיאות שמוחזרות בקוד הפונקציה עצמו. הסיבות לכך יכולות להיות:

  • הפונקציה מכילה באג וזמן הריצה יוצר חריגה.
  • הפונקציה לא מצליחה להגיע לנקודת קצה של שירות, או שהזמן הקצוב לתפוגה שלה מסתיים במהלך הניסיון לעשות זאת.
  • הפונקציה יוצרת בכוונה חריגה (לדוגמה, כשפרמטר לא עובר אימות).
  • פונקציית Node.js מחזירה הבטחה שנדחתה, או מעבירה ערך שאינו null לקריאה חוזרת.

בכל אחד מהמקרים שלמעלה, הפונקציה תפסיק לפעול ותחזיר שגיאה. לגורמים שמפעילים אירועים שיוצרים את ההודעות יש מדיניות ניסיון חוזר שאפשר להתאים אישית כדי לענות על הצרכים של הפונקציה.

סמנטיקה של ניסיון חוזר

Cloud Functions מספקת הפעלה של פונקציה מבוססת-אירועים לפחות פעם אחת לכל אירוע שמופק ממקור אירועים. כברירת מחדל, אם הפעלת פונקציה מסתיימת בשגיאה, הפונקציה לא מופעלת שוב והאירוע מושמט. כשמפעילים ניסיונות חוזרים בפונקציה מבוססת-אירועים, מערכת Cloud Functions מנסה להפעיל שוב פונקציה שנכשלה עד שהיא מסתיימת בהצלחה או עד שחלון הניסיונות החוזרים מסתיים.

כשניסיונות חוזרים לא מופעלים לפונקציה (זו ברירת המחדל), הפונקציה תמיד מדווחת שהיא בוצעה בהצלחה, ויכול להיות שיופיעו קודי תגובה 200 OK ביומנים שלה. זה קורה גם אם הפונקציה נתקלה בשגיאה. כדי שיהיה ברור מתי הפונקציה נתקלת בשגיאה, חשוב לדווח על שגיאות בצורה מתאימה.

הגדרת ניסיונות חוזרים מקוד הפונקציה

באמצעות Cloud Functions for Firebase, אפשר להפעיל ניסיונות חוזרים בקוד של פונקציה. כדי לעשות את זה לאירוע ברקע, כמו יצירה של מסמך חדש ב-Firestore, מגדירים את האפשרות failurePolicy (דור ראשון) או retry (דור שני) של המדיניות ל-true:

דור ראשון

exports.docCreated = functions
  .runWith({
    // retry on failure
    failurePolicy: true,
  })
  .firestore.document("my-collection/{docId}")
  .onCreate((change, context) => {
    /* ... */
  });

דור שני

const { onDocumentCreated } = require("firebase-functions/firestore");

exports.docCreated = onDocumentCreated(
  {
    // retry on failure
    retry: true,
  },
  "my-collection/{docId}",
  (event) => {
    /* ... */
  },
);

ההגדרה true כפי שמוצג מגדירה פונקציה לניסיון חוזר במקרה של כשל.

חלון ניסיון חוזר

בפונקציות מהדור השני, חלון הניסיונות החוזרים הזה יפוג אחרי 24 שעות. בפונקציות מהדור הראשון, הוא יפוג אחרי 7 ימים. ‫Cloud Functions מבצע ניסיונות חוזרים בפונקציות חדשות מבוססות-אירועים באמצעות אסטרטגיית השהיה מעריכית לפני ניסיון חוזר (exponential backoff), עם השהיה הולכת וגדלה של בין 10 ל-600 שניות. המדיניות הזו חלה על פונקציות חדשות בפעם הראשונה שמבצעים בהן פריסה. היא לא חלה רטרואקטיבית על פונקציות קיימות שבוצעה בהן פריסה ראשונית לפני שהשינויים שמתוארים בהערת הגרסה הזו נכנסו לתוקף, גם אם מבצעים פריסה מחדש של הפונקציות.

שיטות מומלצות

בקטע הזה מתוארות שיטות מומלצות לשימוש בניסיונות חוזרים.

שימוש בניסיון חוזר לתיקון שגיאות חולפות

הפונקציה מופעלת מחדש באופן רציף עד שהיא מופעלת בהצלחה, ולכן צריך לבצע בדיקות כדי להסיר מהקוד שגיאות קבועות כמו באגים לפני שמפעילים את הניסיונות החוזרים. מומלץ להשתמש בניסיונות חוזרים כדי לטפל בכשלים לסירוגין או זמניים, שיש סיכוי גבוה שייפתרו אם יופעלו מחדש, כמו נקודת קצה של שירות לא יציב או פסק זמן.

הגדרת תנאי סיום כדי למנוע לולאות אינסופיות של ניסיונות חוזרים

מומלץ להגן על הפונקציה מפני לולאות חוזרות בלתי פוסקות כשמשתמשים בניסיונות חוזרים. כדי לעשות את זה, צריך לכלול תנאי סיום מוגדר היטב, לפני שהפונקציה מתחילה לעבד. שימו לב שהטכניקה הזו פועלת רק אם הפונקציה מתחילה לפעול בהצלחה ויכולה להעריך את תנאי הסיום.

גישה פשוטה ויעילה היא להשליך אירועים עם חותמות זמן ישנות יותר מזמן מסוים. כך אפשר למנוע הפעלות מוגזמות כשיש כשלים מתמשכים או כשהם נמשכים יותר זמן מהצפוי.

לדוגמה, קטע הקוד הזה מבטל את כל האירועים שהתרחשו לפני יותר מ-10 שניות:

const eventAgeMs = Date.now() - Date.parse(event.timestamp);
const eventMaxAgeMs = 10000;
if (eventAgeMs > eventMaxAgeMs) {
  console.log(`Dropping event ${event} with age[ms]: ${eventAgeMs}`);
  callback();
  return;
}

שימוש ב-catch עם Promises

אם הפעלתם ניסיונות חוזרים בפונקציה, כל שגיאה שלא טופלה תפעיל ניסיון חוזר. מוודאים שהקוד מתעד שגיאות שלא אמורות להוביל לניסיון חוזר.

דוגמה להטמעה תקינה:

return doFooAsync().catch((err) => {
    if (isFatal(err)) {
        console.error(`Fatal error ${err}`);
    }
    return Promise.reject(err);
});

איך יוצרים פונקציות אידמפוטנטיות מבוססות-אירועים שאפשר לנסות להפעיל מחדש

פונקציות מבוססות-אירועים שאפשר לנסות להפעיל מחדש חייבות להיות אידמפוטנטיות. הנה כמה הנחיות כלליות להפיכת פונקציה כזו לאידמפוטנטית:

  • הרבה ממשקי API חיצוניים (כמו Stripe) מאפשרים לספק מפתח אידמפוטנטיות כפרמטר. אם אתם משתמשים ב-API כזה, עליכם להשתמש במזהה האירוע כמפתח האידמפוטנטיות.
  • אידמפוטנטיות פועלת היטב עם מסירה אחת לפחות, כי היא מאפשרת ניסיון חוזר בבטחה. לכן, שיטה מומלצת כללית לכתיבת קוד מהימן היא לשלב אידמפוטנטיות עם ניסיונות חוזרים.
  • חשוב לוודא שהקוד שלכם הוא אידמפוטנטי באופן פנימי. לדוגמה:
    • מוודאים שהמוטציות יכולות לקרות יותר מפעם אחת בלי לשנות את התוצאה.
    • שאילתת מצב מסד הנתונים בעסקה לפני שינוי המצב.
    • חשוב לוודא שכל תופעות הלוואי הן אידמפוטנטיות.
  • כדאי להטיל בדיקה של העסקה מחוץ לפונקציה, בלי קשר לקוד. לדוגמה, אפשר לשמור את המצב במקום כלשהו שבו מתועד שמזהה אירוע מסוים כבר עבר עיבוד.
  • טיפול בקריאות כפולות לפונקציות מחוץ לתהליך. לדוגמה, אפשר להגדיר תהליך ניקוי נפרד שינקה אחרי קריאות כפולות לפונקציות.

הגדרת מדיניות הניסיון החוזר

בהתאם לצרכים של הפונקציה, יכול להיות שתרצו להגדיר את מדיניות הניסיון החוזר ישירות. כך תוכלו להגדיר כל שילוב של האפשרויות הבאות:

  • לקצר את חלון הניסיון החוזר מ-7 ימים ל-10 דקות בלבד.
  • שינוי הזמן המינימלי והמקסימלי להשהיה באסטרטגיית הניסיון החוזר עם השהיה אקספוננציאלית.
  • משנים את אסטרטגיית הניסיון החוזר לניסיון חוזר מיידי.
  • מגדירים נושא להודעות ללא מוצא.
  • הגדרת מספר מקסימלי ומינימלי של ניסיונות מסירה.

כדי להגדיר את מדיניות הניסיון החוזר:

  1. כתיבת פונקציית HTTP.
  2. משתמשים ב-Pub/Sub API כדי ליצור מינוי Pub/Sub, ומציינים את כתובת ה-URL של הפונקציה כיעד.

מידע נוסף על הגדרה ישירה של Pub/Sub זמין בPub/Subמאמר בנושא טיפול בכשלים.