Skip to content

[refactor][queue-service] Lua Script + INCR 기반 tie-breaker 도입#36

Merged
rlaxxwls13 merged 5 commits into
devfrom
refactor/11-lua-script
May 19, 2026
Merged

[refactor][queue-service] Lua Script + INCR 기반 tie-breaker 도입#36
rlaxxwls13 merged 5 commits into
devfrom
refactor/11-lua-script

Conversation

@rlaxxwls13
Copy link
Copy Markdown
Contributor

@rlaxxwls13 rlaxxwls13 commented May 19, 2026

🌱 설명

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

Redis 자료구조의 원자성 강화 및 동시 진입 FIFO 보장을 위한 리팩토링

  • 기존 MULTI/EXEC 기반 enqueue / delete 로직을 Lua Script 로 전환하여 단일 호출의 원자성 보장
  • 같은 ms 에 동시 진입 시 발생하는 score 충돌을 막기 위해 INCR 기반 tie-breaker 도입 (FIFO 보장)
  • deleteAllByProgramId 에 compare-and-delete 적용 (동시 발급된 토큰의 역인덱스 보호)

📌 관련 이슈

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

💻 커밋 유형

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

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

📝 체크리스트

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

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

📚 추가 설명

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

  • 예) 라이브러리 버전을 올리면서 설정 방식이 변경되었습니다.

Summary by CodeRabbit

릴리스 노트

  • Refactor
    • 등록/삭제/프로그램별 일괄삭제 흐름을 Lua 실행으로 원자화해 동시성 안정성·중복 방지·일관성 향상
    • 순번·타임스탬프 기반 순서 결정 및 토큰 만료(TTL) 처리 일관성 개선
  • New Features
    • 프로그램 단위 토큰 인덱스 관리로 대량 삭제 및 조회 효율성 향상
  • Documentation
    • 중복 처리, 원자성, 역인덱스 동작 관련 주석 및 문서 갱신

Review Change Stack

- enqueue.lua / delete.lua: SETNX + ZADD + HSET 을 단일 Lua 스크립트로 통합 (원자성)
- score 생성: epoch_milli * 1000000 + INCR 시퀀스 (동시 진입 FIFO 보장)
- RedisScriptConfig: DefaultRedisScript 빈 등록
- RedisQueueTokenRepository: Lua 스크립트 기반 enqueue/delete 정정
- deleteAllByProgramId: 역인덱스 compare-and-delete (동시 발급 토큰 보호) + seq 키 정리

Closes #9, #11
@rlaxxwls13 rlaxxwls13 self-assigned this May 19, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 19, 2026

📝 Walkthrough

Walkthrough

enqueue/delete/deleteAllByProgramId를 Redis Lua 스크립트로 전환하고 스크립트를 Spring 빈으로 등록해 엔큐·삭제·프로그램 전체 삭제를 Redis에서 원자적으로 처리하도록 변경했다.

Changes

Lua 스크립트 기반 원자 처리 통합

Layer / File(s) Summary
Lua 스크립트 및 설정
src/main/resources/lua/enqueue.lua, src/main/resources/lua/delete.lua, src/main/resources/lua/deleteAllByProgram.lua, src/main/java/com/firstticket/queueservice/queuetoken/infrastructure/redis/RedisScriptConfig.java
enqueue.lua/delete.lua/deleteAllByProgram.lua 스크립트 추가 및 DefaultRedisScript<Long> 빈 3개 등록.
Enqueue 전환 및 생성자/seqKey 변경
src/main/java/com/firstticket/queueservice/queuetoken/infrastructure/redis/RedisQueueTokenRepository.java
Repository 생성자 시 Lua 스크립트 빈(enqueueScript, deleteScript, deleteAllByProgramScript) 주입으로 변경, enqueueenqueueScript 호출로 전환되고 SEQ_KEY_PREFIX/seqKey() 헬퍼가 추가됨. 스크립트 반환값이 null 또는 0이면 DuplicateTokenException을 던짐.
Delete 전환
src/main/java/com/firstticket/queueservice/queuetoken/infrastructure/redis/RedisQueueTokenRepository.java, src/main/resources/lua/delete.lua
delete가 Java 레벨의 GET/compare/MULTI/EXEC 삭제에서 deleteScript 실행으로 전환되어 userProgramKey 비교 후 삭제 + ZREM, DEL tokenHash, SREM programTokens을 원자 처리하도록 변경됨.
조회·승인·프로그램 전체 삭제 전환
src/main/java/com/firstticket/queueservice/queuetoken/infrastructure/redis/RedisQueueTokenRepository.java, src/main/resources/lua/deleteAllByProgram.lua
findByUserIdAndProgramId, findPosition, admit의 로직/주석 재배치 및 정리. deleteAllByProgramId가 Java SCAN 기반 수집/삭제에서 deleteAllByProgramScript 단일 호출로 전환되어 Lua에서 programTokensKey 기반 일괄 삭제 및 관련 키 정리(processedCount 반환)를 수행.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Repository
  participant RedisTemplate
  participant Redis
  
  rect rgba(100, 150, 255, 0.5)
  Note over Client,Redis: Enqueue Flow
  Client->>Repository: enqueue(tokenId, userId, programId)
  Repository->>RedisTemplate: execute(enqueueScript, keys, argv)
  RedisTemplate->>Redis: Lua script execution (SET NX + INCR + ZADD + HSET + EXPIRE + SADD)
  alt duplicate token
    Redis-->>RedisTemplate: 0
    RedisTemplate-->>Repository: 0
    Repository->>Client: DuplicateTokenException
  else first entry
    Redis-->>RedisTemplate: 1
    RedisTemplate-->>Repository: 1
    Repository-->>Client: queued
  end
  end
  
  rect rgba(150, 100, 255, 0.5)
  Note over Client,Redis: Delete Flow
  Client->>Repository: delete(tokenId, userId, programId)
  Repository->>RedisTemplate: execute(deleteScript, keys, argv)
  RedisTemplate->>Redis: Lua script execution (GET compare + DEL userProgramKey if match + ZREM + DEL tokenHash + SREM programTokens)
  Redis-->>RedisTemplate: 1 or 2
  RedisTemplate-->>Repository: deletion result
  Repository-->>Client: deletion completed
  end
  
  rect rgba(200, 200, 100, 0.5)
  Note over Client,Redis: DeleteAllByProgram Flow
  Client->>Repository: deleteAllByProgramId(programId)
  Repository->>RedisTemplate: execute(deleteAllByProgramScript, keys, argv)
  RedisTemplate->>Redis: Lua script execution (SMEMBERS programTokens -> per-token HGET/GET/DEL -> DEL programKey/seqKey/programTokensKey)
  Redis-->>RedisTemplate: processedCount
  RedisTemplate-->>Repository: processedCount
  Repository-->>Client: processedCount
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

Suggested reviewers

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

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 68.75% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 변경의 핵심 내용인 Lua Script 기반 원자성 강화와 INCR 기반 tie-breaker 도입을 명확하게 반영합니다.
Linked Issues check ✅ Passed 모든 연계 이슈의 목표를 충족합니다: #9의 tie-breaker INCR 시퀀스 도입, #11의 enqueue/delete Lua Script 원자성 개선이 구현되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 정의된 목표 범위 내에 있으며, Redis 자료구조 확장(프로그램 토큰 Set), Lua 스크립트 통합, deleteAllByProgramScript 추가 모두 이슈 #9/#11을 지원합니다.

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

✨ 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 refactor/11-lua-script

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 ♻️ refactor 코드 리팩토링 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: 2

🤖 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/java/com/firstticket/queueservice/queuetoken/infrastructure/redis/RedisQueueTokenRepository.java`:
- Around line 329-334: In RedisQueueTokenRepository, the current
read-then-batch-delete flow that builds userProgramKeysToDelete by comparing
tokenKey.substring(tokenKeyPrefix.length()) to
redisTemplate.opsForValue().get(userProgramKey) is vulnerable to TOCTOU; replace
the separate comparison + later DEL with a Lua compare-and-delete executed at
deletion time (use redisTemplate.execute or script execution) that takes
userProgramKey and expected tokenIdStr and only deletes if the stored value
equals the expected tokenIdStr, ensuring atomicity for the check-and-delete
operation for tokenKey, tokenKeyPrefix, userProgramKey, and
userProgramKeysToDelete handling.

In `@src/main/resources/lua/enqueue.lua`:
- Around line 47-50: The score calculation uses issuedAtEpochMilli * 1000000 +
seq which overflows double precision and zeroes out small seq values; change the
score to use seconds instead of milliseconds by computing issuedAtEpochSeconds =
math.floor(issuedAtEpochMilli / 1000) and then score = issuedAtEpochSeconds *
1000000 + seq (update the lines that compute seq and score around seqKey,
ttlSeconds, issuedAtEpochMilli), so the base is smaller and the seq tie-breaker
is preserved.
🪄 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: 9d33db9f-7f88-464c-ad0d-9cd206c8d309

📥 Commits

Reviewing files that changed from the base of the PR and between 592fcee and cbcdcc0.

📒 Files selected for processing (4)
  • src/main/java/com/firstticket/queueservice/queuetoken/infrastructure/redis/RedisQueueTokenRepository.java
  • src/main/java/com/firstticket/queueservice/queuetoken/infrastructure/redis/RedisScriptConfig.java
  • src/main/resources/lua/delete.lua
  • src/main/resources/lua/enqueue.lua

Comment thread src/main/resources/lua/enqueue.lua
- enqueue.lua: score 생성 시 epoch_milli → epoch_second 정정
  double 정밀도 손실 (ULP=256) 로 같은 ms 안에 128 명 이상 enqueue 시
  seq 손실 → FIFO 깨지는 본문 정정

- deleteAllByProgram.lua 신규: SCAN + compare-and-delete + 일괄 정리를 단일 Lua 로 통합
  TOCTOU 안전 보장 (역인덱스 삭제 시 새 토큰 보호)
  Java 측 SCAN 제거

- RedisScriptConfig: deleteAllByProgramScript 빈 등록
- RedisQueueTokenRepository.deleteAllByProgramId 정정: Lua 호출로 단일화

Related to #11
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: 3

🤖 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/java/com/firstticket/queueservice/queuetoken/infrastructure/redis/RedisQueueTokenRepository.java`:
- Around line 82-91: Wrap all direct calls to redisTemplate.execute (used with
enqueueScript in enqueue, the delete script in delete, and the delete-all script
in deleteAllByProgramId) in try-catch blocks that catch RedisSystemException
(and broader DataAccessException/RuntimeException as needed) and rethrow a
repository-layer/domain-friendly exception (e.g., QueueTokenRepositoryException
or InfrastructureException) with contextual message (including which operation:
enqueue/delete/deleteAllByProgramId) and the original exception as the cause;
ensure this same conversion policy is applied to the three code sites referenced
and keep the thrown exception type consistent across the repository so
domain/application layers never see raw Redis exceptions.
- Around line 56-58: The constructor injection in RedisQueueTokenRepository uses
three parameters of the same type DefaultRedisScript<Long> (enqueueScript,
deleteScript, deleteAllByProgramScript) which can cause
NoUniqueBeanDefinitionException because parameter names are not preserved; fix
by adding explicit `@Qualifier` annotations to the corresponding constructor
parameters (or on the fields) referencing the bean names for enqueueScript,
deleteScript and deleteAllByProgramScript in the RedisQueueTokenRepository
constructor so Spring can disambiguate, or alternatively enable the Java
compiler -parameters option in the build configuration to preserve parameter
names.

In `@src/main/resources/lua/deleteAllByProgram.lua`:
- Around line 36-66: The Lua script currently scans and deletes all matching
token keys in one run (using SCAN, ZREM, DEL, cursor and processedCount), which
can block Redis; change it to a batched, non-blocking approach: iterate only a
limited number of items per invocation (e.g., add a maxBatch param and stop the
repeat loop when processedCount >= maxBatch), or switch to targeting the
program-specific sorted set (programKey) and delete members in small slices
(ZRANGE/ZPOP or ZRANGE with LIMIT) instead of scanning the entire keyspace;
preserve the compare-and-delete logic using userProgramKeyPrefix + userId +
programKeyInfix + programIdStr and keep deleting tokenKey and removing
tokenIdStr from programKey, but ensure each run only processes up to the batch
limit and returns the cursor/remaining flag so the caller can re-invoke until
completion.
🪄 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: 82db390a-c377-453c-84bb-272a24af9b70

📥 Commits

Reviewing files that changed from the base of the PR and between cbcdcc0 and 742a308.

📒 Files selected for processing (4)
  • src/main/java/com/firstticket/queueservice/queuetoken/infrastructure/redis/RedisQueueTokenRepository.java
  • src/main/java/com/firstticket/queueservice/queuetoken/infrastructure/redis/RedisScriptConfig.java
  • src/main/resources/lua/deleteAllByProgram.lua
  • src/main/resources/lua/enqueue.lua
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/resources/lua/enqueue.lua

Comment thread src/main/resources/lua/deleteAllByProgram.lua Outdated
- enqueue.lua / delete.lua: 프로그램 단위 토큰 인덱스
  (queue:program:{programId}:tokens, Set) SADD/SREM 추가
- deleteAllByProgram.lua 정정: SCAN 제거하고 프로그램 단위 인덱스
  SMEMBERS 기반으로 처리 (대규모 데이터 시 Redis 블로킹 회피)

Related to #11
@rlaxxwls13
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 19, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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.

♻️ Duplicate comments (1)
src/main/resources/lua/deleteAllByProgram.lua (1)

36-57: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

대량 토큰에서 단일 Lua 실행 블로킹 위험이 남아 있습니다.

Line 36의 SMEMBERS 전체 로드와 Line 40-57의 전량 순회 삭제를 한 번에 수행하면, 프로그램별 토큰이 큰 경우 Redis 이벤트 루프를 오래 점유할 수 있습니다. maxBatch(예: ARGV) 기반으로 건수를 제한하고, 남은 작업은 호출 측에서 반복 실행하도록 쪼개는 방식이 안전합니다.

🤖 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/resources/lua/deleteAllByProgram.lua` around lines 36 - 57, The
current logic loads all token IDs with SMEMBERS into tokenIds and iterates them
(tokenIds, processedCount, tokenKeyPrefix, userProgramKeyPrefix,
programKeyInfix, programIdStr), which can block Redis for large sets; change the
Lua to read a max batch only (pass maxBatch and a cursor via ARGV), replace the
SMEMBERS call with an SSCAN-based fetch using the supplied cursor and
COUNT=maxBatch, iterate and delete only the returned members (maintain the same
per-token actions: check HGET on tokenKey, compare GET on userProgramKey and DEL
where needed, DEL tokenKey), increment processedCount for only those processed,
and return the new cursor plus processedCount so the caller can repeat until the
cursor is 0.
🤖 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.

Duplicate comments:
In `@src/main/resources/lua/deleteAllByProgram.lua`:
- Around line 36-57: The current logic loads all token IDs with SMEMBERS into
tokenIds and iterates them (tokenIds, processedCount, tokenKeyPrefix,
userProgramKeyPrefix, programKeyInfix, programIdStr), which can block Redis for
large sets; change the Lua to read a max batch only (pass maxBatch and a cursor
via ARGV), replace the SMEMBERS call with an SSCAN-based fetch using the
supplied cursor and COUNT=maxBatch, iterate and delete only the returned members
(maintain the same per-token actions: check HGET on tokenKey, compare GET on
userProgramKey and DEL where needed, DEL tokenKey), increment processedCount for
only those processed, and return the new cursor plus processedCount so the
caller can repeat until the cursor is 0.

ℹ️ Review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Pro Plus

Run ID: 219d1798-6d1f-40b3-923f-e1e041e97041

📥 Commits

Reviewing files that changed from the base of the PR and between 742a308 and 0f90045.

📒 Files selected for processing (4)
  • src/main/java/com/firstticket/queueservice/queuetoken/infrastructure/redis/RedisQueueTokenRepository.java
  • src/main/resources/lua/delete.lua
  • src/main/resources/lua/deleteAllByProgram.lua
  • src/main/resources/lua/enqueue.lua
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/resources/lua/enqueue.lua
  • src/main/java/com/firstticket/queueservice/queuetoken/infrastructure/redis/RedisQueueTokenRepository.java

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

Labels

♻️ refactor 코드 리팩토링

Projects

Status: Done

2 participants