Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* 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));
}
}