Skip to content

Commit

Permalink
feat: 서킷브레이커가 열려있다면 fallback 메서드를 실행한다.
Browse files Browse the repository at this point in the history
  • Loading branch information
wonyongChoi05 committed Nov 11, 2023
1 parent 1bfa052 commit 3859dd0
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 9 deletions.
12 changes: 8 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,25 +27,29 @@ repositories {
}

dependencies {
implementation("org.springframework.boot:spring-boot-starter")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
implementation 'org.mindrot:jbcrypt:0.4'

// Resilience4j-CircuitBreaker
implementation 'io.github.resilience4j:resilience4j-spring-boot3'
implementation "org.springframework.boot:spring-boot-starter-aop"
implementation group: 'io.github.resilience4j', name: 'resilience4j-spring-boot3', version: '2.1.0'


// WebClient
implementation 'org.springframework.boot:spring-boot-starter-webflux'

//redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation("it.ozimov:embedded-redis:0.7.2")
implementation "it.ozimov:embedded-redis:0.7.2"

compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'

annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
annotationProcessor 'org.projectlombok:lombok'

// JWT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
import com.integrated.techhub.auth.application.client.dto.response.GithubPrInfoResponse;
import com.integrated.techhub.auth.application.client.dto.response.OAuthGithubUsernameResponse;
import com.integrated.techhub.auth.application.client.dto.response.OAuthTokensResponse;
import com.integrated.techhub.resilience4j.circuitbreaker.exception.CircuitBreakerInvalidException;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
Expand All @@ -19,6 +22,7 @@

@Component
@RequiredArgsConstructor
@Slf4j
public class WebClientGithubClient implements GithubClient {

private static final int LAST_PAGE = 40;
Expand All @@ -28,6 +32,7 @@ public class WebClientGithubClient implements GithubClient {
private final GithubClientProperties githubClientProperties;

@Override
@CircuitBreaker(name = "webClientGithubClient", fallbackMethod = "throwBadGateway")
public OAuthTokensResponse getGithubTokens(final String code) {
final String clientId = githubClientProperties.clientId();
final String clientSecret = githubClientProperties.clientSecret();
Expand All @@ -40,6 +45,7 @@ public OAuthTokensResponse getGithubTokens(final String code) {
}

@Override
@CircuitBreaker(name = "webClientGithubClient", fallbackMethod = "throwBadGateway")
public OAuthTokensResponse getNewAccessToken(final String refreshToken) {
final String clientId = githubClientProperties.clientId();
final String clientSecret = githubClientProperties.clientSecret();
Expand All @@ -53,6 +59,7 @@ public OAuthTokensResponse getNewAccessToken(final String refreshToken) {

@Override
@Deprecated
@CircuitBreaker(name = "webClientGithubClient", fallbackMethod = "throwBadGateway")
public OAuthGithubUsernameResponse getGithubUsername(final String accessToken) {
final HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);
Expand All @@ -69,11 +76,12 @@ public OAuthGithubUsernameResponse getGithubUsername(final String accessToken) {
* 인증된 유저 기준 시간당 5,000회
* using: 사용자가 직접 요청하는 동기화 API
* */

@Override
@CircuitBreaker(name = "webClientGithubClient", fallbackMethod = "fallback")
public List<GithubPrInfoResponse> getPrsByRepoName(final String accessToken, final String repo) {
final List<GithubPrInfoResponse> responses = new ArrayList<>();
final List<String> prRequestUrls = createPrApiRequestUrls(repo, LAST_PAGE);

Flux.fromIterable(prRequestUrls)
.flatMap(url -> fetchPrs(accessToken, url))
.collectList()
Expand All @@ -100,4 +108,8 @@ private Flux<List<GithubPrInfoResponse>> fetchPrs(final String accessToken, fina
.flux();
}

private List<GithubPrInfoResponse> fallback(Throwable t) {
throw new CircuitBreakerInvalidException();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.integrated.techhub.resilience4j.circuitbreaker.aspect;

import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import java.net.URI;
import java.util.Optional;

@Aspect
@Component
@RequiredArgsConstructor
public class WebClientCircuitBreakerAspect {

private final CircuitBreakerRegistry registry;

@Around("execution(* org.springframework.web.reactive.function.client.WebClient.*(..)) && args(url,..)")
public Object aspect(ProceedingJoinPoint pjp, String url) throws Throwable {
return aspect(pjp, new URI(url));
}

@Around("execution(* org.springframework.web.reactive.function.client.WebClient.*(..)) && args(uri,..)")
public Object aspect(ProceedingJoinPoint pjp, URI uri) throws Throwable {
return registry.circuitBreaker(findHost(uri))
.executeCheckedSupplier(pjp::proceed);
}

private String findHost(URI uri) {
return Optional.ofNullable(uri)
.map(URI::getHost)
.orElse("default");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.integrated.techhub.resilience4j.circuitbreaker.config;

import com.integrated.techhub.common.exception.TechHubException;

import java.util.function.Predicate;

public class CircuitRecordFailurePredicate implements Predicate<Throwable> {

@Override
public boolean test(Throwable throwable) {
if (throwable instanceof TechHubException) {
return false;
}
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.integrated.techhub.resilience4j.circuitbreaker.exception;

import com.integrated.techhub.common.exception.ErrorCode;
import com.integrated.techhub.common.exception.TechHubException;

import static org.springframework.http.HttpStatus.*;

public class CircuitBreakerInvalidException extends TechHubException {

public CircuitBreakerInvalidException() {
super(new ErrorCode(NOT_FOUND, "깃허브 서버가 불안정합니다. 다른 API를 사용해주세요"));
}

}
2 changes: 1 addition & 1 deletion src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ spring:
database-platform: org.hibernate.dialect.MySQL57Dialect
generate-ddl: true
hibernate:
ddl-auto: update
ddl-auto: none
properties:
hibernate:
format_sql: true
Expand Down
8 changes: 5 additions & 3 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,18 @@ resilience4j:
circuitbreaker:
configs:
default:
sliding-window-type: COUNT_BASED
registerHealthIndicator: true
slidingWindowSize: 10
minimumNumberOfCalls: 5
slidingWindowSize: 3
minimumNumberOfCalls: 1
permittedNumberOfCallsInHalfOpenState: 3
automaticTransitionFromOpenToHalfOpenEnabled: true
waitDurationInOpenState: 5s
failureRateThreshold: 50
eventConsumerBufferSize: 10
recordFailurePredicate: com.integrated.techhub.resilience4j.circuitbreaker.config.CircuitRecordFailurePredicate
instances:
orderService:
webClientGithubClient:
baseConfig: default
timelimiter:
configs:
Expand Down

0 comments on commit 3859dd0

Please sign in to comment.