adds reconnection method to proxy class which is called when the sio client does not reconnect

This commit is contained in:
Mose Müller 2024-10-02 09:18:32 +02:00
parent 5ec7a8b530
commit 9eedf03c01
3 changed files with 122 additions and 2 deletions

View File

@ -86,7 +86,9 @@ class Client:
self._url = url self._url = url
self._sio = socketio.AsyncClient(**sio_client_kwargs) self._sio = socketio.AsyncClient(**sio_client_kwargs)
self._loop = asyncio.new_event_loop() self._loop = asyncio.new_event_loop()
self.proxy = ProxyClass(sio_client=self._sio, loop=self._loop) self.proxy = ProxyClass(
sio_client=self._sio, loop=self._loop, reconnect=self.connect
)
"""A proxy object representing the remote service, facilitating interaction as """A proxy object representing the remote service, facilitating interaction as
if it were local.""" if it were local."""
self._thread = threading.Thread( self._thread = threading.Thread(

View File

@ -1,5 +1,6 @@
import asyncio import asyncio
import logging import logging
from collections.abc import Callable
from copy import deepcopy from copy import deepcopy
from typing import TYPE_CHECKING, cast from typing import TYPE_CHECKING, cast
@ -24,6 +25,8 @@ class ProxyClass(ProxyClassMixin, pydase.components.DeviceConnection):
pydase service server. pydase service server.
loop: loop:
The event loop in which the client operations are managed and executed. The event loop in which the client operations are managed and executed.
reconnect:
The method that is called periodically when the client is not connected.
This class is used to create a proxy object that behaves like a local representation This class is used to create a proxy object that behaves like a local representation
of a remote pydase service, facilitating direct interaction as if it were local of a remote pydase service, facilitating direct interaction as if it were local
@ -47,7 +50,10 @@ class ProxyClass(ProxyClassMixin, pydase.components.DeviceConnection):
""" """
def __init__( def __init__(
self, sio_client: socketio.AsyncClient, loop: asyncio.AbstractEventLoop self,
sio_client: socketio.AsyncClient,
loop: asyncio.AbstractEventLoop,
reconnect: Callable[..., None],
) -> None: ) -> None:
if TYPE_CHECKING: if TYPE_CHECKING:
self._service_representation: None | SerializedObject = None self._service_representation: None | SerializedObject = None
@ -56,6 +62,7 @@ class ProxyClass(ProxyClassMixin, pydase.components.DeviceConnection):
pydase.components.DeviceConnection.__init__(self) pydase.components.DeviceConnection.__init__(self)
self._initialise(sio_client=sio_client, loop=loop) self._initialise(sio_client=sio_client, loop=loop)
object.__setattr__(self, "_service_representation", None) object.__setattr__(self, "_service_representation", None)
self.reconnect = reconnect
def serialize(self) -> SerializedObject: def serialize(self) -> SerializedObject:
if self._service_representation is None: if self._service_representation is None:
@ -99,3 +106,7 @@ class ProxyClass(ProxyClassMixin, pydase.components.DeviceConnection):
"readonly": readonly, "readonly": readonly,
"doc": doc, "doc": doc,
} }
def connect(self) -> None:
if not self._sio.reconnection or self._sio.reconnection_attempts > 0:
self.reconnect(block_until_connected=False)

View File

@ -0,0 +1,107 @@
import threading
from collections.abc import Callable, Generator
from typing import Any
import pydase
import pytest
import socketio.exceptions
@pytest.fixture(scope="function")
def pydase_restartable_server() -> (
Generator[
tuple[
pydase.Server,
threading.Thread,
pydase.DataService,
Callable[
[pydase.Server, threading.Thread, pydase.DataService],
tuple[pydase.Server, threading.Thread],
],
],
None,
Any,
]
):
class MyService(pydase.DataService):
def __init__(self) -> None:
super().__init__()
self._name = "MyService"
self._my_property = 12.1
@property
def my_property(self) -> float:
return self._my_property
@my_property.setter
def my_property(self, value: float) -> None:
self._my_property = value
@property
def name(self) -> str:
return self._name
service_instance = MyService()
server = pydase.Server(service_instance, web_port=9999)
thread = threading.Thread(target=server.run, daemon=True)
thread.start()
def restart(
server: pydase.Server,
thread: threading.Thread,
service_instance: pydase.DataService,
) -> tuple[pydase.Server, threading.Thread]:
server.handle_exit()
thread.join()
server = pydase.Server(service_instance, web_port=9999)
new_thread = threading.Thread(target=server.run, daemon=True)
new_thread.start()
return server, new_thread
yield server, thread, service_instance, restart
server.handle_exit()
thread.join()
def test_reconnection(
pydase_restartable_server: tuple[
pydase.Server,
threading.Thread,
pydase.DataService,
Callable[
[pydase.Server, threading.Thread, pydase.DataService],
tuple[pydase.Server, threading.Thread],
],
],
) -> None:
client = pydase.Client(
url="ws://localhost:9999",
sio_client_kwargs={
"reconnection": False,
},
)
client_2 = pydase.Client(
url="ws://localhost:9999",
sio_client_kwargs={
"reconnection_attempts": 1,
},
)
server, thread, service_instance, restart = pydase_restartable_server
service_instance._name = "New service name"
server, thread = restart(server, thread, service_instance)
with pytest.raises(socketio.exceptions.BadNamespaceError):
client.proxy.name
client_2.proxy.name
client.proxy.reconnect()
client_2.proxy.reconnect()
# the service proxies successfully reconnect and get the new service name
assert client.proxy.name == "New service name"
assert client_2.proxy.name == "New service name"