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

New websockets #2158

Merged
merged 22 commits into from
Sep 29, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
190ce7b
First attempt at new Websockets implementation based on websockets >=…
ashleysommer Jun 8, 2021
4b814ec
Merge remote-tracking branch 'origin/main' into new_websockets
ashleysommer Jun 9, 2021
4e9d984
Update sanic/websocket.py
ashleysommer Jun 16, 2021
2c8f750
Update sanic/websocket.py
ashleysommer Jun 16, 2021
e2d0198
Update sanic/websocket.py
ashleysommer Jun 16, 2021
e6379ea
Merge remote-tracking branch 'origin/main' into new_websockets
ashleysommer Aug 31, 2021
98785e7
Merge remote-tracking branch 'ashleysommer_github/new_websockets' int…
ashleysommer Sep 1, 2021
cc2082c
wip, update websockets code to new Sans/IO API
ashleysommer Sep 1, 2021
e687517
Merge branch 'main' into new_websockets
ashleysommer Sep 1, 2021
2436780
Refactored new websockets impl into own modules
ashleysommer Sep 1, 2021
b24e914
Merge remote-tracking branch 'origin/main' into new_websockets
ashleysommer Sep 15, 2021
aea3538
Another round of work on the new websockets impl
ashleysommer Sep 15, 2021
13d49b8
Further new websockets impl fixes
ashleysommer Sep 15, 2021
5f6cc06
Change a warning message to debug level
ashleysommer Sep 23, 2021
37d462a
Fix flake8 errors
ashleysommer Sep 23, 2021
cb495ac
Fix a couple of missed failing tests
ashleysommer Sep 23, 2021
955d515
remove websocket bench from examples
ashleysommer Sep 26, 2021
19c98b9
Integrate suggestions from code reviews
ashleysommer Sep 26, 2021
cd26e00
Merge branch 'main' into new_websockets
ahopkins Sep 27, 2021
791b693
Fix long line lengths of debug messages
ashleysommer Sep 28, 2021
f264477
remove unused import in websocket example app
ashleysommer Sep 28, 2021
2162854
re-run isort after Flake8 fixes
ashleysommer Sep 28, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 0 additions & 12 deletions sanic/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -762,22 +762,10 @@ async def handle_request(self, request: Request):
async def _websocket_handler(
self, handler, request, *args, subprotocols=None, **kwargs
):
request.app = self
if not getattr(handler, "__blueprintname__", False):
request._name = handler.__name__
else:
request._name = (
getattr(handler, "__blueprintname__", "") + handler.__name__
)

pass

if self.asgi:
ws = request.transport.get_websocket_connection()
else:
protocol = request.transport.get_protocol()
protocol.app = self

ws = await protocol.websocket_handshake(request, subprotocols)

# schedule the application handler
Expand Down
2 changes: 0 additions & 2 deletions sanic/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes
"REQUEST_TIMEOUT": 60, # 60 seconds
"RESPONSE_TIMEOUT": 60, # 60 seconds
"WEBSOCKET_MAX_QUEUE": 32,
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte
"WEBSOCKET_PING_INTERVAL": 20,
"WEBSOCKET_PING_TIMEOUT": 20,
Expand All @@ -62,7 +61,6 @@ class Config(dict):
REQUEST_MAX_SIZE: int
REQUEST_TIMEOUT: int
RESPONSE_TIMEOUT: int
WEBSOCKET_MAX_QUEUE: int
WEBSOCKET_MAX_SIZE: int
WEBSOCKET_PING_INTERVAL: int
WEBSOCKET_PING_TIMEOUT: int
Expand Down
7 changes: 5 additions & 2 deletions sanic/mixins/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,11 @@ def decorator(handler):
"Expected either string or Iterable of host strings, "
"not %s" % host
)

if isinstance(subprotocols, (list, tuple, set)):
if isinstance(subprotocols, list):
# Ordered subprotocols, maintain order
subprotocols = tuple(subprotocols)
if isinstance(subprotocols, set):
ahopkins marked this conversation as resolved.
Show resolved Hide resolved
# subprotocol is unordered, keep it unordered
subprotocols = frozenset(subprotocols)

route = FutureRoute(
Expand Down
165 changes: 106 additions & 59 deletions sanic/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,21 +99,118 @@ def __init__(self, transport: TransportProtocol, unix=None):
self.client_port = addr[1]


class HttpProtocol(asyncio.Protocol):
"""
This class provides a basic HTTP implementation of the sanic framework.
"""

class SanicProtocol(asyncio.Protocol):
__slots__ = (
# app
"app",
# event loop, connection
"loop",
"transport",
"connections",
"signal",
"conn_info",
"ctx",
"signal",
"_can_write",
"_time",
"_task",
"_unix",
"_data_received",
)

def __init__(
self,
*,
loop,
app: Sanic,
signal=None,
connections=None,
unix=None,
**kwargs,
):
asyncio.set_event_loop(loop)
self.loop = loop
self.app: Sanic = app
self.signal = signal or Signal()
self.transport: Optional[Transport] = None
self.connections = connections if connections is not None else set()
self.conn_info: Optional[ConnInfo] = None
self._can_write = asyncio.Event()
self._can_write.set()
self._unix = unix
self._time = 0.0 # type: float
self._task = None # type: Optional[asyncio.Task]
ahopkins marked this conversation as resolved.
Show resolved Hide resolved
self._data_received = asyncio.Event()

@property
def ctx(self):
if self.conn_info is not None:
return self.conn_info.ctx
else:
return self.ctx
ahopkins marked this conversation as resolved.
Show resolved Hide resolved

async def send(self, data):
"""
Writes data with backpressure control.
"""
await self._can_write.wait()
if self.transport.is_closing():
raise CancelledError
self.transport.write(data)
self._time = current_time()

def close(self):
"""
Force close the connection.
"""
# Cause a call to connection_lost where further cleanup occurs
if self.transport:
self.transport.close()
self.transport = None

# asyncio.Protocol API Callbacks #
# ------------------------------ #
def connection_made(self, transport):
try:
# TODO: Benchmark to find suitable write buffer limits
ahopkins marked this conversation as resolved.
Show resolved Hide resolved
transport.set_write_buffer_limits(low=16384, high=65536)
self.connections.add(self)
self.transport = transport
self.conn_info = ConnInfo(self.transport, unix=self._unix)
except Exception:
error_logger.exception("protocol.connect_made")

def connection_lost(self, exc):
try:
self.connections.discard(self)
self.resume_writing()
if self._task:
self._task.cancel()
except BaseException:
ahopkins marked this conversation as resolved.
Show resolved Hide resolved
error_logger.exception("protocol.connection_lost")

def pause_writing(self):
self._can_write.clear()

def resume_writing(self):
self._can_write.set()

def data_received(self, data: bytes):
try:
self._time = current_time()
if not data:
return self.close()

if self._data_received:
self._data_received.set()
except BaseException:
error_logger.exception("protocol.data_received")


class HttpProtocol(SanicProtocol):
ahopkins marked this conversation as resolved.
Show resolved Hide resolved
"""
This class provides a basic HTTP implementation of the sanic framework.
"""

__slots__ = (
# request params
"request",
# request config
Expand All @@ -130,14 +227,9 @@ class HttpProtocol(asyncio.Protocol):
"state",
"url",
"_handler_task",
"_can_write",
"_data_received",
"_time",
"_task",
"_http",
"_exception",
"recv_buffer",
"_unix",
)

def __init__(
Expand All @@ -151,16 +243,10 @@ def __init__(
unix=None,
**kwargs,
):
asyncio.set_event_loop(loop)
self.loop = loop
self.app: Sanic = app
super().__init__(loop=loop, app=app, signal=signal, connections=connections, unix=unix)
self.url = None
self.transport: Optional[Transport] = None
self.conn_info: Optional[ConnInfo] = None
self.request: Optional[Request] = None
self.signal = signal or Signal()
self.access_log = self.app.config.ACCESS_LOG
self.connections = connections if connections is not None else set()
self.request_handler = self.app.handle_request
self.error_handler = self.app.error_handler
self.request_timeout = self.app.config.REQUEST_TIMEOUT
Expand All @@ -171,11 +257,7 @@ def __init__(
self.state = state if state else {}
if "requests_count" not in self.state:
self.state["requests_count"] = 0
self._data_received = asyncio.Event()
self._can_write = asyncio.Event()
self._can_write.set()
self._exception = None
self._unix = unix

def _setup_connection(self):
self._http = Http(self)
Expand Down Expand Up @@ -256,16 +338,6 @@ def check_timeouts(self):
except Exception:
error_logger.exception("protocol.check_timeouts")

async def send(self, data):
"""
Writes data with backpressure control.
"""
await self._can_write.wait()
if self.transport.is_closing():
raise CancelledError
self.transport.write(data)
self._time = current_time()

def close_if_idle(self) -> bool:
"""
Close the connection if a request is not being sent or received
Expand All @@ -277,15 +349,6 @@ def close_if_idle(self) -> bool:
return True
return False

def close(self):
"""
Force close the connection.
"""
# Cause a call to connection_lost where further cleanup occurs
if self.transport:
self.transport.close()
self.transport = None

# -------------------------------------------- #
# Only asyncio.Protocol callbacks below this
# -------------------------------------------- #
Expand All @@ -302,21 +365,6 @@ def connection_made(self, transport):
except Exception:
error_logger.exception("protocol.connect_made")

def connection_lost(self, exc):
try:
self.connections.discard(self)
self.resume_writing()
if self._task:
self._task.cancel()
except Exception:
error_logger.exception("protocol.connection_lost")

def pause_writing(self):
self._can_write.clear()

def resume_writing(self):
self._can_write.set()

def data_received(self, data: bytes):
try:
self._time = current_time()
Expand Down Expand Up @@ -602,7 +650,7 @@ def serve(
coros = []
for conn in connections:
if hasattr(conn, "websocket") and conn.websocket:
coros.append(conn.websocket.close_connection())
coros.append(conn.websocket.close(code=1001))
else:
conn.close()

Expand All @@ -620,7 +668,6 @@ def _build_protocol_kwargs(
if hasattr(protocol, "websocket_handshake"):
return {
"websocket_max_size": config.WEBSOCKET_MAX_SIZE,
"websocket_max_queue": config.WEBSOCKET_MAX_QUEUE,
"websocket_read_limit": config.WEBSOCKET_READ_LIMIT,
"websocket_write_limit": config.WEBSOCKET_WRITE_LIMIT,
"websocket_ping_timeout": config.WEBSOCKET_PING_TIMEOUT,
Expand Down