import time
from unittest.mock import MagicMock, call, patch
import pytest
from modelexpress.metadata.publisher import PublisherThread
READY = 2
STALE = 3
@pytest.fixture
def mx_client():
client = MagicMock()
client.update_status.return_value = True
return client
@pytest.fixture
def nixl_manager():
manager = MagicMock()
manager.is_healthy.return_value = True
manager.disconnect_remote_agents.return_value = 1
manager.data_plane_error = None
return manager
@pytest.fixture
def publisher(mx_client, nixl_manager):
with patch.dict("os.environ", {"MX_HEARTBEAT_INTERVAL_SECS": "1"}):
pub = PublisherThread(
mx_client=mx_client,
mx_source_id="abc123",
worker_id="w1",
worker_rank=0,
nixl_manager=nixl_manager,
)
yield pub
pub.stop()
def _status_calls(mx_client, status):
expected = {
"mx_source_id": "abc123",
"worker_id": "w1",
"worker_rank": 0,
"status": status,
}
return [
c
for c in mx_client.update_status.call_args_list
if all(c.kwargs.get(k) == v for k, v in expected.items())
]
class TestPublisherDoesNotOwnSharedPeers:
def test_stop_leaves_manager_peers_connected(self, publisher, nixl_manager):
publisher.start()
time.sleep(1.5)
publisher.stop()
nixl_manager.disconnect_remote_agents.assert_not_called()
def test_atexit_leaves_manager_teardown_to_its_owner(
self, publisher, nixl_manager
):
publisher.start()
time.sleep(1.5)
publisher._on_exit()
nixl_manager.disconnect_remote_agents.assert_not_called()
def test_atexit_joins_before_marking_stale(self, publisher, mx_client):
order = []
publisher._thread = MagicMock()
publisher._thread.join.side_effect = lambda **_: order.append("join")
publisher._started = True
mx_client.update_status.side_effect = lambda **kw: order.append(
f"status:{kw['status']}"
) or True
publisher._on_exit()
assert order == ["join", f"status:{STALE}"]
publisher._thread.join.assert_called_once_with(timeout=publisher._interval + 5)
def test_a_publisher_without_a_nixl_manager_is_fine(self, mx_client):
with patch.dict("os.environ", {"MX_HEARTBEAT_INTERVAL_SECS": "1"}):
pub = PublisherThread(
mx_client=mx_client,
mx_source_id="abc123",
worker_id="w1",
worker_rank=0,
nixl_manager=None,
)
pub.start()
time.sleep(1.2)
pub.stop()
class TestDemotesUnhealthyWorker:
def test_an_unhealthy_worker_is_marked_stale(
self, publisher, mx_client, nixl_manager
):
publisher.start()
time.sleep(1.5) nixl_manager.is_healthy.return_value = False
nixl_manager.data_plane_error = "Transfer timed out after 300.0s"
time.sleep(1.5)
assert _status_calls(mx_client, STALE), "unhealthy worker was not demoted"
def test_demotion_happens_once_not_every_interval(
self, publisher, mx_client, nixl_manager
):
publisher.start()
time.sleep(1.5)
nixl_manager.is_healthy.return_value = False
time.sleep(2.5)
publisher.stop()
assert len(_status_calls(mx_client, STALE)) <= 2
def test_failed_demotion_is_retried(
self, publisher, mx_client, nixl_manager
):
publisher._started = True
mx_client.update_status.side_effect = [RuntimeError("temporary"), True]
publisher._mark_unhealthy()
assert publisher._unhealthy is False
publisher._mark_unhealthy()
assert publisher._unhealthy is True
assert mx_client.update_status.call_count == 2
def test_recovery_restores_ready(self, publisher, mx_client, nixl_manager):
publisher.start()
time.sleep(1.5)
nixl_manager.is_healthy.return_value = False
time.sleep(1.5)
ready_before = len(_status_calls(mx_client, READY))
nixl_manager.is_healthy.return_value = True
time.sleep(1.5)
assert len(_status_calls(mx_client, READY)) > ready_before
def test_a_worker_that_never_went_ready_is_not_demoted(
self, mx_client, nixl_manager
):
nixl_manager.is_healthy.return_value = False
with patch.dict("os.environ", {"MX_HEARTBEAT_INTERVAL_SECS": "1"}):
pub = PublisherThread(
mx_client=mx_client,
mx_source_id="abc123",
worker_id="w1",
worker_rank=0,
nixl_manager=nixl_manager,
)
pub.start()
time.sleep(1.5)
pub.stop()
assert _status_calls(mx_client, STALE) == []