Fast handler vs slow handler (the robosim split)
examples/2-fast-handler-cli.py is the pattern robosim's server
uses to answer commands: some responses can be computed the instant the message arrives, others must
be handled by the app's main loop. It drives ConnectionManager directly (no UICLIManager):
python examples/2-fast-handler-cli.py # run the server
printf 'fast\nslow\n' | ncat localhost 42069 # fast: answered from the network thread; slow: via the main loop
fast is answered immediately; slow is round-tripped through the main loop; anything else replies
"Unknown message" and closes the app.
Two threads, two ways to answer
The listener hands each connection to its own daemon thread (_handle_client in
ConnectionManager). Every received message is passed to network_handler, and whatever it
returns is sent back over the socket. That one fact gives you the two answer paths:
TIMEOUT_S = 10
def network_handler(message: Message) -> str:
logger.info(f"Received: {message}")
if message.data == "fast":
return "fast response\n" # fast: answer here, in the network thread
else:
return message.client.channel.put_then_get(message, timeout=TIMEOUT_S) # slow: hand off to the main loop and block
- Fast path — return the response directly. The network thread computes it and the bytes go straight out; the main loop never sees the message. Zero latency, no coupling.
- Slow path —
channel.put_then_get(message)pushes the message into the channel'stcp2mainqueue and blocks the network thread waiting for the app's answer onmain2tcp. The main loop does the real work and replies;put_then_getreturns that reply to the network thread, which sends it back.
The channel is the two queues (micronetcode.channel) that connect the two threads: tcp2main
(wire → app, capped by channel_queue_maxsize) and main2tcp (app → wire, depth 1). This is the
same mechanism the default handler uses — the one UICLIManager builds on top of.
The main loop is the slow handler
get_one_message() is the drain: it polls every channel's tcp2main queue (non-blocking) and
returns the first message found, raising Empty when there is nothing. The Message carries
message.client.channel, so the loop knows exactly which socket it is answering — it replies with
main2tcp.put(...), which unblocks the parked network thread:
while manager.is_alive():
try:
msg = manager.get_one_message()
if msg.data == "slow":
msg.client.channel.main2tcp.put("slow response\n", timeout=TIMEOUT_S)
else:
msg.client.channel.main2tcp.put(f"Unknown message: '{msg.data}'. Exiting\n", timeout=TIMEOUT_S)
break
except Empty:
pass
time.sleep(0.1)
Note the responses already end in \n: ShlexASCIICodec.encode sends the string byte-for-byte, it
does not append a newline for you. And TIMEOUT_S on both the forward (put_then_get) and return
(main2tcp.put) legs is the deadlock insurance: if one side stops answering, the other raises
Empty instead of hanging forever.
The robosim case
This is exactly the split in src/robosim/protocol.py. The network_handler there runs in the TCP
thread and answers robot_get_state_with_camera (FPV streaming) directly — the camera frame is read
under its lock and returned on the spot. Every other command (move, robot_get_state, connect,
...) is forwarded through put_then_get to the simulator main loop, which drains with
get_one_message() and answers on main2tcp. The main loop owns the sim state; the fast path owns
only what can be answered without touching it.
Full code
Everything above, in one file.
#!/usr/bin/env python
"""examples/2-fast-handler-cli.py - minimal CLI app with a TCP socket and a 'fast handler' (robosim-like)"""
from argparse import ArgumentParser, Namespace
from queue import Empty
import time
from loggez import loggez_logger as logger
from micronetcode import ConnectionManager, Message
from ui_cli_manager import ShlexASCIICodec
TIMEOUT_S = 10
def network_handler(message: Message) -> str:
logger.info(f"Received: {message}")
if message.data == "fast":
return "fast response\n"
else:
return message.client.channel.put_then_get(message, timeout=TIMEOUT_S)
def main(args: Namespace):
"""main fn"""
manager = ConnectionManager("0.0.0.0", port=args.port, codec=ShlexASCIICodec(), network_handler=network_handler)
manager.start()
while manager.is_alive():
try:
msg = manager.get_one_message()
if msg.data == "slow":
msg.client.channel.main2tcp.put("slow response\n", timeout=TIMEOUT_S)
else:
msg.client.channel.main2tcp.put(f"Unknown message: '{msg.data}'. Exiting\n", timeout=TIMEOUT_S)
break
except Empty:
pass
time.sleep(0.1)
if __name__ == "__main__":
parser = ArgumentParser()
parser.add_argument("--port", type=int, default=42069)
arg = parser.parse_args()
main(arg)