Skip to content

fix: 채팅 재구독 세대 가드 — 동기 closed 재진입 - #308

Merged
seizeh merged 2 commits into
mainfrom
fix/chat-resubscribe-gen
Aug 10, 2026
Merged

fix: 채팅 재구독 세대 가드 — 동기 closed 재진입#308
seizeh merged 2 commits into
mainfrom
fix/chat-resubscribe-gen

Conversation

@seizeh

@seizeh seizeh commented Aug 10, 2026

Copy link
Copy Markdown
Owner

#305 후속. 백오프 사다리는 RealtimeService 에서 가져왔는데 같이 있어야 할 세대 카운터가 빠졌다.

무엇이 문제인가

unsubscribe()removeChannel 은 그 채널의 subscribe 콜백에 closed 를 한 번 더 발화시킨다. 소켓이 이미 끊겨 있으면 — 끊김 복구의 대부분이 이 경우다 — 동기로 되돌아온다:

// realtime_channel.dart:728
if (!canPush) { leavePush.trigger('ok', {}); }   // canPush = isConnected && isJoined
// → onClose() → _onClose(() => callback(RealtimeSubscribeStatus.closed, null))  (:150)

_resubscribe 는 그 unsubscribe 뒤에 대입되므로 재진입 시점엔 아직 null — 재진입 가드를 그냥 통과한다.

결과
① 동기 재진입 재진입이 T1 을 잡고 바깥이 T2 로 덮어쓴다(cancel 없이). 둘 다 발화 → 채널 둘, 하나는 주인 없이 남아 계속 수신하고 그 이벤트가 다시 재구독을 부른다
② 소켓이 살아 있는 경우 leave ack 의 closed 가 재구독 뒤에 도착해 멀쩡한 새 채널을 파괴. refill 이 빈손이면 _attempt 리셋도 없어 30초 간격 재구독이 영구히 돈다

RealtimeService 는 정확히 이 함정을 _gen 으로 막고 있다(realtime_service.dart:50 주석이 이유를 적어 뒀다). 사다리만 이식되고 가드가 빠진 셈.

고치는 방법

같은 _gen 패턴. 채널 identity 비교 대신 세대 카운터를 쓴 이유는 콜백이 _channel 대입 전에 발화할 수 있어서다.

검증

대역이 동기 재발화를 흉내 낸다 — 이게 없으면 테스트가 버그를 못 잡는다.

  • 수정 : 2개 모두 실패(각각 구독 3회)
  • 수정 : 통과. flutter analyze 클린, 전체 328개 통과

첫 테스트는 2.6초를 기다린다. 이중 예약된 두 타이머는 _attempt 가 달라 1초·2초로 갈리므로 1.4초만 보면 버그가 있어도 통과해 버린다 (처음 이 함정에 빠졌다).

🤖 Generated with Claude Code

seizeh and others added 2 commits August 10, 2026 11:11
#305 가 알림 채널(RealtimeService)의 백오프 사다리는 가져왔는데 같이
있어야 할 세대 카운터를 빠뜨렸다.

unsubscribe → removeChannel 은 그 채널의 subscribe 콜백에 closed 를 한 번
더 발화시킨다. 소켓이 이미 끊겨 있으면 — 끊김 복구의 대부분이 이 경우다 —
realtime_channel 의 `if (!canPush) leavePush.trigger('ok')` 를 타 **동기로**
되돌아온다(canPush = isConnected && isJoined).

_resubscribe 는 그 unsubscribe 뒤에 대입되므로 재진입 시점엔 아직 null 이라
재진입 가드를 그냥 통과한다. 결과:

  ① 재진입이 타이머 T1 을 잡고, 바깥이 이어서 T2 로 덮어쓴다(cancel 없이).
     둘 다 발화해 채널이 둘이 되고 하나는 주인 없이 남는다. 샌 채널의
     이후 이벤트가 다시 재구독을 부른다.
  ② 소켓이 살아 있으면 leave ack 의 closed 가 재구독 **뒤에** 도착해
     멀쩡한 새 채널을 파괴한다 — refill 이 빈손이면 _attempt 리셋도 없어
     30초 간격 재구독이 영구히 돈다.

RealtimeService 가 정확히 같은 함정을 _gen 으로 막고 있다. 그 패턴을
그대로 쓴다 — 채널 identity 비교는 콜백이 대입 전에 발화할 수 있어
세대 카운터가 안전하다.

t: 대역이 동기 재발화를 흉내 낸다. 수정 전 2개 실패(각각 구독 3회),
수정 후 통과. 첫 테스트는 2.6초를 기다린다 — 이중 예약된 두 타이머는
_attempt 가 달라 1초·2초로 갈리므로 1.4초만 보면 버그를 놓친다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

전체 라인 커버리지: 20.5% (4291/20903)

이 PR 의 변경 파일:

파일 커버리지
lib/state/chat_room_state.dart 🟡 78% (121/155)

@seizeh
seizeh merged commit 07cd255 into main Aug 10, 2026
1 check passed
@seizeh
seizeh deleted the fix/chat-resubscribe-gen branch August 10, 2026 02:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant