Skip to content

[feat][queue-service] Server-driven Adaptive Polling 도입#35

Merged
rlaxxwls13 merged 2 commits into
devfrom
feat/34-adaptive-polling
May 19, 2026
Merged

[feat][queue-service] Server-driven Adaptive Polling 도입#35
rlaxxwls13 merged 2 commits into
devfrom
feat/34-adaptive-polling

Conversation

@rlaxxwls13
Copy link
Copy Markdown
Contributor

@rlaxxwls13 rlaxxwls13 commented May 19, 2026

🌱 설명

이 PR에서 어떤 작업을 했는지 간략하게 설명해주세요.

응답 DTO 에 retryAfterMs 필드를 추가하여 Server-driven Adaptive Polling 도입.
서버가 사용자 순번에 따라 폴링 간격을 동적으로 제어하여 1:N fan-out 구조의 대기열에서 폴링 부하를 효율적으로 분산.

  • PollingIntervalPolicy 클래스 추가 (Presentation 레이어)
    • 순번별 차등 적용: 임박(1초) / 중간(5초) / 후순위(15초) / 최후순위(30초)
    • Jitter 적용으로 동시 요청 분산 (Thundering Herd 방지)
  • QueueTokenResponseretryAfterMs 필드 추가
  • QueueTokenController 에서 Policy 주입 및 응답 생성
  • 테스트 코드 + RestDocs 갱신
  • k6 부하 테스트 스크립트 추가 (before / after 비교)

📌 관련 이슈

이 PR과 연관된 이슈 번호를 작성해주세요. (이슈 없으면 생략 가능)
close #34

💻 커밋 유형

해당하는 항목에 x를 채워주세요.

  • feat : 새로운 기능 추가
  • fix : 버그 수정 (긴급 핫픽스 포함)
  • refactor : 리팩토링
  • chore : 빌드 설정, 의존성 업데이트 등
  • docs : 문서 수정
  • comment : 주석 추가 및 변경
  • rename : 파일 / 폴더 이동 또는 이름 변경
  • remove : 파일 삭제
  • test : 테스트 코드 추가 / 수정

📝 체크리스트

PR 올리기 전에 아래 항목을 확인해주세요.

  • develop 브랜치 기준으로 feature 브랜치를 생성했나요?
  • 코드 컨벤션 및 스타일 가이드를 준수했나요?
  • 관련 이슈와 연결했나요?
  • 설명이 필요한 부분 / 어려운 부분 / 공유가 필요한 부분에 주석을 추가했습니다.
  • 제가 작성한 코드를 스스로 리뷰했습니다.
  • 기존 테스트와 충돌하지 않음을 확인했나요?

📚 추가 설명

리뷰어가 참고해야 할 내용, 첨부 이미지 등이 있다면 자유롭게 추가해주세요. (선택)

API 응답 변경

Before

{
  "tokenId": "...",
  "status": "WAITING",
  "issuedAt": "...",
  "position": 234
}

After

{
  "tokenId": "...",
  "status": "WAITING",
  "issuedAt": "...",
  "position": 234,
  "retryAfterMs": 5300
}

ADMITTED 등 큐에서 빠진 상태(position = null)에서는 retryAfterMs = null@JsonInclude(NON_NULL) 로 응답에서 제외됨 (클라이언트 폴링 종료 신호).

부하 테스트 결과 (k6, Vuser 3000, Duration 3분)

지표 Before After 개선율
폴링 TPS 883.7/s 588.3/s -33.5%
평균 응답시간 11.86ms 3.95ms -66.7%
p95 응답시간 28.37ms 6.55ms -76.9%
에러율 0% 0% 유지

폴링 부하 1/3 감소, 응답시간 65% 이상 개선 확인.


Summary by CodeRabbit

릴리스 노트

  • New Features

    • 대기열 응답에 retryAfterMs 필드 추가 - 서버가 권장하는 다음 폴링 대기 시간을 동적으로 제공합니다.
    • Adaptive Polling 방식 지원 - 대기 순번에 따라 폴링 간격이 자동으로 조정됩니다.
  • Documentation

    • 대기 정보 응답 스펙 문서 업데이트 - 새로운 필드 및 폴링 권장 간격 가이드 추가.

Review Change Stack

- 응답 DTO 에 retryAfterMs 필드 추가
- PollingIntervalPolicy 클래스 추가 (Presentation 레이어)
  - 순번별 차등 폴링 간격 (1초 / 5초 / 15초 / 30초)
  - Jitter 적용으로 동시 요청 분산
- Controller 에서 Policy 주입 및 응답 생성
- 테스트 코드 + RestDocs 갱신
- 부하 테스트 스크립트 (k6) 추가: before/after 비교

Related to #34
@rlaxxwls13 rlaxxwls13 self-assigned this May 19, 2026
@rlaxxwls13 rlaxxwls13 linked an issue May 19, 2026 that may be closed by this pull request
11 tasks
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 19, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository: first-ticket/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: db808273-56fb-450b-8364-70290a7906d6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

이 PR은 서버 권장 폴링 간격을 응답에 포함하는 Adaptive Polling 기능을 구현합니다. 큐 위치별 동적 계산 정책, 응답 계약 변경, 컨트롤러 통합, API 문서화, 부하 테스트 스크립트, 그리고 테스트 커버리지를 함께 업데이트합니다.

Changes

Adaptive Polling Implementation

Layer / File(s) Summary
Polling Interval Policy
src/main/java/com/firstticket/queueservice/queuetoken/presentation/PollingIntervalPolicy.java
nextRetryAfterMs(Long position)이 사용자 순번 범위별 기본 interval(1s30s)과 jitter(0±4000ms)를 조합해 다음 폴링 대기 시간을 계산하며, position == null일 때는 폴링 불필요를 나타내기 위해 null을 반환합니다.
API Response Contract
src/main/java/com/firstticket/queueservice/queuetoken/presentation/dto/QueueTokenResponse.java
QueueTokenResponse record에 entryTokenretryAfterMs 필드를 추가하고, 팩토리 메서드를 from(QueueTokenResult result, Integer retryAfterMs) 시그니처로 갱신하여 폴링 정보를 응답에 포함합니다.
Controller Integration
src/main/java/com/firstticket/queueservice/queuetoken/presentation/QueueTokenController.java
PollingIntervalPolicy 의존성을 주입하고, POST/GET 엔드포인트에서 각각 result.position() 기반 retryAfterMs를 계산해 QueueTokenResponse.from(result, retryAfterMs)로 응답을 생성하도록 변경합니다.
API Documentation
src/docs/asciidoc/index.adoc
대기 정보 응답 예시 JSON에 positionretryAfterMs 필드를 추가하고, Adaptive Polling 전략(순번 구간별 권장 폴링 간격 및 jitter 범위)과 ADMITTED 상태에서 retryAfterMs 제외 동작을 설명하는 섹션을 추가합니다.
Load Test Scripts
scripts/before.js, scripts/after.js
VU별 고유 userId로 1회 enqueue 후 폴링을 반복하는 k6 스크립트 두 버전을 추가합니다. before.js는 고정 3초 sleep으로 기존 동작을 시뮬레이션하고, after.js는 응답 retryAfterMs를 파싱해 동적 sleep 시간을 적용합니다.
Test Coverage
src/test/java/com/firstticket/queueservice/queuetoken/presentation/QueueTokenControllerTest.java
PollingIntervalPolicy@MockitoBean으로 모킹하고, 성공 케이스(enqueue 성공, queue info GET, ADMITTED 상태)와 에러 케이스(인증 실패 401, 중복 409, not found 404, invalid state 400)의 REST Docs 스펙을 정비하여 retryAfterMs 필드 포함 여부를 문서화합니다.

Sequence Diagram

sequenceDiagram
  participant Client as 클라이언트
  participant Controller as QueueTokenController
  participant Policy as PollingIntervalPolicy
  participant Response as QueueTokenResponse
  Client->>Controller: POST /api/v1/queues/programs/{id}
  Controller->>Controller: 프로그램 큐 enqueue 처리
  Controller->>Policy: nextRetryAfterMs(position)
  Policy-->>Controller: 계산된 retryAfterMs 반환
  Controller->>Response: from(result, retryAfterMs)
  Response-->>Controller: 응답 객체 생성
  Controller-->>Client: 200 OK {position, retryAfterMs, ...}
  Client->>Client: retryAfterMs 밀리초 대기
  Client->>Controller: GET /api/v1/queues/programs/{id}
  Controller->>Controller: 토큰 상태 조회
  Controller->>Policy: nextRetryAfterMs(position)
  Policy-->>Controller: 계산된 retryAfterMs 반환
  Controller->>Response: from(result, retryAfterMs)
  Response-->>Controller: 응답 객체 생성
  alt WAITING 상태
    Controller-->>Client: {position, retryAfterMs, ...}
    Client->>Client: retryAfterMs 밀리초 대기
  else ADMITTED 상태
    Controller-->>Client: {entryToken, retryAfterMs: null, ...}
    Client->>Client: 폴링 중단, entryToken으로 입장
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • first-ticket/queue-service#16: 메인 PR이 src/docs/asciidoc/index.adoc를 수정해 retryAfterMs와 polling 흐름을 문서화하는 반면, 해당 PR은 REST Docs 스니펫 생성 및 Asciidoc 문서 작성 파이프라인을 추가하여 동일 문서 산출물과 직접 통합됩니다.

Suggested labels

▶️ test

Suggested reviewers

  • sweetRainShin
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.46% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 Server-driven Adaptive Polling 도입이라는 핵심 변경사항을 명확하게 반영하고 있으며, 변경 사항의 주요 목적을 잘 요약하고 있습니다.
Linked Issues check ✅ Passed PR의 모든 코드 변경사항이 이슈 #34의 요구사항을 충족합니다. retryAfterMs 필드 추가, 위치별 동적 계산, jitter 적용, ADMITTED 상태 null 처리, 테스트 및 문서 갱신, k6 스크립트 수정이 모두 포함되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 Server-driven Adaptive Polling 도입(이슈 #34)과 직접 관련되어 있으며, 범위를 벗어난 변경은 없습니다.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/34-adaptive-polling

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@rlaxxwls13 rlaxxwls13 added the 💻 feature 새로운 기능 개발 label May 19, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
scripts/after.js (2)

22-26: ⚡ Quick win

Adaptive 시나리오도 enqueue 성공을 체크해 주세요.

Line 22-26에서 enqueue 결과 검증이 없어, 진입 실패 상황이 동적 폴링 효과 측정에 섞일 수 있습니다.

제안 diff
-        http.post(
+        const enqueueRes = http.post(
             `${BASE_URL}/api/v1/queues/programs/${PROGRAM_ID}`,
             null,
             { headers: { 'X-User-Id': userIdsByVU[__VU] }, tags: { name: 'enqueue' } }
         );
+        check(enqueueRes, { 'enqueue 201': (r) => r.status === 201 });
🤖 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 `@scripts/after.js` around lines 22 - 26, The current http.post call that
enqueues programs (the expression using BASE_URL, PROGRAM_ID, userIdsByVU[__VU],
tags: { name: 'enqueue' }) does not verify success; update the code to capture
the response from http.post, check its HTTP status (e.g., 200/201 or expected
success code) and/or response body for an enqueue success indicator, and handle
failures by recording/logging the error and failing the VU iteration (or
incrementing a failure metric) so adaptive scenario runs don't mix entry
failures into performance measurements; ensure the check surrounds the same
http.post invocation and uses the same identifiers (BASE_URL, PROGRAM_ID,
userIdsByVU, __VU, tags: { name: 'enqueue' }).

44-46: ⚡ Quick win

retryAfterMs 값의 하한 검증을 추가해 과도한 루프를 방지해 주세요.

Line 44-46에서 0 이하/비정상 값이 들어오면 즉시 재요청 루프가 발생할 수 있어, 기본값(3초) 폴백 조건을 두는 편이 안전합니다.

제안 diff
-            if (retryAfterMs != null) {
+            if (typeof retryAfterMs === 'number' && retryAfterMs > 0) {
                 sleepSec = retryAfterMs / 1000;
             }
🤖 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 `@scripts/after.js` around lines 44 - 46, The current assignment of sleepSec
from retryAfterMs (retryAfterMs -> sleepSec) can produce immediate/very short
loops if retryAfterMs is 0, negative, or non-numeric; modify the logic around
retryAfterMs so you validate its lower bound and fall back to a safe default
(e.g., 3000 ms / 3 sec) when retryAfterMs is null/undefined, <= 0, or NaN, then
set sleepSec = validatedRetryAfterMs / 1000; ensure you reference and update the
same variables (retryAfterMs and sleepSec) and keep the default value constant
for readability.
scripts/before.js (1)

25-29: ⚡ Quick win

enqueue 응답도 검증해서 기준(Before) 지표 신뢰도를 보장해주세요.

Line 25-29에서 enqueue 성공 여부를 확인하지 않으면, 초기 진입 실패가 폴링 결과에 섞여 Before/After 비교가 왜곡될 수 있습니다.

제안 diff
-        http.post(
+        const enqueueRes = http.post(
             `${BASE_URL}/api/v1/queues/programs/${PROGRAM_ID}`,
             null,
             { headers: { 'X-User-Id': userIdsByVU[__VU] }, tags: { name: 'enqueue' } }
         );
+        check(enqueueRes, { 'enqueue 201': (r) => r.status === 201 });
🤖 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 `@scripts/before.js` around lines 25 - 29, 현재 http.post call that enqueues the
program (the call using BASE_URL, PROGRAM_ID, userIdsByVU[__VU], tag 'enqueue')
does not validate the response; ensure you capture the response from http.post,
check the HTTP status (e.g., status >=200 && status <300) and/or response body
for success, and if it is not successful log the response details and fail/exit
or throw so the before step does not silently continue; add this validation
immediately after the http.post invocation and include the request context
(PROGRAM_ID, __VU) in the log.
🤖 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/test/java/com/firstticket/queueservice/queuetoken/presentation/QueueTokenControllerTest.java`:
- Line 109: The test docs mark data.retryAfterMs as optional but it is always
present for a successful queue-token-issue (WAITING) response; locate the
fieldWithPath("data.retryAfterMs") call in QueueTokenControllerTest and remove
the .optional() so the field is documented as required (keep the same
description text), ensuring the contract reflects the actual response.

---

Nitpick comments:
In `@scripts/after.js`:
- Around line 22-26: The current http.post call that enqueues programs (the
expression using BASE_URL, PROGRAM_ID, userIdsByVU[__VU], tags: { name:
'enqueue' }) does not verify success; update the code to capture the response
from http.post, check its HTTP status (e.g., 200/201 or expected success code)
and/or response body for an enqueue success indicator, and handle failures by
recording/logging the error and failing the VU iteration (or incrementing a
failure metric) so adaptive scenario runs don't mix entry failures into
performance measurements; ensure the check surrounds the same http.post
invocation and uses the same identifiers (BASE_URL, PROGRAM_ID, userIdsByVU,
__VU, tags: { name: 'enqueue' }).
- Around line 44-46: The current assignment of sleepSec from retryAfterMs
(retryAfterMs -> sleepSec) can produce immediate/very short loops if
retryAfterMs is 0, negative, or non-numeric; modify the logic around
retryAfterMs so you validate its lower bound and fall back to a safe default
(e.g., 3000 ms / 3 sec) when retryAfterMs is null/undefined, <= 0, or NaN, then
set sleepSec = validatedRetryAfterMs / 1000; ensure you reference and update the
same variables (retryAfterMs and sleepSec) and keep the default value constant
for readability.

In `@scripts/before.js`:
- Around line 25-29: 현재 http.post call that enqueues the program (the call using
BASE_URL, PROGRAM_ID, userIdsByVU[__VU], tag 'enqueue') does not validate the
response; ensure you capture the response from http.post, check the HTTP status
(e.g., status >=200 && status <300) and/or response body for success, and if it
is not successful log the response details and fail/exit or throw so the before
step does not silently continue; add this validation immediately after the
http.post invocation and include the request context (PROGRAM_ID, __VU) in the
log.
🪄 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: Repository: first-ticket/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b49d6b1b-3124-485a-80ea-090502a598f5

📥 Commits

Reviewing files that changed from the base of the PR and between be21d54 and 7718baa.

📒 Files selected for processing (7)
  • scripts/after.js
  • scripts/before.js
  • src/docs/asciidoc/index.adoc
  • src/main/java/com/firstticket/queueservice/queuetoken/presentation/PollingIntervalPolicy.java
  • src/main/java/com/firstticket/queueservice/queuetoken/presentation/QueueTokenController.java
  • src/main/java/com/firstticket/queueservice/queuetoken/presentation/dto/QueueTokenResponse.java
  • src/test/java/com/firstticket/queueservice/queuetoken/presentation/QueueTokenControllerTest.java

@rlaxxwls13 rlaxxwls13 moved this from Todo to Done in First Ticket May 19, 2026
@rlaxxwls13 rlaxxwls13 merged commit 592fcee into dev May 19, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💻 feature 새로운 기능 개발

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[feat][queue-service] adaptive polling 도입

2 participants