I'm creating a load balance feature on my project in which I have three server that will simultaneously ping for 15 seconds. However, when I already run my client-side, it always goes to the fallback page and received an error of "LoadBalancer [server]: Error choosing server for key default" even if the servers are already running.
Here are the codes in my project:
app.properties
server.port=8788
server.ribbon.eureka.enabled=false
server.ribbon.listOfServers=localhost:8787,localhost:8789,localhost:8790
#every 15 seconds
server.ribbon.ServerListRefreshInterval=15000
client service (wherein it is my fallback method)
private LoadBalancerClient loadBalancer;
private RestTemplate restTemplate;
public ClientService(RestTemplate rest) {
this.restTemplate = rest;
}
#HystrixCommand(fallbackMethod = "reliable")
public String login() {
ServiceInstance instance = loadBalancer.choose("server");
URI uri = URI.create(String.format("http://%s:%s/admin/ping", instance.getHost(), instance.getPort()));
//URI uri = URI.create("http://localhost:8787/admin/ping");
return this.restTemplate.getForObject(uri, String.class);
}
MainController
public class MainController{
private final static Logger LOGGER = LoggerFactory.getLogger(MainController.class);
#Autowired
private ClientService clientService;
#LoadBalanced
#Bean
public RestTemplate rest(RestTemplateBuilder builder) {
return builder.build();
}
#Autowired
RestTemplate restTemplate;
...
Client client = new Client();
WebResource resource = client.resource("http://%s:%s/auth/loginvalidate");
ClientResponse response = resource.type(MediaType.APPLICATION_JSON)
.header("Authorization", "Basic " + encodePw)
.get(ClientResponse.class);
I got rid of that error by doing two things:
1) Add the following properties to the remote service:
management.endpoints.web.exposure.include: "*"
management.endpoint.health.enabled: "true"
management.endpoint.restart.enabled: "true"
management.endpoint.info.enabled: "true"
2) Make sure that there is a ping endpoint in the remote service:
public class MainController{
#RequestMapping("/")
public String ribbonPing() {
return this.hostName;
}
}
I added a few amendments to the example provided by Kubernetes Circuit Breaker & Load Balancer Example to test this scenario and put in here.
I suggest that you follow those links as a kind of "best practises" guide in order to build your Hystrix/Ribbon solution. Pay special attention to:
the starters/dependencies added to the pom files
the structure of the Java classes (how and where each bean is declared and injected)
how you configure your (micro-)services (in this case with K8s ConfigMaps)
Related
I have been having issues trying to get endpoint mapping to work for my web service. I am using Tomcat to host my web service and I am using soapUI to send test messages to it.
Endpoint
#Endpoint
public class ProductEndpoint {
private static final String NAMESPACE_URL="http://com.springbootsoap.allapis";
#Autowired
ProductService productService;
#PayloadRoot(namespace = NAMESPACE_URL, localPart = "addProductRequest")
#ResponsePayload
public AddProductResponse addProduct(#RequestPayload AddProductRequest request) {
AddProductResponse response= new AddProductResponse();
ServiceStatus servicestatus=new ServiceStatus();
Product product=new Product();
BeanUtils.copyProperties(request.getProductInfo(),product);
productService.addProduct(product);
servicestatus.setStatus("Success");
servicestatus.setMessage("Content Added Successfully");
response.setServiceStatus(servicestatus);
return response;
}
#PayloadRoot(namespace = NAMESPACE_URL, localPart = "getProductByIdRequest")
#ResponsePayload
public GetProductResponse GetProduct(#RequestPayload GetProductByIdRequest request) {
GetProductResponse response=new GetProductResponse();
ProductInfo productInfo=new ProductInfo();
BeanUtils.copyProperties(productService.getProductById(request.getProductId()),productInfo);
response.setProductInfo(productInfo);
return response;
}
}
SoapUI
enter image description here
here is what I got in soapUi.
I do not have any idea what should I do to make it correct, I saw many questions regarding this problem but did not find any solution.
I also had the same issue. At that time I change the version of java to 1.8 in pom.xml file
I dont know if my configuration is wrong or something with mockwebserver.
I have method in one of my services
public UserResponse getUsername(String username) {
return webClient.get()
.uri("http://user-service/api/user",
uriBuilder -> uriBuilder.queryParam("username", username).build())
.retrieve()
.bodyToMono(UserResponse.class)
.block();
}
And I want to test this method by MockWebServer as below
public class CardWebClientTest {
private MockWebServer server;
private CardService service;
#BeforeEach
void setup() {
server = new MockWebServer();
WebClient webClient = WebClient.builder().baseUrl(server.url("").toString()).build();
service = new CardService(webClient);
}
#Test
void test() {
server.enqueue(new MockResponse().setBody("{\"id\": 12345}")
.addHeader("Content-Type", "application/json")
.setResponseCode(200));
var result = service.getUsername("john");
assertThat(result.getId()).isEqualTo(12345L);
}
Am I missing something? It's propably something trivial but I can't find solution.
CardService which is calling UserService are communicating in EurekaServer with loadbalancer but Im assuming MockWebServer can handle this without any further configuration or not - idk. I will appreciate any help.
UserResponse dto looks like this
public class UserResponse
private Long id;
I forgot to mention error message. It's always
Failed to resolve 'user-service' after 2 queries ; nested exception is java.net.UnknownHostException: Failed to resolve 'user-service' after 2 queries
Alright. Now I can see that mockwebserver has different hostname - it's calling http://kubernetes.docker.internal:52654/api/user and webclient is asking for http://user-service. But still I dont know how to configure mockwebserver to get response. I should set somehow hostname or maybe leave like that and just test webclient.get() to see if response is correct?
I have the below code in Billing service microservice:
#RestController
#RequestMapping("/billing")
public class WebController {
#Autowired
private BillingService service;
#GetMapping("/hi")
#CircuitBreaker(name="BillingServiceCapture", fallbackMethod = "hiFallback")
public String hi() {
return "Hello Khushboo!";
}
public String hiFallback() {
return "Hello Khushboo FallBack!";
}
Application.Properties file:
server.port=9191
spring.h2.console.enable=true
spring.application.name=billing-service
eureka.client.serviceurl.defaultzone=http://localhost:8761/eureka
eureka.instance.hostname=localhost
management.health.circuitbreakers.enabled=true
#actuator settings
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
resilience4j.circuitbreaker.instances.BillingServiceCapture.registerHealthIndicator=true
resilience4j.circuitbreaker.instances.BillingServiceCapture.eventConsumerBufferSize=10
resilience4j.circuitbreaker.instances.BillingServiceCapture.failureRateThreshold=20
resilience4j.circuitbreaker.instances.BillingServiceCapture.minimumNumberOfCalls=5
resilience4j.circuitbreaker.instances.BillingServiceCapture.automaticTransitionFromOpenToHalfOpenEnabled=true
resilience4j.circuitbreaker.instances.BillingServiceCapture.waitDurationInOpenState=5s
resilience4j.circuitbreaker.instances.BillingServiceCapture.permittedNumberOfCallsInHalfOpenState=3
resilience4j.circuitbreaker.instances.BillingServiceCapture.slidingWindowSize=10
resilience4j.circuitbreaker.instances.BillingServiceCapture.slidingWindowType=COUNT_BASED
However, if I send a Get Request: localhost:8765/billing/hi
I get Hello Khushboo message.
But when I stop the BillingService microservice and again send the same request, the circuit breaker method doesn't get invoked.
Also, while accessing the Actuator Health status, I do not see circuit breaker information in the status logs which I should see.
I even added the CircuitBreaker code in OrderService which actually calls the BillingService:
#CircuitBreaker(name="BillingServiceCapture", fallbackMethod = "getAllBillingDetails")
public TransactionResponse saveOrder(TransactionRequest request) {
Order order=request.getOrder();
Billing billing=request.getBilling();
billing.setOrderId(order.getId());
billing.setAmount(order.getPrice());
Order ordermade=orderRepo.save(order);
Billing billingresponse=billingproxy.getBillingDone(billing);
TransactionResponse response=null;
String responseStr= billingresponse.getPaymentStatus().equals("success")?"Payment processing successful":"Payment failed";
response=new TransactionResponse(order, billingresponse.getTransactionId(),billingresponse.getAmount(),responseStr);
return response;
}
public Billing getAllBillingDetails(Billing bill,Exception e) {
return new Billing(1000,"pass",101,102,1000);
}
When I call http://localhost:8765/order/bookorder - this throws a 500 internal server exception but CircuitBreaker is not called. The error is:
[503] during [GET] to [http://billing-service/billing/preparebill] [BillingProxy#getBillingDone(Billing)]: [Load balancer does not contain an instance for the service billing-service]
Please note for testing purpose I'm not starting BillingService so the instance is not available for OrderService Feign to call.
Any insights will be appreciated.
Thanks.
The fallback method should pass the Exception parameter and return the same type as the original method:
public String hiFallback(Exception e) {
return "Hello Khushboo FallBack!";
}
Some context
The project is a spring boot application (v 2.3.12) using spring-cloud-openfeign-core (v 2.2.8) to make REST requests.
The service has 2 clients
one that needs to make REST requests using a proxy
one that needs to make REST request without a proxy
I'm unable to get both client to work simultaneously, i.e. have requests work for both proxied and non-proxied resources.
Is it possible to use different configuration files to support this, I have tried something like this
Configuration class X
#bean
public Client client1() {
return new Client.Default(null, null);
}
Configuration class Y
#bean
public Client client2() {
return new Client.Proxied(null, null,
new Proxy(Proxy.Type.HTTP, new InetSocketAddress("10.0.0.1", 8080)));
}
Feign interface code looks something like this
#Service
#FeignClient(name = "service1", url = "internal-url", configuration = X.class)
#Headers("Content-Type: application/json")
public interface serviceClient1 {
#PostMapping(value = "/v1/path}")
Response1 update(#RequestBody Request1 request1);
}
#Service
#FeignClient(name = "service2", url = "external-url", configuration = Y.class)
#Headers("Content-Type: application/json")
public interface serviceClient2 {
#PostMapping(value = "/v1/path}")
Response2 update(#RequestBody Request2 request2);
}
It seems only one of the client beans is used when making requests.
I'm getting an http 403 forbidden error trying to delete an aws elasticsearch index via the java Jest(v6.3) elasticsearch client (which delegates the http calls to apache httpclient(v4.5.2) I know my permissions are setup correctly in AWS as I'm able to successfully use postman(with the help of AWS Signature authorization helper). however, with apache httpclient, when I issue the DELETE /{myIndexName} I receive the following error:
The request signature we calculated does not match the signature you provided.
Check your AWS Secret Access Key and signing method.
Consult the service documentation for details.
I'm signing the aws request by configuring the apache httpclient with an interceptor that signs the request.(The code below is for a Spring Framework #Configuration class that wires up the java Jest client and underlying apache httpclient) but I imagine if I used apache httpclient directly I'd experience the same issue.
#Configuration
public class ElasticSearchConfiguration {
#Autowired
private CredentialsProviderFactoryBean awsCredentialsProvider;
#Bean
public JestClient awsJestClient(#Value("${elasticsearch.url}") String connectionUrl) throws Exception {
com.amazonaws.auth.AWSCredentialsProvider provider = awsCredentialsProvider.getObject();
final com.google.common.base.Supplier<LocalDateTime> clock = () -> LocalDateTime.now(ZoneOffset.UTC);
final vc.inreach.aws.request.AWSSigner awsSigner = new vc.inreach.aws.request.AWSSigner(provider, "us-east-1", "es", clock);
final vc.inreach.aws.request.AWSSigningRequestInterceptor requestInterceptor = new vc.inreach.aws.request.AWSSigningRequestInterceptor(awsSigner);
final JestClientFactory factory = new JestClientFactory() {
#Override
protected HttpClientBuilder configureHttpClient(HttpClientBuilder builder) {
builder.addInterceptorLast(requestInterceptor);
return builder;
}
#Override
protected HttpAsyncClientBuilder configureHttpClient(HttpAsyncClientBuilder builder) {
builder.addInterceptorLast(requestInterceptor);
return builder;
}
};
factory.setHttpClientConfig(new HttpClientConfig
.Builder(connectionUrl)
.connTimeout(60000)
.multiThreaded(true)
.build());
return factory.getObject();
}
}
Since it's working with postman it points to the a signing error but I'm at a loss to where the discrepancy is occurring. The configuration above works for all apache httpclient requests besides http DELETE requests.
After a bunch of research I found some clues that pointed to the possibility that the presence of the Content-Length (length=0) in request issued to aws were causing the signature mismatch. I'm guessing that the signature done via the client interceptor was not factoring in the Content-Length header but since we were sending the Content-Length header to the aws server it was factoring it in and thus causing the signature mismatch. I believe this to be the case because I added an additional interceptor(before the AWS signing interceptor) that explicitly removes the Content-Length header for DELETE requests and the request goes through successfully. (i.e. I'm able to delete the index). Updated code below:
#Configuration
public class ElasticSearchConfiguration {
private static final Logger log = LoggerFactory.getLogger(ElasticSearchConfiguration.class);
#Autowired
private CredentialsProviderFactoryBean awsCredentialsProvider;
#Bean
public JestClient awsJestClient(#Value("${elasticsearch.url}") String connectionUrl) throws Exception {
com.amazonaws.auth.AWSCredentialsProvider provider = awsCredentialsProvider.getObject();
final com.google.common.base.Supplier<LocalDateTime> clock = () -> LocalDateTime.now(ZoneOffset.UTC);
final vc.inreach.aws.request.AWSSigner awsSigner = new vc.inreach.aws.request.AWSSigner(provider, "us-east-1", "es", clock);
final vc.inreach.aws.request.AWSSigningRequestInterceptor requestInterceptor = new vc.inreach.aws.request.AWSSigningRequestInterceptor(awsSigner);
final HttpRequestInterceptor removeDeleteMethodContentLengthHeaderRequestInterceptor = (request, context) -> {
if(request.getRequestLine().getMethod().equals("DELETE")) {
log.warn("intercepted aws es DELETE request, will remove 'Content-Length' header as it's presence invalidates the signature check on AWS' end");
request.removeHeaders("Content-Length");
}
};
final JestClientFactory factory = new JestClientFactory() {
#Override
protected HttpClientBuilder configureHttpClient(HttpClientBuilder builder) {
builder.addInterceptorLast(removeDeleteMethodContentLengthHeaderRequestInterceptor);
builder.addInterceptorLast(requestInterceptor);
return builder;
}
#Override
protected HttpAsyncClientBuilder configureHttpClient(HttpAsyncClientBuilder builder) {
builder.addInterceptorLast(removeDeleteMethodContentLengthHeaderRequestInterceptor);
builder.addInterceptorLast(requestInterceptor);
return builder;
}
};
factory.setHttpClientConfig(new HttpClientConfig
.Builder(connectionUrl)
.connTimeout(60000)
.multiThreaded(true)
.build());
return factory.getObject();
}
}