All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.microsoft.azure.documentdb.LocationCache Maven / Gradle / Ivy

/*
 * Copyright (c) Microsoft Corporation.  All rights reserved.
 */

package com.microsoft.azure.documentdb;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.apache.commons.lang3.StringUtils;
import org.joda.time.Instant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.microsoft.azure.documentdb.internal.DocumentServiceRequest;
import com.microsoft.azure.documentdb.internal.OperationType;
import com.microsoft.azure.documentdb.internal.ResourceType;
import com.microsoft.azure.documentdb.internal.Utils;

/**
 * Implements the abstraction to resolve target location for geo-replicated DatabaseAccount 
 * with multiple writable and readable locations.
 */
public class LocationCache {

    private enum EndpointOperationType { None, Read, Write };
    private class LocationUnavailabilityInfo {

        private Long lastUnavailabilityCheckTimeStamp;
        private EnumSet operationTypes;
        
        public LocationUnavailabilityInfo(long timeStamp, EnumSet operationTypes) {
            this.lastUnavailabilityCheckTimeStamp = timeStamp;
            this.operationTypes = operationTypes;
        }

        public Long getLastUnavailabilityCheckTimeStamp() {
            return this.lastUnavailabilityCheckTimeStamp;
        }

        public EnumSet getOperationTypes() {
            return this.operationTypes;
        }
    }

    public static class CanRefreshInBackground {
        private boolean value;
        
        public CanRefreshInBackground(boolean value) {
            this.value = value;
        }
        
        public boolean getValue() {
            return this.value;
        }
        
        public void setValue(boolean value) {
            this.value = value;
        }
    }

    public static class UpdatableList {
        private List value;
        
        public UpdatableList(ArrayList value) {
            this.value = new ArrayList(value);
        }
        
        public List getValue() {
            return this.value;
        }
        
        public void setValue(List value) {
            this.value = new ArrayList(value);
        }
    }
    
    private class DatabaseAccountLocationsInfo {
        public List availableWriteLocations;
        public List availableReadLocations;
        public Map availableReadEndpointByLocation;
        public Map availableWriteEndpointByLocation;
        public List writeEndpoints;
        public List readEndpoints;
        
        public DatabaseAccountLocationsInfo(URI defaultEndpoint) {
            this.availableReadLocations = new ArrayList();
            this.availableWriteLocations = new ArrayList();
            this.writeEndpoints = new ArrayList();
            this.writeEndpoints.add(defaultEndpoint);
            this.readEndpoints = new ArrayList();
            this.readEndpoints.add(defaultEndpoint);
        }

        public DatabaseAccountLocationsInfo(DatabaseAccountLocationsInfo other) {
            this.availableWriteLocations = new ArrayList(other.availableWriteLocations);
            this.availableReadLocations = new ArrayList(other.availableReadLocations);
            if (other.availableWriteEndpointByLocation != null) {
                this.availableWriteEndpointByLocation = new HashMap(other.availableWriteEndpointByLocation);
            }
            if (other.availableReadEndpointByLocation != null) {
                this.availableReadEndpointByLocation = new HashMap(other.availableReadEndpointByLocation);
            }
            this.writeEndpoints = new ArrayList(other.writeEndpoints);
            this.readEndpoints = new ArrayList(other.readEndpoints);
        }
    }
    
    private final Logger logger = LoggerFactory.getLogger(LocationCache.class);
    private final Collection preferredLocations;
    private final boolean enableEndpointDiscovery;
    private final URI defaultEndpoint;
    private boolean enableMultipleWritableLocations;
    private boolean useMultipleWritableLocations;
    private final long backgroundRefreshLocationTimeIntervalInMS;
    private long lastCacheUpdateTimestamp;
    private DatabaseAccountLocationsInfo locationInfo;
    private ConcurrentMap locationUnavailablityInfoByEndpoint;
    
    LocationCache(Collection preferredLocations, URI defaultEndpoint, boolean enableEndpointDiscovery,
            boolean useMultipleWriteLocations, long backgroundRefreshLocationTimeIntervalInMS) {
        this.locationInfo = new DatabaseAccountLocationsInfo(defaultEndpoint);
        this.preferredLocations = preferredLocations;
        this.defaultEndpoint = defaultEndpoint;
        this.enableEndpointDiscovery = enableEndpointDiscovery;
        this.useMultipleWritableLocations = useMultipleWriteLocations;
        this.locationUnavailablityInfoByEndpoint = new ConcurrentHashMap<>();
        this.backgroundRefreshLocationTimeIntervalInMS = backgroundRefreshLocationTimeIntervalInMS;
        this.lastCacheUpdateTimestamp = Long.MIN_VALUE;
    }
    
    /**
     * Gets list of write endpoints ordered by
     * 1. Preferred location
     * 2. Endpoint availability
     * @return
     */
    List getWriteEndpoints() {
        if (this.locationUnavailablityInfoByEndpoint.size() > 0 
                && System.currentTimeMillis() - this.lastCacheUpdateTimestamp > this.backgroundRefreshLocationTimeIntervalInMS) {
            this.updateEndpointsCache();
        }
        return this.locationInfo.writeEndpoints;
    }

    /**
     * Gets list of read endpoints ordered by
     *
     * 1. Preferred location
     * 2. Endpoint availability
     * @return
     */
    List getReadEndpoints() {
        if (this.locationUnavailablityInfoByEndpoint.size() > 0 
                && System.currentTimeMillis() - this.lastCacheUpdateTimestamp > this.backgroundRefreshLocationTimeIntervalInMS) {
            this.updateEndpointsCache();
        }
        return this.locationInfo.readEndpoints;
    }

    URI getWriteEndpoint() {
        return this.getWriteEndpoints().get(0);
    }

    URI getReadEndpoint() {
        return this.getReadEndpoints().get(0);
    }

    List getOrderedWriteEndpoints() {
        return this.locationInfo.availableWriteLocations;
    }

    List getOrderedReadEndpoints() {
        return this.locationInfo.availableReadLocations;
    }

    /**
     * Marks the current location unavailable for read
     * @param endpoint 
     */
    void markCurrentLocationUnavailableForRead(URI endpoint) {
        this.markEndpointUnavailable(endpoint, EndpointOperationType.Read);
    }

    /**
     * Marks the current location unavailable for read
     * @param endpoint 
     */
    void markCurrentLocationUnavailableForWrite(URI endpoint) {
        this.markEndpointUnavailable(endpoint, EndpointOperationType.Write);
    }
    
    /**
     * Invoked when {@link DatabaseAccount} is read
     * @param databaseAccount Read DatabaseAccount
     */
    public void onDatabaseAccountRead(DatabaseAccount databaseAccount) {
        this.updateLocationCache(databaseAccount.getWritableLocations(),
                databaseAccount.getReadableLocations(),
                databaseAccount.getEnableMultipleWritableLocations());
    }
    
    /**
     * Resolves request to service endpoint.
     * 1. If this is a write request
     *    (a) If UseMultipleWriteLocations = true
     *        (i) For document writes, resolve to most preferred and available write endpoint.
     *            Once the endpoint is marked unavailable, it is moved to the end of available write endpoint. Current request will
     *            be retried on next preferred available write endpoint.
     *        (ii) For all other resources, always resolve to first/second (regardless of preferred locations)
     *             write endpoint in {@link DatabaseAccount#getWritableLocations()}.
     *             Endpoint of first write location in {@link DatabaseAccount#getWritableLocations()} is the only endpoint that supports
     *             write operation on all resource types (except during that region's failover).
     *             Only during manual failover, client would retry write on second write location in {@link DatabaseAccount#getWritableLocations()}.
     *    (b) Else resolve the request to first write endpoint in {@link DatabaseAccount#getWritableLocations()} OR
     *        second write endpoint in {@link DatabaseAccount#getWritableLocations()} in case of manual failover of that location.
     * 2. Else resolve the request to most preferred available read endpoint (automatic failover for read requests)
     * @param request Request for which endpoint is to be resolved
     * @return Resolved endpoint
     */
     URI resolveServiceEndpoint(DocumentServiceRequest request) {
        
        if (request.getLocationEndpointToRoute() != null) {
            return request.getLocationEndpointToRoute();
        }
        
        int locationIndex = request.getLocationIndexToRoute() != null ? request.getLocationIndexToRoute() : 0;
        boolean usePreferredLocations = request.getUsePreferredLocations() != null ? request.getUsePreferredLocations() : true;

        if (!usePreferredLocations // Should not use preferred location? 
                || (Utils.isWriteOperation(request.getOperationType())  && !this.canUseMultipleWriteLocations(request))) {

            // For non-document resource types in case of client can use multiple write locations
            // or when client cannot use multiple write locations, flip-flop between the
            // first and the second writable region in DatabaseAccount (for manual failover)
            DatabaseAccountLocationsInfo currentLocationInfo = this.locationInfo;
            
            if (this.enableEndpointDiscovery && currentLocationInfo.availableWriteLocations.size() > 0) {
                locationIndex = Math.min(locationIndex % 2, currentLocationInfo.availableWriteLocations.size() - 1);
                String writeLocation = currentLocationInfo.availableWriteLocations.get(locationIndex);
                return currentLocationInfo.availableWriteEndpointByLocation.get(writeLocation);
            } else {
                return this.defaultEndpoint;
            }
        } else {
            List endpoints = Utils.isWriteOperation(request.getOperationType()) ? this.getWriteEndpoints() : this.getReadEndpoints();
            return endpoints.get(locationIndex % endpoints.size());
        }
    }
    
    boolean shouldRefreshEndpoints(CanRefreshInBackground canRefreshInBackground) {
        String mostPreferredLocation = this.preferredLocations != null && this.preferredLocations.size() > 0 ?
                this.preferredLocations.iterator().next() : null;

        canRefreshInBackground.setValue(true);
        DatabaseAccountLocationsInfo currentLocationInfo = this.locationInfo;

        // we should schedule refresh in background if we are unable to target the user's most preferredLocation.
        if (this.enableEndpointDiscovery) {
            
            // Refresh if client opts-in to useMultipleWriteLocations but server-side setting is disabled
            boolean shouldRefresh = this.useMultipleWritableLocations && !this.enableMultipleWritableLocations;
            
            if (StringUtils.isNotEmpty(mostPreferredLocation)) {
                if (currentLocationInfo.availableReadEndpointByLocation != null) {
                    URI mostPreferredReadEndpoint = currentLocationInfo.availableReadEndpointByLocation.get(mostPreferredLocation);
                    if (mostPreferredReadEndpoint != null && !mostPreferredReadEndpoint.equals(currentLocationInfo.readEndpoints.get(0))) {
                        
                        // For reads, we can always refresh in background as we can alternate to
                        // other available read endpoints
                        logger.trace("shouldRefreshEndpoints = true since most preferred location [{}]" +
                                " is not available for read.", mostPreferredLocation);
                        return true;
                    }
                } else {
                    logger.trace("shouldRefreshEndpoints = true since most preferred location [{}] " +
                            "is not in available read locations.", mostPreferredLocation);
                    return true;
                }
            }
            
            if (!this.canUseMultipleWriteLocations()) {
                if (this.isEndpointUnavailable(currentLocationInfo.writeEndpoints.get(0), EndpointOperationType.Write)) {
                    
                    // Since most preferred write endpoint is unavailable, we can only refresh in background if 
                    // we have an alternate write endpoint
                    canRefreshInBackground.setValue(currentLocationInfo.writeEndpoints.size() > 1);
                    logger.trace("shouldRefreshEndpoints = true since most preferred location " +
                            "[{}] endpoint [{}] is not available for write. canRefreshInBackground = [{}]",
                                mostPreferredLocation,
                                currentLocationInfo.writeEndpoints.get(0),
                                canRefreshInBackground.getValue());
                    return true;
                } else {
                    return shouldRefresh;
                }
            } else if (StringUtils.isNotEmpty(mostPreferredLocation)) {
                URI  mostPreferredWriteEndpoint = currentLocationInfo.availableWriteEndpointByLocation.get(mostPreferredLocation);
                if (mostPreferredWriteEndpoint != null) {
                    shouldRefresh |= !mostPreferredWriteEndpoint.equals(currentLocationInfo.writeEndpoints.get(0));
                    logger.trace("shouldRefreshEndpoints = [{}] since most preferred location [{}] is not available for write.",
                            shouldRefresh, mostPreferredLocation);
                    return shouldRefresh;
                } else {
                    logger.trace("shouldRefreshEndpoints = true since most preferred location [{}] is not in available write locations",
                            mostPreferredLocation);
                    return true;
                }
            } else {
                return shouldRefresh;
            }
        } else {
            return false;
        }
    }

    private void clearStaleEndpointUnavailabilityInfo() {
        if (this.locationUnavailablityInfoByEndpoint.size() > 0) {
            Set unavailableEndpoints = this.locationUnavailablityInfoByEndpoint.keySet();
            for (String unavailableEndpoint : unavailableEndpoints) {
                LocationUnavailabilityInfo unavailabilityInfo = locationUnavailablityInfoByEndpoint.get(unavailableEndpoint);
                if (unavailabilityInfo != null 
                        && System.currentTimeMillis() - unavailabilityInfo.getLastUnavailabilityCheckTimeStamp() > this.backgroundRefreshLocationTimeIntervalInMS) {
                    locationUnavailablityInfoByEndpoint.remove(unavailableEndpoint);
                    logger.trace("Removed endpoint [{}] unavailable for operations [{}] from unavailableEndpoints",
                            unavailableEndpoint, unavailabilityInfo.getOperationTypes().toString());
                }
            }
        }
    }
    
    private boolean isEndpointUnavailable(URI endpoint, EndpointOperationType expectedAvailableOperations) {
        LocationUnavailabilityInfo unavailabilityInfo = this.locationUnavailablityInfoByEndpoint.get(endpoint.toString());

        if (expectedAvailableOperations == EndpointOperationType.None || unavailabilityInfo == null
                || !unavailabilityInfo.getOperationTypes().contains(expectedAvailableOperations)) {
            return false;
        } else {
            if (System.currentTimeMillis() - unavailabilityInfo.getLastUnavailabilityCheckTimeStamp() > this.backgroundRefreshLocationTimeIntervalInMS) {
                return false;
            } else {
                logger.trace(
                        "Endpoint [{}] unavailable for operations [{}] present in unavailableEndpoints",
                        endpoint,
                        unavailabilityInfo.getOperationTypes().toString());
                // Unexpired entry present. Endpoint is unavailable
                return true;
            }
        }
    }
    
    private void markEndpointUnavailable(URI unavailableEndpoint, EndpointOperationType unavailableOperationType) {
        LocationUnavailabilityInfo unavailablilityInfo = this.locationUnavailablityInfoByEndpoint.get(unavailableEndpoint.toString());
        Instant currentTime = Instant.now();
        if (unavailablilityInfo == null) {
             this.locationUnavailablityInfoByEndpoint.put(unavailableEndpoint.toString(), 
                    new LocationUnavailabilityInfo(currentTime.getMillis(), EnumSet.of(unavailableOperationType)));
        } else {
            EnumSet unavailableOperations = EnumSet.of(unavailableOperationType);
            unavailableOperations.addAll(unavailablilityInfo.getOperationTypes());
            this.locationUnavailablityInfoByEndpoint.put(unavailableEndpoint.toString(), 
                    new LocationUnavailabilityInfo(currentTime.getMillis(), unavailableOperations));
        }

        this.updateEndpointsCache();

        logger.trace(
                "Endpoint [{}] unavailable for [{}] added/updated to unavailableEndpoints with timestamp [{}]",
                unavailableEndpoint,
                unavailableOperationType,
                currentTime.toDateTime());     
    }

    Collection getPreferredLocations() {
        return this.preferredLocations;
    }
    
    private synchronized void updateEndpointsCache() {
        this.updateLocationCache(null, null, null);
    }

    synchronized void updateLocationCache(Iterable writeLocations,
            Iterable readLocations, Boolean enableMultipleWritableLocations) {        
        
        DatabaseAccountLocationsInfo nextLocationInfo = new DatabaseAccountLocationsInfo(this.locationInfo);

        if (enableMultipleWritableLocations != null) {
            this.enableMultipleWritableLocations = enableMultipleWritableLocations;
        }
        
        this.clearStaleEndpointUnavailabilityInfo();
        
        if (this.enableEndpointDiscovery) {
            if (readLocations != null) {
                UpdatableList availableReadLocations = new UpdatableList(new ArrayList());
                nextLocationInfo.availableReadEndpointByLocation = Collections
                        .unmodifiableMap(this.getEndpointByLocation(readLocations, availableReadLocations));
                nextLocationInfo.availableReadLocations = availableReadLocations.getValue();
            }

            if (writeLocations != null) {
                UpdatableList availableWriteLocations = new UpdatableList(new ArrayList());
                nextLocationInfo.availableWriteEndpointByLocation = Collections
                        .unmodifiableMap(this.getEndpointByLocation(writeLocations, availableWriteLocations));
                nextLocationInfo.availableWriteLocations = availableWriteLocations.getValue();
            }
        }
        
        nextLocationInfo.writeEndpoints = this.GetPreferredAvailableEndpoints(nextLocationInfo.availableWriteEndpointByLocation, nextLocationInfo.availableWriteLocations, EndpointOperationType.Write, this.defaultEndpoint);
        nextLocationInfo.readEndpoints = this.GetPreferredAvailableEndpoints(nextLocationInfo.availableReadEndpointByLocation, nextLocationInfo.availableWriteLocations, EndpointOperationType.Read, nextLocationInfo.writeEndpoints.get(0));
        this.lastCacheUpdateTimestamp = System.currentTimeMillis();
        
        logger.trace("Current writeEndpoints = ({}) readEndpoints = ({})",
                String.join(", ", nextLocationInfo.writeEndpoints.toString()),
                String.join(", ", nextLocationInfo.readEndpoints.toString()));

        this.locationInfo = nextLocationInfo;
    }

    private List GetPreferredAvailableEndpoints(Map endpointsByLocation, List orderedLocations,
            EndpointOperationType expectedAvailableOperation, URI fallbackEndpoint) {
        List endpoints = new ArrayList();
        
        // if enableEndpointDiscovery is false, we always use the defaultEndpoint that user passed in during documentClient init
        if (this.enableEndpointDiscovery && endpointsByLocation != null && !endpointsByLocation.isEmpty()) {
            if (this.canUseMultipleWriteLocations() || expectedAvailableOperation == EndpointOperationType.Read) {
                List unavailableEndpoints = new ArrayList();
                if (this.preferredLocations != null && !this.preferredLocations.isEmpty()) {
                    
                    // When client can not use multiple write locations, preferred locations list should only be used
                    // determining read endpoints order. 
                    // If client can use multiple write locations, preferred locations list should be used for determining
                    // both read and write endpoints order.
                    for (String location : this.preferredLocations) {
                        URI endpoint = endpointsByLocation.get(location);
                        if (endpoint != null) {
                            if (this.isEndpointUnavailable(endpoint, expectedAvailableOperation)) {
                                unavailableEndpoints.add(endpoint);
                            } else {
                                endpoints.add(endpoint);
                            }
                        }
                    }
                }
                if (endpoints.size() == 0) {
                    endpoints.add(fallbackEndpoint);
                }
                
                endpoints.addAll(unavailableEndpoints);
            } else {
                
                for (String location : orderedLocations) { // location is empty during manual failover
                    URI endpoint;
                    if (!StringUtils.isEmpty(location)) {
                        endpoint = endpointsByLocation.get(location);
                        if (endpoint != null) {
                            endpoints.add(endpoint);
                        }
                    }
                }
            }
        }
        
        if (endpoints.size() == 0) {
            endpoints.add(fallbackEndpoint);
        }
        
        return Collections.unmodifiableList(endpoints);
    }

    Map getEndpointByLocation(Iterable locations, UpdatableList orderedLocations) {
        // using LinkedHahsMap to maintain insertion order while iterating over the map
        LinkedHashMap endpointsByLocation = new LinkedHashMap();
        List parsedLocations = new ArrayList();

        for (DatabaseAccountLocation location : locations) {
            if (StringUtils.isEmpty(location.getName())) {
                // during fail-over the location name is empty
                continue;
            }
            try {
                URI regionUri = new URI(location.getEndpoint());
                endpointsByLocation.put(location.getName(), regionUri);
                parsedLocations.add(location.getName());
            } catch (URISyntaxException e) {
                logger.warn("GetAvailableEndpointsByLocation() - skipping add for location = [{}] as it is location name is either empty or endpoint is malformed [{}]",
                        location.getName(),
                        location.getEndpoint());
            }
        }
        
        orderedLocations.setValue(Collections.unmodifiableList(parsedLocations));
        return endpointsByLocation;
    }
    
    private boolean canUseMultipleWriteLocations() {
        return this.useMultipleWritableLocations && this.enableMultipleWritableLocations;
    }
    

    public boolean canUseMultipleWriteLocations(DocumentServiceRequest request) {
        return this.canUseMultipleWriteLocations() &&
                (request.getResourceType() == ResourceType.Document ||
                (request.getResourceType() == ResourceType.StoredProcedure && request.getOperationType() == OperationType.ExecuteJavaScript));
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy