(py)ZMQ - REQ-REP DEALER-ROUTER shared latency?

Viewed 22

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?

0 Answers
Related