Updated tests and added core live value handling

Removed useless tests, will need to complete a more comprehensive test
suite however this is not the priority right now

Added a live value dict in the core whose values are populated by the
core's input thread
This commit is contained in:
2026-08-05 20:42:19 +01:00
parent a882b29654
commit 561f42aca2
9 changed files with 104 additions and 79 deletions
+16 -13
View File
@@ -5,9 +5,10 @@
import logging import logging
from queue import Empty, Queue from queue import Empty, Queue
import threading import threading
from threading import Thread from threading import Lock, Thread
from time import monotonic, monotonic_ns, sleep from time import monotonic, monotonic_ns, sleep
from typing import Literal from typing import Literal
from uuid import UUID
from dynalab_core.config import CoreConfig from dynalab_core.config import CoreConfig
@@ -18,6 +19,7 @@ from dynalab_core.protocols.common import VersionDescriptor
from dynalab_core.protocols.json.server import JsonServer from dynalab_core.protocols.json.server import JsonServer
from dynalab_core.protocols.packets import ProtocolMessage from dynalab_core.protocols.packets import ProtocolMessage
from dynalab_core.protocols.packets.data import ValueDescriptor from dynalab_core.protocols.packets.data import ValueDescriptor
from dynalab_core.values import Value
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -32,6 +34,9 @@ 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
# TODO: Implement live values dict
self._live_values: dict[UUID, Value] = {}
self._live_values_lock = Lock()
self._data_input_queue: Queue[ProtocolMessage] = Queue(524288) self._data_input_queue: Queue[ProtocolMessage] = Queue(524288)
self._input_worker_thread = Thread( self._input_worker_thread = Thread(
@@ -98,21 +103,19 @@ class Core:
) )
def _input_worker(self) -> None: def _input_worker(self) -> None:
last_log = 0
while not self._stop_event.is_set(): while not self._stop_event.is_set():
now = monotonic_ns()
messages: list[ProtocolMessage] = []
try: try:
while not self._data_input_queue.empty(): message = self._data_input_queue.get_nowait()
messages.append(self._data_input_queue.get_nowait())
except Empty: except Empty:
self._stop_event.wait(0.01) self._stop_event.wait(0.01)
continue continue
else:
for message in messages:
if isinstance(message, ValueDescriptor):
pass
if now > last_log + 2000 * 1_000_000: if isinstance(message, ValueDescriptor):
log.debug(f"Core queue size: {self._data_input_queue.qsize()}") with self._live_values_lock:
last_log = now live_value = self._live_values.get(message.signal_id)
if live_value is None:
live_value = Value()
self._live_values[message.signal_id] = live_value
live_value.update(message.value)
+18 -4
View File
@@ -6,7 +6,7 @@ import logging
from queue import Empty, Full, Queue from queue import Empty, Full, Queue
import threading import threading
from threading import RLock, Thread from threading import RLock, Thread
from time import monotonic, sleep from time import monotonic, monotonic_ns, sleep
from uuid import UUID from uuid import UUID
from dynalab_core.constants import HELLO_PACKET from dynalab_core.constants import HELLO_PACKET
@@ -15,7 +15,7 @@ from dynalab_core.protocols.errors import (
ConnectorRegistryAlreadyRegisteredError, ConnectorRegistryAlreadyRegisteredError,
) )
from dynalab_core.protocols.packets import ProtocolMessage from dynalab_core.protocols.packets import ProtocolMessage
from dynalab_core.protocols.packets.data import ValueDescriptor from dynalab_core.protocols.packets.data import ValueBatch, ValueDescriptor
from dynalab_core.protocols.packets.handshake import ( from dynalab_core.protocols.packets.handshake import (
ConnectorHello, ConnectorHello,
HandshakeAccepted, HandshakeAccepted,
@@ -208,13 +208,17 @@ class ConnectorEndpoint:
"thread_name": self._input_worker_thread.name, "thread_name": self._input_worker_thread.name,
}, },
) )
last_log = 0
while not self._stop_event.is_set(): while not self._stop_event.is_set():
now = monotonic_ns()
messages: list[ProtocolMessage] = []
try: try:
message = self._get_ingress_packet_no_wait() while not self._packet_ingress_queue.empty():
messages.append(self._get_ingress_packet_no_wait())
except Empty: except Empty:
self._stop_event.wait(0.01)
continue continue
else: else:
for message in messages:
if isinstance(message, Heartbeat): if isinstance(message, Heartbeat):
self._heartbeat_ingress_queue.put_nowait(message) self._heartbeat_ingress_queue.put_nowait(message)
elif isinstance(message, ValueDescriptor): elif isinstance(message, ValueDescriptor):
@@ -222,6 +226,16 @@ class ConnectorEndpoint:
signal.id for signal in self._connector_hello.signals signal.id for signal in self._connector_hello.signals
): ):
self._core_input_queue.put_nowait(message) self._core_input_queue.put_nowait(message)
elif isinstance(message, ValueBatch):
for value in message.values:
if value.signal_id in (
signal.id for signal in self._connector_hello.signals
):
self._core_input_queue.put_nowait(value)
if now > last_log + 2000 * 1_000_000:
log.debug(f"Endpoint queue size: {self._packet_ingress_queue.qsize()}")
last_log = now
self._input_worker_stopped_event.set() self._input_worker_stopped_event.set()
log.debug( log.debug(
"Connector endpoint input worker %s stopped", "Connector endpoint input worker %s stopped",
+12 -1
View File
@@ -6,9 +6,10 @@ import asyncio
from asyncio import Server from asyncio import Server
import logging import logging
from queue import Empty from queue import Empty
from statistics import mean
import threading import threading
from threading import Thread from threading import Thread
from time import monotonic from time import monotonic, monotonic_ns
from uuid import uuid4 from uuid import uuid4
@@ -163,9 +164,19 @@ class JsonServer:
endpoint: ConnectorEndpoint, endpoint: ConnectorEndpoint,
) -> None: ) -> None:
log.debug(f"Started input task for {endpoint.uuid()}") log.debug(f"Started input task for {endpoint.uuid()}")
intervals: list[int] = []
last_val: int = 0
last_print: int = 0
while not self._stop_event.is_set(): while not self._stop_event.is_set():
message = await read_message(reader) message = await read_message(reader)
endpoint.put_ingress_packet(message) endpoint.put_ingress_packet(message)
now = monotonic_ns()
intervals.append(now - last_val)
last_val = now
if now > last_print + 2000 * 1_000_000:
log.debug(f"Server input frequency: {1_000_000 / mean(intervals)}kHz")
last_print = now
intervals = []
async def _output_task( async def _output_task(
self, self,
@@ -6,7 +6,7 @@ from typing import Annotated
from pydantic import Field from pydantic import Field
from dynalab_core.protocols.packets.data import ValueDescriptor from dynalab_core.protocols.packets.data import ValueBatch, ValueDescriptor
from dynalab_core.protocols.packets.handshake import ( from dynalab_core.protocols.packets.handshake import (
ConnectorHello, ConnectorHello,
DynaLabHello, DynaLabHello,
@@ -22,6 +22,7 @@ ProtocolMessage = Annotated[
| HandshakeAccepted | HandshakeAccepted
| HandshakeRejected | HandshakeRejected
| Heartbeat | Heartbeat
| ValueDescriptor, | ValueDescriptor
| ValueBatch,
Field(discriminator="type"), Field(discriminator="type"),
] ]
@@ -11,3 +11,8 @@ class ValueDescriptor(BaseModel):
type: Literal["value_descriptor"] = "value_descriptor" type: Literal["value_descriptor"] = "value_descriptor"
signal_id: UUID signal_id: UUID
value: float value: float
class ValueBatch(BaseModel):
type: Literal["value_batch"] = "value_batch"
values: list[ValueDescriptor]
+20
View File
@@ -0,0 +1,20 @@
# 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 threading import Lock
class Value:
_value: float = 0
_last_updated: int = 0
_lock: Lock = Lock()
def update(self, value: float) -> None:
with self._lock:
self._value = value
def get(self) -> float:
with self._lock:
return self._value
+19 -4
View File
@@ -12,7 +12,7 @@ from rich.logging import RichHandler
from dynalab_core.protocols.common import VersionDescriptor from dynalab_core.protocols.common import VersionDescriptor
from dynalab_core.protocols.constants import PROTOCOL_VERSION from dynalab_core.protocols.constants import PROTOCOL_VERSION
from dynalab_core.protocols.json.wire import read_message, write_message from dynalab_core.protocols.json.wire import read_message, write_message
from dynalab_core.protocols.packets.data import ValueDescriptor from dynalab_core.protocols.packets.data import ValueBatch, ValueDescriptor
from dynalab_core.protocols.packets.handshake import ( from dynalab_core.protocols.packets.handshake import (
ConnectorHello, ConnectorHello,
DynaLabHello, DynaLabHello,
@@ -26,9 +26,9 @@ PORT = 8765
# Set to None to send as quickly as possible. # Set to None to send as quickly as possible.
# For a controlled rate, use something like 5_000.0. # For a controlled rate, use something like 5_000.0.
TARGET_FREQUENCY_HZ: float | None = None TARGET_FREQUENCY_HZ: float | None = 1000
FREQUENCY_SAMPLE_SIZE = 500 FREQUENCY_SAMPLE_SIZE = 20000
CONNECTOR_VERSION = VersionDescriptor( CONNECTOR_VERSION = VersionDescriptor(
type="alpha", type="alpha",
@@ -116,11 +116,26 @@ def value_sender_thread(
next_send_time += period next_send_time += period
message = ValueDescriptor( value = ValueDescriptor(
signal_id=signal_id, signal_id=signal_id,
value=2.0, value=2.0,
) )
message = ValueBatch(
values=[
value,
value,
value,
value,
value,
value,
value,
value,
value,
value,
]
)
future = asyncio.run_coroutine_threadsafe( future = asyncio.run_coroutine_threadsafe(
send_message( send_message(
writer, writer,
-41
View File
@@ -1,41 +0,0 @@
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
+4 -7
View File
@@ -11,7 +11,7 @@ from dynalab_core.protocols.json.errors import (
JsonServerStartupError, JsonServerStartupError,
JsonServerTimeoutError, JsonServerTimeoutError,
) )
from dynalab_core.protocols.packets.handshake import ConnectorHello from dynalab_core.protocols.packets.handshake import ConnectorHello, SignalDescriptor
from test.common import find_available_port from test.common import find_available_port
@@ -93,9 +93,7 @@ def test_json_server_logs_invalid_handshake(
assert server_hello assert server_hello
peer.sendall(b"not-json\n") peer.sendall(b"not-json\n")
rejection = _wait_for_event( rejection = _wait_for_event(caplog, "connector.handshake_rejected")
caplog, "connector.handshake_rejected"
)
finally: finally:
core.stop() core.stop()
@@ -116,6 +114,7 @@ def test_json_server_waits_for_connection_handlers_on_stop(
protocol_version=PROTOCOL_VERSION, protocol_version=PROTOCOL_VERSION,
connector_name="Test connector", connector_name="Test connector",
connector_version="0.1.0-test", connector_version="0.1.0-test",
signals=[SignalDescriptor(id=uuid4(), name="Dummy signal", type="number")],
) )
with caplog.at_level(logging.DEBUG, logger="dynalab_core"): with caplog.at_level(logging.DEBUG, logger="dynalab_core"):
@@ -124,9 +123,7 @@ def test_json_server_waits_for_connection_handlers_on_stop(
with socket.create_connection(("127.0.0.1", port), timeout=1) as peer: with socket.create_connection(("127.0.0.1", port), timeout=1) as peer:
peer_file = peer.makefile("rb") peer_file = peer.makefile("rb")
assert peer_file.readline() assert peer_file.readline()
peer.sendall( peer.sendall(connector_hello.model_dump_json().encode("utf-8") + b"\n")
connector_hello.model_dump_json().encode("utf-8") + b"\n"
)
assert b'"type":"handshake_accepted"' in peer_file.readline() assert b'"type":"handshake_accepted"' in peer_file.readline()
core.stop() core.stop()