← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~julian-edwards/launchpad/deferred_timeouts into lp:launchpad/devel

 

Julian Edwards has proposed merging lp:~julian-edwards/launchpad/deferred_timeouts into lp:launchpad/devel.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)


= Summary =
Add lp.services.twistedsupport.cancel_on_timeout utility

== Proposed fix ==
I've added a new utility cancel_on_timeout() that will cancel a Deferred after 
the supplied timeout.  The timeout does not fire if the Deferred fires first.

== Pre-implementation notes ==
jml supplied the code, I supplied the tests :)

== Implementation details ==
A delayed call is used which will call cancel() on the Deferred if it fires.  
The Deferred also gets another callback added to it that will unconditionally 
cancel the delayed call Deferred if it fires.

The tests work by using a Clock() reactor where we can wind the time forwards 
to simulate the timeout period passing.

== Tests ==
bin/test -cvvt  TestCancelOnTimeout
-- 
https://code.launchpad.net/~julian-edwards/launchpad/deferred_timeouts/+merge/38446
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~julian-edwards/launchpad/deferred_timeouts into lp:launchpad/devel.
=== modified file 'lib/lp/services/twistedsupport/__init__.py'
--- lib/lp/services/twistedsupport/__init__.py	2010-08-20 20:31:18 +0000
+++ lib/lp/services/twistedsupport/__init__.py	2010-10-14 18:56:08 +0000
@@ -5,6 +5,7 @@
 
 __metaclass__ = type
 __all__ = [
+    'cancel_on_timeout',
     'defer_to_thread',
     'extract_result',
     'gatherResults',
@@ -18,6 +19,7 @@
 from twisted.internet import (
     defer,
     threads,
+    reactor as default_reactor,
     )
 from twisted.python.util import mergeFunctionMetadata
 
@@ -92,3 +94,23 @@
         return successes[0]
     else:
         raise AssertionError("%r has not fired yet." % (deferred,))
+
+
+def cancel_on_timeout(d, timeout, reactor=None):
+    """Cancel a Deferred if it doesn't fire before the timeout is up.
+
+    :param d: The Deferred to cancel
+    :param timeout: The timeout in seconds
+    :param reactor: Override the default reactor (useful for tests).
+
+    :return: The same deferred, d.
+    """
+    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)
+

=== modified file 'lib/lp/services/twistedsupport/tests/test_twistedsupport.py'
--- lib/lp/services/twistedsupport/tests/test_twistedsupport.py	2010-08-20 20:31:18 +0000
+++ lib/lp/services/twistedsupport/tests/test_twistedsupport.py	2010-10-14 18:56:08 +0000
@@ -8,13 +8,18 @@
 import unittest
 
 from twisted.internet import defer
+from twisted.internet.task import Clock
+from twisted.trial.unittest import TestCase as TrialTestCase
 
-from lp.services.twistedsupport import extract_result
+from lp.services.twistedsupport import (
+    cancel_on_timeout,
+    extract_result,
+    )
 from lp.testing import TestCase
 
 
 class TestExtractResult(TestCase):
-    """Tests for `canonical.twisted_support.extract_result`."""
+    """Tests for `lp.services.twistedsupport.extract_result`."""
 
     def test_success(self):
         # extract_result on a Deferred that has a result returns the result.
@@ -37,6 +42,25 @@
         self.assertRaises(AssertionError, extract_result, deferred)
 
 
+class TestCancelOnTimeout(TrialTestCase):
+    """Tests for lp.services.twistedsupport.cancel_on_timeout."""
+
+    def test_deferred_is_cancelled(self):
+        clock = Clock()
+        d = cancel_on_timeout(defer.Deferred(), 1, clock)
+        clock.advance(2)
+        return self.assertFailure(d, defer.CancelledError)
+
+    def test_deferred_is_not_cancelled(self):
+        clock = Clock()
+        d = cancel_on_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_suite():
     return unittest.TestLoader().loadTestsFromName(__name__)