com.microsoft.azure.documentdb.GlobalEndpointManager Maven / Gradle / Ivy
package com.microsoft.azure.documentdb;
import com.microsoft.azure.documentdb.LocationCache.CanRefreshInBackground;
import com.microsoft.azure.documentdb.internal.AsyncCache;
import com.microsoft.azure.documentdb.internal.Constants;
import com.microsoft.azure.documentdb.internal.DocumentServiceRequest;
import com.microsoft.azure.documentdb.internal.EndpointManager;
import com.microsoft.azure.documentdb.internal.HttpConstants;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
/**
* This class implements the logic for endpoint management for geo-replicated
* database accounts.
*
* When ConnectionPolicy.getEnableEndpointDiscovery is true,
* the GlobalEndpointManager will choose the correct endpoint to use for write
* and read operations based on database account information retrieved from the
* service in conjunction with user's preference as specified in
* ConnectionPolicy().getPreferredLocations.
*/
class GlobalEndpointManager implements EndpointManager {
private final DatabaseAccountManagerInternal client;
private final Logger logger = LoggerFactory.getLogger(GlobalEndpointManager.class);
private final boolean enableEndpointDiscovery;
private final URI defaultEndpoint;
private final Object refreshLock;
private DocumentClient documentClient;
private LocationCache locationCache;
private Future endpointRefreshTaskFuture;
private long backgroundRefreshLocationTimeIntervalInMS;
private volatile boolean isRefreshing;
private AsyncCache databaseAccountCache;
public GlobalEndpointManager(final DocumentClient client) {
this(new DatabaseAccountManagerInternal() {
@Override
public URI getServiceEndpoint() {
return client.getServiceEndpoint();
}
@Override
public DatabaseAccount getDatabaseAccountFromEndpoint(URI endpoint) throws DocumentClientException {
return client.getDatabaseAccountFromEndpoint(endpoint);
}
@Override
public ConnectionPolicy getConnectionPolicy() {
return client.getConnectionPolicy();
}
});
this.documentClient = client;
this.databaseAccountCache = new AsyncCache(client.getExecutorService());
}
public GlobalEndpointManager(DatabaseAccountManagerInternal client) {
this.client = client;
this.enableEndpointDiscovery = client.getConnectionPolicy().getEnableEndpointDiscovery();
Collection preferredLocations = client.getConnectionPolicy().getPreferredLocations() != null ?
Collections.unmodifiableCollection(client.getConnectionPolicy().getPreferredLocations()) : null;
this.defaultEndpoint = client.getServiceEndpoint();
this.backgroundRefreshLocationTimeIntervalInMS = getBackgroundRefreshLocationTimeIntervalInMS();
this.locationCache = createLocationCache(preferredLocations,
client.getServiceEndpoint(),
this.enableEndpointDiscovery,
client.getConnectionPolicy().isUsingMultipleWriteLocations(),
this.backgroundRefreshLocationTimeIntervalInMS);
this.isRefreshing = false;
this.refreshLock = new Object();
}
long getBackgroundRefreshLocationTimeIntervalInMS() {
return Constants.Properties.DEFAULT_UNAVAILABLE_LOCATION_EXPIRATION_TIME;
}
LocationCache createLocationCache(Collection preferredLocations, URI serviceEndpoint,
boolean enableEndpointDiscovery, boolean useMultipleWriteLocations,
long defaultUnavailableLocationExpirationTime) {
return new LocationCache(preferredLocations,
serviceEndpoint,
enableEndpointDiscovery,
useMultipleWriteLocations,
defaultUnavailableLocationExpirationTime);
}
@Override
public URI getWriteEndpoint() {
return this.locationCache.getWriteEndpoint();
}
@Override
public URI getReadEndpoint() {
return this.locationCache.getReadEndpoint();
}
@Override
public List getOrderedWriteEndpoints() {
return this.locationCache.getOrderedWriteEndpoints();
}
@Override
public List getOrderedReadEndpoints() {
return this.locationCache.getOrderedReadEndpoints();
}
public DatabaseAccount getDatabaseAccountFromAnyEndpoint() {
DatabaseAccount databaseAccount = null;
try {
databaseAccount = this.client.getDatabaseAccountFromEndpoint(this.defaultEndpoint);
} catch (DocumentClientException e) {
this.logger.warn("Failed to retrieve database account information. {}", e.toString());
}
// The global endpoint was not working. Try other endpoints in the preferred read region list.
Collection preferredLocations = this.locationCache.getPreferredLocations();
if (databaseAccount == null &&
preferredLocations != null &&
preferredLocations.size() > 0) {
for (String regionName : preferredLocations) {
URI regionalUri = this.getRegionalEndpoint(regionName);
if (regionalUri != null) {
try {
databaseAccount = this.client.getDatabaseAccountFromEndpoint(regionalUri);
break;
} catch (DocumentClientException e) {
this.logger.warn("Failed to retrieve database account information. {}", e.toString());
}
}
}
}
return databaseAccount;
}
@Override
public URI resolveServiceEndpoint(DocumentServiceRequest request) {
return this.locationCache.resolveServiceEndpoint(request);
}
@Override
public void markEndpointUnavailableForRead(URI endpoint) {
//this.logger.warn(String.format("Marking endpoint %s unavailable for Read", endpoint.toString()));
this.locationCache.markCurrentLocationUnavailableForRead(endpoint);
}
@Override
public void markEndpointUnavailableForWrite(URI endpoint) {
//this.logger.warn(String.format("Marking endpoint %s unavailable for Write", endpoint.toString()));
this.locationCache.markCurrentLocationUnavailableForWrite(endpoint);
}
public boolean canUseMultipleWriteLocations(DocumentServiceRequest request) {
return this.locationCache.canUseMultipleWriteLocations(request);
}
public void close() {
if (endpointRefreshTaskFuture != null) {
endpointRefreshTaskFuture.cancel(true);
}
}
URI getRegionalEndpoint(String regionName) {
if (StringUtils.isNotEmpty(regionName)) {
String databaseAccountName = this.defaultEndpoint.getHost();
int indexOfDot = this.defaultEndpoint.getHost().indexOf('.');
if (indexOfDot >= 0) {
databaseAccountName = databaseAccountName.substring(0, indexOfDot);
}
// Add region name suffix to the account name.
String regionalAccountName = databaseAccountName + "-" + regionName.replace(" ", "");
String regionalUrl = this.defaultEndpoint.toString().replaceFirst(databaseAccountName, regionalAccountName);
try {
return new URI(regionalUrl);
} catch (URISyntaxException e) {
return null;
}
}
return null;
}
@Override
public void refreshEndpointList(DatabaseAccount databaseAccount, boolean forceRefresh) {
if (forceRefresh) {
DatabaseAccount refreshedDatabaseAccount = this.refreshDatabaseAccountInternal();
this.locationCache.onDatabaseAccountRead(refreshedDatabaseAccount);
return;
}
this.refreshEndpointList(databaseAccount);
}
@Override
public synchronized void refreshEndpointList(DatabaseAccount databaseAccount) {
synchronized (this.refreshLock) {
if (this.isRefreshing) return;
this.isRefreshing = true;
}
try {
this.refreshEndpointListPrivate(databaseAccount);
} catch (Exception e) {
this.isRefreshing = false;
throw e;
}
}
public synchronized void refreshEndpointListPrivate(DatabaseAccount databaseAccount) {
if (databaseAccount != null) {
logger.debug("Refreshing endpoints list");
this.locationCache.onDatabaseAccountRead(databaseAccount);
}
CanRefreshInBackground canRefreshInBackground = new CanRefreshInBackground(false);
if (this.locationCache.shouldRefreshEndpoints(canRefreshInBackground)) {
if (databaseAccount == null && !canRefreshInBackground.getValue()) {
logger.debug("Refreshing endpoints list");
databaseAccount = this.refreshDatabaseAccountInternal();
this.locationCache.onDatabaseAccountRead(databaseAccount);
}
this.startRefreshLocationTimer();
} else {
this.isRefreshing = false;
}
}
public void startRefreshLocationTimer() {
final GlobalEndpointManager that = this;
endpointRefreshTaskFuture = this.documentClient.getExecutorService().submit(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(that.backgroundRefreshLocationTimeIntervalInMS);
DatabaseAccount databaseAccount = that.refreshDatabaseAccountInternal();
that.refreshEndpointListPrivate(databaseAccount);
} catch (Exception e) {
logger.warn("Preferred location background task was interrupted");
that.startRefreshLocationTimer();
}
}
});
}
public DatabaseAccount getDatabaseAccountFromCache() {
try {
final GlobalEndpointManager that = this;
return this.databaseAccountCache.get(StringUtils.EMPTY, null, new Callable() {
@Override
public DatabaseAccount call() throws Exception {
DatabaseAccount databaseAccount = that.getDatabaseAccountFromAnyEndpoint();
GlobalEndpointManager.this.refreshEndpointList(databaseAccount);
return databaseAccount;
}
}).get();
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
private DatabaseAccount refreshDatabaseAccountInternal() {
final GlobalEndpointManager that = this;
Callable fetchDatabaseAccount = new Callable() {
@Override
public DatabaseAccount call() throws Exception {
return that.getDatabaseAccountFromAnyEndpoint();
}
};
try {
DatabaseAccount obsoleteDatabaseAccount = this.databaseAccountCache.get(StringUtils.EMPTY, null, fetchDatabaseAccount).get();
DatabaseAccount newDatabaseAccount = this.databaseAccountCache.get(StringUtils.EMPTY, obsoleteDatabaseAccount, fetchDatabaseAccount).get();
if(newDatabaseAccount == null && obsoleteDatabaseAccount != null) {
this.databaseAccountCache.put(StringUtils.EMPTY, obsoleteDatabaseAccount);
}
return newDatabaseAccount;
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
}