Files
2026-07-13 12:24:33 +08:00

1164 lines
40 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""
Unit tests for RegistryTree with fine-grained locking.
Tests verify that the object-based locking mechanism provides:
1. Thread-safe operations on instances, workers, and KV stores
2. Concurrent operations on different instances don't block each other
3. Data consistency under high concurrency
"""
# Standard
from concurrent.futures import ThreadPoolExecutor
import threading
import time
# First Party
from lmcache.v1.cache_controller.message import BatchedKVOperationMsg, KVOpEvent, OpType
from lmcache.v1.cache_controller.utils import (
InstanceNode,
RegistryTree,
WorkerNode,
)
class TestWorkerNodeLocking:
"""Test WorkerNode's internal locking for kv_store and seq_tracker."""
def test_concurrent_admit_kv(self):
"""Test concurrent KV admission on the same worker."""
worker = WorkerNode(
worker_id=0,
ip="127.0.0.1",
port=8000,
peer_init_url=None,
socket=None,
registration_time=time.time(),
last_heartbeat_time=time.time(),
)
location = "test_location"
num_threads = 10
keys_per_thread = 100
def admit_keys(thread_id):
for i in range(keys_per_thread):
key = thread_id * keys_per_thread + i
msg = BatchedKVOperationMsg(
instance_id="test_instance",
worker_id=worker.worker_id,
location=location,
operations=[
KVOpEvent(
op_type=OpType.ADMIT,
key=key,
seq_num=thread_id * keys_per_thread + i,
)
],
)
worker.handle_batched_kv_operations(msg)
threads = [
threading.Thread(target=admit_keys, args=(i,)) for i in range(num_threads)
]
for t in threads:
t.start()
for t in threads:
t.join()
# All keys should be admitted
assert worker.get_kv_count() == num_threads * keys_per_thread
def test_concurrent_admit_evict_kv(self):
"""Test concurrent admit and evict on the same worker."""
worker = WorkerNode(
worker_id=0,
ip="127.0.0.1",
port=8000,
peer_init_url=None,
socket=None,
registration_time=time.time(),
last_heartbeat_time=time.time(),
)
location = "test_location"
num_threads = 5
operations_per_thread = 100
errors = []
def mixed_operations(thread_id):
for i in range(operations_per_thread):
key = i # Same key range for all threads
try:
if i % 2 == 0:
msg = BatchedKVOperationMsg(
instance_id="test_instance",
worker_id=worker.worker_id,
location=location,
operations=[
KVOpEvent(
op_type=OpType.ADMIT,
key=key,
seq_num=thread_id * operations_per_thread + i,
)
],
)
else:
msg = BatchedKVOperationMsg(
instance_id="test_instance",
worker_id=worker.worker_id,
location=location,
operations=[
KVOpEvent(
op_type=OpType.EVICT,
key=key,
seq_num=thread_id * operations_per_thread + i,
)
],
)
worker.handle_batched_kv_operations(msg)
except Exception as e:
errors.append("Thread %d error: %s" % (thread_id, e))
threads = [
threading.Thread(target=mixed_operations, args=(i,))
for i in range(num_threads)
]
for t in threads:
t.start()
for t in threads:
t.join()
assert not errors, "Errors occurred: %s" % errors
def test_concurrent_seq_num_update(self):
"""Test concurrent sequence number updates."""
worker = WorkerNode(
worker_id=0,
ip="127.0.0.1",
port=8000,
peer_init_url=None,
socket=None,
registration_time=time.time(),
last_heartbeat_time=time.time(),
)
location = "test_location"
num_threads = 10
updates_per_thread = 100
def update_seq(thread_id):
for i in range(updates_per_thread):
worker.update_seq_num(location, thread_id * updates_per_thread + i)
threads = [
threading.Thread(target=update_seq, args=(i,)) for i in range(num_threads)
]
for t in threads:
t.start()
for t in threads:
t.join()
# Final seq_num should be set (exact value depends on thread order)
final_seq = worker.get_seq_num(location)
assert final_seq is not None
class TestInstanceNodeLocking:
"""Test InstanceNode's internal locking for workers dict."""
def test_concurrent_add_workers(self):
"""Test concurrent worker additions to same instance."""
instance = InstanceNode(instance_id="test_instance")
num_threads = 10
def add_worker(worker_id):
worker = WorkerNode(
worker_id=worker_id,
ip="127.0.0.1",
port=8000 + worker_id,
peer_init_url=None,
socket=None,
registration_time=time.time(),
last_heartbeat_time=time.time(),
)
instance.add_worker(worker)
threads = [
threading.Thread(target=add_worker, args=(i,)) for i in range(num_threads)
]
for t in threads:
t.start()
for t in threads:
t.join()
# All workers should be added
assert len(instance.get_worker_ids()) == num_threads
def test_concurrent_add_remove_workers(self):
"""Test concurrent worker add/remove on same instance."""
instance = InstanceNode(instance_id="test_instance")
errors = []
# Pre-add some workers
for i in range(50):
worker = WorkerNode(
worker_id=i,
ip="127.0.0.1",
port=8000 + i,
peer_init_url=None,
socket=None,
registration_time=time.time(),
last_heartbeat_time=time.time(),
)
instance.add_worker(worker)
def add_workers(start_id):
for i in range(10):
try:
worker = WorkerNode(
worker_id=start_id + i,
ip="127.0.0.1",
port=9000 + start_id + i,
peer_init_url=None,
socket=None,
registration_time=time.time(),
last_heartbeat_time=time.time(),
)
instance.add_worker(worker)
except Exception as e:
errors.append("Add error: %s" % e)
def remove_workers(start_id):
for i in range(10):
try:
instance.remove_worker(start_id + i)
except Exception as e:
errors.append("Remove error: %s" % e)
threads = []
# Add workers 100-199
for i in range(10):
threads.append(threading.Thread(target=add_workers, args=(100 + i * 10,)))
# Remove workers 0-49
for i in range(5):
threads.append(threading.Thread(target=remove_workers, args=(i * 10,)))
for t in threads:
t.start()
for t in threads:
t.join()
assert not errors, "Errors occurred: %s" % errors
class TestRegistryTreeFineGrainedLocking:
"""Test RegistryTree's fine-grained object-based locking."""
def test_concurrent_operations_different_instances(self):
"""
Test that operations on different instances don't block each other.
This is the key benefit of fine-grained locking.
"""
registry = RegistryTree()
num_instances = 5
workers_per_instance = 20
errors = []
timing_results = []
def operate_on_instance(instance_idx):
nonlocal errors
instance_id = "instance_%d" % instance_idx
start_time = time.time()
for worker_id in range(workers_per_instance):
try:
# Register worker
registry.register_worker(
instance_id=instance_id,
worker_id=worker_id,
ip="192.168.%d.%d" % (instance_idx, worker_id),
port=8000 + worker_id,
peer_init_url=None,
socket=None,
registration_time=time.time(),
)
# KV operations
for kv_key in range(10):
msg = BatchedKVOperationMsg(
instance_id=instance_id,
worker_id=worker_id,
location="location_%d" % worker_id,
operations=[
KVOpEvent(
op_type=OpType.ADMIT,
key=kv_key,
seq_num=kv_key,
)
],
)
registry.handle_batched_kv_operations(msg)
except Exception as e:
errors.append("Instance %d error: %s" % (instance_idx, e))
elapsed = time.time() - start_time
timing_results.append((instance_idx, elapsed))
threads = [
threading.Thread(target=operate_on_instance, args=(i,))
for i in range(num_instances)
]
for t in threads:
t.start()
for t in threads:
t.join()
assert not errors, "Errors occurred: %s" % errors
# Verify all data
for i in range(num_instances):
instance_id = "instance_%d" % i
worker_ids = registry.get_worker_ids(instance_id)
assert len(worker_ids) == workers_per_instance, (
"Instance %d: expected %d workers, got %d"
% (
i,
workers_per_instance,
len(worker_ids),
)
)
def test_concurrent_register_deregister_same_instance(self):
"""Test concurrent register/deregister on the same instance."""
registry = RegistryTree()
instance_id = "test_instance"
num_threads = 10
operations_per_thread = 50
errors = []
def register_deregister(thread_id):
for i in range(operations_per_thread):
worker_id = thread_id * 1000 + i
try:
# Register
registry.register_worker(
instance_id=instance_id,
worker_id=worker_id,
ip="192.168.1.%d" % thread_id,
port=8000 + i,
peer_init_url=None,
socket=None,
registration_time=time.time(),
)
# Verify registration
worker = registry.get_worker(instance_id, worker_id)
if worker is None:
errors.append(
"Thread %d: Worker %d not found after register"
% (thread_id, worker_id)
)
continue
# Deregister
result = registry.deregister_worker(instance_id, worker_id)
if result is None:
errors.append(
"Thread %d: Deregister failed for worker %d"
% (thread_id, worker_id)
)
except Exception as e:
errors.append("Thread %d error: %s" % (thread_id, e))
threads = [
threading.Thread(target=register_deregister, args=(i,))
for i in range(num_threads)
]
for t in threads:
t.start()
for t in threads:
t.join()
assert not errors, "Errors occurred: %s" % errors
def test_concurrent_kv_operations_same_worker(self):
"""Test concurrent KV operations on the same worker."""
registry = RegistryTree()
instance_id = "test_instance"
worker_id = 0
registry.register_worker(
instance_id=instance_id,
worker_id=worker_id,
ip="127.0.0.1",
port=8000,
peer_init_url=None,
socket=None,
registration_time=time.time(),
)
location = "test_location"
num_threads = 10
keys_per_thread = 100
errors = []
def kv_operations(thread_id):
nonlocal errors
for i in range(keys_per_thread):
key = thread_id * keys_per_thread + i
try:
# Admit using batched operation message
msg = BatchedKVOperationMsg(
instance_id=instance_id,
worker_id=worker_id,
location=location,
operations=[
KVOpEvent(
op_type=OpType.ADMIT,
key=key,
seq_num=thread_id * keys_per_thread + i,
)
],
)
result = registry.handle_batched_kv_operations(msg)
if not result:
errors.append(
"Thread %d: handle_batched_kv_operations failed for key %d"
% (thread_id, key)
)
except Exception as e:
errors.append("Thread %d error: %s" % (thread_id, e))
threads = [
threading.Thread(target=kv_operations, args=(i,))
for i in range(num_threads)
]
for t in threads:
t.start()
for t in threads:
t.join()
assert not errors, "Errors occurred: %s" % errors
# Verify total KV count
total = registry.get_total_kv_count()
assert total == num_threads * keys_per_thread
def test_find_kv_concurrent_with_admit(self):
"""Test find_kv while concurrent admit operations are happening."""
registry = RegistryTree()
num_instances = 3
workers_per_instance = 3
# Setup: register workers
for i in range(num_instances):
for w in range(workers_per_instance):
registry.register_worker(
instance_id="instance_%d" % i,
worker_id=w,
ip="192.168.%d.%d" % (i, w),
port=8000 + w,
peer_init_url=None,
socket=None,
registration_time=time.time(),
)
errors = []
found_keys = []
def admit_keys(instance_idx, worker_id):
nonlocal errors
for key in range(100):
try:
msg = BatchedKVOperationMsg(
instance_id="instance_%d" % instance_idx,
worker_id=worker_id,
location="location_%d" % worker_id,
operations=[
KVOpEvent(
op_type=OpType.ADMIT,
key=key + instance_idx * 1000,
seq_num=key,
)
],
)
registry.handle_batched_kv_operations(msg)
except Exception as e:
errors.append("Admit error: %s" % e)
def find_keys():
for _ in range(50):
for key in range(100):
try:
result = registry.find_kv(key)
if result:
found_keys.append(key)
except Exception as e:
errors.append("Find error: %s" % e)
time.sleep(0.001)
threads = []
# Admit threads
for i in range(num_instances):
for w in range(workers_per_instance):
threads.append(threading.Thread(target=admit_keys, args=(i, w)))
# Find threads
threads.append(threading.Thread(target=find_keys))
for t in threads:
t.start()
for t in threads:
t.join()
assert not errors, "Errors occurred: %s" % errors
def test_heartbeat_update_concurrent(self):
"""Test concurrent heartbeat updates."""
registry = RegistryTree()
instance_id = "test_instance"
num_workers = 10
# Register workers
for w in range(num_workers):
registry.register_worker(
instance_id=instance_id,
worker_id=w,
ip="127.0.0.1",
port=8000 + w,
peer_init_url=None,
socket=None,
registration_time=time.time(),
)
errors = []
num_threads = 20
updates_per_thread = 100
def update_heartbeats():
for _ in range(updates_per_thread):
for w in range(num_workers):
try:
result = registry.update_heartbeat(instance_id, w, time.time())
if not result:
errors.append("Heartbeat update failed for worker %d" % w)
except Exception as e:
errors.append("Heartbeat error: %s" % e)
threads = [
threading.Thread(target=update_heartbeats) for _ in range(num_threads)
]
for t in threads:
t.start()
for t in threads:
t.join()
assert not errors, "Errors occurred: %s" % errors
def test_get_all_worker_infos_concurrent(self):
"""Test get_all_worker_infos during concurrent modifications."""
registry = RegistryTree()
errors = []
def register_workers():
for i in range(100):
try:
registry.register_worker(
instance_id="instance_%d" % (i % 5),
worker_id=i,
ip="192.168.1.%d" % i,
port=8000 + i,
peer_init_url=None,
socket=None,
registration_time=time.time(),
)
except Exception as e:
errors.append("Register error: %s" % e)
def get_infos():
for _ in range(50):
try:
infos = registry.get_all_worker_infos_cached()
# Just verify it returns a list without error
assert isinstance(infos, list)
except Exception as e:
errors.append("Get infos error: %s" % e)
time.sleep(0.001)
threads = [
threading.Thread(target=register_workers),
threading.Thread(target=get_infos),
threading.Thread(target=get_infos),
]
for t in threads:
t.start()
for t in threads:
t.join()
assert not errors, "Errors occurred: %s" % errors
def test_instance_cleanup_on_last_worker_deregister(self):
"""Test that empty instances are cleaned up correctly."""
registry = RegistryTree()
instance_id = "test_instance"
num_workers = 10
# Register workers
for w in range(num_workers):
registry.register_worker(
instance_id=instance_id,
worker_id=w,
ip="127.0.0.1",
port=8000 + w,
peer_init_url=None,
socket=None,
registration_time=time.time(),
)
# Deregister all workers concurrently
errors = []
def deregister(worker_id):
try:
registry.deregister_worker(instance_id, worker_id)
except Exception as e:
errors.append("Deregister error: %s" % e)
threads = [
threading.Thread(target=deregister, args=(w,)) for w in range(num_workers)
]
for t in threads:
t.start()
for t in threads:
t.join()
assert not errors, "Errors occurred: %s" % errors
def test_high_contention_stress(self):
"""Stress test with high contention on a single instance."""
registry = RegistryTree()
instance_id = "stress_instance"
errors = []
num_threads = 30
operations_per_thread = 100
def stress_operations(thread_id):
nonlocal errors
for i in range(operations_per_thread):
worker_id = thread_id * operations_per_thread + i
try:
# Register
registry.register_worker(
instance_id=instance_id,
worker_id=worker_id,
ip="10.0.0.%d" % (thread_id % 256),
port=8000 + (i % 1000),
peer_init_url=None,
socket=None,
registration_time=time.time(),
)
# KV operations
msg = BatchedKVOperationMsg(
instance_id=instance_id,
worker_id=worker_id,
location="loc1",
operations=[
KVOpEvent(
op_type=OpType.ADMIT,
key=i,
seq_num=i,
)
],
)
registry.handle_batched_kv_operations(msg)
# Read operations
registry.get_worker(instance_id, worker_id)
# Deregister
registry.deregister_worker(instance_id, worker_id)
except Exception as e:
errors.append(
"Thread %d iteration %d error: %s" % (thread_id, i, e)
)
with ThreadPoolExecutor(max_workers=num_threads) as executor:
futures = [
executor.submit(stress_operations, i) for i in range(num_threads)
]
for f in futures:
f.result()
assert not errors, "Errors occurred (first 10): %s" % errors[:10]
def test_data_consistency_after_concurrent_ops(self):
"""Verify data consistency after heavy concurrent operations."""
registry = RegistryTree()
num_instances = 5
workers_per_instance = 10
kv_keys = 50
# Register all workers
for inst in range(num_instances):
for w in range(workers_per_instance):
registry.register_worker(
instance_id="inst_%d" % inst,
worker_id=w,
ip="10.%d.%d.1" % (inst, w),
port=8000,
peer_init_url=None,
socket=None,
registration_time=time.time(),
)
errors = []
def concurrent_operations(inst_idx):
nonlocal errors
instance_id = "inst_%d" % inst_idx
for w in range(workers_per_instance):
for key in range(kv_keys):
try:
msg = BatchedKVOperationMsg(
instance_id=instance_id,
worker_id=w,
location="loc",
operations=[
KVOpEvent(
op_type=OpType.ADMIT,
key=key,
seq_num=key,
)
],
)
registry.handle_batched_kv_operations(msg)
except Exception as e:
errors.append("Admit error: %s" % e)
threads = [
threading.Thread(target=concurrent_operations, args=(i,))
for i in range(num_instances)
]
for t in threads:
t.start()
for t in threads:
t.join()
assert not errors, "Errors during concurrent ops: %s" % errors
# Verify final state
total_kv = registry.get_total_kv_count()
expected_kv = num_instances * workers_per_instance * kv_keys
assert total_kv == expected_kv, "Expected %d KV entries, got %d" % (
expected_kv,
total_kv,
)
for inst in range(num_instances):
worker_ids = registry.get_worker_ids("inst_%d" % inst)
assert len(worker_ids) == workers_per_instance, (
"Instance %d: expected %d workers, got %d"
% (
inst,
workers_per_instance,
len(worker_ids),
)
)
class TestBatchOperations:
"""Test batch KV operations for performance optimization."""
def test_batch_admit_kv_basic(self):
"""Test basic batch admit functionality using handle_batched_kv_operations."""
worker = WorkerNode(
worker_id=0,
ip="127.0.0.1",
port=8000,
peer_init_url=None,
socket=None,
registration_time=time.time(),
last_heartbeat_time=time.time(),
)
location = "test_location"
keys = list(range(100))
# Create batch operation message
operations = [
KVOpEvent(op_type=OpType.ADMIT, key=key, seq_num=key) for key in keys
]
msg = BatchedKVOperationMsg(
instance_id="test_instance",
worker_id=worker.worker_id,
location=location,
operations=operations,
)
worker.handle_batched_kv_operations(msg)
assert worker.get_kv_count() == 100
for key in keys:
assert worker.has_kv(location, key)
def test_batch_evict_kv_basic(self):
"""Test basic batch evict functionality using handle_batched_kv_operations."""
worker = WorkerNode(
worker_id=0,
ip="127.0.0.1",
port=8000,
peer_init_url=None,
socket=None,
registration_time=time.time(),
last_heartbeat_time=time.time(),
)
location = "test_location"
keys = list(range(100))
# First admit all keys using batch operation - start with seq_num=0
admit_operations = [
KVOpEvent(op_type=OpType.ADMIT, key=key, seq_num=i)
for i, key in enumerate(keys)
]
admit_msg = BatchedKVOperationMsg(
instance_id="test_instance",
worker_id=worker.worker_id,
location=location,
operations=admit_operations,
)
worker.handle_batched_kv_operations(admit_msg)
assert worker.get_kv_count() == 100
# Evict half of them using batch operation - continue sequence
evict_keys = list(range(50))
evict_operations = [
KVOpEvent(op_type=OpType.EVICT, key=key, seq_num=100 + i)
for i, key in enumerate(evict_keys)
]
evict_msg = BatchedKVOperationMsg(
instance_id="test_instance",
worker_id=worker.worker_id,
location=location,
operations=evict_operations,
)
worker.handle_batched_kv_operations(evict_msg)
assert worker.get_kv_count() == 50
# Check remaining keys
for key in range(50, 100):
assert worker.has_kv(location, key)
def test_batch_evict_nonexistent_keys(self):
"""Test batch evict with some non-existent keys
using handle_batched_kv_operations."""
worker = WorkerNode(
worker_id=0,
ip="127.0.0.1",
port=8000,
peer_init_url=None,
socket=None,
registration_time=time.time(),
last_heartbeat_time=time.time(),
)
location = "test_location"
# Admit only keys 0-49 using batch operation - start with seq_num=0
admit_keys = list(range(50))
admit_operations = [
KVOpEvent(op_type=OpType.ADMIT, key=key, seq_num=i)
for i, key in enumerate(admit_keys)
]
admit_msg = BatchedKVOperationMsg(
instance_id="test_instance",
worker_id=worker.worker_id,
location=location,
operations=admit_operations,
)
worker.handle_batched_kv_operations(admit_msg)
assert worker.get_kv_count() == 50
# Try to evict keys 0-99 (half don't exist)
# using batch operation - continue sequence
evict_keys = list(range(100))
evict_operations = [
KVOpEvent(op_type=OpType.EVICT, key=key, seq_num=50 + i)
for i, key in enumerate(evict_keys)
]
evict_msg = BatchedKVOperationMsg(
instance_id="test_instance",
worker_id=worker.worker_id,
location=location,
operations=evict_operations,
)
worker.handle_batched_kv_operations(evict_msg)
# All admitted keys should be evicted
assert worker.get_kv_count() == 0
def test_registry_batch_operations(self):
"""Test batch operations through RegistryTree
using handle_batched_kv_operations."""
registry = RegistryTree()
registry.register_worker(
instance_id="inst_0",
worker_id=0,
ip="127.0.0.1",
port=8000,
peer_init_url=None,
socket=None,
registration_time=time.time(),
)
location = "test_location"
keys = list(range(1000))
# Batch admit using handle_batched_kv_operations - start with seq_num=0
admit_operations = [
KVOpEvent(op_type=OpType.ADMIT, key=key, seq_num=i)
for i, key in enumerate(keys)
]
admit_msg = BatchedKVOperationMsg(
instance_id="inst_0",
worker_id=0,
location=location,
operations=admit_operations,
)
result = registry.handle_batched_kv_operations(admit_msg)
assert result is not False # Should succeed
assert registry.get_total_kv_count() == 1000
# Batch evict using handle_batched_kv_operations - continue sequence
evict_keys = keys[:500]
evict_operations = [
KVOpEvent(op_type=OpType.EVICT, key=key, seq_num=1000 + i)
for i, key in enumerate(evict_keys)
]
evict_msg = BatchedKVOperationMsg(
instance_id="inst_0",
worker_id=0,
location=location,
operations=evict_operations,
)
registry.handle_batched_kv_operations(evict_msg)
assert registry.get_total_kv_count() == 500
def test_batch_operations_nonexistent_worker(self):
"""Test batch operations on non-existent worker
using handle_batched_kv_operations."""
registry = RegistryTree()
# Batch admit to non-existent worker using handle_batched_kv_operations
admit_operations = [
KVOpEvent(op_type=OpType.ADMIT, key=key, seq_num=key) for key in [1, 2, 3]
]
admit_msg = BatchedKVOperationMsg(
instance_id="inst_0",
worker_id=0,
location="loc",
operations=admit_operations,
)
result = registry.handle_batched_kv_operations(admit_msg)
# Should return None or indicate failure when worker doesn't exist
assert result is None or result is False
# Batch evict from non-existent worker using handle_batched_kv_operations
evict_operations = [
KVOpEvent(op_type=OpType.EVICT, key=key, seq_num=key) for key in [1, 2, 3]
]
evict_msg = BatchedKVOperationMsg(
instance_id="inst_0",
worker_id=0,
location="loc",
operations=evict_operations,
)
result = registry.handle_batched_kv_operations(evict_msg)
# Should return None or indicate failure when worker doesn't exist
assert result is None or result is False
def test_concurrent_batch_operations(self):
"""Test concurrent batch operations on same worker
using handle_batched_kv_operations."""
worker = WorkerNode(
worker_id=0,
ip="127.0.0.1",
port=8000,
peer_init_url=None,
socket=None,
registration_time=time.time(),
last_heartbeat_time=time.time(),
)
location = "test_location"
num_threads = 10
keys_per_batch = 100
errors = []
def batch_admit(thread_id):
keys = [thread_id * keys_per_batch + i for i in range(keys_per_batch)]
try:
operations = [
KVOpEvent(op_type=OpType.ADMIT, key=key, seq_num=key)
for key in keys
]
msg = BatchedKVOperationMsg(
instance_id="test_instance",
worker_id=worker.worker_id,
location=location,
operations=operations,
)
worker.handle_batched_kv_operations(msg)
except Exception as e:
errors.append("Thread %d error: %s" % (thread_id, e))
threads = [
threading.Thread(target=batch_admit, args=(i,)) for i in range(num_threads)
]
for t in threads:
t.start()
for t in threads:
t.join()
assert not errors, "Errors occurred: %s" % errors
assert worker.get_kv_count() == num_threads * keys_per_batch
def test_registry_batch_with_seq_check(self):
"""Test batch operations with sequence check through RegistryTree
using handle_batched_kv_operations."""
registry = RegistryTree()
registry.register_worker(
instance_id="inst_0",
worker_id=0,
ip="127.0.0.1",
port=8000,
peer_init_url=None,
socket=None,
registration_time=time.time(),
)
location = "test_location"
discontinuity_detected = False
def on_discontinuity():
nonlocal discontinuity_detected
discontinuity_detected = True
# First batch admit with seq check (continuous)
batch1_operations = [
KVOpEvent(op_type=OpType.ADMIT, key=key, seq_num=seq_num)
for key, seq_num in [(1, 0), (2, 1), (3, 2)]
]
batch1_msg = BatchedKVOperationMsg(
instance_id="inst_0",
worker_id=0,
location=location,
operations=batch1_operations,
)
result = registry.handle_batched_kv_operations(batch1_msg)
# Check discontinuity count instead
discontinuity_count = registry.get_seq_discontinuity_count()
assert result is not False and discontinuity_count == 0, (
"First batch should succeed and be continuous"
)
assert registry.get_total_kv_count() == 3
# Second batch admit (continuous)
batch2_operations = [
KVOpEvent(op_type=OpType.ADMIT, key=key, seq_num=seq_num)
for key, seq_num in [(4, 3), (5, 4), (6, 5)]
]
batch2_msg = BatchedKVOperationMsg(
instance_id="inst_0",
worker_id=0,
location=location,
operations=batch2_operations,
)
result = registry.handle_batched_kv_operations(batch2_msg)
discontinuity_count = registry.get_seq_discontinuity_count()
assert result is not False and discontinuity_count == 0, (
"Second batch should succeed and be continuous"
)
assert registry.get_total_kv_count() == 6
# Third batch with gap
batch3_operations = [
KVOpEvent(op_type=OpType.ADMIT, key=key, seq_num=seq_num)
for key, seq_num in [(10, 10), (11, 11)]
]
batch3_msg = BatchedKVOperationMsg(
instance_id="inst_0",
worker_id=0,
location=location,
operations=batch3_operations,
)
result = registry.handle_batched_kv_operations(batch3_msg)
discontinuity_count = registry.get_seq_discontinuity_count()
assert result is not False, "Batch with gap should still succeed"
assert discontinuity_count > 0, "Sequence discontinuity should be detected"
assert registry.get_total_kv_count() == 8
def test_registry_batch_evict_with_seq_check(self):
"""Test batch evict with sequence check through RegistryTree
using handle_batched_kv_operations."""
registry = RegistryTree()
registry.register_worker(
instance_id="inst_0",
worker_id=0,
ip="127.0.0.1",
port=8000,
peer_init_url=None,
socket=None,
registration_time=time.time(),
)
location = "test_location"
# First admit some keys (continuous sequence starting from 0)
admit_operations = [
KVOpEvent(op_type=OpType.ADMIT, key=key, seq_num=i)
for i, key in enumerate(range(100))
]
admit_msg = BatchedKVOperationMsg(
instance_id="inst_0",
worker_id=0,
location=location,
operations=admit_operations,
)
result = registry.handle_batched_kv_operations(admit_msg)
discontinuity_count = registry.get_seq_discontinuity_count()
assert result is not False and discontinuity_count == 0, (
"Admit should succeed and be continuous"
)
assert registry.get_total_kv_count() == 100
# Batch evict with seq check (continuous) - continue sequence from 100
evict_operations = [
KVOpEvent(op_type=OpType.EVICT, key=key, seq_num=100 + i)
for i, key in enumerate(range(50))
]
evict_msg = BatchedKVOperationMsg(
instance_id="inst_0",
worker_id=0,
location=location,
operations=evict_operations,
)
result = registry.handle_batched_kv_operations(evict_msg)
discontinuity_count_after_evict = registry.get_seq_discontinuity_count()
assert discontinuity_count_after_evict == 0, (
"Evict should succeed and be continuous"
)
assert result is not False, "Evict should succeed"
# Note: Evict operations don't check sequence continuity,
# so count shouldn't increase
assert registry.get_total_kv_count() == 50