Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion fusil/python/write_python_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,11 +559,27 @@ def skip_trivial_type(obj_instance_or_class):
# turning rare "GC fires while an object is tracked but half-initialised"
# races (tp_traverse reading a NULL field) into deterministic crashes. A cheap
# global coercion like the OOM hook; composes with the bombs + class fuzzing.
#
# GUARDED, and the guard is load-bearing: gc.set_threshold is CPython-only.
# PyPy's gc module does not have it, so an unguarded call raises AttributeError at
# MODULE LEVEL -- before a single target call -- and every session dies in the
# prelude scoring 0. A 43 368-session PyPy fleet produced exactly zero crashes,
# zero timeouts and 0.3-second sessions that way, and nothing in the logs said why:
# the sessions look "clean", not broken. This is the same shape as the pre-3.13
# target-compat break fixed in #256; the lesson is that anything CPython-specific
# in the prelude must be guarded, because the prelude runs on the TARGET.
#
# The flag stays a no-op rather than an error on such a target: it is a tuning
# knob, not a request, and PyPy's GC is not generational in CPython's sense so
# there is nothing to coerce.
self.write_block(
0,
"""
import gc
gc.set_threshold(1, 1, 1)
try:
gc.set_threshold(1, 1, 1)
except AttributeError:
pass # not CPython: no generational threshold to tighten
""",
)
self.emptyLine()
Expand Down
62 changes: 62 additions & 0 deletions tests/python/test_gc_aggressive.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,68 @@ def test_absent_when_off(self):
ast.parse(src)
self.assertNotIn("gc.set_threshold(1, 1, 1)", src)

def test_guarded_so_a_non_cpython_target_survives_the_prelude(self):
"""`gc.set_threshold` is CPython-only, and the prelude runs on the TARGET.

Unguarded, it raises AttributeError at module level on PyPy -- before a single target
call -- so every session dies in the prelude scoring 0. That failure is silent: the
sessions look clean rather than broken. One 43 368-session PyPy fleet produced zero
crashes, zero timeouts and 0.3-second sessions entirely for this reason.
"""
src = _generate(gc_aggressive=True)
tree = ast.parse(src)

call = None
for node in ast.walk(tree):
if (
isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute)
and node.func.attr == "set_threshold"
):
call = node
break
self.assertIsNotNone(call, "gc.set_threshold was not emitted")

# It must sit inside a try/except that catches AttributeError.
guarded = False
for node in ast.walk(tree):
if not isinstance(node, ast.Try):
continue
if not any(n is call for n in ast.walk(node)):
continue
for handler in node.handlers:
names = []
if isinstance(handler.type, ast.Name):
names = [handler.type.id]
elif isinstance(handler.type, ast.Tuple):
names = [e.id for e in handler.type.elts if isinstance(e, ast.Name)]
elif handler.type is None:
names = ["AttributeError"] # bare except catches it
if "AttributeError" in names or "Exception" in names:
guarded = True
self.assertTrue(
guarded,
"gc.set_threshold must be wrapped in try/except AttributeError: it is CPython-only "
"and the prelude runs on the target interpreter",
)

def test_the_emitted_guard_actually_survives_a_gc_without_set_threshold(self):
"""Exec the emitted snippet against a gc module that lacks the attribute."""
src = _generate(gc_aggressive=True)
start = src.index("import gc")
snippet = src[start : src.index("\n\n", start)]

import types

fake_gc = types.ModuleType("gc") # no set_threshold, like PyPy's
real = sys.modules.get("gc")
sys.modules["gc"] = fake_gc
try:
exec(compile(snippet, "<emitted>", "exec"), {})
finally:
if real is not None:
sys.modules["gc"] = real


if __name__ == "__main__":
unittest.main()
Loading