Changeset 25928

Show
Ignore:
Timestamp:
01/09/2009 01:18:47 PM (6 months ago)
Author:
exarkun
Message:
Merge dtpfactory-timeouts-3596 Author: exarkun Reviewer: therve Fixes: #3596 Fix the interaction between DTP connection setup success, timeout, and failure events. In particular, fix the behavior when a connection fails after the specified timeout has elapsed. Add direct unit tests for DTPFactory as well.
Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/twisted/protocols/ftp.py

    r25413 r25928  
    485485class DTPFactory(protocol.ClientFactory): 
    486486    """ 
    487     DTP protocol factory. 
     487    Client factory for I{data transfer process} protocols. 
    488488 
    489489    @ivar peerCheck: perform checks to make sure the ftp-pi's peer is the same 
    490490        as the dtp's 
    491491    @ivar pi: a reference to this factory's protocol interpreter 
    492     """ 
     492 
     493    @ivar _state: Indicates the current state of the DTPFactory.  Initially, 
     494        this is L{_IN_PROGRESS}.  If the connection fails or times out, it is 
     495        L{_FAILED}.  If the connection succeeds before the timeout, it is 
     496        L{_FINISHED}. 
     497    """ 
     498 
     499    _IN_PROGRESS = object() 
     500    _FAILED = object() 
     501    _FINISHED = object() 
     502 
     503    _state = _IN_PROGRESS 
    493504 
    494505    # -- configuration variables -- 
     
    496507 
    497508    # -- class variables -- 
    498     def __init__(self, pi, peerHost=None): 
     509    def __init__(self, pi, peerHost=None, reactor=None): 
    499510        """Constructor 
    500511        @param pi: this factory's protocol interpreter 
     
    505516        self.peerHost = peerHost            # the from FTP.transport.peerHost() 
    506517        self.deferred = defer.Deferred()    # deferred will fire when instance is connected 
    507         self.deferred.addBoth(lambda ign: (delattr(self, 'deferred'), ign)[1]) 
    508518        self.delayedCall = None 
     519        if reactor is None: 
     520            from twisted.internet import reactor 
     521        self._reactor = reactor 
     522 
    509523 
    510524    def buildProtocol(self, addr): 
    511525        log.msg('DTPFactory.buildProtocol', debug=True) 
     526 
     527        if self._state is not self._IN_PROGRESS: 
     528            return None 
     529        self._state = self._FINISHED 
     530 
    512531        self.cancelTimeout() 
    513         if self.pi.dtpInstance:   # only create one instance 
    514             return 
    515532        p = DTP() 
    516533        p.factory = self 
     
    519536        return p 
    520537 
     538 
    521539    def stopFactory(self): 
    522540        log.msg('dtpFactory.stopFactory', debug=True) 
    523541        self.cancelTimeout() 
    524542 
     543 
    525544    def timeoutFactory(self): 
    526545        log.msg('timed out waiting for DTP connection') 
    527         if self.deferred: 
    528             d, self.deferred = self.deferred, None 
    529  
    530             # TODO: LEFT OFF HERE! 
    531  
    532             d.addErrback(debugDeferred, 'timeoutFactory firing errback') 
    533             d.errback(defer.TimeoutError()) 
    534         self.stopFactory() 
     546        if self._state is not self._IN_PROGRESS: 
     547            return 
     548        self._state = self._FAILED 
     549 
     550        d = self.deferred 
     551        self.deferred = None 
     552        d.errback( 
     553            PortConnectionError(defer.TimeoutError("DTPFactory timeout"))) 
     554 
    535555 
    536556    def cancelTimeout(self): 
    537         if not self.delayedCall.called and not self.delayedCall.cancelled: 
     557        if self.delayedCall is not None and self.delayedCall.active(): 
    538558            log.msg('cancelling DTP timeout', debug=True) 
    539559            self.delayedCall.cancel() 
    540             assert self.delayedCall.cancelled 
    541             log.msg('timeout has been cancelled', debug=True) 
     560 
    542561 
    543562    def setTimeout(self, seconds): 
    544563        log.msg('DTPFactory.setTimeout set to %s seconds' % seconds) 
    545         self.delayedCall = reactor.callLater(seconds, self.timeoutFactory) 
     564        self.delayedCall = self._reactor.callLater(seconds, self.timeoutFactory) 
     565 
    546566 
    547567    def clientConnectionFailed(self, connector, reason): 
    548         self.deferred.errback(PortConnectionError(reason)) 
     568        if self._state is not self._IN_PROGRESS: 
     569            return 
     570        self._state = self._FAILED 
     571        d = self.deferred 
     572        self.deferred = None 
     573        d.errback(PortConnectionError(reason)) 
     574 
    549575 
    550576# -- FTP-PI (Protocol Interpreter) -- 
  • trunk/twisted/test/test_ftp.py

    r25201 r25928  
    1616from twisted.trial import unittest, util 
    1717from twisted.protocols import basic 
    18 from twisted.internet import reactor, protocol, defer, error 
     18from twisted.internet import reactor, task, protocol, defer, error 
    1919from twisted.internet.interfaces import IConsumer 
    2020from twisted.cred import portal, checkers, credentials 
     
    536536                ["425 Can't open data connection."]) 
    537537        return d.addCallback(gotPortNum) 
     538 
     539 
     540 
     541class DTPFactoryTests(unittest.TestCase): 
     542    """ 
     543    Tests for L{ftp.DTPFactory}. 
     544    """ 
     545    def setUp(self): 
     546        """ 
     547        Create a fake protocol interpreter and a L{ftp.DTPFactory} instance to 
     548        test. 
     549        """ 
     550        self.reactor = task.Clock() 
     551 
     552        class ProtocolInterpreter(object): 
     553            dtpInstance = None 
     554 
     555        self.protocolInterpreter = ProtocolInterpreter() 
     556        self.factory = ftp.DTPFactory( 
     557            self.protocolInterpreter, None, self.reactor) 
     558 
     559 
     560    def test_setTimeout(self): 
     561        """ 
     562        L{ftp.DTPFactory.setTimeout} uses the reactor passed to its initializer 
     563        to set up a timed event to time out the DTP setup after the specified 
     564        number of seconds. 
     565        """ 
     566        # Make sure the factory's deferred fails with the right exception, and 
     567        # make it so we can tell exactly when it fires. 
     568        finished = [] 
     569        d = self.assertFailure(self.factory.deferred, ftp.PortConnectionError) 
     570        d.addCallback(finished.append) 
     571 
     572        self.factory.setTimeout(6) 
     573 
     574        # Advance the clock almost to the timeout 
     575        self.reactor.advance(5) 
     576 
     577        # Nothing should have happened yet. 
     578        self.assertFalse(finished) 
     579 
     580        # Advance it to the configured timeout. 
     581        self.reactor.advance(1) 
     582 
     583        # Now the Deferred should have failed with TimeoutError. 
     584        self.assertTrue(finished) 
     585 
     586        # There should also be no calls left in the reactor. 
     587        self.assertFalse(self.reactor.calls) 
     588 
     589 
     590    def test_buildProtocolOnce(self): 
     591        """ 
     592        A L{ftp.DTPFactory} instance's C{buildProtocol} method can be used once 
     593        to create a L{ftp.DTP} instance. 
     594        """ 
     595        protocol = self.factory.buildProtocol(None) 
     596        self.assertIsInstance(protocol, ftp.DTP) 
     597 
     598        # A subsequent call returns None. 
     599        self.assertIdentical(self.factory.buildProtocol(None), None) 
     600 
     601 
     602    def test_timeoutAfterConnection(self): 
     603        """ 
     604        If a timeout has been set up using L{ftp.DTPFactory.setTimeout}, it is 
     605        cancelled by L{ftp.DTPFactory.buildProtocol}. 
     606        """ 
     607        self.factory.setTimeout(10) 
     608        protocol = self.factory.buildProtocol(None) 
     609        # Make sure the call is no longer active. 
     610        self.assertFalse(self.reactor.calls) 
     611 
     612 
     613    def test_connectionAfterTimeout(self): 
     614        """ 
     615        If L{ftp.DTPFactory.buildProtocol} is called after the timeout 
     616        specified by L{ftp.DTPFactory.setTimeout} has elapsed, C{None} is 
     617        returned. 
     618        """ 
     619        # Handle the error so it doesn't get logged. 
     620        d = self.assertFailure(self.factory.deferred, ftp.PortConnectionError) 
     621 
     622        # Set up the timeout and then cause it to elapse so the Deferred does 
     623        # fail. 
     624        self.factory.setTimeout(10) 
     625        self.reactor.advance(10) 
     626 
     627        # Try to get a protocol - we should not be able to. 
     628        self.assertIdentical(self.factory.buildProtocol(None), None) 
     629 
     630        # Make sure the Deferred is doing the right thing. 
     631        return d 
     632 
     633 
     634    def test_timeoutAfterConnectionFailed(self): 
     635        """ 
     636        L{ftp.DTPFactory.deferred} fails with L{PortConnectionError} when 
     637        L{ftp.DTPFactory.clientConnectionFailed} is called.  If the timeout 
     638        specified with L{ftp.DTPFactory.setTimeout} expires after that, nothing 
     639        additional happens. 
     640        """ 
     641        finished = [] 
     642        d = self.assertFailure(self.factory.deferred, ftp.PortConnectionError) 
     643        d.addCallback(finished.append) 
     644 
     645        self.factory.setTimeout(10) 
     646        self.assertFalse(finished) 
     647        self.factory.clientConnectionFailed(None, None) 
     648        self.assertTrue(finished) 
     649        self.reactor.advance(10) 
     650        return d 
     651 
     652 
     653    def test_connectionFailedAfterTimeout(self): 
     654        """ 
     655        If L{ftp.DTPFactory.clientConnectionFailed} is called after the timeout 
     656        specified by L{ftp.DTPFactory.setTimeout} has elapsed, nothing beyond 
     657        the normal timeout before happens. 
     658        """ 
     659        # Handle the error so it doesn't get logged. 
     660        d = self.assertFailure(self.factory.deferred, ftp.PortConnectionError) 
     661 
     662        # Set up the timeout and then cause it to elapse so the Deferred does 
     663        # fail. 
     664        self.factory.setTimeout(10) 
     665        self.reactor.advance(10) 
     666 
     667        # Now fail the connection attempt.  This should do nothing.  In 
     668        # particular, it should not raise an exception. 
     669        self.factory.clientConnectionFailed(None, defer.TimeoutError("foo")) 
     670 
     671        # Give the Deferred to trial so it can make sure it did what we 
     672        # expected. 
     673        return d 
     674 
    538675 
    539676