Skip to content

Commit

Permalink
fix: 대기열, 참가열 분리 (#250)
Browse files Browse the repository at this point in the history
* feat: 참가열 zset 분리

* faet: 대기열 큐 만료시간 설정 로직 추가
  • Loading branch information
annahxxl committed Jan 16, 2024
1 parent fe7bf88 commit 0ac1069
Show file tree
Hide file tree
Showing 12 changed files with 139 additions and 46 deletions.
Binary file added .DS_Store
Binary file not shown.
Binary file added api/api-booking/.DS_Store
Binary file not shown.
2 changes: 1 addition & 1 deletion api/api-booking/http/booking.http
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Booking-Session-Id: {{sessionId}}
}

### 대기열 조회
GET http://localhost:8082/api/v1/bookings/order-in-queue?eventId=1
GET http://localhost:8082/api/v1/bookings/waiting-order?eventId=1
Authorization: Bearer {{accessToken}}
Booking-Session-Id: {{sessionId}}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ public class RedisOperator {

private final RedisTemplate<String, String> redisTemplate;

public Boolean exists(String key) {
return tryOperation(() -> redisTemplate.hasKey(key));
}

public void expire(String key, long expirationSeconds) {
Duration timeout = Duration.ofSeconds(expirationSeconds);
tryOperation(() -> redisTemplate.expire(key, timeout));
}

public void setIfAbsent(String key, String value, Integer expirationSeconds) {
Duration timeout = Duration.ofSeconds(expirationSeconds);
tryOperation(() -> redisTemplate.opsForValue().setIfAbsent(key, value, timeout));
Expand All @@ -36,10 +45,18 @@ public void addToZSet(String key, String value, double score) {
tryOperation(() -> redisTemplate.opsForZSet().add(key, value, score));
}

public Long getSizeOfZSet(String key) {
return tryOperation(() -> redisTemplate.opsForZSet().size(key));
}

public Long getRankFromZSet(String key, String value) {
return tryOperation(() -> redisTemplate.opsForZSet().rank(key, value));
}

public Double getScoreFromZSet(String key, String index) {
return tryOperation(() -> redisTemplate.opsForZSet().score(key, index));
}

public void removeElementFromZSet(String key, String value) {
tryOperation(() -> redisTemplate.opsForZSet().remove(key, value));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public class WebConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(bookingSessionInterceptor)
.addPathPatterns("/api/*/bookings/enter-queue")
.addPathPatterns("/api/*/bookings/order-in-queue")
.addPathPatterns("/api/*/bookings/waiting-order")
.addPathPatterns("/api/*/bookings/issue-token")
.addPathPatterns("/api/*/bookings/exit-queue");
registry.addInterceptor(bookingTokenInterceptor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ public BookingGetResponse getBooking(String id, Long memberId) {

@Async
protected void removeSessionIdInBookingQueue(Long eventId, String tokenSessionId) {
bookingQueueManager.remove(eventId, tokenSessionId);
bookingQueueManager.removeFromParticipantQueue(eventId, tokenSessionId);
}

private EventTime getBookableTimeWithEvent(Long timeId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,16 @@ public ResponseEntity<ApiResponse<SessionIdIssueResponse>> issueSessionId() {

@Operation(summary = "대기열 진입")
@PostMapping("/enter-queue")
public ResponseEntity<Void> enterQueue(@RequestBody @Valid BookingQueueEnterRequest request, @RequestAttribute("bookingSessionId") String bookingSessionId) {
bookingQueueService.enterQueue(request, bookingSessionId);
public ResponseEntity<Void> enterWaitingQueue(@RequestBody @Valid BookingQueueEnterRequest request, @RequestAttribute("bookingSessionId") String bookingSessionId) {
bookingQueueService.enterWaitingQueue(request, bookingSessionId);
return ResponseEntity.noContent().build();
}

@Operation(summary = "내 대기 순서 확인")
@GetMapping("/order-in-queue")
public ResponseEntity<ApiResponse<OrderInQueueGetResponse>> getOrderInQueue(@RequestParam Long eventId, @RequestAttribute("bookingSessionId") String bookingSessionId) {
@GetMapping("/waiting-order")
public ResponseEntity<ApiResponse<OrderInQueueGetResponse>> getWaitingOrder(@RequestParam Long eventId, @RequestAttribute("bookingSessionId") String bookingSessionId) {
ApiResponse<OrderInQueueGetResponse> response =
ApiResponse.ok(bookingQueueService.getOrderInQueue(eventId, bookingSessionId));
ApiResponse.ok(bookingQueueService.getWaitingOrder(eventId, bookingSessionId));
return ResponseEntity.ok(response);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,74 @@
@RequiredArgsConstructor
public class BookingQueueManager {

private static final String WAITING_QUEUE_KEY_PREFIX = "waiting:eventId:";
private static final String PARTICIPANT_QUEUE_KEY_PREFIX = "participant:eventId:";
private static final long QUEUE_TIMEOUT_SECONDS = 60 * 24 * 60 * 60; // 2 months
private final RedisOperator redisOperator;

public void add(Long eventId, String sessionId, double currentTimeSeconds) {
redisOperator.addToZSet(String.valueOf(eventId), sessionId, currentTimeSeconds);
public void addToWaitingQueue(Long eventId, String sessionId, double currentTimeSeconds) {
String key = generateWaitingQueueKey(eventId);

if (redisOperator.exists(key)) {
redisOperator.addToZSet(key, sessionId, currentTimeSeconds);
} else {
redisOperator.addToZSet(key, sessionId, currentTimeSeconds);
redisOperator.expire(key, QUEUE_TIMEOUT_SECONDS);
}
}

public Optional<Long> getRank(Long eventId, String sessionId) {
Long rank = redisOperator.getRankFromZSet(String.valueOf(eventId), sessionId);

public Optional<Long> getRankInWaitingQueue(Long eventId, String sessionId) {
String key = generateWaitingQueueKey(eventId);
Long rank = redisOperator.getRankFromZSet(key, sessionId);
return Optional.ofNullable(rank);
}

public void remove(Long eventId, String sessionId) {
redisOperator.removeElementFromZSet(String.valueOf(eventId), sessionId);
public void removeFromWaitingQueue(Long eventId, String sessionId) {
String key = generateWaitingQueueKey(eventId);
redisOperator.removeElementFromZSet(key, sessionId);
}

public void removeRangeByScoreFromWaitingQueue(Long eventId, double minScore, double maxScore) {
String key = generateWaitingQueueKey(eventId);
redisOperator.removeRangeByScoreFromZSet(key, minScore, maxScore);
}

public void addToParticipantQueue(Long eventId, String sessionId, double currentTimeSeconds) {
String key = generateParticipantQueueKey(eventId);

if(redisOperator.exists(key)) {
redisOperator.addToZSet(key, sessionId, currentTimeSeconds);
} else {
redisOperator.addToZSet(key, sessionId, currentTimeSeconds);
redisOperator.expire(key, QUEUE_TIMEOUT_SECONDS);
}
}

public Long getSizeOfParticipantQueue(Long eventId) {
String key = generateParticipantQueueKey(eventId);
return redisOperator.getSizeOfZSet(key);
}

public Double getElementScore(Long eventId, String sessionId) {
String key = generateParticipantQueueKey(eventId);
return redisOperator.getScoreFromZSet(key, sessionId);
}

public void removeFromParticipantQueue(Long eventId, String sessionId) {
String key = generateParticipantQueueKey(eventId);
redisOperator.removeElementFromZSet(key, sessionId);
}

public void removeRangeByScoreFromParticipantQueue(Long eventId, double minScore, double maxScore) {
String key = generateParticipantQueueKey(eventId);
redisOperator.removeRangeByScoreFromZSet(key, minScore, maxScore);
}

private String generateWaitingQueueKey(Long eventId) {
return WAITING_QUEUE_KEY_PREFIX + eventId;
}

public void removeRangeByScore(Long eventId, double minScore, double maxScore) {
redisOperator.removeRangeByScoreFromZSet(String.valueOf(eventId), minScore, maxScore);
private String generateParticipantQueueKey(Long eventId) {
return PARTICIPANT_QUEUE_KEY_PREFIX + eventId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,58 +22,82 @@
public class BookingQueueService {

private final static double MILLISECONDS_PER_SECOND = 1000.0;
private final static double TIMEOUT_SECONDS = 7 * 60;
private final static long ENTRY_LIMIT = 2;
private final static long WAITING_QUEUE_TIMEOUT_SECONDS = 2 * 60 * 60; // 2 hours
private final static long PARTICIPANT_QUEUE_TIMEOUT_SECONDS = 7 * 60; // 7 minutes
private final static int ENTRY_LIMIT = 2;

private final BookingQueueManager bookingQueueManager;
private final BookingJwtProvider bookingJwtProvider;

public void enterQueue(BookingQueueEnterRequest request, String sessionId) {
public SessionIdIssueResponse issueSessionId() {
UUID sessionId = UUID.randomUUID();
return SessionIdIssueResponse.from(sessionId.toString());
}

public void enterWaitingQueue(BookingQueueEnterRequest request, String sessionId) {
double currentTimeSeconds = System.currentTimeMillis() / MILLISECONDS_PER_SECOND;
bookingQueueManager.add(request.eventId(), sessionId, currentTimeSeconds);
bookingQueueManager.addToWaitingQueue(request.eventId(), sessionId, currentTimeSeconds);
}

public OrderInQueueGetResponse getOrderInQueue(Long eventId, String sessionId) {
public OrderInQueueGetResponse getWaitingOrder(Long eventId, String sessionId) {
cleanQueue(eventId);
Long order = getOrder(eventId, sessionId);
Boolean isMyTurn = order <= ENTRY_LIMIT;
Long myOrder = isMyTurn ? 0 : order - ENTRY_LIMIT;

Long myOrder = getOrderInWaitingQueue(eventId, sessionId);
Boolean isMyTurn = myOrder <= getAvailableEntryCountForParticipantQueue(eventId);

if (isMyTurn) {
bookingQueueManager.removeFromWaitingQueue(eventId, sessionId);
double currentTimeSeconds = System.currentTimeMillis() / MILLISECONDS_PER_SECOND;
bookingQueueManager.addToParticipantQueue(eventId, sessionId, currentTimeSeconds);
}

return OrderInQueueGetResponse.of(myOrder, isMyTurn);
}

public TokenIssueResponse issueToken(TokenIssueRequest request, String sessionId) {
Long order = getOrder(request.eventId(), sessionId);

if (order > ENTRY_LIMIT) {
if (!existsParticipant(request.eventId(), sessionId)) {
throw new BookingException(BookingErrorCode.OUT_OF_ORDER);
}

BookingJwtPayload payload = new BookingJwtPayload(sessionId);
String token = bookingJwtProvider.generateToken(payload);

return TokenIssueResponse.from(token);
}

private Long getOrder(Long eventId, String sessionId) {
return bookingQueueManager.getRank(eventId, sessionId)
public void exitQueue(BookingQueueExitRequest request, String sessionId) {
bookingQueueManager.removeFromWaitingQueue(request.eventId(), sessionId);
bookingQueueManager.removeFromParticipantQueue(request.eventId(), sessionId);
}

private Long getOrderInWaitingQueue(Long eventId, String sessionId) {
Long rank = bookingQueueManager.getRankInWaitingQueue(eventId, sessionId)
.orElseThrow(() -> new BookingException(BookingErrorCode.NOT_IN_QUEUE));
return rank + 1;
}

public void exitQueue(BookingQueueExitRequest request, String sessionId) {
bookingQueueManager.remove(request.eventId(), sessionId);
private Long getAvailableEntryCountForParticipantQueue(Long eventId) {
Long participantCount = bookingQueueManager.getSizeOfParticipantQueue(eventId);
return ENTRY_LIMIT - participantCount;
}

public SessionIdIssueResponse issueSessionId() {
UUID sessionId = UUID.randomUUID();
return SessionIdIssueResponse.from(sessionId.toString());
private Boolean existsParticipant(Long eventId, String sessionId) {
return bookingQueueManager.getElementScore(eventId, sessionId) != null;
}

/*
* 대기열에 존재하는 세션 중 타임아웃된 세션을 제거한다.
* 대기열, 참가열에 존재하는 세션 중 타임아웃된 세션을 제거한다.
*/
private void cleanQueue(Long eventId) {
double currentTimeSeconds = System.currentTimeMillis() / MILLISECONDS_PER_SECOND;
double timeLimitSeconds = currentTimeSeconds - TIMEOUT_SECONDS;
bookingQueueManager.removeRangeByScore(eventId, 0, timeLimitSeconds);
bookingQueueManager.removeRangeByScoreFromWaitingQueue(
eventId,
0,
currentTimeSeconds - WAITING_QUEUE_TIMEOUT_SECONDS
);
bookingQueueManager.removeRangeByScoreFromParticipantQueue(
eventId,
0,
currentTimeSeconds - PARTICIPANT_QUEUE_TIMEOUT_SECONDS
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
@RequiredArgsConstructor
public class SeatLockManager {

private final static String SEAT_LOCK_CACHE_KEY_PREFIX = "seatId:";
private final static String SEAT_LOCK_CACHE_VALUE_PREFIX = "sessionId:";
private final static String SEAT_LOCK_KEY_PREFIX = "seatId:";
private final static String SEAT_LOCK_VALUE_PREFIX = "sessionId:";

private final RedisOperator redisOperator;

Expand All @@ -34,14 +34,14 @@ public void unlockSeat(Long seatId) {
}

private String generateSeatLockKey(Long seatId) {
return SEAT_LOCK_CACHE_KEY_PREFIX + seatId;
return SEAT_LOCK_KEY_PREFIX + seatId;
}

private String generateSeatLockValue(String tokenSessionId) {
return SEAT_LOCK_CACHE_VALUE_PREFIX + tokenSessionId;
return SEAT_LOCK_VALUE_PREFIX + tokenSessionId;
}

private String extractSessionId(String value) {
return value.replace(SEAT_LOCK_CACHE_VALUE_PREFIX, "");
return value.replace(SEAT_LOCK_VALUE_PREFIX, "");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ void setup() {
);

given(seatLockManager.getSelectorId(any(Long.class))).willReturn(Optional.of(SESSION_ID));
doNothing().when(bookingQueueManager).remove(any(Long.class), any(String.class));
doNothing().when(bookingQueueManager).removeFromWaitingQueue(any(Long.class), any(String.class));

// when
BookingCreateResponse response = bookingService.createBooking(request, member.getId(), SESSION_ID);
Expand Down
2 changes: 1 addition & 1 deletion http/bingterpark.http
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ Booking-Session-Id: {{sessionId}}
}

### 대기열 조회
GET http://localhost:8082/api/v1/bookings/order-in-queue?eventId=1
GET http://localhost:8082/api/v1/bookings/waiting-order?eventId=1
Authorization: Bearer {{accessToken}}
Booking-Session-Id: {{sessionId}}

Expand Down

0 comments on commit 0ac1069

Please sign in to comment.