[Twisted-Python] Mimicking a blocking API using Twisted

Paul Reznicek maillists at ergo-pc.net
Sun Mar 25 21:27:42 EDT 2012


Hello,
I have similar needs and did not found usable answers, so I wrote some
q&d hacks for doctests and also for UI interactions, where the user should
be blocked until something in non-blocking deferred finish or "time-outed".

Below hacks are working with twisted 10.0.0, no idea about more actual versions.

1. Multiple start/stop of reactor during doctests works when using "reactor.crash"
   Example - 2 tested class methods in one class:
# ---------------------
def method_a(self, parm):
    '''
    >>> g = GateWay('TEST')
    >>> s1 = reactor.callWhenRunning(lambda: print(g.method_a('X') ) )
    >>> s2 = reactor.callWhenRunning(lambda: print(g.method_a('Y') ) )
    >>> s3 = reactor.callWhenRunning(lambda: print(g.method_a('Z') ) )
    >>> s0 = reactor.callLater(11.1, reactor.crash); reactor.run()  # doctest:+ELLIPSIS
    {u'result of X...}
    {u'result of Y...}
    {u'result of Z...}
    '''
    [code]

def method_b(self):
    >>> g = GateWay('TEST')
    >>> s1 = reactor.callWhenRunning(lambda: print(g.method_b() ) )
    >>> s0 = reactor.callLater(5.1, reactor.crash); reactor.run()   # doctest:+ELLIPSIS
    {u'bytes': ..., u'packets': ..., u'duration': ...}
    '''
    [code]
# ---------------------
This way, it is possible to use doctests with reactor start/stop in several
methods/functions in a class/module and also in external doctests files.


2. Sleep w/o blocking reactor:
# ---------------------
def nonBlockingSleep(delay):
    '''Hack to allow sleep for a period of time w/o blocking the reactor
    Do *block* the caller for `delay` seconds.
    '''
    if delay > 90:
        print('WARNING nonBlockingSleep delay={0} sec. dangerous, reset to 90s'.format(delay) )
        delay = 90.0
    toTime = time.time() + delay
    while toTime >= time.time():
        try:
            reactor.iterate(0.055)
        except Exception as err:
            pass
        time.sleep(0.001)
# ---------------------
The values of .iterate() and .sleep() are the best, I found.
time.sleep(0.001) is important, it is not reliable w/o + CPU load is high.


3. Wait until deferred return a result or timeout expire
# ---------------------
def sleepUntilDFRDresult(dfrd, timeout=60):
    '''Hack to make non-blocking deferred waiting for its result or error for
    in `timeout` specified number of seconds, but not to block reactor loop.
    This wrapper *block* the caller until deferred.result or timeout appears.

    >>> dfrd = defer.Deferred() #.addBoth(cbReactorStop)
    >>> sleepUntilDFRDresult(dfrd)       # doctest:+ELLIPSIS
    Exception(u'ERROR sleepUntilDFRDresult server not running for: <Deferred at 0x...> from <doctest
__main__/<module> #L1',)

    >>> startTime = time.time()
    >>> f = lambda: print(sleepUntilDFRDresult(dfrd, timeout=999), 'in:', time.time()-startTime,
'sec.' )
    >>> s1 = reactor.callWhenRunning( f );
    >>> dc2 = reactor.callLater(2, dfrd.callback, 'CALLED')
    >>> dc0 = reactor.callLater(3, reactor.crash); reactor.run()   # doctest:+ELLIPSIS
    WARNING sleepUntilDFRDresult timeout=999 sec. dangerous
    CALLED in: 2.0... sec.
    '''
    if timeout > 600:
        print('WARNING sleepUntilDFRDresult timeout={0} sec. dangerous'.format(timeout) )

    err = None
    if not isinstance(dfrd, defer.Deferred):
        err = 'ERROR sleepUntilDFRDresult dfrd "{0}" not Deferred: '.format(type(dfrd))
    elif not reactor.running:
        err = 'ERROR sleepUntilDFRDresult server not running for: '
    else:
        toTime = time.time() + timeout
        while not dfrd.called and toTime >= time.time():
            try:
                reactor.iterate(0.055)
            except Exception as err:
                pass
            Htime.sleep(0.001)
        if dfrd.called and hasattr(dfrd, 'result'):
            return dfrd.result
    func_name, module_name, line_no = tools.getCallerNameLocation()
    repr_dfrd = '{0} from {2}/{1} #L{3}'.format(dfrd, func_name, module_name, line_no)
    err = Exception( (err or 'ERROR TIMEOUT sleepUntilDFRDresult for: ') + repr_dfrd )
##     print(err)
    return err
# ---------------------
This hack is not 100% reliable, but I did not found a better solution.
There is sometime an "Already called..." error in the logs, when server is under
high load with lot of connections, but since around 2 years I did not observed
a data loss or other severer problem.

I hope it helps.
If somebody have a better (less "hacky") solution for above 3 tasks,
please send it to this list.

Regards
Paul

Laurens Van Houtven wrote:
> The thing I'm documenting is a server, I'm documenting it by interacting with it as a client. I realize that doesn't entirely detract from your point -- you might still be introducing problems that would not affect a "real" client.
> 
> My intention is to write BDD-ish stuff (except not with the usual cucumber-style scenario text).
> 
> cheers
> lvh
> 
> 
> On 25 Mar 2012, at 23:44, Itamar Turner-Trauring wrote:
> 
>> On 03/25/2012 05:02 PM, Laurens Van Houtven wrote:
>>> Hi,
>>>
>>>
>>> I'm trying to find out if there's a reasonable way to mimic a blocking 
>>> API with an existing non-blocking API. I want to do this so I can 
>>> write doctests.
>>>
>>> For example, I want to make a remote AMP call. It returns a deferred. 
>>> Instead of returning a deferred, I want it to block until the deferred 
>>> fires.
>> Doctests are supposed to document and test, or perhaps have tested 
>> documentation. If the API you're presenting doesn't match actual usage, 
>> that's bad documentation...



More information about the Twisted-Python mailing list