package com.netflix.discovery.endpoint;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.EurekaClientConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
/**
* This class contains some of the utility functions previously found in DiscoveryClient, but should be elsewhere.
* It *does not yet* clean up the moved code.
*/
public class EndpointUtils {
private static final Logger logger = LoggerFactory.getLogger(EndpointUtils.class);
public static final String DEFAULT_REGION = "default";
public static final String DEFAULT_ZONE = "default";
public enum DiscoveryUrlType {
CNAME, A
}
public static interface ServiceUrlRandomizer {
void randomize(List<String> urlList);
}
public static class InstanceInfoBasedUrlRandomizer implements ServiceUrlRandomizer {
private final InstanceInfo instanceInfo;
public InstanceInfoBasedUrlRandomizer(InstanceInfo instanceInfo) {
this.instanceInfo = instanceInfo;
}
@Override
public void randomize(List<String> urlList) {
int listSize = 0;
if (urlList != null) {
listSize = urlList.size();
}
if ((instanceInfo == null) || (listSize == 0)) {
return;
}
// Find the hashcode of the instance hostname and use it to find an entry
// and then arrange the rest of the entries after this entry.
int instanceHashcode = instanceInfo.getHostName().hashCode();
if (instanceHashcode < 0) {
instanceHashcode = instanceHashcode * -1;
}
int backupInstance = instanceHashcode % listSize;
for (int i = 0; i < backupInstance; i++) {
String zone = urlList.remove(0);
urlList.add(zone);
}
}
}
/**
* Get the list of all eureka service urls for the eureka client to talk to.
*
* @param clientConfig the clientConfig to use
* @param zone the zone in which the client resides
* @param randomizer a randomizer to randomized returned urls, if loading from dns
*
* @return The list of all eureka service urls for the eureka client to talk to.
*/
public static List<String> getDiscoveryServiceUrls(EurekaClientConfig clientConfig, String zone, ServiceUrlRandomizer randomizer) {
boolean shouldUseDns = clientConfig.shouldUseDnsForFetchingServiceUrls();
if (shouldUseDns) {
return getServiceUrlsFromDNS(clientConfig, zone, clientConfig.shouldPreferSameZoneEureka(), randomizer);
}
return getServiceUrlsFromConfig(clientConfig, zone, clientConfig.shouldPreferSameZoneEureka());
}
/**
* Get the list of all eureka service urls from DNS for the eureka client to
* talk to. The client picks up the service url from its zone and then fails over to
* other zones randomly. If there are multiple servers in the same zone, the client once
* again picks one randomly. This way the traffic will be distributed in the case of failures.
*
* @param clientConfig the clientConfig to use
* @param instanceZone The zone in which the client resides.
* @param preferSameZone true if we have to prefer the same zone as the client, false otherwise.
* @param randomizer a randomizer to randomized returned urls
*
* @return The list of all eureka service urls for the eureka client to talk to.
*/
public static List<String> getServiceUrlsFromDNS(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone, ServiceUrlRandomizer randomizer) {
String region = getRegion(clientConfig);
// Get zone-specific DNS names for the given region so that we can get a
// list of available zones
Map<String, List<String>> zoneDnsNamesMap = getZoneBasedDiscoveryUrlsFromRegion(clientConfig, region);
Set<String> availableZones = zoneDnsNamesMap.keySet();
List<String> zones = new ArrayList<String>(availableZones);
if (zones.isEmpty()) {
throw new RuntimeException("No available zones configured for the instanceZone " + instanceZone);
}
int zoneIndex = 0;
boolean zoneFound = false;
for (String zone : zones) {
logger.debug("Checking if the instance zone {} is the same as the zone from DNS {}", instanceZone, zone);
if (preferSameZone) {
if (instanceZone.equalsIgnoreCase(zone)) {
zoneFound = true;
}
} else {
if (!instanceZone.equalsIgnoreCase(zone)) {
zoneFound = true;
}
}
if (zoneFound) {
Object[] args = {zones, instanceZone, zoneIndex};
logger.debug("The zone index from the list {} that matches the instance zone {} is {}", args);
break;
}
zoneIndex++;
}
if (zoneIndex >= zones.size()) {
logger.warn("No match for the zone {} in the list of available zones {}",
instanceZone, Arrays.toString(zones.toArray()));
} else {
// Rearrange the zones with the instance zone first
for (int i = 0; i < zoneIndex; i++) {
String zone = zones.remove(0);
zones.add(zone);
}
}
// Now get the eureka urls for all the zones in the order and return it
List<String> serviceUrls = new ArrayList<String>();
for (String zone : zones) {
for (String zoneCname : zoneDnsNamesMap.get(zone)) {
List<String> ec2Urls = new ArrayList<String>(getEC2DiscoveryUrlsFromZone(zoneCname, DiscoveryUrlType.CNAME));
// Rearrange the list to distribute the load in case of
// multiple servers
if (ec2Urls.size() > 1) {
randomizer.randomize(ec2Urls);
}
for (String ec2Url : ec2Urls) {
String serviceUrl = "http://" + ec2Url + ":"
+ clientConfig.getEurekaServerPort()
+ "/" + clientConfig.getEurekaServerURLContext()
+ "/";
logger.debug("The EC2 url is {}", serviceUrl);
serviceUrls.add(serviceUrl);
}
}
}
// Rearrange the fail over server list to distribute the load
String primaryServiceUrl = serviceUrls.remove(0);
randomizer.randomize(serviceUrls);
serviceUrls.add(0, primaryServiceUrl);
logger.debug("This client will talk to the following serviceUrls in order : {} ",
Arrays.toString(serviceUrls.toArray()));
return serviceUrls;
}
/**
* Get the list of all eureka service urls from properties file for the eureka client to talk to.
*
* @param clientConfig the clientConfig to use
* @param instanceZone The zone in which the client resides
* @param preferSameZone true if we have to prefer the same zone as the client, false otherwise
* @return The list of all eureka service urls for the eureka client to talk to
*/
public static List<String> getServiceUrlsFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) {
List<String> orderedUrls = new ArrayList<String>();
String region = getRegion(clientConfig);
String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion());
if (availZones == null || availZones.length == 0) {
availZones = new String[1];
availZones[0] = DEFAULT_ZONE;
}
logger.debug("The availability zone for the given region {} are {}", region, Arrays.toString(availZones));
int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones);
List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[myZoneOffset]);
if (serviceUrls != null) {
orderedUrls.addAll(serviceUrls);
}
int currentOffset = myZoneOffset == (availZones.length - 1) ? 0 : (myZoneOffset + 1);
while (currentOffset != myZoneOffset) {
serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[currentOffset]);
if (serviceUrls != null) {
orderedUrls.addAll(serviceUrls);
}
if (currentOffset == (availZones.length - 1)) {
currentOffset = 0;
} else {
currentOffset++;
}
}
if (orderedUrls.size() < 1) {
throw new IllegalArgumentException("DiscoveryClient: invalid serviceUrl specified!");
}
return orderedUrls;
}
/**
* Get the list of all eureka service urls from properties file for the eureka client to talk to.
*
* @param clientConfig the clientConfig to use
* @param instanceZone The zone in which the client resides
* @param preferSameZone true if we have to prefer the same zone as the client, false otherwise
* @return an (ordered) map of zone -> list of urls mappings, with the preferred zone first in iteration order
*/
public static Map<String, List<String>> getServiceUrlsMapFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) {
Map<String, List<String>> orderedUrls = new LinkedHashMap<>();
String region = getRegion(clientConfig);
String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion());
if (availZones == null || availZones.length == 0) {
availZones = new String[1];
availZones[0] = DEFAULT_ZONE;
}
logger.debug("The availability zone for the given region {} are {}", region, Arrays.toString(availZones));
int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones);
String zone = availZones[myZoneOffset];
List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(zone);
if (serviceUrls != null) {
orderedUrls.put(zone, serviceUrls);
}
int currentOffset = myZoneOffset == (availZones.length - 1) ? 0 : (myZoneOffset + 1);
while (currentOffset != myZoneOffset) {
zone = availZones[currentOffset];
serviceUrls = clientConfig.getEurekaServerServiceUrls(zone);
if (serviceUrls != null) {
orderedUrls.put(zone, serviceUrls);
}
if (currentOffset == (availZones.length - 1)) {
currentOffset = 0;
} else {
currentOffset++;
}
}
if (orderedUrls.size() < 1) {
throw new IllegalArgumentException("DiscoveryClient: invalid serviceUrl specified!");
}
return orderedUrls;
}
/**
* Get the list of EC2 URLs given the zone name.
*
* @param dnsName The dns name of the zone-specific CNAME
* @param type CNAME or EIP that needs to be retrieved
* @return The list of EC2 URLs associated with the dns name
*/
public static Set<String> getEC2DiscoveryUrlsFromZone(String dnsName, DiscoveryUrlType type) {
Set<String> eipsForZone = null;
try {
dnsName = "txt." + dnsName;
logger.debug("The zone url to be looked up is {} :", dnsName);
Set<String> ec2UrlsForZone = DnsResolver.getCNamesFromTxtRecord(dnsName);
for (String ec2Url : ec2UrlsForZone) {
logger.debug("The eureka url for the dns name {} is {}", dnsName, ec2Url);
ec2UrlsForZone.add(ec2Url);
}
if (DiscoveryUrlType.CNAME.equals(type)) {
return ec2UrlsForZone;
}
eipsForZone = new TreeSet<String>();
for (String cname : ec2UrlsForZone) {
String[] tokens = cname.split("\\.");
String ec2HostName = tokens[0];
String[] ips = ec2HostName.split("-");
StringBuilder eipBuffer = new StringBuilder();
for (int ipCtr = 1; ipCtr < 5; ipCtr++) {
eipBuffer.append(ips[ipCtr]);
if (ipCtr < 4) {
eipBuffer.append(".");
}
}
eipsForZone.add(eipBuffer.toString());
}
logger.debug("The EIPS for {} is {} :", dnsName, eipsForZone);
} catch (Throwable e) {
throw new RuntimeException("Cannot get cnames bound to the region:" + dnsName, e);
}
return eipsForZone;
}
/**
* Get the zone based CNAMES that are bound to a region.
*
* @param region
* - The region for which the zone names need to be retrieved
* @return - The list of CNAMES from which the zone-related information can
* be retrieved
*/
public static Map<String, List<String>> getZoneBasedDiscoveryUrlsFromRegion(EurekaClientConfig clientConfig, String region) {
String discoveryDnsName = null;
try {
discoveryDnsName = "txt." + region + "." + clientConfig.getEurekaServerDNSName();
logger.debug("The region url to be looked up is {} :", discoveryDnsName);
Set<String> zoneCnamesForRegion = new TreeSet<String>(DnsResolver.getCNamesFromTxtRecord(discoveryDnsName));
Map<String, List<String>> zoneCnameMapForRegion = new TreeMap<String, List<String>>();
for (String zoneCname : zoneCnamesForRegion) {
String zone = null;
if (isEC2Url(zoneCname)) {
throw new RuntimeException(
"Cannot find the right DNS entry for "
+ discoveryDnsName
+ ". "
+ "Expected mapping of the format <aws_zone>.<domain_name>");
} else {
String[] cnameTokens = zoneCname.split("\\.");
zone = cnameTokens[0];
logger.debug("The zoneName mapped to region {} is {}", region, zone);
}
List<String> zoneCnamesSet = zoneCnameMapForRegion.get(zone);
if (zoneCnamesSet == null) {
zoneCnamesSet = new ArrayList<String>();
zoneCnameMapForRegion.put(zone, zoneCnamesSet);
}
zoneCnamesSet.add(zoneCname);
}
return zoneCnameMapForRegion;
} catch (Throwable e) {
throw new RuntimeException("Cannot get cnames bound to the region:" + discoveryDnsName, e);
}
}
/**
* Get the region that this particular instance is in.
*
* @return - The region in which the particular instance belongs to.
*/
public static String getRegion(EurekaClientConfig clientConfig) {
String region = clientConfig.getRegion();
if (region == null) {
region = DEFAULT_REGION;
}
region = region.trim().toLowerCase();
return region;
}
// FIXME this is no valid for vpc
private static boolean isEC2Url(String zoneCname) {
return zoneCname.startsWith("ec2");
}
/**
* Gets the zone to pick up for this instance.
*/
private static int getZoneOffset(String myZone, boolean preferSameZone, String[] availZones) {
for (int i = 0; i < availZones.length; i++) {
if (myZone != null && (availZones[i].equalsIgnoreCase(myZone.trim()) == preferSameZone)) {
return i;
}
}
logger.warn("DISCOVERY: Could not pick a zone based on preferred zone settings. My zone - {}," +
" preferSameZone- {}. Defaulting to " + availZones[0], myZone, preferSameZone);
return 0;
}
}