Skip to content

Commit

Permalink
Close HTTP loop when connection task cancelled (sanic-org#2245)
Browse files Browse the repository at this point in the history
* Terminate loop when no transport exists

* Add log when closing HTTP loop because of shutdown

* Add unit test
  • Loading branch information
ahopkins authored Sep 27, 2021
1 parent d9796e9 commit 595d2c7
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 7 deletions.
6 changes: 6 additions & 0 deletions sanic/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@ async def http1(self):
await self.response.send(end_stream=True)
except CancelledError:
# Write an appropriate response before exiting
if not self.protocol.transport:
logger.info(
f"Request: {self.request.method} {self.request.url} "
"stopped. Transport is closed."
)
return
e = self.exception or ServiceUnavailable("Cancelled")
self.exception = None
self.keep_alive = False
Expand Down
2 changes: 1 addition & 1 deletion sanic/server/protocols/http_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ async def connection_task(self): # no cov
except Exception:
error_logger.exception("protocol.connection_task uncaught")
finally:
if self.app.debug and self._http:
if self.app.debug and self._http and self.transport:
ip = self.transport.get_extra_info("peername")
error_logger.error(
"Connection lost before response written"
Expand Down
46 changes: 46 additions & 0 deletions tests/test_graceful_shutdown.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import asyncio
import logging
import time

from collections import Counter
from multiprocessing import Process

import httpx


PORT = 42101


def test_no_exceptions_when_cancel_pending_request(app, caplog):
app.config.GRACEFUL_SHUTDOWN_TIMEOUT = 1

@app.get("/")
async def handler(request):
await asyncio.sleep(5)

@app.after_server_start
def shutdown(app, _):
time.sleep(0.2)
app.stop()

def ping():
time.sleep(0.1)
response = httpx.get("http://127.0.0.1:8000")
print(response.status_code)

p = Process(target=ping)
p.start()

with caplog.at_level(logging.INFO):
app.run()

p.kill()

counter = Counter([r[1] for r in caplog.record_tuples])

assert counter[logging.INFO] == 5
assert logging.ERROR not in counter
assert (
caplog.record_tuples[3][2]
== "Request: GET http://127.0.0.1:8000/ stopped. Transport is closed."
)
7 changes: 1 addition & 6 deletions tests/test_signal_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import pytest

from sanic_testing.reusable import ReusableClient
from sanic_testing.testing import HOST, PORT

from sanic.compat import ctrlc_workaround_for_windows
Expand All @@ -29,13 +28,9 @@ def set_loop(app, loop):
signal.signal = mock
else:
loop.add_signal_handler = mock
print(">>>>>>>>>>>>>>>1", id(loop))
print(">>>>>>>>>>>>>>>1", loop.add_signal_handler)


def after(app, loop):
print(">>>>>>>>>>>>>>>2", id(loop))
print(">>>>>>>>>>>>>>>2", loop.add_signal_handler)
calledq.put(mock.called)


Expand Down Expand Up @@ -100,7 +95,7 @@ async def atest(stop_first):
os.kill(os.getpid(), signal.SIGINT)
await asyncio.sleep(0.2)
assert app.is_stopping
assert app.stay_active_task.result() == None
assert app.stay_active_task.result() is None
# Second Ctrl+C should raise
with pytest.raises(KeyboardInterrupt):
os.kill(os.getpid(), signal.SIGINT)
Expand Down

0 comments on commit 595d2c7

Please sign in to comment.