Skip to content

SSE 이벤트 통합 replay - Redis Stream 기반 Last-Event-ID 복구#756

Closed
m-a-king wants to merge 3 commits into
devfrom
feat/750-sse-redis-stream-replay
Closed

SSE 이벤트 통합 replay - Redis Stream 기반 Last-Event-ID 복구#756
m-a-king wants to merge 3 commits into
devfrom
feat/750-sse-redis-stream-replay

Conversation

@m-a-king

@m-a-king m-a-king commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Situation

  • SSE 이벤트에 id(seq)가 없어 재연결 시 끊김 동안의 이벤트를 스트림 차원에서 복구할 수 없었다. 연결이 30분 타임아웃으로 주기적으로 강제 재연결되는 구조라 유실이 구조적으로 일어난다.
  • 이 작업의 실제 동인은 아이템 등록 화면이다: 참여자 화면의 PENDING 로딩 카드를 끝내는 신호(silent-syncTOURNAMENT_ITEM_PARSED)가 SSE 단독 채널·비영속이라, 놓치면 카드가 낡은 채 남는다.
  • 1차 설계(PR SSE 재연결 시 놓친 알림 replay (Last-Event-ID 기반) #751, 닫음)는 id 를 알림 PK 에 앵커해 notification 만 replay 했다. 비영속 silent-sync 가 복구 대상에서 빠져 정작 동인을 못 닫았고, "스트림 절반만 신뢰 가능"한 비대칭과 FE 재조회 계약 병행 부담이 남았다. SSE 의 lastEventId 는 스트림당 스칼라 하나라 알림 PK 와 별도 id 공간을 섞을 수도 없다.

Task

  • 모든 SSE 데이터 이벤트(notification·silent-sync)를 균일하게 복구하는 통합 이벤트 로그를 설계·구현한다. FE 는 "놓친 이벤트는 다시 온다, 이벤트 id 로 dedup" 규칙 하나만 가지면 된다.
  • 결정 포인트: 로그 저장소(인메모리 vs Redis vs DB), 상한과 trim 의 상호작용, live 와 replay 의 와이어 동일성, 저장 직렬화의 배포 간 호환.

Action

저장소 결정

후보 배포(blue-green) 재연결 스케일아웃 seq 채번 비고
인메모리 버퍼 전환 순간 전 연결이 동시 재연결되는데 버퍼가 비어 있어 가장 필요한 때 무력 유저별 단조 채번 불가 탈락
Redis Stream (채택) 앱 수명과 분리돼 재연결 직후에도 로그 생존 XADD 가 id 발급을 중앙에서 해결 MySQL 트랜잭션과 비원자적(아래 한계)
DB 통합 로그 생존 AUTO_INCREMENT 로 해결 도메인 커밋과 원자적이나 이벤트당 insert·보존 잡 필요. 논의 끝에 Redis 채택

동작 계약

  • 적재와 id: emit 시점에 유저별 스트림(sse:events:{userId})에 XADD 하고, stream entry id 를 SSE id 필드에 싣는다. 연결이 없어도 적재된다(이후 재연결에서 복구).
  • replay: subscribeLast-Event-ID 헤더(형식 검증, 아니면 첫 연결 취급)를 받아 초과분을 발생 순서대로 그 연결에만 재전송한다. 등록 후 조회 순서로 "유실 대신 중복"을 택하고 클라가 이벤트 id 로 dedup 한다.
  • 상한과 trim: replay 상한 100건 초과 공백은 통째 생략한다(부분 replay 는 뒤에 조용한 구멍을 남긴다). 보관 상한 MAX_LEN(200) 을 REPLAY_LIMIT(100) 보다 크게 둬, trim 으로 중간이 잘린 위험 케이스는 잔존 건수가 반드시 상한을 넘어 같은 생략 분기로 귀결된다 - 구멍 난 구간을 연속인 척 replay 하는 일이 없고, 이 상수 관계는 테스트가 고정한다.
  • degrade: Redis 적재 실패 시 id 없이 live 전송만 한다. id 없는 이벤트는 클라 lastEventId 를 갱신하지 않아 복구 기준점이 오염되지 않는다(connect·하트비트와 같은 원리).
  • 와이어 동일성: payload 를 한 번 직렬화(JSON 문자열)해 live 전송·로그·replay 가 같은 바이트를 공유한다.

구현

  • 신규: SseEventLog(인터페이스) · RedisSseEventLog(XADD + 정확 trim + TTL 24h) · SseReconnectReplayer.
  • LocalSseDelivery 가 직렬화·적재·id 부여를 한 곳(loggedEvent)에 모으고, replay 도 같은 write 경로(sendOrEvict)를 쓴다.
  • 스케일아웃 seam 유지: send 가 Redis publish 로 바뀌면 적재만 발행 측으로 옮기면 된다(인스턴스 수만큼 중복 적재 방지). 주석으로 명시.
  • 문서: notification-sse-spec.md(id 필드·replay 계약·dedup·상한·TTL 창·WEB 자동/APP 저장 예시)와 NotificationSseApi OpenAPI 반영.

검증

  • 통합 테스트(스텁 로그): 재연결 시 notification 과 silent-sync 가 함께 순서대로 replay, 기준점 제외, 상한 초과 통째 생략, 형식 불일치 헤더 무시, 적재 실패 시 id 없는 live 전송.
  • 실 Redis 테스트(RedisSseEventLogIntegrationTest): XADD id 단조·exclusive 조회·limit·정확 trim·TTL, MAX_LEN > REPLAY_LIMIT 관계 고정.
  • 직렬화 호환성 테스트(테스트 컨벤션의 Redis 객체 저장 규약, 이 PR 이 첫 대상): payload 5종 스냅샷 + 스트림 필드 키("name"·"payload") 고정 - blue-green 중 구·신버전이 같은 스트림을 읽는 계약.
  • 부수 정리: 파일별로 중복이던 recording emitter 4개를 support/RecordingSseEmitter 로 통합(SSE 파싱 동기화: 이벤트 배선(AFTER_COMMIT+@Async) 통합 테스트 + RecordingSseEmitter 공유 #578 의 공유 test double 항목). payload 단언이 와이어 JSON 트리 기준으로 바뀌어 직렬화 회귀도 함께 잡는다.

Result

  • 아이템 등록 화면 시나리오가 닫힌다: 카드를 만드는 신호(TOURNAMENT_ITEM_ADDED)와 끝내는 신호(TOURNAMENT_ITEM_PARSED)가 모두 재연결 replay 로 복구된다. 웹(EventSource)은 클라 변경 없이 동작하고, 앱은 마지막 이벤트 id 저장·전송만 추가하면 된다.
  • additive 라 서버 단독 선배포가 가능하다. 헤더를 안 보내는 기존 클라이언트는 지금과 동일하게 동작한다.
  • 스냅샷 실측 중 발견한 기존 문서 버그를 함께 정정했다: createdAtJacksonConfig 의 KST 변환으로 +09:00 오프셋 포함 형식으로 나가는데, 스펙 문서가 "오프셋 없음"으로 낡아 있었다.
  • 남는 한계(스펙·이슈에 명시): Redis 적재는 MySQL 트랜잭션과 원자적이지 않아, emit 경로에 도달하지 못한 이벤트(executor 태스크 거부 등)는 로그에도 없다. 그 틈과 상한·TTL 초과 공백은 기존 재조회 fallback 이 복구를 책임진다. 재연결 시 목록/배지 재조회 병행 권장은 그대로 유지된다.

연관 이슈

Summary by CodeRabbit

  • 새 기능
    • SSE 재연결 시 Last-Event-ID를 기준으로 누락된 notificationsilent-sync를 발생 순서대로 재전송합니다(이벤트 ID 포함).
    • 재생은 최대 범위가 있으며, 범위 초과 시 재전송을 건너뜁니다.
    • 재연결 이벤트는 수신 측에서 중복 제거가 가능하도록 설계되었습니다.
  • 문서
    • SSE 재연결/재생 규칙, 이벤트 ID·createdAt 형식 및 재생 대상 판정 기준을 명확히 했습니다.
  • 테스트
    • 재연결 replay, 이벤트 순서/상한, 중복 제거, 페이로드 직렬화 호환성 및 Redis 스트림 동작을 검증하는 케이스를 추가/갱신했습니다.

- 유저별 Redis Stream(sse:events:{userId})을 SSE 이벤트 로그로 두고 notification·silent-sync 를 emit 시점에 적재한다. stream entry id 를 SSE id 필드에 실어, 재연결 시 Last-Event-ID 초과분을 종류 구분 없이 원본 그대로 replay 한다
- 1차 설계(알림 PK 앵커, PR #751 닫음)는 비영속 silent-sync 가 복구 대상에서 빠져 이 작업의 동인(아이템 등록 화면 파싱 동기화 유실)을 못 닫았다. SSE 의 lastEventId 는 스트림당 스칼라 하나라 두 id 공간을 섞을 수 없어 통합 이벤트 로그로 전환
- 저장소를 인메모리가 아닌 Redis 에 둔 이유: blue-green 배포 전환이 곧 전 연결 동시 재연결 시점인데 인메모리 버퍼는 그 순간 비어 있고, 스케일아웃 시 유저별 단조 id 채번도 중앙 저장소가 필요하다
- payload 를 한 번 직렬화(JSON 문자열)해 live 전송·로그·replay 가 같은 바이트를 공유한다. Redis 객체 저장 규약대로 직렬화 호환성 테스트(payload 스냅샷 + 스트림 필드 키 고정)를 함께 둔다
- 상한 초과 공백은 replay 통째 생략(부분 replay 는 뒤에 조용한 구멍을 남긴다). MAX_LEN(200) > REPLAY_LIMIT(100) 관계로 trim 이 만든 구멍을 연속인 척 replay 하는 경우를 배제하고, 관계 자체를 테스트로 고정
- 적재 실패는 id 없이 live 전송만 하는 degrade — id 없는 이벤트는 클라 lastEventId 를 갱신하지 않아 복구 기준점을 오염시키지 않는다
- 스냅샷 실측 중 발견: createdAt 은 JacksonConfig 의 KST 변환(+09:00 오프셋)으로 나가는데 스펙 문서는 "오프셋 없음"으로 낡아 있었다. 실측대로 정정
- 파일별로 중복이던 recording emitter 4개를 support/RecordingSseEmitter 로 통합(#578 의 공유 test double 항목). 와이어 JSON 트리 단언으로 바뀌어 직렬화 회귀도 함께 잡는다
@m-a-king m-a-king added the feat 외부 가시적 새 기능 label Jul 17, 2026
@m-a-king m-a-king self-assigned this Jul 17, 2026
@github-actions

Copy link
Copy Markdown

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

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: 85430d42-9889-4809-ac28-10dafcfb92d9

📥 Commits

Reviewing files that changed from the base of the PR and between 3e8e702 and 0e645a6.

📒 Files selected for processing (2)
  • notification-sse-spec.md
  • src/main/kotlin/com/depromeet/piki/notification/sse/RedisSseEventLog.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/kotlin/com/depromeet/piki/notification/sse/RedisSseEventLog.kt
  • notification-sse-spec.md

Walkthrough

SSE notification과 silent-sync 이벤트에 Redis Stream 로그와 Last-Event-ID 기반 replay를 추가했다. 라이브·replay payload 직렬화를 통일하고, replay 상한·장애 degrade·클라이언트 dedup 규칙과 관련 통합 테스트 및 명세를 갱신했다.

Changes

SSE 재연결 replay

Layer / File(s) Summary
이벤트 로그 계약과 Redis 저장
src/main/kotlin/com/depromeet/piki/notification/sse/*, src/test/kotlin/com/depromeet/piki/notification/sse/RedisSseEventLogIntegrationTest.kt, src/test/kotlin/com/depromeet/piki/notification/sse/SsePayloadSerializationCompatibilityTest.kt, src/test/resources/sse-payload/*
SseEventLogSseEventRecord를 추가하고, 사용자별 Redis Stream에 이벤트를 저장·조회·trim·TTL 관리한다. 운영 payload와 저장 payload의 JSON 계약을 스냅샷으로 검증한다.
이벤트 전달과 replay 오케스트레이션
src/main/kotlin/com/depromeet/piki/notification/sse/LocalSseDelivery.kt, src/main/kotlin/com/depromeet/piki/notification/sse/SseReconnectReplayer.kt, src/test/kotlin/com/depromeet/piki/support/*
notification과 silent-sync에 저장된 SSE id를 부여하고, 최대 100건까지 원본 이벤트를 replay한다. 로그 저장 실패 시 id 없는 라이브 전송으로 동작하며 emitter 전송 실패 시 replay를 중단한다.
구독 API와 재연결 흐름
src/main/kotlin/com/depromeet/piki/notification/controller/*, src/test/kotlin/com/depromeet/piki/notification/sse/NotificationSseIntegrationTest.kt
선택적 Last-Event-ID 헤더를 구독 API에 추가하고, 유효한 Stream entry id에 대해서만 connect 이후 누락 이벤트를 재전송한다. 형식 오류, replay 없음, 상한 초과, 저장 실패 시나리오를 검증한다.
클라이언트 계약과 회귀 검증
notification-sse-spec.md, src/test/kotlin/com/depromeet/piki/notification/service/*, src/test/kotlin/com/depromeet/piki/notification/sse/TournamentItemParsedSseIntegrationTest.kt
웹·앱 클라이언트의 Last-Event-ID 및 이벤트 id dedup 규칙을 문서화하고, SSE 테스트를 실제 wire JSON과 id 기반 검증으로 전환했다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant NotificationSseController
  participant SseReconnectReplayer
  participant RedisSseEventLog
  participant LocalSseDelivery
  Client->>NotificationSseController: Last-Event-ID 헤더로 재연결
  NotificationSseController->>SseReconnectReplayer: 누락 이벤트 replay 요청
  SseReconnectReplayer->>RedisSseEventLog: 기준 id 이후 이벤트 조회
  RedisSseEventLog-->>SseReconnectReplayer: 이벤트 레코드 반환
  SseReconnectReplayer->>LocalSseDelivery: 이벤트 재전송
  LocalSseDelivery-->>Client: id/name/payload 순서대로 전달
Loading

Assessment against linked issues

Objective Addressed Explanation
[ #750 ] 사용자별 Redis Stream에 notification·silent-sync를 저장하고 Last-Event-ID 이후 원본 순서로 replay
[ #750 ] replay 상한 초과 시 생략하고 MAXLEN·TTL을 적용
[ #750 ] 라이브·replay JSON 직렬화 통일 및 Redis 장애 시 id 없는 라이브 전송
[ #750 ] 클라이언트 dedup 계약, OpenAPI·명세 갱신 및 통합 테스트
🚥 Pre-merge checks | ✅ 1 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/750-sse-redis-stream-replay

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/kotlin/com/depromeet/piki/notification/controller/NotificationSseController.kt (1)

36-52: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

재연결 전환 구간에서 live/replay 순서가 원자적으로 보장되지 않습니다. 최신 라이브 이벤트 뒤에 과거 replay가 적용되면 화면 상태와 Last-Event-ID 커서가 함께 역행할 수 있습니다.

  • src/main/kotlin/com/depromeet/piki/notification/controller/NotificationSseController.kt#L36-L52: replay 완료 전 라이브 이벤트를 버퍼링하고 replay 이후 ID 순서로 flush하도록 전환 과정을 직렬화하세요.
  • src/test/kotlin/com/depromeet/piki/notification/sse/NotificationSseIntegrationTest.kt#L329-L370: replay 도중 라이브 발행을 끼워 넣어 ID 순서와 최종 최신 상태를 검증하세요.
  • notification-sse-spec.md#L320-L323: Last-Event-ID는 현재 값보다 큰 Stream ID일 때만 저장하도록 예시를 수정하세요.

경로 지침의 동시성·데이터 정합성 우선 원칙과 PR의 발생 순서 보장 계약에 따른 검토입니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/kotlin/com/depromeet/piki/notification/controller/NotificationSseController.kt`
around lines 36 - 52, Update NotificationSseController’s reconnect flow so live
events are buffered while replayer.replayMissed completes, then flushed in
Stream ID order before normal live delivery resumes; preserve registration
before replay and unregister safely on failure. In
src/test/kotlin/com/depromeet/piki/notification/sse/NotificationSseIntegrationTest.kt
lines 329-370, add coverage that publishes during replay and verifies ordered
IDs and the final latest state. In notification-sse-spec.md lines 320-323,
revise the Last-Event-ID example to persist only IDs greater than the current
stored Stream ID.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/kotlin/com/depromeet/piki/notification/sse/LocalSseDelivery.kt`:
- Around line 63-76: Update the SSE delivery flow around subscribe(),
replayTo(), deliver(), and deliverSilentSync() so each user’s register → replay
→ live fan-out sequence is serialized through a shared per-user lock or queue.
Ensure live sends cannot interleave with replay on the same emitter, while
preserving existing eviction and delivery behavior.

In `@src/main/kotlin/com/depromeet/piki/notification/sse/RedisSseEventLog.kt`:
- Around line 38-48: RedisSseEventLog의 이벤트 적재 흐름을 수정해 XADD, XTRIM, EXPIRE를 하나의
Lua 스크립트에서 원자적으로 수행하고 생성된 record ID를 반환하도록 하세요. XADD 이후 housekeeping 단계의 실패를 전체
적재 실패로 처리해 null을 반환하지 않도록 하며, live 이벤트 전송에 실제 Stream ID가 항상 사용되게 하세요.

---

Outside diff comments:
In
`@src/main/kotlin/com/depromeet/piki/notification/controller/NotificationSseController.kt`:
- Around line 36-52: Update NotificationSseController’s reconnect flow so live
events are buffered while replayer.replayMissed completes, then flushed in
Stream ID order before normal live delivery resumes; preserve registration
before replay and unregister safely on failure. In
src/test/kotlin/com/depromeet/piki/notification/sse/NotificationSseIntegrationTest.kt
lines 329-370, add coverage that publishes during replay and verifies ordered
IDs and the final latest state. In notification-sse-spec.md lines 320-323,
revise the Last-Event-ID example to persist only IDs greater than the current
stored Stream ID.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6838978d-846c-44b2-afa6-e99f8ef4a27c

📥 Commits

Reviewing files that changed from the base of the PR and between 9521ce3 and 3e8e702.

📒 Files selected for processing (21)
  • notification-sse-spec.md
  • src/main/kotlin/com/depromeet/piki/notification/controller/NotificationSseApi.kt
  • src/main/kotlin/com/depromeet/piki/notification/controller/NotificationSseController.kt
  • src/main/kotlin/com/depromeet/piki/notification/sse/LocalSseDelivery.kt
  • src/main/kotlin/com/depromeet/piki/notification/sse/RedisSseEventLog.kt
  • src/main/kotlin/com/depromeet/piki/notification/sse/SseEventLog.kt
  • src/main/kotlin/com/depromeet/piki/notification/sse/SseReconnectReplayer.kt
  • src/test/kotlin/com/depromeet/piki/notification/service/NotificationBadgeSyncAsyncIntegrationTest.kt
  • src/test/kotlin/com/depromeet/piki/notification/service/NotificationCleanupSchedulerIntegrationTest.kt
  • src/test/kotlin/com/depromeet/piki/notification/sse/NotificationSseIntegrationTest.kt
  • src/test/kotlin/com/depromeet/piki/notification/sse/RedisSseEventLogIntegrationTest.kt
  • src/test/kotlin/com/depromeet/piki/notification/sse/SsePayloadSerializationCompatibilityTest.kt
  • src/test/kotlin/com/depromeet/piki/notification/sse/TournamentItemParsedSseIntegrationTest.kt
  • src/test/kotlin/com/depromeet/piki/support/IntegrationStubs.kt
  • src/test/kotlin/com/depromeet/piki/support/RecordingSseEmitter.kt
  • src/test/kotlin/com/depromeet/piki/support/StubSseEventLog.kt
  • src/test/resources/sse-payload/notification-reference.json
  • src/test/resources/sse-payload/notification-tournament-routed.json
  • src/test/resources/sse-payload/notification-wish-parsing.json
  • src/test/resources/sse-payload/silent-sync-tournament-item-parsed.json
  • src/test/resources/sse-payload/silent-sync-unread-count-changed.json

Comment thread src/main/kotlin/com/depromeet/piki/notification/sse/RedisSseEventLog.kt Outdated
@github-actions
github-actions Bot requested a review from sevineleven July 17, 2026 05:32
- XADD 성공 뒤 trim·EXPIRE 만 실패해도 null 을 돌려줘, 로그엔 남았는데 live 는 id 없이 나가는 불일치가 있었다 (이후 더 큰 id 를 커서로 잡은 클라이언트는 그 레코드를 replay 로도 못 받는다) — CodeRabbit 지적 수용
- 적재(XADD) 실패만 degrade(null) 로 처리하고, 정리(trim·TTL)는 별도 격리해 실패해도 id 를 반환한다. trim·TTL 은 멱등 정리라 다음 적재가 자연 재시도한다
- 제안된 Lua 원자화는 반영하지 않았다 — id 만 살리면 정리의 원자성은 정확성에 불필요하고 스크립트 유지 비용만 남는다
- 스펙 APP 예시의 커서 저장을 "지금보다 큰 id 일 때만" 으로 보강 — 순서 어긋난 도착에도 커서가 역행하지 않아 다음 재연결의 중복 replay 가 준다
@m-a-king

m-a-king commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator Author

CodeRabbit review body 의 Outside diff range 코멘트(NotificationSseController.kt L36-52 — 재연결 전환 구간 live 버퍼링·직렬화) 처리 기록:

  • live 버퍼링·직렬화는 미반영(reject) — 인라인 thread(LocalSseDelivery) reply 와 같은 사유. 경합의 통상 결과는 중복(이벤트 id dedup 계약이 흡수)이고, 잔여 유실 케이스는 spec 이 재조회 fallback 에 위임한 부류다. user 단위 버퍼·락은 blocking emitter IO 와 결합해 liveness 위험(SSE 라이브 동기화를 알림 executor 에서 분리 + 동기화 실패 관측 #577 결)이 더 크다.
  • 커서 저장 힌트는 반영(accept) — spec APP 예시를 "지금보다 큰 id 일 때만 커서 저장" 으로 보강해 순서 어긋난 도착에도 커서가 역행하지 않게 했다. Fixed in 0e645a68.

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@m-a-king 확인했습니다. 명시된 dedup 및 목록·배지 재조회 fallback 계약 아래에서는 재연결 전환 구간을 위해 user 단위 버퍼/락을 추가할 필요가 없으며, blocking SseEmitter I/O와 결합했을 때의 liveness 리스크가 더 큰 트레이드오프입니다. 해당 코멘트는 철회하겠습니다.

0e645a68더 큰 이벤트 ID에만 커서를 저장하는 보강도 순서가 어긋난 도착으로 커서가 역행하는 문제를 막습니다. 좋은 마무리입니다. 🐇


✏️ Learnings added
Learnt from: m-a-king
URL: https://github.com/TeamPiKi/core/pull/756

Timestamp: 2026-07-19T06:59:39.040Z
Learning: `NotificationSseController` 재연결 전환 구간에서 user 단위 live-event 버퍼링이나 락 기반 직렬화는 의도적으로 적용하지 않는다. 중복 전달은 SSE event ID 기반 클라이언트 dedup 계약으로 처리하고, 경합으로 인한 잔여 유실 및 보존 범위 밖 이벤트는 목록·배지 재조회 fallback이 처리한다. `SseEmitter`의 blocking I/O와 per-user lock을 결합하면 `#577과` 같은 liveness 위험이 커진다.

Learnt from: m-a-king
URL: https://github.com/TeamPiKi/core/pull/756

Timestamp: 2026-07-19T06:59:39.040Z
Learning: SSE 앱 클라이언트는 순서가 어긋난 이벤트 도착으로 `Last-Event-ID` 커서가 역행하지 않도록, 수신 이벤트 ID가 현재 저장된 ID보다 클 때만 커서를 갱신해야 한다.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@m-a-king m-a-king closed this Jul 20, 2026
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 이벤트 통합 replay - Redis Stream 기반 Last-Event-ID 복구

1 participant