All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.kiwiproject.registry.eureka.server.EurekaRegistryService Maven / Gradle / Ivy

package org.kiwiproject.registry.eureka.server;

import static com.google.common.base.Preconditions.checkState;
import static jakarta.ws.rs.core.Response.Status.NO_CONTENT;
import static jakarta.ws.rs.core.Response.Status.OK;
import static java.time.ZoneOffset.UTC;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static org.kiwiproject.base.KiwiStrings.f;
import static org.kiwiproject.base.KiwiStrings.format;
import static org.kiwiproject.collect.KiwiLists.isNotNullOrEmpty;
import static org.kiwiproject.jaxrs.KiwiEntities.safeReadEntity;
import static org.kiwiproject.jaxrs.KiwiResponses.closeQuietly;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import jakarta.ws.rs.core.Response;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.kiwiproject.base.KiwiEnvironment;
import org.kiwiproject.base.Optionals;
import org.kiwiproject.jaxrs.KiwiGenericTypes;
import org.kiwiproject.registry.config.ServiceInfo;
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.EurekaRegistrationConfig;
import org.kiwiproject.registry.exception.RegistrationException;
import org.kiwiproject.registry.model.NativeRegistryData;
import org.kiwiproject.registry.model.ServiceInstance;
import org.kiwiproject.registry.model.ServiceInstance.Status;
import org.kiwiproject.registry.server.RegistryService;
import org.kiwiproject.retry.SimpleRetryer;

import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Supplier;

@Slf4j
public class EurekaRegistryService implements RegistryService {

    /**
     * @implNote Need to specify the override zone via {@link DateTimeFormatter#withZone(ZoneId)}, otherwise when trying
     * to format a {@link Date} the formatter will fail with the error:
     * {@code "java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: YearOfEra"}
     * 

* This is because {@link DateTimeFormatter#format(TemporalAccessor)} accepts a {@link java.time.temporal.TemporalAccessor} * of which {@link Date} is definitely not an instance. You therefore need to convert the Date to an Instant and * then call format. However, a time zone is required to format an Instant, thus this is an easy way to do it * globally for any Date instance that is converted to an Instant. */ @VisibleForTesting static final DateTimeFormatter APP_TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss.SSS").withZone(UTC); /** * Funny story ... this class doesn't actually exist. It's for "backwards compatibility". *

* See com.netflix.discovery.converters.jackson.DataCenterTypeInfoResolver for details. * (NOTE: javadoc annotation not used to avoid unnecessary errors.) */ public static final String DEFAULT_DATA_CENTER_INFO_CLASS = "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo"; public static final String DEFAULT_DATA_CENTER_NAME = "MyOwn"; /** * Key for lease info representing lease duration in seconds. */ public static final String LEASE_DURATION_IN_SECONDS = "durationInSecs"; /** * Key for lease info representing renewal interval in seconds. */ public static final String LEASE_RENEWAL_INTERVAL_IN_SECONDS = "renewalIntervalInSecs"; private static final String SERVICE_UP_TIMESTAMP_FIELD = "serviceUpTimestamp"; /** * Maximum number of attempts we will make to await confirmation that we're registered with Eureka. */ public static final int MAX_AWAIT_REGISTRATION_CONFIRMATION_TRIES = 10; /** * Maximum number of attempts we will make trying to register with Eureka (via POST) */ public static final int MAX_REGISTRATION_ATTEMPTS = 60; /** * Value for delay to wait if a call to Eureka fails. */ private static final long RETRY_DELAY = 1; /** * Unit for delay to wait if a call to Eureka fails. */ private static final TimeUnit RETRY_DELAY_UNIT = TimeUnit.SECONDS; /** * Value for delay when un-registering if a call to Eureka fails. */ private static final long UNREGISTER_RETRY_DELAY = 3; /** * Time unit for delay when un-registering if a call to Eureka fails. */ private static final TimeUnit UNREGISTER_RETRY_DELAY_UNIT = TimeUnit.SECONDS; /** * Maximum number of attempts we will make to un-register from Eureka (via DELETE request). */ private static final int MAX_UNREGISTER_ATTEMPTS = 5; /** * Maximum number of attempts we will make to update status in Eureka (via PUT request). */ private static final int MAX_UPDATE_STATUS_ATTEMPTS = 5; private final EurekaRegistrationConfig config; private final EurekaRestClient client; private final SimpleRetryer registerRetryer; private final SimpleRetryer awaitRetryer; private final SimpleRetryer updateStatusRetryer; private final SimpleRetryer unregisterRetryer; private final KiwiEnvironment environment; private final EurekaUrlProvider urlProvider; @VisibleForTesting @Getter(AccessLevel.PACKAGE) final AtomicReference registeredInstance; @VisibleForTesting final AtomicReference heartbeatExecutor; @VisibleForTesting AtomicLong heartbeatCount; private final boolean trackHeartbeats; public EurekaRegistryService(EurekaRegistrationConfig config, EurekaRestClient client, KiwiEnvironment environment) { this(config, client, environment, SimpleRetryer.builder() .environment(environment) .maxAttempts(MAX_REGISTRATION_ATTEMPTS) .retryDelayTime(RETRY_DELAY) .retryDelayUnit(RETRY_DELAY_UNIT) .build(), SimpleRetryer.builder() .environment(environment) .maxAttempts(MAX_AWAIT_REGISTRATION_CONFIRMATION_TRIES) .retryDelayTime(RETRY_DELAY) .retryDelayUnit(RETRY_DELAY_UNIT) .build(), SimpleRetryer.builder() .environment(environment) .maxAttempts(MAX_UPDATE_STATUS_ATTEMPTS) .retryDelayTime(RETRY_DELAY) .retryDelayUnit(RETRY_DELAY_UNIT) .build(), SimpleRetryer.builder() .environment(environment) .maxAttempts(MAX_UNREGISTER_ATTEMPTS) .retryDelayTime(UNREGISTER_RETRY_DELAY) .retryDelayUnit(UNREGISTER_RETRY_DELAY_UNIT) .build()); } public EurekaRegistryService(EurekaRegistrationConfig config, EurekaRestClient client, KiwiEnvironment environment, SimpleRetryer registerRetryer, SimpleRetryer awaitRetryer, SimpleRetryer updateStatusRetryer, SimpleRetryer unregisterRetryer) { this.config = config; this.client = client; this.environment = environment; this.urlProvider = new EurekaUrlProvider(config.getRegistryUrls()); this.registeredInstance = new AtomicReference<>(); this.heartbeatExecutor = new AtomicReference<>(); this.registerRetryer = registerRetryer; this.awaitRetryer = awaitRetryer; this.updateStatusRetryer = updateStatusRetryer; this.unregisterRetryer = unregisterRetryer; this.trackHeartbeats = config.isTrackHeartbeats(); if (trackHeartbeats) { heartbeatCount = new AtomicLong(0); } } @Override public ServiceInstance createCandidateFrom(ServiceInfo serviceInfo) { return ServiceInstance.fromServiceInfo(serviceInfo) .withStatus(Status.STARTING); } @Override public ServiceInstance register(ServiceInstance serviceToRegister) { checkState(isNotRegistered(), "Cannot register. Already managing a registered instance: %s", registeredInstance.get()); // NOTE: We are calling `toUpperCase()` because the Eureka server will do it on the server side anyway, so this keeps it consistent here. var appId = f("{}-{}", serviceToRegister.getServiceName(), APP_TIMESTAMP_FORMATTER.format(environment.currentInstant())) .toUpperCase(Locale.getDefault()); registerWithEureka(appId, serviceToRegister); var registeredInstanceFromEureka = waitForInstanceToBeRegistered(appId, serviceToRegister.getHostName()); LOG.trace("Received instance registered with Eureka: {}", registeredInstanceFromEureka); // I'm pretty sure this can never happen since waitForInstanceToBeRegistered will throw an exception after the retries run out if (isNull(registeredInstanceFromEureka)) { return null; } registeredInstance.set(registeredInstanceFromEureka); LOG.info("Successful registration of app {}, instance {} with vip address {}", registeredInstanceFromEureka.getApp(), registeredInstanceFromEureka.getInstanceId(), registeredInstanceFromEureka.getVipAddress()); startHeartbeat(); var includeNativeData = config.isIncludeNativeData() ? NativeRegistryData.INCLUDE_NATIVE_DATA : NativeRegistryData.IGNORE_NATIVE_DATA; return registeredInstanceFromEureka.toServiceInstance(includeNativeData); } private void startHeartbeat() { if (nonNull(heartbeatExecutor.get())) { shutdownHeartbeat(); } var heartbeatInterval = config.getHeartbeatIntervalInSeconds(); LOG.debug("Starting heartbeat with interval {} seconds", heartbeatInterval); heartbeatExecutor.set(newHeartbeatExecutor()); heartbeatExecutor.get().scheduleWithFixedDelay( new EurekaHeartbeatSender(client, this, registeredInstance.get(), urlProvider, this::updateHeartbeatCount), heartbeatInterval, heartbeatInterval, TimeUnit.SECONDS); } private void updateHeartbeatCount() { if (trackHeartbeats) { heartbeatCount.incrementAndGet(); } } private void shutdownHeartbeat() { var executor = heartbeatExecutor.get(); if (isNull(executor)) { LOG.trace("Heartbeat executor was null; nothing to shut down."); return; } LOG.info("Shutting heartbeat executor down: {}", executor); var tasks = executor.shutdownNow(); if (isNotNullOrEmpty(tasks)) { LOG.info("There are {} task(s) that never started for heartbeat executor: {}", tasks.size(), executor); } heartbeatExecutor.set(null); if (trackHeartbeats) { heartbeatCount.set(0); } } private static ScheduledExecutorService newHeartbeatExecutor() { var threadFactory = new ThreadFactoryBuilder() .setNameFormat("eureka-heartbeat-%d") .setDaemon(true) .build(); return Executors.newScheduledThreadPool(1, threadFactory); } private void registerWithEureka(String appId, ServiceInstance serviceToRegister) { var eurekaInstance = EurekaInstance.fromServiceInstance(serviceToRegister) .withApp(appId) .withStatus(Status.UP.name()) .withDataCenterInfo(Map.of( "name", DEFAULT_DATA_CENTER_NAME, "@class", DEFAULT_DATA_CENTER_INFO_CLASS )) .withLeaseInfo(Map.of( LEASE_DURATION_IN_SECONDS, config.getExpirationIntervalInSeconds(), LEASE_RENEWAL_INTERVAL_IN_SECONDS, config.getHeartbeatIntervalInSeconds(), SERVICE_UP_TIMESTAMP_FIELD, System.currentTimeMillis() )); var registrationFunction = registrationSender(appId, eurekaInstance); var responseSupplier = eurekaCallRetrySupplier(registrationFunction, NO_CONTENT.getStatusCode()); var responseOptional = registerRetryer.tryGetObject("registration Response", responseSupplier); Optionals.ifPresentOrElseThrow(responseOptional, resp -> { LOG.info("Registration for app {} has been received by Eureka", appId); var entity = safelyReadEntity(resp); LOG.debug("Response from server: Status [{}], Body {}", resp.getStatus(), entity); }, () -> { var errMsg = format("Received errors or non-204 responses on ALL %s attempts to register (via POST) with Eureka", MAX_REGISTRATION_ATTEMPTS); return new RegistrationException(errMsg); } ); } private Function registrationSender(String appId, EurekaInstance candidate) { return eurekaUrl -> { try { return client.register(eurekaUrl, appId, candidate); } catch (Exception e) { LOG.error("Failed to register app {} with body {} to Eureka at {}", appId, candidate, eurekaUrl, e); return null; } }; } private EurekaInstance waitForInstanceToBeRegistered(String appId, String instanceId) { LOG.debug("Wait for registration to show in Eureka for app {}, instance {}", appId, instanceId); var instanceGetterFunction = instanceRequester(appId, instanceId); var responseOptional = awaitRetryer.tryGetObject("await registration Response", eurekaCallRetrySupplier(instanceGetterFunction, OK.getStatusCode())); return responseOptional .map(resp -> { var eurekaResponse = resp.readEntity(KiwiGenericTypes.MAP_OF_STRING_TO_OBJECT_GENERIC_TYPE); return EurekaResponseParser.parseEurekaInstanceResponse(eurekaResponse); }) .orElseThrow(() -> { LOG.error("Registration failed, or there is some other problem getting app {}, instance {}", appId, instanceId); var errMsg = format("Unable to obtain app %s, instance %s from Eureka during registration after %s attempts", appId, instanceId, MAX_AWAIT_REGISTRATION_CONFIRMATION_TRIES); return new RegistrationException(errMsg); }); } private Function instanceRequester(String appId, String instanceId) { return eurekaUrl -> { try { return client.findInstance(eurekaUrl, appId, instanceId); } catch (Exception e) { LOG.error("Failed to get instance with appId {}, instanceId {} from Eureka at {} due to unexpected exception", appId, instanceId, eurekaUrl, e); return null; } }; } @Override public ServiceInstance updateStatus(Status newStatus) { checkState(isRegistered(), "Can not update status before calling register"); var instanceToUpdate = registeredInstance.get(); var appId = instanceToUpdate.getApp(); var instanceId = instanceToUpdate.getInstanceId(); var responseSupplier = eurekaCallRetrySupplier(updateStatusSender(appId, instanceId, newStatus), OK.getStatusCode()); var responseOptional = updateStatusRetryer.tryGetObject("update status Response", responseSupplier); Optionals.ifPresentOrElseThrow(responseOptional, resp -> { LOG.info("Instance with appId {}, instanceId {} has been updated successfully to status {}", appId, instanceId, newStatus); registeredInstance.set(instanceToUpdate.withStatus(newStatus.name())); closeQuietly(resp); }, () -> { var msg = format("Error updating status for app {}, instance {}", appId, instanceId); LOG.error(msg); return new RegistrationException(msg); }); var includeNativeData = config.isIncludeNativeData() ? NativeRegistryData.INCLUDE_NATIVE_DATA : NativeRegistryData.IGNORE_NATIVE_DATA; return registeredInstance.get().toServiceInstance(includeNativeData); } private Function updateStatusSender(String appId, String instanceId, Status newStatus) { return eurekaUrl -> { try { return client.updateStatus(eurekaUrl, appId, instanceId, newStatus); } catch (Exception e) { LOG.error("Failed to update status to {} for instance with appId {}, instanceId {} from Eureka at {} due to unexpected exception", newStatus, appId, instanceId, eurekaUrl, e); return null; } }; } @Override public void unregister() { shutdownHeartbeat(); if (isNotRegistered()) { LOG.warn("Ignoring un-register request because not currently registered (call register first)"); return; } unregisterFromEureka(); } private void unregisterFromEureka() { var instanceToUnregister = registeredInstance.get(); var appId = instanceToUnregister.getApp(); var instanceId = instanceToUnregister.getInstanceId(); var responseSupplier = eurekaCallRetrySupplier(unregisterSender(appId, instanceId), OK.getStatusCode()); var responseOptional = unregisterRetryer.tryGetObject("unregister Response", responseSupplier); Optionals.ifPresentOrElseThrow(responseOptional, resp -> { LOG.info("Instance with appId {}, instanceId {} has been unregistered successfully", appId, instanceId); registeredInstance.set(null); closeQuietly(resp); }, () -> { var msg = format("Error un-registering app {}, instance {}", appId, instanceId); LOG.error(msg); return new RegistrationException(msg); }); } private Function unregisterSender(String appId, String instanceId) { return eurekaUrl -> { try { return client.unregister(eurekaUrl, appId, instanceId); } catch (Exception e) { LOG.error("Failed to unregister instance with appId {}, instanceId {} from Eureka at {} due to unexpected exception", appId, instanceId, eurekaUrl, e); return null; } }; } private boolean isRegistered() { return nonNull(registeredInstance.get()); } private boolean isNotRegistered() { return !isRegistered(); } /** * The returned Supplier returns the Response if its status code matches successfulStatusCode or null otherwise. *

* If the Supplier contains a Response, it is the caller's responsibility to ensure it is closed. */ private Supplier eurekaCallRetrySupplier(Function restCallFunction, int successfulStatusCode) { return () -> { var eurekaUrl = urlProvider.getCurrentEurekaUrl(); LOG.debug("Attempting a call to Eureka"); var response = restCallFunction.apply(eurekaUrl); if (isNull(response)) { urlProvider.getNextEurekaUrl(); LOG.error("Call to Eureka failed. See previous error for details"); return null; } if (successfulStatusCode == response.getStatus()) { // Caller is responsible for closing the returned Response return response; } urlProvider.getNextEurekaUrl(); var entity = safelyReadEntity(response); LOG.error("HTTP {} - Call to Eureka at {} failed to respond successfully. Response body: {}", response.getStatus(), eurekaUrl, entity); return null; }; } private static String safelyReadEntity(Response response) { return safeReadEntity(response).orElse("[Error reading response entity]"); } String getRegisteredAppOrNull() { return Optional.ofNullable(registeredInstance.get()) .map(EurekaInstance::getApp) .orElse(null); } void clearRegisteredInstance() { registeredInstance.set(null); if (trackHeartbeats) { heartbeatCount.set(0); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy