launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #06201
[Merge] lp:~julian-edwards/maas/cobbler-timeouts into lp:maas
Julian Edwards has proposed merging lp:~julian-edwards/maas/cobbler-timeouts into lp:maas.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~julian-edwards/maas/cobbler-timeouts/+merge/90402
Mostly self explanatory except for a couple of things:
* AsynchronousDeferredRunTestForBrokenTwisted is needed when you have leftover unfired Deferreds, it cleans them up.
* RecordingSession is fixed so you can actually pass in a proxy to use like its docstring said you could :)
--
https://code.launchpad.net/~julian-edwards/maas/cobbler-timeouts/+merge/90402
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~julian-edwards/maas/cobbler-timeouts into lp:maas.
=== modified file 'src/provisioningserver/cobblerclient.py'
--- src/provisioningserver/cobblerclient.py 2012-01-26 22:33:02 +0000
+++ src/provisioningserver/cobblerclient.py 2012-01-27 12:48:09 +0000
@@ -23,6 +23,7 @@
import xmlrpclib
+from twisted.internet import reactor as default_reactor
from twisted.internet.defer import (
DeferredLock,
inlineCallbacks,
@@ -123,6 +124,24 @@
else:
return arg
+ def _with_timeout(self, d, timeout=30, reactor=None):
+ """Wrap the xmlrpc call that returns "d" so that it is cancelled if
+ it exceeds a timeout.
+
+ :param d: The Deferred to cancel.
+ :param timeout: timeout in seconds, defaults to 30.
+ :param reactor: override the default reactor, useful for testing.
+ """
+ if reactor is None:
+ reactor = default_reactor
+ delayed_call = reactor.callLater(timeout, d.cancel)
+
+ def cancel_timeout(passthrough):
+ if not delayed_call.called:
+ delayed_call.cancel()
+ return passthrough
+ return d.addBoth(cancel_timeout)
+
def _issue_call(self, method, *args):
"""Initiate call to XMLRPC method.
@@ -133,7 +152,7 @@
:return: `Deferred`.
"""
args = map(self.substitute_token, args)
- d = self.proxy.callRemote(method, *args)
+ d = self._with_timeout(self.proxy.callRemote(method, *args))
return d
@inlineCallbacks
=== modified file 'src/provisioningserver/tests/test_cobblersession.py'
--- src/provisioningserver/tests/test_cobblersession.py 2012-01-26 22:33:02 +0000
+++ src/provisioningserver/tests/test_cobblersession.py 2012-01-27 12:48:09 +0000
@@ -11,21 +11,29 @@
__metaclass__ = type
__all__ = []
+import fixtures
+
from random import Random
from xmlrpclib import Fault
from provisioningserver import cobblerclient
from provisioningserver.testing.fakecobbler import fake_token
from testtools.content import text_content
-from testtools.deferredruntest import AsynchronousDeferredRunTest
+from testtools.deferredruntest import (
+ assert_fails_with,
+ AsynchronousDeferredRunTest,
+ AsynchronousDeferredRunTestForBrokenTwisted,
+ )
from testtools.testcase import (
ExpectedException,
TestCase,
)
+from twisted.internet import defer
from twisted.internet.defer import (
inlineCallbacks,
returnValue,
)
+from twisted.internet.task import Clock
randomizer = Random()
@@ -72,10 +80,9 @@
"""
self.return_values = values
- @inlineCallbacks
def callRemote(self, method, *args):
if method == 'login':
- returnValue(self.fake_token)
+ return defer.succeed(self.fake_token)
self.calls.append((method, ) + args)
if self.return_values:
@@ -85,8 +92,14 @@
if isinstance(value, Exception):
raise value
else:
- value = yield value
- returnValue(value)
+ return defer.succeed(value)
+
+
+class DeadProxy(RecordingFakeProxy):
+ """Fake proxy that returns nothing. Useful for timeout testing."""
+
+ def callRemote(self, method, *args):
+ return defer.Deferred()
class RecordingSession(cobblerclient.CobblerSession):
@@ -104,17 +117,19 @@
that the session should pretend it gets from the server on login.
"""
fake_token = kwargs.pop('fake_token')
- self.fake_proxy = RecordingFakeProxy(fake_token)
+ proxy_class = kwargs.pop('fake_proxy', RecordingFakeProxy)
+ if proxy_class is None:
+ proxy_class = RecordingFakeProxy
+
+ self.fake_proxy = proxy_class(fake_token)
super(RecordingSession, self).__init__(*args, **kwargs)
def _make_twisted_proxy(self):
return self.fake_proxy
-class TestCobblerSession(TestCase):
- """Test session management against a fake XMLRPC session."""
-
- run_tests_with = AsynchronousDeferredRunTest.make_factory()
+class TestCobblerSessionBase(TestCase):
+ """Base class helpers for testing `CobblerSession`."""
def make_url_user_password(self):
"""Produce arbitrary API URL, username, and password."""
@@ -124,13 +139,21 @@
'password%d' % pick_number(),
)
- def make_recording_session(self, session_args=None, token=None):
+ def make_recording_session(self, session_args=None, token=None,
+ fake_proxy=RecordingFakeProxy):
"""Create a `RecordingSession`."""
if session_args is None:
session_args = self.make_url_user_password()
if token is None:
token = fake_token()
- return RecordingSession(*session_args, fake_token=token)
+ return RecordingSession(
+ *session_args, fake_token=token, fake_proxy=fake_proxy)
+
+
+class TestCobblerSession(TestCobblerSessionBase):
+ """Test session management against a fake XMLRPC session."""
+
+ run_tests_with = AsynchronousDeferredRunTest.make_factory()
def test_initializes_but_does_not_authenticate_on_creation(self):
url, user, password = self.make_url_user_password()
@@ -307,6 +330,41 @@
[('authenticate_me_first', session.token)], session.proxy.calls)
+class TestConnectionTimeouts(TestCobblerSessionBase,
+ fixtures.TestWithFixtures):
+ """Tests for connection timeouts on `CobblerSession`."""
+
+ run_tests_with = AsynchronousDeferredRunTestForBrokenTwisted
+
+ def test__with_timeout_cancels(self):
+ clock = Clock()
+ session = self.make_recording_session()
+ d = session._with_timeout(defer.Deferred(), 1, clock)
+ clock.advance(2)
+ return assert_fails_with(d, defer.CancelledError)
+
+ def test__with_timeout_not_cancelled(self):
+ clock = Clock()
+ session = self.make_recording_session()
+ d = session._with_timeout(defer.succeed("frobnicle"), 1, clock)
+ clock.advance(2)
+ def result(value):
+ self.assertEqual(value, "frobnicle")
+ self.assertEqual([], clock.getDelayedCalls())
+ return d.addCallback(result)
+
+ def test__issue_call_times_out(self):
+ clock = Clock()
+ patch = fixtures.MonkeyPatch(
+ "provisioningserver.cobblerclient.default_reactor", clock)
+ self.useFixture(patch)
+
+ session = self.make_recording_session(fake_proxy=DeadProxy)
+ d = session._issue_call("login", "foo")
+ clock.advance(31)
+ return assert_fails_with(d, defer.CancelledError)
+
+
class CobblerObject(TestCase):
"""Tests for the `CobblerObject` classes."""
@@ -319,3 +377,4 @@
self.assertEqual(
'x_systems_y',
cobblerclient.CobblerSystem.name_method('x_%s_y', plural=True))
+