-
Notifications
You must be signed in to change notification settings - Fork 225
Make killable threads more robust to race condition #740
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9b04704
b75f787
22d0ceb
8daf31b
22da6c9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -60,7 +60,7 @@ def run(self): | |
| self._thread_proc() | ||
| except Exception: # pylint: disable=broad-except | ||
| if not self._thread_exception(*sys.exc_info()): | ||
| logging.exception('Thread raised an exception: %s', self.name) | ||
| _LOG.exception('Thread raised an exception: %s', self.name) | ||
| raise | ||
| finally: | ||
| self._thread_finished() | ||
|
|
@@ -83,6 +83,14 @@ class KillableThread(ExceptionSafeThread): | |
| """Killable thread raises an internal exception when killed. | ||
|
|
||
| Based on recipe available at http://tomerfiliba.com/recipes/Thread2/ | ||
|
|
||
| Note: To fully address race conditions involved with the use of | ||
| PyThreadState_SetAsyncExc, the GIL must be held from when the thread is | ||
| checked to when it's async-raised. In this case, we're not doing that, and | ||
| there remains the remote possibility that a thread identifier is reused and we | ||
| accidentally kill the wrong thread. | ||
|
|
||
|
|
||
| """ | ||
|
|
||
| def kill(self): | ||
|
|
@@ -92,15 +100,25 @@ def kill(self): | |
|
|
||
| def async_raise(self, exc_type): | ||
| """Raise the exception.""" | ||
| assert self.is_alive(), 'Only running threads have a thread identity' | ||
| # Should only be called on a started thread so raise otherwise | ||
| assert self.ident is not None, 'Only started threads have thread identifier' | ||
|
|
||
| # If the thread has died we don't want to raise an exception so log. | ||
| if not self.is_alive(): | ||
| _LOG.debug('Not raising %s because thread %s (%s) is not alive', | ||
| exc_type, self.name, self.ident) | ||
| return | ||
|
|
||
| result = ctypes.pythonapi.PyThreadState_SetAsyncExc( | ||
| ctypes.c_long(self.ident), ctypes.py_object(exc_type)) | ||
| if result == 0: | ||
| if result == 0 and self.is_alive(): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like you actually got bit by this before so I'd leave a comment here so it's emphasized that this isn't redundant with the earlier check:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
| # Don't raise an exception an error unnecessarily if the thread is dead. | ||
| raise ValueError('Thread ID was invalid.', self.ident) | ||
| elif result != 1: | ||
| elif result > 1: | ||
| # Something bad happened, call with a NULL exception to undo. | ||
| ctypes.pythonapi.PyThreadState_SetAsyncExc(self.ident, None) | ||
| raise SystemError('PyThreadState_SetAsyncExc failed.', self.ident) | ||
| raise RuntimeError('Error: PyThreadState_SetAsyncExc %s %s (%s) %s', | ||
| exc_type, self.name, self.ident, result) | ||
|
|
||
| def _thread_exception(self, exc_type, exc_val, exc_tb): | ||
| """Suppress the exception when we're kill()'d.""" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
needs
returnhereI don't think we should print
self.identsince as far as the documentation indicates it's a Python-specificmagic cookie to be used e.g. to index a dictionary of thread-specific data. I notice this is the only file where it is used.self.nameshould be sufficient.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's printed out in case it is special or odd. I think it can be None for threads that aren't started yet
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The none-check is on the previous line, I'm not sure if there are other odd cases
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added return.
As farz pointed out, including ident could potentially be useful debugging information and there is no harm to including it at debug level.