fix: improve proxy liveness and graceful shutdown

This commit is contained in:
2026-07-22 14:37:05 +02:00
parent 4566d53ebf
commit 97c82b38e4
6 changed files with 318 additions and 30 deletions
+126 -3
View File
@@ -2,7 +2,13 @@ import asyncio
import unittest
from unittest.mock import AsyncMock, patch
from arkteos_proxy import ArkteosProxy, KEEPALIVE_INTERVAL, RECONNECT_DELAY
from arkteos_proxy import (
ArkteosProxy,
ConfigurationError,
KEEPALIVE_INTERVAL,
RECONNECT_DELAY,
validate_configuration,
)
class QueueReader:
@@ -149,18 +155,135 @@ class ArkteosProxyTests(unittest.IsolatedAsyncioTestCase):
proxy.stop_event.set()
proxy.run_pac_connection = fake_pac_connection
proxy.wait_for_reconnect_delay = AsyncMock()
with (
patch("arkteos_proxy.asyncio.start_server", new_callable=AsyncMock, return_value=FakeServer()),
patch("arkteos_proxy.asyncio.sleep", new_callable=AsyncMock) as sleep,
):
await proxy.serve()
self.assertEqual(attempts, 2)
sleep.assert_awaited_once_with(RECONNECT_DELAY)
proxy.wait_for_reconnect_delay.assert_awaited_once_with()
def test_missing_option_defaults_to_read_only(self):
self.assertFalse(ArkteosProxy("pac", 9641, 9641).allow_client_writes)
async def test_connection_timeout_stops_a_single_attempt(self):
proxy = ArkteosProxy("pac", 9641, 9641)
async def never_connect(*_args, **_kwargs):
await asyncio.Event().wait()
with (
patch("arkteos_proxy.PAC_CONNECT_TIMEOUT", 0.01),
patch("arkteos_proxy.asyncio.open_connection", new_callable=AsyncMock, side_effect=never_connect) as connect,
):
with self.assertRaises(ConnectionError):
await proxy.run_pac_connection()
connect.assert_awaited_once_with("pac", 9641)
async def test_silent_pac_closes_writer_and_cancels_keepalive(self):
proxy = ArkteosProxy("pac", 9641, 9641)
reader = QueueReader()
writer = FakeWriter()
keepalive_cancelled = asyncio.Event()
async def tracked_keepalive():
try:
await asyncio.Event().wait()
except asyncio.CancelledError:
keepalive_cancelled.set()
raise
proxy.pac_keepalive = tracked_keepalive
with (
patch("arkteos_proxy.PAC_READ_TIMEOUT", 0.01),
patch("arkteos_proxy.asyncio.open_connection", new_callable=AsyncMock, return_value=(reader, writer)),
):
await proxy.run_pac_connection()
self.assertTrue(writer.closed)
self.assertTrue(keepalive_cancelled.is_set())
self.assertIsNone(proxy.pac_writer)
self.assertIsNone(proxy.keepalive_task)
async def test_pac_activity_is_distributed_before_connection_closes(self):
proxy = ArkteosProxy("pac", 9641, 9641)
reader = QueueReader()
writer = FakeWriter()
client = FakeWriter()
proxy.clients.add(client)
await reader.items.put(b"pac-data")
await reader.items.put(b"")
with (
patch("arkteos_proxy.PAC_READ_TIMEOUT", 0.01),
patch("arkteos_proxy.asyncio.open_connection", new_callable=AsyncMock, return_value=(reader, writer)),
):
await proxy.run_pac_connection()
self.assertEqual(client.writes, [b"pac-data"])
self.assertTrue(writer.closed)
async def test_graceful_stop_closes_server_clients_and_pac_writer(self):
proxy = ArkteosProxy("pac", 9641, 0)
server = FakeServer()
pac_writer = FakeWriter()
client = FakeWriter()
proxy.pac_writer = pac_writer
proxy.clients.add(client)
async def wait_for_stop():
await proxy.stop_event.wait()
proxy.run_pac_connection = wait_for_stop
with patch("arkteos_proxy.asyncio.start_server", new_callable=AsyncMock, return_value=server):
task = asyncio.create_task(proxy.serve())
await asyncio.sleep(0)
proxy.request_stop()
await task
self.assertTrue(server.closed)
self.assertTrue(client.closed)
self.assertTrue(pac_writer.closed)
self.assertEqual(proxy.client_tasks, set())
async def test_stop_during_reconnect_does_not_wait_for_full_delay(self):
proxy = ArkteosProxy("pac", 9641, 0)
server = FakeServer()
async def failed_connection():
raise ConnectionError("PAC indisponible")
async def stop_during_delay():
proxy.request_stop()
proxy.run_pac_connection = failed_connection
proxy.wait_for_reconnect_delay = stop_during_delay
with patch("arkteos_proxy.asyncio.start_server", new_callable=AsyncMock, return_value=server):
await asyncio.wait_for(proxy.serve(), timeout=0.1)
self.assertTrue(server.closed)
def test_configuration_validation_rejects_invalid_values(self):
invalid_configurations = (
(" ", "9641", "9641", "false"),
("pac", "0", "9641", "false"),
("pac", "65536", "9641", "false"),
("pac", "9641", "invalid", "false"),
("pac", "9641", "9641", "maybe"),
)
for configuration in invalid_configurations:
with self.subTest(configuration=configuration):
with self.assertRaises(ConfigurationError):
validate_configuration(*configuration)
def test_configuration_validation_accepts_supported_values(self):
self.assertEqual(
validate_configuration(" pac ", "9641", 9641, "false"),
("pac", 9641, 9641, False),
)
if __name__ == "__main__":
unittest.main()