본문으로 건너뛰기

Code Review: 4803df4a — Add LRU eviction retry for rust raw-block backend

커밋: 4803df4a 브랜치: bitbucket/priv/dg/raw-block-cache-eviction 제목: Add LRU eviction retry for rust raw-block backend 작성자: Daegyu Han daegyu94.han@samsung.com 일자: 2026-06-01


변경 요약

RustRawBlockBackend에 파이썬 레이어 LRU 트래커(OrderedDict)를 추가하고, put 시 슬롯이 꽉 찼을 때 LRU 키를 evict한 뒤 재시도하는 로직을 도입.

핵심 변경

파일변경 내용
rust_raw_block_backend.py_lru_keys: OrderedDict 추가, _sync_lru_with_index(), _touch_lru(), _evict_one_lru() 신규, _submit_put_one에 evict-retry 루프 도입
core.pyRawBlockPutManyResultno_free_slot_keys: list[str] 필드 추가, put_many 내 슬롯 부족 케이스에서 해당 필드 채움
test_rust_raw_block_backend.py기존 test_rust_raw_block_backend_rejects_when_full 대체 + 신규 테스트 8건 추가

동작 흐름 (신규)

put_many → 성공 → _touch_lru → 완료
→ 슬롯 부족 → _evict_one_lru → 재시도 (루프)
┕ evict 불가 (전부 pinned) → RuntimeError
→ 기타 실패 → RuntimeError (evict 없이 즉시)
  • capacity miss 판별: put_result.no_free_slot_keys로 슬롯 부족인지 아닌지 구분 → 슬롯 부족이 아닌 실패에서는 LRU 항목을 건드리지 않음
  • pinned 키 보호: _pinned_keys set에 있는 키는 evict 후보에서 제외
  • 체크포인트 복구: __init__apply_loaded_state 이후 _sync_lru_with_index()로 코어 인덱스와 LRU 상태 동기화

분류 요약

카테고리건수가장 심각한 항목
Correctness — 락 보유 중 I/O1_pin_lock 보유 중 delete_many / contains_key 호출
Correctness — 레이스 조건1apply_loaded_state_sync_lru_with_index 호출 시 진행 중 put과 레이스
Design — 에러 메시지 모호성1크기 초과와 슬롯 부족이 동일 메시지
Naming — 락 이름1_pin_lock이 LRU + pin 상태를 함께 보호
Test — assert 사용1테스트 헬퍼에서 코딩 표준 위반
Info — 성능1_evict_one_lru 내 전체 키 목록 복사

상세 리뷰

[error] 락 순서 불일치 — 잠재적 데드락

위치: rust_raw_block_backend.py:626-638 (_evict_one_lru)

_evict_one_lru()_pin_lock을 보유한 채로 self._core.delete_many()self._core.contains_key()를 호출합니다. 이 Rust 코어 메서드들은 내부 뮤텍스를 취득합니다.

현재 코드베이스에서는 역방향 락 취득 경로(코어 내부 → 파이썬 _pin_lock)가 없으므로 실제 데드락은 발생하지 않습니다. 그러나 향후 코어에서 파이썬 콜백을 호출하거나 다른 메서드가 락 순서를 역전시키는 경우 감지가 어렵습니다.

권장 수정: 락 보유 중 코어 I/O를 호출하지 않는 패턴으로 리팩터링하거나, 최소한 주석으로 "이 락을 보유한 채 코어 I/O를 호출해도 안전한 이유"를 명문화.

def _evict_one_lru(self) -> bool:
# Candidate selection under lock, I/O outside.
with self._pin_lock:
candidate = next(
(k for k in self._lru_keys if k not in self._pinned_keys),
None,
)
if candidate is None:
return False
# _pin_lock NOT held during core I/O.
deleted = self._core.delete_many([candidate], force=False)[0]
with self._pin_lock:
if deleted:
self._lru_keys.pop(candidate, None)
return True
if not self._core.contains_key(candidate, lock=False):
self._lru_keys.pop(candidate, None)
return False

단, 위 패턴은 선택 후 삭제 사이에 다른 스레드가 같은 키를 evict할 수 있으므로 그 케이스를 처리하는 코드가 필요합니다.


[error] apply_loaded_state_sync_lru_with_index 레이스

위치: rust_raw_block_backend.py:213-216 (apply_loaded_state)

apply_loaded_state_put_lock 없이 호출됩니다. 진행 중인 _submit_put_one_touch_lru를 막 완료하거나 실행 중인 상태에서 _sync_lru_with_index가 새 OrderedDict로 교체하면, 직전에 _touch_lru로 추가된 키가 새 딕셔너리에 누락됩니다.

_pin_lock으로 딕셔너리 객체 자체의 동시 접근은 막혀 있지만, _lru_keys 참조 교체와 _touch_lru 삽입 사이의 순서가 _pin_lock 단일 구간으로 완전히 묶이지 않아 논리적 일관성이 깨질 수 있습니다.

권장 수정: apply_loaded_state 호출을 문서화된 "no active puts" 전제 조건으로 명시하거나, _put_lock도 함께 취득한 뒤 _sync_lru_with_index를 호출.


[warning] 크기 초과 에러 메시지가 슬롯 부족과 동일

위치: rust_raw_block_backend.py:398-399

if len(memory_obj.byte_array) > self.slot_bytes - self.header_bytes:
raise RuntimeError(f"Failed to persist raw-block key {spec.encoded}")

사전 크기 검사 실패와 슬롯 부족 evict 실패가 같은 문자열을 사용합니다. 운영 중 로그나 테스트 match= 패턴으로 두 케이스를 구분할 수 없습니다.

raise RuntimeError(
f"Key {spec.encoded}: data size {len(memory_obj.byte_array)} bytes "
f"exceeds slot capacity {self.slot_bytes - self.header_bytes} bytes"
)

[warning] _pin_lock 이름이 역할을 반영하지 않음

위치: rust_raw_block_backend.py:145

_pin_lock_pinned_keys뿐 아니라 _lru_keys도 보호합니다. 이름만 보면 LRU 상태도 같이 보호한다는 것을 알기 어렵습니다. _eviction_lock 또는 _state_lock이 더 명확합니다.


[warning] _sync_lru_with_index — 복구 키의 LRU 순서가 재시작 전 접근 이력을 보존하지 않음

위치: rust_raw_block_backend.py:611-618

snapshot_indexed_keys()_index dict의 삽입 순서, 즉 최초 put 순서를 반환합니다. 따라서 복구 후 LRU 순서는 "원래 put된 순서 (oldest-first)"로 리셋됩니다. 재시작 전에 get으로 최근 접근된 키가 있었더라도 그 정보는 체크포인트에 저장되지 않으므로 재시작 후에는 put 순서 기준으로 먼저 evict 대상이 됩니다.

우연히 oldest-put-first이므로 LRU 의미론과 부분적으로 일치하지만, 재시작 전 access 패턴과 다를 수 있다는 점을 _sync_lru_with_index docstring에 명시해야 합니다.


[info] _evict_one_lru 내 전체 키 목록 복사

위치: rust_raw_block_backend.py:628

for encoded_key in list(self._lru_keys.keys()):

_pin_lock 보유 중 전체 키를 복사합니다. 락 보유 중 _lru_keys를 수정하기 때문에 복사가 필요한 것으로 보이지만, 이유를 주석으로 남겨 두면 좋습니다. 캐시 규모가 커지면 성능 영향이 있을 수 있습니다.


[info] 테스트 헬퍼에서 assert 사용

위치: test_rust_raw_block_backend.py:262-265 (_make_test_raw_block_obj)

assert obj is not None and obj.tensor is not None

코딩 표준에 따르면 runtime 검사에는 if/raise ValueError를 사용해야 합니다. 테스트 코드라 우선순위는 낮지만 일관성을 위해 수정 권장.


테스트 커버리지 평가

테스트커버 시나리오
test_rust_raw_block_backend_evicts_lru_when_full기본 evict-retry: k1 최근 접근 → k2 evict
test_rust_raw_block_backend_eviction_skips_pinned_keypinned k1 보존, k2 evict
test_rust_raw_block_backend_put_fails_when_all_candidates_locked모든 후보 pinned → RuntimeError
test_rust_raw_block_backend_write_failure_does_not_evict_lru비용량 실패 시 LRU 변경 없음
test_rust_raw_block_backend_remove_clears_lru_entryremove 후 LRU에서 정리
test_rust_raw_block_backend_checkpoint_rebuilds_lru_for_eviction체크포인트 복구 후 evict 가능
test_rust_raw_block_backend_apply_loaded_state_rebuilds_lruapply_loaded_state 후 LRU 재구성
test_rust_raw_block_backend_concurrent_puts_preserve_slot_invariants동시 put, 슬롯 수 불변식 유지
test_rust_raw_block_backend_pinned_read_survives_concurrent_eviction_pressure동시 put pressure 중 pinned 읽기 보존

핵심 시나리오들이 잘 커버되어 있습니다. 누락된 케이스:

  • _submit_put_one 진행 중 apply_loaded_state 동시 호출 (위 [error] 레이스 재현)
  • 크기 초과 사전 검사 경로 (현재 테스트 없음)