במאמר הזה מוסבר איך לבקש שפונקציות אסינכרוניות (לא HTTPS) שפועלות ברקע ינסו שוב במקרה של כשל.
למה פונקציות מבוססות-אירועים לא מסתיימות
במקרים נדירים, פונקציה עשויה לצאת לפני הזמן בגלל שגיאה פנימית, וכברירת מחדל, יכול להיות שהפונקציה תנסה שוב באופן אוטומטי או לא.
בדרך כלל, פונקציה מבוססת-אירועים נכשלת בגלל שגיאות בקוד של הפונקציה עצמה. הסיבות האפשריות לכך:
- בפונקציה יש באג וסביבת זמן הריצה זורקת חריגה.
- הפונקציה לא מצליחה להגיע לנקודת קצה (endpoint) של שירות, או שהזמן הקצוב לתפוגה מסתיים בזמן שהיא מנסה לעשות זאת.
- הפונקציה זורקת חריגה בכוונה (לדוגמה, כשפרמטר נכשל באימות).
- פונקציית Node.js מחזירה הבטחה שנדחתה, או מעבירה ערך שאינו
nullלקריאה חוזרת (callback).
בכל אחד מהמקרים שלמעלה, הפונקציה תפסיק לפעול ותחזיר שגיאה. לטריגרים של אירועים שמפיקים את ההודעות יש מדיניות ניסיונות חוזרים שאפשר להתאים אישית לפי הצרכים של הפונקציה.
סמנטיקה של ניסיון חוזר
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) => {
/* ... */
});
דור 2
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 דקות בלבד.
- משנים את זמן ההשהיה המינימלי והמקסימלי לפני ניסיון חוזר (exponential backoff).
- משנים את אסטרטגיית הניסיון החוזר לניסיון חוזר מיידי.
- מגדירים נושא להודעות ללא מוצא.
- מגדירים מספר מינימלי ומקסימלי של ניסיונות מסירה.
כדי להגדיר את מדיניות הניסיונות החוזרים:
- כותבים פונקציית HTTP.
- משתמשים ב-API של Pub/Sub כדי ליצור מינוי ל-Pub/Sub, ומציינים את כתובת ה-URL של הפונקציה כיעד.
במסמכי התיעוד של Pub/Sub בנושא טיפול בכשלים מוסבר איך להגדיר את Pub/Sub ישירות.