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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ bridges_compiled=0
descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
guard_failures=3
guard_failures=2
internal_compile_panics=0
loops_aborted=0
loops_compiled=1
2 changes: 1 addition & 1 deletion pyre/bench/synth/exception_subclass_attrs.dynasm.jitstats
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ bridges_compiled=0
descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
guard_failures=3
guard_failures=2
internal_compile_panics=0
loops_aborted=0
loops_compiled=1
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
bridges_compiled=23
bridges_compiled=26
descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
guard_failures=5182
fbw_rolled_back_with_effects=0
guard_failures=5165
internal_compile_panics=0
loops_aborted=4
loops_aborted=2
loops_compiled=3
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ bridges_compiled=0
descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
guard_failures=5
guard_failures=4
internal_compile_panics=0
loops_aborted=3
loops_compiled=7
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ bridges_compiled=4
descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
guard_failures=823
fbw_rolled_back_with_effects=0
guard_failures=824
internal_compile_panics=0
loops_aborted=0
loops_compiled=6
116 changes: 116 additions & 0 deletions pyre/extra_tests/parity_tests/thread_start_walk_abort_no_replay.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""A walk that aborts after executing a side-effecting residual must not replay it.

An authoritative full-body walk executes residual calls concretely as it goes.
When it then aborts on a kept-stack branch guard it cannot compile, the only
sound continuations resume FORWARD (`pyjitpl.py:2949
run_blackhole_interp_to_cancel_tracing`); handing the frame back at its entry
re-runs everything the walk already applied. `threading.Thread.start` is where
that is observable rather than merely wasteful: the walk runs
`_start_joinable_thread`, which spawns the OS thread, and a replay calls it a
second time on a handle that is already started.

The three faces of that single replay, depending on how far the freshly spawned
worker got before the interpreter re-entered `start`:

- the worker had already run `_started.set()` -> the fresh `Event` this object
built in `__init__`, still unpublished, reads set,
- it had not -> `RuntimeError: thread already started`,
- with the stock `Thread.start`, whose own guard sees the set Event ->
`RuntimeError: threads can only be started once`.

⚠️ This file is a REPRODUCER, not a tidy test, and the difference matters. The
walk only reaches the aborting guard for a narrow traced shape: `start` spelled
out instead of delegating, the three Event reads kept, `Event` bound as a global
rather than reached through `threading.`, the two locals read inside the final
`try`, and failures leaving through `die` (a plain call) rather than `raise`,
whose exception edge changes the guard. Every one of those was measured — each
simplification made the file pass against a build WITHOUT the fix, i.e. cover
nothing. Re-measure against such a build before simplifying anything here.

Every acquire on the main thread is bounded so a lost release reports instead of
hanging, and `die` uses `os._exit` so a failure cannot block on shutdown.
"""

import contextvars
import os
import threading
from threading import Event

N = 20
R = 150


def die(msg):
os.write(2, msg.encode())
os._exit(1)


class T(threading.Thread):
def __init__(self, *a, **kw):
threading.Thread.__init__(self, *a, **kw)
self._ev_ref = self._started
if self._started._flag:
die("BORN TRUE: fresh Event already set at __init__ id=%d\n"
% id(self._started))

def start(self):
pre = self._started
if pre is not self._ev_ref:
die("STALE LOAD_ATTR self._started: got id=%d %r, stored id=%d %r\n"
% (id(pre), pre, id(self._ev_ref), self._ev_ref))
if not isinstance(pre, Event):
die("WRONG SLOT (pre) %r %s\n" % (pre, type(pre)))
f_call = pre.is_set()
f_attr = pre._flag
f_call2 = pre.is_set()
if not (f_call is f_attr is f_call2):
die("DISAGREE call=%r attr=%r call2=%r ev_id=%d\n"
% (f_call, f_attr, f_call2, id(pre)))
if f_call:
die("FLIPPED AFTER INIT: a fresh Event reads set before start() "
"published it — start() ran twice. ev_id=%d thread_id=%d\n"
% (id(pre), id(self)))
if not threading._active_limbo_lock.acquire(True, 5.0):
die("BLOCKED on _active_limbo_lock\n")
try:
threading._limbo[self] = self
finally:
threading._active_limbo_lock.release()
if self._context is None:
self._context = contextvars.Context()
threading._start_joinable_thread(
self._bootstrap, handle=self._os_thread_handle, daemon=self.daemon)
post = self._started
if not isinstance(post, Event):
die("WRONG SLOT (post) %r %s same=%s\n" % (post, type(post), post is pre))
# Event.wait(), with the outer Condition acquire bounded.
cond = post._cond
if not cond._lock.acquire(True, 5.0):
die("LOST RELEASE on _started._cond._lock: flag=%r alive=%r waiters=%d\n"
% (post._flag, self.is_alive(), len(cond._waiters)))
try:
alive_before = self.is_alive()
w_before = len(cond._waiters)
if not post._flag and not cond.wait(5.0):
die("NO SIGNAL | check: alive=%r waiters=%d | now: flag=%r "
"alive=%r waiters=%d\n"
% (alive_before, w_before, post._flag, self.is_alive(),
len(cond._waiters)))
finally:
cond._lock.release()


for r in range(R):
ts = [T(target=lambda: None) for _ in range(N)]
try:
for t in ts:
t.start()
except RuntimeError as e:
die("START RAISED %r in round %d — start() ran twice on one handle\n"
% (e, r))
for i, t in enumerate(ts):
t.join(5.0)
if t.is_alive():
die("JOIN TIMEOUT round=%d idx=%d\n" % (r, i))

print("OK")
Loading
Loading