Skip to content

Commit 754fd30

Browse files
committed
Rework SSLStream's receive size handling
1 parent 1c16947 commit 754fd30

File tree

1 file changed

+35
-7
lines changed

1 file changed

+35
-7
lines changed

trio/_ssl.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,30 @@
165165
# SSLStream
166166
################################################################
167167

168-
# XX TODO: this number was pulled out of a hat. We should tune it with
169-
# science.
170-
DEFAULT_RECEIVE_SIZE = 65536
168+
# Ideally, when the user calls SSLStream.receive_some() with no argument, then
169+
# we should do exactly one call to self.transport_stream.receive_some(),
170+
# decrypt everything we got, and return it. Unfortunately, the way openssl's
171+
# API works, we have to pick how much data we want to allow when we call
172+
# read(), and then it (potentially) triggers a call to
173+
# transport_stream.receive_some(). So at the time we pick the amount of data
174+
# to decrypt, we don't know how much data we've read. As a simple heuristic,
175+
# we record the max amount of data returned by previous calls to
176+
# transport_stream.receive_some(), and we use that for future calls to read().
177+
# But what do we use for the very first call? That's what this constant sets.
178+
#
179+
# Note that the value passed to read() is a limit on the amount of
180+
# *decrypted* data, but we can only see the size of the *encrypted* data
181+
# returned by transport_stream.receive_some(). TLS adds a small amount of
182+
# framing overhead, and TLS compression is rarely used these days because it's
183+
# insecure. So the size of the encrypted data should be a slight over-estimate
184+
# of the size of the decrypted data, which is exactly what we want.
185+
#
186+
# The specific value is not really based on anything; it might be worth tuning
187+
# at some point. But, if you have an TCP connection with the typical 1500 byte
188+
# MTU and an initial window of 10 (see RFC 6928), then the initial burst of
189+
# data will be limited to ~15000 bytes (or a bit less due to IP-level framing
190+
# overhead), so this is chosen to be larger than that.
191+
STARTING_RECEIVE_SIZE = 16384
171192

172193

173194
class NeedHandshakeError(Exception):
@@ -342,6 +363,8 @@ def __init__(
342363
"another task is currently receiving data on this SSLStream"
343364
)
344365

366+
self._estimated_receive_size = STARTING_RECEIVE_SIZE
367+
345368
_forwarded = {
346369
"context",
347370
"server_side",
@@ -537,6 +560,9 @@ async def _retry(self, fn, *args, ignore_want_read=False):
537560
if not data:
538561
self._incoming.write_eof()
539562
else:
563+
self._estimated_receive_size = max(
564+
self._estimated_receive_size, len(data)
565+
)
540566
self._incoming.write(data)
541567
self._inner_recv_count += 1
542568
if not yielded:
@@ -617,10 +643,12 @@ async def receive_some(self, max_bytes=None):
617643
else:
618644
raise
619645
if max_bytes is None:
620-
# Heuristic: normally we use DEFAULT_RECEIVE_SIZE, but if
621-
# the transport gave us a bunch of data last time then we'll
622-
# try to decrypt and pass it all back at once.
623-
max_bytes = max(DEFAULT_RECEIVE_SIZE, self._incoming.pending)
646+
# If we somehow have more data already in our pending buffer
647+
# than the estimate receive size, bump up our size a bit for
648+
# this read only.
649+
max_bytes = max(
650+
self._estimated_receive_size, self._incoming.pending
651+
)
624652
else:
625653
max_bytes = _operator.index(max_bytes)
626654
if max_bytes < 1:

0 commit comments

Comments
 (0)