Merge lp:~free.ekanayaka/landscape-client/amp-cleanup-9 into lp:~landscape/landscape-client/trunk

Proposed by Free Ekanayaka
Status: Merged
Approved by: Free Ekanayaka
Approved revision: 674
Merged at revision: 672
Proposed branch: lp:~free.ekanayaka/landscape-client/amp-cleanup-9
Merge into: lp:~landscape/landscape-client/trunk
Diff against target: 535 lines (+190/-194)
2 files modified
landscape/lib/amp.py (+11/-60)
landscape/lib/tests/test_amp.py (+179/-134)
To merge this branch: bzr merge lp:~free.ekanayaka/landscape-client/amp-cleanup-9
Reviewer Review Type Date Requested Status
Christopher Armstrong (community) Approve
Chris Glass (community) Approve
Review via email: mp+162642@code.launchpad.net

Commit message

Alright, one of the last episodes of the AMP cleanup saga. In this branch:

- Drop landscape.lib.amp.RemoteObjectConnector, as most of its functionality has no been pushed down to MethodCallClientFactory which is a more standard Twisted primitive more importantly and is transport-agnostic, making landscape.lib.amp really generic.

- Convert landscape.lib.tests.test_amp.RemoteObjectConnectorTest to MethodCallFunctionalTest. The RemoteObjectConnectorTest class was exercising the (now gone) RemoteObjectConnector in a functional-like way, that means spinning the real twisted reactor and listening/connecting to real Unix sockets. Even if from a strict unit-test point of view all the functionality in landscape.lib.amp is already tested, I felt it was nice to still have some functional-like coverage mainly to protect us against regressions or API changes in the Twisted machinery that we depend on. I've used @inlineCallbacks to make tests a bit more readable.

Description of the change

Alright, one of the last episodes of the AMP cleanup saga. In this branch:

- Drop landscape.lib.amp.RemoteObjectConnector, as most of its functionality has no been pushed down to MethodCallClientFactory which is a more standard Twisted primitive more importantly and is transport-agnostic, making landscape.lib.amp really generic.

- Convert landscape.lib.tests.test_amp.RemoteObjectConnectorTest to MethodCallFunctionalTest. The RemoteObjectConnectorTest class was exercising the (now gone) RemoteObjectConnector in a functional-like way, that means spinning the real twisted reactor and listening/connecting to real Unix sockets. Even if from strict unit-test point of view all the functionality in landscape.lib.amp is already tested, I felt it was nice to still have some functional-like coverage mainly to protect us against regressions or API changes in the Twisted machinery that we depend on. I've used @inlineCallbacks to make tests a bit more readable.

To post a comment you must log in.
Revision history for this message
Chris Glass (tribaal) wrote :

Great branch! Thanks a lot for all this refactoring, the code is much, much cleaner and easier to read now. +1
Man, inline callbacks + yield is so much nicer :)

[1]
While not directly related to your branch, could you please rename the "object" variable to "obj" or something in MethodCallReceiver's __init__()? It's not hugely important in this particular case, but overriding builtins even for a limited scope is a bit ugly :)

[2]
Same comment in MethodCallServerProtocol.

[3]
Same comment in MethodCallServerFactory's __init__

review: Approve
Revision history for this message
Christopher Armstrong (radix) wrote :

Nothing at all offensive about this branch. I like that almost all of the points are "remove foo" :-) +1

review: Approve
674. By Free Ekanayaka

Rename object to obj

Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

Thanks! [1] to [3] fixed.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'landscape/lib/amp.py'
2--- landscape/lib/amp.py 2013-05-06 18:15:21 +0000
3+++ landscape/lib/amp.py 2013-05-07 15:57:26 +0000
4@@ -3,7 +3,7 @@
5 This module implements an AMP-based protocol for performing remote procedure
6 calls in a convenient and easy way. It's conceptually similar to DBus in that
7 it supports exposing a Python object to a remote process, with communication
8-happening over plain Unix domain sockets.
9+happening over any Twisted-supported transport, e.g. Unix domain sockets.
10
11 For example let's say we have a Python process "A" that creates an instance of
12 this class::
13@@ -35,8 +35,8 @@
14 factory.getRemoteObject().addCallback(got_remote)
15
16 Note that when invoking a method via the remote proxy, the parameters
17-are required to be serializable with bpickle, so that they can be sent
18-over the wire.
19+are required to be serializable with bpickle, so they can be sent over
20+the wire.
21
22 See also::
23
24@@ -130,14 +130,14 @@
25 class MethodCallReceiver(CommandLocator):
26 """Expose methods of a local object over AMP.
27
28- @param object: The Python object to be exposed.
29+ @param obj: The Python object to be exposed.
30 @param methods: The list of the object's methods that can be called
31 remotely.
32 """
33
34- def __init__(self, object, methods):
35+ def __init__(self, obj, methods):
36 CommandLocator.__init__(self)
37- self._object = object
38+ self._object = obj
39 self._methods = methods
40 self._pending_chunks = {}
41
42@@ -293,8 +293,8 @@
43 class MethodCallServerProtocol(AMP):
44 """Receive L{MethodCall} commands over the wire and send back results."""
45
46- def __init__(self, object, methods):
47- AMP.__init__(self, locator=MethodCallReceiver(object, methods))
48+ def __init__(self, obj, methods):
49+ AMP.__init__(self, locator=MethodCallReceiver(obj, methods))
50
51
52 class MethodCallClientProtocol(AMP):
53@@ -419,7 +419,7 @@
54 @param protocol: The newly connected protocol instance.
55 """
56 self._sender = MethodCallSender(protocol, self._factory.clock)
57- if self._retry_on_reconnect:
58+ if self._factory.retryOnReconnect:
59 self._retry()
60
61 def _retry(self):
62@@ -442,14 +442,14 @@
63
64 protocol = MethodCallServerProtocol
65
66- def __init__(self, object, methods):
67+ def __init__(self, obj, methods):
68 """
69 @param object: The object exposed by the L{MethodCallProtocol}s
70 instances created by this factory.
71 @param methods: A list of the names of the methods that remote peers
72 are allowed to call on the C{object} that we publish.
73 """
74- self.object = object
75+ self.object = obj
76 self.methods = methods
77
78 def buildProtocol(self, addr):
79@@ -560,52 +560,3 @@
80
81 for deferred in requests:
82 deferred.callback(result)
83-
84-
85-class RemoteObjectConnector(object):
86- """Connect to remote objects exposed by a L{MethodCallProtocol}."""
87-
88- factory = MethodCallClientFactory
89- remote = RemoteObject
90-
91- def __init__(self, reactor, socket_path):
92- """
93- @param reactor: A reactor able to connect to Unix sockets.
94- @param socket: The path to the socket we want to connect to.
95- @param args: Arguments to be passed to the created L{RemoteObject}.
96- @param kwargs: Keyword arguments for the created L{RemoteObject}.
97- """
98- self._socket_path = socket_path
99- self._reactor = reactor
100- self._factory = None
101- self._connector = None
102-
103- def connect(self, max_retries=None, factor=None):
104- """Connect to a remote object exposed by a L{MethodCallProtocol}.
105-
106- This method will connect to the socket provided in the constructor
107- and return a L{Deferred} resulting in a connected L{RemoteObject}.
108-
109- @param max_retries: If not C{None} give up try to connect after this
110- amount of times, otherwise keep trying to connect forever.
111- @param factor: Optionally a float indicating by which factor the
112- delay between subsequent retries should increase. Smaller values
113- result in a faster reconnection attempts pace.
114- """
115- factory = self.factory(self._reactor)
116- factory.maxRetries = max_retries
117- if factor:
118- factory.factor = factor
119- self._connector = self._reactor.connectUNIX(self._socket_path, factory)
120- return factory.getRemoteObject()
121-
122- def disconnect(self):
123- """Disconnect the L{RemoteObject} that we have created."""
124- if self._connector is not None:
125- factory = self._connector.factory
126- factory.stopTrying()
127- # XXX we should be using self._connector.disconnect() here
128- remote = factory._remote
129- if remote:
130- if remote._sender.protocol.transport:
131- remote._sender.protocol.transport.loseConnection()
132
133=== modified file 'landscape/lib/tests/test_amp.py'
134--- landscape/lib/tests/test_amp.py 2013-05-06 18:15:21 +0000
135+++ landscape/lib/tests/test_amp.py 2013-05-07 15:57:26 +0000
136@@ -1,13 +1,14 @@
137 from twisted.internet import reactor
138 from twisted.internet.error import ConnectError, ConnectionDone
139 from twisted.internet.task import Clock
140-from twisted.internet.defer import Deferred, DeferredList
141+from twisted.internet.defer import Deferred, inlineCallbacks
142 from twisted.python.failure import Failure
143+from twisted.trial.unittest import TestCase
144
145 from landscape.lib.amp import (
146 MethodCallError, MethodCallServerProtocol, MethodCallClientProtocol,
147 MethodCallServerFactory, MethodCallClientFactory, RemoteObject,
148- RemoteObjectConnector, MethodCallSender)
149+ MethodCallSender)
150 from landscape.tests.helpers import LandscapeTest
151
152
153@@ -178,32 +179,6 @@
154 "google"]
155
156
157-class WordsFactory(MethodCallClientFactory):
158-
159- factor = 0.19
160-
161- retryOnReconnect = True
162- retryTimeout = 0.7
163-
164-
165-class RemoteWordsConnector(RemoteObjectConnector):
166-
167- factory = WordsFactory
168-
169-
170-METHODS = ["empty",
171- "motd",
172- "capitalize",
173- "is_short",
174- "concatenate",
175- "lower_case",
176- "multiply_alphabetically",
177- "translate",
178- "meaning_of_life",
179- "guess",
180- "google"]
181-
182-
183 class MethodCallTest(LandscapeTest):
184
185 def setUp(self):
186@@ -368,7 +343,7 @@
187 def setUp(self):
188 super(RemoteObjectTest, self).setUp()
189 self.clock = Clock()
190- self.factory = WordsFactory(self.clock)
191+ self.factory = MethodCallClientFactory(self.clock)
192 server_factory = MethodCallServerFactory(Words(self.clock), METHODS)
193 self.connector = FakeConnector(self.factory, server_factory)
194 self.connector.connect()
195@@ -564,6 +539,8 @@
196 factory, the L{RemoteObject} will transparently retry to perform
197 the L{MethodCall} requests that failed due to the broken connections.
198 """
199+ self.factory.factor = 0.19
200+ self.factory.retryOnReconnect = True
201 self.connector.disconnect()
202 deferred = self.remote.capitalize("john")
203
204@@ -583,6 +560,8 @@
205 the L{RemoteObject} will properly propagate the error to the original
206 caller.
207 """
208+ self.factory.factor = 0.19
209+ self.factory.retryOnReconnect = True
210 self.connector.disconnect()
211 deferred = self.remote.secret()
212
213@@ -689,151 +668,217 @@
214 self.assertIsNone(self.successResultOf(deferred))
215
216
217-class RemoteObjectConnectorTest(LandscapeTest):
218+class MethodCallFunctionalTest(TestCase):
219
220 def setUp(self):
221- super(RemoteObjectConnectorTest, self).setUp()
222+ super(MethodCallFunctionalTest, self).setUp()
223 self.socket = self.mktemp()
224- self.server_factory = MethodCallServerFactory(Words(reactor), METHODS)
225- self.port = reactor.listenUNIX(self.socket, self.server_factory)
226- self.connector = RemoteWordsConnector(reactor, self.socket)
227-
228- def set_remote(words):
229- self.words = words
230-
231- connected = self.connector.connect()
232- return connected.addCallback(set_remote)
233+ self.server = MethodCallServerFactory(Words(reactor), METHODS)
234+ self.client = MethodCallClientFactory(reactor)
235+ self.port = reactor.listenUNIX(self.socket, self.server)
236+ #self.client.maxRetries = 0 # By default, don't try to reconnect
237
238 def tearDown(self):
239- self.connector.disconnect()
240+ super(MethodCallFunctionalTest, self).tearDown()
241 self.port.stopListening()
242- super(RemoteObjectConnectorTest, self).tearDown()
243
244+ @inlineCallbacks
245 def test_connect(self):
246 """
247 The L{RemoteObject} resulting form the deferred returned by
248- L{RemoteObjectConnector.connect} is properly connected to the
249- remote peer.
250- """
251- return self.assertSuccess(self.words.empty())
252-
253+ L{MethodCallClientFactory.getRemoteObject} is properly connected
254+ to the remote peer.
255+ """
256+ connector = reactor.connectUNIX(self.socket, self.client)
257+ remote = yield self.client.getRemoteObject()
258+ result = yield remote.capitalize("john")
259+ self.assertEqual(result, "John")
260+ self.client.stopTrying()
261+ connector.disconnect()
262+
263+ @inlineCallbacks
264+ def test_connect_with_max_retries(self):
265+ """
266+ If L{MethodCallClientFactory.maxRetries} is set, then the factory
267+ will give up trying to connect after that amout of times.
268+ """
269+ self.port.stopListening()
270+ self.client.maxRetries = 0
271+ reactor.connectUNIX(self.socket, self.client)
272+ yield self.assertFailure(self.client.getRemoteObject(), ConnectError)
273+
274+ @inlineCallbacks
275+ def test_reconnect(self):
276+ """
277+ If the connection is lost, the L{RemoteObject} created by the factory
278+ will transparently handle the reconnection.
279+ """
280+ self.client.factor = 0.01 # Try reconnecting very quickly
281+ connector = reactor.connectUNIX(self.socket, self.client)
282+ remote = yield self.client.getRemoteObject()
283+
284+ # Disconnect and wait till we connect again
285+ deferred = Deferred()
286+ self.client.notifyOnConnect(deferred.callback)
287+ connector.disconnect()
288+ yield deferred
289+
290+ # The remote object is still working
291+ result = yield remote.capitalize("john")
292+ self.assertEqual(result, "John")
293+ self.client.stopTrying()
294+ connector.disconnect()
295+
296+ @inlineCallbacks
297+ def test_retry(self):
298+ """
299+ If the connection is lost, the L{RemoteObject} created by the creator
300+ will transparently retry to perform the L{MethodCall} requests that
301+ failed due to the broken connection.
302+ """
303+ self.client.factor = 0.01 # Try reconnecting very quickly
304+ self.client.retryOnReconnect = True
305+ connector = reactor.connectUNIX(self.socket, self.client)
306+ remote = yield self.client.getRemoteObject()
307+
308+ # Disconnect
309+ connector.disconnect()
310+
311+ # This call will fail but it's transparently retried
312+ result = yield remote.capitalize("john")
313+ self.assertEqual(result, "John")
314+ self.client.stopTrying()
315+ connector.disconnect()
316+
317+ @inlineCallbacks
318+ def test_retry_with_method_call_error(self):
319+ """
320+ If a retried L{MethodCall} request fails due to a L{MethodCallError},
321+ the L{RemoteObject} will properly propagate the error to the original
322+ caller.
323+ """
324+ self.client.factor = 0.01 # Try reconnecting very quickly
325+ self.client.retryOnReconnect = True
326+ connector = reactor.connectUNIX(self.socket, self.client)
327+ remote = yield self.client.getRemoteObject()
328+
329+ # Disconnect
330+ connector.disconnect()
331+
332+ # A method call error is not retried
333+ yield self.assertFailure(remote.secret(), MethodCallError)
334+ self.client.stopTrying()
335+ connector.disconnect()
336+
337+ @inlineCallbacks
338 def test_wb_retry_with_while_still_disconnected(self):
339 """
340 The L{RemoteObject._retry} method gets called as soon as a new
341 connection is ready. If for whatever reason the connection drops
342 again very quickly, the C{_retry} method will behave as expected.
343 """
344- self.words._sender.protocol.transport.loseConnection()
345- self.port.stopListening()
346- real_handle_connect = self.words._handle_connect
347-
348- def handle_connect(protocol):
349+ self.client.factor = 0.01 # Try reconnecting very quickly
350+ self.client.retryOnReconnect = True
351+ connector = reactor.connectUNIX(self.socket, self.client)
352+ remote = yield self.client.getRemoteObject()
353+
354+ # Disconnect
355+ connector.disconnect()
356+
357+ def handle_reconnect(protocol):
358 # In this precise moment we have a newly connected protocol
359- self.words._sender.protocol = protocol
360+ remote._sender.protocol = protocol
361
362 # Pretend that the connection is lost again very quickly
363 protocol.transport.loseConnection()
364- self.port.stopListening()
365
366 # Force RemoteObject._retry to run using a disconnected protocol
367- reactor.callLater(0, self.words._retry)
368+ reactor.callLater(0, remote._retry)
369
370 # Restore the real handler and start listening again very soon
371- factory.dontNotifyOnConnect(handle_connect)
372- factory.notifyOnConnect(real_handle_connect)
373- reactor.callLater(0.2, restart_listening)
374-
375- def restart_listening():
376- self.port = reactor.listenUNIX(self.socket, self.server_factory)
377+ self.client.dontNotifyOnConnect(handle_reconnect)
378+ self.client.notifyOnConnect(remote._handle_connect)
379
380 def assert_failure(error):
381 self.assertEqual(str(error), "Forbidden method 'secret'")
382
383 # Use our own reconnect handler
384- factory = self.connector._connector.factory
385- factory.dontNotifyOnConnect(real_handle_connect)
386- factory.notifyOnConnect(handle_connect)
387-
388- reactor.callLater(0.2, restart_listening)
389- result = self.words.secret()
390- self.assertFailure(result, MethodCallError)
391- return result.addCallback(assert_failure)
392-
393+ self.client.dontNotifyOnConnect(remote._handle_connect)
394+ self.client.notifyOnConnect(handle_reconnect)
395+
396+ error = yield self.assertFailure(remote.secret(), MethodCallError)
397+ self.assertEqual(str(error), "Forbidden method 'secret'")
398+
399+ self.client.stopTrying()
400+ connector.disconnect()
401+
402+ @inlineCallbacks
403 def test_retry_with_many_method_calls(self):
404 """
405 If several L{MethodCall} requests were issued while disconnected, they
406 will be all eventually completed when the connection gets established
407 again.
408 """
409- self.words._sender.protocol.transport.loseConnection()
410- self.port.stopListening()
411-
412- def restart_listening():
413- self.port = reactor.listenUNIX(self.socket, self.server_factory)
414-
415- def assert_guess(response):
416- self.assertEqual(response, "Guessed!")
417-
418- def assert_secret(failure):
419- self.assertEqual(str(failure.value), "Forbidden method 'secret'")
420-
421- def assert_motd(response):
422- self.assertEqual(response, "Words are cool")
423-
424- reactor.callLater(0.1, restart_listening)
425-
426- results = [self.words.guess("word", "cool", value=4),
427- self.words.secret(),
428- self.words.motd()]
429- results[0].addCallback(assert_guess)
430- results[1].addErrback(assert_secret)
431- results[2].addCallback(assert_motd)
432- return DeferredList(results)
433-
434+ self.client.factor = 0.01 # Try reconnecting very quickly
435+ self.client.retryOnReconnect = True
436+ connector = reactor.connectUNIX(self.socket, self.client)
437+ remote = yield self.client.getRemoteObject()
438+
439+ # Disconnect
440+ connector.disconnect()
441+
442+ result1 = yield remote.guess("word", "cool", value=4)
443+ result2 = yield remote.motd()
444+
445+ self.assertEqual(result1, "Guessed!")
446+ self.assertEqual(result2, "Words are cool")
447+ self.client.stopTrying()
448+ connector.disconnect()
449+
450+ @inlineCallbacks
451 def test_retry_without_retry_on_reconnect(self):
452 """
453- If C{retry_on_reconnect} is C{False}, the L{RemoteObject} object won't
454+ If C{retryOnReconnect} is C{False}, the L{RemoteObject} object won't
455 retry to perform requests which failed because the connection was
456 lost, however requests made after a reconnection will still succeed.
457 """
458- self.words._sender.protocol.transport.loseConnection()
459- self.port.stopListening()
460-
461- def restart_listening():
462- self.port = reactor.listenUNIX(self.socket, self.server_factory)
463- reactor.callLater(0.3, assert_reconnected)
464-
465- def assert_reconnected():
466- result = self.words.empty()
467- result.addCallback(lambda x: reconnected.callback(None))
468- return result
469-
470- reactor.callLater(0.1, restart_listening)
471- self.words._factory.retryOnReconnect = False
472- result = self.words.empty()
473- self.assertFailure(result, ConnectionDone)
474- reconnected = Deferred()
475- return result.addCallback(lambda x: reconnected)
476-
477+ self.client.factor = 0.01 # Try reconnecting very quickly
478+ connector = reactor.connectUNIX(self.socket, self.client)
479+ remote = yield self.client.getRemoteObject()
480+
481+ # Disconnect
482+ deferred = Deferred()
483+ self.client.notifyOnConnect(deferred.callback)
484+ connector.disconnect()
485+
486+ yield self.assertFailure(remote.modt(), ConnectionDone)
487+
488+ # Wait for reconnection and peform another call
489+ yield deferred
490+ result = yield remote.motd()
491+ self.assertEqual(result, "Words are cool")
492+
493+ self.client.stopTrying()
494+ connector.disconnect()
495+
496+ @inlineCallbacks
497 def test_retry_with_timeout(self):
498 """
499- If a C{timeout} is set, the L{RemoteObject} object will errback failed
500- L{MethodCall}s after that amount of seconds, without retrying them when
501- the connection established again.
502+ If a C{retryTimeout} is set, the L{RemoteObject} object will errback
503+ failed L{MethodCall}s after that amount of seconds, without retrying
504+ them when the connection established again.
505 """
506- self.words._sender.protocol.transport.loseConnection()
507- self.port.stopListening()
508-
509- def restart_listening():
510- self.port = reactor.listenUNIX(self.socket, self.server_factory)
511- reactor.callLater(0.1, reconnected.callback, None)
512-
513- def assert_failure(error):
514- self.assertEqual(str(error), "timeout")
515- return reconnected
516-
517- reactor.callLater(0.9, restart_listening)
518- result = self.words.empty()
519- self.assertFailure(result, MethodCallError)
520- reconnected = Deferred()
521- return result.addCallback(assert_failure)
522+ self.client.retryOnReconnect = True
523+ self.client.retryTimeout = 0.1
524+ self.client.factor = 1 # Reconnect slower than timeout
525+ connector = reactor.connectUNIX(self.socket, self.client)
526+ remote = yield self.client.getRemoteObject()
527+
528+ # Disconnect
529+ connector.disconnect()
530+
531+ error = yield self.assertFailure(remote.modt(), MethodCallError)
532+ self.assertEqual("timeout", str(error))
533+
534+ self.client.stopTrying()
535+ connector.disconnect()

Subscribers

People subscribed via source and target branches

to all changes: