diff --git a/concore_base.py b/concore_base.py index 0676bf7..4362424 100644 --- a/concore_base.py +++ b/concore_base.py @@ -42,6 +42,11 @@ def __init__(self, port_type, address, zmq_socket_type, context=None): self.socket.setsockopt(zmq.SNDTIMEO, 2000) # 2 sec send timeout self.socket.setsockopt(zmq.LINGER, 0) # Drop pending messages on close + # Let REQ send again after a missed reply, and drop stale replies + if zmq_socket_type == zmq.REQ: + self.socket.setsockopt(zmq.REQ_RELAXED, 1) + self.socket.setsockopt(zmq.REQ_CORRELATE, 1) + # Bind or connect if self.port_type == "bind": self.socket.bind(address) diff --git a/tests/test_concore.py b/tests/test_concore.py index 1e782ae..033a347 100644 --- a/tests/test_concore.py +++ b/tests/test_concore.py @@ -69,6 +69,40 @@ def test_class_is_defined(self): assert ZeroMQPort is not None + @patch("concore_base.time.sleep") + def test_req_recovers_after_missing_reply(self, _mock_sleep): + """Issue #598 – a REQ port must be able to send again after a + receive timeout, and a late reply must not be returned.""" + import zmq + import concore_base + + ctx = zmq.Context() + peer = ctx.socket(zmq.REP) + peer.setsockopt(zmq.LINGER, 0) + peer.setsockopt(zmq.RCVTIMEO, 2000) + port_num = peer.bind_to_random_port("tcp://127.0.0.1") + req = concore_base.ZeroMQPort( + "connect", f"tcp://127.0.0.1:{port_num}", zmq.REQ, ctx + ) + req.socket.setsockopt(zmq.RCVTIMEO, 50) + try: + req.send_json_with_retry([0, 1.0]) + assert peer.recv_json() == [0, 1.0] + with pytest.raises(TimeoutError): + req.recv_json_with_retry() + + req.send_json_with_retry([0, 2.0]) + peer.send_json("stale") + assert peer.recv_json() == [0, 2.0] + peer.send_json("fresh") + + req.socket.setsockopt(zmq.RCVTIMEO, 2000) + assert req.recv_json_with_retry() == "fresh" + finally: + req.socket.close() + peer.close() + ctx.term() + class TestDefaultConfiguration: def test_default_input_path(self):