[Feature] Coordinator PUB mechanism optimization (#2442)

Signed-off-by: NumberWan <wantszkin2003@gmail.com>
This commit is contained in:
NumberWan authored and GitHub committed 2026-04-24 15:09:23 +08:00
1 parent 5632b78b3d
commit 678af19240
2 files changed
+107 -30

No files matched your search

@@ -41,6 +41,74 @@ def _wait_for_instance_list(
return None
def _drain_sub_messages(sub: zmq.Socket, max_seconds: float = 0.4) -> None:
"""Drain queued SUB messages for a short window."""
deadline = time.time() + max_seconds
while time.time() < deadline:
_recv_instance_list(sub, timeout_ms=50)
def test_omni_coordinator_pub_coalescing_on_rapid_queue_updates():
"""Rapid updates should be coalesced into fewer PUB messages."""
router_addr = get_engine_client_zmq_addr(
local_only=False,
host="127.0.0.1",
port=0,
)
pub_addr = get_engine_client_zmq_addr(
local_only=False,
host="127.0.0.1",
port=0,
)
coordinator = OmniCoordinator(
router_zmq_addr=router_addr,
pub_zmq_addr=pub_addr,
heartbeat_timeout=1000.0,
)
sub_ctx = zmq.Context.instance()
sub = sub_ctx.socket(zmq.SUB)
sub.connect(pub_addr)
sub.setsockopt(zmq.SUBSCRIBE, b"")
time.sleep(0.3) # PUB/SUB slow-joiner
client = OmniCoordClientForStage(
router_addr,
"tcp://stage:coalesce",
"tcp://stage:coalesce-out",
0,
)
# Wait for initial registration broadcast and clear any queued messages.
msg = _wait_for_instance_list(sub, expected_count=1)
assert msg is not None
_drain_sub_messages(sub)
# Burst many queue updates in a short period.
update_count = 80
for i in range(update_count):
client.update_info(queue_length=i)
# With publish_min_interval=0.1s, received messages over ~1s should be
# much smaller than update_count (coalescing effect).
window_s = 1.1
deadline = time.time() + window_s
recv_count = 0
while time.time() < deadline:
if _recv_instance_list(sub, timeout_ms=100) is not None:
recv_count += 1
assert recv_count < update_count // 2, (
f"expected coalesced PUB traffic, got {recv_count} for {update_count} updates"
)
client.close()
coordinator.close()
sub.close(0)
sub_ctx.term()
def test_omni_coordinator_registration_broadcast():
"""Verify that after multiple OmniCoordClientForStage instances register,
OmniCoordinator publishes an InstanceList containing all registered instances.
@@ -61,6 +61,7 @@ class OmniCoordinator:
self._publish_min_interval: float = 0.1 # seconds
self._pending_broadcast: bool = False
self._pending_lock = threading.Lock()
self._running = True
self._closed = False
@@ -84,13 +85,13 @@ class OmniCoordinator:
"""Add a new instance based on an incoming event."""
with self._lock:
self._add_new_instance_locked(event)
self.publish_instance_list_update()
self._schedule_broadcast()
def update_instance_info(self, event: InstanceEvent) -> None:
"""Update an existing instance based on an incoming event."""
with self._lock:
self._update_instance_info_locked(event)
self.publish_instance_list_update()
self._schedule_broadcast()
def remove_instance(self, event: InstanceEvent) -> None:
"""Mark an instance as removed / down based on an incoming event.
@@ -101,10 +102,15 @@ class OmniCoordinator:
"""
with self._lock:
self._remove_instance_locked(event)
self.publish_instance_list_update()
self._schedule_broadcast()
def publish_instance_list_update(self) -> None:
"""Publish the current active instance list to all subscribers."""
def publish_instance_list_update(self) -> bool:
"""Publish the current active instance list to all subscribers.
Returns:
True if the PUB send succeeded, False if it was dropped (e.g.
socket not ready when using ``zmq.NOBLOCK``).
"""
active_list = self.get_active_instances()
payload = asdict(active_list)
data = json.dumps(payload).encode("utf-8")
@@ -113,20 +119,18 @@ class OmniCoordinator:
try:
# PUB socket is best-effort; drop update if not ready.
self._pub.send(data, flags=zmq.NOBLOCK)
return True
except (zmq.Again, zmq.ZMQError):
# Silently ignore send failures; next update will catch up.
return
return False
def _schedule_broadcast(self, force: bool) -> None:
"""Schedule a broadcast, optionally bypassing throttling.
def _schedule_broadcast(self) -> None:
"""Request a broadcast to be flushed by the periodic loop.
When ``force`` is True, publish immediately. Otherwise, mark a pending
broadcast that will be flushed by the periodic loop at most once per
``_publish_min_interval``.
All broadcast requests are coalesced via ``_pending_broadcast`` and
flushed at most once per ``_publish_min_interval``.
"""
if force:
self.publish_instance_list_update()
else:
with self._pending_lock:
self._pending_broadcast = True
def _mark_instance_error_locked(self, info: InstanceInfo) -> None:
@@ -152,8 +156,8 @@ class OmniCoordinator:
for input_addr in to_delete:
del self._instances[input_addr]
if timed_out:
# Instance liveness changed; force immediate broadcast.
self._schedule_broadcast(force=True)
# Instance liveness changed; request broadcast.
self._schedule_broadcast()
def close(self) -> None:
"""Shut down background threads and close all ZMQ sockets."""
@@ -228,9 +232,9 @@ class OmniCoordinator:
def _periodic_loop(self) -> None:
"""Periodic loop to check heartbeat timeouts and flush broadcasts.
Heartbeat timeouts are checked on their original cadence, while
queue_length / non-liveness updates are coalesced and flushed at
most once per ``_publish_min_interval``.
Heartbeat timeouts are checked on their original cadence, while all
broadcast requests are coalesced and flushed at most once per
``_publish_min_interval``.
"""
heartbeat_interval = max(1.0, min(self._heartbeat_timeout / 2.0, 5.0))
loop_interval = self._publish_min_interval
@@ -243,9 +247,18 @@ class OmniCoordinator:
self._check_heartbeat_timeouts()
last_heartbeat_check = now
if self._pending_broadcast:
self.publish_instance_list_update()
self._pending_broadcast = False
with self._pending_lock:
has_pending_broadcast = self._pending_broadcast
if not has_pending_broadcast:
if self._stop_event.wait(timeout=loop_interval):
break
continue
# Publish outside lock. Clear pending only on success.
if self.publish_instance_list_update():
with self._pending_lock:
self._pending_broadcast = False
if self._stop_event.wait(timeout=loop_interval):
break
@@ -267,27 +280,23 @@ class OmniCoordinator:
info.status = StageStatus.UP
promote = True
if promote:
self._schedule_broadcast(force=True)
self._schedule_broadcast()
return
# Check-and-act under single lock to avoid TOCTOU race (duplicate
# registration when concurrent events arrive for the same instance).
with self._lock:
force_broadcast = False
if input_addr not in self._instances:
self._add_new_instance_locked(event)
force_broadcast = True
else:
if event.status == StageStatus.DOWN:
self._remove_instance_locked(event)
force_broadcast = True
else:
self._update_instance_info_locked(event)
# New instances / DOWN events are broadcast immediately; other
# updates (e.g. queue_length changes) are throttled via the
# periodic loop.
self._schedule_broadcast(force=force_broadcast)
# Any non-heartbeat state change that affects the active list
# is coalesced and flushed via the periodic loop.
self._schedule_broadcast()
except (KeyError, ValueError, TypeError) as e:
logger.warning("Dropping malformed event: %s", e)