Skip to content
This repository was archived by the owner on Dec 21, 2025. It is now read-only.
This repository was archived by the owner on Dec 21, 2025. It is now read-only.

Need a way to protect a region from cancellation (like "masking interrupts") #96

Closed

Description

@njsmith

Here's an example of a simple bidirectional proxy using curio: https://gist.github.com/njsmith/4b4e75a9ab9e975705412f8f8e866948

The core idea is that we spawn two tasks to handle the copying in each direction:

async def copy_all_bidir(sock1, sock2):
    await all_or_none([
        await curio.spawn(copy_all(sock1, sock2)),
        await curio.spawn(copy_all(sock2, sock1)),
])

The tricky part is managing these tasks. Basically with regards to exceptions/timeouts/cancellation/etc., we want it to act like the two copy_all tasks are both "part of" the copy_all_bidir task. So:

  • If one of the tasks raises an exception, the other task should immediately be canceled, and the exception should propagate out of copy_all_bidir
  • If both of the tasks complete, then copy_all_bidir should complete successfully
  • If copy_all_bidir is cancelled, then both tasks should be canceled as part of copy_all_bidir's cleanup

The all_or_none helper there tries to do this, but AFAICT it can't quite achieve the last requirement given curio's current APIs. The problem sequence is:

  1. One of the tasks raises an exception
  2. all_or_none catches this exception, and starts to cancel the other task
  3. all_or_none itself gets canceled, thus canceling the cancellation and making everyone's head explode (and also leaking the other task)

In general this is a problem for cleanup code -- once you're executing the cancellation cleanup path you really don't want to get canceled again!

My suggestion for what it should look like is:

async def all_or_none(tasks):
    try:
        async for task in curio.wait(tasks):
            await task.join()
    except:
        async with curio.defer_cancellation:
            for task in tasks:
                await task.cancel()
        raise

except that curio.defer_cancellation isn't a thing that exists right now.

So this issue is to propose that curio.defer_cancellation be a thing that exists.

(Guess how I'm distracting myself from election news!)

Activity

  1. njsmith commented on Nov 9, 2016

    @njsmith
    ContributorAuthor

    Oh also, this should probably replace the current curio.timeout_after(None) thing. I.e., defer_cancellation should protect against both timeouts and explicit calls to Task.cancel(), and then timeout_after(None) becomes unnecessary.

    And the exact semantics I'm proposing are that if a cancellation arrives while defer_cancellation is in effect, then the cancellation exception gets raised from defer_cancellation.__aexit__.

    Prior art: asyncio.shield

  2. njsmith commented on Nov 9, 2016

    @njsmith
    ContributorAuthor

    MMmmmmaybe we would also want to make it so that cancellations are automatically deferred when we're in the span between when a timeout fires, and when the timeout exception propagates out to the enclosing timeout block (and gets converted into a different exception). The theory would be that firing a cancellation while we're in the middle of handling a cancellation is not helpful -- same logic that says that calling Task.cancel twice shouldn't fire two cancellation exceptions.

  3. njsmith commented on Nov 20, 2016

    @njsmith
    ContributorAuthor

    This was fixed by #99, so closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions