/* * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.netflix.eureka.cluster; import java.net.MalformedURLException; import java.net.URL; import com.netflix.appinfo.InstanceInfo; import com.netflix.appinfo.InstanceInfo.InstanceStatus; import com.netflix.discovery.shared.transport.EurekaHttpResponse; import com.netflix.eureka.EurekaServerConfig; import com.netflix.eureka.lease.Lease; import com.netflix.eureka.registry.PeerAwareInstanceRegistry; import com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl.Action; import com.netflix.eureka.resources.ASGResource.ASGStatus; import com.netflix.eureka.util.batcher.TaskDispatcher; import com.netflix.eureka.util.batcher.TaskDispatchers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The <code>PeerEurekaNode</code> represents a peer node to which information * should be shared from this node. * * <p> * This class handles replicating all update operations like * <em>Register,Renew,Cancel,Expiration and Status Changes</em> to the eureka * node it represents. * <p> * * @author Karthik Ranganathan, Greg Kim * */ public class PeerEurekaNode { /** * A time to wait before continuing work if there is network level error. */ private static final long RETRY_SLEEP_TIME_MS = 100; /** * A time to wait before continuing work if there is congestion on the server side. */ private static final long SERVER_UNAVAILABLE_SLEEP_TIME_MS = 1000; /** * Maximum amount of time in ms to wait for new items prior to dispatching a batch of tasks. */ private static final long MAX_BATCHING_DELAY_MS = 500; /** * Maximum batch size for batched requests. */ private static final int BATCH_SIZE = 250; private static final Logger logger = LoggerFactory.getLogger(PeerEurekaNode.class); public static final String BATCH_URL_PATH = "peerreplication/batch/"; public static final String HEADER_REPLICATION = "x-netflix-discovery-replication"; private final String serviceUrl; private final EurekaServerConfig config; private final long maxProcessingDelayMs; private final PeerAwareInstanceRegistry registry; private final String targetHost; private final HttpReplicationClient replicationClient; private final TaskDispatcher<String, ReplicationTask> batchingDispatcher; private final TaskDispatcher<String, ReplicationTask> nonBatchingDispatcher; public PeerEurekaNode(PeerAwareInstanceRegistry registry, String targetHost, String serviceUrl, HttpReplicationClient replicationClient, EurekaServerConfig config) { this(registry, targetHost, serviceUrl, replicationClient, config, BATCH_SIZE, MAX_BATCHING_DELAY_MS, RETRY_SLEEP_TIME_MS, SERVER_UNAVAILABLE_SLEEP_TIME_MS); } /* For testing */ PeerEurekaNode(PeerAwareInstanceRegistry registry, String targetHost, String serviceUrl, HttpReplicationClient replicationClient, EurekaServerConfig config, int batchSize, long maxBatchingDelayMs, long retrySleepTimeMs, long serverUnavailableSleepTimeMs) { this.registry = registry; this.targetHost = targetHost; this.replicationClient = replicationClient; this.serviceUrl = serviceUrl; this.config = config; this.maxProcessingDelayMs = config.getMaxTimeForReplication(); String batcherName = getBatcherName(); ReplicationTaskProcessor taskProcessor = new ReplicationTaskProcessor(targetHost, replicationClient); this.batchingDispatcher = TaskDispatchers.createBatchingTaskDispatcher( batcherName, config.getMaxElementsInPeerReplicationPool(), batchSize, config.getMaxThreadsForPeerReplication(), maxBatchingDelayMs, serverUnavailableSleepTimeMs, retrySleepTimeMs, taskProcessor ); this.nonBatchingDispatcher = TaskDispatchers.createNonBatchingTaskDispatcher( targetHost, config.getMaxElementsInStatusReplicationPool(), config.getMaxThreadsForStatusReplication(), maxBatchingDelayMs, serverUnavailableSleepTimeMs, retrySleepTimeMs, taskProcessor ); } /** * Sends the registration information of {@link InstanceInfo} receiving by * this node to the peer node represented by this class. * * @param info * the instance information {@link InstanceInfo} of any instance * that is send to this instance. * @throws Exception */ public void register(final InstanceInfo info) throws Exception { long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info); batchingDispatcher.process( taskId("register", info), new InstanceReplicationTask(targetHost, Action.Register, info, null, true) { public EurekaHttpResponse<Void> execute() { return replicationClient.register(info); } }, expiryTime ); } /** * Send the cancellation information of an instance to the node represented * by this class. * * @param appName * the application name of the instance. * @param id * the unique identifier of the instance. * @throws Exception */ public void cancel(final String appName, final String id) throws Exception { long expiryTime = System.currentTimeMillis() + maxProcessingDelayMs; batchingDispatcher.process( taskId("cancel", appName, id), new InstanceReplicationTask(targetHost, Action.Cancel, appName, id) { @Override public EurekaHttpResponse<Void> execute() { return replicationClient.cancel(appName, id); } @Override public void handleFailure(int statusCode, Object responseEntity) throws Throwable { super.handleFailure(statusCode, responseEntity); if (statusCode == 404) { logger.warn("{}: missing entry.", getTaskName()); } } }, expiryTime ); } /** * Send the heartbeat information of an instance to the node represented by * this class. If the instance does not exist the node, the instance * registration information is sent again to the peer node. * * @param appName * the application name of the instance. * @param id * the unique identifier of the instance. * @param info * the instance info {@link InstanceInfo} of the instance. * @param overriddenStatus * the overridden status information if any of the instance. * @throws Throwable */ public void heartbeat(final String appName, final String id, final InstanceInfo info, final InstanceStatus overriddenStatus, boolean primeConnection) throws Throwable { if (primeConnection) { // We do not care about the result for priming request. replicationClient.sendHeartBeat(appName, id, info, overriddenStatus); return; } ReplicationTask replicationTask = new InstanceReplicationTask(targetHost, Action.Heartbeat, info, overriddenStatus, false) { @Override public EurekaHttpResponse<InstanceInfo> execute() throws Throwable { return replicationClient.sendHeartBeat(appName, id, info, overriddenStatus); } @Override public void handleFailure(int statusCode, Object responseEntity) throws Throwable { super.handleFailure(statusCode, responseEntity); if (statusCode == 404) { logger.warn("{}: missing entry.", getTaskName()); if (info != null) { logger.warn("{}: cannot find instance id {} and hence replicating the instance with status {}", getTaskName(), info.getId(), info.getStatus()); register(info); } } else if (config.shouldSyncWhenTimestampDiffers()) { InstanceInfo peerInstanceInfo = (InstanceInfo) responseEntity; if (peerInstanceInfo != null) { syncInstancesIfTimestampDiffers(appName, id, info, peerInstanceInfo); } } } }; long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info); batchingDispatcher.process(taskId("heartbeat", info), replicationTask, expiryTime); } /** * Send the status information of of the ASG represented by the instance. * * <p> * ASG (Autoscaling group) names are available for instances in AWS and the * ASG information is used for determining if the instance should be * registered as {@link InstanceStatus#DOWN} or {@link InstanceStatus#UP}. * * @param asgName * the asg name if any of this instance. * @param newStatus * the new status of the ASG. */ public void statusUpdate(final String asgName, final ASGStatus newStatus) { long expiryTime = System.currentTimeMillis() + maxProcessingDelayMs; nonBatchingDispatcher.process( asgName, new AsgReplicationTask(targetHost, Action.StatusUpdate, asgName, newStatus) { public EurekaHttpResponse<?> execute() { return replicationClient.statusUpdate(asgName, newStatus); } }, expiryTime ); } /** * * Send the status update of the instance. * * @param appName * the application name of the instance. * @param id * the unique identifier of the instance. * @param newStatus * the new status of the instance. * @param info * the instance information of the instance. */ public void statusUpdate(final String appName, final String id, final InstanceStatus newStatus, final InstanceInfo info) { long expiryTime = System.currentTimeMillis() + maxProcessingDelayMs; batchingDispatcher.process( taskId("statusUpdate", appName, id), new InstanceReplicationTask(targetHost, Action.StatusUpdate, info, null, false) { @Override public EurekaHttpResponse<Void> execute() { return replicationClient.statusUpdate(appName, id, newStatus, info); } }, expiryTime ); } /** * Delete instance status override. * * @param appName * the application name of the instance. * @param id * the unique identifier of the instance. * @param info * the instance information of the instance. */ public void deleteStatusOverride(final String appName, final String id, final InstanceInfo info) { long expiryTime = System.currentTimeMillis() + maxProcessingDelayMs; batchingDispatcher.process( taskId("deleteStatusOverride", appName, id), new InstanceReplicationTask(targetHost, Action.DeleteStatusOverride, info, null, false) { @Override public EurekaHttpResponse<Void> execute() { return replicationClient.deleteStatusOverride(appName, id, info); } }, expiryTime); } /** * Get the service Url of the peer eureka node. * * @return the service Url of the peer eureka node. */ public String getServiceUrl() { return serviceUrl; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((serviceUrl == null) ? 0 : serviceUrl.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } PeerEurekaNode other = (PeerEurekaNode) obj; if (serviceUrl == null) { if (other.serviceUrl != null) { return false; } } else if (!serviceUrl.equals(other.serviceUrl)) { return false; } return true; } /** * Shuts down all resources used for peer replication. */ public void shutDown() { batchingDispatcher.shutdown(); nonBatchingDispatcher.shutdown(); } /** * Synchronize {@link InstanceInfo} information if the timestamp between * this node and the peer eureka nodes vary. */ private void syncInstancesIfTimestampDiffers(String appName, String id, InstanceInfo info, InstanceInfo infoFromPeer) { try { if (infoFromPeer != null) { logger.warn("Peer wants us to take the instance information from it, since the timestamp differs," + "Id : {} My Timestamp : {}, Peer's timestamp: {}", id, info.getLastDirtyTimestamp(), infoFromPeer.getLastDirtyTimestamp()); if (infoFromPeer.getOverriddenStatus() != null && !InstanceStatus.UNKNOWN.equals(infoFromPeer.getOverriddenStatus())) { logger.warn("Overridden Status info -id {}, mine {}, peer's {}", id, info.getOverriddenStatus(), infoFromPeer.getOverriddenStatus()); registry.storeOverriddenStatusIfRequired(appName, id, infoFromPeer.getOverriddenStatus()); } registry.register(infoFromPeer, true); } } catch (Throwable e) { logger.warn("Exception when trying to set information from peer :", e); } } public String getBatcherName() { String batcherName; try { batcherName = new URL(serviceUrl).getHost(); } catch (MalformedURLException e1) { batcherName = serviceUrl; } return "target_" + batcherName; } private static String taskId(String requestType, String appName, String id) { return requestType + '#' + appName + '/' + id; } private static String taskId(String requestType, InstanceInfo info) { return taskId(requestType, info.getAppName(), info.getId()); } private static int getLeaseRenewalOf(InstanceInfo info) { return (info.getLeaseInfo() == null ? Lease.DEFAULT_DURATION_IN_SECS : info.getLeaseInfo().getRenewalIntervalInSecs()) * 1000; } }