Skip to content

Commit

Permalink
Merge pull request #27 from tswlun002/feature/add-circuit-breaker-pat…
Browse files Browse the repository at this point in the history
…tern-for-services

Feature/add circuit breaker pattern for services
  • Loading branch information
tswlun002 authored Jul 17, 2024
2 parents 83a3af0 + 07ffa23 commit ab06ae7
Show file tree
Hide file tree
Showing 19 changed files with 335 additions and 42 deletions.
1 change: 1 addition & 0 deletions config-server/src/main/resources/config/documents.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ download:
message:
successful: "Document is uploaded."
fail: "Failed to upload document."

10 changes: 9 additions & 1 deletion config-server/src/main/resources/config/gatewayserver.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
server:
port: 8072
port: 8072
resilience4j.circuitbreaker:
configs:
default:
register-health-indicator: true
sliding-window-size: 5
wait-duration-in-open-state: 20000
permitted-number-of-calls-in-half-open-state: 3
failure-rate-threshold: 60
4 changes: 4 additions & 0 deletions document-service/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,39 @@
package com.documentservice.config;

import com.documentservice.exception.CustomErrorDecoder;
import feign.FeignException;
import feign.codec.ErrorDecoder;
import io.github.resilience4j.core.IntervalFunction;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import io.github.resilience4j.retry.RetryRegistry;
import io.github.resilience4j.timelimiter.TimeLimiterConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.modelmapper.ModelMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory;
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JConfigBuilder;
import org.springframework.cloud.client.circuitbreaker.Customizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeoutException;

@Configuration
public class Config {
@Value("${rest-template.pool-size}")
private int POOL_SIZE;
@Value("${rest-template.timeout}")
private int TIME_OUT;
private final static Logger LOGGER = LoggerFactory.getLogger(Config.class);
@Bean
public HttpComponentsClientHttpRequestFactory requestFactory() {
return new HttpComponentsClientHttpRequestFactory();
Expand All @@ -44,12 +60,35 @@ public RestTemplate restTemplate(){
requestFactory.setConnectTimeout(TIME_OUT);
return new RestTemplate(requestFactory);
}
@Bean
@Bean
public ErrorDecoder errorDecoder() {
return new CustomErrorDecoder();
}

@Bean
public Retry retry(RetryRegistry registry){

var config= RetryConfig.custom()
.maxAttempts(3)
.intervalFunction(IntervalFunction.ofExponentialBackoff(Duration.of(100, ChronoUnit.MILLIS),2))
.retryOnException(e -> e.getCause() instanceof TimeoutException || e.getCause() instanceof FeignException.ServiceUnavailable)
.build();
;
return registry.retry("users",config) ;
}

@Bean
ModelMapper modelMapper(){
return new ModelMapper();
}

@Bean
public Customizer<ReactiveResilience4JCircuitBreakerFactory> circuitBreakerFactoryCustomizer(){
return factory->factory.configureDefault(
id->new Resilience4JConfigBuilder(id)
.timeLimiterConfig(
TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(10)).build()
)
.build() );
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
package com.documentservice.exception;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Builder;

import java.io.Serializable;

@Builder
public record AppException(

public record AppException (
String statusCodeMessage,
String message,
String path,
String timestamp,
int status
) {
) implements Serializable{

public String toJson() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
return objectMapper.writeValueAsString(this);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,12 @@ protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(HttpRequest
return new ResponseEntity<>(exc, HttpStatus.METHOD_NOT_ALLOWED);
}
@ExceptionHandler({InternalServerError.class})
public ResponseEntity<?> InternalException(InternalServerError exception, HttpServletRequest request){
public ResponseEntity<?> InternalException(InternalServerError exception,final HttpServletRequest request){
var exc = AppException.builder()
.status(HttpStatus.INTERNAL_SERVER_ERROR.value())
.statusCodeMessage(HttpStatus.INTERNAL_SERVER_ERROR.name())
.message(exception.getMessage()).
path(request.getContextPath())
path(request.getRequestURI())
.timestamp(LocalDateTime.now().toString())
.build();
logger.error("Internal server exception exception: {} , trace-Id: {} ", exc,request.getHeader("trace-Id"));
Expand All @@ -111,7 +111,7 @@ public ResponseEntity<?> Exits(EntityAlreadyExistException exception, HttpServ
.status(HttpStatus.CONFLICT.value())
.statusCodeMessage(HttpStatus.CONFLICT.name())
.message(exception.getMessage()).
path(request.getContextPath())
path(request.getRequestURI())
.timestamp(LocalDateTime.now().toString())
.build();
logger.error("Duplicated entity exception: {}, trace-Id: {} ", exc,request.getHeader("trace-Id"));
Expand All @@ -124,7 +124,7 @@ public ResponseEntity<?> Invalid(InvalidDocument exception, HttpServletRequest
.status(HttpStatus.BAD_REQUEST.value())
.statusCodeMessage(HttpStatus.BAD_REQUEST.name())
.message(exception.getMessage()).
path(request.getContextPath())
path(request.getRequestURI())
.timestamp(LocalDateTime.now().toString())
.build();
logger.error("Invalid entity exception: {}, trace-Id: {} ", exc,request.getHeader("trace-Id"));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.documentservice.exception;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import feign.Response;
import feign.codec.ErrorDecoder;
Expand All @@ -16,39 +17,40 @@ public class CustomErrorDecoder implements ErrorDecoder {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomErrorDecoder.class);
@Override
public Exception decode(String s, Response response) {

var api = s.split("#")[0];
var entity = api.substring(0,api.length()-3);
AppException message = null;
AppException error = null;
ObjectMapper mapper = new ObjectMapper().
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
try (InputStream bodyIs = response.body()
.asInputStream()) {
ObjectMapper mapper = new ObjectMapper();
message = mapper.readValue(bodyIs, AppException.class);

error = mapper.readValue(bodyIs, AppException.class);
} catch (IOException e) {
LOGGER.error(e.getMessage(),e);
return new Exception(e.getMessage());
}
LOGGER.info("Error: {}",message);
var isErrorMessage=message.message()!=null;
switch (response.status()){

case 400->{
LOGGER.error(isErrorMessage?"Error: "+message:entity+" invalid request.");
throw new InvalidRequestException(isErrorMessage? message.message():entity+" invalid request.");
LOGGER.error("User service error: {}",error);
var isErrorMessage=error.message()!=null;
var traceId=response.request().headers().get("trace-id");
switch (error.status()) {
case 400 -> {
LOGGER.error("User invalid request, trace-id: {}",traceId);
throw new InvalidRequestException(" invalid request.");
}
case 404->{
LOGGER.error(isErrorMessage?"Error: "+message:entity+" is not found.");
throw new EntityNotFoundException(isErrorMessage? message.message():entity+" is not found.");
case 404 -> {
LOGGER.error(isErrorMessage ? "Error: " + error : "User is not found, trace-id: {}",traceId);
throw new EntityNotFoundException(isErrorMessage ? error.message() : "User is not found.");
}
case 409 ->{
LOGGER.error(isErrorMessage?"Error: "+message:entity+" already exists.");
throw new EntityAlreadyExistException(isErrorMessage? message.message():entity+" already exists.");
case 409 -> {
LOGGER.error(isErrorMessage ? "Error: " + error : "User already exists, trace-id: {}",traceId);
throw new EntityAlreadyExistException(isErrorMessage ? error.message() : "User already exists.");
}
default -> {
LOGGER.error(isErrorMessage?"Error: "+message:entity+" service internal server error.");
throw new InternalServerError(isErrorMessage? message.message():entity+" service internal server error.");
}

LOGGER.error("Unexpected Error: trace-id:{}, error: {}",traceId,error);
throw new InternalServerError(isErrorMessage ? error.message() : "User service internal server error.");

}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.documentservice.exception;

public class UserServiceException extends RuntimeException {
public UserServiceException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

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

@RestController
@RequestMapping( "pdf-editor/documents/")
Expand All @@ -34,6 +33,7 @@ public ResponseEntity<?> saveDocument(@RequestHeader("trace-Id") String traceId,
var save= service.saveDocument(traceId,new UserDocument(email,pdf.toByteArray(),name+"_file_" ));
return new ResponseEntity<>(save?"Document is uploaded":"Failed to upload document",save?OK:NOT_ACCEPTABLE);
}

@GetMapping("download/{email}/{id}")
public ResponseEntity<?> downloadDocument(@RequestHeader("trace-Id") String traceId,@PathVariable("email")
@Email(message = "Email must be valid email address")String email,@PathVariable("id")String id){
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,44 @@
package com.documentservice.user;

import com.documentservice.exception.InvalidUser;
import com.documentservice.exception.InternalServerError;
import feign.RetryableException;
import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.retry.annotation.Retry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;

@FeignClient("gatewayserver")
import java.util.concurrent.TimeoutException;

@FeignClient(value = "users")
public interface UserApi {
@GetMapping(value = "/pdf-editor/users/{username}")
ResponseEntity<UserDto> getUser(@RequestHeader("trace-Id") String traceId, @PathVariable("username") String username) throws InvalidUser;
@CircuitBreaker(name = "users", fallbackMethod = "getUserFallback")
@Retry(name = "users")
@GetMapping(value = "/pdf-editor/users/{username}",produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<?> getUser(@RequestHeader("trace-Id") String traceId, @PathVariable("username") String username);
default ResponseEntity<String> getUserFallback( @RequestHeader("trace-Id") String traceId, @PathVariable("username") String username,TimeoutException ex){
final Logger logger = LoggerFactory.getLogger(UserApi.class);
logger.error("User service is not available, error: {}, trace-id:{}",ex.getCause(),traceId,ex);
throw new InternalServerError( "Request timed out, please try again sometime later.");
}
default ResponseEntity<String> getUserFallback(@RequestHeader("trace-Id") String traceId, @PathVariable("username") String username, RetryableException ex) {
final Logger logger = LoggerFactory.getLogger(UserApi.class);
logger.error("User service is not available, error: {}, trace-id:{}",ex,traceId);
throw new InternalServerError("Service is down, please try again sometime later.");

}
default ResponseEntity<String> getUserFallback( @RequestHeader("trace-Id") String traceId, @PathVariable("username") String username,CallNotPermittedException ex){
final Logger logger = LoggerFactory.getLogger(UserApi.class);
logger.error("User service is not available, error: {}, trace-id:{}",ex.getCause(),traceId,ex);
var string = String.format("Hello %s!, we notice your request please try sometime later or contact the support team ", username);
throw new InternalServerError(string);

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
public class UserImpl implements User {
private final UserApi userApi;
private final ModelMapper mapper;
private static Logger logger = LoggerFactory.getLogger(UserImpl.class);
private static final Logger logger = LoggerFactory.getLogger(UserImpl.class);
@Override
public UserDto getUser(String traceId,String username) throws InvalidUser {
logger.info("Getting user by username {}, trace-Id: {}", username,traceId);
Expand Down
Loading

0 comments on commit ab06ae7

Please sign in to comment.