Skip to content

Commit

Permalink
feat: apiCall 동시성 이슈를 분산락에서 비관적 락으로 대체 (#947)
Browse files Browse the repository at this point in the history
* chore: redis 관련 파일 삭제

* style: 불필요한 import문 삭제

* refactor: 오후 11시 55분마다 초기화로직 작성

* refactor: 초기 0 count 저장 로직 작성

* test: 변화된 로직에 맞게 테스트 수정

* refactor: apiCall 저장 로직 추가

* refactor: entityManager clear 로직 추가

* test: routeClient 초기화함수로 수정

* test: 공통좌표를 상수로 분리

* test: baseTest에 오늘일자 apiCall저장 setUp 추가

* refactor: 락 조회 메서드 통일로 동시성 이슈 해결

* style: 잘못된 개행 추가

* test: apiCall 초기화 테스트 진행

* chore: 변수명 변경

routeclientManager > routeClientManager

* feat: 오류를 복구하도록 저장
  • Loading branch information
coli-geonwoo authored Jan 28, 2025
1 parent 5246033 commit a89b34c
Show file tree
Hide file tree
Showing 21 changed files with 130 additions and 467 deletions.
22 changes: 0 additions & 22 deletions backend/src/main/java/com/ody/common/aop/DistributedLock.java

This file was deleted.

64 changes: 0 additions & 64 deletions backend/src/main/java/com/ody/common/aop/DistributedLockAop.java

This file was deleted.

This file was deleted.

2 changes: 0 additions & 2 deletions backend/src/main/java/com/ody/eta/service/EtaService.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.ody.eta.service;

import com.ody.common.aop.DistributedLock;
import com.ody.common.exception.OdyNotFoundException;
import com.ody.eta.domain.Eta;
import com.ody.eta.domain.EtaStatus;
Expand Down Expand Up @@ -36,7 +35,6 @@ public Eta saveFirstEtaOfMate(Mate mate, RouteTime routeTime) {
}

@Transactional
@DistributedLock(key = "'FIND_ETAS'")
public MateEtaResponsesV2 findAllMateEtas(MateEtaRequest mateEtaRequest, Mate mate) {
Meeting meeting = mate.getMeeting();
Eta mateEta = findByMateId(mate.getId());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.ody.meeting.service;

import com.ody.common.aop.DistributedLock;
import com.ody.common.exception.OdyBadRequestException;
import com.ody.common.exception.OdyNotFoundException;
import com.ody.mate.domain.Mate;
Expand Down Expand Up @@ -117,7 +116,6 @@ private Mate findMateByMemberAndMeeting(Member member, Meeting meeting) {
}

@Transactional
@DistributedLock(key = "'MATE_SAVE'")
public MateSaveResponseV2 saveMateAndSendNotifications(MateSaveRequestV2 mateSaveRequest, Member member) {
Meeting meeting = findByInviteCode(mateSaveRequest.inviteCode());
if (meeting.isEnd()) {
Expand Down
4 changes: 2 additions & 2 deletions backend/src/main/java/com/ody/route/domain/ApiCall.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ public class ApiCall {

private Boolean enabled;

public ApiCall(ClientType clientType) {
this(null, clientType, 0, LocalDate.now(), null);
public ApiCall(ClientType clientType, Integer count, LocalDate date, Boolean enabled) {
this(null, clientType, count, date, enabled);
}

public ApiCall(ClientType clientType, Integer count, LocalDate date) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@

import com.ody.route.domain.ApiCall;
import com.ody.route.domain.ClientType;
import jakarta.persistence.LockModeType;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;

public interface ApiCallRepository extends JpaRepository<ApiCall, Long> {

Optional<ApiCall> findFirstByDateAndClientType(LocalDate date, ClientType clientType);
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<ApiCall> findByDateAndClientType(LocalDate date, ClientType clientType);

Optional<ApiCall> findFirstByDateBetweenAndClientType(LocalDate start, LocalDate end, ClientType clientType);

Expand Down
31 changes: 21 additions & 10 deletions backend/src/main/java/com/ody/route/service/ApiCallService.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.ody.route.service;

import com.ody.common.exception.OdyServerErrorException;
import com.ody.route.domain.ApiCall;
import com.ody.route.domain.ClientType;
import com.ody.route.dto.ApiCallCountResponse;
Expand All @@ -21,6 +22,11 @@ public class ApiCallService {

private final ApiCallRepository apiCallRepository;

@Transactional
public ApiCall save(ApiCall apiCall) {
return apiCallRepository.save(apiCall);
}

public ApiCallCountResponse countApiCall(ClientType clientType) {
LocalDate end = LocalDate.now();
LocalDate start = clientType.determineResetDate(end);
Expand All @@ -34,35 +40,40 @@ public ApiCallCountResponse countApiCall(ClientType clientType) {

@Transactional
public void increaseCountByClientType(ClientType clientType) {
ApiCall apiCall = findOrSaveFirstByClientTypeAndDate(clientType);
ApiCall apiCall = findOrSaveTodayApiCallByClientType(clientType);
apiCall.increaseCount();
}

private ApiCall findOrSaveFirstByClientTypeAndDate(ClientType clientType) {
return apiCallRepository.findFirstByDateAndClientType(LocalDate.now(), clientType)
.orElseGet(() -> apiCallRepository.save(new ApiCall(clientType)));
}

public ApiCallEnabledResponse getApiCallEnabled(ClientType clientType) {
boolean enabled = getEnabledByClientType(clientType);
return new ApiCallEnabledResponse(enabled);
}

public boolean getEnabledByClientType(ClientType clientType) {
ApiCall apiCall = findOrSaveApiCallForToggleByClientType(clientType);
ApiCall apiCall = findOrSaveTodayApiCallByClientType(clientType);
return apiCall.getEnabled();
}

public ApiCall findOrSaveTodayApiCallByClientType(ClientType clientType) {
LocalDate now = LocalDate.now();
return apiCallRepository.findByDateAndClientType(now, clientType)
.orElseGet(() -> {
log.error("date : {}, clientType : {} apiCall을 찾을 수 없습니다.", now, clientType);
return save(new ApiCall(clientType, 0, now));
});
}

@Transactional
public void toggleApiCallEnabled(ClientType clientType) {
ApiCall apiCall = findOrSaveApiCallForToggleByClientType(clientType);
ApiCall apiCall = findApiCallForToggleByClientType(clientType);
apiCall.updateEnabled();
}

private ApiCall findOrSaveApiCallForToggleByClientType(ClientType clientType) {
private ApiCall findApiCallForToggleByClientType(ClientType clientType) {
LocalDate end = LocalDate.now();
LocalDate start = clientType.determineResetDate(end);
Optional<ApiCall> apiCall = apiCallRepository.findFirstByDateBetweenAndClientType(start, end, clientType);
return apiCall.orElseGet(() -> apiCallRepository.save(new ApiCall(clientType)));
return apiCall
.orElseThrow(() -> new OdyServerErrorException(clientType + "의 apiCall이 존재하지 않습니다."));
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.ody.route.service;

import com.ody.common.exception.OdyServerErrorException;
import com.ody.route.domain.ApiCall;
import java.time.LocalDate;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Slf4j
Expand All @@ -13,11 +16,10 @@ public class RouteClientManager {

private final List<RouteClient> routeClients;
private final ApiCallService apiCallService;
private final RouteClientCircuitBreaker routeClientCircuitBreaker;

public List<RouteClient> getAvailableClients() {
List<RouteClient> availableClients = routeClients.stream()
.filter(client -> isAvailable(client) && isEnabled(client))
.filter(this::isEnabled)
.toList();

if (availableClients.isEmpty()) {
Expand All @@ -27,8 +29,13 @@ public List<RouteClient> getAvailableClients() {
return availableClients;
}

private boolean isAvailable(RouteClient routeClient) {
return !routeClientCircuitBreaker.isBlocked(routeClient);
@Scheduled(cron = "0 55 23 * * *", zone = "Asia/Seoul")
public void initializeClientApiCalls() {
LocalDate nextDay = LocalDate.now().plusDays(1);
routeClients.stream()
.map(client -> apiCallService.findOrSaveTodayApiCallByClientType(client.getClientType()))
.map(apiCall -> new ApiCall(apiCall.getClientType(), 0, nextDay, apiCall.getEnabled()))
.forEach(apiCallService::save);
}

private boolean isEnabled(RouteClient routeClient) {
Expand Down
3 changes: 0 additions & 3 deletions backend/src/main/java/com/ody/route/service/RouteService.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ public class RouteService {

private final RouteClientManager routeClientManager;
private final ApiCallService apiCallService;
private final RouteClientCircuitBreaker routeClientCircuitBreaker;

public RouteTime calculateRouteTime(Coordinates origin, Coordinates target) {
List<RouteClient> availableClients = routeClientManager.getAvailableClients();
Expand All @@ -35,8 +34,6 @@ public RouteTime calculateRouteTime(Coordinates origin, Coordinates target) {
return routeTime;
} catch (OdyServerErrorException exception) {
log.warn("{} API 에러 발생 : ", routeClient.getClientType(), exception);
routeClientCircuitBreaker.recordFailCountInMinutes(routeClient);
routeClientCircuitBreaker.determineBlock(routeClient);
}
}
log.error("모든 RouteClient API 사용 불가");
Expand Down
Loading

0 comments on commit a89b34c

Please sign in to comment.