faulthandler: one more time, fix usage of locks in the watchdog thread

* Write a new test to ensure that dump_tracebacks_later() still works if
   it was already called and then cancelled before
 * Don't use a variable to check the status of the thread, only rely on locks
 * The thread only releases cancel_event if it was able to acquire it (if
   the timer was interrupted)
 * The main thread always hold this lock. It is only released when
   faulthandler_thread() is interrupted until this thread exits, or at Python
   exit.
This commit is contained in:
Victor Stinner 2011-04-08 12:57:06 +02:00
parent cfa7123ef1
commit de10f4054b
2 changed files with 55 additions and 47 deletions

View File

@ -352,7 +352,7 @@ Current thread XXX:
with temporary_filename() as filename: with temporary_filename() as filename:
self.check_dump_traceback_threads(filename) self.check_dump_traceback_threads(filename)
def _check_dump_tracebacks_later(self, repeat, cancel, filename): def _check_dump_tracebacks_later(self, repeat, cancel, filename, loops):
""" """
Check how many times the traceback is written in timeout x 2.5 seconds, Check how many times the traceback is written in timeout x 2.5 seconds,
or timeout x 3.5 seconds if cancel is True: 1, 2 or 3 times depending or timeout x 3.5 seconds if cancel is True: 1, 2 or 3 times depending
@ -364,42 +364,43 @@ Current thread XXX:
import faulthandler import faulthandler
import time import time
def func(repeat, cancel, timeout): def func(timeout, repeat, cancel, file, loops):
if cancel: for loop in range(loops):
faulthandler.dump_tracebacks_later(timeout, repeat=repeat, file=file)
if cancel:
faulthandler.cancel_dump_tracebacks_later()
time.sleep(timeout * 2.5)
faulthandler.cancel_dump_tracebacks_later() faulthandler.cancel_dump_tracebacks_later()
time.sleep(timeout * 2.5)
faulthandler.cancel_dump_tracebacks_later()
timeout = {timeout} timeout = {timeout}
repeat = {repeat} repeat = {repeat}
cancel = {cancel} cancel = {cancel}
loops = {loops}
if {has_filename}: if {has_filename}:
file = open({filename}, "wb") file = open({filename}, "wb")
else: else:
file = None file = None
faulthandler.dump_tracebacks_later(timeout, func(timeout, repeat, cancel, file, loops)
repeat=repeat, file=file)
func(repeat, cancel, timeout)
if file is not None: if file is not None:
file.close() file.close()
""".strip() """.strip()
code = code.format( code = code.format(
filename=repr(filename), timeout=TIMEOUT,
has_filename=bool(filename),
repeat=repeat, repeat=repeat,
cancel=cancel, cancel=cancel,
timeout=TIMEOUT, loops=loops,
has_filename=bool(filename),
filename=repr(filename),
) )
trace, exitcode = self.get_output(code, filename) trace, exitcode = self.get_output(code, filename)
trace = '\n'.join(trace) trace = '\n'.join(trace)
if not cancel: if not cancel:
count = loops
if repeat: if repeat:
count = 2 count *= 2
else:
count = 1
header = 'Thread 0x[0-9a-f]+:\n' header = 'Thread 0x[0-9a-f]+:\n'
regex = expected_traceback(7, 19, header, count=count) regex = expected_traceback(9, 20, header, count=count)
self.assertRegex(trace, regex) self.assertRegex(trace, regex)
else: else:
self.assertEqual(trace, '') self.assertEqual(trace, '')
@ -408,12 +409,17 @@ if file is not None:
@unittest.skipIf(not hasattr(faulthandler, 'dump_tracebacks_later'), @unittest.skipIf(not hasattr(faulthandler, 'dump_tracebacks_later'),
'need faulthandler.dump_tracebacks_later()') 'need faulthandler.dump_tracebacks_later()')
def check_dump_tracebacks_later(self, repeat=False, cancel=False, def check_dump_tracebacks_later(self, repeat=False, cancel=False,
file=False): file=False, twice=False):
if twice:
loops = 2
else:
loops = 1
if file: if file:
with temporary_filename() as filename: with temporary_filename() as filename:
self._check_dump_tracebacks_later(repeat, cancel, filename) self._check_dump_tracebacks_later(repeat, cancel,
filename, loops)
else: else:
self._check_dump_tracebacks_later(repeat, cancel, None) self._check_dump_tracebacks_later(repeat, cancel, None, loops)
def test_dump_tracebacks_later(self): def test_dump_tracebacks_later(self):
self.check_dump_tracebacks_later() self.check_dump_tracebacks_later()
@ -427,6 +433,9 @@ if file is not None:
def test_dump_tracebacks_later_file(self): def test_dump_tracebacks_later_file(self):
self.check_dump_tracebacks_later(file=True) self.check_dump_tracebacks_later(file=True)
def test_dump_tracebacks_later_twice(self):
self.check_dump_tracebacks_later(twice=True)
@unittest.skipIf(not hasattr(faulthandler, "register"), @unittest.skipIf(not hasattr(faulthandler, "register"),
"need faulthandler.register") "need faulthandler.register")
def check_register(self, filename=False, all_threads=False, def check_register(self, filename=False, all_threads=False,

View File

@ -48,13 +48,14 @@ static struct {
int fd; int fd;
PY_TIMEOUT_T timeout_ms; /* timeout in microseconds */ PY_TIMEOUT_T timeout_ms; /* timeout in microseconds */
int repeat; int repeat;
int running;
PyInterpreterState *interp; PyInterpreterState *interp;
int exit; int exit;
/* released by parent thread when cancel request */ /* The main thread always hold this lock. It is only released when
faulthandler_thread() is interrupted until this thread exits, or at
Python exit. */
PyThread_type_lock cancel_event; PyThread_type_lock cancel_event;
/* released by child thread when joined */ /* released by child thread when joined */
PyThread_type_lock join_event; PyThread_type_lock running;
} thread; } thread;
#endif #endif
@ -414,7 +415,7 @@ faulthandler_thread(void *unused)
st = PyThread_acquire_lock_timed(thread.cancel_event, st = PyThread_acquire_lock_timed(thread.cancel_event,
thread.timeout_ms, 0); thread.timeout_ms, 0);
if (st == PY_LOCK_ACQUIRED) { if (st == PY_LOCK_ACQUIRED) {
/* Cancelled by user */ PyThread_release_lock(thread.cancel_event);
break; break;
} }
/* Timeout => dump traceback */ /* Timeout => dump traceback */
@ -431,21 +432,22 @@ faulthandler_thread(void *unused)
} while (ok && thread.repeat); } while (ok && thread.repeat);
/* The only way out */ /* The only way out */
PyThread_release_lock(thread.cancel_event); PyThread_release_lock(thread.running);
PyThread_release_lock(thread.join_event);
} }
static void static void
faulthandler_cancel_dump_tracebacks_later(void) cancel_dump_tracebacks_later(void)
{ {
if (thread.running) { /* notify cancellation */
/* Notify cancellation */ PyThread_release_lock(thread.cancel_event);
PyThread_release_lock(thread.cancel_event);
}
/* Wait for thread to join */ /* Wait for thread to join */
PyThread_acquire_lock(thread.join_event, 1); PyThread_acquire_lock(thread.running, 1);
PyThread_release_lock(thread.join_event); PyThread_release_lock(thread.running);
thread.running = 0;
/* The main thread should always hold the cancel_event lock */
PyThread_acquire_lock(thread.cancel_event, 1);
Py_CLEAR(thread.file); Py_CLEAR(thread.file);
} }
@ -489,7 +491,7 @@ faulthandler_dump_tracebacks_later(PyObject *self,
return NULL; return NULL;
/* Cancel previous thread, if running */ /* Cancel previous thread, if running */
faulthandler_cancel_dump_tracebacks_later(); cancel_dump_tracebacks_later();
Py_XDECREF(thread.file); Py_XDECREF(thread.file);
Py_INCREF(file); Py_INCREF(file);
@ -501,14 +503,10 @@ faulthandler_dump_tracebacks_later(PyObject *self,
thread.exit = exit; thread.exit = exit;
/* Arm these locks to serve as events when released */ /* Arm these locks to serve as events when released */
PyThread_acquire_lock(thread.join_event, 1); PyThread_acquire_lock(thread.running, 1);
PyThread_acquire_lock(thread.cancel_event, 1);
thread.running = 1;
if (PyThread_start_new_thread(faulthandler_thread, NULL) == -1) { if (PyThread_start_new_thread(faulthandler_thread, NULL) == -1) {
thread.running = 0; PyThread_release_lock(thread.running);
PyThread_release_lock(thread.join_event);
PyThread_release_lock(thread.cancel_event);
Py_CLEAR(thread.file); Py_CLEAR(thread.file);
PyErr_SetString(PyExc_RuntimeError, PyErr_SetString(PyExc_RuntimeError,
"unable to start watchdog thread"); "unable to start watchdog thread");
@ -521,7 +519,7 @@ faulthandler_dump_tracebacks_later(PyObject *self,
static PyObject* static PyObject*
faulthandler_cancel_dump_tracebacks_later_py(PyObject *self) faulthandler_cancel_dump_tracebacks_later_py(PyObject *self)
{ {
faulthandler_cancel_dump_tracebacks_later(); cancel_dump_tracebacks_later();
Py_RETURN_NONE; Py_RETURN_NONE;
} }
#endif /* FAULTHANDLER_LATER */ #endif /* FAULTHANDLER_LATER */
@ -1001,15 +999,15 @@ int _PyFaulthandler_Init(void)
} }
#endif #endif
#ifdef FAULTHANDLER_LATER #ifdef FAULTHANDLER_LATER
thread.running = 0;
thread.file = NULL; thread.file = NULL;
thread.cancel_event = PyThread_allocate_lock(); thread.cancel_event = PyThread_allocate_lock();
thread.join_event = PyThread_allocate_lock(); thread.running = PyThread_allocate_lock();
if (!thread.cancel_event || !thread.join_event) { if (!thread.cancel_event || !thread.running) {
PyErr_SetString(PyExc_RuntimeError, PyErr_SetString(PyExc_RuntimeError,
"could not allocate locks for faulthandler"); "could not allocate locks for faulthandler");
return -1; return -1;
} }
PyThread_acquire_lock(thread.cancel_event, 1);
#endif #endif
return faulthandler_env_options(); return faulthandler_env_options();
@ -1023,14 +1021,15 @@ void _PyFaulthandler_Fini(void)
#ifdef FAULTHANDLER_LATER #ifdef FAULTHANDLER_LATER
/* later */ /* later */
faulthandler_cancel_dump_tracebacks_later(); cancel_dump_tracebacks_later();
if (thread.cancel_event) { if (thread.cancel_event) {
PyThread_release_lock(thread.cancel_event);
PyThread_free_lock(thread.cancel_event); PyThread_free_lock(thread.cancel_event);
thread.cancel_event = NULL; thread.cancel_event = NULL;
} }
if (thread.join_event) { if (thread.running) {
PyThread_free_lock(thread.join_event); PyThread_free_lock(thread.running);
thread.join_event = NULL; thread.running = NULL;
} }
#endif #endif