Skip to content

טסטים מחליפים את השעון של כל התהליך (monkeypatch על mod.time ו-freeze_time) — וזה דולף לתהליכוני רקע #3487

Description

@amirbiron

מה הבעיה

הרבה טסטים שולטים בזמן דרך הכינוי של מודול, למשל monkeypatch.setattr(limits.time, "monotonic", ...). זה נראה כאילו רק השעון של limits משתנה, אבל limits.time הוא המודול time הגלובלי (ב-mcp_server/limits.py יש import time). כלומר לאורך הטסט time.monotonic מוחלף לכל התהליך, בכל התהליכונים. freeze_time עושה אותו דבר מעצם הגדרתו: ב-freezegun 1.5.5, _freeze_time.start ב-freezegun/api.py מציב time.monotonic = fake_monotonic, וגם את time.time, time.perf_counter ועוד.

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

ראיות שזה נושך

  • push-sender עולה בכל תהליך שמייבא את webapp.app — ב-CI הוא נתקע עם _DB_INIT_LOCK ומפיל קובץ טסטים שלם #3486: push-sender חישב את הדדליין של pymongo בתוך freeze_time והחזיק את _DB_INIT_LOCK לתמיד, כלומר לדדליין שרחוק 57 שנה. התוצאה: 7 שגיאות ב-CI.
  • הרצה מקומית, על הקוד האמיתי: limits.time is time, sticky_notes_api.time is time ו-backend_mod._time is time מחזירים שלושתם True. התקנתי שעון שמקדים ב-600 שניות דרך limits.time.monotonic, והסרתי אותו בדיוק כמו ש-monkeypatch עושה. 20 שניות אחרי שהשעון כבר שוחזר, _DB_INIT_LOCK עדיין היה תפוס, ולדדליין של pymongo נותרו 585 שניות.
  • פגיעה קודמת באותה משפחה כבר מתועדת בריפו: tests/test_http_sync_new.py, שורות 35–38: "this patches the *global* time module, so any extra perf_counter() call (an error path, metrics, logging) drains the list and raises IndexError". הצורה השבירה שההערה מזהירה ממנה עדיין נמצאת ב-tests/test_http_sync_logging.py:30, עם lambda: counters.pop(0).

מתי ערך מזויף מסוכן

השעון המונוטוני ב-Linux סופר מאז שהמכונה עלתה. כאן מדדתי time.monotonic() = 301.5, ובאותו רגע /proc/uptime = 301.53.

  • ערך מזויף שמקדים את השעון האמיתי, כמו 1000.0 על מכונה שעלתה לפני פחות מ-1000 שניות, יוצר דדליין שנמשך ההפרש ביניהם בשניות.
  • freezegun תמיד מקדים, כי הוא מחזיר שניות מאז 1970.
  • time.sleep שהוחלף בפונקציה ריקה גורם לכל לולאת רקע שקוראת לו בתוך החלון לרוץ בלי הפסקה.

לא בדקתי כמה זמן מכונות ה-CI דולקות כשהטסטים רצים, ולכן אני לא יודע אם 1000.0 ו-500.0 שבטסטים הקדימו שם את השעון האמיתי.

הרשימה (מצב נכון ל-29.09.2026)

מה מוחלף איפה
monotonic (4) tests/test_mcp_limits.py:612, tests/test_mcp_notes_handlers.py:619, tests/test_note_boards_api.py:1107, tests/test_distributed_lock_heartbeat_and_wait.py:70
sleep (2) tests/test_distributed_lock_heartbeat_and_wait.py:71, tests/test_google_drive_retries.py:180
perf_counter (3) tests/test_http_sync_logging.py:30, tests/test_http_sync_new.py:42, tests/test_observability_api.py:283
time (20, ב-7 קבצים) tests/test_cache_manager_fallback_local.py:46, tests/test_github_rate_limit_resources.py:13,45, tests/test_github_rate_limit_more_resources.py:130, tests/test_main_maintenance_gate.py:130,172, tests/test_observability_log_aggregator_processor.py:69,80,82,84, tests/test_single_error_alerts.py:51,70,74,78,89,110,121, tests/test_webserver_observability_extras.py:231,269,318
freeze_time (2) tests/test_israel_timezone_display.py:80,97

בדקתי שכל מודול שהשעון שלו מוחלף טוען את time הגלובלי:

  • import time: cache_manager.py, github_menu_handler.py, http_sync.py, main.py, monitoring/log_analyzer.py, observability.py, services/webserver.py, services/google_drive_service.py, mcp_server/limits.py, webapp/sticky_notes_api.py.
  • import time as _time: mcp_server/backend.py, webapp/app.py.

הרשימה היא צילום מצב. כדי לרענן אותה:

grep -rnE "setattr\(\s*[A-Za-z_][A-Za-z_0-9.]*\b_?time\s*,\s*['\"](monotonic|time|sleep|perf_counter|monotonic_ns|time_ns)['\"]|freeze_time" tests/

כיוון לתיקון (לא מומש, להכרעה)

  • תפר שעון לכל מודול. המודול קורא לשעון דרך הפניה משלו, למשל _monotonic = time.monotonic ברמת המודול ו-_monotonic() בקוד, והטסט מחליף את limits._monotonic. אז השינוי נשאר בתוך המודול ולא נוגע בתהליכונים אחרים. זה שינוי קטן בקוד הייצור, מודול אחרי מודול.
  • freezegun גלובלי מעצם הגדרתו. יש שתי דרכים: לתת לפונקציה שנבדקת now מפורש, או להשאיר את freezegun ולוודא שאין בתהליך הטסטים תהליכון ייצור שרץ ברקע (push-sender עולה בכל תהליך שמייבא את webapp.app — ב-CI הוא נתקע עם _DB_INIT_LOCK ומפיל קובץ טסטים שלם #3486).
  • freezegun.configure(extend_ignore_list=[...]) מגן רק חלקית. _should_use_real_time מחזיר זמן אמיתי רק כשמודול מרשימת ההתעלמות נמצא עד 5 מסגרות מעל הקריאה (call_stack_inspection_limit = 5 ב-freezegun/api.py). למשל "pymongo" יגן על הקריאות של pymongo עצמו, אבל לא על קוד הייצור שלנו שרץ באותו תהליכון.

קשור

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions