Merge ~adam-collard/maas:flaky-lease-socket-service-fix into maas:master

Proposed by Adam Collard
Status: Merged
Approved by: Adam Collard
Approved revision: 5ab1a6674dde7bda111167ca95f879087da0a057
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~adam-collard/maas:flaky-lease-socket-service-fix
Merge into: maas:master
Diff against target: 370 lines (+110/-127)
3 files modified
src/provisioningserver/dhcp/tests/test_helper_notify.py (+40/-42)
src/provisioningserver/rackdservices/testing.py (+35/-0)
src/provisioningserver/rackdservices/tests/test_lease_socket_service.py (+35/-85)
Reviewer Review Type Date Requested Status
MAAS Lander Approve
Christian Grabowski Approve
Review via email: mp+446793@code.launchpad.net

Commit message

fix(lease_service): flaky tests

Introduce configure_lease_service_for_one_shot helper coroutine

Refactor test_helper_notify and test_lease_socket_service to use the
helper which guarantees that we don't lose a race with the LoopingTask
at the heart of the LeaseSocketService

Fixes LP:2026360 and LP:2019216

To post a comment you must log in.
Revision history for this message
Christian Grabowski (cgrabowski) wrote :

+1

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b flaky-lease-socket-service-fix lp:~adam-collard/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 5ab1a6674dde7bda111167ca95f879087da0a057

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/provisioningserver/dhcp/tests/test_helper_notify.py b/src/provisioningserver/dhcp/tests/test_helper_notify.py
2index d5ef091..b00f4f4 100644
3--- a/src/provisioningserver/dhcp/tests/test_helper_notify.py
4+++ b/src/provisioningserver/dhcp/tests/test_helper_notify.py
5@@ -4,22 +4,21 @@
6 """Tests for maas-dhcp-support notify command."""
7
8
9+from functools import partial
10 import os
11 import random
12 from unittest.mock import sentinel
13
14-from testtools.content import text_content
15 from twisted.internet import defer, reactor
16
17 from maastesting import dev_root, get_testing_timeout
18 from maastesting.factory import factory
19 from maastesting.testcase import MAASTestCase, MAASTwistedRunTest
20 from provisioningserver.rackdservices import lease_socket_service
21-from provisioningserver.rackdservices.lease_socket_service import (
22- LeaseSocketService,
23+from provisioningserver.rackdservices.testing import (
24+ configure_lease_service_for_one_shot,
25 )
26 from provisioningserver.utils.shell import call_and_check
27-from provisioningserver.utils.twisted import DeferredValue
28
29 TIMEOUT = get_testing_timeout()
30
31@@ -37,15 +36,13 @@ class TestDHCPNotify(MAASTestCase):
32
33 def catch_packet_on_socket(self):
34 socket_path = self.patch_socket_path()
35- service = LeaseSocketService(sentinel.service, reactor)
36- dv = DeferredValue()
37-
38- def mock_processNotification(*args, **kwargs):
39- dv.set(args)
40-
41- self.patch(service, "processNotification", mock_processNotification)
42-
43- return socket_path, service, dv
44+ service = lease_socket_service.LeaseSocketService(
45+ sentinel.service, reactor
46+ )
47+ helper = configure_lease_service_for_one_shot(self, service)
48+ # Get to the point of the done callback
49+ next(helper)
50+ return socket_path, service, helper
51
52 @defer.inlineCallbacks
53 def test_sends_notification_over_socket_for_processing(self):
54@@ -56,36 +53,37 @@ class TestDHCPNotify(MAASTestCase):
55 lease_time = random.randint(30, 1000)
56 hostname = factory.make_name("host")
57
58- socket_path, service, done = self.catch_packet_on_socket()
59- service.startService()
60- self.addCleanup(service.stopService)
61- cmd = [
62- f"{dev_root}/package-files/usr/sbin/maas-dhcp-helper",
63- "notify",
64- "--action",
65- action,
66- "--mac",
67- mac,
68- "--ip-family",
69- ip_family,
70- "--ip",
71- ip,
72- "--lease-time",
73- str(lease_time),
74- "--hostname",
75- hostname,
76- "--socket",
77- socket_path,
78- ]
79- self.addDetail("cmd", text_content(repr(cmd)))
80- call_and_check(cmd)
81- yield done.get(timeout=TIMEOUT)
82-
83- self.addDetail(
84- "notifications", text_content(repr(service.notifications))
85+ socket_path, service, helper = self.catch_packet_on_socket()
86+
87+ send_notification = partial(
88+ call_and_check,
89+ [
90+ f"{dev_root}/package-files/usr/sbin/maas-dhcp-helper",
91+ "notify",
92+ "--action",
93+ action,
94+ "--mac",
95+ mac,
96+ "--ip-family",
97+ ip_family,
98+ "--ip",
99+ ip,
100+ "--lease-time",
101+ str(lease_time),
102+ "--hostname",
103+ hostname,
104+ "--socket",
105+ socket_path,
106+ ],
107 )
108- self.assertEqual(1, len(done.value[0]["updates"]))
109- update = done.value[0]["updates"][0]
110+
111+ # Wait for the service to be done
112+ yield next(helper)
113+
114+ helper.send(send_notification)
115+ notifications = yield from helper
116+ self.assertEqual(len(notifications), 1)
117+ update = notifications[0]
118 self.assertEqual(action, update["action"])
119 self.assertEqual(mac, update["mac"])
120 self.assertEqual(ip_family, update["ip_family"])
121diff --git a/src/provisioningserver/rackdservices/testing.py b/src/provisioningserver/rackdservices/testing.py
122index 1a5881f..52b4bd7 100644
123--- a/src/provisioningserver/rackdservices/testing.py
124+++ b/src/provisioningserver/rackdservices/testing.py
125@@ -3,12 +3,14 @@
126
127 """Testing resources for `provisioningserver.rackdservices`."""
128
129+from unittest.mock import MagicMock
130
131 from maastesting.factory import factory
132 from maastesting.twisted import always_succeed_with
133 from provisioningserver import services
134 from provisioningserver.rpc import region
135 from provisioningserver.rpc.testing import MockLiveClusterToRegionRPCFixture
136+from provisioningserver.utils.twisted import pause, retries
137
138
139 def prepareRegionForGetControllerType(test, is_region=False, is_rack=True):
140@@ -33,3 +35,36 @@ def prepareRegionForGetControllerType(test, is_region=False, is_rack=True):
141 return services.getServiceNamed("rpc"), protocol
142
143 return connecting.addCallback(connected)
144+
145+
146+def configure_lease_service_for_one_shot(testcase, service):
147+ """Helper for getting notifications out of a LeaseSocketService.
148+
149+ I'm a coroutine that works alongside an @inlineCallbacks test.
150+
151+ ```
152+ coroutine = configure_lease_service_for_one_shot(self, service)
153+
154+ next(coroutine) # First one initialises
155+ yield next(coroutine) # This one is for putting the service.done in the callback chain
156+ notifications = yield from coroutine
157+ assert notifications is not None
158+ ```
159+ """
160+ service.startService()
161+ testcase.addCleanup(service.stopService)
162+ # Stop the looping call to check that the notification gets added
163+ # to notifications.
164+ process_done = service.done
165+ service.processor.stop()
166+ yield process_done
167+ service.processor = MagicMock()
168+ create_notification = yield
169+ create_notification()
170+ # Loop until the notifications has a notification.
171+ for elapsed, remaining, wait in retries(5, 0.1, service.reactor):
172+ if len(service.notifications) > 0:
173+ return service.notifications
174+ else:
175+ yield pause(wait, service.reactor)
176+ assert False, "No notifications found"
177diff --git a/src/provisioningserver/rackdservices/tests/test_lease_socket_service.py b/src/provisioningserver/rackdservices/tests/test_lease_socket_service.py
178index 4433884..309e551 100644
179--- a/src/provisioningserver/rackdservices/tests/test_lease_socket_service.py
180+++ b/src/provisioningserver/rackdservices/tests/test_lease_socket_service.py
181@@ -4,6 +4,7 @@
182 """Tests for src/provisioningserver/rackdservices/lease_socket_service.py"""
183
184
185+from functools import partial
186 import json
187 import os
188 import socket
189@@ -14,20 +15,20 @@ from testtools.matchers import Not, PathExists
190 from twisted.application.service import Service
191 from twisted.internet import defer, reactor
192 from twisted.internet.protocol import DatagramProtocol
193-from twisted.internet.threads import deferToThread
194
195 from maastesting import get_testing_timeout
196 from maastesting.factory import factory
197-from maastesting.matchers import MockCalledOnceWith
198 from maastesting.testcase import MAASTestCase, MAASTwistedRunTest
199 from provisioningserver.rackdservices import lease_socket_service
200 from provisioningserver.rackdservices.lease_socket_service import (
201 LeaseSocketService,
202 )
203+from provisioningserver.rackdservices.testing import (
204+ configure_lease_service_for_one_shot,
205+)
206 from provisioningserver.rpc import clusterservice, getRegionClient
207 from provisioningserver.rpc.region import UpdateLeases
208 from provisioningserver.rpc.testing import MockLiveClusterToRegionRPCFixture
209-from provisioningserver.utils.twisted import DeferredValue, pause, retries
210
211
212 class TestLeaseSocketService(MAASTestCase):
213@@ -99,82 +100,50 @@ class TestLeaseSocketService(MAASTestCase):
214 def test_notification_gets_added_to_notifications(self):
215 socket_path = self.patch_socket_path()
216 service = LeaseSocketService(sentinel.service, reactor)
217- service.startService()
218- self.addCleanup(service.stopService)
219
220- # Stop the looping call to check that the notification gets added
221- # to notifications.
222- process_done = service.done
223- service.processor.stop()
224- yield process_done
225- service.processor = MagicMock()
226-
227- # Create test payload to send.
228 packet = self.create_lease_notification()
229+ helper = configure_lease_service_for_one_shot(self, service)
230+ next(helper)
231+ yield next(helper)
232+ helper.send(partial(self.send_notification, socket_path, packet))
233
234- # Send notification to the socket should appear in notifications.
235- yield deferToThread(self.send_notification, socket_path, packet)
236-
237- # Loop until the notifications has a notification.
238- for elapsed, remaining, wait in retries(5, 0.1, reactor):
239- if len(service.notifications) > 0:
240- break
241- else:
242- yield pause(wait, reactor)
243+ notifications = yield from helper
244
245- # Should have one notitication.
246- self.assertEqual([packet], list(service.notifications))
247+ # Should have one notification.
248+ self.assertItemsEqual([packet], notifications)
249
250 @defer.inlineCallbacks
251 def test_processNotification_gets_called_with_notification(self):
252 socket_path = self.patch_socket_path()
253 service = LeaseSocketService(sentinel.service, reactor)
254- dv = DeferredValue()
255
256- # Mock processNotifcation to catch the call.
257- def mock_processNotification(*args, **kwargs):
258- dv.set(args)
259-
260- self.patch(service, "processNotification", mock_processNotification)
261-
262- # Start the service and stop it at the end of the test.
263- service.startService()
264- self.addCleanup(service.stopService)
265+ helper = configure_lease_service_for_one_shot(self, service)
266+ next(helper)
267+ yield next(helper)
268
269 # Create test payload to send.
270 packet1 = self.create_lease_notification()
271 packet2 = self.create_lease_notification()
272- payload = {
273- "cluster_uuid": None,
274- "updates": [packet1, packet2],
275- }
276
277 # Send notification to the socket and wait for notification.
278- yield deferToThread(self.send_notification, socket_path, packet1)
279- yield deferToThread(self.send_notification, socket_path, packet2)
280- yield dv.get(timeout=10)
281+ def _send_notifications():
282+ self.send_notification(socket_path, packet1)
283+ self.send_notification(socket_path, packet2)
284+
285+ helper.send(_send_notifications)
286+
287+ notifications = yield from helper
288
289- # Payload should be the argument passed to processNotifcation
290- self.assertEqual((payload,), dv.value)
291+ self.assertItemsEqual([packet1, packet2], notifications)
292
293 @defer.inlineCallbacks
294 def test_processNotification_dont_allow_same_address(self):
295 socket_path = self.patch_socket_path()
296 service = LeaseSocketService(sentinel.service, reactor)
297- dvs = [DeferredValue(), DeferredValue()]
298
299- # Mock processNotifcation to catch the call.
300- def mock_processNotification(*args, **kwargs):
301- for dv in dvs:
302- if not dv.isSet:
303- dv.set(args)
304- break
305-
306- self.patch(service, "processNotification", mock_processNotification)
307-
308- # Start the service and stop it at the end of the test.
309- service.startService()
310- self.addCleanup(service.stopService)
311+ helper = configure_lease_service_for_one_shot(self, service)
312+ next(helper)
313+ yield next(helper)
314
315 # Create test payload to send.
316 ip = factory.make_ipv4_address()
317@@ -182,31 +151,17 @@ class TestLeaseSocketService(MAASTestCase):
318 packet2 = self.create_lease_notification(ip=ip)
319
320 # Send notifications to the socket and wait for notifications.
321- yield deferToThread(self.send_notification, socket_path, packet1)
322- yield deferToThread(self.send_notification, socket_path, packet2)
323- yield dvs[0].get(timeout=10)
324- yield dvs[1].get(timeout=10)
325+ def _send_notifications():
326+ self.send_notification(socket_path, packet1)
327+ self.send_notification(socket_path, packet2)
328+
329+ helper.send(_send_notifications)
330+
331+ notifications = yield from helper
332
333 # Packet should be the argument passed to processNotification in
334 # order.
335- self.assertEqual(
336- (
337- {
338- "cluster_uuid": None,
339- "updates": [packet1],
340- },
341- ),
342- dvs[0].value,
343- )
344- self.assertEqual(
345- (
346- {
347- "cluster_uuid": None,
348- "updates": [packet2],
349- },
350- ),
351- dvs[1].value,
352- )
353+ self.assertEqual([packet1, packet2], list(notifications))
354
355 @defer.inlineCallbacks
356 def test_processNotification_send_to_region(self):
357@@ -225,11 +180,6 @@ class TestLeaseSocketService(MAASTestCase):
358 "updates": [packet],
359 }
360 yield service.processNotification(payload, clock=reactor)
361- self.assertThat(
362- protocol.UpdateLeases,
363- MockCalledOnceWith(
364- protocol,
365- cluster_uuid=client.localIdent,
366- updates=[packet],
367- ),
368+ protocol.UpdateLeases.assert_called_once_with(
369+ protocol, cluster_uuid=client.localIdent, updates=[packet]
370 )

Subscribers

People subscribed via source and target branches