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
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-29 13:03:13 +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-29 13:03:13 +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-29 13:03:13 +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-29 13:03:13 +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-29 13:03:13 +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-29 13:03:13 +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-29 13:03:13 +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.

Subscribers

People subscribed via source and target branches

to all changes: