마지막 업데이트 2026-07-22
ppi-socket Loki 서버 로그를 같은 roomId·시각으로 교차 정렬. (아동 세션 → roomId → 진행자 세션 매칭)
guest-step-update act1 + guest-auto-finish) emit이 서버에 도달하는 데 약 26초 지연됐다. 서버가 06:05:20까지 전환을 몰랐으므로 모니터도 받을 수 없었고, 그 사이 진행자의 수동 개입이 stale 상태를 덮어써 아동을 회귀시켰다. 병목은 게스트의 socket.io 업링크(guest→server 송신)였다 — 서버·인프라·다른 룸·앱 코드는 모두 정상.
06:04:54.317 [게스트] auto-finish 발동 → goToNextStep (하랑대화1→2 자체 전환)
06:04:54.322 [게스트] socket.emit(guest-step-update act1) · notifyAutoFinish 호출 ✅
└ 클라는 emit 호출했으나… 서버 미도달
06:04:56.7 [게스트] 호스트 "응" 메시지 수신 정상(다운링크 OK)
06:05:00.909 [서버] Host changed step act1 ← 진행자 수동 점프(host→server 실시간 정상)
06:05:06.507 [서버] monitoring peer …c7c32921 disconnect ← 진행자 모니터 새로고침
06:05:07.046 [서버] Host requested room state {hasState:true} → stale(act0/1) 복원
06:05:17/18 [서버] Host changed step act0/1 ← 진행자 "새로고침" 버튼(stale push) → 아동 회귀
06:05:20.580 [서버] Guest auto-finish {matchedCondition} ← 26초 만에 첫 도달
06:05:20.623 [서버] Guest step update act1 ← 26초 만에 첫 도달
a77c4cf8_5의 guest-step-update는 정상 연속(최대 공백 5.4초). 우리 룸만 27.4초 공백.대시보드 실시간 로그 패널(ppi-logs)은 행 가상화로 스크롤이 안 돼, Loki query_range API를 직접 호출해 roomId로 필터링했다.
# LogQL — 룸 단위 전 인스턴스, 시각 윈도우 {service="ppi-socket", env=~"prod"} |= "id-011-586a-4d1f-a415-747776dec5c7_42" # 패널이 가상화돼 스크롤 불가 → Grafana 프록시로 직접 호출 GET /api/datasources/proxy/uid/<lokiUid>/loki/api/v1/query_range ?query=...&start=<ns>&end=<ns>&direction=forward&limit=5000
06:04:53.272 Guest step update in room …_42 {"activityIndex":0,"stepIndex":1} ← 마지막
⟨ 06:04:54 게스트가 act1 emit… 서버 수신 0건 ⟩
06:05:20.580 Guest auto-finish from guest-…_42 {"matchedCondition":"그러면같이한번찾으러가보자!"} ← 26s 만에
06:05:20.623 Guest step update in room …_42 {"activityIndex":1,"stepIndex":0} ← 26s 만에
06:05:00.909 Host changed step …_42 {act1} ← 진행자 수동 점프(정상 실시간)
06:05:06.507 Cleaning up peer monitoring:…c7c32921… after disconnect ← 모니터 소켓 끊김
06:05:07.040 Peer …c7c32921…-545e7890 joined room, found 3 existing producers ← 재입장
06:05:07.046 Host requested room state …_42 {"hasState":true} ← stale(act0/1) 복원
# {…} |= "Guest transport state: guest-id-011…_42" 04:52.307 RTT=9ms ← 정상(초당 보고) ⟨ 04:52 → 05:20 보고 0건 (step-update 공백과 동일 구간) ⟩ 05:20.580 RTT=9ms / 05:20.715 RTT=102ms ← 복구 찰나 스파이크
RTT 값이 아니라 보고 자체가 28초 끊김 = 게스트發 모든 데이터(step-update·auto-finish·transport stat)가 그 구간 안 올라감.
# {…} |= "Guest step update in room" — 06:04:50~06:05:22, 룸별 최대 공백 a77c4cf8…_5 6건 maxGap 5.4s ← 정상 연속 id-011…_42 7건 maxGap 27.4s ← 우리 룸만 이상
engine.io 하트비트는 서버 PING(다운링크) → 클라 PONG(업링크). PONG도 같은 WebSocket/TCP 스트림이라 stall 동안 같이 묶인다. 그런데:
disconnect 이벤트가 없으니 socket.io 재연결·앱의 connect 기반 복구가 하나도 안 걸렸고, 보낸 emit은 그냥 26초 대기. (※ 25/20s는 기본값 — 단, "disconnect 미발생" 경험 사실만으로 "한계 미초과"는 확정)
useMonitorNetworkLevel은 4개 입력을 보는데, 이 stall은 전부 미충족:
| 입력 | 감시 대상 | stall 중 |
|---|---|---|
| guestTransportRtt ≥400ms ×2 | WebRTC(UDP) 미디어 RTT | 업데이트 끊겨 마지막 ~9ms에 stale → 임계 미달 |
| 트랙 7초 끊김 | track.readyState==="live" | 미디어 안 흘러도 "live" 유지 |
| socketConnected 7초 | 모니터 자신의 소켓 | 게스트 소켓 아님 + connected |
| bitrate/packetLoss | WebRTC stats | 업데이트 끊겨 stale |
진행자 모니터의 guestCurrentStep은 게스트發 이벤트로만 갱신된다(use-host-socket.ts). 호스트 자신의 수동 점프는 갱신하지 않음. 26초간 게스트 업데이트 미수신 + 06:05:06 리마운트의 stale 복원으로 그 값은 하랑대화1에 멈춤.
handleRefreshCurrentStep)은 그 stale한 guestCurrentStep(하랑대화1)을 게스트에 그대로 push → 이미 하랑대화2로 넘어간 아동을 하랑대화1로 되돌려 같은 대화를 재시작시킴.
platform: "Linux armv81"(ARM) + 전면 카메라 + 데스크톱 UA(X11; Linux x86_64) → 안드로이드 태블릿(데스크톱 사이트 모드) 추정.연결은 유지(disconnect 미발생)되나 emit이 서버에 안 닿는 stall을 클라가 ack+타임아웃으로 감지해 LogRocket 커스텀 이벤트로 기록. 사후 분석 시 "자동전환 미반영 = 업링크 stall"을 즉시 식별.
// apps/web/lib/socket-uplink-stall-detector.ts (신규) export function emitWithUplinkStallDetection(eventName, payload, { timeoutMs = 5000, roomId }) { if (!socket.connected) { socket.emit(eventName, payload); return; } // 미연결은 버퍼링·판정 제외 socket.timeout(timeoutMs).emit(eventName, payload, (err) => { if (err) trackEvent("GuestUplinkStall", {...}); // ack 미수신 = 업링크 정체 (시작 1회) else if (wasStalled) trackEvent("GuestUplinkRecovered", { stalledMs }); // 복구 1회 }); }
| 파일 | 변경 |
|---|---|
apps/web/lib/socket-uplink-stall-detector.ts | 신규 — ack+타임아웃 헬퍼 + LogRocket 기록 |
apps/web/lib/guest-step-utils.ts | emitGuestStepUpdate → 헬퍼 경유 |
apps/web/entities/guest-socket/model/use-guest-socket.ts | notifyAutoFinish → 헬퍼 경유 |
apps/socket/src/sfu-socket/handlers/session-handlers.ts | GUEST_STEP_UPDATE·GUEST_AUTO_FINISH 핸들러가 ack 응답 |
GuestUplinkStall 폭주.
tsc --noEmit 클린. 단 ack 왕복/타임아웃은 런타임 통합 동작이라 dev에서 네트워크 throttle로 실측 권장.이번 작업은 감지·로깅만 — stall을 고치거나 재전송하지 않음. 백로그:
guestCurrentStep push 금지(미확인 시 비활성/게스트 재요청).