Merge lp:~allenap/maas/dns-timeout--bug-1672735 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: 5923
Proposed branch: lp:~allenap/maas/dns-timeout--bug-1672735
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 357 lines (+89/-23)
11 files modified
src/maasserver/listener.py (+2/-1)
src/maasserver/regiondservices/reverse_dns.py (+6/-4)
src/maasserver/regiondservices/tests/test_reverse_dns.py (+16/-0)
src/maasserver/utils/async.py (+1/-5)
src/provisioningserver/events.py (+2/-2)
src/provisioningserver/rpc/clusterservice.py (+2/-2)
src/provisioningserver/utils/services.py (+2/-2)
src/provisioningserver/utils/testing.py (+20/-0)
src/provisioningserver/utils/tests/test_network.py (+2/-5)
src/provisioningserver/utils/tests/test_twisted.py (+24/-2)
src/provisioningserver/utils/twisted.py (+12/-0)
To merge this branch: bzr merge lp:~allenap/maas/dns-timeout--bug-1672735
Reviewer Review Type Date Requested Status
Mike Pontillo (community) Approve
Review via email: mp+321825@code.launchpad.net

Commit message

Suppress/ignore DNS time-outs when consuming neighbour events.

This also creates a new suppress function that is used instead of Failure.trap to suppress exceptions in a Deferred callback chain, and adds a testing helper callWithRunningService to make testing with Twisted services a little easier.

Description of the change

This branch contains some other, related, changes:

- Makes the module-local `suppress` function into a shared util.
  Previously I had used Failure.trap, but this returns the unwrapped
  exception when it matches, and this is then passed to the next
  callback in a Deferred. I don't think this has caused any harm, but
  it's a loose end that /may/ conceivably have caused problems, and will
  inevitably cause confusion some day.

- Creates a testing helper, callWithRunningService, that starts a
  Twisted service, calls a function, then stops the service again (akin
  to wrapping it in a try...finally). The result of calling the function
  is returned as the result of the Deferred.

To post a comment you must log in.
Revision history for this message
Mike Pontillo (mpontillo) wrote :

Looks good; thanks for the fix.

Minor nit: I think some of your description deserves to be in the commit message.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/listener.py'
2--- src/maasserver/listener.py 2016-11-17 11:46:08 +0000
3+++ src/maasserver/listener.py 2017-04-05 08:55:59 +0000
4@@ -17,6 +17,7 @@
5 from provisioningserver.utils.enum import map_enum
6 from provisioningserver.utils.twisted import (
7 callOut,
8+ suppress,
9 synchronous,
10 )
11 from twisted.application.service import Service
12@@ -332,7 +333,7 @@
13 self.disconnecting.callback(None)
14 else:
15 # Still connecting: cancel before disconnect.
16- self.connecting.addErrback(Failure.trap, CancelledError)
17+ self.connecting.addErrback(suppress, CancelledError)
18 self.connecting.chainDeferred(self.disconnecting)
19 self.connecting.cancel()
20
21
22=== modified file 'src/maasserver/regiondservices/reverse_dns.py'
23--- src/maasserver/regiondservices/reverse_dns.py 2016-10-28 15:58:32 +0000
24+++ src/maasserver/regiondservices/reverse_dns.py 2017-04-05 08:55:59 +0000
25@@ -17,8 +17,9 @@
26 from maasserver.utils.threads import deferToDatabase
27 from provisioningserver.logger import LegacyLogger
28 from provisioningserver.utils.network import reverseResolve
29+from provisioningserver.utils.twisted import suppress
30 from twisted.application.service import Service
31-from twisted.internet.defer import inlineCallbacks
32+from twisted.internet import defer
33
34
35 log = LegacyLogger()
36@@ -34,7 +35,7 @@
37 # need to look it up every time a DNS entry changes.
38 self.region = None
39
40- @inlineCallbacks
41+ @defer.inlineCallbacks
42 def startService(self):
43 super().startService()
44 self.region = yield deferToDatabase(
45@@ -67,7 +68,7 @@
46 """
47 RDNS.objects.delete_current_entry(ip, self.region)
48
49- @inlineCallbacks
50+ @defer.inlineCallbacks
51 def consumeNeighbourEvent(self, action: str=None, cidr: str=None):
52 """Given an event from the postgresListener, resolve RDNS for an IP.
53
54@@ -85,7 +86,8 @@
55 # the same IP address, and because an IP address might repeatedly
56 # go back-and-forth between two MACs in the case of a duplicate IP
57 # address.
58- results = yield reverseResolve(ip)
59+ results = yield reverseResolve(ip).addErrback(
60+ suppress, defer.TimeoutError, instead=None)
61 if results is not None:
62 if len(results) > 0:
63 yield deferToDatabase(self.set_rdns_entry, ip, results)
64
65=== modified file 'src/maasserver/regiondservices/tests/test_reverse_dns.py'
66--- src/maasserver/regiondservices/tests/test_reverse_dns.py 2016-10-20 23:17:48 +0000
67+++ src/maasserver/regiondservices/tests/test_reverse_dns.py 2017-04-05 08:55:59 +0000
68@@ -15,6 +15,7 @@
69 from maasserver.testing.testcase import MAASTransactionServerTestCase
70 from maasserver.utils.threads import deferToDatabase
71 from maastesting.matchers import MockCalledOnceWith
72+from provisioningserver.utils.testing import callWithServiceRunning
73 from provisioningserver.utils.tests.test_network import (
74 TestReverseResolveMixIn,
75 )
76@@ -22,6 +23,7 @@
77 Equals,
78 Is,
79 )
80+from twisted.internet import defer
81 from twisted.internet.defer import inlineCallbacks
82
83
84@@ -109,3 +111,17 @@
85 self.assertThat(listener.unregister, MockCalledOnceWith(
86 'neighbour', service.consumeNeighbourEvent
87 ))
88+
89+ @wait_for(30)
90+ @inlineCallbacks
91+ def test__ignores_timeouts_when_consuming_neighbour_event(self):
92+ reverseResolve = self.patch(reverse_dns_module, "reverseResolve")
93+ reverseResolve.return_value = defer.fail(defer.TimeoutError())
94+ ip = factory.make_ip_address(ipv6=False)
95+ service = ReverseDNSService()
96+ yield callWithServiceRunning(
97+ service, service.consumeNeighbourEvent,
98+ "create", "%s/32" % ip)
99+ self.assertThat(reverseResolve, MockCalledOnceWith(ip))
100+ result = yield deferToDatabase(RDNS.objects.first)
101+ self.assertThat(result, Is(None))
102
103=== modified file 'src/maasserver/utils/async.py'
104--- src/maasserver/utils/async.py 2016-11-01 09:18:42 +0000
105+++ src/maasserver/utils/async.py 2017-04-05 08:55:59 +0000
106@@ -20,6 +20,7 @@
107 asynchronous,
108 FOREVER,
109 LONGTIME,
110+ suppress,
111 synchronous,
112 )
113 from twisted.internet import reactor
114@@ -160,11 +161,6 @@
115 return UseOnceIterator(queue.get, done)
116
117
118-def suppress(failure, *exceptions):
119- """Used as a errback, suppress the given exceptions."""
120- failure.trap(*exceptions)
121-
122-
123 class DeferredHooks(threading.local):
124 """A utility class for managing hooks that are specified as Deferreds.
125
126
127=== modified file 'src/provisioningserver/events.py'
128--- src/provisioningserver/events.py 2017-02-17 14:23:04 +0000
129+++ src/provisioningserver/events.py 2017-04-05 08:55:59 +0000
130@@ -39,12 +39,12 @@
131 callOut,
132 DeferredValue,
133 FOREVER,
134+ suppress,
135 )
136 from twisted.internet.defer import (
137 maybeDeferred,
138 succeed,
139 )
140-from twisted.python.failure import Failure
141
142
143 maaslog = get_maas_logger("events")
144@@ -464,7 +464,7 @@
145 # tracebacks telling us about it is not useful. Perhaps the region
146 # should store these logs anyway. Then, if and when the node is
147 # enlisted, logs prior to enlistment can be seen.
148- d.addErrback(Failure.trap, NoSuchNode)
149+ d.addErrback(suppress, NoSuchNode)
150
151 return d
152
153
154=== modified file 'src/provisioningserver/rpc/clusterservice.py'
155--- src/provisioningserver/rpc/clusterservice.py 2017-04-03 14:09:30 +0000
156+++ src/provisioningserver/rpc/clusterservice.py 2017-04-05 08:55:59 +0000
157@@ -100,6 +100,7 @@
158 deferred,
159 DeferredValue,
160 makeDeferredWithProcessProtocol,
161+ suppress,
162 )
163 from twisted import web
164 from twisted.application.internet import TimerService
165@@ -120,7 +121,6 @@
166 )
167 from twisted.internet.threads import deferToThread
168 from twisted.protocols import amp
169-from twisted.python.failure import Failure
170 from twisted.python.reflect import fullyQualifiedName
171 from twisted.web import http
172 import twisted.web.client
173@@ -651,7 +651,7 @@
174 executeScanNetworksSubprocess, scan_all=scan_all,
175 force_ping=force_ping, slow=slow, cidrs=cidrs, threads=threads,
176 interface=interface)
177- d.addErrback(Failure.trap, ProcessDone) # Exited normally.
178+ d.addErrback(suppress, ProcessDone) # Exited normally.
179 d.addErrback(log.err, 'Failed to scan all networks.')
180 d.addBoth(callOut, lock.release)
181 return {}
182
183=== modified file 'src/provisioningserver/utils/services.py'
184--- src/provisioningserver/utils/services.py 2017-03-24 15:28:07 +0000
185+++ src/provisioningserver/utils/services.py 2017-04-05 08:55:59 +0000
186@@ -32,6 +32,7 @@
187 from provisioningserver.utils.twisted import (
188 callOut,
189 deferred,
190+ suppress,
191 terminateProcess,
192 )
193 from twisted.application.internet import TimerService
194@@ -48,7 +49,6 @@
195 )
196 from twisted.internet.protocol import ProcessProtocol
197 from twisted.internet.threads import deferToThread
198-from twisted.python.failure import Failure
199
200
201 maaslog = get_maas_logger("networks.monitor")
202@@ -349,7 +349,7 @@
203 # During the update, we might fail to get the interface monitoring
204 # state from the region. We can safely ignore this, as it will be
205 # retried shortly.
206- d.addErrback(Failure.trap, NoConnectionsAvailable)
207+ d.addErrback(suppress, NoConnectionsAvailable)
208 d.addErrback(failed)
209 return d
210
211
212=== modified file 'src/provisioningserver/utils/testing.py'
213--- src/provisioningserver/utils/testing.py 2016-09-23 15:26:39 +0000
214+++ src/provisioningserver/utils/testing.py 2017-04-05 08:55:59 +0000
215@@ -4,6 +4,7 @@
216 """Testing helpers for provisioningserver.utils."""
217
218 __all__ = [
219+ "callWithServiceRunning",
220 "MAASIDFixture",
221 "RegistryFixture",
222 ]
223@@ -11,6 +12,11 @@
224 from fixtures import Fixture
225 from provisioningserver.utils import env
226 from provisioningserver.utils.registry import _registry
227+from provisioningserver.utils.twisted import (
228+ call,
229+ callOut,
230+)
231+from twisted.internet import defer
232
233
234 class RegistryFixture(Fixture):
235@@ -34,3 +40,17 @@
236 super(MAASIDFixture, self)._setUp()
237 self.addCleanup(env.set_maas_id, env.get_maas_id())
238 env.set_maas_id(self.system_id)
239+
240+
241+def callWithServiceRunning(service, f, *args, **kwargs):
242+ """Call `f` with `service` running.
243+
244+ The given service is a Twisted service. It is started, the given function
245+ called with the given arguments, then the service is stopped.
246+
247+ Returns a `Deferred`, firing with the result of the call to `f`.
248+ """
249+ d = defer.maybeDeferred(service.startService)
250+ d.addCallback(call, f, *args, **kwargs)
251+ d.addBoth(callOut, service.stopService)
252+ return d
253
254=== modified file 'src/provisioningserver/utils/tests/test_network.py'
255--- src/provisioningserver/utils/tests/test_network.py 2016-12-07 12:46:14 +0000
256+++ src/provisioningserver/utils/tests/test_network.py 2017-04-05 08:55:59 +0000
257@@ -1975,11 +1975,8 @@
258 data = (rrset,)
259 for hostname in hostnames:
260 rr = Mock()
261- rr.payload = Mock()
262- rr.payload.name = Mock()
263- rr.payload.name.name = Mock()
264- rr.payload.name.name.decode = Mock()
265- rr.payload.name.name.decode.return_value = hostname
266+ decode = rr.payload.name.name.decode
267+ decode.return_value = hostname
268 rrset.append(rr)
269 self.reply = data
270
271
272=== modified file 'src/provisioningserver/utils/tests/test_twisted.py'
273--- src/provisioningserver/utils/tests/test_twisted.py 2017-03-30 22:10:58 +0000
274+++ src/provisioningserver/utils/tests/test_twisted.py 2017-04-05 08:55:59 +0000
275@@ -66,6 +66,7 @@
276 reducedWebLogFormatter,
277 retries,
278 RPCFetcher,
279+ suppress,
280 synchronous,
281 terminateProcess,
282 ThreadPool,
283@@ -304,6 +305,27 @@
284 % (Something.__qualname__, a, b)))
285
286
287+class TestSuppress(MAASTestCase):
288+ """Tests for `suppress`."""
289+
290+ def test__suppresses_given_exception(self):
291+ error_type = factory.make_exception_type()
292+ failure = Failure(error_type())
293+ self.assertThat(suppress(failure, error_type), Is(None))
294+
295+ def test__does_not_suppress_other_exceptions(self):
296+ error_type = factory.make_exception_type()
297+ failure = Failure(factory.make_exception())
298+ self.assertThat(suppress(failure, error_type), Is(failure))
299+
300+ def test__returns_instead_value_when_suppressing(self):
301+ error_type = factory.make_exception_type()
302+ failure = Failure(error_type())
303+ self.assertThat(
304+ suppress(failure, error_type, instead=sentinel.instead),
305+ Is(sentinel.instead))
306+
307+
308 class TestRetries(MAASTestCase):
309
310 def assertRetry(
311@@ -1857,7 +1879,7 @@
312 pid = process.pid
313 # Terminate and wait for it to exit.
314 self.terminateSignalPrinter(process, protocol)
315- yield protocol.done.addErrback(Failure.trap, ProcessTerminated)
316+ yield protocol.done.addErrback(suppress, ProcessTerminated)
317 # os.kill was called once then os.killpg was called twice because the
318 # subprocess made itself a process group leader.
319 self.assertThat(
320@@ -1878,7 +1900,7 @@
321 pid = process.pid
322 # Terminate and wait for it to exit.
323 self.terminateSignalPrinter(process, protocol)
324- yield protocol.done.addErrback(Failure.trap, ProcessTerminated)
325+ yield protocol.done.addErrback(suppress, ProcessTerminated)
326 # os.kill was called 3 times because the subprocess did not make
327 # itself a process group leader.
328 self.assertThat(
329
330=== modified file 'src/provisioningserver/utils/twisted.py'
331--- src/provisioningserver/utils/twisted.py 2017-03-30 22:10:58 +0000
332+++ src/provisioningserver/utils/twisted.py 2017-04-05 08:55:59 +0000
333@@ -22,6 +22,7 @@
334 'pause',
335 'reducedWebLogFormatter',
336 'retries',
337+ 'suppress',
338 'synchronous',
339 'ThreadPool',
340 'ThreadPoolLimiter',
341@@ -246,6 +247,17 @@
342 return wrapper
343
344
345+def suppress(failure, *exceptions, instead=None):
346+ """Used as a errback, suppress the given exceptions.
347+
348+ Returns the given `instead` value... instead.
349+ """
350+ if failure.check(*exceptions) is None:
351+ return failure
352+ else:
353+ return instead
354+
355+
356 def retries(timeout=30, intervals=1, clock=reactor):
357 """Helper for retrying something, sleeping between attempts.
358