client.py
import threading
from time import time
from functools import lru_cache
from typing import Any
import zmq
from .common import HEARTBEAT_TOPIC, HEARTBEAT_TOLERANCE
class RemoteException(Exception):
"""
RPC remote exception
"""
def __init__(self, value: Any) -> None:
"""
Constructor
"""
self._value: Any = value
def __str__(self) -> str:
"""
Output error message
"""
return str(self._value)
class RpcClient:
""""""
def __init__(self) -> None:
"""Constructor"""
# zmq port related
self._context: zmq.Context = zmq.Context()
# Request socket (Request–reply pattern)
self._socket_req: zmq.Socket = self._context.socket(zmq.REQ)
# Subscribe socket (Publish–subscribe pattern)
self._socket_sub: zmq.Socket = self._context.socket(zmq.SUB)
# Set socket option to keepalive
for socket in [self._socket_req, self._socket_sub]:
socket.setsockopt(zmq.TCP_KEEPALIVE, 1)
socket.setsockopt(zmq.TCP_KEEPALIVE_IDLE, 60)
# Worker thread relate, used to process data pushed from server
self._active: bool = False # RpcClient status
self._thread: threading.Thread | None = None # RpcClient thread
self._lock: threading.Lock = threading.Lock()
self._last_received_ping: float = time()
@lru_cache(100) # noqa
def __getattr__(self, name: str) -> Any:
"""
Realize remote call function
"""
# Perform remote call task
def dorpc(*args: Any, **kwargs: Any) -> Any:
# Get timeout value from kwargs, default value is 30 seconds
timeout: int = kwargs.pop("timeout", 30000)
# Generate request
req: list = [name, args, kwargs]
# Send request and wait for response
with self._lock:
self._socket_req.send_pyobj(req)
# Timeout reached without any data
n: int = self._socket_req.poll(timeout)
if not n:
msg: str = f"Timeout of {timeout}ms reached for {req}"
raise RemoteException(msg)
rep = self._socket_req.recv_pyobj()
# Return response if successed; Trigger exception if failed
if rep[0]:
return rep[1]
else:
raise RemoteException(rep[1])
return dorpc
def start(
self,
req_address: str,
sub_address: str
) -> None:
"""
Start RpcClient
"""
if self._active:
return
# Connect zmq port
self._socket_req.connect(req_address)
self._socket_sub.connect(sub_address)
# Start RpcClient status
self._active = True
# Start RpcClient thread
self._thread = threading.Thread(target=self.run)
self._thread.start()
self._last_received_ping = time()
def stop(self) -> None:
"""
Stop RpcClient
"""
if not self._active:
return
# Stop RpcClient status
self._active = False
def join(self) -> None:
# Wait for RpcClient thread to exit
if self._thread and self._thread.is_alive():
self._thread.join()
self._thread = None
def run(self) -> None:
"""
Run RpcClient function
"""
pull_tolerance: int = HEARTBEAT_TOLERANCE * 1000
while self._active:
if not self._socket_sub.poll(pull_tolerance):
self.on_disconnected()
continue
# Receive data from subscribe socket
topic, data = self._socket_sub.recv_pyobj(flags=zmq.NOBLOCK)
if topic == HEARTBEAT_TOPIC:
self._last_received_ping = data
else:
# Process data by callable function
self.callback(topic, data)
# Close socket
self._socket_req.close()
self._socket_sub.close()
def callback(self, topic: str, data: Any) -> None:
"""
Callable function
"""
raise NotImplementedError
def subscribe_topic(self, topic: str) -> None:
"""
Subscribe data
"""
self._socket_sub.setsockopt_string(zmq.SUBSCRIBE, topic)
def on_disconnected(self) -> None:
"""
Callback when heartbeat is lost.
"""
msg: str = f"RpcServer has no response over {HEARTBEAT_TOLERANCE} seconds, please check you connection."
print(msg)