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

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 5866
Proposed branch: lp:~allenap/maas/process-group-leader-race
Merge into: lp:~maas-committers/maas/trunk
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
Reviewer Review Type Date Requested Status
Mike Pontillo (community) Approve
Review via email: mp+320945@code.launchpad.net

Commit message

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
Mike Pontillo (mpontillo) wrote :

Looks good; thanks for the fix.

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (1.8 MiB)

The attempt to merge lp:~allenap/maas/process-group-leader-race into lp:maas failed. Below is the output from the failed tests.

Hit:1 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial InRelease
Get:2 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates InRelease [102 kB]
Get:3 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-backports InRelease [102 kB]
Get:4 http://security.ubuntu.com/ubuntu xenial-security InRelease [102 kB]
Fetched 306 kB in 0s (617 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
    --no-install-recommends install apache2 archdetect-deb authbind avahi-utils bash bind9 bind9utils build-essential bzr bzr-builddeb chromium-browser chromium-chromedriver curl daemontools debhelper dh-apport dh-systemd distro-info dnsutils firefox freeipmi-tools git gjs ipython isc-dhcp-common isc-dhcp-server libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libnss-wrapper libpq-dev make nodejs-legacy npm postgresql psmisc pxelinux python3-all python3-apt python3-attr python3-bson python3-convoy python3-crochet python3-cssselect python3-curtin python3-dev python3-distro-info python3-django python3-django-nose python3-django-piston3 python3-dnspython python3-docutils python3-formencode python3-hivex python3-httplib2 python3-jinja2 python3-jsonschema python3-lxml python3-netaddr python3-netifaces python3-novaclient python3-oauth python3-oauthlib python3-openssl python3-paramiko python3-petname python3-pexpect python3-psycopg2 python3-pyinotify python3-pyparsing python3-pyvmomi python3-requests python3-seamicroclient python3-setuptools python3-simplestreams python3-sphinx python3-tempita python3-twisted python3-txtftp python3-tz python3-yaml python3-zope.interface python-bson python-crochet python-django python-django-piston python-djorm-ext-pgarray python-formencode python-lxml python-netaddr python-netifaces python-pocket-lint python-psycopg2 python-simplejson python-tempita python-twisted python-yaml socat syslinux-common tgt ubuntu-cloudimage-keyring wget xvfb
Reading package lists...
Building dependency tree...
Reading state information...
authbind is already the newest version (2.1.1+nmu1).
avahi-utils is already the newest version (0.6.32~rc+dfsg-1ubuntu2).
build-essential is already the newest version (12.1ubuntu2).
debhelper is already the newest version (9.20160115ubuntu3).
distro-info is already the newest version (0.14build1).
git is already the newest version (1:2.7.4-0ubuntu1).
libjs-angularjs is already the newest version (1.2.28-1ubuntu2).
libjs-jquery is already the newest version (1.11.3+dfsg-4).
libjs-yui3-full is already the newest version (3.5.1-1ubuntu3).
libjs-yui3-min is already the newest version (3.5.1-1ubuntu3).
make is already the newest version (4.1-6).
postgresql is already the newest version (9.5+173).
psmisc is already the newest version (22.21-2.1build1).
pxelinux is already the newest version (3:6.03+dfsg-11ubuntu1).
python-formencode is already the newest version (1.3.0-0ubuntu5).
python-lxml is already the newest version (3.5.0-1build1).
python-netaddr is already the newest version (0.7.18-1).
python-n...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/provisioningserver/utils/arp.py'
2--- src/provisioningserver/utils/arp.py 2016-09-27 11:56:15 +0000
3+++ src/provisioningserver/utils/arp.py 2017-03-24 15:31:21 +0000
4@@ -370,6 +370,11 @@
5 def run(args, output=sys.stdout, stdin=sys.stdin,
6 stdin_buffer=sys.stdin.buffer):
7 """Observe an Ethernet interface and print ARP bindings."""
8+
9+ # First, become a progress group leader, so that signals can be directed
10+ # to this process and its children; see p.u.twisted.terminateProcess.
11+ os.setpgrp()
12+
13 network_monitor = None
14 if args.input_file is None:
15 if args.interface is None:
16
17=== modified file 'src/provisioningserver/utils/avahi.py'
18--- src/provisioningserver/utils/avahi.py 2016-09-21 15:25:11 +0000
19+++ src/provisioningserver/utils/avahi.py 2017-03-24 15:31:21 +0000
20@@ -38,6 +38,7 @@
21 from contextlib import contextmanager
22 import io
23 import json
24+import os
25 import re
26 import subprocess
27 import sys
28@@ -203,6 +204,11 @@
29
30 def run(args, output=sys.stdout, stdin=sys.stdin):
31 """Observe an Ethernet interface and print ARP bindings."""
32+
33+ # First, become a progress group leader, so that signals can be directed
34+ # to this process and its children; see p.u.twisted.terminateProcess.
35+ os.setpgrp()
36+
37 if args.input_file is None:
38 reader = _reader_from_avahi()
39 elif args.input_file == "-":
40
41=== modified file 'src/provisioningserver/utils/services.py'
42--- src/provisioningserver/utils/services.py 2016-10-28 15:58:32 +0000
43+++ src/provisioningserver/utils/services.py 2017-03-24 15:31:21 +0000
44@@ -32,7 +32,6 @@
45 from provisioningserver.utils.twisted import (
46 callOut,
47 deferred,
48- ProcessGroupLeaderMixin,
49 terminateProcess,
50 )
51 from twisted.application.internet import TimerService
52@@ -124,8 +123,7 @@
53 self.done.errback(reason)
54
55
56-class ProtocolForObserveARP(
57- ProcessGroupLeaderMixin, JSONPerLineProtocol):
58+class ProtocolForObserveARP(JSONPerLineProtocol):
59 """Protocol used when spawning `maas-rack observe-arp`.
60
61 The difference between `JSONPerLineProtocol` and `ProtocolForObserveARP`
62@@ -149,8 +147,7 @@
63 log.msg("observe-arp[%s]:" % self.interface, line)
64
65
66-class ProtocolForObserveMDNS(
67- ProcessGroupLeaderMixin, JSONPerLineProtocol):
68+class ProtocolForObserveMDNS(JSONPerLineProtocol):
69 """Protocol used when spawning `maas-rack observe-mdns`.
70
71 This ensures that the spawned process is configured as a process group
72
73=== modified file 'src/provisioningserver/utils/tests/test_arp.py'
74--- src/provisioningserver/utils/tests/test_arp.py 2016-08-25 16:10:40 +0000
75+++ src/provisioningserver/utils/tests/test_arp.py 2017-03-24 15:31:21 +0000
76@@ -14,6 +14,8 @@
77 import time
78 from unittest.mock import Mock
79
80+from maastesting.factory import factory
81+from maastesting.matchers import MockCalledOnceWith
82 from maastesting.testcase import MAASTestCase
83 from netaddr import (
84 EUI,
85@@ -509,3 +511,10 @@
86 with ExpectedException(
87 SystemExit, '.*42.*'):
88 run(args, output=output)
89+
90+ def test__sets_self_as_process_group_leader(self):
91+ exception_type = factory.make_exception_type()
92+ os = self.patch(arp_module, "os")
93+ os.setpgrp.side_effect = exception_type
94+ self.assertRaises(exception_type, run, [])
95+ self.assertThat(os.setpgrp, MockCalledOnceWith())
96
97=== modified file 'src/provisioningserver/utils/tests/test_avahi.py'
98--- src/provisioningserver/utils/tests/test_avahi.py 2016-09-13 17:15:27 +0000
99+++ src/provisioningserver/utils/tests/test_avahi.py 2017-03-24 15:31:21 +0000
100@@ -12,6 +12,8 @@
101 from tempfile import NamedTemporaryFile
102 import time
103
104+from maastesting.factory import factory
105+from maastesting.matchers import MockCalledOnceWith
106 from maastesting.testcase import MAASTestCase
107 from provisioningserver.utils import avahi as avahi_module
108 from provisioningserver.utils.avahi import (
109@@ -279,3 +281,10 @@
110 output = io.StringIO()
111 with ExpectedException(SystemExit, '.*42.*'):
112 run(args, output=output)
113+
114+ def test__sets_self_as_process_group_leader(self):
115+ exception_type = factory.make_exception_type()
116+ os = self.patch(avahi_module, "os")
117+ os.setpgrp.side_effect = exception_type
118+ self.assertRaises(exception_type, run, [])
119+ self.assertThat(os.setpgrp, MockCalledOnceWith())
120
121=== modified file 'src/provisioningserver/utils/tests/test_twisted.py'
122--- src/provisioningserver/utils/tests/test_twisted.py 2016-10-24 14:36:30 +0000
123+++ src/provisioningserver/utils/tests/test_twisted.py 2017-03-24 15:31:21 +0000
124@@ -14,7 +14,6 @@
125 from random import (
126 randint,
127 random,
128- randrange,
129 )
130 import re
131 import signal
132@@ -64,7 +63,6 @@
133 LONGTIME,
134 makeDeferredWithProcessProtocol,
135 pause,
136- ProcessGroupLeaderMixin,
137 retries,
138 RPCFetcher,
139 synchronous,
140@@ -1723,6 +1721,30 @@
141
142 # A script that prints the signals it receives, as long as they're SIGTERM or
143 # SIGQUIT. It prints "Ready." on stderr once it's registered signal handlers.
144+
145+# This sets itself as process group leader.
146+signal_printer_pgl = """\
147+from os import setpgrp
148+from signal import Signals, signal
149+from sys import stderr
150+from time import sleep
151+from traceback import print_exc
152+
153+def print_signal(signum, frame):
154+ print(Signals(signum).name, flush=True)
155+
156+signal(Signals.SIGTERM, print_signal)
157+signal(Signals.SIGQUIT, print_signal)
158+
159+setpgrp()
160+
161+print("Ready.", file=stderr, flush=True)
162+
163+sleep(5.0)
164+"""
165+
166+# This variant should be identical, except that it does
167+# not set itself as process group leader.
168 signal_printer = """\
169 from signal import Signals, signal
170 from sys import stderr
171@@ -1763,14 +1785,6 @@
172 self.done.callback(reason)
173
174
175-class SignalPrinterProtocolAsGroupLeader(
176- ProcessGroupLeaderMixin, SignalPrinterProtocol):
177- """Process protocol for use with `signal_printer` that ...
178-
179- ...configures itself as a process group leader.
180- """
181-
182-
183 class TestTerminateProcess(MAASTestCase):
184 """Tests for `terminateProcess`."""
185
186@@ -1778,7 +1792,6 @@
187
188 def setUp(self):
189 super(TestTerminateProcess, self).setUp()
190- self.sigprint = self.make_file("sigprint.py", signal_printer)
191 # Allow spying on calls to os.kill and os.killpg by terminateProcess.
192 self.assertThat(twisted_module._os_kill, Is(os.kill))
193 self.patch(
194@@ -1789,13 +1802,16 @@
195 twisted_module, "_os_killpg",
196 Mock(wraps=twisted_module._os_killpg))
197
198- def startSignalPrinter(self, protocol):
199+ def startSignalPrinter(self, protocol, *, pgrp=False):
200+ script_filename = self.make_file(
201+ "sigprint.py", signal_printer_pgl if pgrp else signal_printer)
202+
203 self.assertThat(protocol, IsInstance(SignalPrinterProtocol))
204 self.addDetail("out", content_from_stream(protocol.out, seek_offset=0))
205 self.addDetail("err", content_from_stream(protocol.err, seek_offset=0))
206 python = sys.executable.encode("utf-8")
207 process = reactor.spawnProcess(
208- protocol, python, (python, self.sigprint.encode("utf-8")))
209+ protocol, python, (python, script_filename.encode("utf-8")))
210
211 # Wait for the spawned subprocess to tell us that it's ready.
212 def cbReady(message):
213@@ -1829,8 +1845,8 @@
214
215 @inlineCallbacks
216 def test__terminates_with_kill_and_killpg(self):
217- protocol = SignalPrinterProtocolAsGroupLeader()
218- process = yield self.startSignalPrinter(protocol)
219+ protocol = SignalPrinterProtocol()
220+ process = yield self.startSignalPrinter(protocol, pgrp=True)
221 # Capture the pid now; it gets cleared when the process exits.
222 pid = process.pid
223 # Terminate and wait for it to exit.
224@@ -1851,7 +1867,7 @@
225 @inlineCallbacks
226 def test__terminates_with_kill_if_not_in_separate_process_group(self):
227 protocol = SignalPrinterProtocol()
228- process = yield self.startSignalPrinter(protocol)
229+ process = yield self.startSignalPrinter(protocol, pgrp=False)
230 # Capture the pid now; it gets cleared when the process exits.
231 pid = process.pid
232 # Terminate and wait for it to exit.
233@@ -1867,15 +1883,3 @@
234 ))
235 self.assertThat(
236 twisted_module._os_killpg, MockNotCalled())
237-
238-
239-class TestProcessGroupLeaderMixin(MAASTestCase):
240- """Tests for `ProcessGroupLeaderMixin`."""
241-
242- def test__calls_setpgid_on_child_process(self):
243- self.assertThat(twisted_module._os_setpgid, Is(os.setpgid))
244- setpgid = self.patch(twisted_module, "_os_setpgid")
245- protocol = ProcessGroupLeaderMixin()
246- transport = Mock(pid=randrange(99999, 9999999))
247- protocol.makeConnection(transport)
248- self.assertThat(setpgid, MockCalledOnceWith(transport.pid, 0))
249
250=== modified file 'src/provisioningserver/utils/twisted.py'
251--- src/provisioningserver/utils/twisted.py 2016-10-28 15:58:32 +0000
252+++ src/provisioningserver/utils/twisted.py 2017-03-24 15:31:21 +0000
253@@ -20,7 +20,6 @@
254 'LONGTIME',
255 'makeDeferredWithProcessProtocol',
256 'pause',
257- 'ProcessGroupLeaderMixin',
258 'retries',
259 'synchronous',
260 'ThreadPool',
261@@ -46,7 +45,6 @@
262 from os import (
263 kill as _os_kill,
264 killpg as _os_killpg,
265- setpgid as _os_setpgid,
266 )
267 import signal
268 import threading
269@@ -1003,9 +1001,8 @@
270 Steps #3 and #5 have a safeguard: if the process identified by `pid` has
271 the same process group as the invoking process the signal is sent only to
272 the process and not to the process group. This prevents the caller from
273- inadvertently killing itself. You may want to use this in conjunction with
274- `ProcessGroupLeaderMixin` to ensure that the child process becomes a
275- process group leader soon after spawning.
276+ inadvertently killing itself. For best effect, ensure that new processes
277+ become process group leaders soon after spawning.
278
279 :param pid: The PID to terminate.
280 :param done: A `Deferred` that fires when the process exits.
281@@ -1046,15 +1043,3 @@
282 killer.cancel()
283
284 done.addBoth(callOut, ended)
285-
286-
287-class ProcessGroupLeaderMixin(ProcessProtocol):
288- """Mix-in to ensure that the spawned process is a process group leader."""
289-
290- def connectionMade(self):
291- super().connectionMade()
292- if self.transport.pid is not None:
293- try:
294- _os_setpgid(self.transport.pid, 0)
295- except ProcessLookupError:
296- pass # Already exited.