Files
multi-agent-mux/.agents/reports/reviewer-grok-01/report-9b88790c.md
T
GodopuandClaude Fable 5.1 5611859930 docs(reports): archive v4.5.0 PASS review reports (Issue #18)
Final HEAD 263d6bf: claude, agy and grok PASS (delta review of the
lifecycle-lock commits on top of their ab842f1 PASS), codex PASS (its own
round after F1/F2). Full suite at 263d6bf: 906 passed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
2026-09-14 01:30:28 +09:00

54 lines
3.2 KiB
Markdown

# 🔍 Review Report — Job 9b88790c (델타 `ab842f1...263d6bf`)
- **Reviewer**: `reviewer-grok-01` (role: reviewer) · 2026-09-14
- **범위**: `git diff`/`git log` **`ab842f1...HEAD`** (그 이전 브랜치는 PASS 범위)
- **HEAD**: `263d6bf` `fix(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 은 context `finally` 로 해제 (`: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]