Skip to content

Commit

Permalink
Add websocket ping variables (#1906)
Browse files Browse the repository at this point in the history
* Add config params for websocket ping_timeout & ping_interval

* Include changelog

* Pass websocket config values to WebSocketProtocol init, test

* Linting

* Improve docs

Co-authored-by: shawnhill <[email protected]>
  • Loading branch information
shawnhill and shawnhill-equipmentshare authored Aug 7, 2020
1 parent 0072fd1 commit 58e4087
Show file tree
Hide file tree
Showing 7 changed files with 69 additions and 1 deletion.
3 changes: 3 additions & 0 deletions changelogs/1904.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Adds WEBSOCKET_PING_TIMEOUT and WEBSOCKET_PING_INTERVAL configuration values

Allows setting the ping_interval and ping_timeout arguments when initializing `WebSocketCommonProtocol`.
6 changes: 5 additions & 1 deletion docs/sanic/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ The config files are regular Python files which are executed in order to load th
Builtin Configuration Values
----------------------------

Out of the box there are just a few predefined values which can be overwritten when creating the application.
Out of the box there are just a few predefined values which can be overwritten when creating the application. Note that websocket configuration values will have no impact if running in ASGI mode.

+---------------------------+-------------------+-----------------------------------------------------------------------------+
| Variable | Default | Description |
Expand All @@ -123,6 +123,10 @@ Out of the box there are just a few predefined values which can be overwritten w
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| WEBSOCKET_WRITE_LIMIT | 2^16 | High-water limit of the buffer for outgoing bytes |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| WEBSOCKET_PING_INTERVAL | 20 | A Ping frame is sent every ping_interval seconds. |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| WEBSOCKET_PING_TIMEOUT | 20 | Connection is closed when Pong is not received after ping_timeout seconds |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| GRACEFUL_SHUTDOWN_TIMEOUT | 15.0 | How long to wait to force close non-idle connection (sec) |
+---------------------------+-------------------+-----------------------------------------------------------------------------+
| ACCESS_LOG | True | Disable or enable access log |
Expand Down
4 changes: 4 additions & 0 deletions docs/sanic/websocket.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,9 @@ You could setup your own WebSocket configuration through ``app.config``, like
app.config.WEBSOCKET_MAX_QUEUE = 32
app.config.WEBSOCKET_READ_LIMIT = 2 ** 16
app.config.WEBSOCKET_WRITE_LIMIT = 2 ** 16
app.config.WEBSOCKET_PING_INTERVAL = 20
app.config.WEBSOCKET_PING_TIMEOUT = 20
These settings will have no impact if running in ASGI mode.

Find more in ``Configuration`` section.
2 changes: 2 additions & 0 deletions sanic/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
"WEBSOCKET_MAX_QUEUE": 32,
"WEBSOCKET_READ_LIMIT": 2 ** 16,
"WEBSOCKET_WRITE_LIMIT": 2 ** 16,
"WEBSOCKET_PING_TIMEOUT": 20,
"WEBSOCKET_PING_INTERVAL": 20,
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
"ACCESS_LOG": True,
"FORWARDED_SECRET": None,
Expand Down
19 changes: 19 additions & 0 deletions sanic/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
from signal import SIG_IGN, SIGINT, SIGTERM, Signals
from signal import signal as signal_func
from time import time
from typing import Type

from httptools import HttpRequestParser # type: ignore
from httptools.parser.errors import HttpParserError # type: ignore

from sanic.compat import Header, ctrlc_workaround_for_windows
from sanic.config import Config
from sanic.exceptions import (
HeaderExpectationFailed,
InvalidUsage,
Expand Down Expand Up @@ -844,6 +846,7 @@ def serve(
app.asgi = False

connections = connections if connections is not None else set()
protocol_kwargs = _build_protocol_kwargs(protocol, app.config)
server = partial(
protocol,
loop=loop,
Expand All @@ -852,6 +855,7 @@ def serve(
app=app,
state=state,
unix=unix,
**protocol_kwargs,
)
asyncio_server_kwargs = (
asyncio_server_kwargs if asyncio_server_kwargs else {}
Expand Down Expand Up @@ -948,6 +952,21 @@ def serve(
remove_unix_socket(unix)


def _build_protocol_kwargs(
protocol: Type[HttpProtocol], config: Config
) -> dict:
if hasattr(protocol, "websocket_timeout"):
return {
"max_size": config.WEBSOCKET_MAX_SIZE,
"max_queue": config.WEBSOCKET_MAX_QUEUE,
"read_limit": config.WEBSOCKET_READ_LIMIT,
"write_limit": config.WEBSOCKET_WRITE_LIMIT,
"ping_timeout": config.WEBSOCKET_PING_TIMEOUT,
"ping_interval": config.WEBSOCKET_PING_INTERVAL,
}
return {}


def bind_socket(host: str, port: int, *, backlog=100) -> socket.socket:
"""Create TCP server socket.
:param host: IPv4, IPv6 or hostname may be specified
Expand Down
6 changes: 6 additions & 0 deletions sanic/websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ def __init__(
websocket_max_queue=None,
websocket_read_limit=2 ** 16,
websocket_write_limit=2 ** 16,
websocket_ping_interval=20,
websocket_ping_timeout=20,
**kwargs
):
super().__init__(*args, **kwargs)
Expand All @@ -45,6 +47,8 @@ def __init__(
self.websocket_max_queue = websocket_max_queue
self.websocket_read_limit = websocket_read_limit
self.websocket_write_limit = websocket_write_limit
self.websocket_ping_interval = websocket_ping_interval
self.websocket_ping_timeout = websocket_ping_timeout

# timeouts make no sense for websocket routes
def request_timeout_callback(self):
Expand Down Expand Up @@ -119,6 +123,8 @@ async def websocket_handshake(self, request, subprotocols=None):
max_queue=self.websocket_max_queue,
read_limit=self.websocket_read_limit,
write_limit=self.websocket_write_limit,
ping_interval=self.websocket_ping_interval,
ping_timeout=self.websocket_ping_timeout,
)
# Following two lines are required for websockets 8.x
self.websocket.is_client = False
Expand Down
30 changes: 30 additions & 0 deletions tests/test_app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio
import logging
import sys
from unittest.mock import patch

from inspect import isawaitable

Expand Down Expand Up @@ -148,6 +149,35 @@ async def handler(request, ws):
assert app.websocket_enabled == True


@patch("sanic.app.WebSocketProtocol")
def test_app_websocket_parameters(websocket_protocol_mock, app):
app.config.WEBSOCKET_MAX_SIZE = 44
app.config.WEBSOCKET_MAX_QUEUE = 45
app.config.WEBSOCKET_READ_LIMIT = 46
app.config.WEBSOCKET_WRITE_LIMIT = 47
app.config.WEBSOCKET_PING_TIMEOUT = 48
app.config.WEBSOCKET_PING_INTERVAL = 50

@app.websocket("/ws")
async def handler(request, ws):
await ws.send("test")

try:
# This will fail because WebSocketProtocol is mocked and only the call kwargs matter
app.test_client.get("/ws")
except:
pass

websocket_protocol_call_args = websocket_protocol_mock.call_args
ws_kwargs = websocket_protocol_call_args[1]
assert ws_kwargs["max_size"] == app.config.WEBSOCKET_MAX_SIZE
assert ws_kwargs["max_queue"] == app.config.WEBSOCKET_MAX_QUEUE
assert ws_kwargs["read_limit"] == app.config.WEBSOCKET_READ_LIMIT
assert ws_kwargs["write_limit"] == app.config.WEBSOCKET_WRITE_LIMIT
assert ws_kwargs["ping_timeout"] == app.config.WEBSOCKET_PING_TIMEOUT
assert ws_kwargs["ping_interval"] == app.config.WEBSOCKET_PING_INTERVAL


def test_handle_request_with_nested_exception(app, monkeypatch):

err_msg = "Mock Exception"
Expand Down

0 comments on commit 58e4087

Please sign in to comment.