Merge lp:~allenap/maas/process-group-leader-race--2.1 into lp:maas/2.1

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 5597
Proposed branch: lp:~allenap/maas/process-group-leader-race--2.1
Merge into: lp:maas/2.1
Diff against target: 296 lines (+65/-50)
7 files modified
src/provisioningserver/utils/arp.py (+5/-0)
src/provisioningserver/utils/avahi.py (+6/-0)
src/provisioningserver/utils/services.py (+2/-5)
src/provisioningserver/utils/tests/test_arp.py (+9/-0)
src/provisioningserver/utils/tests/test_avahi.py (+9/-0)
src/provisioningserver/utils/tests/test_twisted.py (+32/-28)
src/provisioningserver/utils/twisted.py (+2/-17)
To merge this branch: bzr merge lp:~allenap/maas/process-group-leader-race--2.1
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+321288@code.launchpad.net

Commit message

Backport r5866 from trunk: Remove ProcessGroupLeaderMixin: it is subject to a race with reactor.spawnProcess.

Instead, subprocesses must set themselves as process group leaders.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Backport, hence selfie.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/provisioningserver/utils/arp.py'
--- src/provisioningserver/utils/arp.py 2016-09-27 11:56:15 +0000
+++ src/provisioningserver/utils/arp.py 2017-03-29 13:03:13 +0000
@@ -370,6 +370,11 @@
370def run(args, output=sys.stdout, stdin=sys.stdin,370def run(args, output=sys.stdout, stdin=sys.stdin,
371 stdin_buffer=sys.stdin.buffer):371 stdin_buffer=sys.stdin.buffer):
372 """Observe an Ethernet interface and print ARP bindings."""372 """Observe an Ethernet interface and print ARP bindings."""
373
374 # First, become a progress group leader, so that signals can be directed
375 # to this process and its children; see p.u.twisted.terminateProcess.
376 os.setpgrp()
377
373 network_monitor = None378 network_monitor = None
374 if args.input_file is None:379 if args.input_file is None:
375 if args.interface is None:380 if args.interface is None:
376381
=== modified file 'src/provisioningserver/utils/avahi.py'
--- src/provisioningserver/utils/avahi.py 2016-09-21 15:25:11 +0000
+++ src/provisioningserver/utils/avahi.py 2017-03-29 13:03:13 +0000
@@ -38,6 +38,7 @@
38from contextlib import contextmanager38from contextlib import contextmanager
39import io39import io
40import json40import json
41import os
41import re42import re
42import subprocess43import subprocess
43import sys44import sys
@@ -203,6 +204,11 @@
203204
204def run(args, output=sys.stdout, stdin=sys.stdin):205def run(args, output=sys.stdout, stdin=sys.stdin):
205 """Observe an Ethernet interface and print ARP bindings."""206 """Observe an Ethernet interface and print ARP bindings."""
207
208 # First, become a progress group leader, so that signals can be directed
209 # to this process and its children; see p.u.twisted.terminateProcess.
210 os.setpgrp()
211
206 if args.input_file is None:212 if args.input_file is None:
207 reader = _reader_from_avahi()213 reader = _reader_from_avahi()
208 elif args.input_file == "-":214 elif args.input_file == "-":
209215
=== modified file 'src/provisioningserver/utils/services.py'
--- src/provisioningserver/utils/services.py 2016-10-28 15:58:32 +0000
+++ src/provisioningserver/utils/services.py 2017-03-29 13:03:13 +0000
@@ -32,7 +32,6 @@
32from provisioningserver.utils.twisted import (32from provisioningserver.utils.twisted import (
33 callOut,33 callOut,
34 deferred,34 deferred,
35 ProcessGroupLeaderMixin,
36 terminateProcess,35 terminateProcess,
37)36)
38from twisted.application.internet import TimerService37from twisted.application.internet import TimerService
@@ -124,8 +123,7 @@
124 self.done.errback(reason)123 self.done.errback(reason)
125124
126125
127class ProtocolForObserveARP(126class ProtocolForObserveARP(JSONPerLineProtocol):
128 ProcessGroupLeaderMixin, JSONPerLineProtocol):
129 """Protocol used when spawning `maas-rack observe-arp`.127 """Protocol used when spawning `maas-rack observe-arp`.
130128
131 The difference between `JSONPerLineProtocol` and `ProtocolForObserveARP`129 The difference between `JSONPerLineProtocol` and `ProtocolForObserveARP`
@@ -149,8 +147,7 @@
149 log.msg("observe-arp[%s]:" % self.interface, line)147 log.msg("observe-arp[%s]:" % self.interface, line)
150148
151149
152class ProtocolForObserveMDNS(150class ProtocolForObserveMDNS(JSONPerLineProtocol):
153 ProcessGroupLeaderMixin, JSONPerLineProtocol):
154 """Protocol used when spawning `maas-rack observe-mdns`.151 """Protocol used when spawning `maas-rack observe-mdns`.
155152
156 This ensures that the spawned process is configured as a process group153 This ensures that the spawned process is configured as a process group
157154
=== modified file 'src/provisioningserver/utils/tests/test_arp.py'
--- src/provisioningserver/utils/tests/test_arp.py 2016-08-25 16:10:40 +0000
+++ src/provisioningserver/utils/tests/test_arp.py 2017-03-29 13:03:13 +0000
@@ -14,6 +14,8 @@
14import time14import time
15from unittest.mock import Mock15from unittest.mock import Mock
1616
17from maastesting.factory import factory
18from maastesting.matchers import MockCalledOnceWith
17from maastesting.testcase import MAASTestCase19from maastesting.testcase import MAASTestCase
18from netaddr import (20from netaddr import (
19 EUI,21 EUI,
@@ -509,3 +511,10 @@
509 with ExpectedException(511 with ExpectedException(
510 SystemExit, '.*42.*'):512 SystemExit, '.*42.*'):
511 run(args, output=output)513 run(args, output=output)
514
515 def test__sets_self_as_process_group_leader(self):
516 exception_type = factory.make_exception_type()
517 os = self.patch(arp_module, "os")
518 os.setpgrp.side_effect = exception_type
519 self.assertRaises(exception_type, run, [])
520 self.assertThat(os.setpgrp, MockCalledOnceWith())
512521
=== modified file 'src/provisioningserver/utils/tests/test_avahi.py'
--- src/provisioningserver/utils/tests/test_avahi.py 2016-09-13 17:15:27 +0000
+++ src/provisioningserver/utils/tests/test_avahi.py 2017-03-29 13:03:13 +0000
@@ -12,6 +12,8 @@
12from tempfile import NamedTemporaryFile12from tempfile import NamedTemporaryFile
13import time13import time
1414
15from maastesting.factory import factory
16from maastesting.matchers import MockCalledOnceWith
15from maastesting.testcase import MAASTestCase17from maastesting.testcase import MAASTestCase
16from provisioningserver.utils import avahi as avahi_module18from provisioningserver.utils import avahi as avahi_module
17from provisioningserver.utils.avahi import (19from provisioningserver.utils.avahi import (
@@ -279,3 +281,10 @@
279 output = io.StringIO()281 output = io.StringIO()
280 with ExpectedException(SystemExit, '.*42.*'):282 with ExpectedException(SystemExit, '.*42.*'):
281 run(args, output=output)283 run(args, output=output)
284
285 def test__sets_self_as_process_group_leader(self):
286 exception_type = factory.make_exception_type()
287 os = self.patch(avahi_module, "os")
288 os.setpgrp.side_effect = exception_type
289 self.assertRaises(exception_type, run, [])
290 self.assertThat(os.setpgrp, MockCalledOnceWith())
282291
=== modified file 'src/provisioningserver/utils/tests/test_twisted.py'
--- src/provisioningserver/utils/tests/test_twisted.py 2016-10-24 14:36:30 +0000
+++ src/provisioningserver/utils/tests/test_twisted.py 2017-03-29 13:03:13 +0000
@@ -14,7 +14,6 @@
14from random import (14from random import (
15 randint,15 randint,
16 random,16 random,
17 randrange,
18)17)
19import re18import re
20import signal19import signal
@@ -64,7 +63,6 @@
64 LONGTIME,63 LONGTIME,
65 makeDeferredWithProcessProtocol,64 makeDeferredWithProcessProtocol,
66 pause,65 pause,
67 ProcessGroupLeaderMixin,
68 retries,66 retries,
69 RPCFetcher,67 RPCFetcher,
70 synchronous,68 synchronous,
@@ -1723,6 +1721,30 @@
17231721
1724# A script that prints the signals it receives, as long as they're SIGTERM or1722# A script that prints the signals it receives, as long as they're SIGTERM or
1725# SIGQUIT. It prints "Ready." on stderr once it's registered signal handlers.1723# SIGQUIT. It prints "Ready." on stderr once it's registered signal handlers.
1724
1725# This sets itself as process group leader.
1726signal_printer_pgl = """\
1727from os import setpgrp
1728from signal import Signals, signal
1729from sys import stderr
1730from time import sleep
1731from traceback import print_exc
1732
1733def print_signal(signum, frame):
1734 print(Signals(signum).name, flush=True)
1735
1736signal(Signals.SIGTERM, print_signal)
1737signal(Signals.SIGQUIT, print_signal)
1738
1739setpgrp()
1740
1741print("Ready.", file=stderr, flush=True)
1742
1743sleep(5.0)
1744"""
1745
1746# This variant should be identical, except that it does
1747# not set itself as process group leader.
1726signal_printer = """\1748signal_printer = """\
1727from signal import Signals, signal1749from signal import Signals, signal
1728from sys import stderr1750from sys import stderr
@@ -1763,14 +1785,6 @@
1763 self.done.callback(reason)1785 self.done.callback(reason)
17641786
17651787
1766class SignalPrinterProtocolAsGroupLeader(
1767 ProcessGroupLeaderMixin, SignalPrinterProtocol):
1768 """Process protocol for use with `signal_printer` that ...
1769
1770 ...configures itself as a process group leader.
1771 """
1772
1773
1774class TestTerminateProcess(MAASTestCase):1788class TestTerminateProcess(MAASTestCase):
1775 """Tests for `terminateProcess`."""1789 """Tests for `terminateProcess`."""
17761790
@@ -1778,7 +1792,6 @@
17781792
1779 def setUp(self):1793 def setUp(self):
1780 super(TestTerminateProcess, self).setUp()1794 super(TestTerminateProcess, self).setUp()
1781 self.sigprint = self.make_file("sigprint.py", signal_printer)
1782 # Allow spying on calls to os.kill and os.killpg by terminateProcess.1795 # Allow spying on calls to os.kill and os.killpg by terminateProcess.
1783 self.assertThat(twisted_module._os_kill, Is(os.kill))1796 self.assertThat(twisted_module._os_kill, Is(os.kill))
1784 self.patch(1797 self.patch(
@@ -1789,13 +1802,16 @@
1789 twisted_module, "_os_killpg",1802 twisted_module, "_os_killpg",
1790 Mock(wraps=twisted_module._os_killpg))1803 Mock(wraps=twisted_module._os_killpg))
17911804
1792 def startSignalPrinter(self, protocol):1805 def startSignalPrinter(self, protocol, *, pgrp=False):
1806 script_filename = self.make_file(
1807 "sigprint.py", signal_printer_pgl if pgrp else signal_printer)
1808
1793 self.assertThat(protocol, IsInstance(SignalPrinterProtocol))1809 self.assertThat(protocol, IsInstance(SignalPrinterProtocol))
1794 self.addDetail("out", content_from_stream(protocol.out, seek_offset=0))1810 self.addDetail("out", content_from_stream(protocol.out, seek_offset=0))
1795 self.addDetail("err", content_from_stream(protocol.err, seek_offset=0))1811 self.addDetail("err", content_from_stream(protocol.err, seek_offset=0))
1796 python = sys.executable.encode("utf-8")1812 python = sys.executable.encode("utf-8")
1797 process = reactor.spawnProcess(1813 process = reactor.spawnProcess(
1798 protocol, python, (python, self.sigprint.encode("utf-8")))1814 protocol, python, (python, script_filename.encode("utf-8")))
17991815
1800 # Wait for the spawned subprocess to tell us that it's ready.1816 # Wait for the spawned subprocess to tell us that it's ready.
1801 def cbReady(message):1817 def cbReady(message):
@@ -1829,8 +1845,8 @@
18291845
1830 @inlineCallbacks1846 @inlineCallbacks
1831 def test__terminates_with_kill_and_killpg(self):1847 def test__terminates_with_kill_and_killpg(self):
1832 protocol = SignalPrinterProtocolAsGroupLeader()1848 protocol = SignalPrinterProtocol()
1833 process = yield self.startSignalPrinter(protocol)1849 process = yield self.startSignalPrinter(protocol, pgrp=True)
1834 # Capture the pid now; it gets cleared when the process exits.1850 # Capture the pid now; it gets cleared when the process exits.
1835 pid = process.pid1851 pid = process.pid
1836 # Terminate and wait for it to exit.1852 # Terminate and wait for it to exit.
@@ -1851,7 +1867,7 @@
1851 @inlineCallbacks1867 @inlineCallbacks
1852 def test__terminates_with_kill_if_not_in_separate_process_group(self):1868 def test__terminates_with_kill_if_not_in_separate_process_group(self):
1853 protocol = SignalPrinterProtocol()1869 protocol = SignalPrinterProtocol()
1854 process = yield self.startSignalPrinter(protocol)1870 process = yield self.startSignalPrinter(protocol, pgrp=False)
1855 # Capture the pid now; it gets cleared when the process exits.1871 # Capture the pid now; it gets cleared when the process exits.
1856 pid = process.pid1872 pid = process.pid
1857 # Terminate and wait for it to exit.1873 # Terminate and wait for it to exit.
@@ -1867,15 +1883,3 @@
1867 ))1883 ))
1868 self.assertThat(1884 self.assertThat(
1869 twisted_module._os_killpg, MockNotCalled())1885 twisted_module._os_killpg, MockNotCalled())
1870
1871
1872class TestProcessGroupLeaderMixin(MAASTestCase):
1873 """Tests for `ProcessGroupLeaderMixin`."""
1874
1875 def test__calls_setpgid_on_child_process(self):
1876 self.assertThat(twisted_module._os_setpgid, Is(os.setpgid))
1877 setpgid = self.patch(twisted_module, "_os_setpgid")
1878 protocol = ProcessGroupLeaderMixin()
1879 transport = Mock(pid=randrange(99999, 9999999))
1880 protocol.makeConnection(transport)
1881 self.assertThat(setpgid, MockCalledOnceWith(transport.pid, 0))
18821886
=== modified file 'src/provisioningserver/utils/twisted.py'
--- src/provisioningserver/utils/twisted.py 2016-10-28 15:58:32 +0000
+++ src/provisioningserver/utils/twisted.py 2017-03-29 13:03:13 +0000
@@ -20,7 +20,6 @@
20 'LONGTIME',20 'LONGTIME',
21 'makeDeferredWithProcessProtocol',21 'makeDeferredWithProcessProtocol',
22 'pause',22 'pause',
23 'ProcessGroupLeaderMixin',
24 'retries',23 'retries',
25 'synchronous',24 'synchronous',
26 'ThreadPool',25 'ThreadPool',
@@ -46,7 +45,6 @@
46from os import (45from os import (
47 kill as _os_kill,46 kill as _os_kill,
48 killpg as _os_killpg,47 killpg as _os_killpg,
49 setpgid as _os_setpgid,
50)48)
51import signal49import signal
52import threading50import threading
@@ -1003,9 +1001,8 @@
1003 Steps #3 and #5 have a safeguard: if the process identified by `pid` has1001 Steps #3 and #5 have a safeguard: if the process identified by `pid` has
1004 the same process group as the invoking process the signal is sent only to1002 the same process group as the invoking process the signal is sent only to
1005 the process and not to the process group. This prevents the caller from1003 the process and not to the process group. This prevents the caller from
1006 inadvertently killing itself. You may want to use this in conjunction with1004 inadvertently killing itself. For best effect, ensure that new processes
1007 `ProcessGroupLeaderMixin` to ensure that the child process becomes a1005 become process group leaders soon after spawning.
1008 process group leader soon after spawning.
10091006
1010 :param pid: The PID to terminate.1007 :param pid: The PID to terminate.
1011 :param done: A `Deferred` that fires when the process exits.1008 :param done: A `Deferred` that fires when the process exits.
@@ -1046,15 +1043,3 @@
1046 killer.cancel()1043 killer.cancel()
10471044
1048 done.addBoth(callOut, ended)1045 done.addBoth(callOut, ended)
1049
1050
1051class ProcessGroupLeaderMixin(ProcessProtocol):
1052 """Mix-in to ensure that the spawned process is a process group leader."""
1053
1054 def connectionMade(self):
1055 super().connectionMade()
1056 if self.transport.pid is not None:
1057 try:
1058 _os_setpgid(self.transport.pid, 0)
1059 except ProcessLookupError:
1060 pass # Already exited.

Subscribers

People subscribed via source and target branches

to all changes: