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

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

זהות הפעולה היא החלטת מוצר

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

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

בדיקה ואז כתיבה אינן פעולה אטומית

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

CREATE TABLE request_keys (
  account_id uuid NOT NULL,
  operation text NOT NULL,
  request_key text NOT NULL,
  payload_hash text NOT NULL,
  response_json jsonb,
  PRIMARY KEY (account_id, operation, request_key)
);

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

מה מחזירים כשעדיין עובדים

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

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

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

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

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

לכמה זמן שומרים מפתח

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

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

תרחישי בדיקה שכדאי להריץ

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

תרגיל: תכננו endpoint להקצאת קרדיט. באילו גבולות אפשר להבטיח פעולה יחידה, ובאילו חייבים מנגנון בירור? הרחיבו עם מדריך התורים. לעיון נוסף: המדריך של AWS לתכנון retries בטוחים.