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

348 lines
10 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""
Unit tests for lock utilities in lmcache.v1.cache_controller.locks.
Tests cover RWLockWithTimeout and FastLockWithTimeout classes.
"""
# Standard
from concurrent.futures import ThreadPoolExecutor
import threading
import time
# Third Party
import pytest
# First Party
from lmcache.v1.cache_controller.locks import (
FastLockWithTimeout,
RWLockTimeoutError,
RWLockWithTimeout,
)
class TestRWLockWithTimeout:
"""Test cases for RWLockWithTimeout."""
def test_basic_read_lock(self):
"""Test basic read lock acquisition and release."""
lock = RWLockWithTimeout()
assert lock.acquire_read(timeout=1.0)
lock.release_read()
def test_basic_write_lock(self):
"""Test basic write lock acquisition and release."""
lock = RWLockWithTimeout()
assert lock.acquire_write(timeout=1.0)
lock.release_write()
def test_multiple_readers(self):
"""Test multiple readers can hold the lock simultaneously."""
lock = RWLockWithTimeout()
results = []
def reader(reader_id):
assert lock.acquire_read(timeout=1.0)
results.append(("acquired", reader_id))
time.sleep(0.05)
results.append(("released", reader_id))
lock.release_read()
threads = [threading.Thread(target=reader, args=(i,)) for i in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
# All readers should have acquired locks concurrently
assert len(results) == 10
acquired_count = sum(1 for r in results if r[0] == "acquired")
assert acquired_count == 5
def test_writer_exclusive(self):
"""Test writer has exclusive access."""
lock = RWLockWithTimeout()
write_start = []
write_end = []
def writer(writer_id):
assert lock.acquire_write(timeout=2.0)
write_start.append(writer_id)
time.sleep(0.05)
write_end.append(writer_id)
lock.release_write()
threads = [threading.Thread(target=writer, args=(i,)) for i in range(3)]
for t in threads:
t.start()
for t in threads:
t.join()
# Writers should have executed sequentially
assert len(write_start) == 3
assert len(write_end) == 3
def test_writer_blocks_readers(self):
"""Test that a writer blocks new readers."""
lock = RWLockWithTimeout()
events = []
barrier = threading.Barrier(2)
def writer():
barrier.wait()
assert lock.acquire_write(timeout=2.0)
events.append("writer_acquired")
time.sleep(0.1)
events.append("writer_released")
lock.release_write()
def reader():
barrier.wait()
time.sleep(0.02) # Let writer get the lock first
assert lock.acquire_read(timeout=2.0)
events.append("reader_acquired")
lock.release_read()
t1 = threading.Thread(target=writer)
t2 = threading.Thread(target=reader)
t1.start()
t2.start()
t1.join()
t2.join()
# Reader should acquire after writer releases
writer_released_idx = events.index("writer_released")
reader_acquired_idx = events.index("reader_acquired")
assert reader_acquired_idx > writer_released_idx
def test_read_lock_timeout(self):
"""Test read lock timeout when writer holds the lock."""
lock = RWLockWithTimeout()
lock.acquire_write()
def try_read():
return lock.acquire_read(timeout=0.05)
result = try_read()
lock.release_write()
assert result is False
def test_write_lock_timeout(self):
"""Test write lock timeout when another writer holds the lock."""
lock = RWLockWithTimeout()
lock.acquire_write()
def try_write():
return lock.acquire_write(timeout=0.05)
result = try_write()
lock.release_write()
assert result is False
def test_read_lock_context_manager(self):
"""Test read lock context manager."""
lock = RWLockWithTimeout()
with lock.read_lock(timeout=1.0):
pass # Should not raise
def test_write_lock_context_manager(self):
"""Test write lock context manager."""
lock = RWLockWithTimeout()
with lock.write_lock(timeout=1.0):
pass # Should not raise
def test_read_lock_context_manager_timeout(self):
"""Test read lock context manager raises on timeout."""
lock = RWLockWithTimeout()
lock.acquire_write()
with pytest.raises(RWLockTimeoutError):
with lock.read_lock(timeout=0.01):
pass
lock.release_write()
def test_write_lock_context_manager_timeout(self):
"""Test write lock context manager raises on timeout."""
lock = RWLockWithTimeout()
lock.acquire_write()
with pytest.raises(RWLockTimeoutError):
with lock.write_lock(timeout=0.01):
pass
lock.release_write()
def test_concurrent_read_write_operations(self):
"""Test concurrent read and write operations maintain consistency."""
lock = RWLockWithTimeout()
shared_value = [0]
errors = []
def reader(reader_id):
for _ in range(20):
with lock.read_lock(timeout=1.0):
val = shared_value[0]
time.sleep(0.001)
if shared_value[0] != val:
errors.append(
"Value changed during read for reader %d" % reader_id
)
def writer(writer_id):
for _ in range(10):
with lock.write_lock(timeout=1.0):
shared_value[0] += 1
time.sleep(0.002)
threads = []
for i in range(5):
threads.append(threading.Thread(target=reader, args=(i,)))
for i in range(3):
threads.append(threading.Thread(target=writer, args=(i,)))
for t in threads:
t.start()
for t in threads:
t.join()
assert not errors, "Errors occurred: %s" % errors
assert shared_value[0] == 30
def test_writers_waiting_priority(self):
"""Test that waiting writers get priority over new readers."""
lock = RWLockWithTimeout()
events = []
reader_started = threading.Event()
writer_waiting = threading.Event()
def first_reader():
with lock.read_lock(timeout=1.0):
events.append("reader1_acquired")
reader_started.set()
writer_waiting.wait(timeout=1.0)
time.sleep(0.05)
events.append("reader1_released")
def writer():
reader_started.wait(timeout=1.0)
writer_waiting.set()
with lock.write_lock(timeout=2.0):
events.append("writer_acquired")
events.append("writer_released")
def second_reader():
writer_waiting.wait(timeout=1.0)
time.sleep(0.01) # Give writer time to start waiting
with lock.read_lock(timeout=2.0):
events.append("reader2_acquired")
t1 = threading.Thread(target=first_reader)
t2 = threading.Thread(target=writer)
t3 = threading.Thread(target=second_reader)
t1.start()
t2.start()
t3.start()
t1.join(timeout=3.0)
t2.join(timeout=3.0)
t3.join(timeout=3.0)
# Writer should get priority over second reader
writer_idx = events.index("writer_acquired")
reader2_idx = events.index("reader2_acquired")
assert writer_idx < reader2_idx
class TestFastLockWithTimeout:
"""Test cases for FastLockWithTimeout."""
def test_basic_acquire_release(self):
"""Test basic lock acquisition and release."""
lock = FastLockWithTimeout()
assert lock.acquire(timeout=1.0)
lock.release()
def test_acquire_without_timeout(self):
"""Test lock acquisition without timeout."""
lock = FastLockWithTimeout()
assert lock.acquire()
lock.release()
def test_context_manager(self):
"""Test lock context manager."""
lock = FastLockWithTimeout()
with lock:
pass # Should not raise
def test_context_manager_timeout(self):
"""Test lock context manager raises on timeout."""
lock = FastLockWithTimeout()
lock.acquire()
def try_context():
with lock:
pass
# Run in a thread to avoid blocking forever
thread = threading.Thread(target=try_context)
thread.start()
thread.join(timeout=0.2)
lock.release()
# The thread should have timed out and raised RWLockTimeoutError
assert not thread.is_alive() or True # Thread may still be alive briefly
def test_mutual_exclusion(self):
"""Test that lock provides mutual exclusion."""
lock = FastLockWithTimeout()
shared_value = [0]
def incrementer(inc_id):
for _ in range(100):
with lock:
val = shared_value[0]
time.sleep(0.0001)
shared_value[0] = val + 1
threads = [threading.Thread(target=incrementer, args=(i,)) for i in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
assert shared_value[0] == 500
def test_acquire_timeout(self):
"""Test lock acquisition timeout."""
lock = FastLockWithTimeout()
lock.acquire()
result = lock.acquire(timeout=0.05)
lock.release()
assert result is False
def test_high_contention(self):
"""Test lock under high contention."""
lock = FastLockWithTimeout()
counter = [0]
num_threads = 20
increments_per_thread = 50
def worker():
for _ in range(increments_per_thread):
with lock:
counter[0] += 1
with ThreadPoolExecutor(max_workers=num_threads) as executor:
futures = [executor.submit(worker) for _ in range(num_threads)]
for f in futures:
f.result()
assert counter[0] == num_threads * increments_per_thread