אזהרה שחוזרת בכל cold start
ארבע issues חדשות ב-Sentry, כולן עם אותה כותרת: Unknown city in resolve_neighborhood. שלוש מהן היו מושבים קטנים — עזריקם, חגור, בית זית — והרביעית הייתה 'Afula', באנגלית, עם השכונה 'רובע יזרעאל' בעברית. ההפרדה הזו בין השלוש לאחת היא כל הסיפור.
הבאג שצדק
Afula הוא באג אמיתי. בקוד יש מילון _CITY_OVERRIDES שממפה איותים לטיניים לעברית — אבל הוא מכיל רק את משפחת Tel Aviv. סקרייפר אחד החליט להחזיר את שם העיר באנגלית, וזה נפל בין הכיסאות: לא תואם לאף canonical, לא מתורגם, נכנס ל-Sentry ויוצר potentially שורת neighborhoods כפולה לעפולה. שורה אחת:
_CITY_OVERRIDES = {
"תל אביב": "תל אביב יפו",
"Tel Aviv": "תל אביב יפו",
"Tel Aviv-Yafo": "תל אביב יפו",
"Afula": "עפולה", # ←
}
זה הקל. המעניין היה השלושה האחרים.
הרעש שלא הבנתי
עזריקם, חגור ובית זית הם מושבים קטנים. הקוד מתעד אותם בכוונה — זה האות שצריך להוסיף שכונה קנונית חדשה. הבעיה: מצופה שהאזהרה תיירה פעם אחת לעיר. יש בקוד _warned_cities set שמונע חזרות. אבל ב-Sentry ראיתי את אותה עיר נדלקת שוב ושוב.
לקח לי רגע להבין: ה-set הזה הוא משתנה גלובלי ב-Python process. ב-Lambda זה אומר שכל cold start מאתחל אותו מחדש. סקרייפר רץ פעם ביום, Lambda הצטיא, container חדש, set ריק, אזהרה חוזרת. ה-dedup עובד בדיוק כמו שתוכנן — רק שהתכנון הזה הניח process ארוך-חיים.
התיקון
הפיתוי הראשון היה לשמור seen cities ב-DB. יותר מדי תשתית לבעיה הזו. הפיתוי השני היה להוריד את ה-log level. זה היה משתיק את הרעש אבל גם את האות.
הפתרון בפועל היה לשאול שאלה אחרת: למה אנחנו בכלל מאזהירים על עיר שכבר יש לה שורה ב-Neighborhood? אם יש canonical, היא לא חדשה — לא משנה אם יש לה alias כלשהו או לא. עד עכשיו _known_cities נטענה מ-neighborhood_aliases בלבד. שורה נוספת ב-_load_cache:
city_result = await session.execute(
select(Neighborhood.city).distinct()
)
_known_cities.update(c for (c,) in city_result.all() if c)
עיר עם canonical אחד שותקת. עיר שלא נוצרה לה עוד שורה — נשמעת. אות הדריפט נשמר, הרעש נעלם.
הלקח
dedup in-memory שמתבסס על משתנה גלובלי הוא הנחה על אורך החיים של ה-process. ב-Lambda זו הנחה שגויה. כשהאזהרה חוזרת לסירוגין על אותו ערך, זה לא תמיד באג ב-dedup — זה לפעמים סימן ש-State שאתה חושב עליו כ-long-lived הוא בעצם ephemeral. הפתרון הוא לעיתים קרובות לא להוסיף persistence, אלא לגזור את ה-state ממקור שכבר persistent — במקרה הזה, ה-DB עצמו.