Skip to content

Commit

Permalink
Merge branch 'feat/stream-patch' of github.com:imgurbot12/sanic into …
Browse files Browse the repository at this point in the history
…feat/stream-patch
  • Loading branch information
imgurbot12 committed Aug 27, 2020
2 parents c6ffbc1 + bf54872 commit 280d953
Show file tree
Hide file tree
Showing 10 changed files with 150 additions and 3 deletions.
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.
4 changes: 3 additions & 1 deletion sanic/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ def get_websocket_connection(self) -> WebSocketConnection:
def create_websocket_connection(
self, send: ASGISend, receive: ASGIReceive
) -> WebSocketConnection:
self._websocket_connection = WebSocketConnection(send, receive)
self._websocket_connection = WebSocketConnection(
send, receive, self.scope.get("subprotocols", [])
)
return self._websocket_connection

def add_task(self) -> None:
Expand Down
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 @@ -845,6 +847,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 @@ -853,6 +856,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 @@ -949,6 +953,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
18 changes: 17 additions & 1 deletion sanic/websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Awaitable,
Callable,
Dict,
List,
MutableMapping,
Optional,
Union,
Expand Down Expand Up @@ -34,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 @@ -44,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 @@ -118,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 All @@ -137,9 +144,11 @@ def __init__(
self,
send: Callable[[ASIMessage], Awaitable[None]],
receive: Callable[[], Awaitable[ASIMessage]],
subprotocols: Optional[List[str]] = None,
) -> None:
self._send = send
self._receive = receive
self.subprotocols = subprotocols or []

async def send(self, data: Union[str, bytes], *args, **kwargs) -> None:
message: Dict[str, Union[str, bytes]] = {"type": "websocket.send"}
Expand All @@ -164,7 +173,14 @@ async def recv(self, *args, **kwargs) -> Optional[str]:
receive = recv

async def accept(self) -> None:
await self._send({"type": "websocket.accept", "subprotocol": ""})
await self._send(
{
"type": "websocket.accept",
"subprotocol": ",".join(
[subprotocol for subprotocol in self.subprotocols]
),
}
)

async def close(self) -> None:
pass
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
47 changes: 47 additions & 0 deletions tests/test_asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,53 @@ async def test_websocket_receive(send, receive, message_stack):
assert text == msg["text"]


@pytest.mark.asyncio
async def test_websocket_accept_with_no_subprotocols(
send, receive, message_stack
):
ws = WebSocketConnection(send, receive)
await ws.accept()

assert len(message_stack) == 1

message = message_stack.popleft()
assert message["type"] == "websocket.accept"
assert message["subprotocol"] == ""
assert "bytes" not in message


@pytest.mark.asyncio
async def test_websocket_accept_with_subprotocol(send, receive, message_stack):
subprotocols = ["graphql-ws"]

ws = WebSocketConnection(send, receive, subprotocols)
await ws.accept()

assert len(message_stack) == 1

message = message_stack.popleft()
assert message["type"] == "websocket.accept"
assert message["subprotocol"] == "graphql-ws"
assert "bytes" not in message


@pytest.mark.asyncio
async def test_websocket_accept_with_multiple_subprotocols(
send, receive, message_stack
):
subprotocols = ["graphql-ws", "hello", "world"]

ws = WebSocketConnection(send, receive, subprotocols)
await ws.accept()

assert len(message_stack) == 1

message = message_stack.popleft()
assert message["type"] == "websocket.accept"
assert message["subprotocol"] == "graphql-ws,hello,world"
assert "bytes" not in message


def test_improper_websocket_connection(transport, send, receive):
with pytest.raises(InvalidUsage):
transport.get_websocket_connection()
Expand Down
20 changes: 20 additions & 0 deletions tests/test_request_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,23 @@ def handler(request):
}
)

@app.middleware("response")
def modify(request, response):
# Using response-middleware to access request ctx
try:
user = request.ctx.user
except AttributeError as e:
user = str(e)
try:
invalid = request.ctx.missing
except AttributeError as e:
invalid = str(e)

j = loads(response.body)
j['response_mw_valid'] = user
j['response_mw_invalid'] = invalid
return json(j)

request, response = app.test_client.get("/")
assert response.json == {
"user": "sanic",
Expand All @@ -41,6 +58,9 @@ def handler(request):
"has_session": True,
"has_missing": False,
"invalid": "'types.SimpleNamespace' object has no attribute 'missing'",
"response_mw_valid": "sanic",
"response_mw_invalid":
"'types.SimpleNamespace' object has no attribute 'missing'"
}


Expand Down

0 comments on commit 280d953

Please sign in to comment.