Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PYTHON-4981 - Create workaround for asyncio.Task.cancelling support in older Python versions #2009

Merged
merged 4 commits into from
Nov 20, 2024

Conversation

NoahStapp
Copy link
Contributor

No description provided.

@ShaneHarvey
Copy link
Member

As discussed in person, we need to workaround the fast that task.cancelling() was only added in Python 3.11.

Copy link
Member

@ShaneHarvey ShaneHarvey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you track this in a new python ticket?

def __init__(self, coro: Coroutine[Any, Any, Any], *, name: Optional[str] = None) -> None:
super().__init__(coro, name=name)
self._cancelled: bool = False
asyncio._register_task(self)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't use private asyncio apis. Can we do this pattern instead?:

_Task(asyncio.create_task(...))

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see the problem. That wouldn't work because asyncio.all_tasks() would return the non-wrapper class.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem still remains though. If we can't do this using only public apis then we can't add this workaround at all.

Copy link
Contributor Author

@NoahStapp NoahStapp Nov 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any implementation needs to support our _Task getting cancelled by external components like unittest or pytest. The approach you outline, where _Task is just a wrapper around the actual task, doesn't work in this case: the loop will only interact with the task it owns, rather than the _Task itself. When our testing framework goes to cancel all remaining tasks, it only has access to the loop, which won't have a reference to any _Task instances.

Subclassing asyncio.Task and overriding the cancel method to support pre-3.11 Python versions solves this issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asyncio._register_task is documented in the asyncio docs here: https://docs.python.org/3/library/asyncio-extending.html as the only way to extend Task functionality.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The above passes all tests for me locally and doesn't use any private asyncio methods or import hacking, only MethodType tricks.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we own the task and its cancellation, can't we keep a mapping of tasks to cancelled state ourselves?

Copy link
Contributor Author

@NoahStapp NoahStapp Nov 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't own the cancellation when the test runner cancels the task at the end of the test that created it. We could write our own teardown method that uses such a mapping to run before the runner's teardown, but that seems messy.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I relent for now since we plan remove this code anyway once we figure out the real issue causing the hangs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add a TODO for us to come back and revisit this at that time.

@NoahStapp NoahStapp changed the title Explicitly raise asyncio.CancelledError when tasks are being cancelled PYTHON-4981 - Create workaround for asyncio.Task.cancelling support in older Python versions Nov 19, 2024


def create_task(coro: Coroutine[Any, Any, Any], *, name: Optional[str] = None) -> _Task:
return _Task(coro, name=name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay if we're going down this route, can we at least only use this workaround on <=3.10?
Like:

if version >= 3.11:
    return asyncio.create_task(...)
return _Task(coro, name=name)

Copy link
Contributor Author

@NoahStapp NoahStapp Nov 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're fine with _Task.cancelling and asyncio.Task.cancelling having the same name but different actual purposes, then yes. asyncio.Task.cancelling technically returns the number of cancelled requests for a task instead of a boolean. I could also just implement a counter instead of a boolean to make it match.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would they differ? Oh I see, the stdlib returns a count of the cancellation requests as an integer. Is that what you mean?

Also I see this:

This method is used by asyncio’s internals and isn’t expected to be used by end-user code. See uncancel() for more details.

So we are going against their guidance in multiple ways.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes let's implement the cancel/uncancel counter pattern. It's simple enough.

Copy link
Member

@blink1073 blink1073 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@ShaneHarvey
Copy link
Member

@NoahStapp Could you show a patch build for this? I'm a little skeptical this will resolve the hangs.

@NoahStapp
Copy link
Contributor Author

@NoahStapp Could you show a patch build for this? I'm a little skeptical this will resolve the hangs.

https://spruce.mongodb.com/version/673e2a3b4ccfab00076b6c6b/tasks?sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC

@NoahStapp NoahStapp merged commit 29c16db into mongodb:async-improvements Nov 20, 2024
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants