Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: 대기열, 참가열 분리 #250

Merged
merged 2 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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