import threading import traceback from time import time from collections.abc import Callable import zmq from .common import HEARTBEAT_TOPIC, HEARTBEAT_INTERVAL class RpcServer: """""" def __init__(self) -> None: """ Constructor """ # Save functions dict: key is function name, value is function object self._functions: dict[str, Callable] = {} # Zmq port related self._context: zmq.Context = zmq.Context() # Reply socket (Request–reply pattern) self._socket_rep: zmq.Socket = self._context.socket(zmq.REP) # Publish socket (Publish–subscribe pattern) self._socket_pub: zmq.Socket = self._context.socket(zmq.PUB) # Worker thread related self._active: bool = False # RpcServer status self._thread: threading.Thread | None = None # RpcServer thread self._lock: threading.Lock = threading.Lock() # Heartbeat related self._heartbeat_at: float | None = None def is_active(self) -> bool: """""" return self._active def start( self, rep_address: str, pub_address: str, ) -> None: """ Start RpcServer """ if self._active: return # Bind socket address self._socket_rep.bind(rep_address) self._socket_pub.bind(pub_address) # Start RpcServer status self._active = True # Start RpcServer thread self._thread = threading.Thread(target=self.run) self._thread.start() # Init heartbeat publish timestamp self._heartbeat_at = time() + HEARTBEAT_INTERVAL def stop(self) -> None: """ Stop RpcServer """ if not self._active: return # Stop RpcServer status self._active = False def join(self) -> None: # Wait for RpcServer thread to exit if self._thread and self._thread.is_alive(): self._thread.join() self._thread = None def run(self) -> None: """ Run RpcServer functions """ while self._active: # Poll response socket for 1 second n: int = self._socket_rep.poll(1000) self.check_heartbeat() if not n: continue # Receive request data from Reply socket req = self._socket_rep.recv_pyobj() # Get function name and parameters name, args, kwargs = req # Try to get and execute callable function object; capture exception information if it fails try: func: Callable = self._functions[name] r: object = func(*args, **kwargs) rep: list = [True, r] except Exception as e: # noqa rep = [False, traceback.format_exc()] # send callable response by Reply socket self._socket_rep.send_pyobj(rep) # Unbind socket address self._socket_pub.close() self._socket_rep.close() def publish(self, topic: str, data: object) -> None: """ Publish data """ with self._lock: self._socket_pub.send_pyobj([topic, data]) def register(self, func: Callable) -> None: """ Register function """ self._functions[func.__name__] = func def check_heartbeat(self) -> None: """ Check whether it is required to send heartbeat. """ now: float = time() if self._heartbeat_at and now >= self._heartbeat_at: # Publish heartbeat self.publish(HEARTBEAT_TOPIC, now) # Update timestamp of next publish self._heartbeat_at = now + HEARTBEAT_INTERVAL