Browse Source

Fix a race condition where supervisord might ignore a signal. Closes #54.

Mike Naberezny 13 years ago
parent
commit
277eef1f81

+ 4 - 0
CHANGES.txt

@@ -33,6 +33,10 @@ Next release
   send it to its whole process group instead to take care of possible
   send it to its whole process group instead to take care of possible
   children as well and not leave them behind.  Patch by Samuele Pedroni.
   children as well and not leave them behind.  Patch by Samuele Pedroni.
 
 
+- Fixed a race condition where supervisord might not act on a signal sent
+  to it.  Thanks to Adar Dembo for reporting the issue and supplying the
+  initial patch.
+
 3.0a10 (2011-03-30)
 3.0a10 (2011-03-30)
 -------------------
 -------------------
 
 

+ 25 - 9
supervisor/options.py

@@ -357,7 +357,6 @@ class ServerOptions(Options):
     pidfile = None
     pidfile = None
     passwdfile = None
     passwdfile = None
     nodaemon = None
     nodaemon = None
-    signal = None
     environment = None
     environment = None
     httpservers = ()
     httpservers = ()
     unlink_socketfiles = True
     unlink_socketfiles = True
@@ -404,6 +403,7 @@ class ServerOptions(Options):
         self.pidhistory = {}
         self.pidhistory = {}
         self.process_group_configs = []
         self.process_group_configs = []
         self.parse_warnings = []
         self.parse_warnings = []
+        self.signal_receiver = SignalReceiver()
 
 
     def version(self, dummy):
     def version(self, dummy):
         """Print version to stdout and exit(0).
         """Print version to stdout and exit(0).
@@ -1033,15 +1033,16 @@ class ServerOptions(Options):
         self.logger.close()
         self.logger.close()
 
 
     def setsignals(self):
     def setsignals(self):
-        signal.signal(signal.SIGTERM, self.sigreceiver)
-        signal.signal(signal.SIGINT, self.sigreceiver)
-        signal.signal(signal.SIGQUIT, self.sigreceiver)
-        signal.signal(signal.SIGHUP, self.sigreceiver)
-        signal.signal(signal.SIGCHLD, self.sigreceiver)
-        signal.signal(signal.SIGUSR2, self.sigreceiver)
+        receive = self.signal_receiver.receive
+        signal.signal(signal.SIGTERM, receive)
+        signal.signal(signal.SIGINT, receive)
+        signal.signal(signal.SIGQUIT, receive)
+        signal.signal(signal.SIGHUP, receive)
+        signal.signal(signal.SIGCHLD, receive)
+        signal.signal(signal.SIGUSR2, receive)
 
 
-    def sigreceiver(self, sig, frame):
-        self.signal = sig
+    def get_signal(self):
+        return self.signal_receiver.get_signal()
 
 
     def openhttpservers(self, supervisord):
     def openhttpservers(self, supervisord):
         try:
         try:
@@ -1850,6 +1851,21 @@ def _init_signames():
             d[v] = k
             d[v] = k
     _signames = d
     _signames = d
 
 
+class SignalReceiver:
+    def __init__(self):
+        self._signals_recvd = []
+
+    def receive(self, sig, frame):
+        if sig not in self._signals_recvd:
+            self._signals_recvd.append(sig)
+
+    def get_signal(self):
+        if self._signals_recvd:
+            sig = self._signals_recvd.pop(0)
+        else:
+            sig = None
+        return sig
+
 # miscellaneous utility functions
 # miscellaneous utility functions
 
 
 def expand(s, expansions, name):
 def expand(s, expansions, name):

+ 2 - 2
supervisor/supervisord.py

@@ -291,8 +291,8 @@ class Supervisor:
                 self.reap() # keep reaping until no more kids to reap
                 self.reap() # keep reaping until no more kids to reap
 
 
     def handle_signal(self):
     def handle_signal(self):
-        if self.options.signal:
-            sig, self.options.signal = self.options.signal, None
+        sig = self.options.get_signal()
+        if sig:
             if sig in (signal.SIGTERM, signal.SIGINT, signal.SIGQUIT):
             if sig in (signal.SIGTERM, signal.SIGINT, signal.SIGQUIT):
                 self.options.logger.warn(
                 self.options.logger.warn(
                     'received %s indicating exit request' % signame(sig))
                     'received %s indicating exit request' % signame(sig))

+ 4 - 1
supervisor/tests/base.py

@@ -39,7 +39,7 @@ class DummyOptions:
         self.directory = None
         self.directory = None
         self.waitpid_return = None, None
         self.waitpid_return = None, None
         self.kills = {}
         self.kills = {}
-        self.signal = None
+        self._signal = None
         self.parent_pipes_closed = None
         self.parent_pipes_closed = None
         self.child_pipes_closed = None
         self.child_pipes_closed = None
         self.forkpid = 0
         self.forkpid = 0
@@ -102,6 +102,9 @@ class DummyOptions:
     def setsignals(self):
     def setsignals(self):
         self.signals_set = True
         self.signals_set = True
 
 
+    def get_signal(self):
+        return self._signal
+
     def get_socket_map(self):
     def get_socket_map(self):
         return self.socket_map
         return self.socket_map
 
 

+ 41 - 0
supervisor/tests/test_options.py

@@ -470,6 +470,14 @@ class ServerOptionsTests(unittest.TestCase):
         instance = self._makeOne()
         instance = self._makeOne()
         self.assertEqual(os.getpid(), instance.get_pid())
         self.assertEqual(os.getpid(), instance.get_pid())
 
 
+    def test_get_signal_delegates_to_signal_receiver(self):
+        instance = self._makeOne()
+        instance.signal_receiver.receive(signal.SIGTERM, None)
+        instance.signal_receiver.receive(signal.SIGCHLD, None)
+        self.assertEqual(instance.get_signal(), signal.SIGTERM)
+        self.assertEqual(instance.get_signal(), signal.SIGCHLD)
+        self.assertEqual(instance.get_signal(), None)
+
     def test_check_execv_args_cant_find_command(self):
     def test_check_execv_args_cant_find_command(self):
         instance = self._makeOne()
         instance = self._makeOne()
         from supervisor.options import NotFound
         from supervisor.options import NotFound
@@ -1499,6 +1507,39 @@ class FastCGIGroupConfigTests(unittest.TestCase):
         self.assertTrue(instance1 != instance2)
         self.assertTrue(instance1 != instance2)
         self.assertFalse(instance1 == instance2)
         self.assertFalse(instance1 == instance2)
 
 
+class SignalReceiverTests(unittest.TestCase):
+    def test_returns_None_initially(self):
+        from supervisor.options import SignalReceiver
+        sr = SignalReceiver()
+        self.assertEquals(sr.get_signal(), None)
+
+    def test_returns_signals_in_order_received(self):
+        from supervisor.options import SignalReceiver
+        sr = SignalReceiver()
+        sr.receive(signal.SIGTERM, 'frame')
+        sr.receive(signal.SIGCHLD, 'frame')
+        self.assertEquals(sr.get_signal(), signal.SIGTERM)
+        self.assertEquals(sr.get_signal(), signal.SIGCHLD)
+        self.assertEquals(sr.get_signal(), None)
+
+    def test_does_not_queue_duplicate_signals(self):
+        from supervisor.options import SignalReceiver
+        sr = SignalReceiver()
+        sr.receive(signal.SIGTERM, 'frame')
+        sr.receive(signal.SIGTERM, 'frame')
+        self.assertEquals(sr.get_signal(), signal.SIGTERM)
+        self.assertEquals(sr.get_signal(), None)
+
+    def test_queues_again_after_being_emptied(self):
+        from supervisor.options import SignalReceiver
+        sr = SignalReceiver()
+        sr.receive(signal.SIGTERM, 'frame')
+        self.assertEquals(sr.get_signal(), signal.SIGTERM)
+        self.assertEquals(sr.get_signal(), None)
+        sr.receive(signal.SIGCHLD, 'frame')
+        self.assertEquals(sr.get_signal(), signal.SIGCHLD)
+        self.assertEquals(sr.get_signal(), None)
+
 class UtilFunctionsTests(unittest.TestCase):
 class UtilFunctionsTests(unittest.TestCase):
     def test_make_namespec(self):
     def test_make_namespec(self):
         from supervisor.options import make_namespec
         from supervisor.options import make_namespec

+ 6 - 6
supervisor/tests/test_supervisord.py

@@ -137,7 +137,7 @@ class SupervisordTests(unittest.TestCase):
 
 
     def test_handle_sigterm(self):
     def test_handle_sigterm(self):
         options = DummyOptions()
         options = DummyOptions()
-        options.signal = signal.SIGTERM
+        options._signal = signal.SIGTERM
         supervisord = self._makeOne(options)
         supervisord = self._makeOne(options)
         supervisord.handle_signal()
         supervisord.handle_signal()
         self.assertEqual(supervisord.options.mood, -1)
         self.assertEqual(supervisord.options.mood, -1)
@@ -146,7 +146,7 @@ class SupervisordTests(unittest.TestCase):
 
 
     def test_handle_sigint(self):
     def test_handle_sigint(self):
         options = DummyOptions()
         options = DummyOptions()
-        options.signal = signal.SIGINT
+        options._signal = signal.SIGINT
         supervisord = self._makeOne(options)
         supervisord = self._makeOne(options)
         supervisord.handle_signal()
         supervisord.handle_signal()
         self.assertEqual(supervisord.options.mood, -1)
         self.assertEqual(supervisord.options.mood, -1)
@@ -155,7 +155,7 @@ class SupervisordTests(unittest.TestCase):
 
 
     def test_handle_sigquit(self):
     def test_handle_sigquit(self):
         options = DummyOptions()
         options = DummyOptions()
-        options.signal = signal.SIGQUIT
+        options._signal = signal.SIGQUIT
         supervisord = self._makeOne(options)
         supervisord = self._makeOne(options)
         supervisord.handle_signal()
         supervisord.handle_signal()
         self.assertEqual(supervisord.options.mood, -1)
         self.assertEqual(supervisord.options.mood, -1)
@@ -164,7 +164,7 @@ class SupervisordTests(unittest.TestCase):
 
 
     def test_handle_sighup(self):
     def test_handle_sighup(self):
         options = DummyOptions()
         options = DummyOptions()
-        options.signal = signal.SIGHUP
+        options._signal = signal.SIGHUP
         supervisord = self._makeOne(options)
         supervisord = self._makeOne(options)
         supervisord.handle_signal()
         supervisord.handle_signal()
         self.assertEqual(supervisord.options.mood, 0)
         self.assertEqual(supervisord.options.mood, 0)
@@ -173,7 +173,7 @@ class SupervisordTests(unittest.TestCase):
 
 
     def test_handle_sigusr2(self):
     def test_handle_sigusr2(self):
         options = DummyOptions()
         options = DummyOptions()
-        options.signal = signal.SIGUSR2
+        options._signal = signal.SIGUSR2
         pconfig1 = DummyPConfig(options, 'process1', 'process1','/bin/process1')
         pconfig1 = DummyPConfig(options, 'process1', 'process1','/bin/process1')
         from supervisor.process import ProcessStates
         from supervisor.process import ProcessStates
         process1 = DummyProcess(pconfig1, state=ProcessStates.STOPPING)
         process1 = DummyProcess(pconfig1, state=ProcessStates.STOPPING)
@@ -191,7 +191,7 @@ class SupervisordTests(unittest.TestCase):
 
 
     def test_handle_unknown_signal(self):
     def test_handle_unknown_signal(self):
         options = DummyOptions()
         options = DummyOptions()
-        options.signal = signal.SIGUSR1
+        options._signal = signal.SIGUSR1
         supervisord = self._makeOne(options)
         supervisord = self._makeOne(options)
         supervisord.handle_signal()
         supervisord.handle_signal()
         self.assertEqual(supervisord.options.mood, 1)
         self.assertEqual(supervisord.options.mood, 1)