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

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

Router 매니저 — 크로스인스턴스 소유권·router-sync 코드레벨…: 입력: 개요 · 범위, 주요 처리 단계: Owner 권한 모델 — setter 분기, 결과: reaper 연계 (요약) 흐름
동작 흐름 요약
  1. 입력: 개요 · 범위
  2. 주요 처리 단계: Owner 권한 모델 — setter 분기
  3. 결과: reaper 연계 (요약)
작성일: 2026-06-14 대상: 개발자 — 다중 인스턴스 room 소유권/동기화 파악 핵심 파일: mediasoup/routerManager.ts, redis/room-store.ts
💬 대화로 먼저 이해하기 — "은행 지점 담당제·원장 비유" (비개발자·처음 읽는 사람용)
Q서버(인스턴스)가 여러 대인데, 한 수업(room)의 참가자들이 서로 다른 서버에 붙으면 어떻게 되나요?
A은행 지점 담당제처럼 room마다 담당 지점(owner 인스턴스)을 딱 하나 정해 고정(pin)해요. 다른 지점에 잘못 오면 "담당 지점으로 가세요"(WRONG_INSTANCE redirect)라고 안내하죠. 실제 미디어(mediasoup Router)는 그 지점 금고(메모리)에만 있거든요.
Q수업 상태(설정·guestState 같은 것)는 어디에 저장되나요?
A진실은 항상 중앙 원장(Redis, SoT)이에요. 각 지점의 수첩(메모리 캐시)은 빠른 참조용 사본이고, 둘이 어긋나면 원장이 옳습니다.
Q담당이 아닌 지점에 붙은 호스트가 설정을 바꾸면요?
A비담당 지점은 자기 수첩을 절대 고치지 않고 원장(Redis)만 고친 뒤, 담당 지점에 쪽지(router-sync pub/sub)를 보내요. 담당 지점이 쪽지를 보고 원장을 다시 읽어 수첩을 갱신하죠. 순서도 중요해요 — 원장 저장이 끝난 뒤 방송(Redis → broadcast).
Q담당 지점이 갑자기 죽으면 그 수업은요?
A담당 표시(pin)에 유효기간(TTL 90s)이 있어서, heartbeat가 끊긴 지점의 room은 다음 입장 시 다른 지점이 자동 인수(stale takeover)해요. 단 금고 속 미디어는 복구 불가라 클라이언트가 재접속으로 새로 붙습니다. 상세는 아래 "Room Pin"·"router-sync" 섹션과 routerManager.ts·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 원본

관련 문서