2022-12-13 07:28:23 +00:00
|
|
|
from logging import ERROR, INFO
|
2022-12-15 09:49:26 +00:00
|
|
|
from signal import SIGINT
|
2022-09-18 15:17:23 +01:00
|
|
|
from unittest.mock import Mock, call, patch
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
2022-12-15 09:49:26 +00:00
|
|
|
from sanic.compat import OS_IS_WINDOWS
|
2022-12-07 12:42:17 +00:00
|
|
|
from sanic.exceptions import ServerKilled
|
2022-12-18 12:09:17 +00:00
|
|
|
from sanic.worker.constants import RestartOrder
|
2022-09-18 15:17:23 +01:00
|
|
|
from sanic.worker.manager import WorkerManager
|
|
|
|
|
|
|
|
|
2022-12-15 09:49:26 +00:00
|
|
|
if not OS_IS_WINDOWS:
|
|
|
|
from signal import SIGKILL
|
|
|
|
else:
|
|
|
|
SIGKILL = SIGINT
|
|
|
|
|
|
|
|
|
2022-09-18 15:17:23 +01:00
|
|
|
def fake_serve():
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
|
|
def test_manager_no_workers():
|
|
|
|
message = "Cannot serve with no workers"
|
|
|
|
with pytest.raises(RuntimeError, match=message):
|
2022-12-13 07:28:23 +00:00
|
|
|
WorkerManager(0, fake_serve, {}, Mock(), (Mock(), Mock()), {})
|
2022-09-18 15:17:23 +01:00
|
|
|
|
|
|
|
|
|
|
|
@patch("sanic.worker.process.os")
|
|
|
|
def test_terminate(os_mock: Mock):
|
|
|
|
process = Mock()
|
|
|
|
process.pid = 1234
|
|
|
|
context = Mock()
|
|
|
|
context.Process.return_value = process
|
2022-12-13 07:28:23 +00:00
|
|
|
manager = WorkerManager(1, fake_serve, {}, context, (Mock(), Mock()), {})
|
2022-09-18 15:17:23 +01:00
|
|
|
manager.terminate()
|
|
|
|
os_mock.kill.assert_called_once_with(1234, SIGINT)
|
|
|
|
|
|
|
|
|
|
|
|
@patch("sanic.worker.process.os")
|
|
|
|
def test_shutown(os_mock: Mock):
|
|
|
|
process = Mock()
|
|
|
|
process.pid = 1234
|
|
|
|
process.is_alive.return_value = True
|
|
|
|
context = Mock()
|
|
|
|
context.Process.return_value = process
|
2022-12-13 07:28:23 +00:00
|
|
|
manager = WorkerManager(1, fake_serve, {}, context, (Mock(), Mock()), {})
|
2022-09-18 15:17:23 +01:00
|
|
|
manager.shutdown()
|
|
|
|
os_mock.kill.assert_called_once_with(1234, SIGINT)
|
|
|
|
|
|
|
|
|
|
|
|
@patch("sanic.worker.manager.os")
|
|
|
|
def test_kill(os_mock: Mock):
|
|
|
|
process = Mock()
|
|
|
|
process.pid = 1234
|
|
|
|
context = Mock()
|
|
|
|
context.Process.return_value = process
|
2022-12-13 07:28:23 +00:00
|
|
|
manager = WorkerManager(1, fake_serve, {}, context, (Mock(), Mock()), {})
|
2022-12-07 12:42:17 +00:00
|
|
|
with pytest.raises(ServerKilled):
|
|
|
|
manager.kill()
|
2022-09-18 15:17:23 +01:00
|
|
|
os_mock.kill.assert_called_once_with(1234, SIGKILL)
|
2022-12-18 12:40:38 +00:00
|
|
|
|
|
|
|
|
|
|
|
@patch("sanic.worker.process.os")
|
|
|
|
@patch("sanic.worker.manager.os")
|
|
|
|
def test_shutdown_signal_send_kill(
|
|
|
|
manager_os_mock: Mock, process_os_mock: Mock
|
|
|
|
):
|
|
|
|
process = Mock()
|
|
|
|
process.pid = 1234
|
|
|
|
context = Mock()
|
|
|
|
context.Process.return_value = process
|
|
|
|
manager = WorkerManager(1, fake_serve, {}, context, (Mock(), Mock()), {})
|
|
|
|
assert manager._shutting_down is False
|
|
|
|
manager.shutdown_signal(SIGINT, None)
|
|
|
|
assert manager._shutting_down is True
|
|
|
|
process_os_mock.kill.assert_called_once_with(1234, SIGINT)
|
|
|
|
manager.shutdown_signal(SIGINT, None)
|
|
|
|
manager_os_mock.kill.assert_called_once_with(1234, SIGKILL)
|
2022-09-18 15:17:23 +01:00
|
|
|
|
|
|
|
|
|
|
|
def test_restart_all():
|
|
|
|
p1 = Mock()
|
|
|
|
p2 = Mock()
|
|
|
|
context = Mock()
|
|
|
|
context.Process.side_effect = [p1, p2, p1, p2]
|
2022-12-13 07:28:23 +00:00
|
|
|
manager = WorkerManager(2, fake_serve, {}, context, (Mock(), Mock()), {})
|
2022-09-18 15:17:23 +01:00
|
|
|
assert len(list(manager.transient_processes))
|
|
|
|
manager.restart()
|
|
|
|
p1.terminate.assert_called_once()
|
|
|
|
p2.terminate.assert_called_once()
|
|
|
|
context.Process.assert_has_calls(
|
|
|
|
[
|
|
|
|
call(
|
|
|
|
name="Sanic-Server-0-0",
|
|
|
|
target=fake_serve,
|
|
|
|
kwargs={"config": {}},
|
|
|
|
daemon=True,
|
|
|
|
),
|
|
|
|
call(
|
|
|
|
name="Sanic-Server-1-0",
|
|
|
|
target=fake_serve,
|
|
|
|
kwargs={"config": {}},
|
|
|
|
daemon=True,
|
|
|
|
),
|
|
|
|
call(
|
|
|
|
name="Sanic-Server-0-0",
|
|
|
|
target=fake_serve,
|
|
|
|
kwargs={"config": {}},
|
|
|
|
daemon=True,
|
|
|
|
),
|
|
|
|
call(
|
|
|
|
name="Sanic-Server-1-0",
|
|
|
|
target=fake_serve,
|
|
|
|
kwargs={"config": {}},
|
|
|
|
daemon=True,
|
|
|
|
),
|
|
|
|
]
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2022-12-18 12:09:17 +00:00
|
|
|
@pytest.mark.parametrize("zero_downtime", (False, True))
|
|
|
|
def test_monitor_all(zero_downtime):
|
2022-09-18 15:17:23 +01:00
|
|
|
p1 = Mock()
|
|
|
|
p2 = Mock()
|
|
|
|
sub = Mock()
|
2022-12-18 12:09:17 +00:00
|
|
|
incoming = (
|
|
|
|
"__ALL_PROCESSES__::STARTUP_FIRST"
|
|
|
|
if zero_downtime
|
|
|
|
else "__ALL_PROCESSES__:"
|
|
|
|
)
|
|
|
|
sub.recv.side_effect = [incoming, ""]
|
2022-09-18 15:17:23 +01:00
|
|
|
context = Mock()
|
|
|
|
context.Process.side_effect = [p1, p2]
|
2022-12-13 07:28:23 +00:00
|
|
|
manager = WorkerManager(2, fake_serve, {}, context, (Mock(), sub), {})
|
2022-09-18 15:17:23 +01:00
|
|
|
manager.restart = Mock() # type: ignore
|
|
|
|
manager.wait_for_ack = Mock() # type: ignore
|
|
|
|
manager.monitor()
|
|
|
|
|
2022-12-18 12:09:17 +00:00
|
|
|
restart_order = (
|
|
|
|
RestartOrder.STARTUP_FIRST
|
|
|
|
if zero_downtime
|
|
|
|
else RestartOrder.SHUTDOWN_FIRST
|
|
|
|
)
|
2022-09-18 15:17:23 +01:00
|
|
|
manager.restart.assert_called_once_with(
|
2022-12-18 12:09:17 +00:00
|
|
|
process_names=None,
|
|
|
|
reloaded_files="",
|
|
|
|
restart_order=restart_order,
|
2022-09-18 15:17:23 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2022-12-18 12:09:17 +00:00
|
|
|
@pytest.mark.parametrize("zero_downtime", (False, True))
|
|
|
|
def test_monitor_all_with_files(zero_downtime):
|
2022-09-18 15:17:23 +01:00
|
|
|
p1 = Mock()
|
|
|
|
p2 = Mock()
|
|
|
|
sub = Mock()
|
2022-12-18 12:09:17 +00:00
|
|
|
incoming = (
|
|
|
|
"__ALL_PROCESSES__:foo,bar:STARTUP_FIRST"
|
|
|
|
if zero_downtime
|
|
|
|
else "__ALL_PROCESSES__:foo,bar"
|
|
|
|
)
|
|
|
|
sub.recv.side_effect = [incoming, ""]
|
2022-09-18 15:17:23 +01:00
|
|
|
context = Mock()
|
|
|
|
context.Process.side_effect = [p1, p2]
|
2022-12-13 07:28:23 +00:00
|
|
|
manager = WorkerManager(2, fake_serve, {}, context, (Mock(), sub), {})
|
2022-09-18 15:17:23 +01:00
|
|
|
manager.restart = Mock() # type: ignore
|
|
|
|
manager.wait_for_ack = Mock() # type: ignore
|
|
|
|
manager.monitor()
|
|
|
|
|
2022-12-18 12:09:17 +00:00
|
|
|
restart_order = (
|
|
|
|
RestartOrder.STARTUP_FIRST
|
|
|
|
if zero_downtime
|
|
|
|
else RestartOrder.SHUTDOWN_FIRST
|
|
|
|
)
|
2022-09-18 15:17:23 +01:00
|
|
|
manager.restart.assert_called_once_with(
|
2022-12-18 12:09:17 +00:00
|
|
|
process_names=None,
|
|
|
|
reloaded_files="foo,bar",
|
|
|
|
restart_order=restart_order,
|
2022-09-18 15:17:23 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2022-12-18 12:09:17 +00:00
|
|
|
@pytest.mark.parametrize("zero_downtime", (False, True))
|
|
|
|
def test_monitor_one_process(zero_downtime):
|
2022-09-18 15:17:23 +01:00
|
|
|
p1 = Mock()
|
|
|
|
p1.name = "Testing"
|
|
|
|
p2 = Mock()
|
|
|
|
sub = Mock()
|
2022-12-18 12:09:17 +00:00
|
|
|
incoming = (
|
|
|
|
f"{p1.name}:foo,bar:STARTUP_FIRST"
|
|
|
|
if zero_downtime
|
|
|
|
else f"{p1.name}:foo,bar"
|
|
|
|
)
|
|
|
|
sub.recv.side_effect = [incoming, ""]
|
2022-09-18 15:17:23 +01:00
|
|
|
context = Mock()
|
|
|
|
context.Process.side_effect = [p1, p2]
|
2022-12-13 07:28:23 +00:00
|
|
|
manager = WorkerManager(2, fake_serve, {}, context, (Mock(), sub), {})
|
2022-09-18 15:17:23 +01:00
|
|
|
manager.restart = Mock() # type: ignore
|
|
|
|
manager.wait_for_ack = Mock() # type: ignore
|
|
|
|
manager.monitor()
|
|
|
|
|
2022-12-18 12:09:17 +00:00
|
|
|
restart_order = (
|
|
|
|
RestartOrder.STARTUP_FIRST
|
|
|
|
if zero_downtime
|
|
|
|
else RestartOrder.SHUTDOWN_FIRST
|
|
|
|
)
|
2022-09-18 15:17:23 +01:00
|
|
|
manager.restart.assert_called_once_with(
|
2022-12-18 12:09:17 +00:00
|
|
|
process_names=[p1.name],
|
|
|
|
reloaded_files="foo,bar",
|
|
|
|
restart_order=restart_order,
|
2022-09-18 15:17:23 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def test_shutdown_signal():
|
|
|
|
pub = Mock()
|
2022-12-13 07:28:23 +00:00
|
|
|
manager = WorkerManager(1, fake_serve, {}, Mock(), (pub, Mock()), {})
|
2022-09-18 15:17:23 +01:00
|
|
|
manager.shutdown = Mock() # type: ignore
|
|
|
|
|
|
|
|
manager.shutdown_signal(SIGINT, None)
|
|
|
|
pub.send.assert_called_with(None)
|
|
|
|
manager.shutdown.assert_called_once_with()
|
2022-12-13 07:28:23 +00:00
|
|
|
|
|
|
|
|
|
|
|
def test_shutdown_servers(caplog):
|
|
|
|
p1 = Mock()
|
|
|
|
p1.pid = 1234
|
|
|
|
context = Mock()
|
|
|
|
context.Process.side_effect = [p1]
|
|
|
|
pub = Mock()
|
|
|
|
manager = WorkerManager(1, fake_serve, {}, context, (pub, Mock()), {})
|
|
|
|
|
|
|
|
with patch("os.kill") as kill:
|
|
|
|
with caplog.at_level(ERROR):
|
|
|
|
manager.shutdown_server()
|
|
|
|
|
|
|
|
kill.assert_called_once_with(1234, SIGINT)
|
|
|
|
kill.reset_mock()
|
|
|
|
|
|
|
|
assert not caplog.record_tuples
|
|
|
|
|
|
|
|
manager.shutdown_server()
|
|
|
|
|
|
|
|
kill.assert_not_called()
|
|
|
|
|
|
|
|
assert (
|
|
|
|
"sanic.error",
|
|
|
|
ERROR,
|
|
|
|
"Server shutdown failed because a server was not found.",
|
|
|
|
) in caplog.record_tuples
|
|
|
|
|
|
|
|
|
|
|
|
def test_shutdown_servers_named():
|
|
|
|
p1 = Mock()
|
|
|
|
p1.pid = 1234
|
|
|
|
p2 = Mock()
|
|
|
|
p2.pid = 6543
|
|
|
|
context = Mock()
|
|
|
|
context.Process.side_effect = [p1, p2]
|
|
|
|
pub = Mock()
|
|
|
|
manager = WorkerManager(2, fake_serve, {}, context, (pub, Mock()), {})
|
|
|
|
|
|
|
|
with patch("os.kill") as kill:
|
|
|
|
with pytest.raises(KeyError):
|
|
|
|
manager.shutdown_server("foo")
|
|
|
|
manager.shutdown_server("Server-1")
|
|
|
|
|
|
|
|
kill.assert_called_once_with(6543, SIGINT)
|
|
|
|
|
|
|
|
|
|
|
|
def test_scale(caplog):
|
|
|
|
p1 = Mock()
|
|
|
|
p1.pid = 1234
|
|
|
|
p2 = Mock()
|
|
|
|
p2.pid = 3456
|
|
|
|
p3 = Mock()
|
|
|
|
p3.pid = 5678
|
|
|
|
context = Mock()
|
|
|
|
context.Process.side_effect = [p1, p2, p3]
|
|
|
|
pub = Mock()
|
|
|
|
manager = WorkerManager(1, fake_serve, {}, context, (pub, Mock()), {})
|
|
|
|
|
|
|
|
assert len(manager.transient) == 1
|
|
|
|
|
|
|
|
manager.scale(3)
|
|
|
|
assert len(manager.transient) == 3
|
|
|
|
|
|
|
|
with patch("os.kill") as kill:
|
|
|
|
manager.scale(2)
|
|
|
|
assert len(manager.transient) == 2
|
|
|
|
|
|
|
|
manager.scale(1)
|
|
|
|
assert len(manager.transient) == 1
|
|
|
|
|
|
|
|
kill.call_count == 2
|
|
|
|
|
|
|
|
with caplog.at_level(INFO):
|
|
|
|
manager.scale(1)
|
|
|
|
|
|
|
|
assert (
|
|
|
|
"sanic.root",
|
|
|
|
INFO,
|
|
|
|
"No change needed. There are already 1 workers.",
|
|
|
|
) in caplog.record_tuples
|
|
|
|
|
|
|
|
with pytest.raises(ValueError, match=r"Cannot scale to 0 workers\."):
|
|
|
|
manager.scale(0)
|