/*
* 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.discovery.shared;
import javax.annotation.Nullable;
import java.util.AbstractQueue;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.appinfo.InstanceInfo.ActionType;
import com.netflix.appinfo.InstanceInfo.InstanceStatus;
import com.netflix.discovery.EurekaClientConfig;
import com.netflix.discovery.InstanceRegionChecker;
import com.netflix.discovery.provider.Serializer;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import com.thoughtworks.xstream.annotations.XStreamImplicit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The class that wraps all the registry information returned by eureka server.
*
* <p>
* Note that the registry information is fetched from eureka server as specified
* in {@link EurekaClientConfig#getRegistryFetchIntervalSeconds()}. Once the
* information is fetched it is shuffled and also filtered for instances with
* {@link InstanceStatus#UP} status as specified by the configuration
* {@link EurekaClientConfig#shouldFilterOnlyUpInstances()}.
* </p>
*
* @author Karthik Ranganathan
*
*/
@Serializer("com.netflix.discovery.converters.EntityBodyConverter")
@XStreamAlias("applications")
@JsonRootName("applications")
public class Applications {
private static final String APP_INSTANCEID_DELIMITER = "$$";
private static final Logger logger = LoggerFactory.getLogger(Applications.class);
private static final String STATUS_DELIMITER = "_";
private Long versionDelta = Long.valueOf(-1);
@XStreamImplicit
private AbstractQueue<Application> applications;
private Map<String, Application> appNameApplicationMap = new ConcurrentHashMap<String, Application>();
private Map<String, AbstractQueue<InstanceInfo>> virtualHostNameAppMap = new ConcurrentHashMap<String, AbstractQueue<InstanceInfo>>();
private Map<String, AbstractQueue<InstanceInfo>> secureVirtualHostNameAppMap = new ConcurrentHashMap<String, AbstractQueue<InstanceInfo>>();
private Map<String, AtomicLong> virtualHostNameIndexMap = new ConcurrentHashMap<String, AtomicLong>();
private Map<String, AtomicLong> secureVirtualHostNameIndexMap = new ConcurrentHashMap<String, AtomicLong>();
private Map<String, AtomicReference<List<InstanceInfo>>> shuffleVirtualHostNameMap = new ConcurrentHashMap<String, AtomicReference<List<InstanceInfo>>>();
private Map<String, AtomicReference<List<InstanceInfo>>> shuffledSecureVirtualHostNameMap = new ConcurrentHashMap<String, AtomicReference<List<InstanceInfo>>>();
private String appsHashCode;
/**
* Create a new, empty Eureka application list.
*/
public Applications() {
this.applications = new ConcurrentLinkedQueue<Application>();
}
/**
* Note that appsHashCode and versionDelta key names are formatted in a custom/configurable way.
*/
@JsonCreator
public Applications(
@JsonProperty("appsHashCode") String appsHashCode,
@JsonProperty("versionDelta") Long versionDelta,
@JsonProperty("application") List<Application> registeredApplications) {
this.applications = new ConcurrentLinkedQueue<Application>();
for (Application app : registeredApplications) {
this.addApplication(app);
}
this.appsHashCode = appsHashCode;
this.versionDelta = versionDelta;
}
/**
* Create a new Eureka application list, based on the provided applications. The provided container is
* not modified.
*
* @param apps the initial list of apps to store in this applications list
*/
public Applications(List<Application> apps) {
this.applications = new ConcurrentLinkedQueue<Application>();
this.applications.addAll(apps);
}
/**
* Add the <em>application</em> to the list.
*
* @param app
* the <em>application</em> to be added.
*/
public void addApplication(Application app) {
appNameApplicationMap.put(app.getName().toUpperCase(Locale.ROOT), app);
addInstancesToVIPMaps(app);
applications.add(app);
}
/**
* Gets the list of all registered <em>applications</em> from eureka.
*
* @return list containing all applications registered with eureka.
*/
@JsonProperty("application")
public List<Application> getRegisteredApplications() {
List<Application> list = new ArrayList<Application>();
list.addAll(this.applications);
return list;
}
/**
* Gets the list of all registered <em>applications</em> for the given
* application name.
*
* @param appName
* the application name for which the result need to be fetched.
* @return the list of registered applications for the given application
* name.
*/
public Application getRegisteredApplications(String appName) {
return appNameApplicationMap.get(appName.toUpperCase(Locale.ROOT));
}
/**
* Gets the list of <em>instances</em> associated to a virtual host name.
*
* @param virtualHostName
* the virtual hostname for which the instances need to be
* returned.
* @return list of <em>instances</em>.
*/
public List<InstanceInfo> getInstancesByVirtualHostName(String virtualHostName) {
AtomicReference<List<InstanceInfo>> ref = this.shuffleVirtualHostNameMap
.get(virtualHostName.toUpperCase(Locale.ROOT));
if (ref == null || ref.get() == null) {
return new ArrayList<InstanceInfo>();
} else {
return ref.get();
}
}
/**
* Gets the list of secure <em>instances</em> associated to a virtual host
* name.
*
* @param secureVirtualHostName
* the virtual hostname for which the secure instances need to be
* returned.
* @return list of <em>instances</em>.
*/
public List<InstanceInfo> getInstancesBySecureVirtualHostName(String secureVirtualHostName) {
AtomicReference<List<InstanceInfo>> ref = this.shuffledSecureVirtualHostNameMap
.get(secureVirtualHostName.toUpperCase(Locale.ROOT));
if (ref == null || ref.get() == null) {
return new ArrayList<InstanceInfo>();
} else {
return ref.get();
}
}
/**
* @return a weakly consistent size of the number of instances in all the applications
*/
public int size() {
int result = 0;
for (Application application : applications) {
result += application.size();
}
return result;
}
@Deprecated
public void setVersion(Long version) {
this.versionDelta = version;
}
@Deprecated
@JsonIgnore // Handled directly due to legacy name formatting
public Long getVersion() {
return this.versionDelta;
}
/**
* Used by the eureka server. Not for external use.
*
* @param hashCode the hash code to assign for this app collection
*/
public void setAppsHashCode(String hashCode) {
this.appsHashCode = hashCode;
}
/**
* Used by the eureka server. Not for external use.
* @return the string indicating the hashcode based on the applications stored.
*
*/
@JsonIgnore // Handled directly due to legacy name formatting
public String getAppsHashCode() {
return this.appsHashCode;
}
/**
* Gets the hash code for this <em>applications</em> instance. Used for
* comparison of instances between eureka server and eureka client.
*
* @return the internal hash code representation indicating the information
* about the instances.
*/
@JsonIgnore
public String getReconcileHashCode() {
TreeMap<String, AtomicInteger> instanceCountMap = new TreeMap<String, AtomicInteger>();
populateInstanceCountMap(instanceCountMap);
return getReconcileHashCode(instanceCountMap);
}
/**
* Populates the provided instance count map. The instance count map is used as part of the general
* app list synchronization mechanism.
* @param instanceCountMap the map to populate
*/
public void populateInstanceCountMap(TreeMap<String, AtomicInteger> instanceCountMap) {
for (Application app : this.getRegisteredApplications()) {
for (InstanceInfo info : app.getInstancesAsIsFromEureka()) {
AtomicInteger instanceCount = instanceCountMap.get(info.getStatus().name());
if (instanceCount == null) {
instanceCount = new AtomicInteger(0);
instanceCountMap.put(info.getStatus().name(), instanceCount);
}
instanceCount.incrementAndGet();
}
}
}
/**
* Gets the reconciliation hashcode. The hashcode is used to determine whether the applications list
* has changed since the last time it was acquired.
* @param instanceCountMap the instance count map to use for generating the hash
* @return the hash code for this instance
*/
public static String getReconcileHashCode(TreeMap<String, AtomicInteger> instanceCountMap) {
String reconcileHashCode = "";
for (Map.Entry<String, AtomicInteger> mapEntry : instanceCountMap.entrySet()) {
reconcileHashCode = reconcileHashCode + mapEntry.getKey()
+ STATUS_DELIMITER + mapEntry.getValue().get()
+ STATUS_DELIMITER;
}
return reconcileHashCode;
}
/**
* Gets the exact difference between this applications instance and another
* one.
*
* @param apps
* the applications for which to compare this one.
* @return a map containing the differences between the two.
*/
public Map<String, List<String>> getReconcileMapDiff(Applications apps) {
Map<String, List<String>> diffMap = new TreeMap<String, List<String>>();
Set<Pair> allInstanceAppInstanceIds = new HashSet<Pair>();
for (Application otherApp : apps.getRegisteredApplications()) {
Application thisApp = this.getRegisteredApplications(otherApp.getName());
if (thisApp == null) {
logger.warn("Application not found in local cache : {}", otherApp.getName());
continue;
}
for (InstanceInfo instanceInfo : thisApp.getInstancesAsIsFromEureka()) {
allInstanceAppInstanceIds.add(new Pair(thisApp.getName(),
instanceInfo.getId()));
}
for (InstanceInfo otherInstanceInfo : otherApp.getInstancesAsIsFromEureka()) {
InstanceInfo thisInstanceInfo = thisApp.getByInstanceId(otherInstanceInfo.getId());
if (thisInstanceInfo == null) {
List<String> diffList = diffMap.get(ActionType.DELETED.name());
if (diffList == null) {
diffList = new ArrayList<String>();
diffMap.put(ActionType.DELETED.name(), diffList);
}
diffList.add(otherInstanceInfo.getId());
} else if (!thisInstanceInfo.getStatus().name()
.equalsIgnoreCase(otherInstanceInfo.getStatus().name())) {
List<String> diffList = diffMap.get(ActionType.MODIFIED.name());
if (diffList == null) {
diffList = new ArrayList<String>();
diffMap.put(ActionType.MODIFIED.name(), diffList);
}
diffList.add(thisInstanceInfo.getId()
+ APP_INSTANCEID_DELIMITER
+ thisInstanceInfo.getStatus().name()
+ APP_INSTANCEID_DELIMITER
+ otherInstanceInfo.getStatus().name());
}
allInstanceAppInstanceIds.remove(new Pair(otherApp.getName(), otherInstanceInfo.getId()));
}
}
for (Pair pair : allInstanceAppInstanceIds) {
Application app = new Application(pair.getItem1());
InstanceInfo thisInstanceInfo = app.getByInstanceId(pair.getItem2());
if (thisInstanceInfo != null) {
List<String> diffList = diffMap.get(ActionType.ADDED.name());
if (diffList == null) {
diffList = new ArrayList<String>();
diffMap.put(ActionType.ADDED.name(), diffList);
}
diffList.add(thisInstanceInfo.getId());
}
}
return diffMap;
}
private static final class Pair {
private final String item1;
private final String item2;
public Pair(String item1, String item2) {
super();
this.item1 = item1;
this.item2 = item2;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((item1 == null) ? 0 : item1.hashCode());
result = prime * result
+ ((item2 == null) ? 0 : item2.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;
}
Pair other = (Pair) obj;
if (item1 == null) {
if (other.item1 != null) {
return false;
}
} else if (!item1.equals(other.item1)) {
return false;
}
if (item2 == null) {
if (other.item2 != null) {
return false;
}
} else if (!item2.equals(other.item2)) {
return false;
}
return true;
}
public String getItem1() {
return item1;
}
public String getItem2() {
return item2;
}
}
/**
* Shuffles the provided instances so that they will not always be returned in the same order.
* @param filterUpInstances whether to return only UP instances
*/
public void shuffleInstances(boolean filterUpInstances) {
shuffleInstances(filterUpInstances, false, null, null, null);
}
/**
* Shuffles a whole region so that the instances will not always be returned in the same order.
* @param remoteRegionsRegistry the map of remote region names to their registries
* @param clientConfig the {@link EurekaClientConfig}, whose settings will be used to determine whether to
* filter to only UP instances
* @param instanceRegionChecker the instance region checker
*/
public void shuffleAndIndexInstances(Map<String, Applications> remoteRegionsRegistry,
EurekaClientConfig clientConfig, InstanceRegionChecker instanceRegionChecker) {
shuffleInstances(clientConfig.shouldFilterOnlyUpInstances(), true, remoteRegionsRegistry, clientConfig,
instanceRegionChecker);
}
private void shuffleInstances(boolean filterUpInstances, boolean indexByRemoteRegions,
@Nullable Map<String, Applications> remoteRegionsRegistry,
@Nullable EurekaClientConfig clientConfig,
@Nullable InstanceRegionChecker instanceRegionChecker) {
this.virtualHostNameAppMap.clear();
this.secureVirtualHostNameAppMap.clear();
for (Application application : appNameApplicationMap.values()) {
if (indexByRemoteRegions) {
application.shuffleAndStoreInstances(remoteRegionsRegistry, clientConfig, instanceRegionChecker);
} else {
application.shuffleAndStoreInstances(filterUpInstances);
}
this.addInstancesToVIPMaps(application);
}
shuffleAndFilterInstances(this.virtualHostNameAppMap,
this.shuffleVirtualHostNameMap, virtualHostNameIndexMap,
filterUpInstances);
shuffleAndFilterInstances(this.secureVirtualHostNameAppMap,
this.shuffledSecureVirtualHostNameMap,
secureVirtualHostNameIndexMap, filterUpInstances);
}
/**
* Gets the next round-robin index for the given virtual host name. This
* index is reset after every registry fetch cycle.
*
* @param virtualHostname
* the virtual host name.
* @param secure
* indicates whether it is a secure request or a non-secure
* request.
* @return AtomicLong value representing the next round-robin index.
*/
public AtomicLong getNextIndex(String virtualHostname, boolean secure) {
if (secure) {
return this.secureVirtualHostNameIndexMap.get(virtualHostname);
} else {
return this.virtualHostNameIndexMap.get(virtualHostname);
}
}
/**
* Shuffle the instances and filter for only {@link InstanceStatus#UP} if
* required.
*
*/
private void shuffleAndFilterInstances(
Map<String, AbstractQueue<InstanceInfo>> srcMap,
Map<String, AtomicReference<List<InstanceInfo>>> destMap,
Map<String, AtomicLong> vipIndexMap, boolean filterUpInstances) {
for (Map.Entry<String, AbstractQueue<InstanceInfo>> entries : srcMap.entrySet()) {
AbstractQueue<InstanceInfo> instanceInfoQueue = entries.getValue();
List<InstanceInfo> l = new ArrayList<InstanceInfo>(instanceInfoQueue);
if (filterUpInstances) {
Iterator<InstanceInfo> it = l.iterator();
while (it.hasNext()) {
InstanceInfo instanceInfo = it.next();
if (!InstanceStatus.UP.equals(instanceInfo.getStatus())) {
it.remove();
}
}
}
Collections.shuffle(l);
AtomicReference<List<InstanceInfo>> instanceInfoList = destMap.get(entries.getKey());
if (instanceInfoList == null) {
instanceInfoList = new AtomicReference<List<InstanceInfo>>(l);
destMap.put(entries.getKey(), instanceInfoList);
}
instanceInfoList.set(l);
vipIndexMap.put(entries.getKey(), new AtomicLong(0));
}
// finally remove all vips that are completed deleted (i.e. missing) from the srcSet
Set<String> srcVips = srcMap.keySet();
Set<String> destVips = destMap.keySet();
destVips.retainAll(srcVips);
}
/**
* Add the instance to the given map based if the vip address matches with
* that of the instance. Note that an instance can be mapped to multiple vip
* addresses.
*
*/
private void addInstanceToMap(InstanceInfo info, String vipAddresses,
Map<String, AbstractQueue<InstanceInfo>> vipMap) {
if (vipAddresses != null) {
String[] vipAddressArray = vipAddresses.split(",");
for (String vipAddress : vipAddressArray) {
String vipName = vipAddress.toUpperCase(Locale.ROOT);
AbstractQueue<InstanceInfo> instanceInfoList = vipMap.get(vipName);
if (instanceInfoList == null) {
instanceInfoList = new ConcurrentLinkedQueue<InstanceInfo>();
vipMap.put(vipName, instanceInfoList);
}
instanceInfoList.add(info);
}
}
}
/**
* Adds the instances to the internal vip address map.
* @param app - the applications for which the instances need to be added.
*/
private void addInstancesToVIPMaps(Application app) {
// Check and add the instances to the their respective virtual host name
// mappings
for (InstanceInfo info : app.getInstances()) {
String vipAddresses = info.getVIPAddress();
String secureVipAddresses = info.getSecureVipAddress();
if ((vipAddresses == null) && (secureVipAddresses == null)) {
continue;
}
addInstanceToMap(info, vipAddresses, virtualHostNameAppMap);
addInstanceToMap(info, secureVipAddresses,
secureVirtualHostNameAppMap);
}
}
}