Matching Api를 스웨거에서 돌려보는 중
500에러가 떠서 서버 로그를 확인해보기로 했다.
레디스 서버에 문제가 생긴게 아님에도 Circuit Breaker가 작동을 했다.
무슨 일인지 확인해보기 위해서 디버깅을 했다.
profile이 null인 상황이다.
흐름을 따라가보니
Circuit Braker 폴백 메서드로 흐름이 연결됐다.
Circuit Braker가 예외를 catch해버려서
레디스 서버와 관련된 문제가 아님에도 로그로는 그렇게 찍히게 된 것이다.
CustomException이 던져지는 부분은 메시지와 함께 상태코드가 잘 간다.
위에서 500이 나온건 profile이 null인 경우 null체크를 하지 않고 그대로 사용하기 때문에 500이 발생한거다(이건 이렇게 의도한 게 맞다)
CircuitBreaker: How to treat exceptions as a success
여기서 나와 동일한 문제를 겪는 사례를 봤다.
But it is not necessary for an endpoint to return 200 to show that it is running properly. If any endpoint is returning 400 (valid case if the request fails validation error) that means also that the endpoint is working, right? As per me all the 4xx errors should be treated as successful call and only 5xx should be considered as failure because the 5xx ones are the ones which are related to server health.
400번대 상태코드가 반환되더라도 이는 정상적인 흐름으로 처리돼야 한다는 것이다.
여기에 대한 답변으로
Resilience4j is protocol and framework agnostic. That means it doesn't know anything about the protocol or framework you use. Exceptions are either treated as failures or ignored. There is no option yet to treat an exception as a success. It's an interesting use case and could be added to Resilience4j.
But for now the user of Resilience4j must handle the exception and convert it into a ResponseEntity instead of throwing an exception.
명확하게 이해가 안돼서 깃헙 이슈에 내용을 남겨봤다.
답변이 달렸는데 ignored excpetion은 failrate를 계산할 때만 제외시키는 개념이라고 한다.
폴백 매커니즘은 try catch block처럼 작동해서 예외를 잡기 때문에
예외를 좁히는 게 필요하다고 한다.
다만, 이렇게 하면 레디스와 관련된 일부 예외만 Circuitbraker가 잡을 수 있다.
public List<UserMatching> findMatchingCandidates(String loginId, int duration, TimeUnit timeUnit) {
MatchingRedisData redisCache = getRedisCache(loginId);
try {
return getUserMatchingInfo(loginId,
redisCache.candidateList(),
redisCache.matchingList(),
redisCache.hideList(),
redisCache.lastProfileId(),
redisCache.count(),
duration,
timeUnit);
} catch (CustomException e) {
return Collections.emptyList();
}
}
@CircuitBreaker(name = SIMPLE_CIRCUIT_BREAKER_CONIFG, fallbackMethod = "redisCacheFallback")
public MatchingRedisData getRedisCache(String loginId) {
String candidateKey = loginId + MATCHING_CANDIDATE_REDIS_KEY;
String matchingKey = loginId + MATCHING_LIST_REDIS_KEY;
String profileKey = loginId + LAST_PROFILE_ID_REDIS_KEY;
String countKey = loginId + COUNT_REDIS_KEY;
String profileHideKey = loginId + HIDE_PROFILE_KEY;
List<Object> results = redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
connection.listCommands().lRange(candidateKey.getBytes(), 0, -1);
connection.listCommands().lRange(matchingKey.getBytes(), 0, -1);
connection.listCommands().lRange(profileHideKey.getBytes(), 0, -1);
connection.stringCommands().get(profileKey.getBytes());
connection.stringCommands().get(countKey.getBytes());
return null;
});
List<String> candidateList = (List<String>) results.get(0);
List<String> matchingList = (List<String>) results.get(1);
List<String> hideList = (List<String>) results.get(2);
String lastProfileId = (String) results.get(3);
String count = (String) results.get(4);
return new MatchingRedisData(candidateList, matchingList, hideList, lastProfileId, count);
}
public List<UserMatching> redisCacheFallback(String loginId, Exception ex) {
logError(ex);
List<String> candidates = matchingServiceV1.getMatchingCandidate(loginId);
List<String> partners = matchingServiceV1.getMatchingPartner(loginId);
List<String> hideList = profileFacadeV1.getHideList(loginId);
if (!viewCountMap.containsKey(loginId)) {
return matchingServiceV1.searchPartner(loginId, candidates, partners, hideList, 0);
}
int[] counts = viewCountMap.get(loginId);
List<String> availableCandidate = getAvailableCandidates(candidates, counts[0] / MAX_SKIP_COUNT);
return matchingServiceV1.searchPartner(loginId, availableCandidate, partners, hideList, counts[1]);
}
이렇게 레디스와 관련된 부분만 따로 분리를 해서 CiruitBreaker가 적용되는 범위 자체를 좁히기로 했다.
이렇게 하니 레디스 예외가 터졌을 때 CiruitBreaker가 작동하지 않았다.
느낌상 같은 클래스에 있어서 Circuit Breaker의 AOP가 작동하지 않은 것으로 보인다.
public List<UserMatching> findMatchingCandidates(String loginId, int duration, TimeUnit timeUnit) {
MatchingData redisCache = redisService.getRedisCache(loginId);
return redisService.getUserMatchingInfo(redisCache, loginId, duration, timeUnit);
}
이렇게 두개로 분리하고 다른 클래스에서 해당 메서드들을 호출하도록 변경했다.