mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2025-05-02 22:14:55 -04:00
Iteratively encode JSON responses to avoid blocking the reactor. (#8013)
This commit is contained in:
parent
25e55d2598
commit
2f4d60a5ba
5 changed files with 94 additions and 13 deletions
|
@ -22,12 +22,13 @@ import types
|
|||
import urllib
|
||||
from http import HTTPStatus
|
||||
from io import BytesIO
|
||||
from typing import Any, Callable, Dict, Tuple, Union
|
||||
from typing import Any, Callable, Dict, Iterator, List, Tuple, Union
|
||||
|
||||
import jinja2
|
||||
from canonicaljson import encode_canonical_json, encode_pretty_printed_json
|
||||
from canonicaljson import iterencode_canonical_json, iterencode_pretty_printed_json
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.internet import defer
|
||||
from twisted.internet import defer, interfaces
|
||||
from twisted.python import failure
|
||||
from twisted.web import resource
|
||||
from twisted.web.server import NOT_DONE_YET, Request
|
||||
|
@ -499,6 +500,78 @@ class RootOptionsRedirectResource(OptionsResource, RootRedirect):
|
|||
pass
|
||||
|
||||
|
||||
@implementer(interfaces.IPullProducer)
|
||||
class _ByteProducer:
|
||||
"""
|
||||
Iteratively write bytes to the request.
|
||||
"""
|
||||
|
||||
# The minimum number of bytes for each chunk. Note that the last chunk will
|
||||
# usually be smaller than this.
|
||||
min_chunk_size = 1024
|
||||
|
||||
def __init__(
|
||||
self, request: Request, iterator: Iterator[bytes],
|
||||
):
|
||||
self._request = request
|
||||
self._iterator = iterator
|
||||
|
||||
def start(self) -> None:
|
||||
self._request.registerProducer(self, False)
|
||||
|
||||
def _send_data(self, data: List[bytes]) -> None:
|
||||
"""
|
||||
Send a list of strings as a response to the request.
|
||||
"""
|
||||
if not data:
|
||||
return
|
||||
self._request.write(b"".join(data))
|
||||
|
||||
def resumeProducing(self) -> None:
|
||||
# We've stopped producing in the meantime (note that this might be
|
||||
# re-entrant after calling write).
|
||||
if not self._request:
|
||||
return
|
||||
|
||||
# Get the next chunk and write it to the request.
|
||||
#
|
||||
# The output of the JSON encoder is coalesced until min_chunk_size is
|
||||
# reached. (This is because JSON encoders produce a very small output
|
||||
# per iteration.)
|
||||
#
|
||||
# Note that buffer stores a list of bytes (instead of appending to
|
||||
# bytes) to hopefully avoid many allocations.
|
||||
buffer = []
|
||||
buffered_bytes = 0
|
||||
while buffered_bytes < self.min_chunk_size:
|
||||
try:
|
||||
data = next(self._iterator)
|
||||
buffer.append(data)
|
||||
buffered_bytes += len(data)
|
||||
except StopIteration:
|
||||
# The entire JSON object has been serialized, write any
|
||||
# remaining data, finalize the producer and the request, and
|
||||
# clean-up any references.
|
||||
self._send_data(buffer)
|
||||
self._request.unregisterProducer()
|
||||
self._request.finish()
|
||||
self.stopProducing()
|
||||
return
|
||||
|
||||
self._send_data(buffer)
|
||||
|
||||
def stopProducing(self) -> None:
|
||||
self._request = None
|
||||
|
||||
|
||||
def _encode_json_bytes(json_object: Any) -> Iterator[bytes]:
|
||||
"""
|
||||
Encode an object into JSON. Returns an iterator of bytes.
|
||||
"""
|
||||
for chunk in json_encoder.iterencode(json_object):
|
||||
yield chunk.encode("utf-8")
|
||||
|
||||
|
||||
def respond_with_json(
|
||||
request: Request,
|
||||
code: int,
|
||||
|
@ -533,15 +606,23 @@ def respond_with_json(
|
|||
return None
|
||||
|
||||
if pretty_print:
|
||||
json_bytes = encode_pretty_printed_json(json_object) + b"\n"
|
||||
encoder = iterencode_pretty_printed_json
|
||||
else:
|
||||
if canonical_json or synapse.events.USE_FROZEN_DICTS:
|
||||
# canonicaljson already encodes to bytes
|
||||
json_bytes = encode_canonical_json(json_object)
|
||||
encoder = iterencode_canonical_json
|
||||
else:
|
||||
json_bytes = json_encoder.encode(json_object).encode("utf-8")
|
||||
encoder = _encode_json_bytes
|
||||
|
||||
return respond_with_json_bytes(request, code, json_bytes, send_cors=send_cors)
|
||||
request.setResponseCode(code)
|
||||
request.setHeader(b"Content-Type", b"application/json")
|
||||
request.setHeader(b"Cache-Control", b"no-cache, no-store, must-revalidate")
|
||||
|
||||
if send_cors:
|
||||
set_cors_headers(request)
|
||||
|
||||
producer = _ByteProducer(request, encoder(json_object))
|
||||
producer.start()
|
||||
return NOT_DONE_YET
|
||||
|
||||
|
||||
def respond_with_json_bytes(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue