-
-
Notifications
You must be signed in to change notification settings - Fork 756
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Switch to asyncio streams API (#869)
* Switch to asyncio streams API * Tweak buffer swapping * Properly handle exceptions * More explanatory comments, 3.6 compatibility * Drop unused MAX_RECV
- Loading branch information
1 parent
b72c386
commit 960d465
Showing
7 changed files
with
155 additions
and
23 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import asyncio | ||
from typing import TYPE_CHECKING | ||
|
||
from uvicorn.config import Config | ||
|
||
if TYPE_CHECKING: # pragma: no cover | ||
from uvicorn.server import ServerState | ||
|
||
|
||
async def handle_http( | ||
reader: asyncio.StreamReader, | ||
writer: asyncio.StreamWriter, | ||
server_state: "ServerState", | ||
config: Config, | ||
) -> None: | ||
# Run transport/protocol session from streams. | ||
# | ||
# This is a bit fiddly, so let me explain why we do this in the first place. | ||
# | ||
# This was introduced to switch to the asyncio streams API while retaining our | ||
# existing protocols-based code. | ||
# | ||
# The aim was to: | ||
# * Make it easier to support alternative async libaries (all of which expose | ||
# a streams API, rather than anything similar to asyncio's transports and | ||
# protocols) while keeping the change footprint (and risk) at a minimum. | ||
# * Keep a "fast track" for asyncio that's as efficient as possible, by reusing | ||
# our asyncio-optimized protocols-based implementation. | ||
# | ||
# See: https://github.com/encode/uvicorn/issues/169 | ||
# See: https://github.com/encode/uvicorn/pull/869 | ||
|
||
# Use a future to coordinate between the protocol and this handler task. | ||
# https://docs.python.org/3/library/asyncio-protocol.html#connecting-existing-sockets | ||
loop = asyncio.get_event_loop() | ||
connection_lost = loop.create_future() | ||
|
||
# Switch the protocol from the stream reader to our own HTTP protocol class. | ||
protocol = config.http_protocol_class( | ||
config=config, | ||
server_state=server_state, | ||
on_connection_lost=lambda: connection_lost.set_result(True), | ||
) | ||
transport = writer.transport | ||
transport.set_protocol(protocol) | ||
|
||
# Asyncio stream servers don't `await` handler tasks (like the one we're currently | ||
# running), so we must make sure exceptions that occur in protocols but outside the | ||
# ASGI cycle (e.g. bugs) are properly retrieved and logged. | ||
# Vanilla asyncio handles exceptions properly out-of-the-box, but uvloop doesn't. | ||
# So we need to attach a callback to handle exceptions ourselves for that case. | ||
# (It's not easy to know which loop we're effectively running on, so we attach the | ||
# callback in all cases. In practice it won't be called on vanilla asyncio.) | ||
task = _get_current_task() | ||
|
||
@task.add_done_callback | ||
def retrieve_exception(task: asyncio.Task) -> None: | ||
exc = task.exception() | ||
|
||
if exc is None: | ||
return | ||
|
||
loop.call_exception_handler( | ||
{ | ||
"message": "Fatal error in server handler", | ||
"exception": exc, | ||
"transport": transport, | ||
"protocol": protocol, | ||
} | ||
) | ||
# Hang up the connection so the client doesn't wait forever. | ||
transport.close() | ||
|
||
# Kick off the HTTP protocol. | ||
protocol.connection_made(transport) | ||
|
||
# Pass any data already in the read buffer. | ||
# The assumption here is that we haven't read any data off the stream reader | ||
# yet: all data that the client might have already sent since the connection has | ||
# been established is in the `_buffer`. | ||
data = reader._buffer # type: ignore | ||
if data: | ||
protocol.data_received(data) | ||
|
||
# Let the transport run in the background. When closed, this future will complete | ||
# and we'll exit here. | ||
await connection_lost | ||
|
||
|
||
def _get_current_task() -> asyncio.Task: | ||
try: | ||
current_task = asyncio.current_task | ||
except AttributeError: # pragma: no cover | ||
# Python 3.6. | ||
current_task = asyncio.Task.current_task | ||
|
||
task = current_task() | ||
assert task is not None | ||
return task |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters