-
Notifications
You must be signed in to change notification settings - Fork 120
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
Why do client socket resets cause pending Deferred to be cancelled? #546
Comments
Agree that it seems odd for a socket reset by the client to cause all the deferreds associated with the call to be canceled. I'd expect the only impact to be that I cannot write a response on the closed socket. |
See twisted#546. We have been running a similar patch on our enterprise production systems for about 6 months with no issues, and critically we have stopped getting issues on client resets.
If your route is doing a bunch of expensive computation for a client that has gone away, it's wasting resources. The cancellation is how you find out that the place you're writing a response to is gone, so you can stop doing the work to produce the response. The default, I feel, makes sense here. If, at the application level, you have some logic that you want to run to completion regardless of the client being available, you can shield your Deferreds from cancellation. This is tricky enough that we should probably include something in Twisted proper that functions like from functools import wraps
from typing import Callable, ParamSpec, TypeVar
from twisted.internet.defer import Deferred
T = TypeVar("T")
NoCancel = object()
def shield(d: Deferred[T]) -> Deferred[T]:
currentWrapped: Deferred[T]
def rewrap() -> Deferred[T]:
nonlocal currentWrapped
currentWrapped = Deferred(lambda it: it.callback(NoCancel)).addCallback(cancelBlock)
return currentWrapped
def cancelBlock(result: object) -> Deferred[T]:
if result is not NoCancel:
return result
return rewrap()
currentWrapped = rewrap()
d.addBoth(lambda anything: currentWrapped.callback(anything))
return currentWrapped If you're using P = ParamSpec("P")
def shielded(decoratee: Callable[P, Deferred[T]]) -> Callable[P, Deferred[T]]:
@wraps(decoratee)
def decorated(*args: P.args, **kwargs: P.kwargs) -> Deferred[T]:
return shield(decoratee(*args, **kwargs))
return decorated i.e. @app.route("/foo", methods=["POST"])
@shielded
@inlineCallbacks
def doStuff(self, request):
yield foo()
yield bar()
return {"message": "success"} If your entire application is routes like this (everything wants to make a best-effort attempt to run to completion, nothing cares if clients are still listening) then I would be open to having this be If we remove the |
@glyph Thanks for your detailed response! We've currently found a workaround by overriding I disagree that cancelling should be the default behavior. My understanding is that Klein is a library that competes with the likes of Flask or FastAPI, the former of which is not async and the latter of which is non-cancelling by default. I understand that for reads (aka |
@KentShikama You make an interesting case here. I am not totally sure I agree with all of your reasoning here (cancellation remains important to provide a mechanism for defense against e.g. tarpit attacks), but there's clearly a use-case to be supported here, and I think my initial response was indeed too simplistic. |
Imagine you have a route like the following.
And then a client calls
POST /foo
and disconnects right after Klein finishes processingfoo()
. Currently,bar()
would get cancelled. I understand that the socket has closed so that the{"message": "success"}
cannot be written back but it would be nice if the Deferredbar()
was executed. Is there some use case that I'm failing to understand here?Or to put it differently, I believe we should stop executing the
.cancel()
on the Deferred if the request finishes atklein/src/klein/_resource.py
Line 210 in 8964da7
The text was updated successfully, but these errors were encountered: