מה קרה
הלוגים שנוספו ב-#3349 תפסו את זה בריצה הראשונה שלהם בפרודקשן:
pymongo.errors.OperationFailure: PlanExecutor error during aggregation :: caused by ::
$substrBytes: Invalid range, ending index is in the middle of a UTF-8 character.
code: 28657, codeName: Location28657
at webapp/app.py, line 9515, in _safe_search
השורש: שתי יחידות מידה שהתערבבו
מונגו מפרידה בין בייטים לתווים, והקוד מזין את האחד לתוך השני. נמדד מול הקלאסטר על "שלום world":
| מה הורץ |
התוצאה |
היחידה |
$regexFind ← idx |
5 |
תווים |
$indexOfCP |
5 |
תווים |
$indexOfBytes |
9 |
בייטים |
$strLenCP |
10 |
תווים |
$strLenBytes |
14 |
בייטים |
והקוד ב-webapp/app.py:9468‑9484:
'_match_idx': {'$ifNull': ['$_m.idx', 0]}, # ← אינדקס תווים
'_snippet_start': {'$max': [0, {'$subtract': ['$_match_idx', 50]}]},
'snippet_preview': {'$substrBytes': ['$code', '$_snippet_start', 200]}, # ← מצפה לבייטים
באנגלית שתי היחידות זהות ולכן זה עבר סקירה. בעברית כל אות היא שני בייטים, והחישוב פשוט שגוי.
שתי צורות הכשל, שתיהן שוחזרו מול MongoDB 8.0.32 עם אותו נוסח שגיאה בדיוק:
| מה הורץ |
מה מונגו החזירה |
$substrBytes("שלום world", 0, 3) |
ending index is in the middle of a UTF-8 character ← זו שבפרודקשן |
$substrBytes("שלום world", 1, 5) |
starting index is a UTF-8 continuation byte |
החיסרון בפועל — מה זה עולה
מוקד 1 · החיפוש · webapp/app.py:9484
הפייפליין המהיר מת, ו-_safe_search נופל למסלול ה-$regex על code — סריקה מלאה בלי אינדקס. אם גם הוא נופל, יורדים לפייפליין הישן, וזה מחזיר [].
שלושה מחירים, לפי סדר חומרה:
- חיפוש שבור נראה בדיוק כמו חיפוש שלא מצא. אם גם הפולבאק נופל, המשתמש מקבל "לא נמצאו תוצאות" על חיפוש שמעולם לא רץ. אין שום סימן שמשהו נשבר — לא בממשק, ולא בהתנהגות.
- עלות מיותרת. ה-
$regex שרץ במקום הוא סריקה מלאה. השאילתה הזו נמדדה כאיטית ביותר שנרשמה בלוח (3,730ms), והיא רצה רק מפני שהמסלול המהיר נפל.
- הסטטיסטיקה מזדהמת. הפרופיילר רושם את שאילתת הפולבאק כשאילתה איטית אמיתית, ומי שמסתכל בדשבורד מנתח שאילתה שלא הייתה אמורה לרוץ מלכתחילה. (זה מה ששלח את החקירה שלושה סבבים לכיוון הלא נכון.)
מוקד 2 · שיתוף קובץ · webapp/app.py:16166 — גרוע יותר
POST /api/share/<file_id> במצב preview:
'snippet_preview': {'$substrBytes': ['$code', 0, 2000]},
...
except Exception:
meta = {}
if not meta:
return jsonify({'ok': False, 'error': 'קובץ לא נמצא'}), 404
אותו באג, אבל כאן ה-except בולע אותו והתשובה למשתמש היא 404 "קובץ לא נמצא" — על קובץ שקיים. הודעת שגיאה שקרית, בלי שום עקבה בלוג. זה הדפוס של except שבולע, במקום השני באותו קובץ.
פגם שלישי מאותו שורש · ההדגשה בדפדפן
'_match_len': {'$strLenBytes': {'$ifNull': ['$_m.match', '']}}, # בייטים
הערך הזה נכנס ל-highlight_ranges, שמחושב במרחב תווים. global_search.js:315 (highlightSnippet) חותך את ה-snippet לפי המספרים האלה, ולכן התאמה עברית מודגשת בערך בכפול מאורך המילה. קוסמטי, אבל נראה לעין.
התיקון: להשתמש באופרטורים התו-מבוססים
בריפו יש שלושה מסלולים שמייצרים snippet, ושניים מהם כבר עובדים בתווים:
| מסלול |
איך הוא חותך |
היחידה |
search_engine.py:1141 |
content_value[start:end] |
תווים (חיתוך פייתון) |
webapp/app.py:9655 |
code_text[start:end], m.start() |
תווים |
webapp/app.py:9484 — המסלול המהיר |
$substrBytes |
בייטים ← החריג |
אותו דבר בשיתוף: webapp/app.py:16211 ו-webapp/app.py:3566 עושים code[:2000] — תווים. כלומר הכוונה בקוד היא תווים בכל מקום, ורק שני שלבי אגרגציה סוטים ממנה. המעבר ל-$substrCP לא מוסיף התנהגות חדשה, הוא מיישר את מסלול ה-DB לכוונה שכבר כתובה בשאר הקוד. $substrCP ו-$strLenCP אינם בשימוש היום באף מקום בריפו.
נמדד ש-$substrCP מטפל נקי בכל מקרי הקצה שהפילו את $substrBytes:
| מה הורץ |
התוצאה |
$substrCP("שלום world", 0, 3) |
"שלו" |
$substrCP("שלום world", 5, 200) — מעבר לסוף |
"world" |
$substrCP("שלום", 99, 200) — התחלה מעבר לסוף |
"" |
בלי חריגה, בשלושתם.
מה לא משתנה: file_size נשאר $strLenBytes בכל המקומות. גודל קובץ הוא באמת בייטים, וההערה בקוד אומרת זאת מפורשות (size-bytes ולא אורך תוכן). זה השימוש הנכון היחיד ב-$strLenBytes כאן.
וגם: ה-except בשיתוף צריך logger.warning(..., exc_info=True), כדי שהכשל הבא לא יתחפש ל-404. אותו טיפול שניתן ל-_safe_search ב-#3349.
מה עדיין לא ידוע
תדירות הכשל אינה ידועה. ספרתי שב-958 מתוך 1,043 הקבצים הפעילים יש תווים רב-בייטיים ב-code — אבל זה לא אומר שאותו שיעור מהחיפושים נופל. הקריסה תלויה במקום שאליו נוחת גבול החיתוך, לא רק בנוכחות עברית, ובפועל חיפושים רגילים עוברים תקין.
מה שכן שוחזר בעקביות: משפט ארוך בתיבת החיפוש מפיל. הסבר סביר — משפט ארוך אינו מתאים לכלום ב-$regexFind, ולכן _snippet_start נשאר 0 והחיתוך נופל על גבול 200 הבייטים; אבל לא אימתתי את ההסבר הזה, ולכן הוא השערה ולא ממצא.
לפני מימוש כדאי למדוד: על אילו מסמכים ובאילו שאילתות הגבול באמת נוחת באמצע תו.
היקף מוצע
webapp/app.py:9484 — $substrBytes ← $substrCP
webapp/app.py:9470 — _match_len ← $strLenCP, כדי ש-highlight_ranges יהיה במרחב יחידות אחד
webapp/app.py:9468 — _has_code_match ← $strLenCP (שקול לוגית, אבל אורך בבייטים ליד אורך בתווים באותן שלוש שורות הוא בדיוק המלכודת שיצרה את הבאג)
webapp/app.py:16166 — $substrBytes ← $substrCP, וה-except מקבל לוג
- טסטים שקודם נופלים על הקוד הישן, ואימות מול הקלאסטר על מסמכים עבריים אמיתיים
מה קרה
הלוגים שנוספו ב-#3349 תפסו את זה בריצה הראשונה שלהם בפרודקשן:
השורש: שתי יחידות מידה שהתערבבו
מונגו מפרידה בין בייטים לתווים, והקוד מזין את האחד לתוך השני. נמדד מול הקלאסטר על
"שלום world":$regexFind ← idx5$indexOfCP5$indexOfBytes9$strLenCP10$strLenBytes14והקוד ב-
webapp/app.py:9468‑9484:באנגלית שתי היחידות זהות ולכן זה עבר סקירה. בעברית כל אות היא שני בייטים, והחישוב פשוט שגוי.
שתי צורות הכשל, שתיהן שוחזרו מול MongoDB 8.0.32 עם אותו נוסח שגיאה בדיוק:
$substrBytes("שלום world", 0, 3)ending index is in the middle of a UTF-8 character← זו שבפרודקשן$substrBytes("שלום world", 1, 5)starting index is a UTF-8 continuation byteהחיסרון בפועל — מה זה עולה
מוקד 1 · החיפוש ·
webapp/app.py:9484הפייפליין המהיר מת, ו-
_safe_searchנופל למסלול ה-$regexעלcode— סריקה מלאה בלי אינדקס. אם גם הוא נופל, יורדים לפייפליין הישן, וזה מחזיר[].שלושה מחירים, לפי סדר חומרה:
$regexשרץ במקום הוא סריקה מלאה. השאילתה הזו נמדדה כאיטית ביותר שנרשמה בלוח (3,730ms), והיא רצה רק מפני שהמסלול המהיר נפל.מוקד 2 · שיתוף קובץ ·
webapp/app.py:16166— גרוע יותרPOST /api/share/<file_id>במצבpreview:אותו באג, אבל כאן ה-
exceptבולע אותו והתשובה למשתמש היא 404 "קובץ לא נמצא" — על קובץ שקיים. הודעת שגיאה שקרית, בלי שום עקבה בלוג. זה הדפוס של except שבולע, במקום השני באותו קובץ.פגם שלישי מאותו שורש · ההדגשה בדפדפן
הערך הזה נכנס ל-
highlight_ranges, שמחושב במרחב תווים.global_search.js:315(highlightSnippet) חותך את ה-snippet לפי המספרים האלה, ולכן התאמה עברית מודגשת בערך בכפול מאורך המילה. קוסמטי, אבל נראה לעין.התיקון: להשתמש באופרטורים התו-מבוססים
בריפו יש שלושה מסלולים שמייצרים snippet, ושניים מהם כבר עובדים בתווים:
search_engine.py:1141content_value[start:end]webapp/app.py:9655code_text[start:end],m.start()webapp/app.py:9484— המסלול המהיר$substrBytesאותו דבר בשיתוף:
webapp/app.py:16211ו-webapp/app.py:3566עושיםcode[:2000]— תווים. כלומר הכוונה בקוד היא תווים בכל מקום, ורק שני שלבי אגרגציה סוטים ממנה. המעבר ל-$substrCPלא מוסיף התנהגות חדשה, הוא מיישר את מסלול ה-DB לכוונה שכבר כתובה בשאר הקוד.$substrCPו-$strLenCPאינם בשימוש היום באף מקום בריפו.נמדד ש-
$substrCPמטפל נקי בכל מקרי הקצה שהפילו את$substrBytes:$substrCP("שלום world", 0, 3)"שלו"$substrCP("שלום world", 5, 200)— מעבר לסוף"world"$substrCP("שלום", 99, 200)— התחלה מעבר לסוף""בלי חריגה, בשלושתם.
מה לא משתנה:
file_sizeנשאר$strLenBytesבכל המקומות. גודל קובץ הוא באמת בייטים, וההערה בקוד אומרת זאת מפורשות (size-bytes ולא אורך תוכן). זה השימוש הנכון היחיד ב-$strLenBytesכאן.וגם: ה-
exceptבשיתוף צריךlogger.warning(..., exc_info=True), כדי שהכשל הבא לא יתחפש ל-404. אותו טיפול שניתן ל-_safe_searchב-#3349.מה עדיין לא ידוע
תדירות הכשל אינה ידועה. ספרתי שב-958 מתוך 1,043 הקבצים הפעילים יש תווים רב-בייטיים ב-
code— אבל זה לא אומר שאותו שיעור מהחיפושים נופל. הקריסה תלויה במקום שאליו נוחת גבול החיתוך, לא רק בנוכחות עברית, ובפועל חיפושים רגילים עוברים תקין.מה שכן שוחזר בעקביות: משפט ארוך בתיבת החיפוש מפיל. הסבר סביר — משפט ארוך אינו מתאים לכלום ב-
$regexFind, ולכן_snippet_startנשאר0והחיתוך נופל על גבול 200 הבייטים; אבל לא אימתתי את ההסבר הזה, ולכן הוא השערה ולא ממצא.לפני מימוש כדאי למדוד: על אילו מסמכים ובאילו שאילתות הגבול באמת נוחת באמצע תו.
היקף מוצע
webapp/app.py:9484—$substrBytes←$substrCPwebapp/app.py:9470—_match_len←$strLenCP, כדי ש-highlight_rangesיהיה במרחב יחידות אחדwebapp/app.py:9468—_has_code_match←$strLenCP(שקול לוגית, אבל אורך בבייטים ליד אורך בתווים באותן שלוש שורות הוא בדיוק המלכודת שיצרה את הבאג)webapp/app.py:16166—$substrBytes←$substrCP, וה-exceptמקבל לוג