How to implement Circuit breaker in spring framework 6 declarative clients - spring

In a spring boot project, I would like to implement CicrcuitBreaker when connecting to 3rd party services using spring frameworks new declarative client
What I have done till now
Added below dependency in POM.xml as declarative client uses webclient underneath
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>
Added below properties in application.properties
resilience4j.circuitbreaker.configs.default.registerHealthIndicator=true
resilience4j.circuitbreaker.configs.default.slidingWindowSize=10
resilience4j.circuitbreaker.configs.default.minimumNumberOfCalls=5
resilience4j.circuitbreaker.configs.default.permittedNumberOfCallsInHalfOpenState=3
resilience4j.circuitbreaker.configs.default.automaticTransitionFromOpenToHalfOpenEnabled=true
resilience4j.circuitbreaker.configs.default.waitDurationInOpenState=5s
resilience4j.circuitbreaker.configs.default.failureRateThreshold=50
resilience4j.circuitbreaker.configs.default.eventConsumerBufferSize=10
resilience4j.circuitbreaker.configs.default.recordExceptions[0]=org.springframework.web.reactive.function.client.WebClientRequestException
resilience4j.circuitbreaker.configs.default.recordExceptions[1]=java.net.ConnectException
resilience4j.circuitbreaker.configs.default.recordExceptions[2]=java.io.IOException
resilience4j.timelimiter.configs.default.timeoutDuration=5s
resilience4j.timelimiter.configs.default.cancelRunningFuture=true
management.health.circuitbreakers.enabled=true
Created a Bean of type ReactiveResilience4JCircuitBreakerFactory as below.
#Bean
Customizer<ReactiveResilience4JCircuitBreakerFactory> defaultCustomizer() {
return factory ->
factory.configureDefault(
id ->
new Resilience4JConfigBuilder(id)
.circuitBreakerConfig(CircuitBreakerConfig.ofDefaults())
.timeLimiterConfig(TimeLimiterConfig.ofDefaults())
.build());
}
Added CircuitBreaker to the HttpServiceProxyFactory Bean as below
#Bean
public HttpServiceProxyFactory httpServiceProxyFactory(
WebClient.Builder builder,
ReactiveResilience4JCircuitBreakerFactory resilience4JCircuitBreakerFactory) {
CircuitBreaker circuitBreaker =
resilience4JCircuitBreakerFactory
.getCircuitBreakerRegistry()
.circuitBreaker("default");
WebClient webClient =
builder.baseUrl(applicationProperties.getInventoryServiceUrl())
.filter(
(request, next) ->
next.exchange(request)
.transform(
CircuitBreakerOperator.of(circuitBreaker)))
.defaultHeaders(
httpHeaders -> {
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
httpHeaders.setAccept(List.of(MediaType.APPLICATION_JSON));
})
.build();
return HttpServiceProxyFactory.builder(WebClientAdapter.forClient(webClient)).build();
}
When 3rd party service is down it is not giving any fallback response, how to fix this.
Another approach tried
Annotating the method with #CircuitBreaker and creating fallback method, still it is giving me exception.
#CircuitBreaker(
name = "getInventoryByProductCode",
fallbackMethod = "getInventoryByProductCodeFallBack")
private InventoryDto getInventoryByProductCode(String code) {
return inventoryServiceProxy.getInventoryByProductCode(code);
}
private InventoryDto getInventoryByProductCodeFallBack(String code, Exception e) {
log.error("Exception occurred while fetching product details", e);
return new InventoryDto(code, 0);
}
How to fix this?

Related

Is it possible to get PathPattern as a Bean in the SpringBoot web and reuse it in user code?

Is it possible to get PathPattern in the SpringBoot web as a Bean and reuse it in user code?
For example, if the url is : /user/1990/lily, it return the url patten on the Controller: /user/{year}/{name}.
This said:
Patterns are parsed on startup and re-used at runtime for efficient
URL matching
Reactor-netty metrics need a uriTagValue to avoid cardinality explosion,
public class Application {
public static void main(String[] args) {
Metrics.globalRegistry
.config()
.meterFilter(MeterFilter.maximumAllowableTags("reactor.netty.http.server", "URI", 100, MeterFilter.deny()));
DisposableServer server =
HttpServer.create()
.metrics(true, s -> { // HERE is the uriTagValue, it's a smaple of how to handle url mapping.
if (s.startsWith("/stream/")) {
return "/stream/{n}";
}
else if (s.startsWith("/bytes/")) {
return "/bytes/{n}";
}
return s;
})
.route(r ->
r.get("/stream/{n}",
(req, res) -> res.sendString(Mono.just(req.param("n"))))
.get("/bytes/{n}",
(req, res) -> res.sendString(Mono.just(req.param("n")))))
.bindNow();
server.onDispose()
.block();
}
}
Config the Netty to enable metrics in a SpringBoot WebFlux app:
#Configuration
public class NettyWebServerConfig {
#Bean
public ReactiveWebServerFactory reactiveWebServerFactory() {
NettyReactiveWebServerFactory factory = new NettyReactiveWebServerFactory();
factory.addServerCustomizers(httpServer -> httpServer
.wiretap(true)
.metrics(true, s -> "") // enable metrics, ignore all uri, if SpringBoot Web expose URI-Match-Patterns as Bean, we can use it here.
);
return factory;
}
}
My wondering is that is it possible to get PathPattern as a Bean in the SpringBoot web and reuse it in reactor-netty metrics code? As simmple as: bestPattern.matchAndExtract(lookupPath)
I tested PathContainer.parsePath(s);, it seems doesn't work.
With this setup, you are not using Spring WebFlux but actually Reactor Netty directly. PathContainer and PathPattern are then irrevelant here.
I don't think reactor-netty is storing anywhere the matching UriPathTemplate when considering the HttpPredicate.

ListenerExecutionFailedException Nullpointer when trying to index kafka payload through new ElasticSearch Java API Client

I'm migrating from the HLRC to the new client, things were smooth but for some reason I cannot index a specific class/document. Here is my client implementation and index request:
#Configuration
public class ClientConfiguration{
#Autowired
private InternalProperties conf;
public ElasticsearchClient sslClient(){
CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY,
new UsernamePasswordCredentials(conf.getElasticsearchUser(), conf.getElasticsearchPassword()));
HttpHost httpHost = new HttpHost(conf.getElasticsearchAddress(), conf.getElasticsearchPort(), "https");
RestClientBuilder restClientBuilder = RestClient.builder(httpHost);
try {
SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, (x509Certificates, s) -> true).build();
restClientBuilder.setHttpClientConfigCallback(new RestClientBuilder.HttpClientConfigCallback() {
#Override
public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpClientBuilder) {
return httpClientBuilder.setSSLContext(sslContext)
.setDefaultCredentialsProvider(credentialsProvider);
}
});
} catch (Exception e) {
e.printStackTrace();
}
RestClient restClient=restClientBuilder.build();
ElasticsearchTransport transport = new RestClientTransport(
restClient, new JacksonJsonpMapper());
ElasticsearchClient client = new ElasticsearchClient(transport);
return client;
}
}
#Service
public class ThisDtoIndexClass extends ConfigAndProperties{
public ThisDtoIndexClass() {
}
//client is declared in the class it's extending from
public ThisDtoIndexClass(#Autowired ClientConfiguration esClient) {
this.client = esClient.sslClient();
}
#KafkaListener(topics = "esTopic")
public void in(#Payload(required = false) customDto doc)
throws ThisDtoIndexClassException, ElasticsearchException, IOException {
if(doc!= null && doc.getId() != null) {
IndexRequest.Builder<customDto > indexReqBuilder = new IndexRequest.Builder<>();
indexReqBuilder.index("index-for-this-Dto");
indexReqBuilder.id(doc.getId());
indexReqBuilder.document(doc);
IndexResponse response = client.index(indexReqBuilder.build());
} else {
throw new ThisDtoIndexClassException("document is null");
}
}
}
This is all done in spring boot (v2.6.8) with ES 7.17.3. According to the debug, the payload is NOT null! It even fetches the id correctly while stepping through. For some reason, it throws me a org.springframework.kafka.listener.ListenerExecutionFailedException: in the last line (during the .build?). Nothing gets indexed, but the response comes back 200. I'm lost on where I should be looking. I have a different class that also writes to a different index, also getting a payload from kafka directly (all seperate consumers). That one functions just fine.
I suspect it has something to do with the way my client is set up and/or the kafka. Please point me in the right direction.
I solved it by deleting the default constructor. If I put it back it overwrites the extended constructor (or straight up doesn't acknowledge the extended constructor), so my client was always null. The error message it gave me was extremely misleading since it actually wasn't the Kafka's fault!
Removing the default constructor completely initializes the correct constructor and I was able to index again. I assume this was a spring boot loading related "issue".

Autowiring MongoClient and MongoClientSettings without explicitly specifying a Connection String

I am upgrading the MongoDB driver which requires moving away from the older MongoClientOptions to the newer MongoClientSettings.
In the older implementation, the following configuration was used within a #Configuration class with the ConnectionString inferred from the spring.data.mongodb.uri and an #Autowired MongoTemplate:
#Bean
public MongoClientOptions mongoOptions() {
Builder clientOptionsBuilder = MongoClientOptions.builder()
//Timeout Configurations
if(sslIsEnabled) {
clientOptionsBuilder.sslEnabled(true)
//Other SSL options
}
return clientOptionsBuilder.build();
}
And in the Newer Implementation, a ConnectionString parameter is specifically expected, and the property file spring.data.mongodb.uri is not selected automatically. As a result, I have specified the connection string using the #Value Annotation. Not doing this results in the program to infer localhost:27017 as the connection source.
#Value("${spring.data.mongodb.uri}")
String connectionString;
#Bean
public MongoClient mongoClient() {
MongoClientSettings.Builder clientSettingsBuilder = MongoClientSettings.builder()
.applyToSocketSettings(builder -> {
// Timeout Configurations
}).applyConnectionString(new ConnectionString(connectionString));
if (sSLEnabled) {
clientSettingsBuilder.applyToSslSettings(builder -> {
builder.enabled(sslIsEnabled);
//Other SSL Settings
});
}
return MongoClients.create(clientSettingsBuilder.build());
}
While documentation and other StackOverflow posts mention MongoClientSettings overrides the property file entries, is there a way to retrieve/infer the MongoClientSettings from the property files and then append other custom configurations to it?
I am using Spring Boot 2.6 and spring starter dependency for MongoDB
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
I found that a similar question asked on GitHub earlier.
Modify MongoClientSettings while using auto configuration with mongodb #20195
Replacing the #Bean configuration which had MongoClientOptions with MongoClientSettingsBuilderCustomizer helped solve the problem.
#Bean
public MongoClientSettingsBuilderCustomizer mongoDBDefaultSettings()
throws KeyManagementException, NoSuchAlgorithmException {
return builder -> {
builder.applyToSocketSettings(bldr -> {
//Apply any custom socket settings
});
builder.applyToSslSettings(blockBuilder -> {
// Apply SSL settings
});
// Apply other settings to the builder.
};
}

Handling spring reactor exceptions in imperative spring application

I'm using the webflux in an imperative spring boot application. In this app I need to make rest calls to various backends using webclient and wait on all the responses before proceeding to the next step.
ClassA
public ClassA
{
public Mono<String> restCall1()
{
return webclient....exchange()...
.retryWhen(Retry.backoff(maxAttempts, Duration.ofSeconds(minBackOff))
.filter(this::isTransient)
.onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> {
return new MyCustomException();
});
}
}
ClassB
public ClassB
{
public Mono<String> restCall2()
{
return webclient....exchange()...
.retryWhen(Retry.backoff(maxAttempts, Duration.ofSeconds(minBackOff))
.filter(this::isTransient)
.onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> {
return new MyCustomException();
});
}
}
Mono<String> a = classAObj.restCall1();
Mono<String> b = classBObj.restCall2();
ArrayList<Mono<String>> myMonos = new ArrayList<>;
myMonos.add(a);
myMonos.add(b);
try {
List<String> results = Flux.mergeSequential(myMonos).collectList().block();}
catch(WebclientResponseException e) {
....
}
The above code is working as expected. The Webclient is configured to throw error on 5xx and 4xx which I'm able to catch using WebclientResponseException.
The problem is I'm unable to catch any exceptions from the react framework. For example my web clients are configured to retry with exponential backoff and throw exception on exhaustion and I have no way to catch it in my try catch block above. I explored the option to handle that exceptiom in the webclient stream using onErrorReturn but it does not propagate the error back to my subscriber.
I also cannot add the exception to the catch block as it's never being thrown by any part of the code.
Can anyone advice what is the best way to handle these type of error. scenarios. I'm new to webflux and reactive programming.

spring webflux: purely functional way to attach websocket adapter to reactor-netty server

I am not able to figure out a way to attach a WebSocketHandlerAdapter to a reactor netty server.
Requirements:
I want to start a reactor netty server and attach http (REST) endpoints and websocket endpoints to the same server. I have gone through the documentation and some sample demo application mentioned in the documentation. They show how to attach a HttpHandlerAdapter to the the HttpServer using newHandler() function. But when it comes to websockets they switch back to using spring boot and annotation examples. I am not able to find how to attach websockets using functional endpoints.
Please point me in the right direction on how to implement this.
1. how do I attach the websocket adapter to the netty server?
2. Should I use HttpServer or TcpServer?
Note:
1. I am not using spring boot.
2. I am not using annotations.
3. Trying to achieve this only using functional webflux end points.
Sample code:
public HandlerMapping webSocketMapping()
{
Map<String, WebSocketHandler> map = new HashMap<>();
map.put("/echo", new EchoTestingWebSocketHandler());
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setUrlMap(map);
mapping.setOrder(-1);
return mapping;
}
public WebSocketHandlerAdapter wsAdapter()
{
HandshakeWebSocketService wsService = new HandshakeWebSocketService(new ReactorNettyRequestUpgradeStrategy());
return new WebSocketHandlerAdapter(wsService);
}
protected void startServer(String host, int port)
{
HttpServer server = HttpServer.create(host, port);
server.newHandler(wsAdapter()).block(); //how do I attach the websocket adapter to the netty server
}
Unfortunately, there is no easy way to do that without running up whole SpringBootApplication. Otherwise, you will be required to write whole Spring WebFlux handlers hierarchy by your self. Consider to compose your functional routing with SpringBootApplication:
#SpringBootApplication
public class WebSocketApplication {
public static void main(String[] args) {
SpringApplication.run(WebSocketApplication.class, args);
}
#Bean
public RouterFunction<ServerResponse> routing() {
return route(
POST("/api/orders"),
r -> ok().build()
);
}
#Bean
public HandlerMapping wsHandlerMapping() {
HashMap<String, WebSocketHandler> map = new HashMap<>();
map.put("/ws", new WebSocketHandler() {
#Override
public Mono<Void> handle(WebSocketSession session) {
return session.send(
session.receive()
.map(WebSocketMessage::getPayloadAsText)
.map(tMessage -> "Response From Server: " + tMessage)
.map(session::textMessage)
);
}
});
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setUrlMap(map);
mapping.setOrder(-1);
return mapping;
}
#Bean
HandlerAdapter wsHandlerAdapter() {
return new WebSocketHandlerAdapter();
}
}
Incase if SpringBoot infra is not the case
try to consider direct interaction with ReactorNetty instead. Reactor Netty Provides pritty good abstraction around native Netty and you may interacti with it in the same functional maner:
ReactorHttpHandlerAdapter handler =
new ReactorHttpHandlerAdapter(yourHttpHandlers);
HttpServer.create()
.startRouterAndAwait(routes -> {
routes.ws("/pathToWs", (in, out) -> out.send(in.receive()))
.file("/static/**", ...)
.get("**", handler)
.post("**", handler)
.put("**", handler)
.delete("**", handler);
}
);
I deal with it this way. and use native reactor-netty
routes.get(rootPath, (req, resp)->{
// doFilter check the error
return this.doFilter(request, response, new RequestAttribute())
.flatMap(requestAttribute -> {
WebSocketServerHandle handleObject = injector.getInstance(GameWsHandle.class);
return response
.header("content-type", "text/plain")
.sendWebsocket((in, out) ->
this.websocketPublisher3(in, out, handleObject, requestAttribute)
);
});
})
private Publisher<Void> websocketPublisher3(WebsocketInbound in, WebsocketOutbound out, WebSocketServerHandle handleObject, RequestAttribute requestAttribute) {
return out
.withConnection(conn -> {
// on connect
handleObject.onConnect(conn.channel());
conn.channel().attr(AttributeKey.valueOf("request-attribute")).set(requestAttribute);
conn.onDispose().subscribe(null, null, () -> {
conn.channel().close();
handleObject.disconnect(conn.channel());
// System.out.println("context.onClose() completed");
}
);
// get message
in.aggregateFrames()
.receiveFrames()
.map(frame -> {
if (frame instanceof TextWebSocketFrame) {
handleObject.onTextMessage((TextWebSocketFrame) frame, conn.channel());
} else if (frame instanceof BinaryWebSocketFrame) {
handleObject.onBinaryMessage((BinaryWebSocketFrame) frame, conn.channel());
} else if (frame instanceof PingWebSocketFrame) {
handleObject.onPingMessage((PingWebSocketFrame) frame, conn.channel());
} else if (frame instanceof PongWebSocketFrame) {
handleObject.onPongMessage((PongWebSocketFrame) frame, conn.channel());
} else if (frame instanceof CloseWebSocketFrame) {
conn.channel().close();
handleObject.disconnect(conn.channel());
}
return "";
})
.blockLast();
});
}

Resources