package com.netflix.discovery; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import com.google.common.base.Supplier; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static com.netflix.discovery.DefaultEurekaClientConfig.DEFAULT_ZONE; /** * @author Nitesh Kant */ public abstract class AbstractAzToRegionMapper implements AzToRegionMapper { private static final Logger logger = LoggerFactory.getLogger(InstanceRegionChecker.class); private static final String[] EMPTY_STR_ARRAY = new String[0]; protected final EurekaClientConfig clientConfig; /** * A default for the mapping that we know of, if a remote region is configured to be fetched but does not have * any availability zone mapping, we will use these defaults. OTOH, if the remote region has any mapping defaults * will not be used. */ private final Multimap<String, String> defaultRegionVsAzMap = Multimaps.newListMultimap(new HashMap<String, Collection<String>>(), new Supplier<List<String>>() { @Override public List<String> get() { return new ArrayList<String>(); } }); private final Map<String, String> availabilityZoneVsRegion = new ConcurrentHashMap<String, String>(); private String[] regionsToFetch; protected AbstractAzToRegionMapper(EurekaClientConfig clientConfig) { this.clientConfig = clientConfig; populateDefaultAZToRegionMap(); } @Override public synchronized void setRegionsToFetch(String[] regionsToFetch) { if (null != regionsToFetch) { this.regionsToFetch = regionsToFetch; logger.info("Fetching availability zone to region mapping for regions {}", Arrays.toString(regionsToFetch)); availabilityZoneVsRegion.clear(); for (String remoteRegion : regionsToFetch) { Set<String> availabilityZones = getZonesForARegion(remoteRegion); if (null == availabilityZones || (availabilityZones.size() == 1 && availabilityZones.iterator().next().equals(DEFAULT_ZONE)) || availabilityZones.isEmpty()) { logger.info("No availability zone information available for remote region: " + remoteRegion + ". Now checking in the default mapping."); if (defaultRegionVsAzMap.containsKey(remoteRegion)) { Collection<String> defaultAvailabilityZones = defaultRegionVsAzMap.get(remoteRegion); for (String defaultAvailabilityZone : defaultAvailabilityZones) { availabilityZoneVsRegion.put(defaultAvailabilityZone, remoteRegion); } } else { String msg = "No availability zone information available for remote region: " + remoteRegion + ". This is required if registry information for this region is configured to be " + "fetched."; logger.error(msg); throw new RuntimeException(msg); } } else { for (String availabilityZone : availabilityZones) { availabilityZoneVsRegion.put(availabilityZone, remoteRegion); } } } logger.info("Availability zone to region mapping for all remote regions: {}", availabilityZoneVsRegion); } else { logger.info("Regions to fetch is null. Erasing older mapping if any."); availabilityZoneVsRegion.clear(); this.regionsToFetch = EMPTY_STR_ARRAY; } } /** * Returns all the zones in the provided region. * @param region the region whose zones you want * @return a set of zones */ protected abstract Set<String> getZonesForARegion(String region); @Override public String getRegionForAvailabilityZone(String availabilityZone) { String region = availabilityZoneVsRegion.get(availabilityZone); if (null == region) { return parseAzToGetRegion(availabilityZone); } return region; } @Override public synchronized void refreshMapping() { logger.info("Refreshing availability zone to region mappings."); setRegionsToFetch(regionsToFetch); } /** * Tries to determine what region we're in, based on the provided availability zone. * @param availabilityZone the availability zone to inspect * @return the region, if available; null otherwise */ protected String parseAzToGetRegion(String availabilityZone) { // Here we see that whether the availability zone is following a pattern like <region><single letter> // If it is then we take ignore the last letter and check if the remaining part is actually a known remote // region. If yes, then we return that region, else null which means local region. if (!availabilityZone.isEmpty()) { String possibleRegion = availabilityZone.substring(0, availabilityZone.length() - 1); if (availabilityZoneVsRegion.containsValue(possibleRegion)) { return possibleRegion; } } return null; } private void populateDefaultAZToRegionMap() { defaultRegionVsAzMap.put("us-east-1", "us-east-1a"); defaultRegionVsAzMap.put("us-east-1", "us-east-1c"); defaultRegionVsAzMap.put("us-east-1", "us-east-1d"); defaultRegionVsAzMap.put("us-east-1", "us-east-1e"); defaultRegionVsAzMap.put("us-west-1", "us-west-1a"); defaultRegionVsAzMap.put("us-west-1", "us-west-1c"); defaultRegionVsAzMap.put("us-west-2", "us-west-2a"); defaultRegionVsAzMap.put("us-west-2", "us-west-2b"); defaultRegionVsAzMap.put("us-west-2", "us-west-2c"); defaultRegionVsAzMap.put("eu-west-1", "eu-west-1a"); defaultRegionVsAzMap.put("eu-west-1", "eu-west-1b"); defaultRegionVsAzMap.put("eu-west-1", "eu-west-1c"); } }