-
Notifications
You must be signed in to change notification settings - Fork 142
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
Implemented voluntary cancellation in worker threads #629
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Docs impl and tests are good. I don't know what the exact implications of using _parent_cancelled()
for this are, but any edge cases would need documentation rather than fixing.
Without implementing parent task reuse, there might be some divergence of from_thread
behaviors between backends. Most users won't run into that though, so I won't suggest you hold a release over it.
src/anyio/to_thread.py
Outdated
:param cancellable: ``True`` to allow cancellation of the operation | ||
:param abandon_on_cancel: ``True`` to abandon the thread (leaving it to run | ||
unchecked on own) if the host task is cancelled, ``False`` to ignore | ||
cancellations in the host task until the operation has completed in the worker | ||
thread | ||
:param cancellable: deprecated alias of ``abandon_on_cancel`` | ||
:param limiter: capacity limiter to use to limit the total amount of threads running | ||
(if omitted, the default limiter is used) | ||
:return: an awaitable that yields the return value of the function. | ||
|
||
""" | ||
if cancellable is not None: | ||
abandon_on_cancel = cancellable | ||
warn( | ||
"The `cancellable=` keyword argument to `anyio.to_thread.run_sync` is " | ||
"deprecated since AnyIO 4.1.0; use `abandon_on_cancel=` instead", | ||
DeprecationWarning, | ||
stacklevel=2, | ||
) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Allowing both aliases to be passed is a valid choice but departs from trio. Maybe also document/test which one overrides the other.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree if both are passed it'd be nice to throw an error, but then you need to change the types a little 🤷♀️ happy with either implementation in practice since a warning will still be raised.
Can you elaborate on the concept of parent task reuse? |
I think a snippet which demonstrates the differences between this PR and trio would be great. |
Sure, pulled from the test suite: async def test_cancelscope_propagation():
async def async_time_bomb():
cancel_scope.cancel()
with fail_after(1):
await sleep(3)
with CancelScope() as cancel_scope:
await run_sync(from_thread_run, async_time_bomb)
assert cancel_scope.cancelled_caught
async def test_from_thread_reuses_task():
task = get_current_task()
async def async_current_task():
return get_current_task()
assert task is await run_sync(from_thread_run_sync, get_current_task)
assert task is await run_sync(from_thread_run, async_current_task)
assert task is not await run_sync(from_thread_run_sync, get_current_task, abandon_on_cancel=True)
assert task is not await run_sync(from_thread_run, async_current_task, abandon_on_cancel=True) |
@graingert pushed for it in the first place so maybe he can give some concrete use case? |
Right, so what happens here is Task 1 -> worker thread -> Task 2 which then cancels Task 1's scope, but Task 1 can't exit because it still waits for the worker thread. I think this was a good example and I may need to rework the implementation. |
I have a creeping feeling that implementing this for the |
Well on Trio it's implemented as Task 1 -> worker thread -> Task 1 if Again, depending on how long you think it might take to do this on both backends, it might be a higher priority to release as-is and save parent task reuse for the next version. |
I've been scratching my head trying to figure out why |
Oh yeah, I noticed that if I pass the trio token explicitly via |
Ohh...now I see. If an explicit token is provided, it always runs the new task in the system nursery, even if there is a parent task. |
Yeah I view that both as consistency (passing |
I've made some progress on implementing the cancellation semantics for async tasks spawned from worker threads, but it broke some previous tests and I'm still sorting that out. |
Ok, this should have feature parity with Trio now. I have to admit that I don't fully understand why my latest fix worked 😅 but it does. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the tests make a lot of sense, so that makes it easy to trust the implementation, even though that part is relatively confusing.
except CancelledError as exc: | ||
raise concurrent.futures.CancelledError(str(exc)) from None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Won't this break the chain of any exceptions attached to the CancelledError
? I guess that's probably rare.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if the chain is preserved anyway, but I can check if you like.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I checked and it doesn't seem to matter. There's a C level black box between that reraise and the calling synchronous code.
No description provided.