Source code for FIREQ_SERVER.network.send_worker

"""Handles the outgoing queue, sends serialized responses to the client socket, owns the sock.send_all() method.

The only rule on the queue objects is that they must have a "to_buffers" method that returns a sequence of
bytes-like objects ready for sendmsg(). The protocol is defined in the to_buffers method of the object.
"""

import logging
import socket
from queue import Empty
from threading import Event, Thread

from ..utils import ClientDisconnectedError, MemoryBoundedQueue


[docs] class SendWorker: """Worker thread that handles outgoing messages to the client socket. Waits for the client connected event to be set, which is done by the main thread. Then, it will start popping items from the queue_out queue and sending them to the client socket. """ def __init__(self, client_connected: Event, logger: logging.Logger = None) -> None: """Initialize the send worker. :param client_connected: Event that is set when a client is connected. :type client_connected: Event :param logger: Optional logger instance. :type logger: logging.Logger | None """ # output queue self.queue_out = MemoryBoundedQueue() # public attribute to set the client socket self.client_socket = socket.socket() # input mutable objects and events self._client_connected = client_connected self.log = logger or logging.getLogger(__name__) # thread safe events that determine if the thread is running and if the thread should stop self._stop_event = Event() self._thread_running = Event()
[docs] def start(self) -> None: """Start the send worker thread.""" self._thread = Thread(target=self._run, daemon=True) self._thread.start()
[docs] def stop(self) -> None: """Stop the send worker thread. The thread will be joined with a timeout of 2 seconds. """ self._stop_event.set() self._thread.join(timeout=2.0) if self._thread.is_alive(): self.log.warning("Thread did not exit cleanly")
[docs] def clear_output_queue(self) -> None: """Empty the output queue.""" # this is safe because the method owns the put method self.queue_out.clear()
def _run(self) -> None: self._thread_running.set() while not self._stop_event.is_set(): if not self._client_connected.wait(0.5): continue try: # pop an item from the queue item = self.queue_out.get(timeout=0.5) # send it over the socket self._send_payload(item) except Empty: continue except ClientDisconnectedError as e: self.log.info(f"Connection lost: {e}") self._client_connected.clear() continue except Exception as e: self.log.error(f"Unexpected error in send worker: {e}") break # exit the thread safely self.log.info("Detected stop flag or unexpected error, exiting...") self._cleanup_and_exit() def _send_payload(self, item: object) -> bytes: """Send a queue item over the network. The input item should implement the to_buffers() method, the result of which is directly sent over the network without any other checks. :param item: Data object implementing the to_buffers() method. :type item: object :return: Received bytes. :rtype: bytes """ while not self._stop_event.is_set(): try: serialized = item.to_buffers() self.client_socket.sendmsg(serialized) break except TimeoutError: # might change it later to add a maximum number of timeouts continue except (ConnectionResetError, ConnectionAbortedError) as e: # These indicate the connection is gone – no recovery possible. raise ClientDisconnectedError(f"Connection lost: {e}") from e except BrokenPipeError as e: # catch broken pipe in case of a disconnect during send raise ClientDisconnectedError(f"Broken pipe: {e}") from e except OSError as e: # Catch‑all for other operating‑system errors (e.g., EAGAIN, EWOULDBLOCK # in non‑blocking mode). If they arise, the connection is likely broken. raise ClientDisconnectedError(f"Unexpected OS error: {e}") from e def _cleanup_and_exit(self) -> None: """Clean-up and exit the thread safely.""" self._thread_running.clear()