725 lines
22 KiB
Python
725 lines
22 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""
|
|
Unit tests for StoreController.
|
|
|
|
Tests verify the end-to-end flow: L1 write -> listener notification ->
|
|
L2 store submission -> completion handling -> L1 read lock release.
|
|
|
|
Uses a real L1Manager and MockL2Adapter (with debug methods) to exercise
|
|
the full integration without mocking internals.
|
|
"""
|
|
|
|
# Standard
|
|
import select
|
|
import time
|
|
|
|
# Third Party
|
|
import pytest
|
|
import torch
|
|
|
|
# First Party
|
|
from lmcache.v1.distributed.api import MemoryLayoutDesc, ObjectKey
|
|
from lmcache.v1.distributed.config import L1ManagerConfig, L1MemoryManagerConfig
|
|
from lmcache.v1.distributed.eviction_policy.noop import (
|
|
NoOpEvictionPolicy,
|
|
)
|
|
from lmcache.v1.distributed.l1_manager import L1Manager
|
|
from lmcache.v1.distributed.l2_adapters.mock_l2_adapter import (
|
|
MockL2Adapter,
|
|
MockL2AdapterConfig,
|
|
)
|
|
from lmcache.v1.distributed.storage_controllers.store_controller import (
|
|
StoreController,
|
|
StoreListener,
|
|
)
|
|
from lmcache.v1.distributed.storage_controllers.store_policy import (
|
|
AdapterDescriptor,
|
|
BufferOnlyStorePolicy,
|
|
DefaultStorePolicy,
|
|
StorePolicy,
|
|
)
|
|
|
|
# Skip all tests in this module if CUDA is not available
|
|
pytestmark = pytest.mark.skipif(
|
|
not torch.cuda.is_available(), reason="CUDA is not available"
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Helpers
|
|
# =============================================================================
|
|
|
|
|
|
def make_object_key(chunk_id: int) -> ObjectKey:
|
|
"""Create a test ObjectKey with the given chunk ID."""
|
|
return ObjectKey(
|
|
chunk_hash=ObjectKey.IntHash2Bytes(chunk_id),
|
|
model_name="test_model",
|
|
kv_rank=0,
|
|
)
|
|
|
|
|
|
def make_layout() -> MemoryLayoutDesc:
|
|
"""Create a small MemoryLayoutDesc for testing."""
|
|
return MemoryLayoutDesc(
|
|
shapes=[torch.Size([100, 2, 512])],
|
|
dtypes=[torch.bfloat16],
|
|
)
|
|
|
|
|
|
def should_use_lazy_alloc() -> bool:
|
|
"""Determine if lazy allocation should be used based on CUDA availability."""
|
|
return torch.cuda.is_available()
|
|
|
|
|
|
def wait_for_condition(
|
|
predicate,
|
|
timeout: float = 5.0,
|
|
poll_interval: float = 0.05,
|
|
) -> bool:
|
|
"""
|
|
Poll until a predicate returns True or timeout.
|
|
|
|
Args:
|
|
predicate: Callable returning bool.
|
|
timeout: Max wait time in seconds.
|
|
poll_interval: Time between polls in seconds.
|
|
|
|
Returns:
|
|
True if predicate was satisfied, False on timeout.
|
|
"""
|
|
deadline = time.monotonic() + timeout
|
|
while time.monotonic() < deadline:
|
|
if predicate():
|
|
return True
|
|
time.sleep(poll_interval)
|
|
return False
|
|
|
|
|
|
def write_keys_to_l1(
|
|
l1_manager: L1Manager,
|
|
keys: list[ObjectKey],
|
|
layout: MemoryLayoutDesc,
|
|
) -> list[ObjectKey]:
|
|
"""
|
|
Write keys to L1 (reserve_write + finish_write).
|
|
|
|
Args:
|
|
l1_manager: The L1 manager to write to.
|
|
keys: Object keys to write.
|
|
layout: Memory layout description.
|
|
|
|
Returns:
|
|
List of keys that were successfully written.
|
|
"""
|
|
results = l1_manager.reserve_write(
|
|
keys=keys,
|
|
is_temporary=[False] * len(keys),
|
|
layout_desc=layout,
|
|
mode="new",
|
|
)
|
|
written = [k for k, (e, m) in results.items() if m is not None]
|
|
if written:
|
|
l1_manager.finish_write(written)
|
|
return written
|
|
|
|
|
|
# =============================================================================
|
|
# Fixtures
|
|
# =============================================================================
|
|
|
|
|
|
@pytest.fixture
|
|
def l1_manager():
|
|
"""Create an L1Manager with a reasonable memory config."""
|
|
config = L1ManagerConfig(
|
|
memory_config=L1MemoryManagerConfig(
|
|
size_in_bytes=128 * 1024 * 1024,
|
|
use_lazy=should_use_lazy_alloc(),
|
|
init_size_in_bytes=64 * 1024 * 1024,
|
|
align_bytes=0x1000,
|
|
),
|
|
write_ttl_seconds=600,
|
|
read_ttl_seconds=300,
|
|
)
|
|
mgr = L1Manager(config)
|
|
yield mgr
|
|
mgr.close()
|
|
|
|
|
|
def make_adapter() -> MockL2Adapter:
|
|
"""Create a MockL2Adapter with fast bandwidth."""
|
|
config = MockL2AdapterConfig(
|
|
max_size_gb=0.01,
|
|
mock_bandwidth_gb=10.0,
|
|
)
|
|
return MockL2Adapter(config)
|
|
|
|
|
|
def make_descriptor(index: int) -> AdapterDescriptor:
|
|
"""Create an AdapterDescriptor for testing."""
|
|
config = MockL2AdapterConfig(max_size_gb=0.01, mock_bandwidth_gb=10.0)
|
|
return AdapterDescriptor(index=index, config=config)
|
|
|
|
|
|
# =============================================================================
|
|
# StoreListener Tests
|
|
# =============================================================================
|
|
|
|
|
|
class TestStoreListener:
|
|
"""Test StoreListener as a standalone component."""
|
|
|
|
def test_pop_pending_keys_returns_empty_initially(self):
|
|
"""A fresh listener should have no pending keys."""
|
|
listener = StoreListener()
|
|
assert listener.pop_pending_keys() == []
|
|
listener.close()
|
|
|
|
def test_on_l1_keys_write_finished_enqueues_keys(self):
|
|
"""Keys passed to on_l1_keys_write_finished should be retrievable via pop."""
|
|
listener = StoreListener()
|
|
keys = [make_object_key(i) for i in range(3)]
|
|
|
|
listener.on_l1_keys_write_finished(keys)
|
|
popped = listener.pop_pending_keys()
|
|
|
|
assert popped == keys
|
|
listener.close()
|
|
|
|
def test_pop_pending_keys_clears_queue(self):
|
|
"""pop_pending_keys should drain the queue."""
|
|
listener = StoreListener()
|
|
listener.on_l1_keys_write_finished([make_object_key(0)])
|
|
listener.pop_pending_keys()
|
|
|
|
assert listener.pop_pending_keys() == []
|
|
listener.close()
|
|
|
|
def test_on_l1_keys_write_finished_signals_eventfd(self):
|
|
"""The eventfd should become readable after on_l1_keys_write_finished."""
|
|
listener = StoreListener()
|
|
efd = listener.get_event_fd()
|
|
poller = select.poll()
|
|
poller.register(efd, select.POLLIN)
|
|
|
|
listener.on_l1_keys_write_finished([make_object_key(0)])
|
|
|
|
events = poller.poll(1000)
|
|
assert len(events) > 0
|
|
listener.close()
|
|
|
|
def test_multiple_writes_accumulate(self):
|
|
"""Multiple on_l1_keys_write_finished calls should accumulate keys."""
|
|
listener = StoreListener()
|
|
listener.on_l1_keys_write_finished([make_object_key(0)])
|
|
listener.on_l1_keys_write_finished([make_object_key(1), make_object_key(2)])
|
|
|
|
popped = listener.pop_pending_keys()
|
|
assert len(popped) == 3
|
|
listener.close()
|
|
|
|
def test_finish_write_and_reserve_read_does_not_enqueue(self):
|
|
"""on_l1_keys_finish_write_and_reserve_read should not enqueue keys."""
|
|
listener = StoreListener()
|
|
keys = [make_object_key(i) for i in range(3)]
|
|
|
|
listener.on_l1_keys_finish_write_and_reserve_read(keys)
|
|
|
|
assert listener.pop_pending_keys() == []
|
|
assert listener.pending_count() == 0
|
|
listener.close()
|
|
|
|
|
|
# =============================================================================
|
|
# StoreController Integration Tests
|
|
# =============================================================================
|
|
|
|
|
|
class TestStoreControllerLifecycle:
|
|
"""Test StoreController start/stop behavior."""
|
|
|
|
def test_start_stop(self, l1_manager):
|
|
"""StoreController should start and stop cleanly."""
|
|
adapter = make_adapter()
|
|
ctrl = StoreController(
|
|
l1_manager=l1_manager,
|
|
l2_adapters=[adapter],
|
|
adapter_descriptors=[make_descriptor(0)],
|
|
policy=DefaultStorePolicy(),
|
|
)
|
|
ctrl.start()
|
|
ctrl.stop()
|
|
adapter.close()
|
|
|
|
def test_start_stop_no_adapters(self, l1_manager):
|
|
"""Controller should start and stop cleanly with no adapters."""
|
|
ctrl = StoreController(
|
|
l1_manager=l1_manager,
|
|
l2_adapters=[],
|
|
adapter_descriptors=[],
|
|
policy=DefaultStorePolicy(),
|
|
)
|
|
ctrl.start()
|
|
ctrl.stop()
|
|
|
|
|
|
class TestStoreControllerSingleAdapter:
|
|
"""Test StoreController with one MockL2Adapter."""
|
|
|
|
def test_l1_write_triggers_l2_store(self, l1_manager):
|
|
"""Writing to L1 should cause the object to appear in L2."""
|
|
adapter = make_adapter()
|
|
ctrl = StoreController(
|
|
l1_manager=l1_manager,
|
|
l2_adapters=[adapter],
|
|
adapter_descriptors=[make_descriptor(0)],
|
|
policy=DefaultStorePolicy(),
|
|
)
|
|
ctrl.start()
|
|
|
|
layout = make_layout()
|
|
keys = [make_object_key(0)]
|
|
write_keys_to_l1(l1_manager, keys, layout)
|
|
|
|
# Wait for the object to appear in L2
|
|
ok = wait_for_condition(
|
|
lambda: adapter.debug_get_stored_object_count() == 1,
|
|
timeout=5.0,
|
|
)
|
|
assert ok, "Object should be stored in L2 after L1 write"
|
|
assert adapter.debug_has_key(keys[0])
|
|
|
|
ctrl.stop()
|
|
adapter.close()
|
|
|
|
def test_multiple_keys_stored_to_l2(self, l1_manager):
|
|
"""Multiple L1 writes should result in all keys in L2."""
|
|
adapter = make_adapter()
|
|
ctrl = StoreController(
|
|
l1_manager=l1_manager,
|
|
l2_adapters=[adapter],
|
|
adapter_descriptors=[make_descriptor(0)],
|
|
policy=DefaultStorePolicy(),
|
|
)
|
|
ctrl.start()
|
|
|
|
layout = make_layout()
|
|
keys = [make_object_key(i) for i in range(5)]
|
|
write_keys_to_l1(l1_manager, keys, layout)
|
|
|
|
ok = wait_for_condition(
|
|
lambda: adapter.debug_get_stored_object_count() == 5,
|
|
timeout=5.0,
|
|
)
|
|
assert ok, "All 5 objects should be stored in L2"
|
|
for key in keys:
|
|
assert adapter.debug_has_key(key)
|
|
|
|
ctrl.stop()
|
|
adapter.close()
|
|
|
|
def test_read_lock_released_after_store(self, l1_manager):
|
|
"""After L2 store completes, L1 read locks should be released."""
|
|
adapter = make_adapter()
|
|
ctrl = StoreController(
|
|
l1_manager=l1_manager,
|
|
l2_adapters=[adapter],
|
|
adapter_descriptors=[make_descriptor(0)],
|
|
policy=DefaultStorePolicy(),
|
|
)
|
|
ctrl.start()
|
|
|
|
layout = make_layout()
|
|
keys = [make_object_key(0)]
|
|
write_keys_to_l1(l1_manager, keys, layout)
|
|
|
|
# Wait for L2 store to complete
|
|
ok = wait_for_condition(
|
|
lambda: adapter.debug_has_key(keys[0]),
|
|
timeout=5.0,
|
|
)
|
|
assert ok
|
|
|
|
# Verify read lock is released: the key should be updatable
|
|
ok = wait_for_condition(
|
|
lambda: l1_manager.reserve_write(
|
|
keys=keys,
|
|
is_temporary=[False],
|
|
layout_desc=layout,
|
|
mode="update",
|
|
)[keys[0]][1]
|
|
is not None,
|
|
timeout=5.0,
|
|
)
|
|
assert ok, "Key should be updatable after store controller releases read lock"
|
|
|
|
ctrl.stop()
|
|
adapter.close()
|
|
|
|
def test_multiple_concurrent_requests_same_adapter(self, l1_manager):
|
|
"""Each L1-write batch produces an independent in-flight store request.
|
|
|
|
When two batches target the same adapter, the adapter's single
|
|
eventfd may batch-signal both completions. All in-flight requests
|
|
should be advanced and their L1 read locks released.
|
|
"""
|
|
adapter = make_adapter()
|
|
ctrl = StoreController(
|
|
l1_manager=l1_manager,
|
|
l2_adapters=[adapter],
|
|
adapter_descriptors=[make_descriptor(0)],
|
|
policy=DefaultStorePolicy(),
|
|
)
|
|
ctrl.start()
|
|
|
|
layout = make_layout()
|
|
batch_a = [make_object_key(i) for i in range(3)]
|
|
batch_b = [make_object_key(i) for i in range(3, 6)]
|
|
|
|
write_keys_to_l1(l1_manager, batch_a, layout)
|
|
write_keys_to_l1(l1_manager, batch_b, layout)
|
|
|
|
all_keys = batch_a + batch_b
|
|
ok = wait_for_condition(
|
|
lambda: all(adapter.debug_has_key(k) for k in all_keys),
|
|
timeout=5.0,
|
|
)
|
|
assert ok, "Both batches should reach L2"
|
|
|
|
# Every key must be updatable: read locks from both requests released.
|
|
# ``reserve_write`` acquires a write lock on success, so release it
|
|
# via ``finish_write`` after each successful check — otherwise the
|
|
# second poll would find already-locked keys and time out.
|
|
def all_keys_updatable() -> bool:
|
|
for k in all_keys:
|
|
result = l1_manager.reserve_write(
|
|
keys=[k],
|
|
is_temporary=[False],
|
|
layout_desc=layout,
|
|
mode="update",
|
|
)
|
|
if result[k][1] is None:
|
|
return False
|
|
l1_manager.finish_write([k])
|
|
return True
|
|
|
|
ok = wait_for_condition(all_keys_updatable, timeout=5.0)
|
|
assert ok, "Read locks must be released for every concurrent request"
|
|
|
|
ctrl.stop()
|
|
adapter.close()
|
|
|
|
|
|
class TestStoreControllerMultipleAdapters:
|
|
"""Test StoreController with multiple L2 adapters."""
|
|
|
|
def test_stores_to_all_adapters(self, l1_manager):
|
|
"""DefaultStorePolicy sends keys to all adapters."""
|
|
adapters = [make_adapter(), make_adapter()]
|
|
descriptors = [make_descriptor(i) for i in range(2)]
|
|
|
|
ctrl = StoreController(
|
|
l1_manager=l1_manager,
|
|
l2_adapters=adapters,
|
|
adapter_descriptors=descriptors,
|
|
policy=DefaultStorePolicy(),
|
|
)
|
|
ctrl.start()
|
|
|
|
layout = make_layout()
|
|
keys = [make_object_key(0)]
|
|
write_keys_to_l1(l1_manager, keys, layout)
|
|
|
|
# Wait for both adapters to have the object
|
|
ok = wait_for_condition(
|
|
lambda: all(a.debug_has_key(keys[0]) for a in adapters),
|
|
timeout=5.0,
|
|
)
|
|
assert ok, "Object should be stored in both L2 adapters"
|
|
|
|
ctrl.stop()
|
|
for a in adapters:
|
|
a.close()
|
|
|
|
def test_read_lock_released_after_all_adapters_complete(self, l1_manager):
|
|
"""Read locks should be released after all adapter stores complete."""
|
|
adapters = [make_adapter(), make_adapter()]
|
|
descriptors = [make_descriptor(i) for i in range(2)]
|
|
|
|
ctrl = StoreController(
|
|
l1_manager=l1_manager,
|
|
l2_adapters=adapters,
|
|
adapter_descriptors=descriptors,
|
|
policy=DefaultStorePolicy(),
|
|
)
|
|
ctrl.start()
|
|
|
|
layout = make_layout()
|
|
keys = [make_object_key(0)]
|
|
write_keys_to_l1(l1_manager, keys, layout)
|
|
|
|
# Wait for stores to both adapters, then check updatability
|
|
ok = wait_for_condition(
|
|
lambda: all(a.debug_has_key(keys[0]) for a in adapters),
|
|
timeout=5.0,
|
|
)
|
|
assert ok
|
|
|
|
ok = wait_for_condition(
|
|
lambda: l1_manager.reserve_write(
|
|
keys=keys,
|
|
is_temporary=[False],
|
|
layout_desc=layout,
|
|
mode="update",
|
|
)[keys[0]][1]
|
|
is not None,
|
|
timeout=5.0,
|
|
)
|
|
assert ok, "Key should be updatable after all adapter stores complete"
|
|
|
|
ctrl.stop()
|
|
for a in adapters:
|
|
a.close()
|
|
|
|
|
|
class TestStoreControllerNoAdapters:
|
|
"""Test StoreController with no L2 adapters."""
|
|
|
|
def test_write_with_no_adapters_no_read_lock(self, l1_manager):
|
|
"""With no adapters, no read locks should be held after L1 write."""
|
|
ctrl = StoreController(
|
|
l1_manager=l1_manager,
|
|
l2_adapters=[],
|
|
adapter_descriptors=[],
|
|
policy=DefaultStorePolicy(),
|
|
)
|
|
ctrl.start()
|
|
|
|
layout = make_layout()
|
|
keys = [make_object_key(0)]
|
|
write_keys_to_l1(l1_manager, keys, layout)
|
|
|
|
# Give the controller time to process the listener event
|
|
time.sleep(0.3)
|
|
|
|
# Key should be updatable (no read locks)
|
|
result = l1_manager.reserve_write(
|
|
keys=keys,
|
|
is_temporary=[False],
|
|
layout_desc=layout,
|
|
mode="update",
|
|
)
|
|
assert result[keys[0]][1] is not None
|
|
|
|
ctrl.stop()
|
|
|
|
|
|
class TestStoreControllerCustomPolicy:
|
|
"""Test StoreController with custom StorePolicy implementations."""
|
|
|
|
def test_policy_that_skips_all_adapters(self, l1_manager):
|
|
"""A policy returning empty targets should not trigger L2 stores."""
|
|
|
|
class SkipAllPolicy(StorePolicy):
|
|
"""Never store to L2."""
|
|
|
|
def select_store_targets(self, keys, adapters):
|
|
return {}
|
|
|
|
def select_l1_deletions(self, keys):
|
|
return []
|
|
|
|
adapter = make_adapter()
|
|
ctrl = StoreController(
|
|
l1_manager=l1_manager,
|
|
l2_adapters=[adapter],
|
|
adapter_descriptors=[make_descriptor(0)],
|
|
policy=SkipAllPolicy(),
|
|
)
|
|
ctrl.start()
|
|
|
|
layout = make_layout()
|
|
keys = [make_object_key(0)]
|
|
write_keys_to_l1(l1_manager, keys, layout)
|
|
|
|
# Give the controller time to process
|
|
time.sleep(0.5)
|
|
|
|
assert adapter.debug_get_stored_object_count() == 0, (
|
|
"No objects should be stored when policy skips all adapters"
|
|
)
|
|
|
|
ctrl.stop()
|
|
adapter.close()
|
|
|
|
def test_policy_that_deletes_from_l1(self, l1_manager):
|
|
"""A policy that requests L1 deletion should result in deleted keys."""
|
|
|
|
class DeleteAfterStorePolicy(StorePolicy):
|
|
"""Delete all keys from L1 after storing to L2."""
|
|
|
|
def select_store_targets(self, keys, adapters):
|
|
return {ad.index: list(keys) for ad in adapters}
|
|
|
|
def select_l1_deletions(self, keys):
|
|
return list(keys)
|
|
|
|
adapter = make_adapter()
|
|
ctrl = StoreController(
|
|
l1_manager=l1_manager,
|
|
l2_adapters=[adapter],
|
|
adapter_descriptors=[make_descriptor(0)],
|
|
policy=DeleteAfterStorePolicy(),
|
|
)
|
|
ctrl.start()
|
|
|
|
layout = make_layout()
|
|
keys = [make_object_key(0)]
|
|
write_keys_to_l1(l1_manager, keys, layout)
|
|
|
|
# Wait for L2 store and L1 deletion
|
|
ok = wait_for_condition(
|
|
lambda: adapter.debug_has_key(keys[0]),
|
|
timeout=5.0,
|
|
)
|
|
assert ok, "Object should be stored in L2"
|
|
|
|
# After deletion, reserve_write with mode="new" should succeed
|
|
ok = wait_for_condition(
|
|
lambda: l1_manager.reserve_write(
|
|
keys=keys,
|
|
is_temporary=[False],
|
|
layout_desc=layout,
|
|
mode="new",
|
|
)[keys[0]][1]
|
|
is not None,
|
|
timeout=5.0,
|
|
)
|
|
assert ok, "Key should be re-creatable after L1 deletion by policy"
|
|
|
|
ctrl.stop()
|
|
adapter.close()
|
|
|
|
def test_policy_selects_subset_of_adapters(self, l1_manager):
|
|
"""A policy that targets only some adapters should skip others."""
|
|
|
|
class FirstAdapterOnlyPolicy(StorePolicy):
|
|
"""Only store to adapter index 0."""
|
|
|
|
def select_store_targets(self, keys, adapters):
|
|
for ad in adapters:
|
|
if ad.index == 0:
|
|
return {0: list(keys)}
|
|
return {}
|
|
|
|
def select_l1_deletions(self, keys):
|
|
return []
|
|
|
|
adapters = [make_adapter(), make_adapter()]
|
|
descriptors = [make_descriptor(i) for i in range(2)]
|
|
|
|
ctrl = StoreController(
|
|
l1_manager=l1_manager,
|
|
l2_adapters=adapters,
|
|
adapter_descriptors=descriptors,
|
|
policy=FirstAdapterOnlyPolicy(),
|
|
)
|
|
ctrl.start()
|
|
|
|
layout = make_layout()
|
|
keys = [make_object_key(0)]
|
|
write_keys_to_l1(l1_manager, keys, layout)
|
|
|
|
ok = wait_for_condition(
|
|
lambda: adapters[0].debug_has_key(keys[0]),
|
|
timeout=5.0,
|
|
)
|
|
assert ok, "Object should be stored in first adapter"
|
|
|
|
# Give some time and verify second adapter is empty
|
|
time.sleep(0.5)
|
|
assert adapters[1].debug_get_stored_object_count() == 0, (
|
|
"Second adapter should have no objects"
|
|
)
|
|
|
|
ctrl.stop()
|
|
for a in adapters:
|
|
a.close()
|
|
|
|
|
|
class TestBufferOnlyMode:
|
|
"""Test buffer-only mode: BufferOnlyStorePolicy + NoOpEvictionPolicy."""
|
|
|
|
def test_l1_cleaned_after_l2_store(self, l1_manager):
|
|
"""L1 data should be deleted after successful L2 store."""
|
|
adapter = make_adapter()
|
|
|
|
ctrl = StoreController(
|
|
l1_manager=l1_manager,
|
|
l2_adapters=[adapter],
|
|
adapter_descriptors=[make_descriptor(0)],
|
|
policy=BufferOnlyStorePolicy(),
|
|
)
|
|
ctrl.start()
|
|
|
|
layout = make_layout()
|
|
keys = [make_object_key(0)]
|
|
write_keys_to_l1(l1_manager, keys, layout)
|
|
|
|
# Wait for L2 store
|
|
ok = wait_for_condition(
|
|
lambda: adapter.debug_has_key(keys[0]),
|
|
timeout=5.0,
|
|
)
|
|
assert ok, "Object should be stored in L2"
|
|
|
|
# L1 should be cleaned: check key is gone
|
|
ok = wait_for_condition(
|
|
lambda: l1_manager.get_object_state(keys[0]) is None,
|
|
timeout=5.0,
|
|
)
|
|
assert ok, "Key should be deleted from L1 in buffer-only mode"
|
|
|
|
ctrl.stop()
|
|
adapter.close()
|
|
|
|
def test_noop_eviction_never_evicts(self):
|
|
"""NoOpEvictionPolicy should never return eviction actions."""
|
|
noop = NoOpEvictionPolicy()
|
|
noop.on_keys_created([make_object_key(i) for i in range(10)])
|
|
assert noop.get_eviction_actions(1.0) == []
|
|
|
|
def test_buffer_only_multiple_keys(self, l1_manager):
|
|
"""Multiple keys should all flow to L2 and be removed
|
|
from L1."""
|
|
adapter = make_adapter()
|
|
|
|
ctrl = StoreController(
|
|
l1_manager=l1_manager,
|
|
l2_adapters=[adapter],
|
|
adapter_descriptors=[make_descriptor(0)],
|
|
policy=BufferOnlyStorePolicy(),
|
|
)
|
|
ctrl.start()
|
|
|
|
layout = make_layout()
|
|
keys = [make_object_key(i) for i in range(5)]
|
|
write_keys_to_l1(l1_manager, keys, layout)
|
|
|
|
ok = wait_for_condition(
|
|
lambda: adapter.debug_get_stored_object_count() == 5,
|
|
timeout=5.0,
|
|
)
|
|
assert ok, "All 5 objects should be stored in L2"
|
|
|
|
# All keys should be gone from L1
|
|
ok = wait_for_condition(
|
|
lambda: all(l1_manager.get_object_state(k) is None for k in keys),
|
|
timeout=5.0,
|
|
)
|
|
assert ok, "All keys should be deleted from L1 after buffer-only cleanup"
|
|
|
|
ctrl.stop()
|
|
adapter.close()
|