Skip to content

SSE 재연결 시 놓친 알림 replay (Last-Event-ID 기반)#751

Open
m-a-king wants to merge 1 commit into
devfrom
feat/750-sse-last-event-id-replay
Open

SSE 재연결 시 놓친 알림 replay (Last-Event-ID 기반)#751
m-a-king wants to merge 1 commit into
devfrom
feat/750-sse-last-event-id-replay

Conversation

@m-a-king

Copy link
Copy Markdown
Collaborator

Situation

  • SSE 알림 스트림의 이벤트에 id(seq)가 전혀 없어(name + data 만) 클라이언트가 재연결할 때 끊김 동안 발생한 알림을 스트림 차원에서 복구할 수단이 없었다.
  • 연결이 30분 타임아웃으로 주기적으로 강제 재연결되는 구조라 그 틈의 알림 유실이 구조적으로 일어난다. 지금까지는 "재연결 시 목록/배지 API 재조회"가 명문화된 유일한 복구 경로였다 (notification-sse-spec.md).

Task

  • SSE 이벤트에 순번(id)을 부여하고, 재연결 시 그 순번 기준으로 놓친 알림을 다시 흘려보내는 계약을 설계·구현한다.
  • 결정 포인트: 순번의 원천(별도 카운터 vs 알림 PK), id 를 어느 이벤트에 붙일지, 유실과 중복 중 무엇을 감수할지, 긴 공백의 처리.

Action

설계 결정

결정 선택 이유
순번 원천 알림 PK (별도 카운터 없음) DB 영속 값이라 재시작·blue-green·스케일아웃과 무관하게 단조 증가. payload 의 id 와 같은 값이라 클라 dedup 키를 겸한다
id 부여 대상 notification 이벤트만 SSE 프로토콜상 id 없는 이벤트는 클라 lastEventId 를 갱신하지 않는다. 비영속이라 replay 불가능한 connect·silent-sync·하트비트가 복구 기준점을 오염시키지 않게 자연 제외된다
등록·replay 순서 연결 등록 후 replay 조회 반대 순서면 조회와 등록 사이 발행분이 유실된다. 이 순서의 대가는 라이브·replay 중복 도착이고, 클라가 id 로 dedup 하는 계약으로 흡수한다 (유실보다 중복)
상한 초과 공백 replay 통째 생략 (상한 100건) 일부만 보내면 replay 구간 뒤에 조용한 구멍이 남는다. 상한을 넘는 공백은 장기 미접속이라 기존 목록/배지 재조회가 맞는 복구 경로다
잘못된 Last-Event-ID 첫 연결로 취급 (400 아님) SSE 에서 id 재개는 서버가 지원하지 않을 수도 있는 optional 계약이라, EventSource 자동 재연결 루프를 400 으로 깨지 않는다

구현

  • LocalSseDelivery 가 notification 이벤트 빌드를 한 곳(notificationEvent)에 모아 SSE id 필드에 알림 PK 를 싣고, 라이브(deliver)와 replay(replayTo)가 같은 셰입을 공유한다.
  • SseReconnectReplayer(신규)가 Last-Event-ID 초과분을 발생 순서(id asc)로 조회해 재연결한 그 연결에만 흘려보낸다. 조회는 리포지토리의 짧은 트랜잭션으로 끝나고 emitter write 는 트랜잭션 밖이다.
  • subscribeLast-Event-ID 헤더를 optional 로 받는다. 숫자가 아니거나 양수가 아니면 무시하고 첫 연결로 취급한다.
  • replay 조회는 기존 idx_notifications_user_id_id (user_id, id) 인덱스가 그대로 받친다. 마이그레이션 없음.
  • replay 실행 건수·상한 초과 생략을 info 로그로 남겨 상한 적정성을 추세로 볼 수 있게 했다.

계약 문서

  • notification-sse-spec.md: id 필드, Last-Event-ID replay, 상한, 중복 dedup, 희귀 race 한계(PK 발급 순서와 커밋 순서 어긋남)를 명문화. WEB 은 EventSource 가 자동으로 처리하고, APP 은 마지막 id 저장·전송 예시를 추가했다.
  • NotificationSseApi: Last-Event-ID 헤더 파라미터와 replay 계약을 OpenAPI 설명에 반영했다.

Result

  • 30분 타임아웃 재연결 같은 짧은 끊김의 알림 유실이 스트림 차원에서 메워진다. 웹(EventSource)은 클라 코드 변경 없이 동작하고, 앱은 마지막 id 저장·전송만 추가하면 된다.
  • additive 변경이라 서버 단독 선배포가 가능하다. 헤더를 보내지 않는 기존 클라이언트는 지금과 동일하게 동작한다.
  • 검증: 통합 테스트가 실제 와이어(text/event-stream 응답 버퍼)를 단언한다. replay 발생 순서, Last-Event-ID 그 자체는 재전송 안 됨, 상한 초과(101건) 시 통째 생략, 비숫자 헤더 무시, silent-sync 에 id 없음까지 5개 분기.
  • 남는 한계: PK 발급 순서와 커밋 순서가 어긋나는 희귀 race 는 replay 가 놓칠 수 있다. 기존 재조회 fallback 이 커버하며 스펙에 명시했다.

연관 이슈

- notification 이벤트에만 SSE id(알림 PK)를 싣는다. 별도 카운터 대신 DB 영속 값을 써 재시작·blue-green 과 무관하게 단조 증가하고, payload 의 id 와 같은 값이라 클라 dedup 키를 겸한다
- id 없는 이벤트(connect·silent-sync·하트비트)는 SSE 프로토콜상 클라 lastEventId 를 갱신하지 않으므로, 비영속이라 replay 불가능한 이벤트가 복구 기준점에서 자연 제외된다
- subscribe 가 Last-Event-ID 헤더를 받아 초과분을 발생 순서(id asc)로 그 연결에만 replay 한다. register 후 replay 순서로 "유실 대신 중복"을 택했고, 클라가 id 로 dedup 하는 계약을 스펙에 명시
- 상한(100건) 초과 공백은 일부만 보내면 replay 구간 뒤에 조용한 구멍이 남으므로 통째로 생략하고 기존 목록/배지 재조회 계약에 맡긴다
- 숫자가 아닌 Last-Event-ID 는 400 으로 끊지 않고 첫 연결로 취급한다 — EventSource 자동 재연결 루프를 깨지 않기 위함
- replay 조회는 기존 idx_notifications_user_id_id (user_id, id) 인덱스가 그대로 받쳐 마이그레이션이 없다
@m-a-king m-a-king added the feat 외부 가시적 새 기능 label Jul 15, 2026
@m-a-king m-a-king self-assigned this Jul 15, 2026
@github-actions

Copy link
Copy Markdown

Discord 스레드 연동용 메타데이터입니다. discord-pr-bot 워크플로가 자동 생성하며, 수정·삭제하면 PR 과 Discord 알림 연동이 끊깁니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 외부 가시적 새 기능

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SSE notification 이벤트 id(seq) 부여 + Last-Event-ID 재연결 replay

1 participant