Files
lmcache--lmcache/tests/v1/cache_controller/test_registration_controller.py
2026-07-13 12:24:33 +08:00

629 lines
22 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Unit tests for RegistrationController."""
# Standard
from unittest.mock import Mock, patch
import time
# Third Party
import pytest
# First Party
from lmcache.v1.cache_controller.controllers.registration_controller import (
RegistrationController,
)
from lmcache.v1.cache_controller.message import (
DeRegisterMsg,
HealthMsg,
HeartbeatMsg,
QueryInstMsg,
QueryWorkerInfoMsg,
RegisterMsg,
)
@pytest.fixture
def reg_controller(mock_kv_controller, mock_cluster_executor):
"""Create a RegistrationController instance for testing."""
controller = RegistrationController()
controller.post_init(
kv_controller=mock_kv_controller,
cluster_executor=mock_cluster_executor,
)
return controller
class TestRegistrationControllerRegister:
"""Test RegistrationController register operations."""
@pytest.mark.asyncio
async def test_register_new_worker(self, reg_controller, mock_zmq_socket):
"""Test registering a new worker."""
with patch(
"lmcache.v1.cache_controller.controllers.registration_controller.get_zmq_socket"
) as mock_get_socket:
mock_get_socket.return_value = mock_zmq_socket
msg = RegisterMsg(
instance_id="test_instance",
worker_id=0,
ip="127.0.0.1",
port=5000,
peer_init_url="tcp://127.0.0.1:6000",
)
await reg_controller.register(msg)
# Verify worker is registered
worker_ids = reg_controller.registry.get_worker_ids("test_instance")
assert 0 in worker_ids
# Verify socket is created
worker_node = reg_controller.registry.get_worker("test_instance", 0)
assert worker_node is not None
assert worker_node.socket is not None
assert worker_node.peer_init_url == "tcp://127.0.0.1:6000"
# Verify instance mapping
instance_node = reg_controller.registry.get_instance_by_ip("127.0.0.1")
assert instance_node is not None
assert instance_node.instance_id == "test_instance"
@pytest.mark.asyncio
async def test_register_without_peer_url(self, reg_controller, mock_zmq_socket):
"""Test registering a worker without peer_init_url."""
with patch(
"lmcache.v1.cache_controller.controllers.registration_controller.get_zmq_socket"
) as mock_get_socket:
mock_get_socket.return_value = mock_zmq_socket
msg = RegisterMsg(
instance_id="test_instance",
worker_id=0,
ip="127.0.0.1",
port=5000,
peer_init_url=None,
)
await reg_controller.register(msg)
# Verify worker is registered
worker_node = reg_controller.registry.get_worker("test_instance", 0)
assert worker_node is not None
assert worker_node.socket is not None
# Peer URL should not be registered
assert worker_node.peer_init_url is None
@pytest.mark.asyncio
async def test_register_duplicate_worker(self, reg_controller, mock_zmq_socket):
"""Test duplicate registration is prevented."""
with patch(
"lmcache.v1.cache_controller.controllers.registration_controller.get_zmq_socket"
) as mock_get_socket:
mock_get_socket.return_value = mock_zmq_socket
msg = RegisterMsg(
instance_id="test_instance",
worker_id=0,
ip="127.0.0.1",
port=5000,
peer_init_url="tcp://127.0.0.1:6000",
)
# Register once
await reg_controller.register(msg)
# Try to register again
await reg_controller.register(msg)
# Should still have only one entry
worker_ids = reg_controller.registry.get_worker_ids("test_instance")
assert len(worker_ids) == 1
@pytest.mark.asyncio
async def test_register_multiple_workers(self, reg_controller, mock_zmq_socket):
"""Test registering multiple workers for same instance."""
with patch(
"lmcache.v1.cache_controller.controllers.registration_controller.get_zmq_socket"
) as mock_get_socket:
mock_get_socket.return_value = mock_zmq_socket
for worker_id in range(3):
msg = RegisterMsg(
instance_id="test_instance",
worker_id=worker_id,
ip="127.0.0.1",
port=5000 + worker_id,
peer_init_url=f"tcp://127.0.0.1:{6000 + worker_id}",
)
await reg_controller.register(msg)
# Verify all workers are registered
worker_ids = reg_controller.registry.get_worker_ids("test_instance")
assert len(worker_ids) == 3
# Verify workers are sorted
assert worker_ids == [0, 1, 2]
@pytest.mark.asyncio
async def test_register_worker_info_timestamps(
self, reg_controller, mock_zmq_socket
):
"""Test worker info contains correct timestamps."""
with patch(
"lmcache.v1.cache_controller.controllers.registration_controller.get_zmq_socket"
) as mock_get_socket:
mock_get_socket.return_value = mock_zmq_socket
before_time = time.time()
msg = RegisterMsg(
instance_id="test_instance",
worker_id=0,
ip="127.0.0.1",
port=5000,
peer_init_url="tcp://127.0.0.1:6000",
)
await reg_controller.register(msg)
after_time = time.time()
worker_node = reg_controller.registry.get_worker("test_instance", 0)
assert worker_node is not None
assert before_time <= worker_node.registration_time <= after_time
assert before_time <= worker_node.last_heartbeat_time <= after_time
class TestRegistrationControllerDeregister:
"""Test RegistrationController deregister operations."""
@pytest.mark.asyncio
async def test_deregister_worker(self, reg_controller, mock_zmq_socket):
"""Test deregistering a worker."""
with patch(
"lmcache.v1.cache_controller.controllers.registration_controller.get_zmq_socket"
) as mock_get_socket:
mock_get_socket.return_value = mock_zmq_socket
# First register
register_msg = RegisterMsg(
instance_id="test_instance",
worker_id=0,
ip="127.0.0.1",
port=5000,
peer_init_url="tcp://127.0.0.1:6000",
)
await reg_controller.register(register_msg)
# Then deregister
deregister_msg = DeRegisterMsg(
instance_id="test_instance",
worker_id=0,
ip="127.0.0.1",
port=5000,
)
await reg_controller.deregister(deregister_msg)
# Verify cleanup
worker_node = reg_controller.registry.get_worker("test_instance", 0)
assert worker_node is None
instance_node = reg_controller.registry.get_instance("test_instance")
assert instance_node is None
# Note: KV cleanup is now handled directly by the registry's
# deregister_worker method
@pytest.mark.asyncio
async def test_deregister_nonexistent_worker(self, reg_controller):
"""Test deregistering a non-existent worker."""
deregister_msg = DeRegisterMsg(
instance_id="nonexistent_instance",
worker_id=0,
ip="127.0.0.1",
port=5000,
)
# Should not raise an error
await reg_controller.deregister(deregister_msg)
@pytest.mark.asyncio
async def test_deregister_partial_workers(self, reg_controller, mock_zmq_socket):
"""Test deregistering one of multiple workers."""
with patch(
"lmcache.v1.cache_controller.controllers.registration_controller.get_zmq_socket"
) as mock_get_socket:
mock_get_socket.return_value = mock_zmq_socket
# Register two workers
for worker_id in range(2):
msg = RegisterMsg(
instance_id="test_instance",
worker_id=worker_id,
ip="127.0.0.1",
port=5000 + worker_id,
peer_init_url=f"tcp://127.0.0.1:{6000 + worker_id}",
)
await reg_controller.register(msg)
# Deregister only worker 0
deregister_msg = DeRegisterMsg(
instance_id="test_instance",
worker_id=0,
ip="127.0.0.1",
port=5000,
)
await reg_controller.deregister(deregister_msg)
# Worker 1 should still be registered
worker_ids = reg_controller.registry.get_worker_ids("test_instance")
assert worker_ids == [1]
worker_node = reg_controller.registry.get_worker("test_instance", 1)
assert worker_node is not None
assert worker_node.socket is not None
class TestRegistrationControllerGetters:
"""Test RegistrationController getter methods."""
@pytest.mark.asyncio
async def test_get_socket(self, reg_controller, mock_zmq_socket):
"""Test getting socket for a worker."""
with patch(
"lmcache.v1.cache_controller.controllers.registration_controller.get_zmq_socket"
) as mock_get_socket:
mock_get_socket.return_value = mock_zmq_socket
msg = RegisterMsg(
instance_id="test_instance",
worker_id=0,
ip="127.0.0.1",
port=5000,
peer_init_url="tcp://127.0.0.1:6000",
)
await reg_controller.register(msg)
socket = reg_controller.get_socket("test_instance", 0)
assert socket is not None
assert socket == mock_zmq_socket
def test_get_socket_nonexistent(self, reg_controller):
"""Test getting socket for non-existent worker."""
socket = reg_controller.get_socket("nonexistent", 0)
assert socket is None
@pytest.mark.asyncio
async def test_get_peer_init_url(self, reg_controller, mock_zmq_socket):
"""Test getting peer init URL for a worker."""
with patch(
"lmcache.v1.cache_controller.controllers.registration_controller.get_zmq_socket"
) as mock_get_socket:
mock_get_socket.return_value = mock_zmq_socket
msg = RegisterMsg(
instance_id="test_instance",
worker_id=0,
ip="127.0.0.1",
port=5000,
peer_init_url="tcp://127.0.0.1:6000",
)
await reg_controller.register(msg)
url = reg_controller.get_peer_init_url("test_instance", 0)
assert url == "tcp://127.0.0.1:6000"
def test_get_peer_init_url_nonexistent(self, reg_controller):
"""Test getting peer init URL for non-existent worker."""
url = reg_controller.get_peer_init_url("nonexistent", 0)
assert url is None
@pytest.mark.asyncio
async def test_get_workers(self, reg_controller, mock_zmq_socket):
"""Test getting workers for an instance."""
with patch(
"lmcache.v1.cache_controller.controllers.registration_controller.get_zmq_socket"
) as mock_get_socket:
mock_get_socket.return_value = mock_zmq_socket
for worker_id in [2, 0, 1]: # Register out of order
msg = RegisterMsg(
instance_id="test_instance",
worker_id=worker_id,
ip="127.0.0.1",
port=5000 + worker_id,
peer_init_url=f"tcp://127.0.0.1:{6000 + worker_id}",
)
await reg_controller.register(msg)
workers = reg_controller.get_workers("test_instance")
# Should be sorted
assert workers == [0, 1, 2]
def test_get_workers_nonexistent(self, reg_controller):
"""Test getting workers for non-existent instance."""
workers = reg_controller.get_workers("nonexistent")
assert workers == []
class TestRegistrationControllerInstanceMapping:
"""Test RegistrationController instance ID mapping."""
@pytest.mark.asyncio
async def test_get_instance_id(self, reg_controller, mock_zmq_socket):
"""Test getting instance ID from IP."""
with patch(
"lmcache.v1.cache_controller.controllers.registration_controller.get_zmq_socket"
) as mock_get_socket:
mock_get_socket.return_value = mock_zmq_socket
msg = RegisterMsg(
instance_id="test_instance",
worker_id=0,
ip="127.0.0.1",
port=5000,
peer_init_url="tcp://127.0.0.1:6000",
)
await reg_controller.register(msg)
query_msg = QueryInstMsg(
event_id="event_123",
ip="127.0.0.1",
)
result = await reg_controller.get_instance_id(query_msg)
assert result.event_id == "event_123"
assert result.instance_id == "test_instance"
@pytest.mark.asyncio
async def test_get_instance_id_not_found(self, reg_controller):
"""Test getting instance ID for unregistered IP."""
query_msg = QueryInstMsg(
event_id="event_123",
ip="192.168.1.1",
)
result = await reg_controller.get_instance_id(query_msg)
assert result.event_id == "event_123"
assert result.instance_id is None
class TestRegistrationControllerHeartbeat:
"""Test RegistrationController heartbeat operations."""
@pytest.mark.asyncio
async def test_heartbeat_existing_worker(self, reg_controller, mock_zmq_socket):
"""Test heartbeat updates timestamp for existing worker."""
with patch(
"lmcache.v1.cache_controller.controllers.registration_controller.get_zmq_socket"
) as mock_get_socket:
mock_get_socket.return_value = mock_zmq_socket
# Register worker
register_msg = RegisterMsg(
instance_id="test_instance",
worker_id=0,
ip="127.0.0.1",
port=5000,
peer_init_url="tcp://127.0.0.1:6000",
)
await reg_controller.register(register_msg)
worker_node = reg_controller.registry.get_worker("test_instance", 0)
assert worker_node is not None
original_time = worker_node.last_heartbeat_time
# Wait a bit
time.sleep(0.1)
# Send heartbeat
heartbeat_msg = HeartbeatMsg(
instance_id="test_instance",
worker_id=0,
ip="127.0.0.1",
port=5000,
peer_init_url="tcp://127.0.0.1:6000",
)
await reg_controller.heartbeat(heartbeat_msg)
# Verify timestamp was updated
worker_node = reg_controller.registry.get_worker("test_instance", 0)
assert worker_node is not None
new_time = worker_node.last_heartbeat_time
assert new_time > original_time
@pytest.mark.asyncio
async def test_heartbeat_unregistered_worker(self, reg_controller, mock_zmq_socket):
"""Test heartbeat triggers re-registration for unregistered worker."""
with patch(
"lmcache.v1.cache_controller.controllers.registration_controller.get_zmq_socket"
) as mock_get_socket:
mock_get_socket.return_value = mock_zmq_socket
heartbeat_msg = HeartbeatMsg(
instance_id="test_instance",
worker_id=0,
ip="127.0.0.1",
port=5000,
peer_init_url="tcp://127.0.0.1:6000",
)
await reg_controller.heartbeat(heartbeat_msg)
# Worker should now be registered
worker_node = reg_controller.registry.get_worker("test_instance", 0)
assert worker_node is not None
class TestRegistrationControllerQueryWorkerInfo:
"""Test RegistrationController query worker info operations."""
@pytest.mark.asyncio
async def test_query_worker_info(self, reg_controller, mock_zmq_socket):
"""Test querying worker information."""
with patch(
"lmcache.v1.cache_controller.controllers.registration_controller.get_zmq_socket"
) as mock_get_socket:
mock_get_socket.return_value = mock_zmq_socket
# Register workers
for worker_id in range(3):
msg = RegisterMsg(
instance_id="test_instance",
worker_id=worker_id,
ip="127.0.0.1",
port=5000 + worker_id,
peer_init_url=f"tcp://127.0.0.1:{6000 + worker_id}",
)
await reg_controller.register(msg)
# Query all workers
query_msg = QueryWorkerInfoMsg(
event_id="event_123",
instance_id="test_instance",
worker_ids=None,
)
result = await reg_controller.query_worker_info(query_msg)
assert result.event_id == "event_123"
assert len(result.worker_infos) == 3
assert result.worker_infos[0].worker_id == 0
assert result.worker_infos[1].worker_id == 1
assert result.worker_infos[2].worker_id == 2
@pytest.mark.asyncio
async def test_query_worker_info_specific_workers(
self, reg_controller, mock_zmq_socket
):
"""Test querying specific workers."""
with patch(
"lmcache.v1.cache_controller.controllers.registration_controller.get_zmq_socket"
) as mock_get_socket:
mock_get_socket.return_value = mock_zmq_socket
# Register workers
for worker_id in range(3):
msg = RegisterMsg(
instance_id="test_instance",
worker_id=worker_id,
ip="127.0.0.1",
port=5000 + worker_id,
peer_init_url=f"tcp://127.0.0.1:{6000 + worker_id}",
)
await reg_controller.register(msg)
# Query specific workers
query_msg = QueryWorkerInfoMsg(
event_id="event_123",
instance_id="test_instance",
worker_ids=[0, 2],
)
result = await reg_controller.query_worker_info(query_msg)
assert len(result.worker_infos) == 2
assert result.worker_infos[0].worker_id == 0
assert result.worker_infos[1].worker_id == 2
@pytest.mark.asyncio
async def test_query_worker_info_nonexistent_instance(self, reg_controller):
"""Test querying worker info for non-existent instance."""
query_msg = QueryWorkerInfoMsg(
event_id="event_123",
instance_id="nonexistent",
worker_ids=None,
)
result = await reg_controller.query_worker_info(query_msg)
assert result.event_id == "event_123"
assert len(result.worker_infos) == 0
@pytest.mark.asyncio
async def test_query_worker_info_empty_list(self, reg_controller, mock_zmq_socket):
"""Test querying with empty worker_ids list returns all workers."""
with patch(
"lmcache.v1.cache_controller.controllers.registration_controller.get_zmq_socket"
) as mock_get_socket:
mock_get_socket.return_value = mock_zmq_socket
# Register workers
for worker_id in range(2):
msg = RegisterMsg(
instance_id="test_instance",
worker_id=worker_id,
ip="127.0.0.1",
port=5000 + worker_id,
peer_init_url=f"tcp://127.0.0.1:{6000 + worker_id}",
)
await reg_controller.register(msg)
# Query with empty list
query_msg = QueryWorkerInfoMsg(
event_id="event_123",
instance_id="test_instance",
worker_ids=[],
)
result = await reg_controller.query_worker_info(query_msg)
# Should return all workers
assert len(result.worker_infos) == 2
class TestRegistrationControllerHealth:
"""Test RegistrationController health operations."""
@pytest.mark.asyncio
async def test_health_delegates_to_executor(self, reg_controller):
"""Test health check delegates to cluster executor."""
msg = HealthMsg(
event_id="event_123",
instance_id="test_instance",
)
await reg_controller.health(msg)
reg_controller.cluster_executor.execute.assert_called_once_with("health", msg)
class TestRegistrationControllerMetrics:
"""Test RegistrationController metrics setup."""
@patch(
"lmcache.v1.cache_controller.controllers.registration_controller.PrometheusLogger"
)
def test_metrics_setup(self, mock_prometheus_logger):
"""Test metrics are properly set up."""
mock_logger_instance = Mock()
mock_prometheus_logger.GetInstanceOrNone.return_value = mock_logger_instance
_controller = RegistrationController()
# Verify metrics function was set
mock_logger_instance.registered_workers_count.set_function.assert_called_once()
@patch(
"lmcache.v1.cache_controller.controllers.registration_controller.PrometheusLogger"
)
def test_metrics_setup_no_logger(self, mock_prometheus_logger):
"""Test metrics setup when no logger is available."""
mock_prometheus_logger.GetInstanceOrNone.return_value = None
# Should not raise an error
controller = RegistrationController()
assert controller is not None