- Synthesize collaborative multi-agent architectural analysis in NATS_REPORT.md - Establish Option C: retain MQTT client protocol while adopting nats-server as dedicated broker - Add private server deployment and configuration guide in PRIVATE_SERVER.md - Update IMPROVEMENTS.md with latent defect findings (B-14, B-15, B-16, O-5) and 4-track priority roadmap - Archive durable loop planning and review reports in .agents/reports/
25 KiB
📐 심층 분석 계획서 Rev.2 — MAM 메시징 백플레인: MQTT → NATS 전환 타당성
- Job ID:
f1956d2e(Rev.1 =641929ab) - Planner: claude (session:
herdr:canary-projects-multi-agent-mux-creator-claude) - Role: Planner (
MULTI_AGENT_RULES.md§1 — 본 작업에서 저장소 코드 0건 수정) - 반영 대상 Challenge:
10003692(agy, Worker / Plan Reviewer) —[VERDICT: PASS WITH CHALLENGE] - 기준 커밋:
ac82f9b(refactor, 작업 트리 clean) - 테스트 베이스라인: 276 tests collected (실측)
0. Challenge 판정 요약
Challenge 는 지적 1건(C1) 을 제기했고, 나머지 6개 섹션은 승인했습니다. C1 을 실측으로 판정한 결과 결론은 채택, 근거·메커니즘·심각도는 정정입니다.
| # | 지적 | 판정 | 실측 근거 |
|---|---|---|---|
| C1-a | job_subscriber.py 가 위임 경로에서 블로킹 대기 대상이며 Rev.1 이 이를 누락 |
✅ 전면 인정 — Rev.1 §1.2 표가 틀렸습니다 | multi-agent-mux-delegate-job:227 wait "$sub_pid" 실재. run_loop.sh 는 전 호출부가 --type direct 로 이 경로를 탐 |
| C1-b | job_subscriber.py 에 디스크 폴백이 없음 |
✅ 전면 인정 | 이벤트 대기는 watcher.events.get(timeout=wait) 단일 경로. reconcile.sh 의 exit 3 폴백에 해당하는 것이 없음 |
| C1-c | 메커니즘: "publish_event 가 디스크를 갱신하고 종료 → 와이어 메시지만 없음" | ⚠️ 현행 코드와 불일치 — 정정 | 현행은 디스크도 갱신되지 않습니다(F-1). C1-c 는 Track 0 수정 이후의 상태를 기술한 것. 즉 C1 은 기존 버그가 아니라 Track 0 수정의 잔여 결함 |
| C1-d | "idle_timeout(120s) 까지 블록 → 최소 2분 지연" | ❌ 실측 반증 — 기각 | 브로커 도달 불가 시 구독자는 40초에 rc=1 로 사망(traceback), 접속 거부 시 15.1초. 5초 핸드셰이크 창을 넘겨 죽으므로 에이전트는 정상 실행되고, wait 도달 시점엔 이미 종료 → 추가 지연 0초 |
| C1-e | 해결책: 디스크 터미널 상태 확인 후 정상 종료 | ✅ 채택 — 단, 더 강한 사유로 | 지연이 아니라 거짓 실패 판정이 진짜 피해. read_logged_status 는 mqtt_common.py:559 에 실재함(인용 정확) |
추가로, Challenge 가 놓친 결함 2건을 발견했습니다 (§3). 그중 F-4 는 C1 이 지적한 것보다 심각합니다.
[VERDICT: DO NOT MIGRATE THE CLIENT PROTOCOL — ADOPT
nats-serverAS THE BROKER INSTEAD]판정 불변. C1 은 전략 판정이 아니라 Track 0 의 범위를 확장시킵니다. Challenge 도 §3 표에서 판정 자체는 전항목 승인했습니다.
1. C1 정밀 판정 (실측)
1.1 인정 — Rev.1 §1.2 표의 오류
Rev.1 은 job_subscriber.py 를 이렇게 분류했습니다:
|
job_subscriber.py| 라이브 이벤트 tail | ❌run_loop.sh가 호출하지 않음 (호출처:BOOTSTRAP.md:170문서,test_tier4_e2e.py) | 영향 없음 |
이는 틀렸습니다. 원인은 방법론 오류입니다 — 저는 grep -rln --include="*.sh" --include="*.py" --include="*.md" 로 호출처를 찾았는데, 위임 실행 파일 multi-agent-mux-delegate-job 은 확장자가 없어 include 필터에서 제외되었습니다. 실제 호출 사슬은:
run_loop.sh:378 delegate_job_safe submit --type "direct" ...
└→ run_loop.sh:132 bash "$REPO_ROOT/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job"
└→ :164 job_subscriber.py ... & (background)
└→ :227 wait "$sub_pid" || true (blocking join)
run_loop.sh 의 --type "direct" 지정은 :378, :412, :440, :498, :554, :597 … 전 호출부입니다 (TYPE 기본값도 :96 에서 direct). 따라서 job_subscriber.py 는 run_loop 의 제어 경로 안에 간접적으로 존재합니다. Challenge 의 지적이 정확합니다.
단, Rev.1 §1.1 의 핵심 측정은 그대로 유효합니다: run_loop.sh 자체의 MQTT 참조는 :889 1건뿐이고, 잡 완료 판정은 wait_for_job() 의 3초 파일 폴링입니다. 즉 잡 결과 판정은 여전히 브로커와 무관하며, 브로커가 관여하는 것은 join 시점의 대기뿐입니다. 이 구분이 §1.3 의 심각도 산정을 좌우합니다.
1.2 정정 — C1-c 의 메커니즘은 현행 코드와 다릅니다
Challenge §2.2 step 3:
publish_event.pyupdates the on-disk job file (.mam/jobs/<id>.json) and exits. The network publish fails, so no MQTT message is delivered over the wire.
현행 코드는 디스크도 갱신하지 않습니다. publish_event.py:186-190 이 레지스트리 동기화 이전에 return 2 하기 때문입니다 — 이것이 Rev.1 §4 의 F-1 이고, 실측으로 재현했습니다 (status=running 불변, last_seq 0→1 소모, events=0).
즉 C1 이 기술한 상태는 Track 0 수정이 적용된 이후에만 성립합니다. 이 순서를 바로잡는 것이 중요한 이유:
- C1 은 "지금 존재하는 별도 버그"가 아니라 "F-1 을 고쳐도 남는 잔여 결함" 입니다.
- 따라서 F-1 수정만으로는 위임 경로가 완성되지 않는다는 Challenge 의 결론은 옳으며, 두 수정은 같은 트랙에서 함께 이루어져야 합니다. Challenge 의 실행 권고(§4-1)는 정확합니다.
- 반대로, C1 을 먼저 고치고 F-1 을 놔두면 아무 효과가 없습니다 — 디스크에 터미널 상태가 없으므로 폴백이 읽을 것이 없습니다. 순서 의존성이 존재하며 §4 에 명시했습니다.
1.3 기각 — "최소 2분 지연"은 실측으로 성립하지 않습니다
Challenge §2.2 step 6: "hangs on wait "$sub_pid" for at least 2 minutes".
실측 1 — 브로커 도달 불가 (10.255.255.1:1883, 라우팅 블랙홀)
exit_rc=1 elapsed=40s
socket.timeout: timed out ← 미포착 예외로 사망
SUBSCRIBED 출력 횟수: 0
실측 2 — 브로커 접속 거부 (127.0.0.1:1, 즉시 RST)
exit_rc=1 elapsed=15101ms
WARNING ... attempt 4/5 failed: [Errno 61] Connection refused; retrying in 8.0s
ConnectionRefusedError: [Errno 61] Connection refused
핵심 타이밍 3개를 대조하면 C1-d 가 성립하지 않는 이유가 드러납니다:
| 구간 | 값 | 출처 |
|---|---|---|
| 핸드셰이크 대기 창 | 5.0초 (for ((i=0; i<25; i++)) × sleep 0.2) |
multi-agent-mux-delegate-job:171-190 |
| 구독자 접속 재시도 총 시간 | 최소 15초 (attempts=5, base_delay=1.0 → 1+2+4+8) |
job_subscriber.py:200-203 |
| 구독자 실제 사망 시점 | 15.1초 / 40초 (실측) | 위 |
따라서 브로커가 처음부터 죽어 있으면:
- t=5s — 구독자는 아직 살아 있음 →
sub_ready=0→WARNING: subscriber subscribe handshake timed out — falling back to proceed→ 에이전트 정상 실행 - t=15~40s — 구독자가 traceback 과 함께 rc=1 로 사망
- 에이전트 종료 후
:227wait "$sub_pid"도달 → 이미 종료된 프로세스 → 즉시 반환
추가 지연 0초입니다. "최소 2분"이 아니라 최대 0초입니다.
C1 이 기술한 120초 대기가 성립하려면 SUBSCRIBE 성공 이후 브로커가 중도 유실되어야 합니다. 이 경우에도:
idle_timeout은 마지막 수신 이벤트부터 계산됩니다 (job_subscriber.py의last_event = time.monotonic()).- 에이전트는 보통
started발행 후 수 분 동작합니다. 그러면 idle 은 에이전트 실행 도중 만료되어 구독자가 먼저 죽고,wait은 다시 즉시 반환됩니다. - 실제 블로킹은 에이전트가 마지막 성공 이벤트로부터 120초 이내에 끝나는 짧은 잡에서만 발생합니다.
정정된 심각도: 추가 지연은 "항상 최소 120초"가 아니라 "최대 약 120초, 통상 0초" 입니다.
1.4 그럼에도 C1-e 를 채택하는 이유 — 진짜 피해는 지연이 아니라 거짓 판정
:227 은 wait "$sub_pid" || true 로 종료 코드를 폐기합니다. 따라서 run_loop 경로에서 구독자의 rc=1/rc=2 는 잡 판정에 영향을 주지 않습니다(잡 판정은 wait_for_job 의 디스크 폴링). 그러나:
- 감사 산출물인
$REGISTRY_DIR/$JOB_ID.subscriber.out에는 성공한 잡에 대해socket.timeouttraceback 또는ERROR: idle timeout (120s, no events)가 남습니다. :228이 이를 그대로 표준출력에 덤프합니다 (echo "subscriber output:"; cat "$logf").- 즉 정상 완료된 잡의 감사 기록이 실패로 오염됩니다. 이것이 지연보다 실질적 피해가 큽니다.
그리고 rc 를 폐기하지 않는 경로가 존재합니다 — §3 의 F-4.
2. 판정에 영향 없음 — 전략 결론 불변
Challenge §3 은 Option (C), asyncio 마찰, F-1 발견, F-2/F-3, 스파이크 매트릭스를 전항목 승인했습니다. C1 은 브로커 제품 선택과 직교하는 Track 0 범위 확장이므로, Rev.1 §0 의 판정표는 그대로 유지됩니다.
| 선택지 | 코드 변경 | 테스트 변경 | A-2 해소 | 판정 |
|---|---|---|---|---|
| (A) 현행 유지 (공개 HiveMQ) | 0 | 0 | ❌ | 기각 |
(B) 네이티브 NATS (nats-py) |
4개 호출부 재작성 | 46건 | ✅ | 기각 |
(C) nats-server + MQTT 프로토콜 유지 |
0 | 0 | ✅ | ✅ 채택 |
오히려 C1 은 판정을 보강합니다: job_subscriber.py 가 제어 경로에 (간접적으로) 있다는 사실은, 이 파일을 네이티브 NATS 로 재작성하는 것의 위험을 키웁니다. Rev.1 §1.3 에서 이 파일은 raw paho 클라이언트 구동 9줄로 4개 호출부 중 최다입니다. 선택지 (B)는 제어 경로 위의 파일을 재작성하게 되며, (C)는 건드리지 않습니다.
3. Challenge 가 놓친 결함 2건
F-4 (Critical) — loop/discuss 경로에서 구독자 종료 코드가 잡 판정 그 자체
multi-agent-mux-delegate-job:331-341:
local sub_rc=0
wait "$sub_pid" || sub_rc=$?
echo "subscriber output:"; cat "$logf" || true
local job_status="running"
if [[ $sub_rc -eq 0 ]]; then job_status="completed"
elif [[ $sub_rc -eq 1 ]]; then job_status="error" # ← 브로커 도달 불가 = rc 1 (실측)
else job_status="timeout" # ← idle timeout = rc 2
fi
echo "Job role $display_role finished with status: $job_status"
:227 의 || true 와 달리 여기서는 rc 가 잡 상태로 직결됩니다. 그리고 실측했듯 브로커 도달 불가 시 구독자는 미포착 예외로 rc=1 을 냅니다.
job_subscriber.py 가 rc=1 을 내는 정상 경로는 "터미널 error 이벤트를 수신했다" 하나뿐입니다(return 1 at 말미). 그런데 파이썬 미포착 예외도 rc=1 입니다. 따라서:
"에이전트가 error 를 보고했다" 와 "브로커에 접속하지 못했다" 가 구분 불가능하며, 후자가 전자로 보고됩니다.
성공한 잡이 job_status="error" 로 판정됩니다. 이는 지연 문제가 아니라 오케스트레이션 정확성 결함이며, C1 이 지적한 :227 경로보다 심각합니다 — :227 은 rc 를 버리므로 피해가 로그 오염에 그치지만, :331 은 잘못된 판정을 하류로 전파합니다.
적용 범위 주의: run_loop.sh 는 전 호출부가 --type direct 이므로 이 경로를 타지 않습니다. F-4 는 multi-agent-mux-delegate-job loop|discuss 를 직접 호출할 때 발현합니다 (:362, :375 에서 TYPE 분기). 즉 잠재 결함이지 현재 run_loop 회귀는 아닙니다. 그러나 Track 0 이 job_subscriber.py 를 손대는 김에 함께 닫아야 하며, 디스크 폴백만 추가하고 rc 매핑을 놔두면 다른 예외 경로에서 동일 혼동이 남습니다.
F-5 (Medium) — 문서가 주장하는 persistent session 이 코드상 구성 불가
MESSAGING.md:64:
Subscribers connect with persistent session flags to ensure the broker buffers QoS 1 messages during temporary network drops.
그러나 mqtt_common.py:258-262:
client_id = f"{config.client_id_prefix}-{role}-{uuid.uuid4().hex[:8]}" # ← 매 실행 랜덤
client = mqtt.Client(
callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
client_id=client_id,
) # ← clean_session / clean_start 미지정
durable session 은 안정적인 client_id 를 전제합니다. 현재는 매 프로세스 기동마다 client_id 가 바뀌므로, clean-session 플래그를 켜더라도 브로커가 이전 세션을 인식할 수 없습니다. 즉 문서의 주장은 코드로 뒷받침되지 않습니다.
이것이 C1 판정에 미치는 영향: "durable session 을 켜면 중도 유실 문제가 해결된다"는 대안 경로는 client_id 안정화 없이는 불가능합니다. 따라서 C1-e 의 디스크 폴백이 올바른 해법이며, 이 발견은 Challenge 의 결론을 보강합니다. (client_id 안정화는 동시 실행 구독자 충돌 위험을 낳으므로 별도 과제로 분리합니다 — §5 비-목표.)
4. 개정된 Track 0 (F-1 + C1 + F-4) — 최우선
브로커 제품 선택과 완전히 독립이며 우선순위가 더 높습니다.
4.0 순서 의존성 (필수)
Step 1 (publish_event.py) → Step 2 (job_subscriber.py) → Step 3 (rc 매핑)
디스크에 터미널 상태를 디스크를 읽어 조기 종료 판정 혼동 제거
"쓰게" 만든다 (Step 1 없이는 읽을 것이 없음)
Step 2 를 단독 시행하면 효과가 0입니다. §1.2 에서 판정한 대로, 현행은 브로커 실패 시 디스크에도 아무것도 남지 않기 때문입니다.
4.1 Step 1 — publish_event.py 실패 순서 재구성 (Rev.1 대비 불변)
publish_event.py:186-208 재구성:
publish(...)실패를publish_ok = False로 표시하되return하지 않음.- 감사 로그·레지스트리 이벤트·상태 동기화를 발행 성공 여부와 무관하게 항상 수행. 감사 레코드에
"published": publish_ok와"publish_error": str(exc)포함. - 종료 코드 계약 유지 — 발행 실패 시 여전히
return 2. 단 상태는 이미 기록된 뒤. - seq 소모 정책: 현행(실패해도 소모) 유지. 재생방지(
> highest accepted)에 무해하고, (2)의 실패 레코드가 gap 을 설명 가능하게 만들기 때문. 이 결정을 주석으로 명문화.
4.2 Step 2 — job_subscriber.py 디스크 폴백 (C1-e 채택)
이벤트 대기 루프의 queue.Empty 분기(job_subscriber.py:233-239)에서, pending 잡별로 디스크 터미널 상태를 확인합니다.
설계 결정 4가지 (Challenge 가 명시하지 않은 부분):
| 항목 | 결정 | 사유 |
|---|---|---|
| 조회 순서 | registry.load_job() → 없으면 mqtt_common.read_logged_status() |
레지스트리가 라이브 레코드(권위), 감사 로그는 레지스트리가 정리되어도 남는 보조 사본 |
| 조회 주기 | 매 queue.Empty 마다가 아니라 최소 3초 간격 스로틀 |
대기 루프는 wait = min(..., 1.0) 로 최대 1초마다 깨어남. 잡당 파일 2개를 초당 읽으면 불필요한 I/O. wait_for_job 의 3초 폴링 주기와 정렬 |
| 합성 이벤트 | 디스크 상태로 터미널 판정 시 _format_line 과 동일 형식으로 stdout 에 출력하되 "source": "disk-fallback" 표기 |
감사 로그에서 와이어 수신분과 폴백분이 구분 가능해야 함. 무표기 합성은 F-3(HMAC) 우회 통로가 됨 |
| HMAC 검증 | 디스크 폴백분은 HMAC 검증 대상 아님 | 로컬 파일시스템은 이미 신뢰 경계 안. 단 위 표기로 출처를 명시 |
| 종료 코드 | 디스크가 completed → 0, error → 1, cancelled → 1 |
와이어 수신 시의 기존 매핑과 동일하게 유지 (호출부 계약 불변) |
주의 — 조기 종료가 아닌 경우: --wait-any 로 다중 잡을 감시 중이면 모든 pending 잡이 터미널에 도달했을 때만 종료합니다. 일부만 디스크 터미널이면 나머지는 계속 대기합니다.
4.3 Step 3 — rc → job_status 매핑 명확화 (F-4)
multi-agent-mux-delegate-job:333-341 의 3분기 매핑은 "구독자가 정상적으로 판정했다"를 전제하지만, 미포착 예외도 rc=1 을 냅니다. 두 가지를 분리합니다:
job_subscriber.py의main()을 최상위try/except로 감싸 인프라 실패는 전용 코드(예: rc=3) 로 반환하고,rc=1은 "터미널 error 이벤트 수신"에만 예약합니다.:333-341에rc=3분기를 추가해job_status="broker_unavailable"로 판정하고,wait_for_job과 동일하게 디스크를 재확인하도록 합니다.:180-187의sub_exit != 0 → exit 1조기 중단 경로도rc=3을 중단 사유에서 제외합니다 (브로커 부재로 위임 전체를 죽여서는 안 됨). — 실측상 이 경로는 재시도 최소 15초 > 핸드셰이크 창 5초라 현재 도달 불가이나,attempts/base_delay변경 시 살아나는 잠복 경로이므로 함께 닫습니다.
4.4 회귀 가드 (mutation 기준 — 결함을 되살렸을 때 반드시 실패해야 함)
| ID | 가드 | Mutation (이걸 되돌리면 FAIL 해야 함) |
|---|---|---|
| G-1 | 도달 불가 브로커로 --event completed 발행 → rc=2 이면서 레지스트리 status == "completed" |
return 2 를 상태 동기화 앞으로 이동 |
| G-2 | 동일 상황 감사 로그에 published: false + publish_error 레코드 존재 |
append_event 를 성공 경로로만 한정 |
| G-3 | 브로커 정상 시 rc=0 + status == "completed" + 감사 published: true (무회귀) |
— |
| G-4 | 발행 실패 후 last_seq 1 증가, 후속 성공 발행이 더 큰 seq 사용 |
seq 롤백 도입 |
| G-5 | 레지스트리에 status=completed 를 미리 써 두고 도달 불가 브로커로 job_subscriber.py 실행 → rc=0 으로 3~5초 내 종료 |
디스크 폴백 제거 → idle/연결실패로 rc≠0 |
| G-6 | 동일 조건에서 stdout 합성 라인에 disk-fallback 표기 존재 |
표기 누락 시 FAIL (F-3 우회 통로 방지) |
| G-7 | 레지스트리 status=error → 폴백 종료 코드 1 / status=completed → 0 |
매핑 반전 |
| G-8 | --wait-any 로 2개 잡 감시 중 1개만 디스크 터미널 → 종료하지 않음 |
부분 종료 도입 시 FAIL |
| G-9 | 브로커 도달 불가 + 디스크에 터미널 상태 없음 → rc 3 (rc 1 아님) | rc=1 로 되돌리면 FAIL (F-4) |
| G-10 | loop 경로에서 rc=3 수신 시 job_status 가 "error" 가 아님 |
3분기 매핑으로 되돌리면 FAIL |
통합 검증 (가장 중요): 브로커 정지 상태에서 --type direct 위임 1건을 끝까지 돌려, ① wait_for_job 이 3900초가 아니라 3초 내 return 0, ② $JOB_ID.subscriber.out 에 traceback 이나 idle timeout 이 없을 것, ③ 전체 벽시계 시간이 브로커 정상 시와 유의미하게 다르지 않을 것.
4.5 예상 테스트 증분
가드 10건 → 베이스라인 276 → 286. 전량 신규이며 기존 276건 수정은 0건을 목표로 합니다 (기존 rc 계약을 rc=1/rc=0/rc=2 범위에서 유지하고 rc=3 만 신설하기 때문).
5. Track 1 이후 (Rev.1 대비 불변)
Track 1 — 브로커 선택 스파이크
격리 클론(git clone --local --no-hardlinks . "$SCRATCH/nats-spike")에서 수행, 종료 후 삭제.
| ID | 검증 | 통과 기준 |
|---|---|---|
| S-1 | nats-server 가 MAM MQTT 클라이언트 수용 | rc=0, status=completed, last_seq 정상 |
| S-2 | paho CallbackAPIVersion.VERSION2 + MQTT 3.1.1 호환 |
CONNACK rc=0 |
| S-3 | Retained terminal event ← 최고 위험 | 신규 구독자가 즉시 최종 이벤트 수신 |
| S-4 | QoS 1 발행 ACK | is_published() True |
| S-5 | 와일드카드 구독 | SUBSCRIBED 출력 + 이벤트 수신 |
| S-6 | 인증 + TLS | 자격증명 누락 시 거부 |
| S-7 | subject 단위 권한 (A-2 목표) | publisher 구독 거부 / subscriber 발행 거부 |
| S-8 | 전체 회귀 | 286 passed, 0 failed (Track 0 반영 후) |
| S-9 🆕 | Track 0 폴백이 nats-server 에서도 유효 | G-5 · 통합 검증을 nats-server 정지 상태에서 재실행 |
S-3 실패 시 → 선택지 (C) 기각, mosquitto 로 진행. S-3 은 판정 번복의 유일한 조건입니다.
Track 2 — A-2 해소 (F-2 + F-3), 브로커 확정 후
- F-3:
registry.register_job()에서auth_token항상 발급(secrets.token_hex(32)). 기존None잡 하위호환은verify_hmac의 현행 경로가 담당하되, 신규 잡에서는 그 경로가 발생하지 않음을 가드로 고정. - F-2:
DEFAULT_TOPIC_ROOT를 지문 기반(mam/<sha256[:12]>/jobs)으로 전환. 순서 엄수 — ① 발행측 전환 → ② 동작 확인 → ③reconcile.sh:237legacy 구독 제거(별도 커밋, 롤백 보존). - S-7 에서 검증한 subject 단위 권한을 배포 설정에 반영.
Track 3 — 문서 동기화
| 문서 | 변경 |
|---|---|
MESSAGING.md |
§1.2 브로커 제품 갱신. §5 한계에 F-1·C1 해소 기록. §1.2.4 의 persistent session 서술을 F-5 실측에 맞게 정정 🆕 |
IMPROVEMENTS.md |
A-2 갱신, F-1·F-4·F-5 신규 등재, F-2·F-3 상태 갱신 |
VERSIONS.md |
브로커 런타임 버전 등재 |
deploy/install.sh:484-492, install_mam.sh:306-314 |
requirements.txt 변경 없음(paho 유지). 브로커 기동 안내만 추가 |
비-목표 (명시적 제외)
- ❌
nats-py도입 및 클라이언트 프로토콜 재작성 - ❌
.mam/jobs/*.json의 JetStream KV 대체 — 상태 계층 교체는 전송 교체와 별개 결정 - ❌ client_id 안정화 / durable session 도입 🆕 — F-5 의 근본 해결이나, 동시 구독자 client_id 충돌 위험을 새로 낳음. Track 0 의 디스크 폴백이 같은 문제를 부작용 없이 해결하므로 별도 과제로 분리
- ❌
requirements.txt의paho-mqtt>=2.0.0변경
6. Cross-Review 대비 — 반론 선제 대응
Rev.1 §8 의 6개 항목은 유효하며, C1 관련 2개를 추가합니다.
| 예상 반론 | 응답 |
|---|---|
| "C1-d 를 기각했으면서 C1-e 를 채택하는 것은 모순" | 아닙니다. 지적된 결함(디스크 폴백 부재)은 실재하고, 제시된 피해(120초 지연)만 실측 반증되었습니다. 채택 사유를 지연에서 거짓 실패 판정·감사 기록 오염(§1.4)과 F-4 의 오판정(§3)으로 교체했으며, 이는 원래 사유보다 강한 근거입니다 |
| "Rev.1 이 틀렸다면 판정 전체를 재검토해야 한다" | 틀린 것은 §1.2 표의 한 행(호출처 누락, 원인은 grep include 필터)이며, 판정의 토대인 §1.1 측정(run_loop.sh MQTT 참조 1건, wait_for_job 파일 폴링)은 재확인 결과 그대로 유효합니다. 게다가 C1 은 job_subscriber.py 를 제어 경로에 넣음으로써 선택지 (B)의 위험을 키워 판정을 보강합니다(§2) |
| "F-4 는 run_loop 가 안 쓰는 경로이니 무시해도 된다" | 현재 회귀는 아니지만, loop/discuss 는 :80 usage 에 문서화된 공개 인터페이스이며 :362/:375 에서 실제 분기합니다. 무엇보다 Track 0 이 job_subscriber.py 를 이미 여는 이상, rc 계약을 함께 정리하지 않으면 디스크 폴백을 넣고도 다른 예외 경로에서 같은 혼동이 남습니다 |
7. 산출물 및 다음 단계
- Creator: 본 Rev.2 를 종합해
NATS_REPORT.md로 저장합니다. §0 Challenge 판정표, §1 C1 실측 판정, §3 F-4·F-5, §4 개정 Track 0(순서 의존성 + 가드 10건)이 필수 포함 항목입니다. - Reviewer 전원 — 다음 4건을 재현 검증해 주십시오:
run_loop.sh전 호출부가--type "direct"인가 (§1.1 — C1 인정의 근거)- §1.3 타이밍 반증: 도달 불가 브로커에서
job_subscriber.py가 40초에 rc=1, 접속 거부에서 15.1초에 rc=1. 핸드셰이크 창은 5초 - §3 F-4:
:333-341의 rc→job_status매핑에서 브로커 실패(rc=1)가"error"로 판정되는가 - §5 S-3 nats-server retained message 지원 — 판정 번복의 유일한 조건
- Track 0 은 브로커 결정과 무관하게 즉시 착수 가능하며, Step 1 → Step 2 → Step 3 순서를 반드시 지켜야 합니다(§4.0).
8. 판정 재확인
[VERDICT: DO NOT MIGRATE — ADOPT
nats-serverAS BROKER, KEEP MQTT CLIENT PROTOCOL]조건: §5 S-3(retained terminal event) 및 S-8(286 tests green) 통과. S-3 실패 시 mosquitto 로 회귀하며, 어느 경우에도 클라이언트 프로토콜은 변경하지 않습니다.
선행 필수: Track 0 (F-1 + C1 + F-4) — 브로커 선택과 독립이며 우선순위가 더 높습니다. Step 순서 의존성 존재(§4.0).