לרוב תהליכי עבודה שאתם מקודדים הם לא קו ישר ופשוט של הרצת הוראות אחת אחרי השניה, פעמים רבות התהליכים הם אסינכרונים, מבצעים פעולה כלשהי ולא מקבלים תשובה מיידית, אלא צריך לדגום את התהליך, תהליכי עבודה רבים הם סדרה של צעדים אסניכרונים מתואמים שרק בהנתן תנאים מסויימים, תהליך העבודה מסתיים בהצלחה. אחד השירותים המפורסמים בתחום שמאפשר את סנכרון התהליכים האסינכרונים הוא AWS Step Functions אך יש איתו מספר בעיות:
זו שפה חדשה שצריך ללמוד, זה לא קוד מוכר ואהוב.
לבדוק את תהליך העבודה, לכתוב לו טסטים ולדבג אותו מקומית במקרה של שגיאה הוא לא פשוט.
AWS שחררו לפני מספר ימים יכולת חדשה שהם אפו ישירות לתוך הלמדה, מעין מנגנון של Step Functions, עם מירב היתרונות של Step Functions, אך ללא החסרונות. בפוסט הנוכחי נעבור על הפתרון. שוב, זו התרשמות ראשונית, במהלך החודשים הקרובים אלמד יותר.
תרחיש הייחוס
נניח שאתם מפתחים תהליך פיננסי שבו עסק מבצע העברת כספים, תהליך העברת הכספים דורש מספר תתי תהליכים שצריכים להתבצע בהצלחה על מנת שהעברת הכספים תסתיים:
לבדוק מי מעביר את הכסף, האם הוא משתמש ולידי, לדוגמא שהוא לא טרורסט.
לבדוק מי מקבל את את הכסף, לדוגמא אני מעוניין לבדוק שהכסף לא מועבר למטרת הלבנת כספים.
לבצע את ההעברה.
לוודא שהעברה אכן בוצעה והכסף נמצא בצד השני.
ישנה מגבלה נוספת והיא שישנם תתי תהליכים שלא יכולים להתבצע פעמיים, לדוגמא ביצוע ההעברה חייב להתרחש פעם אחת בלבד.
יש כאן תהליך עבודה די מורכב ואנו נכתוב אותו באמצעות Durable Functions.
מודל עבודה
במודל הנוכחי של למדה רגילה, פונקציית למדה מריצה את הקוד שנכתב בתוך ה-handler, הקוד חייב להסתיים לכל היותר תוך 15 דקות ריצה. מודל העבודה הוא סינכרוני במהותו, כשהלמדה מסיימת לרוץ, ריצה נוספת שלה, תריץ את הקוד מחדש, מהתחלה, עם כל המשתנים מאותחלים לערך ההתחלתי שלהם.
Durable Function (פונקציה מתמשכת ? לא מצאתי תרגום טוב יותר) היא שונה, היא עדין למדה והיא עדין מוגבלת באותן 15 דקות, אך היא יכולה לעצור את הריצה באמצע, לבצע פעולה אסינכרונית כלשהן, וברגע שהתנאים מתאימים לחזור לפעולה שוב מאותה נקודה שהיא נעצרה עם התוצר של התהליך האסינכרוני.
נתחיל בדוגמא פשוטה:
async function handler(ctx) { let details = await transferMoneyOnce() await ctx.wait("Wait 1 hour before follow-up", 60 * 60); return { status: details.status };}ההגדרה כאן פשוטה, חכה שעה ואז תמשיך לרוץ. ברגע שה - Durable Function מגיעה להגדרה כפי שרשומה בשורה 3 אז הלמדה מפסיקה לרוץ, לא משלמים עליה כסף, בתום השעה המערכת מאחורי הקלעים של Dureable function קמה לתחיה וממשיכה לרוץ מהיכן שהיא הפסיקה, או שלא… 😈
צעד
האמת היא שהלמדה ברגע שהיא קמה לתחייה מריצה את הכל מהתחלה, המשמעות היא שבתום השעה, שורה מספר 2 תרוץ שוב, לא טוב. אז מה עושים? אנו צריכים להגיד למערכת בצורה כלשהי שישנם שלבים שאין צורך להריצם שוב, כי הם רצו. נרחיב את הדוגמא
async function handler(ctx) { let details = await ctx.step("Transfer money", () => transferMoneyOnce()) await ctx.wait("Wait 1 hour before follow-up", 60 * 60); return { status: details.status };}הגדרנו את הפעולה שלנו כצעד (step), פעולה שמוגדרת כצעד תתבצע פעם אחת והתוצאה שלה תשמר במצב פנימי של ה-Dureable Lambda (שלעניות דעתי אין לנו גישה אליו). המשמעות היא שכשהלמדה תתעורר שוב לאחר כשעה שורה מספר 2 תרוץ שוב, אך ה-Durable function תשלוף את הערך שחישבנו קודם ותחזיר אותו מידית, בלי לבצע את הפעולה מאחורי הקלעים (העברת כספים).
לחכות שעה זה צעד שלרוב לא יעיל, אחד הדרכים לבדוק סיום עבודה של תהליכים אסינכרוניים הוא לדגום אותם כל פרק זמן מסוים.
waitFor
נרחיב את הדוגמא שלנו
async function handler(ctx) { let details = await ctx.step("Transfer money", async () => await transferMoneyOnce()) await ctx.waitForCondition("Check for successful money transfer", async () => await checkForTransfer(), {delay:60}) return { status: details.status };}המערכת, תעיר את הלמדה על 60 שניות ותבצע בדיקה שאנו הגדרנו, ברגע שהבדיקה הוגשרה כהצלחה, המערכת תמשיך הלאה. זכרו בכל פעם שהמערכת מתעוררת, היא מריצה את כל הלמדה מהתחלה ולכן כל פעולה שאמורה להתבצע פעם אחת צריכה להיות בתוך צעד.
נחבר יחד
לאחר שיש ברשותנו את הכלים המתאימים, ננסה לממש את תרחיש הייחוס.
async function handler(event, ctx) { let isValidSender = await ctx.step("Check that the sender is valid", async () => await checkSender(event.user)) let isReceiverValid = await ctx.step("Check that the receiver is valid", async () => await checkReceiver(event.receiver)) if (isValidSender && isReceiverValid) { let details = await ctx.step("Transfer money", async () => await transferMoneyOnce()) let status = await ctx.waitForCondition("Check for successful money transfer", async () => await checkForTransfer(details.id), {delay:60}) return status; } else { return "FAIL" }}שורות 2 3 ו-5 הן אטומיות ורצות פעם אחת בלבד, מרגע שהפעולות הללו הסתיימו, תמיד אותה תוצאה תחזור כשהלמדה תרוץ שוב בכל פעם שהיא תתעורר עקב ההגדרה בשורה 7.
נשאלת השאלה האם כל הגדרה צריכה להיות בתוך צעד? ע״פ הדוקומנטציה וע״פ דעתי ישנם שתי פעולות שתרצו לשים בתוך צעד:
פעולה שהיא לא דטרמיניסטיות, אם בכל ריצה היא עלולה להחזיר תושבה אחרת, אז עטפו אותה בצעד. לדוגמא חישוב תאריך או מספר רנדומלי.
פעולה ארוכה שיכולה לקחת זמן רב ואתם מעוניינים לשמר את התוצאה שלה ולחסוך זמן בריצה הבאה.
לאחר שעברנו על מודל העבודה ועל הדרכים השונות לשלוט במכונת המצבים הזאת, כיצד הדבר בא לידי ביטוי בקוד אמיתי שרץ בענן.
קוד
על מנת להריץ Dureable Function נדרשים שני רכיבים, אחד להגדיר את הלמדה כ-Dureable function והשני הוא להשתמש ב-SDK, בהתאם לשפת הפיתוח שלכם כדי לתקשר עם מכונת המצבים הזאת. כרגע התמיכה הן בשפות NodeJS ו-Python, במידה ואתם משתמשים בשפה אחרת, אתרע מזלכם.
הגדרת למדה
בזמן יצירת הלמדה בקונסול, יש לבחור Dureable Function, שימו לב שכרגע רק us-east-2 (Ohio) מאפשר לייצר פונקציה שכזאת, אך להבנתי ההיצע יורחב בשבוע הקרוב לאזורים המרכזיים של AWS.

SDK
לכל אחת משפות הפיתוח הנתמכות יש SDK, לדוגמא עבור NodeJS החבילה נקראית @aws/durable-execution-sdk-js ,ניתן למצוא קישורים ובאמצעות ה-SDK מגדירים את הצעדים השונים. אחת הסיבות שהלמדה מוגדרת להיות מסוג שונה היא כי בפעם הראשונה בהיסטוריה של הלמדה, יש שימוש אמתי לאובייקט ה-context בחתימה של ה-handler. באמצעות אובייקט ה-context אתם קוראים לפונקציונליות השונות של ה-dureable functions. למעשה יש מנוע שונה מאחורי הקלעים שמריץ את ה-handler שלכם מספק לכם את הפרמטרים והאובייקטים המתאימים לתקשר איתו.
נעבור זריז על דוגמא ל-NodeJS:
import { withDurableExecution } from '@aws/durable-execution-sdk-js'; export const handler = withDurableExecution(async (event, context) => { await context.step('Step #1', (stepCtx) => { stepCtx.logger.info('Hello from step #1'); }); await context.wait({ seconds:1 }); context.logger.info('Waited for 1 second'); const message = await context.step('Step #2', async () => { return 'Hello from Durable Lambda!'; }); const response = { statusCode: 200, body: JSON.stringify(message), }; return response;});שימו לב לעטיפה של ה-handler בשורה 3, מוזרק אובייקט context שמאפשר לכם לשלוט ב-Dureable functions.
בשורה 5 אנו מגדירים צעד ובשורה 9, אנו מפסיקים את ריצת הלמדה למשך שניה אחת.
סיכום
לסיכום, גירדתי את הפוטנציאל ובמידה מסוימת את המורכבות, מטרת הפוסט היתה בעיקר לעשות סדר (גם עבורי) באיך למדה שכזאת עובדת.
שאלות שהן עדין פתוחות עבורי:
מתי Dureable function ומתי Step Function.
כיצד בודקים את הקוד שנכתב. יש ספריה רשמית ש-AWS שחררו שאמורה לעזור בנושא.
כיצד מנטרים את הפונקציה.
כיצד מטפלים בשגיאות.
החודשים הקרובים יהיו מעניינים.
תגובות