Skip to content

Commit 4520d2f

Browse files
committed
Configure message buffer (#82)
1 parent d2ce97b commit 4520d2f

File tree

5 files changed

+204
-40
lines changed

5 files changed

+204
-40
lines changed

docs/backpressure.rst

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
Message Queues
2+
==============
3+
4+
.. currentmodule:: trio_websocket
5+
6+
.. TODO This file will grow into a "backpressure" document once #65 is complete.
7+
For now it is just deals with userspace buffers, since this is a related
8+
topic.
9+
10+
When a connection is open, it runs a background task that reads network data and
11+
automatically handles certain types of events for you. For example, if the
12+
background task receives a ping event, then it will automatically send back a
13+
pong event. When the background task receives a message, it places that message
14+
into an internal queue. When you call ``get_message()``, it returns the first
15+
item from this queue.
16+
17+
If this internal message queue does not have any size limits, then a remote
18+
endpoint could rapidly send large messages and use up all of the memory on the
19+
local machine! In almost all situations, the message queue needs to have size
20+
limits, both in terms of the number of items and the size per message. These
21+
limits create an upper bound for the amount of memory that can be used by a
22+
single WebSocket connection. For example, if the queue size is 10 and the
23+
maximum message size is 1 megabyte, then the connection will use at most 10
24+
megabytes of memory.
25+
26+
When the message queue is full, the background task pauses and waits for the
27+
user to remove a message, i.e. call ``get_message()``. When the background task
28+
is paused, it stops processing background events like replying to ping events.
29+
If a message is received that is larger than the maximum message size, then the
30+
connection is automatically closed with code 1009 and the message is discarded.
31+
32+
The library APIs each take arguments to configure the mesage buffer:
33+
``message_queue_size`` and ``max_message_size``. By default the queue size is
34+
one and the maximum message size is 1 MiB. If you set queue size to zero, then
35+
the background task will block every time it receives a message until somebody
36+
calls ``get_message()``. For an unbounded queue—which is strongly
37+
discouraged—set the queue size to ``math.inf``. Likewise, the maximum message
38+
size may also be disabled by setting it to ``math.inf``.

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Autobahn Test Suite <https://github.com/crossbario/autobahn-testsuite>`__.
3232
getting_started
3333
clients
3434
servers
35+
backpressure
3536
timeouts
3637
api
3738
recipes

docs/servers.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ host/port to bind to. The handler function receives a
4141
:class:`WebSocketRequest` object, and it calls the request's
4242
:func:`~WebSocketRequest.accept` method to finish the handshake and obtain a
4343
:class:`WebSocketConnection` object. When the handler function exits, the
44-
connection is automatically closed.
44+
connection is automatically closed. If the handler function raises an
45+
exception, the server will silently close the connection and cancel the
46+
tasks belonging to it.
4547

4648
.. autofunction:: serve_websocket
4749

tests/test_connection.py

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,15 @@
5353

5454
HOST = '127.0.0.1'
5555
RESOURCE = '/resource'
56+
DEFAULT_TEST_MAX_DURATION = 1
5657

5758
# Timeout tests follow a general pattern: one side waits TIMEOUT seconds for an
5859
# event. The other side delays for FORCE_TIMEOUT seconds to force the timeout
5960
# to trigger. Each test also has maximum runtime (measure by Trio's clock) to
6061
# prevent a faulty test from hanging the entire suite.
6162
TIMEOUT = 1
6263
FORCE_TIMEOUT = 2
63-
MAX_TIMEOUT_TEST_DURATION = 3
64+
TIMEOUT_TEST_MAX_DURATION = 3
6465

6566

6667
@pytest.fixture
@@ -89,12 +90,6 @@ async def echo_request_handler(request):
8990
Accept incoming request and then pass off to echo connection handler.
9091
'''
9192
conn = await request.accept()
92-
await echo_conn_handler(conn)
93-
94-
95-
async def echo_conn_handler(conn):
96-
''' A connection handler that reads one message, sends back the same
97-
message, then exits. '''
9893
try:
9994
msg = await conn.get_message()
10095
await conn.send_message(msg)
@@ -391,7 +386,7 @@ async def handler(stream):
391386
await client.send_message('Hello from client!')
392387

393388

394-
@fail_after(MAX_TIMEOUT_TEST_DURATION)
389+
@fail_after(TIMEOUT_TEST_MAX_DURATION)
395390
async def test_client_open_timeout(nursery, autojump_clock):
396391
'''
397392
The client times out waiting for the server to complete the opening
@@ -411,7 +406,7 @@ async def handler(request):
411406
pass
412407

413408

414-
@fail_after(MAX_TIMEOUT_TEST_DURATION)
409+
@fail_after(TIMEOUT_TEST_MAX_DURATION)
415410
async def test_client_close_timeout(nursery, autojump_clock):
416411
'''
417412
This client times out waiting for the server to complete the closing
@@ -430,15 +425,16 @@ async def handler(request):
430425
pytest.fail('Should not reach this line.')
431426

432427
server = await nursery.start(
433-
partial(serve_websocket, handler, HOST, 0, ssl_context=None))
428+
partial(serve_websocket, handler, HOST, 0, ssl_context=None,
429+
message_queue_size=0))
434430

435431
with pytest.raises(trio.TooSlowError):
436432
async with open_websocket(HOST, server.port, RESOURCE, use_ssl=False,
437433
disconnect_timeout=TIMEOUT) as client_ws:
438434
await client_ws.send_message('test')
439435

440436

441-
@fail_after(MAX_TIMEOUT_TEST_DURATION)
437+
@fail_after(TIMEOUT_TEST_MAX_DURATION)
442438
async def test_server_open_timeout(autojump_clock):
443439
'''
444440
The server times out waiting for the client to complete the opening
@@ -470,7 +466,7 @@ async def handler(request):
470466
nursery.cancel_scope.cancel()
471467

472468

473-
@fail_after(MAX_TIMEOUT_TEST_DURATION)
469+
@fail_after(TIMEOUT_TEST_MAX_DURATION)
474470
async def test_server_close_timeout(autojump_clock):
475471
'''
476472
The server times out waiting for the client to complete the closing
@@ -488,7 +484,7 @@ async def handler(request):
488484
ws = await request.accept()
489485
# Send one message to block the client's reader task:
490486
await ws.send_message('test')
491-
import logging
487+
492488
async with trio.open_nursery() as outer:
493489
server = await outer.start(partial(serve_websocket, handler, HOST, 0,
494490
ssl_context=None, handler_nursery=outer,
@@ -523,7 +519,6 @@ async def handler(request):
523519
with pytest.raises(ConnectionClosed):
524520
await server_ws.get_message()
525521
server = await nursery.start(serve_websocket, handler, HOST, 0, None)
526-
port = server.port
527522
stream = await trio.open_tcp_stream(HOST, server.port)
528523
client_ws = await wrap_client_stream(nursery, stream, HOST, RESOURCE)
529524
async with client_ws:
@@ -566,12 +561,14 @@ async def handler(request):
566561
assert exc.reason.name == 'NORMAL_CLOSURE'
567562

568563

569-
@pytest.mark.skip(reason='Hangs because channel size is hard coded to 0')
564+
@fail_after(DEFAULT_TEST_MAX_DURATION)
570565
async def test_read_messages_after_remote_close(nursery):
571566
'''
572567
When the remote endpoint closes, the local endpoint can still read all
573568
of the messages sent prior to closing. Any attempt to read beyond that will
574569
raise ConnectionClosed.
570+
571+
This test also exercises the configuration of the queue size.
575572
'''
576573
server_closed = trio.Event()
577574

@@ -585,7 +582,10 @@ async def handler(request):
585582
server = await nursery.start(
586583
partial(serve_websocket, handler, HOST, 0, ssl_context=None))
587584

588-
async with open_websocket(HOST, server.port, '/', use_ssl=False) as client:
585+
# The client needs a message queue of size 2 so that it can buffer both
586+
# incoming messages without blocking the reader task.
587+
async with open_websocket(HOST, server.port, '/', use_ssl=False,
588+
message_queue_size=2) as client:
589589
await server_closed.wait()
590590
assert await client.get_message() == '1'
591591
assert await client.get_message() == '2'
@@ -618,12 +618,49 @@ async def handler(request):
618618
client_closed.set()
619619

620620

621-
async def test_client_cm_exit_with_pending_messages(echo_server, autojump_clock):
621+
async def test_cm_exit_with_pending_messages(echo_server, autojump_clock):
622+
'''
623+
Regression test for #74, where a context manager was not able to exit when
624+
there were pending messages in the receive queue.
625+
'''
622626
with trio.fail_after(1):
623627
async with open_websocket(HOST, echo_server.port, RESOURCE,
624628
use_ssl=False) as ws:
625629
await ws.send_message('hello')
626630
# allow time for the server to respond
627631
await trio.sleep(.1)
628-
# bug: context manager exit is blocked on unconsumed message
629-
#await ws.get_message()
632+
633+
634+
@fail_after(DEFAULT_TEST_MAX_DURATION)
635+
async def test_max_message_size(nursery):
636+
'''
637+
Set the client's max message size to 100 bytes. The client can send a
638+
message larger than 100 bytes, but when it receives a message larger than
639+
100 bytes, it closes the connection with code 1009.
640+
'''
641+
async def handler(request):
642+
''' Similar to the echo_request_handler fixture except it runs in a
643+
loop. '''
644+
conn = await request.accept()
645+
while True:
646+
try:
647+
msg = await conn.get_message()
648+
await conn.send_message(msg)
649+
except ConnectionClosed:
650+
break
651+
652+
server = await nursery.start(
653+
partial(serve_websocket, handler, HOST, 0, ssl_context=None))
654+
655+
async with open_websocket(HOST, server.port, RESOURCE, use_ssl=False,
656+
max_message_size=100) as client:
657+
# We can send and receive 100 bytes:
658+
await client.send_message(b'A' * 100)
659+
msg = await client.get_message()
660+
assert len(msg) == 100
661+
# We can send 101 bytes but cannot receive 101 bytes:
662+
await client.send_message(b'B' * 101)
663+
with pytest.raises(ConnectionClosed):
664+
await client.get_message()
665+
assert client.closed
666+
assert client.closed.code == 1009

0 commit comments

Comments
 (0)