Further json isolation and endpoint implementation
Further isolated json specifics to their own module under protocols/json Moved global packet classes to their own protocols/packets module Implemented ConnectorEndpoint and ConnectorRegistry as transport agnostic connector endpoints for universal connector handling, exposed a ConnectorRegistry API that will need to be made available to the JsonServer for it to be able to register and unregister endpoints as it opens and closes connections Implemented a top down shutdown architecture where each object is responsible for its children, this allows for a logical and heirachical flow, each object has its own stop event, which when its stop method is called, it sets and then awaits the stopped event to be set by any worker threads etc, with a mandatory timeout to avoid hanging on shutdown
This commit is contained in:
@@ -9,7 +9,8 @@ from typing import Literal
|
|||||||
from dynalab_core.config import CoreConfig
|
from dynalab_core.config import CoreConfig
|
||||||
from dynalab_core.constants import CORE_VERSION
|
from dynalab_core.constants import CORE_VERSION
|
||||||
from dynalab_core.errors import CoreStateMismatchError
|
from dynalab_core.errors import CoreStateMismatchError
|
||||||
from dynalab_core.protocols.json.common import VersionDescriptor
|
from dynalab_core.protocols.endpoint import ConnectorRegistry
|
||||||
|
from dynalab_core.protocols.common import VersionDescriptor
|
||||||
from dynalab_core.protocols.json.server import JsonServer
|
from dynalab_core.protocols.json.server import JsonServer
|
||||||
|
|
||||||
|
|
||||||
@@ -21,7 +22,8 @@ class Core:
|
|||||||
self._core_version: VersionDescriptor = CORE_VERSION
|
self._core_version: VersionDescriptor = CORE_VERSION
|
||||||
self._stop_event: threading.Event = threading.Event()
|
self._stop_event: threading.Event = threading.Event()
|
||||||
self._core_config: CoreConfig = config
|
self._core_config: CoreConfig = config
|
||||||
self._json_server = JsonServer(self._core_config, self._stop_event)
|
self._json_server = JsonServer(self._core_config)
|
||||||
|
self._connector_registry = ConnectorRegistry()
|
||||||
|
|
||||||
self._state = "initd"
|
self._state = "initd"
|
||||||
|
|
||||||
@@ -37,5 +39,7 @@ class Core:
|
|||||||
self._stop_event.wait(timeout)
|
self._stop_event.wait(timeout)
|
||||||
|
|
||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
|
self._json_server.stop()
|
||||||
|
self._connector_registry.stop()
|
||||||
self._stop_event.set()
|
self._stop_event.set()
|
||||||
self._state = "stopped"
|
self._state = "stopped"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from dynalab_core.protocols.json.common import VersionDescriptor
|
from dynalab_core.protocols.common import VersionDescriptor
|
||||||
|
|
||||||
|
|
||||||
CORE_VERSION = VersionDescriptor(type="alpha", major=0, minor=0, patch=1)
|
CORE_VERSION = VersionDescriptor(type="alpha", major=0, minor=0, patch=1)
|
||||||
|
|||||||
@@ -9,20 +9,3 @@ class CoreError(Exception):
|
|||||||
|
|
||||||
class CoreStateMismatchError(CoreError):
|
class CoreStateMismatchError(CoreError):
|
||||||
"""DynaLab Core state error."""
|
"""DynaLab Core state error."""
|
||||||
|
|
||||||
|
|
||||||
# JsonServer error declarations
|
|
||||||
class JsonServerError(Exception):
|
|
||||||
"""Generic JsonServer Error"""
|
|
||||||
|
|
||||||
|
|
||||||
class JsonServerTimeoutError(JsonServerError):
|
|
||||||
"""JsonServer timed out"""
|
|
||||||
|
|
||||||
|
|
||||||
class JsonServerStartupError(JsonServerError):
|
|
||||||
"""JsonServer crashed on startup"""
|
|
||||||
|
|
||||||
def __init__(self, error: Exception) -> None:
|
|
||||||
self.error = error
|
|
||||||
super().__init__(f"JSON server failed to start: {error}")
|
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
|
||||||
|
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from queue import Empty, Full, Queue
|
||||||
|
from threading import RLock, Thread
|
||||||
|
import threading
|
||||||
|
from uuid import UUID
|
||||||
|
from dynalab_core.protocols.errors import (
|
||||||
|
ConnectorEndpointQueueFullError,
|
||||||
|
ConnectorRegistryAlreadyRegisteredError,
|
||||||
|
)
|
||||||
|
from dynalab_core.protocols.packets import ProtocolMessage
|
||||||
|
from dynalab_core.protocols.packets.handshake import ConnectorHello
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectorEndpoint:
|
||||||
|
def __init__(self, hello: ConnectorHello) -> None:
|
||||||
|
self._packet_ingress_queue: Queue[ProtocolMessage] = Queue(524288)
|
||||||
|
self._packet_egress_queue: Queue[ProtocolMessage] = Queue(524288)
|
||||||
|
self._connector_hello = hello
|
||||||
|
self._worker_thread = Thread(
|
||||||
|
target=self._worker,
|
||||||
|
name=f"endpoint_worker_{self._connector_hello.connector_uuid}",
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
self._stop_event = threading.Event()
|
||||||
|
self._stopped_event = threading.Event()
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
self._stop_event.set()
|
||||||
|
if not self._worker_thread.is_alive():
|
||||||
|
self._stopped_event.set()
|
||||||
|
self._stopped_event.wait(10)
|
||||||
|
|
||||||
|
def uuid(self) -> UUID:
|
||||||
|
return self._connector_hello.connector_uuid
|
||||||
|
|
||||||
|
def put_ingress_packet(self, packet: ProtocolMessage) -> None:
|
||||||
|
try:
|
||||||
|
self._packet_ingress_queue.put_nowait(packet)
|
||||||
|
except Full:
|
||||||
|
raise ConnectorEndpointQueueFullError
|
||||||
|
|
||||||
|
def get_egress_packet(self, timeout: float | None) -> ProtocolMessage | None:
|
||||||
|
try:
|
||||||
|
packet = self._packet_egress_queue.get(block=True, timeout=timeout)
|
||||||
|
except Empty:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return packet
|
||||||
|
|
||||||
|
def get_egress_packet_no_wait(self) -> ProtocolMessage | None:
|
||||||
|
try:
|
||||||
|
packet = self._packet_egress_queue.get_nowait()
|
||||||
|
except Empty:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return packet
|
||||||
|
|
||||||
|
def _get_ingress_packet(self, timeout: float | None) -> ProtocolMessage | None:
|
||||||
|
try:
|
||||||
|
packet = self._packet_ingress_queue.get(block=True, timeout=timeout)
|
||||||
|
except Empty:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return packet
|
||||||
|
|
||||||
|
def _get_ingress_packet_no_wait(self) -> ProtocolMessage | None:
|
||||||
|
try:
|
||||||
|
packet = self._packet_ingress_queue.get_nowait()
|
||||||
|
except Empty:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return packet
|
||||||
|
|
||||||
|
def _put_egress_packet(self, packet: ProtocolMessage) -> None:
|
||||||
|
try:
|
||||||
|
self._packet_egress_queue.put_nowait(packet)
|
||||||
|
except Full:
|
||||||
|
raise ConnectorEndpointQueueFullError
|
||||||
|
|
||||||
|
def _worker(self) -> None:
|
||||||
|
# TODO: build endpoint worker thread
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectorRegistry:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._endpoints: dict[UUID, ConnectorEndpoint] = {}
|
||||||
|
self._lock = RLock()
|
||||||
|
|
||||||
|
def register(self, hello: ConnectorHello) -> None:
|
||||||
|
endpoint = ConnectorEndpoint(hello)
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
current = self._endpoints.get(hello.connector_uuid)
|
||||||
|
|
||||||
|
if current is not None:
|
||||||
|
raise ConnectorRegistryAlreadyRegisteredError
|
||||||
|
|
||||||
|
self._endpoints[hello.connector_uuid] = endpoint
|
||||||
|
|
||||||
|
return endpoint
|
||||||
|
|
||||||
|
def unregister(self, endpoint: ConnectorEndpoint) -> None:
|
||||||
|
with self._lock:
|
||||||
|
current = self._endpoints.get(endpoint.uuid())
|
||||||
|
|
||||||
|
if current is endpoint:
|
||||||
|
del self._endpoints[endpoint.uuid()]
|
||||||
|
|
||||||
|
def get(self, connector_uuid: UUID) -> ConnectorEndpoint | None:
|
||||||
|
with self._lock:
|
||||||
|
return self._endpoints.get(connector_uuid)
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
for endpoint in self._endpoints.values():
|
||||||
|
endpoint.stop()
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
|
||||||
|
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
# ConnectorEndpoint error declarations
|
||||||
|
class ConnectorEndpointError(Exception):
|
||||||
|
"""Generic ConnectorEndpoint Error"""
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectorEndpointQueueFullError(ConnectorEndpointError):
|
||||||
|
"""ConnectorEndpoing queue is full"""
|
||||||
|
|
||||||
|
|
||||||
|
# ConnectorRegistry error declarations
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectorRegistryError(Exception):
|
||||||
|
"""Generic ConnectorRegistry Error"""
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectorRegistryAlreadyRegisteredError(ConnectorRegistryError):
|
||||||
|
"""ConnectorRegistry endpoint already registered"""
|
||||||
@@ -2,6 +2,6 @@
|
|||||||
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from dynalab_core.protocols.json.common import VersionDescriptor
|
from dynalab_core.protocols.common import VersionDescriptor
|
||||||
|
|
||||||
PROTOCOL_VERSION = VersionDescriptor(type="alpha", major=0, minor=0, patch=1)
|
PROTOCOL_VERSION = VersionDescriptor(type="alpha", major=0, minor=0, patch=1)
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
|
||||||
|
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
# JsonServer error declarations
|
||||||
|
class JsonServerError(Exception):
|
||||||
|
"""Generic JsonServer Error"""
|
||||||
|
|
||||||
|
|
||||||
|
class JsonServerTimeoutError(JsonServerError):
|
||||||
|
"""JsonServer timed out"""
|
||||||
|
|
||||||
|
|
||||||
|
class JsonServerStartupError(JsonServerError):
|
||||||
|
"""JsonServer crashed on startup"""
|
||||||
|
|
||||||
|
def __init__(self, error: Exception) -> None:
|
||||||
|
self.error = error
|
||||||
|
super().__init__(f"JSON server failed to start: {error}")
|
||||||
@@ -9,7 +9,7 @@ from threading import Thread
|
|||||||
|
|
||||||
|
|
||||||
from dynalab_core.config import CoreConfig
|
from dynalab_core.config import CoreConfig
|
||||||
from dynalab_core.errors import (
|
from dynalab_core.protocols.json.errors import (
|
||||||
JsonServerStartupError,
|
JsonServerStartupError,
|
||||||
JsonServerTimeoutError,
|
JsonServerTimeoutError,
|
||||||
)
|
)
|
||||||
@@ -19,7 +19,6 @@ class JsonServer:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
config: CoreConfig,
|
config: CoreConfig,
|
||||||
stop_event: threading.Event,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
self._thread: Thread | None = None
|
self._thread: Thread | None = None
|
||||||
self._server: Server | None = None
|
self._server: Server | None = None
|
||||||
@@ -27,8 +26,9 @@ class JsonServer:
|
|||||||
self._startup_error: Exception | None = None
|
self._startup_error: Exception | None = None
|
||||||
self._debug_timeout_test: bool = False
|
self._debug_timeout_test: bool = False
|
||||||
self._config = config
|
self._config = config
|
||||||
self._global_stop_event = stop_event
|
|
||||||
self._timeout: float = 10
|
self._timeout: float = 10
|
||||||
|
self._stop_event = threading.Event()
|
||||||
|
self._stopped_event = threading.Event()
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
self._thread = Thread(
|
self._thread = Thread(
|
||||||
@@ -44,6 +44,10 @@ class JsonServer:
|
|||||||
if self._startup_error is not None:
|
if self._startup_error is not None:
|
||||||
raise JsonServerStartupError(self._startup_error)
|
raise JsonServerStartupError(self._startup_error)
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
self._stop_event.set()
|
||||||
|
self._stopped_event.wait(10)
|
||||||
|
|
||||||
def _json_server_thread_main(self) -> None:
|
def _json_server_thread_main(self) -> None:
|
||||||
asyncio.run(self._run_json_server())
|
asyncio.run(self._run_json_server())
|
||||||
|
|
||||||
@@ -64,14 +68,19 @@ class JsonServer:
|
|||||||
|
|
||||||
if not self._debug_timeout_test:
|
if not self._debug_timeout_test:
|
||||||
self._started_event.set()
|
self._started_event.set()
|
||||||
|
if self._startup_error is not None:
|
||||||
|
self._stopped_event.set()
|
||||||
|
return
|
||||||
async with self._server:
|
async with self._server:
|
||||||
await asyncio.to_thread(self._global_stop_event.wait)
|
await asyncio.to_thread(self._stop_event.wait)
|
||||||
|
self._stopped_event.set()
|
||||||
|
|
||||||
async def _handle_json_connection(
|
async def _handle_json_connection(
|
||||||
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
||||||
) -> None:
|
) -> None:
|
||||||
print(f"Connected to: {writer.get_extra_info('peername')}")
|
print(f"Connected to: {writer.get_extra_info('peername')}")
|
||||||
|
|
||||||
|
# TODO: Build out handler to handshake, register peer and pipe packets into connector queues
|
||||||
try:
|
try:
|
||||||
await reader.readline()
|
await reader.readline()
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from dynalab_core.protocols.packets.handshake import (
|
||||||
|
ConnectorHello,
|
||||||
|
DynaLabHello,
|
||||||
|
HandshakeAccepted,
|
||||||
|
HandshakeRejected,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
ProtocolMessage = Annotated[
|
||||||
|
DynaLabHello | ConnectorHello | HandshakeAccepted | HandshakeRejected,
|
||||||
|
Field(discriminator="type"),
|
||||||
|
]
|
||||||
+1
-1
@@ -6,7 +6,7 @@ from typing import Literal
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from dynalab_core.protocols.json.common import VersionDescriptor
|
from dynalab_core.protocols.common import VersionDescriptor
|
||||||
from dynalab_core.protocols.json.constants import PROTOCOL_VERSION
|
from dynalab_core.protocols.json.constants import PROTOCOL_VERSION
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user