From 409b63bc175bf6433f81404320a163e64ee6fba8 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 24 Sep 2026 16:55:45 -0400 Subject: [PATCH] fix(vegas): tell code the gate must not park in by module name, not path The gate never parks a thread inside logging, threading, importlib or the cache, and it matched those as substrings of each frame's file path. On GitHub's runners Python lives under /opt/hostedtoolcache, so every stdlib frame said "cache" and the gate never parked anything -- three tests failed there and passed here. A virtualenv under ~/.cache would have done the same on a Pi. Match the frame's module name (f_globals['__name__']) instead. Co-Authored-By: Claude Opus 5.5 --- src/common/render_gate.py | 26 +++++++++++------------ test/test_render_gate.py | 43 ++++++++++++++++++++++++++------------- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/src/common/render_gate.py b/src/common/render_gate.py index a096db66..0641ba1e 100644 --- a/src/common/render_gate.py +++ b/src/common/render_gate.py @@ -67,15 +67,16 @@ STALE_SECONDS = 0.05 #: Swaps needed before the refresh period is trusted enough to open a window. MIN_SAMPLES = 8 -#: Parking with any of these on the stack could hold a lock the render -#: thread takes: logging handler locks, Condition and Event internals, the -#: module import locks, and the disk and memory cache locks. -_UNSAFE_PATHS = ( - "logging", - "threading.py", - "importlib", - "cache", -) +#: Parking inside any of these modules could hold a lock the render thread +#: takes: logging handler locks, Condition and Event internals, the module +#: import locks, and the disk and memory cache locks. Matched by module name, +#: not file path: a path can say "cache" or "logging" for reasons of its own -- +#: a virtualenv under ~/.cache, or GitHub's /opt/hostedtoolcache, where every +#: stdlib frame would otherwise count and the gate would never park anything. +_UNSAFE_MODULES = frozenset({ + "logging", "threading", "importlib", "src.cache_manager", "src.cache", +}) +_UNSAFE_PREFIXES = ("logging.", "importlib.", "_frozen_importlib", "src.cache.") def _unsafe(frame: Any, base: Any) -> bool: @@ -85,10 +86,9 @@ def _unsafe(frame: Any, base: Any) -> bool: thread's own bootstrap in threading.py) holds nothing. """ while frame is not None and frame is not base: - filename = frame.f_code.co_filename - for part in _UNSAFE_PATHS: - if part in filename: - return True + name = frame.f_globals.get("__name__") or "" + if name in _UNSAFE_MODULES or name.startswith(_UNSAFE_PREFIXES): + return True frame = frame.f_back return False diff --git a/test/test_render_gate.py b/test/test_render_gate.py index d7c06311..3bdc3b30 100644 --- a/test/test_render_gate.py +++ b/test/test_render_gate.py @@ -141,27 +141,42 @@ class TestWhenToPark: done.set() other.join() - @pytest.mark.parametrize("filename", [ - "/usr/lib/python3.11/logging/__init__.py", - "/usr/lib/python3.11/threading.py", - "", - "/home/pi/LEDMatrix/src/cache/disk_cache.py", - "/home/pi/LEDMatrix/src/cache_manager.py", + @staticmethod + def _frame_in(module): + namespace = {"__name__": module} + exec("import sys\ndef here():\n return sys._getframe()\n", namespace) + return namespace["here"]() + + @pytest.mark.parametrize("module", [ + "logging", "logging.handlers", "threading", "importlib", + "_frozen_importlib", "_frozen_importlib_external", + "src.cache_manager", "src.cache.disk_cache", ]) - def test_never_inside_code_that_takes_shared_locks(self, gate, filename): - namespace = {} - exec(compile("import sys\ndef here():\n return sys._getframe()\n", - filename, "exec"), namespace) - frame = namespace["here"]() + def test_never_inside_code_that_takes_shared_locks(self, gate, module): + frame = self._frame_in(module) assert render_gate._unsafe(frame, None) assert not gate._should_park(frame, gate._open_until + 0.0001) + @pytest.mark.parametrize("module", [ + "src.cache_helpers", "plugin_logging_ticker", "src.vegas_mode.render_pipeline", + ]) + def test_modules_that_only_sound_alike_are_fine(self, module): + assert not render_gate._unsafe(self._frame_in(module), None) + + def test_where_python_is_installed_does_not_matter(self): + # GitHub's runners keep Python under /opt/hostedtoolcache; matching + # paths for "cache" made every stdlib frame unsafe there. + namespace = {"__name__": "json.decoder"} + exec(compile("import sys\ndef here():\n return sys._getframe()\n", + "/opt/hostedtoolcache/Python/3.11/lib/json/decoder.py", "exec"), + namespace) + assert not render_gate._unsafe(namespace["here"](), None) + def test_what_lies_below_the_yielding_block_does_not_count(self): # A thread's stack always starts in threading.py; only frames above # the one that entered yielding() matter. - namespace = {} - exec(compile("def bootstrap(fn):\n return fn()\n", - "/usr/lib/python3.11/threading.py", "exec"), namespace) + namespace = {"__name__": "threading"} + exec("def bootstrap(fn):\n return fn()\n", namespace) def entered(): base = sys._getframe()