Code Review: c7a90bfa — Reduce put_many lock count from 4N to 2N (재검토)
커밋: c7a90bfa0f7e7cf7b907498df9a780e31e75ca34 + working-tree 변경
제목: [Perf][RawBlock] Reduce put_many lock count from 4N to 2N
브랜치: perf/reduce-lock-put-many
작성자: Nayeon Kim nayeoni.kim@samsung.com
일자: 2026-06-02
리뷰 모드: /code-review --xhigh (9-angle finder × 1-vote verify × sweep)
선행 리뷰: cr-5a27732f-rawblock-put-many-lock-coalesce.md
변경 요약
선행 리뷰 (5a27732f) 의 후속 조치가 working-tree 에 적용된 상태에서
재검토. 적용된 변경:
put_many의_last_io_ts = time.monotonic()을if success:가드로 감쌈 (core.py:519-526).inflight_io_count()docstring 을 ON window 에 맞춰 갱신 (core.py:398-406)._CountingLock.__enter__가self를 반환하도록 정정 (tests/...:2300-2303).- 회귀 테스트 추가:
test_put_many_does_not_stamp_last_io_ts_on_prep_failure(tests/...:2387-2421).
이번 재검토 범위는 선행 리뷰가 본 PR 내 보강을 권고했던 4 건이 정확히 적용되었는가 + 그 보강 자체가 새 결함을 만들지 않았는가 두 축.
정합성 (Correctness) — 부분 통과
선행 리뷰의 F1/F3/F5 fix 와 신규 회귀 테스트는 의도대로 동작. 다만
if success: 가드의 docstring 정당화가 사실과 다르고, 선행 F4 가
유지된 read 측 비대칭이 working-tree 의 새 룰과 맞물려 더 명확한 결함이
됨, 그리고 신규 테스트 1건이 의도한 invariant 를 trivially 통과.
또한 public 시맨틱 변경이 외부 release-note 없이 진행 중.
Findings
총 10건 (CONFIRMED 4, PLAUSIBLE 4 — 둘은 doc/altitude). 분류 표:
| # | finding | 파일:라인 | verdict | severity | 권장 액션 |
|---|---|---|---|---|---|
| F1 | load_many_into 가 pread 실패에도 _last_io_ts 갱신 → put 측의 if success: 룰과 비대칭 | core.py:660 | CONFIRMED | medium | 본 PR 내 수정 권고 |
| F2 | _read_slot_header 가 read 실패에도 _last_io_ts 갱신 — F1 과 같은 결함 | core.py:1000 | CONFIRMED | medium | 본 PR 내 수정 권고 |
| F3 | if success: 가드의 코멘트가 "Match pre-L1 semantics" 라 거짓 주장. 실제로는 pwrite 실패 시 pre-L1 과 다름 | core.py:520-525 | CONFIRMED | medium | 본 PR 내 코멘트 정정 |
| F4 | test_checkpoint_idle_gate_blocks_during_put_many 가 dirty short-circuit 으로 trivially 통과 → idle gate invariant 미검증 | tests/...:2520 | CONFIRMED | medium | 본 PR 내 테스트 보강 |
| F5 | inflight_io_count() 시맨틱이 pwrite-only → allocate→commit 으로 넓어졌으나 외부 (report_status) 에 release-note 부재 | core.py:398, 773 | PLAUSIBLE | low | 본 PR 내 release-note 한 줄 |
| F6 | lock-protocol 계약이 3 곳 (put_many docstring, 인라인 코멘트, _write_one Note) 중복 → drift 위험 | core.py:446-452, 505-510, 936-943 | PLAUSIBLE | low | 본 PR 내 통합 권고 |
| F7 | _inflight_io_count accounting 이 3 호출자 (put_many, load_many_into, _read_slot_header) 에 inline 중복 → context-manager helper 화 권고 | core.py:511, 609, 992 | PLAUSIBLE | info | 별도 cleanup PR (F1/F2 와 함께 처리 권장) |
| F8 | BlockingDevice 클래스 + monkeypatch.setitem 보일러플레이트 3건 중복 | tests/...:2436-2450, 2492-2506 | PLAUSIBLE | low | 별도 cleanup PR |
| F9 | _CountingLock 가 post-init core._lock = ... swap 으로 install — meta_enable_periodic=False 가정에 의존 | tests/...:2342 | PLAUSIBLE | info | 본 PR 내 코멘트 보강 또는 별도 cleanup |
| F10 | (보류) inflight is None 분기, _rawdev lazy init race, _closed TOCTOU — 모두 verifier 단계 REFUTED, drop | — | REFUTED | — | drop |
severity 기준: 선행 리뷰와 동일 — high = 회귀, medium = 계약/표준 위반, low = 정리, info = 코멘트 수준.
즉시 조치 권고 (본 PR 내 보강)
F1 / F2 — read 측 _last_io_ts 비대칭 정리 (medium) [CONFIRMED]
- 현상:
put_many는 working-tree 에서if success:가드로 prep/pwrite 실패 시_last_io_ts를 갱신하지 않도록 바뀌었음. 그러나load_many_into(core.py:657-660) 는 finally 에서_inflight_io_count -= 1후 무조건_last_io_ts = time.monotonic()호출._read_slot_header(core.py:997-1000) 도 동일.
- 근거 인용:
# core.py:657-660finally:with self._lock:self._inflight_io_count -= 1self._last_io_ts = time.monotonic()반면 put_many 는:# core.py:997-1000finally:with self._lock:self._inflight_io_count -= 1self._last_io_ts = time.monotonic()# core.py:519-526if success:# Match pre-L1 semantics: only stamp `_last_io_ts` when# an actual device write completed.self._last_io_ts = time.monotonic()
- 트리거: 잘못된 offset 으로
_read_slot_header가 반복 호출되거나 (_validate_loaded_entries같은 recovery 경로),load_many_into의 pread 가 transient OSError 로 반복 실패하는 워크로드. - 결과: 매 실패마다
_last_io_ts가 advance → checkpoint idle gate 가meta_idle_quiet_ms만큼씩 밀림. 선행 F1 가 put 측에서 막고자 했던 바로 그 시나리오가 read 측에 그대로 살아 있음. - 제안 픽스:
load_many_into의success_any = any(results)(또는 한 건도 성공하면 stamp),_read_slot_header는 header decode 가 정상이면 stamp 하는 식으로 put_many 와 일관된 룰 적용. 또는 F7 의 helper 로 통합 시 룰을 contract 안에 박아 넣음. - 이 PR 에 포함: F1, F2 와 F3 가 모두 같은 root (idle gate 룰의 부분 적용) 라 묶어서 처리 권장.
F3 — if success: 가드 코멘트 거짓 주장 정정 (medium) [CONFIRMED]
-
현상: working-tree 의
put_manyfinally 가드 옆 코멘트 (core.py:520-525):Match pre-L1 semantics: only stamp _last_io_ts when an actual device write completed. -
근거: pre-L1
_write_one은 inner try/finally 안에서_last_io_ts를 갱신했음. 분기별 실제 동작:실패 시점 pre-L1 stamp? post-L1 + if success:stamp?_prepare_write_payloadraise (outer except 로 빠짐)NO (inner try 미진입) NO ( success=False)pwrite raise (inner finally 가 stamp) YES NO ( success=False)양쪽 모두 성공 YES YES prep-실패에서는 일치하나, pwrite-실패에서는 새 동작이 pre-L1 보다 엄격함 (덜 자주 stamp). 코멘트의 "match" 주장은 사실과 다름.
-
제안 픽스 (택1):
- (A) 코멘트를 정확히 고쳐쓰기. 예:
"Stamp only on full success. Pre-L1 stamped on pwrite failures too, but that path is rare and the stricter rule prevents transient pwrite errors from delaying checkpoint."
- (B) pre-L1 일치를 진짜 원하면 가드를
if success or attempted_pwrite:같은 형태로 분리 — 실용성 낮음, (A) 권장.
- (A) 코멘트를 정확히 고쳐쓰기. 예:
-
이 PR 에 포함: 코멘트 수정만 — scope 작음.
F4 — test_checkpoint_idle_gate_blocks_during_put_many 보강 (medium) [CONFIRMED]
- 현상: 신규 테스트는
core._checkpoint_once(force=False) is False로 idle gate 가 닫혀있음을 검증한다고 docstring 에 명시. 그러나 assertion 시점에 put 은_write_one의 pwrite 안에서 block 중 → allocate-lock 만 수행됨 →_meta_dirty_total == _meta_persisted == 0. - 근거 인용:
put 의# core.py:1227-1238with self._lock:dirty = self._meta_dirty_total > self._meta_persistedidle_ok = self._inflight_io_count == 0 and (time.monotonic() - self._last_io_ts) >= (self.meta_idle_quiet_ms / 1000.0)if not dirty:return False # ← 여기서 short-circuitif not force and not idle_ok:return False # ← 여기 도달 못 함
_meta_dirty_total += 1은 commit-lock 안 (core.py:532,core.py:539) 에서만 일어나는데, 테스트는 commit-lock 진입 전에 assert 를 실행. 따라서_checkpoint_once는not dirty분기로 빠지며 idle gate 자체는 평가도 되지 않음. - 결과: idle gate 가 broken 이라도 (예:
idle_ok = True로 잘못 됐어도) 테스트는 통과함. 의도된 invariant 미검증. - 제안 픽스 (택1):
- (A) 테스트 시작 시
core.put_many(precursor_keys, precursor_objs)을 한 번 성공시켜_meta_dirty_total > _meta_persisted만든 뒤, BlockingDevice 로 두 번째 put 을 막고 assertion. - (B)
core._meta_dirty_total = core._meta_persisted + 1로 직접 조작 (private state poke, 선행 F2 와 같은 SLF 위반 — 정당화 코멘트 필요). - → (A) 권장. 첫 put 의 commit 도 BlockingDevice 와 충돌하지 않게
하려면 BlockingDevice 의
started/release를 N 번째 호출에만 적용하는 단순 카운터 추가.
- (A) 테스트 시작 시
- 이 PR 에 포함: 테스트 분량 ~10 줄 증가, value 큼.
F5 — inflight_io_count() 외부 시맨틱 변경 명시 (low) [PLAUSIBLE]
- 현상:
inflight_io_count()(core.py:398-406) docstring 은 ON window 명시로 갱신되었으나, 같은 값이report_status()(core.py:773) 의metadata_dirty_total와 별개로 노출되는 publishing 면에는 변경 노트가 없음. - 제안 픽스:
report_status()의 dict key 가inflight_io_count라면 README 또는 변경 문서 §3.3 에 ON window 가 allocate→commit 으로 넓어진 점을 한 줄 추가. 외부 모니터링이 이 값을 "active raw I/O" 로 해석 중이라면 그 의미가 약간 느슨해진 점만 명시. - 이 PR 에 포함: README/CHANGELOG 한 줄 — scope 미미.
F6 — lock-protocol 계약 중복 통합 (low) [PLAUSIBLE]
- 현상: 동일 contract 가 세 곳에 서술됨 —
put_manydocstring (core.py:446-452),put_many인라인 코멘트 (core.py:505-510),_write_oneNote:블록 (core.py:936-943). - 제안 픽스 (택1):
- (A)
put_manydocstring 만 권위 (authoritative) 로 두고,_write_oneNote:는 한 줄 cross-ref 로 축약 (예: "Seeput_manyfor the lock-accounting contract that this method depends on."). - (B)
docs/design/v1/storage_backend/raw_block/lock_protocol.md신규 작성, 양쪽이 그쪽으로 link. - → (A) 권장. (B) 는 분량 크고 본 PR scope 외.
- (A)
- 이 PR 에 포함: 한 함수의 docstring 축약 — 1 줄 이내 패치.
후속 PR 후보 (본 PR scope 외)
F7 — _inflight_io_count accounting helper 화 (info) [PLAUSIBLE]
- 현상: 동일 ritual (
+= 1→ I/O →-= 1+ 조건부_last_io_tsstamp) 이put_many(라인 511/518),load_many_into(609/659),_read_slot_header(992/999) 세 곳에 inline 중복. - 권고: F1/F2 정리와 함께
_inflight_io_window(success: bool)같은 context manager 로 통합. contract 가 helper 안에 박혀 신규 I/O 경로 추가 시 룰 누락 방지. - 이 PR 외 사유: F1/F2 정리만 본 PR 에서 끝내고, helper 화는 read 경로까지 손대는 큰 변경 — 선행 F4 후속 PR 과 묶기.
F8 — 테스트 device 보일러플레이트 통합 (low) [PLAUSIBLE]
- 현상:
BlockingDevice클래스 +monkeypatch.setitem(sys.modules, "lmcache_rust_raw_block_io", types.SimpleNamespace(...))가 신규 테스트 3건에 동일하게 복붙됨 (tests/...:2436-2450,tests/...:2492-2506+_install_fake_raw_block_device). - 권고:
_install_fake_raw_block_device가 device factory 를 받도록 확장하고,BlockingDevice와FailingDevice를 module-level fixture 로 공유. 약 20-30 줄 정리. - 이 PR 외 사유: 분량 작지만 본 PR 의 perf 변경과 무관 — 별도 cleanup PR.
F9 — _CountingLock install 패턴 (info) [PLAUSIBLE]
- 현상:
core._lock = counting의 post-init swap 은__init__시점의 lock 획득을 모두 놓침. 현재는_make_raw_block_core가meta_enable_periodic=False라 안전하나, 향후__init__에 lock 을 잡는 다른 경로가 추가되면 silent 하게 count 가 부풀어 오를 수 있음. - 권고 (택1):
- (A)
_CountingLockinstall 옆에 한 줄 코멘트 추가 — "depends onmeta_enable_periodic=False로 background lock acquirer 가 없음". - (B)
RawBlockCore.__init__에 lock factory injection — 변경 표면 큼. - → (A) 가 작고 충분.
- (A)
- 이 PR 외 사유: F2 (선행) 와 같은 layer — 별도 정리 PR 또는 본 PR 에서 1 줄 코멘트만 추가.
위험 / 호환성 평가
- on-disk format: 미변경 (선행 리뷰와 동일).
- public API:
put_many/_write_one시그니처 미변경.inflight_io_count()반환 타입 미변경, 시맨틱은 약간 넓어짐 — F5 참조. - 스레드 안전성: F1/F2 의 비대칭은 안전성이 아닌 idle gate 정확도 문제. 카운터 ±1 lock 보호는 유지됨.
- 롤백: working-tree 변경 5 줄 (가드 + 코멘트) 단독으로도 revert 가능.
- 회귀 영향:
- put 측 (working-tree 가드) → 선행 F1 픽스로 prep 실패 분기는 pre-L1 동작 복원. pwrite 실패 분기는 의도적으로 더 엄격함 (F3 코멘트만 정정).
- read 측 (F1/F2) → 여전히 pre-L1 의 unconditional stamp 패턴 유지. 읽기 실패가 빈번한 워크로드에서 checkpoint 지연 가능.
리뷰 메타
- 도구:
/code-review --xhigh(9-angle finder × 1-vote verify × sweep). - 단계 요약:
- Phase 0:
git diff @{upstream}...HEAD+git diff HEAD로 committed + working-tree diff 모두 확보. 선행 리뷰 (cr-5a27732f-rawblock-put-many-lock-coalesce.md) 의 fix 적용 상태 그대로. - Phase 1: 9 angles 병렬 (A 라인스캔 / B 제거동작 / C 호출자 / D Python pitfall / E proxy / Reuse / Simplification / Efficiency / Altitude).
- Phase 2: 6 verifier 병렬 (1 vote, CONFIRMED/PLAUSIBLE/REFUTED). REFUTED 3건 drop.
- Phase 3: sweep finder — 추가 신규 결함 0건.
- Phase 0:
- 입력 commit:
c7a90bfa+ working-tree - verdict 분포: CONFIRMED 4 / PLAUSIBLE 5 / REFUTED 3 (drop) / sweep 0.
- 본 리뷰는 read-only — 코드 변경 없음.
REFUTED (참고)
| 후보 | 사유 |
|---|---|
_inflight.pop(...) is None 로 slot leak 가능성 | _inflight mutator 는 put_many.insert/pop 와 delete_many.set-canceled (pop 아님) 뿐 — 동일 invocation 내 insert→pop 보장. None 분기 unreachable. |
_rawdev() lazy init race | __init__ 의 _ensure_capacity_and_layout 가 _rawdev() 를 동기 호출 → 인스턴스 공유 시점에 self._raw 이미 non-None. 경쟁 윈도우 없음. |
_closed TOCTOU | pre-L1 에도 동일 패턴 (if self._closed: break lock 밖) 존재. 본 PR 도입 아님 + _write_one 의 except Exception 으로 graceful fail. |