Close HTTP loop when connection task cancelled (#2245)
* Terminate loop when no transport exists * Add log when closing HTTP loop because of shutdown * Add unit test
This commit is contained in:
parent
d9796e9b1e
commit
595d2c76ac
@ -148,6 +148,12 @@ class Http(metaclass=TouchUpMeta):
|
|||||||
await self.response.send(end_stream=True)
|
await self.response.send(end_stream=True)
|
||||||
except CancelledError:
|
except CancelledError:
|
||||||
# Write an appropriate response before exiting
|
# 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")
|
e = self.exception or ServiceUnavailable("Cancelled")
|
||||||
self.exception = None
|
self.exception = None
|
||||||
self.keep_alive = False
|
self.keep_alive = False
|
||||||
|
@ -109,7 +109,7 @@ class HttpProtocol(SanicProtocol, metaclass=TouchUpMeta):
|
|||||||
except Exception:
|
except Exception:
|
||||||
error_logger.exception("protocol.connection_task uncaught")
|
error_logger.exception("protocol.connection_task uncaught")
|
||||||
finally:
|
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")
|
ip = self.transport.get_extra_info("peername")
|
||||||
error_logger.error(
|
error_logger.error(
|
||||||
"Connection lost before response written"
|
"Connection lost before response written"
|
||||||
|
46
tests/test_graceful_shutdown.py
Normal file
46
tests/test_graceful_shutdown.py
Normal file
@ -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,7 +7,6 @@ from unittest.mock import MagicMock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sanic_testing.reusable import ReusableClient
|
|
||||||
from sanic_testing.testing import HOST, PORT
|
from sanic_testing.testing import HOST, PORT
|
||||||
|
|
||||||
from sanic.compat import ctrlc_workaround_for_windows
|
from sanic.compat import ctrlc_workaround_for_windows
|
||||||
@ -29,13 +28,9 @@ def set_loop(app, loop):
|
|||||||
signal.signal = mock
|
signal.signal = mock
|
||||||
else:
|
else:
|
||||||
loop.add_signal_handler = mock
|
loop.add_signal_handler = mock
|
||||||
print(">>>>>>>>>>>>>>>1", id(loop))
|
|
||||||
print(">>>>>>>>>>>>>>>1", loop.add_signal_handler)
|
|
||||||
|
|
||||||
|
|
||||||
def after(app, loop):
|
def after(app, loop):
|
||||||
print(">>>>>>>>>>>>>>>2", id(loop))
|
|
||||||
print(">>>>>>>>>>>>>>>2", loop.add_signal_handler)
|
|
||||||
calledq.put(mock.called)
|
calledq.put(mock.called)
|
||||||
|
|
||||||
|
|
||||||
@ -100,7 +95,7 @@ def test_windows_workaround():
|
|||||||
os.kill(os.getpid(), signal.SIGINT)
|
os.kill(os.getpid(), signal.SIGINT)
|
||||||
await asyncio.sleep(0.2)
|
await asyncio.sleep(0.2)
|
||||||
assert app.is_stopping
|
assert app.is_stopping
|
||||||
assert app.stay_active_task.result() == None
|
assert app.stay_active_task.result() is None
|
||||||
# Second Ctrl+C should raise
|
# Second Ctrl+C should raise
|
||||||
with pytest.raises(KeyboardInterrupt):
|
with pytest.raises(KeyboardInterrupt):
|
||||||
os.kill(os.getpid(), signal.SIGINT)
|
os.kill(os.getpid(), signal.SIGINT)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user