com.netflix.discovery.shared.Applications.java Source code

Java tutorial

Introduction

Here is the source code for com.netflix.discovery.shared.Applications.java

Source

/*
 * 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);
        }
    }

}