Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement timeouts (#64) #89

Merged
merged 2 commits into from
Nov 17, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Autobahn Test Suite <https://github.com/crossbario/autobahn-testsuite>`__.
getting_started
clients
servers
timeouts
api
recipes
contributing
Expand Down
2 changes: 1 addition & 1 deletion docs/recipes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ feature.
await trio.sleep(interval)

async def main():
async with open_websocket_url('ws://localhost/foo') as ws:
async with open_websocket_url('ws://my.example/') as ws:
belm0 marked this conversation as resolved.
Show resolved Hide resolved
async with trio.open_nursery() as nursery:
nursery.start_soon(heartbeat, ws, 5, 1)
# Your application code goes here:
Expand Down
3 changes: 2 additions & 1 deletion docs/servers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ As explained in the tutorial, a WebSocket server needs a handler function and a
host/port to bind to. The handler function receives a
:class:`WebSocketRequest` object, and it calls the request's
:func:`~WebSocketRequest.accept` method to finish the handshake and obtain a
:class:`WebSocketConnection` object.
:class:`WebSocketConnection` object. When the handler function exits, the
connection is automatically closed.

.. autofunction:: serve_websocket

Expand Down
180 changes: 180 additions & 0 deletions docs/timeouts.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
Timeouts
========

.. currentmodule:: trio_websocket

Networking code is inherently complex due to the unpredictable nature of network
failures and the possibility of a remote peer that is coded incorrectly—or even
maliciously! Therefore, your code needs to deal with unexpected circumstances.
One common failure mode that you should guard against is a slow or unresponsive
peer.

This page describes the timeout behavior in ``trio-websocket`` and shows various
examples for implementing timeouts in your own code. Before reading this, you
might find it helpful to read `"Timeouts and cancellation for humans"
<https://vorpus.org/blog/timeouts-and-cancellation-for-humans/>`__, an article
written by Trio's author that describes an overall philosophy regarding
timeouts. The short version is that Trio discourages libraries from using
internal timeouts. Instead, it encourages the caller to enforce timeouts, which
makes timeout code easier to compose and reason about.

On the other hand, this library is intended to be safe to use, and omitting
timeouts could be a dangerous flaw. Therefore, this library contains takes a
mehaase marked this conversation as resolved.
Show resolved Hide resolved
balanced approach to timeouts, where high-level APIs have internal timeouts, but
you may disable them or use lower-level APIs if you want more control over the
behavior.

Built-in Client Timeouts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer only capitalizing the first word in headings: less of a distracting artifact

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not opposed to the style change, but all of the other documentation has headings that are capitalized like this.

------------------------

The high-level client APIs :func:`open_websocket` and :func:`open_websocket_url`
contain built-in timeouts for connecting to a WebSocket and disconnecting from a
WebSocket. These timeouts are built-in for two reasons:

1. Omitting timeouts may be dangerous, and this library strives to make safe
code easy to write.
2. These high-level APIs are context managers, and composing timeouts with
context managers is tricky.

These built-in timeouts make it easy to write a WebSocket client that won't hang
indefinitely if the remote endpoint or network are misbehaving. The following
example shows a connect timeout of 10 seconds. This guarantees that the block
will start executing (reaching the line that prints "Connected") within 10
seconds. When the context manager exits after the ``print(Received response:
…)``, the disconnect timeout guarantees that it will take no more than 5 seconds
to reach the line that prints "Disconnected". If either timeout is exceeded,
then the entire block raises ``trio.TooSlowError``.

.. code-block:: python

async with open_websocket_url('ws://my.example/', connect_timeout=10,
disconnect_timeout=5) as ws:
print("Connected")
await ws.send_message('hello from client!')
response = await ws.get_message()
print('Received response: {}'.format(response))
print("Disconnected")

.. note::

The built-in timeouts do not affect the contents of the block! In this
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe "not affect websocket messaging within the context manger scope"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you objecting to the phrase "affect the contents" or the term "block"?

I think the former is accurate, in the sense that nothing you do inside that block will be affected by these timeout arguments, i.e. even if you trio.sleep(math.inf) that wouldn't time out. It would be overly narrow to say that the timeouts don't affect messaging.

As for "block", I'm uncertain. I originally wrote something more verbose like "context manager block" but it felt unwieldy to keep using that phrase. Maybe spell it out once and then abbreviate it as CM block?

example, the client waits to receive a message. If the server never sends a
message, then the client will block indefinitely on ``ws.get_message()``.
Placing timeouts inside blocks is discussed below.

What if you decided that you really wanted to manage the timeouts yourself? The
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps open with this example, as motivation for adding timeouts to the API, and to make it clear what the API timeouts are doing under the hood?

following example implements the same timeout behavior explicitly, without
relying on the library's built-in timeouts.

.. code-block:: python

with trio.move_on_after(10) as cancel_scope:
async with open_websocket_url('ws://my.example',
connect_timeout=math.inf, disconnect_timeout=math.inf):
print("Connected")
cancel_scope.deadline = math.inf
await ws.send_message('hello from client!')
response = await ws.get_message()
print('Received response: {}'.format(response))
cancel_scope.deadline = trio.current_time() + 5
print("Disconnected")

Notice that the library's internal timeouts are disabled by passing
``math.inf``. This example is less ergonomic than using the built-in timeouts.
If you really want to customize this behavior, you may want to use the low-level
APIs instead, which are discussed below.

Timeouts Inside Blocks
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still looking for something more descriptive-- we mean the timeouts of messaging other than open/close

----------------------

The built-in timeouts do not apply to the contents of the block. One of the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another thought on reordering: start with this example, because it's a common use, and the library won't provide any API for it. Then mention that these timeouts don't apply to connect/disconnect, and introduce the example of controlling those timeouts manually. Then introduce the API's connect/disconnect timeouts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great suggestion! I will reorder this doc a bit.

examples above would hang on ``ws.get_message()`` if the remote endpoint never
sends a message. If you want to enforce a timeout in this situation, you must to
do it explicitly:

.. code-block:: python

async with open_websocket_url('ws://my.example/', connect_timeout=10,
disconnect_timeout=5) as ws:
with trio.fail_after(15):
msg = await ws.get_message()
print('Received message: {}'.format(msg))

This example waits up to 15 seconds to get one message from the server, raising
``trio.TooSlowError`` if the timeout is exceeded. Notice in this example that
the message timeout is larger than the connect and disconnect timeouts,
illustrating that the connect and disconnect timeouts do not apply to the
contents of the block.

Alternatively, you might apply one timeout to the entire operation: connect to
the server, get one message, and disconnect.

.. code-block:: python

with trio.fail_after(15):
async with open_websocket_url('ws://my.example/',
connect_timeout=math.inf, disconnect_timeout=math.inf) as ws:
msg = await ws.get_message()
print('Received message: {}'.format(msg))

Note that the internal timeouts are disabled in this example.

Timeouts on Low-level APIs
--------------------------

We saw an example above where explicit timeouts were applied to the context
managers. In practice, if you need to customize timeout behavior, the low-level
APIs like :func:`connect_websocket_url` etc. will be clearer and easier to use.
This example implements the same timeouts above using the low-level APIs.

.. code-block:: python

with trio.fail_after(10):
connection = await connect_websocket_url('ws://my.example/')
print("Connected")
try:
await ws.send_message('hello from client!')
response = await ws.get_message()
print('Received response: {}'.format(response))
finally:
with trio.fail_after(5):
await connection.aclose()
print("Disconnected")

The low-level APIs make the timeout code easier to read, but we also have to add
try/finally blocks if we want the same behavior that the context manager
guarantees.

Built-in Server Timeouts
------------------------

The server API also offer built-in timeouts. These timeouts are configured when
the server is created, and they are enforced on each connection.

.. code-block:: python

async def handler(request):
ws = await request.accept()
msg = await ws.get_message()
print('Received message: {}'.format(msg))

await serve_websocket(handler, 'localhost', 8080, ssl_context=None,
connect_timeout=10, disconnect_timeout=5)

The server timeouts work slightly differently from the client timeouts. The
connect timeout measures the time between when a TCP connection is received and
when the user's handler is called. As a consequence, the connect timeout
includes waiting for the client's side of the handshake, which is represented by
the ``request`` object. *It does not include the server's side of the
handshake,* because the server handshake needs to be performed inside the user's
handler, i.e. ``await request.accept()``. The disconnect timeout applies to the
time between the handler exiting and the connection being closed.

Each handler is spawned inside of a nursery, so there is no way for connect and
disconnect timeouts to raise exceptions to your code. Instead, connect timeouts
result cause the connection to be silently closed, and handler is never called.
mehaase marked this conversation as resolved.
Show resolved Hide resolved
For disconnect timeouts, your handler has already exited, so a timeout will
cause the connection to be silently closed.

As with the client APIs, you can disable the internal timeouts by passing
``math.inf``.
Loading