Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cancelling a task cancels a future the task was waiting for. How does it work?

I have an awaitable object implementing a request/reply transaction. If the transaction times out, it will be retried few times before giving up and raising an exception.

Now assume it always times out, because that is the case I have problem with.

When a task starts this operation and then gets cancelled, the retries will continue. This is not what I want. I want to cancel the operation entirely.

I prepared a MCVE and noticed, that a future that the task is waiting for gets cancelled when the task is cancelled. This suits me fine, it could be a base for a solution, but I do not understand why that future gets cancelled and if I can rely on it.

import asyncio

RETRIES = 2
TIMEOUT = 1.0

class ClientRPC:
    def __init__(self):
        self._reply = None
        self._retries = RETRIES

    def __await__(self):
        self.start()
        return self._reply.__await__()

    def start(self):
        loop = asyncio.get_event_loop()
        if self._reply is None:
            self._reply = loop.create_future()
        loop.call_later(TIMEOUT, self.handle_timeout)
        # send a request
        print("REQUEST")

    def handle_timeout(self):
        print("TIMEOUT")
        print("future", repr(self._reply._state))
        if self._retries > 0:
            self._retries -= 1
            self.start()
        else:
            self._reply.set_exception(RuntimeError("Timeout!"))

    def handle_reply(self, reply):
        # unused in this example
        pass

async def client():
    transaction = ClientRPC()
    try:
        reply = await transaction
    except asyncio.CancelledError:
        print("--CANCELLED--")

async def test():
    loop = asyncio.get_event_loop()
    task = loop.create_task(client())
    await asyncio.sleep(1.5)
    task.cancel()
    await asyncio.sleep(3)

asyncio.run(test()) # python 3.7+

Output (traceback omitted):

REQUEST
TIMEOUT
future 'PENDING'
REQUEST
--CANCELLED--
TIMEOUT
future 'CANCELLED'   <-- why?
REQUEST
TIMEOUT
future 'CANCELLED'
Exception in callback ClientRPC.handle_timeout()
handle: 
asyncio.base_futures.InvalidStateError: invalid state
like image 586
VPfB Avatar asked Oct 12 '25 04:10

VPfB


1 Answers

I prepared a MCVE and noticed, that a future that the task is waiting for gets cancelled when the task is cancelled. This suits me fine, it could be a base for a solution, but I do not understand why that future gets cancelled and if I can rely on it.

Yes, if the task awaits a future, that future will be cancelled. That future can be another task, so the cancellation will spread to the bottom-most future that is awaited. The implementation makes sure of that, but the documentation doesn't make it explicit.

I would go on to rely on this behavior, for two reasons:

  • it is impossible to change it at this point without a major breakage in backward compatibility. Developers have already rejected smaller changes because they would break existing code.

  • there is no other way to implement this that wouldn't lead to resource leaks. If the task you are cancelling is awaiting a future, what do you do except cancel it? If you just let it run in the background, you are potentially keeping it around forever, because the future might never exit on its own. If this were "fixed" by just dropping it from the scheduler (again, without cancellation), the future would never get a chance to clean up the resources it acquired, which would certainly cause a resource leak.

Thus it is safe to rely on canceling being propagated downward, with the exception of futures shielded with asyncio.shield(), which is reserved for futures that are meant to remain running in the background and have their own lifetime management.

like image 136
user4815162342 Avatar answered Oct 14 '25 18:10

user4815162342



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!