Apologies for the little bit of commented out code in this minimum working example. Here's a simple REQ-REP server that uses threads or processes to process requests:
import time
import threading
import multiprocessing
import zmq
from zmq.devices import ThreadProxy, ProcessProxy
import msgpack
import pendulum
def rep_worker_thread_func(url_work, url_shutdown):
i = 0
ctx = zmq.Context()
cmds = ctx.socket(zmq.REP)
killer = ctx.socket(zmq.SUB)
cmds.connect(url_work)
killer.connect(url_shutdown)
poll = zmq.Poller()
poll.register(cmds, zmq.POLLIN)
poll.register(killer, zmq.POLLIN)
while True:
socks = dict(poll.poll())
if killer in socks:
_ = killer.recv()
cmds.close()
killer.close()
return
if cmds in socks:
msg = cmds.recv()
msg = msgpack.unpackb(msg, use_list=False)
# msg is an ISO 8601 timestamp
then = pendulum.parse(msg)
if i % 10 == 0:
time.sleep(2) # """random""" high latency
now = pendulum.now('UTC')
diff = now.diff(then)._to_microseconds()
out = (msg, now.isoformat(), diff)
out = msgpack.packb(out)
cmds.send(out)
i += 1
if __name__ == '__main__':
ctx = zmq.Context()
NWORKERS = 20
WORKER_URL = 'tcp://127.0.0.1:6000'
WORKER_DIE_URL = 'tcp://127.0.0.1:6001'
command_in_url = 'tcp://127.0.0.1:9999'
for _ in range(NWORKERS):
# threading.Thread(daemon=True, target=rep_worker_thread_func,
# args=(WORKER_URL, WORKER_DIE_URL)).start()
multiprocessing.Process(daemon=True, target=rep_worker_thread_func, args=(WORKER_URL, WORKER_DIE_URL)).start()
# p = ThreadProxy(zmq.ROUTER, zmq.DEALER)
# p = ProcessProxy(zmq.ROUTER, zmq.DEALER)
# p.setsockopt_in(zmq.IDENTITY, b'ROUTER')
# p.setsockopt_out(zmq.IDENTITY, b'DEALER')
# p.bind_in(command_in_url)
# p.bind_out(WORKER_URL)
frontend = ctx.socket(zmq.ROUTER)
frontend.bind(command_in_url)
backend = ctx.socket(zmq.DEALER)
backend.bind(WORKER_URL)
zmq.proxy(frontend, backend)
# p.start()
# p.join()
You can comment/uncomment to use threads for the workers, or processes, or run the proxy in the main thread, or in a background thread, or a background process.
The corresponding client:
import time
import threading
import zmq
import msgpack
import pendulum
from matplotlib import pyplot as plt
command_in_url = 'tcp://127.0.0.1:9999'
# threads sharing global variables, filthy debug code not for prod
# python GIL, save me from myself
times = []
mu = threading.Lock()
def client_thread():
# I have no idea whether ZMQ spawning new sockets is thread
# safe so let's preclude that issue
# mu.acquire()
# s = ctx.socket(zmq.REQ)
# s.connect(command_in_url)
# mu.release()
ctx = zmq.Context()
s = ctx.socket(zmq.REQ)
s.connect(command_in_url)
while True:
now = pendulum.now('UTC').isoformat()
s.send(msgpack.packb(now))
buf = s.recv()
_, ping, latency = msgpack.unpackb(buf, use_list=False)
print(latency)
times.append(latency)
if __name__ == '__main__':
ctx = zmq.Context()
threads = []
NCLIENTS = 10
for _ in range(NCLIENTS):
t = threading.Thread(daemon=True, target=client_thread, args=tuple())
threads.append(t)
t.start()
try:
time.sleep(1e6)
except KeyboardInterrupt:
# threads won't stop, dirty dirty
plt.plot(times)
plt.show()
When one of the requests has a high latency, all other replies are blocked, creating 'blackout' periods, and in effect only one worker is really working at a time. As best I am able to understand, this is a correct implementation of the pattern in the ZMQ docs. So I would have expected the DEALER-ROUTER pair to transparently multiplex the requests and replies. But it seems their latencies are shared; have I done something wrong?