Spring Cloud Circuit Breaker: Feign doesn't open circuit breaker - java

Circuit breaker works, fallback is called, but circuit breaker doesn't change it's state and every time send request to failed service.
Tried the same YAML config with rest template - works correctly.
Feign client
#FeignClient(
name = MyFeignClient.SERVICE_NAME,
url = "https://httpbin.org/",
configuration = {FeignClientConfiguration.class})
public interface MyFeignClient {
String SERVICE_NAME = "producer-service";
#GetMapping(value = "/status/502")
ResponseEntity<String> gerRequest();
}
Fallback class
public class MyFallback implements MyFeignClient {
private final Exception cause;
public MyFallback(Exception cause) {
this.cause = cause;
}
public ResponseEntity<String> gerRequest() {
if (cause instanceof HttpServerErrorException){
return ResponseEntity.of(Optional.of(cause.getMessage()));
} else {
return ResponseEntity.of(Optional.of(cause.getMessage()));
}
}
}
Feign client configuration
#RequiredArgsConstructor
public class FeignClientConfiguration {
private final CircuitBreakerRegistry registry;
#Bean
#Scope("prototype")
public Feign.Builder feignBuilder() {
CircuitBreaker circuitBreaker = registry.circuitBreaker("producer-service");
FeignDecorators decorators = FeignDecorators.builder()
.withCircuitBreaker(circuitBreaker)
.withFallbackFactory(MyFallback::new)
.build();
return Resilience4jFeign.builder(decorators);
}
}
Circuit breaker YAML config
resilience4j.circuitbreaker:
configs:
default:
registerHealthIndicator: true
slidingWindowType: COUNT_BASED
slidingWindowSize: 5
minimumNumberOfCalls: 3
permittedNumberOfCallsInHalfOpenState: 1
automaticTransitionFromOpenToHalfOpenEnabled: true
waitDurationInOpenState: 5s
failureRateThreshold: 50
eventConsumerBufferSize: 10
writableStackTraceEnabled: true
recordExceptions:
- org.springframework.web.client.HttpServerErrorException
- java.util.concurrent.TimeoutException
- java.io.IOException
shared:
slidingWindowSize: 100
permittedNumberOfCallsInHalfOpenState: 30
waitDurationInOpenState: 1s
failureRateThreshold: 50
eventConsumerBufferSize: 10
instances:
producer-service:
baseConfig: default

Related

Why the the reactive code is not executed in spring-boot?

I am using Spring boot to implement reactive micro-services. However, the reactive code is never executed in lambda function. My implementation as below. publishEventScheduler is created during application start-up. I am using this code together with Kafka to send an event to user micro-service to create user.
MainServiceApplication.java
public class MainServiceApplication {
private final Integer threadPoolSize;
private final Integer taskQueueSize;
public MainServiceApplication(
#Value("${app.threadPoolSize:10}") Integer threadPoolSize,
#Value("${app.taskQueueSize:100}") Integer taskQueueSize) {
this.threadPoolSize = threadPoolSize;
this.taskQueueSize = taskQueueSize;
}
#Bean
public Scheduler publishEventScheduler() {
LOG.info("Creating a message scheduler with connectionPoolSize = {}", threadPoolSize);
return Schedulers.newBoundedElastic(threadPoolSize, taskQueueSize, "publish-pool");
}
public static void main(String[] args) {
SpringApplication.run(MainServiceApplication.class, args);
}
}
MainIntegration.java
the function createUser() is called with a POST request from Postman (break point stop at subscribeOn(publishEventScheduler)) but sendMessageUser() is never executed (break point in the function not working)
#Component
public class MainIntegration implements UserService, TodoService {
private final String todoServiceUrl;
private final String userServiceUrl;
private final WebClient webClient;
private final StreamBridge streamBridge;
private final Scheduler publishEventScheduler;
public MainIntegration(
#Qualifier("publishEventScheduler") Scheduler publishEventScheduler,
WebClient.Builder webClient,
StreamBridge streamBridge,
#Value("${app.user-service.host}") String userServiceHost,
#Value("${app.user-service.port}") int userServicePort
) {
this.publishEventScheduler = publishEventScheduler;
this.webClient = webClient.build();
this.streamBridge = streamBridge;
userServiceUrl = "http://" + userServiceHost + ":" + userServicePort + "/user";
}
#Override
public Mono<User> createUser(User body) {
return Mono.fromCallable(() -> {
sendMessageUser("user-out-0", new Event<Event.Type, String, User >(Event.Type.CREATE, body.getUserName(), body));
return body;
}).subscribeOn(publishEventScheduler);
}
private void sendMessageUser(String bindingName, Event<Type, String, User> event) {
LOG.debug("Sending a {} message to {}", bindingName, event.getEventType());
Message<Event<Type, String, User>> message = MessageBuilder.withPayload(event)
.setHeader("partitionKey", event.getKey())
.build();
streamBridge.send(bindingName, message);
}
application.yaml
server.port: 7000
server.error.include-message: always
app:
user-service:
host: localhost
port: 7002
spring:
cloud:
stream:
default-binder: kafka
default-contentType: application/json
bindings:
user-out-0:
destination: user-service
producer:
required-groups: auditGroup
kafka:
binder:
brokers: 127.0.0.1
defaultBrokerPort: 2181
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest

Spring Reactive WebClient is not calling another service

I have 2 Spring Boot microservices. Microservice (B) calls a reactive api exposed by Microservice (A).
Microservice (A) RestController code :
#RestController
#RequestMapping(value = "/documents")
public class ElasticDocumentController {
private static final Logger LOG = LoggerFactory.getLogger(ElasticDocumentController.class);
private final ElasticQueryService elasticQueryService;
public ElasticDocumentController(ElasticQueryService queryService) {
this.elasticQueryService = queryService;
}
#GetMapping(value = "/", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ElasticQueryServiceResponseModel> getAllDocuments() {
Flux<ElasticQueryServiceResponseModel> response = elasticQueryService.getAllDocuments();
response = response.log();
LOG.info("Returning from query reactive service for all documents");
return response;
}
}
When I call the getAllDocuments() api from Postman, I can see the documents scrolling in the output cosole. So Microservice (A) is correct.
But when I call the api from Microservice (B), I cannot retrieve any documents. Microservice (B) cannot communicate with Microservice (A).
Microservice (B) Service code :
#Service
public class TwitterElasticQueryWebClient implements ElasticQueryWebClient {
private static final Logger LOG = LoggerFactory.getLogger(TwitterElasticQueryWebClient.class);
private final WebClient.Builder webClientBuilder;
private final ElasticQueryWebClientConfigData elasticQueryWebClientConfigData;
public TwitterElasticQueryWebClient(
#Qualifier("webClientBuilder") WebClient.Builder clientBuilder,
ElasticQueryWebClientConfigData configData
) {
this.webClientBuilder = clientBuilder;
this.elasticQueryWebClientConfigData = configData;
}
#Override
public Flux<ElasticQueryWebClientResponseModel> getAllData() {
LOG.info("Querying all data");
return webClientBuilder
.build()
.get()
.uri("/")
.accept(MediaType.valueOf(elasticQueryWebClientConfigData.getQuery().getAccept()))
.retrieve()
.bodyToFlux(ElasticQueryWebClientResponseModel.class);
}
}
Microservice (B) config code :
#Configuration
public class WebClientConfig {
private final ElasticQueryWebClientConfigData.WebClient webClientConfig;
public WebClientConfig(ElasticQueryWebClientConfigData webClientConfigData) {
this.webClientConfig = webClientConfigData.getWebClient();
}
#Bean("webClientBuilder")
WebClient.Builder webClientBuilder() {
return WebClient.builder()
.baseUrl(webClientConfig.getBaseUrl())
.defaultHeader(HttpHeaders.CONTENT_TYPE, webClientConfig.getContentType())
.defaultHeader(HttpHeaders.ACCEPT, webClientConfig.getAcceptType())
.clientConnector(new ReactorClientHttpConnector(HttpClient.from(getTcpClient())))
.codecs(configurer -> configurer.defaultCodecs()
.maxInMemorySize(webClientConfig.getMaxInMemorySize()));
}
private TcpClient getTcpClient() {
return TcpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, webClientConfig.getConnectTimeoutMs())
.doOnConnected(connection -> {
connection.addHandlerLast(new ReadTimeoutHandler(webClientConfig.getReadTimeoutMs(), TimeUnit.MILLISECONDS));
connection.addHandlerLast(new WriteTimeoutHandler(webClientConfig.getWriteTimeoutMs(), TimeUnit.MILLISECONDS));
});
}
}
Microservice (B) application.yml :
elastic-query-web-client:
webclient:
connect-timeout-ms: 10000
read-timeout-ms: 10000
write-timeout-ms: 10000
max-in-memory-size: 10485760 # 10MB
content-type: 'application/json'
accept-type: 'text/event-stream'
base-url: 'http://localhost:8183/reactive-elastic-query-service/documents'
query:
method: POST
uri: "/get-doc-by-text"
accept: ${elastic-query-web-client.webclient.accept-type}
server:
port: 8184
spring:
webflux:
base-path: /reactive-elastic-query-web-client
thymeleaf:
cache: false
reactive:
max-chunk-size: 8192
codec:
max-in-memory-size: 25MB
Microservice (B) controller :
#Controller
public class QueryController {
private static final Logger LOG = LoggerFactory.getLogger(QueryController.class);
private final ElasticQueryWebClient elasticQueryWebClient;
public QueryController(ElasticQueryWebClient webClient) {
this.elasticQueryWebClient = webClient;
}
#GetMapping("/all")
public String queryAll(Model model) {
Flux<ElasticQueryWebClientResponseModel> responseModels = elasticQueryWebClient.getAllData();
responseModels = responseModels.log();
IReactiveDataDriverContextVariable reactiveData = new ReactiveDataDriverContextVariable(responseModels, 1);
model.addAttribute("elasticQueryWebClientResponseModels", reactiveData);
model.addAttribute("searchText", "");
model.addAttribute("elasticQueryWebClientRequestModel", ElasticQueryWebClientRequestModel.builder().build());
LOG.info("Returning from reactive client controller for all data");
return "home";
}
}
There are no exceptions in the output consoles.
I don't see what I am missing here.

Micronaut testing with declarative HTTP client Junit 5

Trying to do the Junit 5 E2E functional testing using Micronaut declarative HTTP client.
public interface IProductOperation {
#Get(value = "/search/{text}")
#Secured(SecurityRule.IS_ANONYMOUS)
Maybe<?> freeTextSearch(#NotBlank String text);
}
Declarative micronaut HTTP client
#Client(
id = "feteBirdProduct",
path = "/product"
)
public interface IProductClient extends IProductOperation {
}
JUnit - 5 testing
#MicronautTest
public record ProductControllerTest(IProductClient iProductClient) {
#Test
#DisplayName("Should search the item based on the name")
void shouldSearchTheItemBasedOnTheName() {
var value = iProductClient.freeTextSearch("test").blockingGet();
System.out.println(value);
}
}
Controller
#Controller("/product")
public class ProductController implements IProductOperation {
private final IProductManager iProductManager;
public ProductController(IProductManager iProductManager) {
this.iProductManager = iProductManager;
}
#Override
public Maybe<List> freeTextSearch(String text) {
LOG.info("Controller --> Finding all the products");
return iProductManager.findFreeText(text);
}
}
When I run the test, I get a 500 internet server error. I think when I run the test the application is also running. Not sure what is the reason for 500 internal server error.
Any help will be appreciated
Is #Get(value = "/search/{text}") causing the issue ?. If yes how can I solve with the declarative client
Service discovery
application.yml
consul:
client:
defaultZone: ${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}
registration:
enabled: true
application-test.yml
micronaut:
server:
port: -1
http:
services:
feteBirdProduct:
urls:
- http://product
consul:
client:
registration:
enabled: false

Unit testing Spring Cloud Stream Producer-Processor-Consumer Scenario

I have created an sample app for producer-processor-consumer scenario using Spring Cloud Scenario. Here, I have used legacy annotation based approach.
In unit tests, I wanted to test simple scenario of producing a message and asserting consumed message after it undergoes transformation. But I am not receiving message at consumer binding end. Please let me know what could be missing here.
Producer.java
#EnableBinding(MyProcessor.class)
public class Producer {
#Bean
#InboundChannelAdapter(value = MyProcessor.OUTPUT, poller = #Poller(fixedDelay = "1000", maxMessagesPerPoll = "1"))
public MessageSource<String> produceMessage() {
return () -> new GenericMessage<>("Hello Spring Cloud World >>> " + Instant.now());
}
}
TransformProcessor.java
#EnableBinding(MyProcessor.class)
public class TransformProcessor {
#Transformer(inputChannel = MyProcessor.OUTPUT, outputChannel = MyProcessor.INPUT)
public String transform(String message) {
System.out.println("Transforming the message: " + message);
return message.toUpperCase();
}
}
Consumer.java
#EnableBinding(MyProcessor.class)
public class Consumer {
#StreamListener(MyProcessor.INPUT)
public void consume(String message) {
System.out.println("Consuming transformed message: " + message);
}
}
MyProcessor.java
public interface MyProcessor {
String INPUT = "my-input";
final static String OUTPUT = "my-output";
#Input(INPUT)
SubscribableChannel anInput();
#Output(OUTPUT)
MessageChannel anOutput();
}
SpringCloudStreamLegacyApplicationTests.java
#SpringBootTest
class SpringCloudStreamLegacyApplicationTests {
#Autowired
private MyProcessor myProcessor;
#Autowired
private MessageCollector messageCollector;
#Test
public void testConsumer() {
myProcessor.anOutput().send(new GenericMessage<byte[]>("hello".getBytes()));
Message<?> poll = messageCollector.forChannel(myProcessor.anInput()).poll();
System.out.println("Received: " + poll.getPayload());
}
}
Here, I am expecting a message to be received in handleMessage method.
Note, I am using following dependency for tests:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-test-support</artifactId>
<scope>test</scope>
</dependency>

Feign Registration - Spring Cloud - Change Target without ribbon over-ride

Introduction
I would like to be able to have two different spring profiles, and depending on the profile to change to a hardcoded address for our feign builders.
Currently was have the following:
return builder.target(cls, "http://" + serviceName);
But I would actually like to do the following and over-ride the address:
return builder.target(cls, "http://our-server:8009/" + serviceName);
Why
Sometimes we don't want to run all the services within our development environment. Additionally, some of the services are only available through a zuul gateway sometimes.
So we run the same code in different situations and conditions.
Technical Details
We have the following code that we use for building our Feign Clients.
We had been using the #FeignClient annotation in the past, but lately we decided to start building our feignClients manually.
Example below:
#FeignClient(name = "ab-document-store", configuration = MultiPartSupportConfiguration.class, fallback = DocumentStoreFallback.class)
We call the feignRegistrar class with the following command:
return registerFeignClient(DocumentStoreClient.class, true);
#RequiredArgsConstructor
//#Component
#Slf4j
public class FeignRegistrar {
#Autowired
private Decoder decoder;
#Autowired
private Encoder encoder;
#Autowired
private Client client;
#Autowired
private Contract feignContract;
#Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
#Autowired
private List<RequestInterceptor> interceptors;
public <T> T register(Class<T> cls, String serviceName, boolean isDocumentStore) {
if(isDocumentStore){
encoder = new MultipartFormEncoder(new SpringEncoder(messageConverters));
}
//Client trustSSLSockets = new Client.Default(getSSLSocketFactory(), new NoopHostnameVerifier());
Feign.Builder builder = Feign.builder()
.client(client)
.encoder(encoder)
.decoder(decoder)
.contract(feignContract)
.logger(new Slf4Logger())
.logLevel(Logger.Level.HEADERS);
builder.requestInterceptor(new RequestInterceptor() {
#Override
public void apply(RequestTemplate template) {
template.header("X-Service-Name", serviceName);
}
});
for(RequestInterceptor interceptor : interceptors) {
builder.requestInterceptor(interceptor);
}
log.debug("Registering {} - as feign proxy ", serviceName);
return builder.target(cls, "http://" + serviceName);
}
public static class Slf4Logger extends Logger {
#Override
protected void log(String configKey, String format, Object... args) {
log.info("{} - {}", configKey, args);
}
}
}
Spring Cloud Property Over-ride
We have also been using property files such as application-ENV.property with entries such as the following:
ab-document-store.ribbon.NIWSServerListClassName:com.netflix.loadbalancer.ConfigurationBasedServerList
ab-document-store.ribbon.listOfServers: localhost:8025
Unfortunately, listOfServers is not enough for us. We would like to be able to assign a directory/path as well. Something like:
ab-document-store.ribbon.listOfServers: localhost:8025/ab-document-store
Otherworkaround
I have thought about sneaking in a header into all requests such as X-SERVICE-NAME using a feign interceptor. Then we could point all services to an address (e.g. localhost:9001) , and forward/proxy those requests to localhost:9001/X-SERVICE-NAME.
However, I would prefer a much easier solution such as:
ab-document-store.ribbon.listOfServers: localhost:8025/ab-document-store
But this doesn't work :(
Introduction
I found a solution for this using a proxy that detects a header.
So, I have a feign interceptor on the java-side that attaches a header x-service-name to every feign-request.
I also have a NodeJS proxy, that analyzes requests, finds x-service-name, and re-writes the requests to become: x-service-name/originalRequestPath.
This allows me to have all the microservices behind a zuul gateway but also access them using a eureka-over-ride.
Java-Feign-Interceptor
Feign.Builder builder = Feign.builder()
.client(client)
.encoder(usedEncoder)
.decoder(decoder)
.contract(feignContract)
.logger(new Slf4Logger())
.logLevel(Logger.Level.HEADERS);
builder.requestInterceptor(new RequestInterceptor() {
#Override
public void apply(RequestTemplate template) {
template.header("X-Service-Name", serviceName);
}
});
NodeJS proxy
In the example, my zuul gateway ( or another proxy ) is on localhost:9001.
I'm listening on localhost:1200 .
let enableProxyForJava = process.env.ENABLE_PROXY_FOR_JAVA;
if (enableProxyForJava != undefined && enableProxyForJava.toLowerCase() === 'true') {
var httpProxyJava = require('http-proxy');
var proxyJava = httpProxyJava.createProxy();
gutil.log( gutil.colors.green('Enabling Proxy for Java. Set your Eureka overrides to localhost:1200.') );
require('http').createServer(function(req, res) {
console.log("req.headers['x-service-name'] = " + req.headers['x-service-name']);
console.log("Before req.url:"+ req.url);
if( req.headers['x-service-name'] != undefined){
let change = req.headers['x-service-name'] +req.url;
console.log("After req.url:"+ change);
req.url = change;
}
proxyJava.web(req, res, {
target: 'http://localhost:9001/'
});
}).listen(1200);
}
Property file inside Java Application that has feign clients
mbak-microservice1.ribbon.NIWSServerListClassName:com.netflix.loadbalancer.ConfigurationBasedServerList
mbak-microservice1.ribbon.listOfServers: localhost:1200
mbak-microservice2.ribbon.NIWSServerListClassName:com.netflix.loadbalancer.ConfigurationBasedServerList
mbak-microservice2.ribbon.listOfServers: localhost:1200
mbak-document-store.ribbon.NIWSServerListClassName:com.netflix.loadbalancer.ConfigurationBasedServerList
mbak-document-store.ribbon.listOfServers: localhost:1200

Categories

Resources