org.kiwiproject.registry.eureka.client.EurekaRegistryClient Maven / Gradle / Ivy
package org.kiwiproject.registry.eureka.client;
import static java.util.Objects.isNull;
import static org.kiwiproject.base.KiwiPreconditions.checkArgumentNotBlank;
import static org.kiwiproject.base.KiwiPreconditions.checkArgumentNotNull;
import static org.kiwiproject.base.KiwiThrowables.typeOfNullable;
import static org.kiwiproject.retry.KiwiRetryerPredicates.CONNECTION_ERROR;
import static org.kiwiproject.retry.KiwiRetryerPredicates.NO_ROUTE_TO_HOST;
import static org.kiwiproject.retry.KiwiRetryerPredicates.SOCKET_TIMEOUT;
import static org.kiwiproject.retry.KiwiRetryerPredicates.SSL_HANDSHAKE_ERROR;
import static org.kiwiproject.retry.KiwiRetryerPredicates.UNKNOWN_HOST;
import com.google.common.annotations.VisibleForTesting;
import jakarta.ws.rs.ServerErrorException;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.kiwiproject.jaxrs.KiwiGenericTypes;
import org.kiwiproject.registry.client.RegistryClient;
import org.kiwiproject.registry.client.ServiceInstanceFilter;
import org.kiwiproject.registry.eureka.common.EurekaInstance;
import org.kiwiproject.registry.eureka.common.EurekaResponseParser;
import org.kiwiproject.registry.eureka.common.EurekaRestClient;
import org.kiwiproject.registry.eureka.common.EurekaUrlProvider;
import org.kiwiproject.registry.eureka.config.EurekaConfig;
import org.kiwiproject.registry.model.NativeRegistryData;
import org.kiwiproject.registry.model.ServiceInstance;
import org.kiwiproject.retry.KiwiRetryer;
import org.kiwiproject.retry.WaitStrategies;
import org.kiwiproject.retry.WaitStrategy;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
/**
* {@link RegistryClient} implementation for looking up services from Eureka registry server.
*/
@Slf4j
public class EurekaRegistryClient implements RegistryClient {
/**
* This number is multiplied by the number of Eureka URLs to determine the number of attempts that will be made
* before giving up. For example, if there are two Eureka URLs, and the multiplier is 3, then a maximum of
* (2 * 3) = 6 attempts will be made.
*/
private static final int EUREKA_ATTEMPT_MULTIPLIER = 3;
private static final int RETRY_MULTIPLIER = 100;
private static final int RETRY_MAX_TIME = 30;
private static final TimeUnit RETRY_MAX_TIME_UNIT = TimeUnit.SECONDS;
private final EurekaRestClient client;
private final EurekaUrlProvider urlProvider;
private final KiwiRetryer clientRetryer;
private final EurekaConfig config;
public EurekaRegistryClient(EurekaConfig config, EurekaRestClient client) {
this.client = client;
this.urlProvider = new EurekaUrlProvider(config.getRegistryUrls());
var maxAttempts = urlProvider.urlCount() * EUREKA_ATTEMPT_MULTIPLIER;
this.clientRetryer = KiwiRetryer.builder()
.retryerId(config.getRetryId())
.processingLogLevel(config.getRetryProcessingLogLevel())
.exceptionLogLevel(config.getRetryExceptionLogLevel())
.exceptionPredicates(List.of(
CONNECTION_ERROR, NO_ROUTE_TO_HOST, SOCKET_TIMEOUT, SSL_HANDSHAKE_ERROR, UNKNOWN_HOST, temporaryServerSideStatusCodes()
))
.maxAttempts(maxAttempts)
.waitStrategy(getWaitStrategy())
.build();
this.config = config;
}
private static Predicate temporaryServerSideStatusCodes() {
return t -> {
if (t instanceof ServerErrorException serverErrorException) {
return checkIfDesiredStatusCodeValueIsFoundIn(serverErrorException);
}
var rootCause = ExceptionUtils.getRootCause(t);
if (rootCause instanceof ServerErrorException serverErrorException) {
return checkIfDesiredStatusCodeValueIsFoundIn(serverErrorException);
}
LOG.warn("Will NOT retry after receiving {} error considered to be not temporary",
typeOfNullable(t).orElse(""));
LOG.debug("Error corresponding to above message:", t);
return false;
};
}
private static boolean checkIfDesiredStatusCodeValueIsFoundIn(ServerErrorException exception) {
return switch (Response.Status.fromStatusCode(exception.getResponse().getStatus())) {
case BAD_GATEWAY, SERVICE_UNAVAILABLE, GATEWAY_TIMEOUT -> true;
default -> false;
};
}
/**
* Allow tests to override to make the wait time much smaller, useful in tests that simulate multiple retry attempts
*/
@VisibleForTesting
WaitStrategy getWaitStrategy() {
return WaitStrategies.exponentialWait(RETRY_MULTIPLIER, RETRY_MAX_TIME, RETRY_MAX_TIME_UNIT);
}
@Override
public Optional findServiceInstanceBy(String serviceName, String instanceId) {
checkArgumentNotBlank(instanceId, "The instance ID cannot be blank");
var instances = findAllServiceInstancesBy(serviceName);
return instances.stream()
.filter(instance -> instance.getInstanceId().equals(instanceId))
.findFirst();
}
@Override
public List findAllServiceInstancesBy(InstanceQuery query) {
checkArgumentNotNull(query, "The query cannot be null");
checkArgumentNotBlank(query.getServiceName(), "The service name cannot be blank");
var eurekaInstances = getRunningServiceInstancesFromEureka(query.getServiceName());
var includeNativeData = config.isIncludeNativeData()
? NativeRegistryData.INCLUDE_NATIVE_DATA : NativeRegistryData.IGNORE_NATIVE_DATA;
var serviceInstances = eurekaInstances.stream()
.map(eurekaInstance -> eurekaInstance.toServiceInstance(includeNativeData))
.toList();
return ServiceInstanceFilter.filterInstancesByVersion(serviceInstances, query);
}
private List getRunningServiceInstancesFromEureka(String vipAddress) {
var response = getRegisteredServicesFromEureka(vipAddress);
if (isNull(response)) {
return List.of();
}
return parseEurekaInstances(response);
}
private Response getRegisteredServicesFromEureka(String vipAddress) {
return clientRetryer.call(() -> {
var targetUrl = urlProvider.getCurrentEurekaUrl();
LOG.debug("Attempting to lookup {} using {}", vipAddress, targetUrl);
try {
return client.findInstancesByVipAddress(targetUrl, vipAddress);
} catch (Exception e) {
urlProvider.getNextEurekaUrl();
throw e;
}
});
}
@Override
public List retrieveAllRegisteredInstances() {
var response = getAllRegisteredServicesFromEureka();
if (isNull(response)) {
return List.of();
}
var eurekaInstances = parseEurekaInstances(response);
var includeNativeData = config.isIncludeNativeData()
? NativeRegistryData.INCLUDE_NATIVE_DATA : NativeRegistryData.IGNORE_NATIVE_DATA;
return eurekaInstances.stream()
.map(eurekaInstance -> eurekaInstance.toServiceInstance(includeNativeData))
.toList();
}
private static List parseEurekaInstances(Response response) {
var eurekaResponse = response.readEntity(KiwiGenericTypes.MAP_OF_STRING_TO_OBJECT_GENERIC_TYPE);
return EurekaResponseParser.parseEurekaApplicationsResponse(eurekaResponse)
.stream()
.filter(instance -> ServiceInstance.Status.UP.name().equals(instance.getStatus()))
.toList();
}
private Response getAllRegisteredServicesFromEureka() {
return clientRetryer.call(() -> {
var targetUrl = urlProvider.getCurrentEurekaUrl();
LOG.debug("Attempting to lookup all service instances using {}", targetUrl);
try {
return client.findAllInstances(targetUrl);
} catch (Exception e) {
urlProvider.getNextEurekaUrl();
throw e;
}
});
}
}