Skip to content

Commit

Permalink
Feature: 대기열 페이지에서 빠져나간 사용자와 대기한 사용자를 구분한다. (#152)
Browse files Browse the repository at this point in the history
  • Loading branch information
hseong3243 authored Aug 28, 2024
2 parents cff55ad + 70cb44a commit cba18aa
Show file tree
Hide file tree
Showing 12 changed files with 308 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.thirdparty.ticketing.domain.waitingsystem;

import com.thirdparty.ticketing.domain.common.Event;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class LastPollingEvent implements Event {

private final String email;
private final long performanceId;
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,12 @@ public void enterWaitingRoom(String email, long performanceId) {
public long getRemainingCount(String email, long performanceId) {
WaitingMember waitingMember = waitingManager.findWaitingMember(email, performanceId);
long runningCount = runningManager.getRunningCount(performanceId);
long remainingCount = waitingMember.getWaitingCount() - runningCount;
eventPublisher.publish(new PollingEvent(performanceId));
return waitingMember.getWaitingCount() - runningCount;
if (remainingCount <= 0) {
eventPublisher.publish(new LastPollingEvent(email, performanceId));
}
return remainingCount;
}

public void moveUserToRunning(long performanceId) {
Expand All @@ -44,4 +48,14 @@ public void pullOutRunningMember(String email, long performanceId) {
runningManager.pullOutRunningMember(email, performanceId);
waitingManager.removeMemberInfo(email, performanceId);
}

/**
* 공연에 해당하는 사용자의 작업 공간 만료 시간을 5분 뒤로 업데이트한다.
*
* @param email 사용자의 이메일
* @param performanceId 공연 ID
*/
public void updateRunningMemberExpiredTime(String email, long performanceId) {
runningManager.updateRunningMemberExpiredTime(email, performanceId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ public interface RunningManager {
void pullOutRunningMember(String email, long performanceId);

Set<String> removeExpiredMemberInfo(long performanceId);

void updateRunningMemberExpiredTime(String email, long performanceId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.thirdparty.ticketing.domain.common.ErrorCode;
import com.thirdparty.ticketing.domain.common.TicketingException;
import com.thirdparty.ticketing.domain.ticket.dto.event.PaymentEvent;
import com.thirdparty.ticketing.domain.waitingsystem.LastPollingEvent;
import com.thirdparty.ticketing.domain.waitingsystem.PollingEvent;
import com.thirdparty.ticketing.domain.waitingsystem.WaitingSystem;

Expand All @@ -27,6 +28,11 @@ public void moveUserToRunningRoom(PollingEvent event) {
waitingSystem.moveUserToRunning(event.getPerformanceId());
}

@EventListener(LastPollingEvent.class)
public void updateRunningMemberExpiredTime(LastPollingEvent event) {
waitingSystem.updateRunningMemberExpiredTime(event.getEmail(), event.getPerformanceId());
}

@TransactionalEventListener(PaymentEvent.class)
public void pullOutRunningMember(PaymentEvent event) {
HttpServletRequest request =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,9 @@ public void pullOutRunningMember(String email, long performanceId) {
public Set<String> removeExpiredMemberInfo(long performanceId) {
return runningRoom.removeExpiredMemberInfo(performanceId);
}

@Override
public void updateRunningMemberExpiredTime(String email, long performanceId) {
runningRoom.updateRunningMemberExpiredTime(email, performanceId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,20 @@ public Set<String> removeExpiredMemberInfo(long performanceId) {
removeMemberEmails.forEach(performanceRoom::remove);
return removeMemberEmails;
}

public void updateRunningMemberExpiredTime(String email, long performanceId) {
room.computeIfPresent(
performanceId,
(key, room) -> {
room.computeIfPresent(
email,
(k, waitingMember) ->
new WaitingMember(
email,
performanceId,
waitingMember.getWaitingCount(),
ZonedDateTime.now().plusMinutes(5)));
return room;
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ public long getAvailableToRunning(long performanceId) {

@Override
public void enterRunningRoom(long performanceId, Set<WaitingMember> waitingMembers) {
runningCounter.increment(performanceId, waitingMembers.size());
runningRoom.enter(performanceId, waitingMembers);
runningCounter.increment(performanceId, waitingMembers.size());
}

@Override
Expand All @@ -44,4 +44,9 @@ public void pullOutRunningMember(String email, long performanceId) {
public Set<String> removeExpiredMemberInfo(long performanceId) {
return runningRoom.removeExpiredMemberInfo(performanceId);
}

@Override
public void updateRunningMemberExpiredTime(String email, long performanceId) {
runningRoom.updateRunningMemberExpiredTime(email, performanceId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public class RedisRunningRoom implements RunningRoom {
private static final int MAX_RUNNING_ROOM_SIZE = 100;
private static final String RUNNING_ROOM_KEY = "running_room:";
private static final int EXPIRED_MINUTE = 5;
private static final int MINIMUM_RUNNING_TIME = 30;

private final ZSetOperations<String, String> runningRoom;

Expand All @@ -37,13 +38,14 @@ public void enter(long performanceId, Set<WaitingMember> waitingMembers) {
if (waitingMembers.isEmpty()) {
return;
}
ZonedDateTime minimumRunningTime = ZonedDateTime.now().plusSeconds(MINIMUM_RUNNING_TIME);
Set<TypedTuple<String>> collect =
waitingMembers.stream()
.map(
member ->
TypedTuple.of(
member.getEmail(),
(double) ZonedDateTime.now().toEpochSecond()))
(double) minimumRunningTime.toEpochSecond()))
.collect(Collectors.toSet());
runningRoom.add(getRunningRoomKey(performanceId), collect);
}
Expand All @@ -56,11 +58,32 @@ public void pullOutRunningMember(String email, long performanceId) {
runningRoom.remove(getRunningRoomKey(performanceId), email);
}

/**
* 주어진 공연에 해당하는 러닝룸에서 만료 시간이 현재 시간 이전인 사람들을 제거한다.
*
* @param performanceId
* @return
*/
public Set<String> removeExpiredMemberInfo(long performanceId) {
long removeRange = ZonedDateTime.now().minusMinutes(EXPIRED_MINUTE).toEpochSecond();
long removeRange = ZonedDateTime.now().toEpochSecond();
String runningRoomKey = getRunningRoomKey(performanceId);
Set<String> removedMemberEmails = runningRoom.rangeByScore(runningRoomKey, 0, removeRange);
runningRoom.removeRangeByScore(runningRoomKey, 0, removeRange);
return removedMemberEmails;
}

/**
* 주어진 공연 - 이메일에 해당하는 사용자의 러닝룸 만료시간을 5분뒤로 업데이트 한다. 동시성 문제가 발생할 수 있다.
*
* @param email 사용자의 이메일
* @param performanceId 공연 ID
*/
public void updateRunningMemberExpiredTime(String email, long performanceId) {
if (runningRoom.score(getRunningRoomKey(performanceId), email) != null) {
runningRoom.add(
getRunningRoomKey(performanceId),
email,
ZonedDateTime.now().plusMinutes(EXPIRED_MINUTE).toEpochSecond());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ void setUp() throws JsonProcessingException {
void removeExpiredMemberInfoFromRunningRoom() {
// given
String anotherEmail = "[email protected]";
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime now = ZonedDateTime.now().plusSeconds(30);
rawRunningRoom.add(getRunningRoomKey(performanceId), anotherEmail, now.toEpochSecond());

// when
Expand All @@ -153,7 +153,7 @@ void removeExpiredMemberInfoFromRunningRoom() {
void removeExpiredMemberInfoFromWaitingRoom() throws JsonProcessingException {
// given
String anotherEmail = "[email protected]";
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime now = ZonedDateTime.now().plusSeconds(30);
WaitingMember waitingMember = new WaitingMember(anotherEmail, performanceId, 2, now);
rawRunningRoom.add(getRunningRoomKey(performanceId), anotherEmail, now.toEpochSecond());
rawWaitingRoom.put(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package com.thirdparty.ticketing.global.waitingsystem;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.within;

import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Set;

import org.junit.jupiter.api.BeforeEach;
Expand All @@ -11,6 +16,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.data.redis.core.ZSetOperations.TypedTuple;

import com.thirdparty.ticketing.domain.common.EventPublisher;
import com.thirdparty.ticketing.domain.waitingsystem.WaitingSystem;
Expand All @@ -32,6 +38,10 @@ void setUp() {
redisTemplate.getConnectionFactory().getConnection().commands().flushAll();
}

private String getRunningRoomKey(long performanceId) {
return "running_room:" + performanceId;
}

@Nested
@DisplayName("폴링 이벤트 발행 시")
class PublishPoolingEventTest {
Expand All @@ -54,4 +64,80 @@ void moveUserToRunningRoom() {
assertThat(waitingSystem.isReadyToHandle(email, performanceId)).isTrue();
}
}

@Nested
@DisplayName("마지막 폴링 이벤트 발행 시")
class PublishLastPollingEventTest {

@Test
@DisplayName("작업 공간의 사용자 만료 시간을 업데이트한다.")
void updateRunningMemberExpiredTime() {
// given
long performanceId = 1;
String email = "[email protected]";

waitingSystem.enterWaitingRoom(email, performanceId);
waitingSystem.moveUserToRunning(performanceId);

// when
waitingSystem.getRemainingCount(email, performanceId);

// then
Set<TypedTuple<String>> tuples =
rawRunningRoom.rangeWithScores(getRunningRoomKey(performanceId), 0, -1);
assertThat(tuples)
.hasSize(1)
.first()
.satisfies(
tuple -> {
ZonedDateTime memberExpiredTime =
ZonedDateTime.ofInstant(
Instant.ofEpochSecond(tuple.getScore().longValue()),
ZoneId.of("Asia/Seoul"));
assertThat(memberExpiredTime)
.isCloseTo(
ZonedDateTime.now().plusMinutes(5),
within(5, ChronoUnit.MINUTES));
});
}

@Test
@DisplayName("이메일에 해당하는 사용자만 업데이트한다.")
void updateOnlyInputMember() {
// given
long performanceId = 1;
String email = "[email protected]";
String anotherEmail = "[email protected]";

waitingSystem.enterWaitingRoom(email, performanceId);
waitingSystem.enterWaitingRoom(anotherEmail, performanceId);
waitingSystem.moveUserToRunning(performanceId);

// when
waitingSystem.getRemainingCount(email, performanceId);

// then
Set<TypedTuple<String>> tuples =
rawRunningRoom.rangeWithScores(getRunningRoomKey(performanceId), 0, -1);
TypedTuple<String> emailMember =
tuples.stream()
.filter(tuple -> tuple.getValue().equals(email))
.findFirst()
.get();
TypedTuple<String> anotherEmailMember =
tuples.stream()
.filter(tuple -> tuple.getValue().equals(anotherEmail))
.findFirst()
.get();
assertThat(getTime(emailMember.getScore()))
.isCloseTo(ZonedDateTime.now().plusMinutes(5), within(1, ChronoUnit.MINUTES));
assertThat(getTime(anotherEmailMember.getScore()))
.isCloseTo(ZonedDateTime.now().plusSeconds(30), within(5, ChronoUnit.SECONDS));
}

private ZonedDateTime getTime(Double score) {
return ZonedDateTime.ofInstant(
Instant.ofEpochSecond(score.longValue()), ZoneId.of("Asia/Seoul"));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.thirdparty.ticketing.global.waitingsystem.memory.running;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchException;
import static org.assertj.core.api.Assertions.within;

import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
Expand Down Expand Up @@ -216,4 +219,43 @@ void notRemoveMemberInfo() {
assertThat(room.get(performanceId).get(email)).isEqualTo(waitingMember);
}
}

@Nested
@DisplayName("사용자 만료 시간 업데이트 시")
class UpdateRunningMemberExpiredTimeTest {

@Test
@DisplayName("사용자의 만료 시간을 5분으로 업데이트 한다.")
void updateRunningMemberExpiredTime() {
// given
long performanceId = 1;
String email = "[email protected]";
runningRoom.enter(performanceId, Set.of(new WaitingMember(email, performanceId)));

// when
runningRoom.updateRunningMemberExpiredTime(email, performanceId);

// then
WaitingMember waitingMember = room.get(performanceId).get(email);
assertThat(waitingMember.getEnteredAt())
.isCloseTo(ZonedDateTime.now().plusMinutes(5), within(1, ChronoUnit.SECONDS));
}

@Test
@DisplayName("사용자가 작업 공간에 존재하지 않으면 무시한다.")
void ignore_notExistsMember() {
// given
long performanceId = 1;
String email = "[email protected]";
room.put(performanceId, new ConcurrentHashMap<>());

// when
Exception exception =
catchException(
() -> runningRoom.updateRunningMemberExpiredTime(email, performanceId));

// then
assertThat(exception).doesNotThrowAnyException();
}
}
}
Loading

0 comments on commit cba18aa

Please sign in to comment.