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: DELETE/timetable 동시성 에러 #743

Merged
merged 2 commits into from
Aug 1, 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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import java.util.Objects;
import java.util.Optional;

import in.koreatech.koin.global.exception.RequestTooFastException;
import jakarta.persistence.EntityManager;
import jakarta.persistence.OptimisticLockException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand Down Expand Up @@ -36,6 +39,7 @@ public class TimetableService {
private final TimetableFrameRepositoryV2 timetableFrameRepositoryV2;
private final SemesterRepositoryV2 semesterRepositoryV2;
private final UserRepository userRepository;
private final EntityManager entityManager;

public List<LectureResponse> getLecturesBySemester(String semester) {
List<Lecture> lectures = lectureRepositoryV2.findBySemester(semester);
Expand Down Expand Up @@ -111,13 +115,17 @@ public TimetableResponse getTimetables(Integer userId, String semesterRequest) {

@Transactional
public void deleteTimetableLecture(Integer userId, Integer timetableLectureId) {
TimetableLecture timetableLecture = timetableLectureRepositoryV2.getById(timetableLectureId);
TimetableFrame frame = timetableFrameRepositoryV2.getById(timetableLecture.getTimetableFrame().getId());
if (!Objects.equals(frame.getUser().getId(), userId)) {
throw AuthorizationException.withDetail("userId: " + userId);
try {
TimetableLecture timetableLecture = timetableLectureRepositoryV2.getById(timetableLectureId);
TimetableFrame frame = timetableFrameRepositoryV2.getById(timetableLecture.getTimetableFrame().getId());
if (!Objects.equals(frame.getUser().getId(), userId)) {
throw AuthorizationException.withDetail("userId: " + userId);
}
timetableLectureRepositoryV2.deleteById(timetableLectureId);
entityManager.flush();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JPA에선 트랜잭션 충돌을 커밋 or flush할 당시에 감지합니다. 그래서 flush를 하지 않는다면, try범위 밖에서 커밋되면서 예외가 발생되어 잡지 못합니다. 그래서 try문 범위 안에서 예외를 발생시킬 수 있도록 flush 코드를 추가했습니다.

} catch (OptimisticLockException e) {
throw new RequestTooFastException("요청이 너무 빠릅니다. 다시 시도해주세요.");
}

timetableLectureRepositoryV2.deleteById(timetableLectureId);
}

private TimetableResponse getTimetableResponse(Integer userId, TimetableFrame timetableFrame) {
Expand Down
48 changes: 48 additions & 0 deletions src/test/java/in/koreatech/koin/acceptance/TimetableApiTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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

import io.restassured.response.Response;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -20,6 +21,12 @@
import io.restassured.RestAssured;
import io.restassured.http.ContentType;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@SuppressWarnings("NonAsciiCharacters")
class TimetableApiTest extends AcceptanceTest {

Expand Down Expand Up @@ -584,4 +591,45 @@ void deleteTimetable() {

assertThat(timetableRepository.findById(2)).isNotPresent();
}

@Test
@DisplayName("시간표 삭제 동시성 예외 적절하게 처리하는지 테스트한다.")
void deleteTimetableConcurrency() throws InterruptedException {
User user = userFixture.준호_학생().getUser();
String token = userFixture.getToken(user);
Semester semester = semesterFixture.semester("20192");

Lecture 건축구조의_이해_및_실습 = lectureFixture.건축구조의_이해_및_실습(semester.getSemester());
Lecture HRD_개론 = lectureFixture.HRD_개론(semester.getSemester());

timetableV2Fixture.시간표6(user, semester, 건축구조의_이해_및_실습, HRD_개론);

ExecutorService executor = Executors.newFixedThreadPool(2);
CountDownLatch latch = new CountDownLatch(2);
Comment on lines +607 to +608
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A

최근에 동시성 문제가 많이 발생하는데, 코드 리뷰하는 과정에서 참 많이 배우게 되는 것 같아요!👍


List<Response> responseList = new ArrayList<>();
Runnable deleteTask = () -> {
Response response = RestAssured
.given()
.header("Authorization", "Bearer " + token)
.when()
.param("id", 2)
.delete("/timetable");
responseList.add(response);
latch.countDown();
};

executor.submit(deleteTask);
executor.submit(deleteTask);

latch.await();

boolean hasConflict = responseList.stream()
.anyMatch(response -> response.getStatusCode() == 409);

assertThat(hasConflict).isTrue();
assertThat(timetableRepository.findById(2)).isNotPresent();

executor.shutdown();
}
}
Loading