Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 5c00151

Browse files
authored
Add @cancellable decorator, for use on request handlers (#12586)
Signed-off-by: Sean Quah <[email protected]>
1 parent 2aad0ae commit 5c00151

File tree

2 files changed

+62
-0
lines changed

2 files changed

+62
-0
lines changed

changelog.d/12586.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `@cancellable` decorator, for use on endpoint methods that can be cancelled when clients disconnect.

synapse/http/server.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
Optional,
3434
Pattern,
3535
Tuple,
36+
TypeVar,
3637
Union,
3738
)
3839

@@ -92,6 +93,66 @@
9293
HTTP_STATUS_REQUEST_CANCELLED = 499
9394

9495

96+
F = TypeVar("F", bound=Callable[..., Any])
97+
98+
99+
_cancellable_method_names = frozenset(
100+
{
101+
# `RestServlet`, `BaseFederationServlet` and `BaseFederationServerServlet`
102+
# methods
103+
"on_GET",
104+
"on_PUT",
105+
"on_POST",
106+
"on_DELETE",
107+
# `_AsyncResource`, `DirectServeHtmlResource` and `DirectServeJsonResource`
108+
# methods
109+
"_async_render_GET",
110+
"_async_render_PUT",
111+
"_async_render_POST",
112+
"_async_render_DELETE",
113+
"_async_render_OPTIONS",
114+
# `ReplicationEndpoint` methods
115+
"_handle_request",
116+
}
117+
)
118+
119+
120+
def cancellable(method: F) -> F:
121+
"""Marks a servlet method as cancellable.
122+
123+
Methods with this decorator will be cancelled if the client disconnects before we
124+
finish processing the request.
125+
126+
During cancellation, `Deferred.cancel()` will be invoked on the `Deferred` wrapping
127+
the method. The `cancel()` call will propagate down to the `Deferred` that is
128+
currently being waited on. That `Deferred` will raise a `CancelledError`, which will
129+
propagate up, as per normal exception handling.
130+
131+
Before applying this decorator to a new endpoint, you MUST recursively check
132+
that all `await`s in the function are on `async` functions or `Deferred`s that
133+
handle cancellation cleanly, otherwise a variety of bugs may occur, ranging from
134+
premature logging context closure, to stuck requests, to database corruption.
135+
136+
Usage:
137+
class SomeServlet(RestServlet):
138+
@cancellable
139+
async def on_GET(self, request: SynapseRequest) -> ...:
140+
...
141+
"""
142+
if method.__name__ not in _cancellable_method_names:
143+
raise ValueError(
144+
"@cancellable decorator can only be applied to servlet methods."
145+
)
146+
147+
method.cancellable = True # type: ignore[attr-defined]
148+
return method
149+
150+
151+
def is_method_cancellable(method: Callable[..., Any]) -> bool:
152+
"""Checks whether a servlet method has the `@cancellable` flag."""
153+
return getattr(method, "cancellable", False)
154+
155+
95156
def return_json_error(f: failure.Failure, request: SynapseRequest) -> None:
96157
"""Sends a JSON error response to clients."""
97158

0 commit comments

Comments
 (0)