|
33 | 33 | Optional,
|
34 | 34 | Pattern,
|
35 | 35 | Tuple,
|
| 36 | + TypeVar, |
36 | 37 | Union,
|
37 | 38 | )
|
38 | 39 |
|
|
92 | 93 | HTTP_STATUS_REQUEST_CANCELLED = 499
|
93 | 94 |
|
94 | 95 |
|
| 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 | + |
95 | 156 | def return_json_error(f: failure.Failure, request: SynapseRequest) -> None:
|
96 | 157 | """Sends a JSON error response to clients."""
|
97 | 158 |
|
|
0 commit comments