-
Notifications
You must be signed in to change notification settings - Fork 8
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
[BE] 자동 예약 및 자동 업데이트 동시성 제어(#586) #604
Changes from 1 commit
521752e
06ecf0f
340d04e
7ec10a6
444428c
22ab3aa
278e85b
a7f2f76
228c331
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,71 +1,24 @@ | ||
package corea.scheduler.service; | ||
|
||
import corea.exception.CoreaException; | ||
import corea.exception.ExceptionType; | ||
import corea.matching.domain.PullRequestInfo; | ||
import corea.matching.service.MatchingService; | ||
import corea.matching.service.PullRequestProvider; | ||
import corea.room.domain.Room; | ||
import corea.room.repository.RoomRepository; | ||
import corea.scheduler.domain.ScheduleStatus; | ||
import corea.scheduler.repository.AutomaticMatchingRepository; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.scheduling.annotation.Async; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.transaction.PlatformTransactionManager; | ||
import org.springframework.transaction.support.TransactionTemplate; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
@Slf4j | ||
@Component | ||
@RequiredArgsConstructor | ||
public class AutomaticMatchingExecutor { | ||
|
||
private final PlatformTransactionManager transactionManager; | ||
private final MatchingService matchingService; | ||
private final PullRequestProvider pullRequestProvider; | ||
private final RoomRepository roomRepository; | ||
private final MatchingExecutor matchingExecutor; | ||
private final AutomaticMatchingRepository automaticMatchingRepository; | ||
|
||
@Async | ||
@Transactional | ||
public void execute(long roomId) { | ||
//TODO: 트랜잭션 분리 | ||
TransactionTemplate template = new TransactionTemplate(transactionManager); | ||
|
||
try { | ||
template.execute(status -> { | ||
startMatching(roomId); | ||
return null; | ||
}); | ||
} catch (CoreaException e) { | ||
log.warn("매칭 실행 중 에러 발생: {}", e.getMessage(), e); | ||
updateRoomStatusToFail(roomId); | ||
} | ||
} | ||
|
||
private void startMatching(long roomId) { | ||
automaticMatchingRepository.findByRoomIdAndStatusForUpdate(roomId, ScheduleStatus.PENDING) | ||
.ifPresent(automaticMatching -> { | ||
Room room = getRoom(roomId); | ||
PullRequestInfo pullRequestInfo = pullRequestProvider.getUntilDeadline(room.getRepositoryLink(), room.getRecruitmentDeadline()); | ||
matchingService.match(roomId, pullRequestInfo); | ||
|
||
matchingExecutor.match(roomId); | ||
automaticMatching.updateStatusToDone(); | ||
}); | ||
} | ||
|
||
private void updateRoomStatusToFail(long roomId) { | ||
//TODO: 위와 동일 | ||
TransactionTemplate template = new TransactionTemplate(transactionManager); | ||
template.execute(status -> { | ||
Room room = getRoom(roomId); | ||
room.updateStatusToFail(); | ||
return null; | ||
}); | ||
} | ||
|
||
private Room getRoom(long roomId) { | ||
return roomRepository.findById(roomId) | ||
.orElseThrow(() -> new CoreaException(ExceptionType.ROOM_NOT_FOUND)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -0,0 +1,67 @@ | ||||
package corea.scheduler.service; | ||||
|
||||
import corea.exception.CoreaException; | ||||
import corea.exception.ExceptionType; | ||||
import corea.matching.domain.PullRequestInfo; | ||||
import corea.matching.service.MatchingService; | ||||
import corea.matching.service.PullRequestProvider; | ||||
import corea.room.domain.Room; | ||||
import corea.room.repository.RoomRepository; | ||||
import lombok.RequiredArgsConstructor; | ||||
import lombok.extern.slf4j.Slf4j; | ||||
import org.springframework.scheduling.annotation.Async; | ||||
import org.springframework.stereotype.Component; | ||||
import org.springframework.transaction.PlatformTransactionManager; | ||||
import org.springframework.transaction.annotation.Transactional; | ||||
import org.springframework.transaction.support.TransactionTemplate; | ||||
|
||||
@Slf4j | ||||
@Component | ||||
@RequiredArgsConstructor | ||||
public class MatchingExecutor { | ||||
|
||||
private final PlatformTransactionManager transactionManager; | ||||
private final PullRequestProvider pullRequestProvider; | ||||
private final MatchingService matchingService; | ||||
private final RoomRepository roomRepository; | ||||
|
||||
@Async | ||||
@Transactional | ||||
public void match(long roomId) { | ||||
//TODO: 트랜잭션 분리 | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 트랜잭션 분리라는 게 정확히 어떤 태스크를 뜻하는 건가요? ㅇ.ㅇ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 제가 적어둔 "트랜잭션 분리" TODO는 매칭 실패 시 예외가 발생했을 때 데이터를 저장하기 위해 명시적으로 트랜잭션을 선언한 부분입니다. 현재는 코드에서 직접 TransactionTemplate을 사용해 트랜잭션을 관리하고 있지만, 이를 더 효율적으로 관리하기 위해 트랜잭션 전파 속성을 활용해 별도의 클래스로 분리할 계획입니다. |
||||
TransactionTemplate template = new TransactionTemplate(transactionManager); | ||||
|
||||
try { | ||||
template.execute(status -> { | ||||
startMatching(roomId); | ||||
return null; | ||||
}); | ||||
} catch (CoreaException e) { | ||||
log.warn("매칭 실행 중 에러 발생: {}", e.getMessage(), e); | ||||
updateRoomStatusToFail(roomId); | ||||
} | ||||
} | ||||
|
||||
private void startMatching(long roomId) { | ||||
Room room = getRoom(roomId); | ||||
PullRequestInfo pullRequestInfo = pullRequestProvider.getUntilDeadline(room.getRepositoryLink(), room.getRecruitmentDeadline()); | ||||
|
||||
matchingService.match(roomId, pullRequestInfo); | ||||
} | ||||
|
||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||
|
||||
private void updateRoomStatusToFail(long roomId) { | ||||
//TODO: 위와 동일 | ||||
TransactionTemplate template = new TransactionTemplate(transactionManager); | ||||
template.execute(status -> { | ||||
Room room = getRoom(roomId); | ||||
room.updateStatusToFail(); | ||||
return null; | ||||
}); | ||||
} | ||||
|
||||
private Room getRoom(long roomId) { | ||||
return roomRepository.findById(roomId) | ||||
.orElseThrow(() -> new CoreaException(ExceptionType.ROOM_NOT_FOUND)); | ||||
} | ||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
package corea.scheduler.service; | ||
|
||
import config.ServiceTest; | ||
import config.TestAsyncConfig; | ||
import corea.fixture.MemberFixture; | ||
import corea.fixture.RoomFixture; | ||
import corea.matching.domain.PullRequestInfo; | ||
import corea.matching.infrastructure.dto.GithubUserResponse; | ||
import corea.matching.infrastructure.dto.PullRequestResponse; | ||
import corea.matching.service.PullRequestProvider; | ||
import corea.matchresult.domain.MatchResult; | ||
import corea.matchresult.repository.MatchResultRepository; | ||
import corea.member.domain.Member; | ||
import corea.member.domain.MemberRole; | ||
import corea.member.repository.MemberRepository; | ||
import corea.participation.domain.Participation; | ||
import corea.participation.repository.ParticipationRepository; | ||
import corea.room.domain.Room; | ||
import corea.room.domain.RoomStatus; | ||
import corea.room.repository.RoomRepository; | ||
import corea.scheduler.domain.AutomaticMatching; | ||
import corea.scheduler.repository.AutomaticMatchingRepository; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.api.Test; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.boot.test.mock.mockito.MockBean; | ||
import org.springframework.context.annotation.Import; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
import java.time.LocalDateTime; | ||
import java.util.List; | ||
import java.util.Map; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.mockito.ArgumentMatchers.any; | ||
import static org.mockito.Mockito.when; | ||
|
||
@ServiceTest | ||
@Import(TestAsyncConfig.class) | ||
class MatchingExecutorTest { | ||
|
||
@Autowired | ||
private MatchingExecutor matchingExecutor; | ||
|
||
@Autowired | ||
private RoomRepository roomRepository; | ||
|
||
@Autowired | ||
private AutomaticMatchingRepository automaticMatchingRepository; | ||
|
||
@Autowired | ||
private MatchResultRepository matchResultRepository; | ||
|
||
@Autowired | ||
private MemberRepository memberRepository; | ||
|
||
@Autowired | ||
private ParticipationRepository participationRepository; | ||
|
||
@MockBean | ||
private PullRequestProvider pullRequestProvider; | ||
|
||
private Room room; | ||
private Room emptyParticipantRoom; | ||
private Member pororo; | ||
private Member ash; | ||
private Member joysun; | ||
private Member movin; | ||
private Member ten; | ||
private Member cho; | ||
|
||
@BeforeEach | ||
void setUp() { | ||
pororo = memberRepository.save(MemberFixture.MEMBER_PORORO()); | ||
ash = memberRepository.save(MemberFixture.MEMBER_ASH()); | ||
joysun = memberRepository.save(MemberFixture.MEMBER_YOUNGSU()); | ||
movin = memberRepository.save(MemberFixture.MEMBER_MOVIN()); | ||
ten = memberRepository.save(MemberFixture.MEMBER_TENTEN()); | ||
cho = memberRepository.save(MemberFixture.MEMBER_CHOCO()); | ||
|
||
room = roomRepository.save(RoomFixture.ROOM_DOMAIN(pororo, LocalDateTime.now().plusSeconds(3))); | ||
emptyParticipantRoom = roomRepository.save(RoomFixture.ROOM_DOMAIN(ash, LocalDateTime.now().plusSeconds(3))); | ||
|
||
participationRepository.save(new Participation(room, pororo, MemberRole.BOTH, room.getMatchingSize())); | ||
participationRepository.save(new Participation(room, ash, MemberRole.BOTH, room.getMatchingSize())); | ||
participationRepository.save(new Participation(room, joysun, MemberRole.BOTH, room.getMatchingSize())); | ||
participationRepository.save(new Participation(room, movin, MemberRole.BOTH, room.getMatchingSize())); | ||
participationRepository.save(new Participation(room, ten, MemberRole.BOTH, room.getMatchingSize())); | ||
participationRepository.save(new Participation(room, cho, MemberRole.BOTH, room.getMatchingSize())); | ||
|
||
when(pullRequestProvider.getUntilDeadline(any(), any())) | ||
.thenReturn(getPullRequestInfo(pororo, ash, joysun, movin, ten, cho)); | ||
} | ||
|
||
private PullRequestInfo getPullRequestInfo(Member pororo, Member ash, Member joysun, Member movin, Member ten, Member cho) { | ||
return new PullRequestInfo(Map.of( | ||
pororo.getGithubUserId(), | ||
new PullRequestResponse("link", new GithubUserResponse(pororo.getGithubUserId()), | ||
LocalDateTime.of(2024, 10, 12, 18, 00)), | ||
ash.getGithubUserId(), | ||
new PullRequestResponse("link", new GithubUserResponse(ash.getGithubUserId()), | ||
LocalDateTime.of(2024, 10, 12, 18, 20)), | ||
joysun.getGithubUserId(), | ||
new PullRequestResponse("link", new GithubUserResponse(joysun.getGithubUserId()), | ||
LocalDateTime.of(2024, 10, 12, 18, 30)), | ||
movin.getGithubUserId(), | ||
new PullRequestResponse("link", new GithubUserResponse(movin.getGithubUserId()), | ||
LocalDateTime.of(2024, 10, 12, 18, 10)), | ||
ten.getGithubUserId(), | ||
new PullRequestResponse("link", new GithubUserResponse(ten.getGithubUserId()), | ||
LocalDateTime.of(2024, 10, 12, 18, 01)), | ||
cho.getGithubUserId(), | ||
new PullRequestResponse("link", new GithubUserResponse(cho.getGithubUserId()), | ||
LocalDateTime.of(2024, 10, 12, 18, 01) | ||
) | ||
)); | ||
} | ||
|
||
@Test | ||
@DisplayName("매칭을 진행한다.") | ||
void match() { | ||
AutomaticMatching automaticMatching = automaticMatchingRepository.save(new AutomaticMatching(room.getId(), room.getRecruitmentDeadline())); | ||
|
||
matchingExecutor.match(automaticMatching.getRoomId()); | ||
|
||
List<MatchResult> matchResults = matchResultRepository.findAll(); | ||
assertThat(matchResults).isNotEmpty(); | ||
} | ||
|
||
@Transactional | ||
@Test | ||
@DisplayName("매칭 시도 중 예외가 발생했다면 방 상태를 FAIL로 변경한다.") | ||
void matchFail() { | ||
AutomaticMatching automaticMatching = automaticMatchingRepository.save(new AutomaticMatching(emptyParticipantRoom.getId(), emptyParticipantRoom.getRecruitmentDeadline())); | ||
|
||
matchingExecutor.match(automaticMatching.getRoomId()); | ||
|
||
assertThat(emptyParticipantRoom.getStatus()).isEqualTo(RoomStatus.FAIL); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
의도에 맞게 잘 변경된거 같아요 👍