diff --git a/common/circuit-breaker/build.gradle b/common/circuit-breaker/build.gradle new file mode 100644 index 00000000..9e2aad8d --- /dev/null +++ b/common/circuit-breaker/build.gradle @@ -0,0 +1,42 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.3.4' + id 'io.spring.dependency-management' version '1.1.6' +} + +group = 'com.ticketPing' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +bootJar { + enabled = false +} + +jar { + enabled = true +} + +dependencies { + api 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0' + + +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/common/circuit-breaker/src/main/java/circuit/config/CircuitBreakerEventConfig.java b/common/circuit-breaker/src/main/java/circuit/config/CircuitBreakerEventConfig.java new file mode 100644 index 00000000..4ff27428 --- /dev/null +++ b/common/circuit-breaker/src/main/java/circuit/config/CircuitBreakerEventConfig.java @@ -0,0 +1,47 @@ +package circuit.config; + +import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; +import io.github.resilience4j.circuitbreaker.event.CircuitBreakerOnErrorEvent; +import io.github.resilience4j.circuitbreaker.event.CircuitBreakerOnFailureRateExceededEvent; +import io.github.resilience4j.circuitbreaker.event.CircuitBreakerOnStateTransitionEvent; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; + +@Slf4j +@Configuration +@RequiredArgsConstructor +public class CircuitBreakerEventConfig { + + private final CircuitBreakerRegistry circuitBreakerRegistry; + + @PostConstruct + public void registerCircuitBreakerEventListeners() { + circuitBreakerRegistry.getAllCircuitBreakers().forEach(circuitBreaker -> { + circuitBreaker.getEventPublisher() + .onStateTransition(this::logStateTransition) + .onFailureRateExceeded(this::logFailureRateExceeded) + .onError(this::logErrorEvent); + }); + } + + private void logStateTransition(CircuitBreakerOnStateTransitionEvent event) { + log.info("CircuitBreaker '{}' state changed from {} to {}", + event.getCircuitBreakerName(), + event.getStateTransition().getFromState(), + event.getStateTransition().getToState()); + } + + private void logFailureRateExceeded(CircuitBreakerOnFailureRateExceededEvent event) { + log.warn("CircuitBreaker '{}' failure rate exceeded: {}%", + event.getCircuitBreakerName(), + event.getFailureRate()); + } + + private void logErrorEvent(CircuitBreakerOnErrorEvent event) { + log.error("CircuitBreaker '{}' recorded an error: {}", + event.getCircuitBreakerName(), + event.getThrowable().getMessage()); + } +} \ No newline at end of file diff --git a/common/circuit-breaker/src/main/resources/application-circuit-breaker.yml b/common/circuit-breaker/src/main/resources/application-circuit-breaker.yml new file mode 100644 index 00000000..484c1388 --- /dev/null +++ b/common/circuit-breaker/src/main/resources/application-circuit-breaker.yml @@ -0,0 +1,29 @@ +spring: + cloud: + openfeign: + circuitbreaker: enabled + +resilience4j: + circuitbreaker: + configs: + default: + registerHealthIndicator: true + slidingWindowType: COUNT_BASED + slidingWindowSize: 10 + minimumNumberOfCalls: 10 + failureRateThreshold: 50 + slowCallRateThreshold: 100 + slowCallDurationThreshold: 10s + waitDurationInOpenState: 10s + permittedNumberOfCallsInHalfOpenState: 3 + recordSlowCalls: true + record-exceptions: + - java.util.concurrent.TimeoutException + - java.net.SocketTimeoutException + - java.net.UnknownHostException + - java.net.ConnectException + - feign.RetryableException + - feign.FeignException.GatewayTimeout + - feign.FeignException.BadGateway + - feign.FeignException.TooManyRequests + - feign.FeignException.ServiceUnavailable \ No newline at end of file diff --git a/gateway/src/main/java/com/ticketPing/gateway/common/exception/CircuitBreakerErrorCase.java b/gateway/src/main/java/com/ticketPing/gateway/common/exception/CircuitBreakerErrorCase.java index 618d23af..f5470444 100644 --- a/gateway/src/main/java/com/ticketPing/gateway/common/exception/CircuitBreakerErrorCase.java +++ b/gateway/src/main/java/com/ticketPing/gateway/common/exception/CircuitBreakerErrorCase.java @@ -8,9 +8,9 @@ @Getter @RequiredArgsConstructor public enum CircuitBreakerErrorCase implements ErrorCase { - SERVICE_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "서비스가 연결 불가능합니다. 관리자에게 문의해주세요."), + SERVICE_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "서비스가 연결 불가능합니다. 잠시 후 다시 시도해주세요."), CONNECTION_TIMEOUT(HttpStatus.GATEWAY_TIMEOUT, "서비스 요청 시간이 초과되었습니다. 잠시 후 다시 시도해주세요."), - SERVICE_IS_OPEN(HttpStatus.SERVICE_UNAVAILABLE, "서비스가 연결 불가능합니다. 잠시 후 다시 시도해주세요."); + SERVICE_IS_OPEN(HttpStatus.SERVICE_UNAVAILABLE, "서비스가 연결 불가능합니다. 관리자에게 문의해주세요."); private final HttpStatus httpStatus; private final String message; diff --git a/gateway/src/main/java/com/ticketPing/gateway/infrastructure/client/AuthWebClient.java b/gateway/src/main/java/com/ticketPing/gateway/infrastructure/client/AuthWebClient.java index ce21f78f..26cd113f 100644 --- a/gateway/src/main/java/com/ticketPing/gateway/infrastructure/client/AuthWebClient.java +++ b/gateway/src/main/java/com/ticketPing/gateway/infrastructure/client/AuthWebClient.java @@ -2,23 +2,40 @@ import auth.UserCacheDto; import com.ticketPing.gateway.application.client.AuthClient; +import com.ticketPing.gateway.common.exception.CircuitBreakerErrorCase; import com.ticketPing.gateway.common.exception.SecurityErrorCase; import exception.ApplicationException; +import io.github.resilience4j.circuitbreaker.CallNotPermittedException; +import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import io.netty.channel.ChannelOption; +import lombok.extern.slf4j.Slf4j; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; import response.CommonResponse; +import java.time.Duration; + +@Slf4j @Component public class AuthWebClient implements AuthClient { private final WebClient webClient; public AuthWebClient(WebClient.Builder webClientBuilder) { - this.webClient = webClientBuilder.build(); + this.webClient = webClientBuilder + .clientConnector(new ReactorClientHttpConnector( + HttpClient.create() + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1000) + .responseTimeout(Duration.ofSeconds(15)))) + .build(); } + @CircuitBreaker(name = "authServiceCircuitBreaker", fallbackMethod = "validateTokenFallback") public Mono validateToken(String token) { return webClient.post() .uri("http://auth/api/v1/auth/validate") @@ -33,4 +50,22 @@ public Mono validateToken(String token) { } }); } + + private Mono validateTokenFallback(String token, Throwable ex) { + if (ex instanceof CallNotPermittedException) { + return Mono.error(new ApplicationException(CircuitBreakerErrorCase.SERVICE_IS_OPEN)); + } else if ( + ex instanceof WebClientResponseException.BadGateway || + ex instanceof WebClientResponseException.GatewayTimeout || + ex instanceof WebClientResponseException.TooManyRequests || + ex instanceof WebClientResponseException.ServiceUnavailable + ) { + return Mono.error(new ApplicationException(CircuitBreakerErrorCase.SERVICE_UNAVAILABLE)); + } else if (ex instanceof WebClientResponseException) { + return Mono.error(ex); + } else { + return Mono.error(new ApplicationException(CircuitBreakerErrorCase.SERVICE_UNAVAILABLE)); + } + } + } diff --git a/gateway/src/main/java/com/ticketPing/gateway/infrastructure/filter/JwtFilter.java b/gateway/src/main/java/com/ticketPing/gateway/infrastructure/filter/JwtFilter.java index 05304b47..428631a8 100644 --- a/gateway/src/main/java/com/ticketPing/gateway/infrastructure/filter/JwtFilter.java +++ b/gateway/src/main/java/com/ticketPing/gateway/infrastructure/filter/JwtFilter.java @@ -1,8 +1,11 @@ package com.ticketPing.gateway.infrastructure.filter; import com.ticketPing.gateway.application.client.AuthClient; +import exception.ApplicationException; import lombok.RequiredArgsConstructor; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; @@ -15,6 +18,7 @@ import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; +import java.nio.charset.StandardCharsets; import java.util.List; @Component @@ -52,6 +56,14 @@ public Mono load(ServerWebExchange exchange) { return Mono.just((SecurityContext) new SecurityContextImpl(authentication)); }) + .onErrorResume(ApplicationException.class, e -> { + exchange.getResponse().setStatusCode(HttpStatus.SERVICE_UNAVAILABLE); + DataBuffer buffer = exchange.getResponse() + .bufferFactory() + .wrap(e.getMessage().getBytes(StandardCharsets.UTF_8)); + return exchange.getResponse().writeWith(Mono.just(buffer)) + .then(Mono.empty()); + }) .onErrorResume(WebClientResponseException.Unauthorized.class, e -> Mono.empty()); } diff --git a/gateway/src/main/java/com/ticketPing/gateway/presentation/controller/FallbackController.java b/gateway/src/main/java/com/ticketPing/gateway/presentation/controller/FallbackController.java index 0ab4d30b..9420b119 100644 --- a/gateway/src/main/java/com/ticketPing/gateway/presentation/controller/FallbackController.java +++ b/gateway/src/main/java/com/ticketPing/gateway/presentation/controller/FallbackController.java @@ -27,7 +27,7 @@ public Mono>> defaultFallback(ServerWebExc private ResponseEntity> handleCircuitBreakerException(Throwable ex) { if (ex instanceof TimeoutException) { - return createServiceUnavailableResponse(CircuitBreakerErrorCase.SERVICE_UNAVAILABLE); + return createServiceUnavailableResponse(CircuitBreakerErrorCase.CONNECTION_TIMEOUT); } else if (ex instanceof CallNotPermittedException) { return createServiceUnavailableResponse(CircuitBreakerErrorCase.SERVICE_IS_OPEN); } else { diff --git a/gateway/src/main/resources/application.yml b/gateway/src/main/resources/application.yml index f6548b2e..f4ddc3ce 100644 --- a/gateway/src/main/resources/application.yml +++ b/gateway/src/main/resources/application.yml @@ -54,8 +54,6 @@ resilience4j: - org.springframework.cloud.gateway.support.NotFoundException - io.netty.channel.AbstractChannel$AnnotatedConnectException instances: - authServiceCircuitBreaker: - baseConfig: default userServiceCircuitBreaker: baseConfig: default performanceServiceCircuitBreaker: @@ -65,4 +63,15 @@ resilience4j: paymentServiceCircuitBreaker: baseConfig: default queueManageServiceCircuitBreaker: - baseConfig: default \ No newline at end of file + baseConfig: default + authServiceCircuitBreaker: + baseConfig: default + record-exceptions: + - java.util.concurrent.TimeoutException + - org.springframework.cloud.gateway.support.NotFoundException + - io.netty.channel.AbstractChannel$AnnotatedConnectException + - org.springframework.web.reactive.function.client.WebClientRequestException + - org.springframework.web.reactive.function.client.WebClientResponseException.BadGateway + - org.springframework.web.reactive.function.client.WebClientResponseException.GatewayTimeout + - org.springframework.web.reactive.function.client.WebClientResponseException.TooManyRequests + - org.springframework.web.reactive.function.client.WebClientResponseException.ServiceUnavailable \ No newline at end of file diff --git a/monitoring/grafana/provisioning/alerting/alert-rule.yml b/monitoring/grafana/provisioning/alerting/alert-rule.yml index c187672d..db916e05 100644 --- a/monitoring/grafana/provisioning/alerting/alert-rule.yml +++ b/monitoring/grafana/provisioning/alerting/alert-rule.yml @@ -145,7 +145,7 @@ groups: model: disableTextWrap: false editorMode: builder - expr: sum by(name) (resilience4j_circuitbreaker_state{state=~"open|half_open"}) + expr: sum by(name, instance) (resilience4j_circuitbreaker_state{state=~"open|half_open"}) fullMetaSearch: false includeNullMetadata: true instant: true @@ -187,7 +187,7 @@ groups: execErrState: Error for: 0s annotations: - description: CircuitBreaker ''{{ $labels.name }}'' has change from CLOSED to OPEN. + description: CircuitBreaker ''{{ $labels.instance }}'' ''{{ $labels.name }}'' has change from CLOSED to OPEN. labels: { } isPaused: false notification_settings: @@ -210,7 +210,7 @@ groups: disableTextWrap: false editorMode: builder exemplar: false - expr: sum by(name) (avg_over_time(resilience4j_circuitbreaker_state{state=~"open|half_open"}[5m])) + expr: sum by(name, instance) (avg_over_time(resilience4j_circuitbreaker_state{state=~"open|half_open"}[5m])) fullMetaSearch: false includeNullMetadata: true instant: true @@ -252,7 +252,7 @@ groups: execErrState: Error for: 1m annotations: - description: 'CircuitBreaker ''{{ $labels.name }}'' has been in an OPEN or HALF_OPEN state for 5 minutes.' + description: 'CircuitBreaker ''{{ $labels.instance }}'' ''{{ $labels.name }}'' has been in an OPEN or HALF_OPEN state for 5 minutes.' labels: {} isPaused: false notification_settings: diff --git a/services/auth/build.gradle b/services/auth/build.gradle index 68bb01f8..882ae4ca 100644 --- a/services/auth/build.gradle +++ b/services/auth/build.gradle @@ -39,6 +39,7 @@ dependencies { implementation project(':common:dtos') implementation project(':common:caching') implementation project(':common:monitoring') + implementation project(':common:circuit-breaker') // MVC implementation 'org.springframework.boot:spring-boot-starter-web' diff --git a/services/auth/src/main/java/com/ticketPing/auth/AuthApplication.java b/services/auth/src/main/java/com/ticketPing/auth/AuthApplication.java index aabe031e..07207ca8 100644 --- a/services/auth/src/main/java/com/ticketPing/auth/AuthApplication.java +++ b/services/auth/src/main/java/com/ticketPing/auth/AuthApplication.java @@ -7,7 +7,7 @@ @SpringBootApplication @EnableFeignClients -@ComponentScan(basePackages = {"com.ticketPing.auth", "aop", "exception", "caching"}) +@ComponentScan(basePackages = {"com.ticketPing.auth", "aop", "exception", "caching", "circuit"}) public class AuthApplication { public static void main(String[] args) { SpringApplication.run(AuthApplication.class, args); diff --git a/services/auth/src/main/java/com/ticketPing/auth/common/exception/CircuitBreakerErrorCase.java b/services/auth/src/main/java/com/ticketPing/auth/common/exception/CircuitBreakerErrorCase.java new file mode 100644 index 00000000..7ec866bf --- /dev/null +++ b/services/auth/src/main/java/com/ticketPing/auth/common/exception/CircuitBreakerErrorCase.java @@ -0,0 +1,16 @@ +package com.ticketPing.auth.common.exception; + +import exception.ErrorCase; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum CircuitBreakerErrorCase implements ErrorCase { + SERVICE_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "서비스가 연결 불가능합니다. 잠시 후 다시 시도해주세요."), + SERVICE_IS_OPEN(HttpStatus.SERVICE_UNAVAILABLE, "서비스가 연결 불가능합니다. 관리자에게 문의해주세요."); + + private final HttpStatus httpStatus; + private final String message; +} diff --git a/services/auth/src/main/java/com/ticketPing/auth/infrastructure/client/UserFeignClient.java b/services/auth/src/main/java/com/ticketPing/auth/infrastructure/client/UserFeignClient.java index d41aa515..c07591ce 100644 --- a/services/auth/src/main/java/com/ticketPing/auth/infrastructure/client/UserFeignClient.java +++ b/services/auth/src/main/java/com/ticketPing/auth/infrastructure/client/UserFeignClient.java @@ -1,17 +1,43 @@ package com.ticketPing.auth.infrastructure.client; import com.ticketPing.auth.application.client.UserClient; +import com.ticketPing.auth.common.exception.CircuitBreakerErrorCase; +import com.ticketPing.auth.infrastructure.config.CustomFeignConfig; +import exception.ApplicationException; +import feign.FeignException; +import feign.RetryableException; +import io.github.resilience4j.circuitbreaker.CallNotPermittedException; +import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestParam; -import java.util.UUID; import response.CommonResponse; import user.UserLookupRequest; import user.UserResponse; -@FeignClient(name = "user") +@FeignClient(name = "user", configuration = CustomFeignConfig.class) public interface UserFeignClient extends UserClient { @GetMapping("/api/v1/users/login") + @CircuitBreaker(name = "userServiceCircuitBreaker", fallbackMethod = "fallbackForUserService") CommonResponse getUserByEmailAndPassword(@RequestBody UserLookupRequest userLookupRequest); + + default CommonResponse fallbackForUserService(UserLookupRequest userLookupRequest, Throwable cause) { + if (cause instanceof CallNotPermittedException) { + throw new ApplicationException(CircuitBreakerErrorCase.SERVICE_IS_OPEN); + } + else if ( + cause instanceof FeignException.GatewayTimeout || + cause instanceof FeignException.ServiceUnavailable || + cause instanceof FeignException.BadGateway || + cause instanceof FeignException.TooManyRequests || + cause instanceof RetryableException + ) { + throw new ApplicationException(CircuitBreakerErrorCase.SERVICE_UNAVAILABLE); + } + else if (cause instanceof FeignException) { + throw (FeignException) cause; + } else { + throw new ApplicationException(CircuitBreakerErrorCase.SERVICE_UNAVAILABLE); + } + } } diff --git a/services/auth/src/main/java/com/ticketPing/auth/infrastructure/config/CustomFeignConfig.java b/services/auth/src/main/java/com/ticketPing/auth/infrastructure/config/CustomFeignConfig.java new file mode 100644 index 00000000..07694870 --- /dev/null +++ b/services/auth/src/main/java/com/ticketPing/auth/infrastructure/config/CustomFeignConfig.java @@ -0,0 +1,14 @@ +package com.ticketPing.auth.infrastructure.config; + +import feign.Request; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class CustomFeignConfig { + + @Bean + public Request.Options requestOptions() { + return new Request.Options(1000, 12000); + } +} diff --git a/services/auth/src/main/resources/application.yml b/services/auth/src/main/resources/application.yml index b821b984..02a8b409 100644 --- a/services/auth/src/main/resources/application.yml +++ b/services/auth/src/main/resources/application.yml @@ -7,6 +7,7 @@ spring: - "classpath:application-eureka.yml" - "classpath:application-redis.yml" - "classpath:application-monitoring.yml" + - "classpath:application-circuit-breaker.yml" server: port: 10010 @@ -16,4 +17,10 @@ jwt: accessToken: expiration: 1800000 # 30분 refreshToken: - expiration: 604800000 # 7일 \ No newline at end of file + expiration: 604800000 # 7일 + +resilience4j: + circuitbreaker: + instances: + userServiceCircuitBreaker: + baseConfig: default \ No newline at end of file diff --git a/services/order/build.gradle b/services/order/build.gradle index aacced62..bff8500e 100644 --- a/services/order/build.gradle +++ b/services/order/build.gradle @@ -40,6 +40,7 @@ dependencies { implementation project(':common:rdb') implementation project(':common:messaging') implementation project(':common:monitoring') + implementation project(':common:circuit-breaker') // MVC implementation 'org.springframework.boot:spring-boot-starter-web' diff --git a/services/order/src/main/java/com/ticketPing/order/OrderApplication.java b/services/order/src/main/java/com/ticketPing/order/OrderApplication.java index 7beeb7f0..1eff9a35 100644 --- a/services/order/src/main/java/com/ticketPing/order/OrderApplication.java +++ b/services/order/src/main/java/com/ticketPing/order/OrderApplication.java @@ -7,7 +7,7 @@ @SpringBootApplication @EnableFeignClients -@ComponentScan(basePackages = {"com.ticketPing.order", "aop", "exception", "audit", "messaging"}) +@ComponentScan(basePackages = {"com.ticketPing.order", "aop", "exception", "audit", "messaging", "circuit"}) public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); diff --git a/services/order/src/main/java/com/ticketPing/order/common/exception/CircuitBreakerErrorCase.java b/services/order/src/main/java/com/ticketPing/order/common/exception/CircuitBreakerErrorCase.java new file mode 100644 index 00000000..ab9425d4 --- /dev/null +++ b/services/order/src/main/java/com/ticketPing/order/common/exception/CircuitBreakerErrorCase.java @@ -0,0 +1,16 @@ +package com.ticketPing.order.common.exception; + +import exception.ErrorCase; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum CircuitBreakerErrorCase implements ErrorCase { + SERVICE_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "서비스가 연결 불가능합니다. 잠시 후 다시 시도해주세요."), + SERVICE_IS_OPEN(HttpStatus.SERVICE_UNAVAILABLE, "서비스가 연결 불가능합니다. 관리자에게 문의해주세요."); + + private final HttpStatus httpStatus; + private final String message; +} diff --git a/services/order/src/main/java/com/ticketPing/order/common/utils/FeignFallbackUtils.java b/services/order/src/main/java/com/ticketPing/order/common/utils/FeignFallbackUtils.java new file mode 100644 index 00000000..0254f4c2 --- /dev/null +++ b/services/order/src/main/java/com/ticketPing/order/common/utils/FeignFallbackUtils.java @@ -0,0 +1,33 @@ +package com.ticketPing.order.common.utils; + +import com.ticketPing.order.common.exception.CircuitBreakerErrorCase; +import exception.ApplicationException; +import feign.FeignException; +import io.github.resilience4j.circuitbreaker.CallNotPermittedException; +import feign.RetryableException; + +public class FeignFallbackUtils { + + private FeignFallbackUtils() { + } + + public static T handleFallback(Throwable cause) { + if (cause instanceof CallNotPermittedException) { + throw new ApplicationException(CircuitBreakerErrorCase.SERVICE_IS_OPEN); + } + else if ( + cause instanceof FeignException.GatewayTimeout || + cause instanceof FeignException.ServiceUnavailable || + cause instanceof FeignException.BadGateway || + cause instanceof FeignException.TooManyRequests || + cause instanceof RetryableException + ) { + throw new ApplicationException(CircuitBreakerErrorCase.SERVICE_UNAVAILABLE); + } + else if (cause instanceof FeignException) { + throw (FeignException) cause; + } else { + throw new ApplicationException(CircuitBreakerErrorCase.SERVICE_UNAVAILABLE); + } + } +} diff --git a/services/order/src/main/java/com/ticketPing/order/infrastructure/client/PaymentFeignClient.java b/services/order/src/main/java/com/ticketPing/order/infrastructure/client/PaymentFeignClient.java index 07a90da0..eee471b3 100644 --- a/services/order/src/main/java/com/ticketPing/order/infrastructure/client/PaymentFeignClient.java +++ b/services/order/src/main/java/com/ticketPing/order/infrastructure/client/PaymentFeignClient.java @@ -1,7 +1,10 @@ package com.ticketPing.order.infrastructure.client; import com.ticketPing.order.application.client.PaymentClient; +import com.ticketPing.order.infrastructure.config.CustomFeignConfig; +import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -10,9 +13,16 @@ import java.util.UUID; -@FeignClient(name = "payment") +import static com.ticketPing.order.common.utils.FeignFallbackUtils.handleFallback; + +@FeignClient(name = "payment", configuration = CustomFeignConfig.class) public interface PaymentFeignClient extends PaymentClient { @GetMapping("/api/v1/payments/completed") + @CircuitBreaker(name = "paymentServiceCircuitBreaker", fallbackMethod = "fallbackForPaymentService") ResponseEntity> getCompletedPaymentByOrderId(@RequestParam("orderId") UUID orderId); + default ResponseEntity> fallbackForPaymentService(UUID orderId, Throwable cause) { + handleFallback(cause); + return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(null); + } } diff --git a/services/order/src/main/java/com/ticketPing/order/infrastructure/client/PerformanceFeignClient.java b/services/order/src/main/java/com/ticketPing/order/infrastructure/client/PerformanceFeignClient.java index 2223cd54..16f74a8c 100644 --- a/services/order/src/main/java/com/ticketPing/order/infrastructure/client/PerformanceFeignClient.java +++ b/services/order/src/main/java/com/ticketPing/order/infrastructure/client/PerformanceFeignClient.java @@ -2,7 +2,10 @@ import com.ticketPing.order.application.client.PerformanceClient; +import com.ticketPing.order.infrastructure.config.CustomFeignConfig; +import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import performance.OrderSeatResponse; @@ -10,17 +13,31 @@ import java.util.UUID; -@FeignClient(name = "performance") +import static com.ticketPing.order.common.utils.FeignFallbackUtils.handleFallback; + +@FeignClient(name = "performance", configuration = CustomFeignConfig.class) public interface PerformanceFeignClient extends PerformanceClient { @GetMapping("/api/v1/client/seats/{seatId}/order-info") + @CircuitBreaker(name = "performanceServiceCircuitBreaker", fallbackMethod = "fallbackForGetOrderInfo") ResponseEntity> getOrderInfo(@RequestHeader("X_USER_ID") UUID userId, @RequestParam("scheduleId") UUID scheduleId, @PathVariable("seatId") UUID seatId); @PostMapping("/api/v1/client/seats/{seatId}/extend-ttl") + @CircuitBreaker(name = "performanceServiceCircuitBreaker", fallbackMethod = "fallbackForExtendTTL") ResponseEntity> extendPreReserveTTL(@RequestParam("scheduleId") UUID scheduleId, @PathVariable("seatId") UUID seatId); + default ResponseEntity> fallbackForGetOrderInfo(UUID userId, UUID scheduleId, UUID seatId, Throwable cause) { + handleFallback(cause); + return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(null); + } + + default ResponseEntity> fallbackForExtendTTL(UUID scheduleId, UUID seatId, Throwable cause) { + handleFallback(cause); + return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(null); + } } + diff --git a/services/order/src/main/java/com/ticketPing/order/infrastructure/config/CustomFeignConfig.java b/services/order/src/main/java/com/ticketPing/order/infrastructure/config/CustomFeignConfig.java new file mode 100644 index 00000000..a534bbf3 --- /dev/null +++ b/services/order/src/main/java/com/ticketPing/order/infrastructure/config/CustomFeignConfig.java @@ -0,0 +1,14 @@ +package com.ticketPing.order.infrastructure.config; + +import feign.Request; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class CustomFeignConfig { + + @Bean + public Request.Options requestOptions() { + return new Request.Options(1000, 12000); + } +} diff --git a/services/order/src/main/resources/application.yml b/services/order/src/main/resources/application.yml index 22ffeb74..a5693fe0 100644 --- a/services/order/src/main/resources/application.yml +++ b/services/order/src/main/resources/application.yml @@ -14,6 +14,15 @@ spring: - "classpath:application-jpa.yml" - "classpath:application-kafka.yml" - "classpath:application-monitoring.yml" + - "classpath:application-circuit-breaker.yml" server: port: 10013 + +resilience4j: + circuitbreaker: + instances: + performanceServiceCircuitBreaker: + baseConfig: default + paymentServiceCircuitBreaker: + baseConfig: default diff --git a/settings.gradle b/settings.gradle index 467ee252..da4bb1cb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,6 +9,7 @@ include ("common:rdb") include ("common:caching") include ("common:messaging") include ("common:monitoring") +include ("common:circuit-breaker") include ("services:auth") include ("services:user")