Skip to content
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

Add the possibility to ignore sigint from other threads #7623

Merged
merged 5 commits into from
Apr 30, 2019

Conversation

blorente
Copy link
Contributor

Problem

In the context of #7596, pants tasks will be executed outside of the main thread. This means that tasks like python-repl cannot override signal handling (code), because this can only be done from the main thread.

Solution

Create an atomic variable, SignalHandler._ignore_sigint, that gates SIGINT handling. Create a global context manager inside ExceptionSink that toggles this for a certain time if needed.

Result

Any thread can pause the handling of SIGINT in a controlled way.
python-repl no longer installs signal handlers, but rather it toggles the handling of the signal.

Caveat

We may want to gate all signals individually, but the semantics of SIGINT are usually different enough from SIGTERM and SIGKILL that I think it's okay to only gate the first one.

Copy link
Contributor

@illicitonion illicitonion left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! One naming thing :)

def _handle_sigint_if_enabled(self, signum, _frame):
with self._ignore_sigint_lock:
self._check_sigint_gate_is_correct()
should_ignore_sigint = self._ignore_sigint
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both of the names on this line are kind of unclear...

should_ignore_sigint reads to me like a boolean (when actually the only "true" value of it is 0, which is False as a boolean)
self._ignore_sigint also reads to me like a boolean.

Maybe we could call both of these number_of_threads_ignoring_sigint or similar?

@@ -310,6 +338,18 @@ def trapped_signals(cls, new_signal_handler):
finally:
cls.reset_signal_handler(previous_signal_handler)

@classmethod
@contextmanager
def ignoring_sigint(cls):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might be more easily and explicitly done by creating a new SignalHandler subclass in python_repl.py with the lock and flag, then using with ExceptionSink.trapped_signals(sigint_toggling_signal_handler): there, instead of reaching in and mutating the global signal handler field, or moving signal handling concerns into ExceptionSink itself. This seems to be more testable, less coupled, and less spooky, and makes it clear when looking at the python repl task that some exclusive locking which modifies global state is going on.

This solution seems to work great as is -- my main goal with the global state in ExceptionSink was to make global state changes explicit and avoid mixing application-specific and process-handling logic. SignalHandler was an attempt to move task-specific or process-specific logic out of ExceptionSink and decouple it from the way we actually exit or log errors. Since it seems like ExceptionSink.trapped_signals() overwriting the existing signal handling provides the temporary override we want, it seems reasonable to use that approach instead of creating a new interface which reaches into the signal handler base class to modify it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #7606 for an example of an Exiter accepting another instance of an Exiter and applying the prototype pattern in order to do a two-step teardown within LocalPantsRunner by calling another exiter passed into the constructor -- we could do something similar here with a SignalHandler subclass which does the same thing as a base signal handler instance, but also adds the ignoring_sigint() logic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, wholeheartedly. But, in a world without pantsd-runner, the repl runs in a non-main thread, so when we try to do ExceptionSink.trapped_signals(), and this tries to signal.signal() for whatever handler we passed from python-repl, it will crash saying that this can only be done from the main thread.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh! I didn't realize that, sorry. Could we then add an underscore to SignalHandler._ignoring_sigint() and then add something like:

NB: This method calls signal.signal(), which will crash if not called from the main thread!

to the docstring for .reset_signal_handler() and .trapped_signals()?

@@ -310,6 +338,18 @@ def trapped_signals(cls, new_signal_handler):
finally:
cls.reset_signal_handler(previous_signal_handler)

@classmethod
@contextmanager
def ignoring_sigint(cls):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh! I didn't realize that, sorry. Could we then add an underscore to SignalHandler._ignoring_sigint() and then add something like:

NB: This method calls signal.signal(), which will crash if not called from the main thread!

to the docstring for .reset_signal_handler() and .trapped_signals()?

@blorente blorente force-pushed the blorente/gated-signal-handler branch from ea1a714 to bd37935 Compare April 29, 2019 17:11
@blorente blorente merged commit 7ca3089 into pantsbuild:master Apr 30, 2019
@blorente blorente mentioned this pull request Apr 30, 2019
2 tasks
blorente added a commit that referenced this pull request Jun 21, 2019
## Problem
After removing forking from pantsd, we introduced a new abstraction to ExceptionSink that allowed ignoring SIGINT without installing signal handlers (#7623).

This was necessary because no pants runs under pantsd run in a non-main thread, and therefore crash when you install signal handlers.

This PR makes use of that new functionality, to ignore SIGINT in python_repls

## Solution
Instead of only ignoring sigint in the cases where we are not running from the daemon, ignore it every time using the new ExceptionSink.ignoring_sigint() contextmanager.

## Result
Using Ctrl-C on a python repl is properly ignored and no longer crashes the daemon.
blorente added a commit to blorente/pants that referenced this pull request Jun 21, 2019
## Problem
After removing forking from pantsd, we introduced a new abstraction to ExceptionSink that allowed ignoring SIGINT without installing signal handlers (pantsbuild#7623).

This was necessary because no pants runs under pantsd run in a non-main thread, and therefore crash when you install signal handlers.

This PR makes use of that new functionality, to ignore SIGINT in python_repls

## Solution
Instead of only ignoring sigint in the cases where we are not running from the daemon, ignore it every time using the new ExceptionSink.ignoring_sigint() contextmanager.

## Result
Using Ctrl-C on a python repl is properly ignored and no longer crashes the daemon.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants