Implemented handshake and heartbeat
Implemented handshake in json server and handoff to endpoint Endpoint handles connector handshake accept/decline then launches both its IO threads alongside its heartbeat thread which contains simple timeout logic, timeout calls connection handles to close, subsequently killing the endpoint
This commit is contained in:
+15
-4
@@ -1,17 +1,28 @@
|
||||
from time import sleep
|
||||
import logging
|
||||
|
||||
from rich.logging import RichHandler
|
||||
|
||||
from dynalab_core import Core
|
||||
from dynalab_core.config import CoreConfig
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format="%(name)s %(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
handlers=[RichHandler(rich_tracebacks=True, show_path=False)],
|
||||
force=True,
|
||||
)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
config = CoreConfig(port=8765)
|
||||
print(config)
|
||||
log.info("Configured manual core on %s", config.bind_str())
|
||||
dl_core = Core(config)
|
||||
dl_core.start()
|
||||
print("started")
|
||||
|
||||
try:
|
||||
while True:
|
||||
dl_core.wait(1)
|
||||
print("waiting")
|
||||
except KeyboardInterrupt:
|
||||
log.info("Received keyboard interrupt")
|
||||
dl_core.stop()
|
||||
|
||||
+52
-3
@@ -1,4 +1,25 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from uuid import uuid4
|
||||
|
||||
from dynalab_core.protocols.packets import ProtocolMessage
|
||||
from rich.logging import RichHandler
|
||||
|
||||
from dynalab_core.protocols.common import VersionDescriptor
|
||||
from dynalab_core.protocols.constants import PROTOCOL_VERSION
|
||||
from dynalab_core.protocols.json.wire import read_message, write_message
|
||||
from dynalab_core.protocols.packets.handshake import ConnectorHello, DynaLabHello
|
||||
|
||||
CONNECTOR_VERSION = VersionDescriptor(type="alpha", major=0, minor=0, patch=1)
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format="%(name)s %(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
handlers=[RichHandler(rich_tracebacks=True, show_path=False)],
|
||||
force=True,
|
||||
)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
@@ -7,14 +28,42 @@ async def main() -> None:
|
||||
port=8765,
|
||||
)
|
||||
|
||||
print("Connected")
|
||||
log.info("Connected to DynaLab core")
|
||||
|
||||
try:
|
||||
await asyncio.sleep(10)
|
||||
try:
|
||||
message = await asyncio.wait_for(read_message(reader), timeout=30.0)
|
||||
except TimeoutError:
|
||||
log.warning("Timed out waiting for server hello")
|
||||
return
|
||||
if not isinstance(message, DynaLabHello):
|
||||
log.warning("Expected server hello, received %s", message.type)
|
||||
return
|
||||
dynalab_hello = message
|
||||
log.debug("Received hello from core instance %s", dynalab_hello.instance_id)
|
||||
connector_hello = ConnectorHello(
|
||||
connector_uuid=uuid4(),
|
||||
protocol_version=PROTOCOL_VERSION,
|
||||
connector_name="Test connector",
|
||||
connector_version=CONNECTOR_VERSION.get_version(),
|
||||
)
|
||||
try:
|
||||
await asyncio.wait_for(write_message(writer, connector_hello), timeout=5.0)
|
||||
except TimeoutError:
|
||||
log.warning("Timed out sending connector hello")
|
||||
return
|
||||
log.info("Connector hello sent for %s", connector_hello.connector_uuid)
|
||||
|
||||
try:
|
||||
while True:
|
||||
message = await read_message(reader)
|
||||
log.info(message)
|
||||
except KeyboardInterrupt:
|
||||
return
|
||||
finally:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
print("Disconnected")
|
||||
log.info("Disconnected from DynaLab core")
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
|
||||
+23
-7
@@ -1,3 +1,5 @@
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
from dynalab_core import Core
|
||||
from dynalab_core.config import CoreConfig
|
||||
@@ -5,14 +7,28 @@ from dynalab_core.errors import CoreStateMismatchError
|
||||
from test.common import find_available_port
|
||||
|
||||
|
||||
def test_core_cannot_start_twice() -> None:
|
||||
def test_core_cannot_start_twice(caplog: pytest.LogCaptureFixture) -> None:
|
||||
port = find_available_port(8765)
|
||||
core = Core(CoreConfig(port=port))
|
||||
|
||||
core.start()
|
||||
with caplog.at_level(logging.DEBUG, logger="dynalab_core"):
|
||||
core.start()
|
||||
|
||||
try:
|
||||
with pytest.raises(CoreStateMismatchError):
|
||||
core.start()
|
||||
finally:
|
||||
core.stop()
|
||||
try:
|
||||
with pytest.raises(CoreStateMismatchError):
|
||||
core.start()
|
||||
finally:
|
||||
core.stop()
|
||||
|
||||
events = [getattr(record, "event", None) for record in caplog.records]
|
||||
assert events.count("core.starting") == 1
|
||||
assert "core.start_rejected" in events
|
||||
assert "core.stopped" in events
|
||||
|
||||
rejection = next(
|
||||
record
|
||||
for record in caplog.records
|
||||
if getattr(record, "event", None) == "core.start_rejected"
|
||||
)
|
||||
assert rejection.levelno == logging.WARNING
|
||||
assert rejection.core_state == "started"
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import logging
|
||||
from queue import Queue
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from dynalab_core.protocols.constants import PROTOCOL_VERSION
|
||||
from dynalab_core.protocols.endpoint import ConnectorEndpoint
|
||||
from dynalab_core.protocols.errors import ConnectorEndpointQueueFullError
|
||||
from dynalab_core.protocols.packets import ProtocolMessage
|
||||
from dynalab_core.protocols.packets.handshake import ConnectorHello
|
||||
|
||||
|
||||
def test_full_endpoint_queue_is_logged(caplog: pytest.LogCaptureFixture) -> None:
|
||||
hello = ConnectorHello(
|
||||
connector_uuid=uuid4(),
|
||||
protocol_version=PROTOCOL_VERSION,
|
||||
connector_name="Test connector",
|
||||
connector_version="0.1.0-test",
|
||||
)
|
||||
endpoint = ConnectorEndpoint(hello)
|
||||
endpoint._packet_ingress_queue = Queue[ProtocolMessage](maxsize=1)
|
||||
|
||||
with caplog.at_level(logging.DEBUG, logger="dynalab_core"):
|
||||
try:
|
||||
endpoint.put_ingress_packet(hello)
|
||||
with pytest.raises(ConnectorEndpointQueueFullError):
|
||||
endpoint.put_ingress_packet(hello)
|
||||
finally:
|
||||
endpoint.stop()
|
||||
|
||||
queue_record = next(
|
||||
record
|
||||
for record in caplog.records
|
||||
if getattr(record, "event", None) == "endpoint.queue_full"
|
||||
)
|
||||
assert queue_record.levelno == logging.WARNING
|
||||
assert queue_record.connector_uuid == str(hello.connector_uuid)
|
||||
assert queue_record.queue_direction == "ingress"
|
||||
assert queue_record.queue_size == 1
|
||||
assert queue_record.queue_capacity == 1
|
||||
+118
-14
@@ -1,37 +1,141 @@
|
||||
import logging
|
||||
import socket
|
||||
import time
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from dynalab_core import Core
|
||||
from dynalab_core.config import CoreConfig
|
||||
from dynalab_core.protocols.constants import PROTOCOL_VERSION
|
||||
from dynalab_core.protocols.json.errors import (
|
||||
JsonServerStartupError,
|
||||
JsonServerTimeoutError,
|
||||
)
|
||||
from dynalab_core.protocols.packets.handshake import ConnectorHello
|
||||
from test.common import find_available_port
|
||||
|
||||
|
||||
def test_json_server_raises_timeout_error() -> None:
|
||||
def _wait_for_event(
|
||||
caplog: pytest.LogCaptureFixture, event: str, timeout: float = 1.0
|
||||
) -> logging.LogRecord:
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
for record in caplog.records:
|
||||
if getattr(record, "event", None) == event:
|
||||
return record
|
||||
time.sleep(0.01)
|
||||
raise AssertionError(f"Log event {event!r} was not emitted")
|
||||
|
||||
|
||||
def test_json_server_raises_timeout_error(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
port = find_available_port(8765)
|
||||
core = Core(CoreConfig(port=port))
|
||||
core._json_server._debug_timeout_test = True
|
||||
core._json_server._timeout = 0.01
|
||||
|
||||
try:
|
||||
with pytest.raises(JsonServerTimeoutError):
|
||||
core.start()
|
||||
finally:
|
||||
core.stop()
|
||||
with caplog.at_level(logging.DEBUG, logger="dynalab_core"):
|
||||
try:
|
||||
with pytest.raises(JsonServerTimeoutError):
|
||||
core.start()
|
||||
finally:
|
||||
core.stop()
|
||||
|
||||
timeout_record = next(
|
||||
record
|
||||
for record in caplog.records
|
||||
if getattr(record, "event", None) == "json_server.start_timeout"
|
||||
)
|
||||
assert timeout_record.levelno == logging.ERROR
|
||||
assert timeout_record.port == port
|
||||
|
||||
|
||||
def test_json_server_raises_startup_error() -> None:
|
||||
def test_json_server_raises_startup_error(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
|
||||
port = find_available_port(8765)
|
||||
core1 = Core(CoreConfig(port=port))
|
||||
core2 = Core(CoreConfig(port=port))
|
||||
|
||||
core1.start()
|
||||
with caplog.at_level(logging.DEBUG, logger="dynalab_core"):
|
||||
core1.start()
|
||||
|
||||
try:
|
||||
with pytest.raises(JsonServerStartupError):
|
||||
core2.start()
|
||||
finally:
|
||||
core1.stop()
|
||||
core2.stop()
|
||||
try:
|
||||
with pytest.raises(JsonServerStartupError):
|
||||
core2.start()
|
||||
finally:
|
||||
core1.stop()
|
||||
core2.stop()
|
||||
|
||||
failure_record = next(
|
||||
record
|
||||
for record in caplog.records
|
||||
if getattr(record, "event", None) == "json_server.start_failed"
|
||||
)
|
||||
assert failure_record.levelno == logging.ERROR
|
||||
assert failure_record.port == port
|
||||
assert failure_record.exc_info is not None
|
||||
|
||||
|
||||
def test_json_server_logs_invalid_handshake(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
port = find_available_port(8765)
|
||||
core = Core(CoreConfig(port=port))
|
||||
|
||||
with caplog.at_level(logging.DEBUG, logger="dynalab_core"):
|
||||
core.start()
|
||||
try:
|
||||
with socket.create_connection(("127.0.0.1", port), timeout=1) as peer:
|
||||
server_hello = peer.makefile("rb").readline()
|
||||
assert server_hello
|
||||
peer.sendall(b"not-json\n")
|
||||
|
||||
rejection = _wait_for_event(
|
||||
caplog, "connector.handshake_rejected"
|
||||
)
|
||||
finally:
|
||||
core.stop()
|
||||
|
||||
assert rejection.levelno == logging.WARNING
|
||||
assert rejection.reason == "invalid_frame"
|
||||
assert rejection.connection_id
|
||||
assert rejection.peer_address
|
||||
|
||||
|
||||
def test_json_server_waits_for_connection_handlers_on_stop(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
port = find_available_port(8765)
|
||||
core = Core(CoreConfig(port=port))
|
||||
connector_uuid = uuid4()
|
||||
connector_hello = ConnectorHello(
|
||||
connector_uuid=connector_uuid,
|
||||
protocol_version=PROTOCOL_VERSION,
|
||||
connector_name="Test connector",
|
||||
connector_version="0.1.0-test",
|
||||
)
|
||||
|
||||
with caplog.at_level(logging.DEBUG, logger="dynalab_core"):
|
||||
core.start()
|
||||
try:
|
||||
with socket.create_connection(("127.0.0.1", port), timeout=1) as peer:
|
||||
peer_file = peer.makefile("rb")
|
||||
assert peer_file.readline()
|
||||
peer.sendall(
|
||||
connector_hello.model_dump_json().encode("utf-8") + b"\n"
|
||||
)
|
||||
assert b'"type":"handshake_accepted"' in peer_file.readline()
|
||||
|
||||
core.stop()
|
||||
finally:
|
||||
if core._state != "stopped":
|
||||
core.stop()
|
||||
|
||||
events = [getattr(record, "event", None) for record in caplog.records]
|
||||
assert "json_server.stop_timeout" not in events
|
||||
assert core._json_server._stopped_event.is_set()
|
||||
assert not core._json_server._handler_tasks
|
||||
assert core._connector_registry.get(connector_uuid) is None
|
||||
|
||||
Reference in New Issue
Block a user