Router 매니저 — 크로스인스턴스 소유권·router-sync 코드레벨 동작 흐름 P0코드레벨

마지막 업데이트 2026-07-22

작성일: 2026-06-14 대상: 개발자 — 다중 인스턴스 room 소유권/동기화 파악 핵심 파일: mediasoup/routerManager.ts, redis/room-store.ts

개요 · 범위

PPI-879 이후 SFU 소켓 서버는 다중 인스턴스 + Redis(Valkey) SoT 구조다. mediasoup Router는 인스턴스 메모리에만 존재(복구 불가)하므로, 한 room의 모든 peer를 한 인스턴스(owner)에 고정(pin)하고, 메모리는 캐시+owner 표지로만 쓰며 진실은 Redis에 둔다. 이 문서는 그 핵심인 routerManager(657L)와 room-store를 다룬다.

AI-driven 영역(@handover-redis): 이 인프라는 AI 도구로 구현됐고 암묵지가 없다. 의도는 코드·커밋·테스트가 진실이다. 설계 의도 출처: docs/handover/jacob/redis-infra.md (PR #634).

5분 핵심 출처: redis-infra.md#5분-안에-알아야-할-것

묻는 것짧은 답
SoT는 어디Redis(Valkey). 메모리는 캐시 + owner 표지
room owner는 누구router가 살아있는(메모리에 room이 있는) 인스턴스. tryPinRoom으로 고정
비-owner가 상태 바꿔도 되나메모리 setter는 owner만(가드 id-021). 비-owner는 Redis만 갱신(id-043)
cross-instance 전파router-sync pub/sub 채널로 owner가 메모리 refresh + 재broadcast (id-041)
저장 순서Redis 저장 → broadcast (반대 금지, id-019)
채널 prefixSTAGE/브랜치별 격리 (#640, id-009)

메모리 vs Redis 역할

routerManager (메모리)room-store (Redis SoT)
보관rooms: Map<roomId, MediasoupRoom> (router 핸들 + guestState/hosts/설정 캐시)RedisRoomMeta 해시(room:{id}) + owner 키(room:{id}:instance)
성격인스턴스 로컬, 복구 불가. room이 메모리에 있으면 = 이 인스턴스가 owner진실(SoT). 모든 인스턴스가 공유
일관성어긋나면 Redis가 옳다cross-instance read의 기준

Invariants 출처: redis-infra.md#invariants

Router 생성 createRouter L137 / getOrCreateRouter L184

Room Pin — 원자적 소유권 획득 room-store.ts: tryPinRoom L124 / PIN_ROOM_LUA L94

JOIN_ROOM(게스트/호스트) 시 tryPinRoom(roomId)을 호출해 owner를 정한다(모니터는 제외). 단일 Lua 스크립트로 완전 원자 처리:

-- KEYS[1]=room:{id}:instance, ARGV: newInstanceId, ttl, keyPrefix, roomId
owner = GET roomInstanceKey
if owner == false       → SET owner=new(EX ttl) + SADD instance:new:rooms → {1,new}
if owner == newInstance  → EXPIRE 갱신 + SADD(멱등)         → {1,new}
if owner heartbeat 없음  → SREM old:rooms, SET new, SADD     → {1,new}  (stale 인계)
else                     → {0, owner}  (살아있는 다른 owner → 실패)

Owner 권한 모델 — setter 분기

모든 상태 setter(setAutoTransitionEnabled, setVadSettings, setNoiseReduction, updateGuestState 등)는 동일한 owner/non-owner 분기를 쓴다.

room = rooms.get(roomId) if (room) // 내가 owner → 메모리 먼저 갱신 + awaitRedis(updateRoomField) // 같은 인스턴스 즉시 정합 else // 비-owner → Redis read → merge → Redis write + publishRoomSync(roomId) // 메모리 건드리지 않음

updateGuestState 예시 L527–561

핵심 불변식(redis-infra.md#invariants): 메모리 in-place 변경은 owner만(id-021). 비-owner는 Redis 갱신만(id-043). 저장 끝난 뒤 broadcast(id-019).

router-sync — cross-instance 메모리 정합 publishRoomSync L115 / initRouterManagerSync L628

비-owner가 Redis를 바꾸면 owner의 메모리 캐시는 모른다. 이를 pub/sub로 해소한다.

[비-owner] Redis write → publishRoomSync(roomId) └─ PUBLISH router-sync {roomId, by: instanceId} [owner] initRouterManagerSync 구독자 if msg.by == 나 → 무시(자기 메시지) if !hasRoom(msg.roomId) → 무시(내가 owner 아님) else → refreshFromRedis(roomId) → broadcastRoomList(io, peers)

peerCount · router 종료 increment/decrementPeerCount L216/234

router 종료 race / pin 폴백 회귀: id-028

reaper 연계 (요약)

인스턴스는 heartbeat를 Redis ZSET에 주기 발행한다. leader로 뽑힌 인스턴스의 reaper가 heartbeat 끊긴 인스턴스의 instance:{id}:rooms / peer 매핑을 원자적으로 정리한다. room pin의 stale takeover(PIN_ROOM_LUA의 heartbeat 체크)와 짝을 이룬다. 상세는 → 로드맵 #16(Redis 인프라/reaper).

함정 · 회귀 패턴 출처: redis-infra.md#failure-modes

안전하게 바꾸는 법(redis-infra.md#안전하게-바꾸는-법): 새 상태는 Redis store에 저장 + hydration fallback + reaper 정리 대상 확인. 새 setter는 owner 가드 + "Redis→broadcast" 순서 + ack 실패 시 클라 롤백.

파일 · 라인 레퍼런스

파일역할
mediasoup/routerManager.tsrouter 생성·소유권·guestState·peerCount·setter·router-sync(657L)
redis/room-store.tsRedisRoomMeta·tryPinRoom(PIN_ROOM_LUA)·peerCount Lua·meta CRUD
redis/key-prefix.ts채널/키 STAGE prefix 정책(#640)
redis/reaper.ts, reaper-scheduler.tsstale 인스턴스 room/peer 정리(#16)
sfu-socket/handlers/connection-handlers.tsJOIN_ROOM에서 tryPinRoom·redirect
docs/handover/jacob/redis-infra.md설계 의도·invariants·failure modes 원본

관련 문서