Search code examples
python-3.xtwistedtwisted.internet

Unable to cancel Twisted Deferred chain (AlreadyCalledError)


I'm trying to cancel a Deferred chain (main) whenever some chained deferred (child) raises an error. But I'm getting an AlreadyCalledError and the chain continues its work.

Here's the code:

from twisted.internet import defer

def asyncText(s, okCallback, errCallback):
  deferred = defer.Deferred()
  deferred.addCallbacks(okCallback, errCallback)
  if s.find('STOP')>=0:
    deferred.errback(ValueError('You are trying to print the unprintable :('))
  else:
    deferred.callback(s)
  return deferred

def asyncChain():
  texts = ['Hello StackOverflow,', 
           'this is an example of Twisted.chainDeferred()',
           'which is raising an AlreadyCalledError',
           'when I try to cancel() it.',
           'STOP => I will raise error and should stop the main deferred',
           'Best regards'
           ]

  mainDeferred= defer.Deferred()

  for text in texts: 
    def onSuccess(res):
      print('>> {}'.format(res))

    def onError(err):
      print('Error!! {}'.format(err))
      mainDeferred.cancel()

    d = asyncText(text, onSuccess, onError)
    mainDeferred.chainDeferred(d)

And here's the output:

>>> asyncChain()
- Hello StackOverflow,
- this is an example of Twisted.chainDeferred()
- which is raising an AlreadyCalledError
- when I try to cancel() it.
Error!! [Failure instance: Traceback (failure with no frames): <class 'ValueError'>: You are trying to print the unprintable :(
]
- Best regards
Unhandled error in Deferred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File ".../test.py", line 1, in asyncChain
    mainDeferred.chainDeferred(d)
  File ".../.venvs/p3/lib/python3.6/site-packages/twisted/internet/defer.py", line 435, in chainDeferred
    return self.addCallbacks(d.callback, d.errback)
  File ".../.venvs/p3/lib/python3.6/site-packages/twisted/internet/defer.py", line 311, in addCallbacks
    self._runCallbacks()
--- <exception caught here> ---
  File ".../.venvs/p3/lib/python3.6/site-packages/twisted/internet/defer.py", line 654, in _runCallbacks
    current.result = callback(current.result, *args, **kw)
  File ".../.venvs/p3/lib/python3.6/site-packages/twisted/internet/defer.py", line 501, in errback
    self._startRunCallbacks(fail)
  File ".../.venvs/p3/lib/python3.6/site-packages/twisted/internet/defer.py", line 561, in _startRunCallbacks
    raise AlreadyCalledError
twisted.internet.defer.AlreadyCalledError: 

I also tried to use a canceller, like so:

def asyncOnCancel(d):
  print('------ cancel() called ------')
  d.errback(ValueError('chain seems to be cancelled!'))
def asyncChainOnError(err):
  print('------ ERROR ON Chain {} ------'.format(err))

...
  mainDeferred= defer.Deferred(canceller= asyncOnCancel)
  mainDeferred.addErrback(asyncChainOnError)
...

But result is the same.

I also tried delaying child .callback(s) calls, or calling them after the .chainDeferred(). But I get always the same behaviour.

  • Is it really possible to cancel a chained Deferred (and getting chained child deferreds to get cancelled too)?
  • Why I'm getting this AlreadyCalledError?

I'm using python 3.6.6 and Twisted 18.9.0.

Thanks!

******* EDIT *******

After Jean-Paul answer, and noticing that .chainDeferred() is not what I need, I'll put here in a clearer way what I want (and how I finnally did it).

What I want was quite simpler: to run several Deferred, in a "synchronous" way (they have to wait for the previous one to have finished), although they don't need to share their results. If one fails, the rest of them is not executed.

Turns out is quite easy to do it with @defer.inlineCallbacks and yield. Here an example:

def asyncText(s):
  deferred = defer.Deferred()
  if s.find('STOP') >= 0:
    deferred.callback(True)
  else:
    deferred.callback(False)
  return deferred

@defer.inlineCallbacks
def asyncChain():
  texts = ['Hello StackOverflow,',
           'this is a simpler way to explain the question.',
           'I need no chainDeferred(). I need no .cancel().',
           'I just need inlineCallbacks decorator.',
           'STOP',
           'Yeah I will not be printed'
           ]
  for text in texts:
    stopHere = yield asyncText(text)
    if stopHere:
      break
    print('- {}'.format(text))

deferred= asyncChain()
>>> asyncChain()
- Hello StackOverflow,
- this is a simpler way to explain the question.
- I need no chainDeferred(). I need no .cancel().
- I just need inlineCallbacks decorator.

Solution

  • It's not clear what you're actually trying to do but I think your chain is backwards from what you expect:

        d = asyncText(text, onSuccess, onError)
        mainDeferred.chainDeferred(d)
    

    d has already fired when asyncText returns. But mainDeferred.chainDeferred(d) means "when mainDeferred fires, pass its result to d". Since d has already fired, this is invalid. A Deferred can only fire once.

    Perhaps you meant d.chainDeferred(mainDeferred) instead. Then "when d fires, pass its result to mainDeferred".

    However, the there is still a problem since if you chain d to mainDeferred then it doesn't make sense to cancel mainDeferred in a callback on d. The result will propagate from d to mainDeferred because they're chained. Cancellation is not necessary or useful.