Final HEAD263d6bf: claude, agy and grok PASS (delta review of the lifecycle-lock commits on top of theirab842f1PASS), codex PASS (its own round after F1/F2). Full suite at263d6bf: 906 passed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
3.2 KiB
3.2 KiB
🔍 Review Report — Job 9b88790c (델타 ab842f1...263d6bf)
- Reviewer:
reviewer-grok-01(role: reviewer) · 2026-09-14 - 범위:
git diff/git logab842f1...HEAD(그 이전 브랜치는 PASS 범위) - HEAD:
263d6bffix(issue-18): let an exiting heartbeat timer skip the busy lifecycle lock; kill a timer whose pid cannot be recorded - 선행:
2b1d590잡별 lifecycle flock - 안전: 실제 레지스트리·모니터·purge 미접촉. 소스 무변경.
git diff --check ab842f1...HEAD 클린.
(1) 락 순서·교착
두 flock: 레지스트리/seq (registry.lock) vs 타이머 소유권 (<job>.heartbeat.lock).
start_timer/stop_timer는 lifecycle 만 잡는다. drain 대기 중 seq lock 을 안 잡으므로, 이미 admit 된 beat 가next_seq로 seq lock 을 잡을 수 있다. 주석대로 drain×seq 교착을 피한다.- 터미널:
raise_barrier(seq lock, 해제) →stop_timer(lifecycle). seq 를 잡은 채 lifecycle 을 기다리지 않는다. - F1: 종료 타이머는
lifecycle_lock(..., blocking=False)(heartbeat.py:472-478). 부모가 drain 동안 lock 을 쥐고 있어도 자식은 기다리지 않고 빠져나간다. lock 을 얻으면 pidfile 이 자기 pid 일 때만 unlink. 바쁜 lock 은 건드리지 않음 — 홀더가 pidfile 을 지운다. - 재현 테스트:
test_an_idle_timer_stops_gracefully_well_before_the_drain_deadline(drain=10, 종료 < 3s, SIGKILL 경고 없음).
(2) 동시 start · barrier · pidfile 실패
- 동시
start_timer: observe/drain/spawn/pidfile 이 한 lock.test_concurrent_start_timer_calls_leave_exactly_one_tracked_owner— 생존 1, pidfile 일치, stop 후 둘 다 사망. - barrier/터미널 뒤: lock 아래 재확인, arm 안 함 (
:318-320).test_start_timer_after_the_terminal_barrier_does_not_arm. - F2: pidfile
open/write실패 시 그Popen을 kill+wait, 부분 pidfile 은 이 pid 접두일 때만 삭제, 예외 재발생, lock 은 contextfinally로 해제 (:344-364). spawn 자체 실패는 pidfile 없음.
(3) 예외·일관성
lifecycle_lock 은 획득 후에만 UNLOCK. BlockingIOError 는 yield False 후 return (UNLOCK 안 함). spawn 실패·pidfile 실패 모두 lock 해제. 테스트가 lock 재획득 LOCK_NB 로 확인.
(4) 테스트 결정성
신규: 동시 start, barrier 후 미arm, idle 정상 종료, pidfile ENOSPC, spawn 실패. 모두 프로세스 finally 회수. 동시 start 는 인터리빙을 강제하지 않으나 lock 이 직렬화하므로 한 소유자면 충분.
(5) 문서·락스텝
MESSAGING §2.5 가 lifecycle 직렬화·non-blocking 자식 정리·pidfile 실패 시 살처분을 반영. VERSIONS 31 = collect 31. 4.5.0 유지 (lib.sh/eager.py).
테스트 실행
env -u MAM_VERSION .venv/bin/python -m pytest -q \
tests/test_issue18_liveness_contract.py \
tests/test_issue5_phase1_delegation.py \
tests/test_version_consistency.py
100 passed in 127.96s (issue18 31 + phase1 + version). 브로커 의존 건 이번 실행 skip 없음.
종합
F1 교착과 F2 미추적 타이머가 닫혔고, seq lock 과 lifecycle lock 의 순서는 drain 과 양립한다. 설계 재작업 불필요.
[VERDICT: PASS]