-
-
Notifications
You must be signed in to change notification settings - Fork 953
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
Avoid unexpected background task cancellation #1699
Conversation
Unfortunately, this is not a solution. 😞 More info on why this is not a solution: #1441 |
Okay, this can be resolved by adding these lines: t = anyio.get_current_task()
if t.name == "anyio.from_thread.BlockingPortal._call_func":
# cancel stuck task due to discarded response
# see: https://github.com/encode/starlette/issues/1022
task_group.cancel_scope.cancel() |
@Kludex Can you review this approach? |
This issue has 4 years. A bit of explanation on what you're doing here would be cool... |
After tracing the tasks under CancelScope, I finally found that task. Probably due to silenced anyio's WouldBlock exception. |
ConclusionTo sum up, to solve the problem of #1022, the Then the approach is simple: just cancel the stuck one, which is PostmortemIf we scrutinize the whole stack and trace which task got stuck, the async def call_next(request: Request) -> Response:
app_exc: typing.Optional[Exception] = None
send_stream, recv_stream = anyio.create_memory_object_stream()
async def coro() -> None:
nonlocal app_exc
async with send_stream:
try:
await self.app(scope, request.receive, send_stream.send)
except Exception as exc:
app_exc = exc
task_group.start_soon(coro) Let's dig into the # from anyio/streams/memory.py
async def send(self, item: T_Item) -> None:
await checkpoint()
try:
self.send_nowait(item)
except WouldBlock:
# Wait until there's someone on the receiving end
send_event = Event()
self._state.waiting_senders[send_event] = item
try:
await send_event.wait()
except BaseException:
self._state.waiting_senders.pop(send_event, None) # type: ignore[arg-type]
raise
if self._state.waiting_senders.pop(send_event, None): # type: ignore[arg-type]
raise BrokenResourceError We got stuck with class Event(BaseEvent):
def __new__(cls) -> "Event":
return object.__new__(cls)
def __init__(self) -> None:
self._event = asyncio.Event()
def set(self) -> DeprecatedAwaitable:
self._event.set()
return DeprecatedAwaitable(self.set)
def is_set(self) -> bool:
return self._event.is_set()
async def wait(self) -> None:
if await self._event.wait():
await checkpoint() From the documentation of
SolutionLet's recap the In order to resolve this problem, you either consume it manually: class CustomMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
resp = await call_next(request)
async def _send(m):
pass
await resp.stream_response(_send)
return PlainTextResponse("Custom") Or just ignore the class CustomMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
return PlainTextResponse("Custom")
===UPDATE=== |
Okay, just to shield the background tasks: diff --git a/starlette/background.py b/starlette/background.py
index 4aaf7ae..db9b38a 100644
--- a/starlette/background.py
+++ b/starlette/background.py
@@ -1,6 +1,8 @@
import sys
import typing
+import anyio
+
if sys.version_info >= (3, 10): # pragma: no cover
from typing import ParamSpec
else: # pragma: no cover
@@ -22,10 +24,11 @@ class BackgroundTask:
self.is_async = is_async_callable(func)
async def __call__(self) -> None:
- if self.is_async:
- await self.func(*self.args, **self.kwargs)
- else:
- await run_in_threadpool(self.func, *self.args, **self.kwargs)
+ with anyio.CancelScope(shield=True):
+ if self.is_async:
+ await self.func(*self.args, **self.kwargs)
+ else:
+ await run_in_threadpool(self.func, *self.args, **self.kwargs) |
But then only shielding the bkg tasks will be enough... Like #1654 |
There's no need to call |
As the reporter on #1438, I would say two things about this PR:
|
The background tasks are shielded in |
Hope to see a more permanent solution to this eventually. For anyone struggling with this and using asyncio... import asyncio
async def doit():
await asyncio.sleep(5)
async def main(request):
await asyncio.shield(doit())
return PlainTextResponse('Hello') The only downside is of the user closes the window before the process completes, you will get a If you need this for a sync function, just use |
Looks like Trio-esque Task Groups and Exception Groups are built into |
The starting point for contributions should usually be a discussion
Simple documentation typos may be raised as stand-alone pull requests, but otherwise please ensure you've discussed your proposal prior to issuing a pull request.
This will help us direct work appropriately, and ensure that any suggested changes have been okayed by the maintainers.