/*
* Copyright (c) 2008-2014 MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mongodb;
import com.mongodb.annotations.Immutable;
import com.mongodb.connection.ClusterDescription;
import com.mongodb.connection.ClusterType;
import com.mongodb.connection.ServerDescription;
import org.bson.BsonArray;
import org.bson.BsonDocument;
import org.bson.BsonInt64;
import org.bson.BsonString;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static com.mongodb.assertions.Assertions.isTrueArgument;
import static com.mongodb.assertions.Assertions.notNull;
import static java.lang.String.format;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
/**
* Abstract class for all preference which can be combined with tags
*/
@Immutable
public abstract class TaggableReadPreference extends ReadPreference {
private static final int SMALLEST_MAX_STALENESS_MS = 90000;
private static final int IDLE_WRITE_PERIOD_MS = 10000;
private final List<TagSet> tagSetList = new ArrayList<TagSet>();
private final Long maxStalenessMS;
TaggableReadPreference() {
this.maxStalenessMS = null;
}
TaggableReadPreference(final List<TagSet> tagSetList, final Long maxStaleness, final TimeUnit timeUnit) {
notNull("tagSetList", tagSetList);
isTrueArgument("maxStaleness is null or >= 0", maxStaleness == null || maxStaleness >= 0);
this.maxStalenessMS = maxStaleness == null ? null : MILLISECONDS.convert(maxStaleness, timeUnit);
for (final TagSet tagSet : tagSetList) {
this.tagSetList.add(tagSet);
}
}
@Override
public boolean isSlaveOk() {
return true;
}
@Override
public BsonDocument toDocument() {
BsonDocument readPrefObject = new BsonDocument("mode", new BsonString(getName()));
if (!tagSetList.isEmpty()) {
readPrefObject.put("tags", tagsListToBsonArray());
}
if (maxStalenessMS != null) {
readPrefObject.put("maxStalenessSeconds", new BsonInt64(MILLISECONDS.toSeconds(maxStalenessMS)));
}
return readPrefObject;
}
/**
* Gets the list of tag sets as a list of {@code TagSet} instances.
*
* @return the list of tag sets
* @since 2.13
*/
public List<TagSet> getTagSetList() {
return Collections.unmodifiableList(tagSetList);
}
/**
* Gets the maximum acceptable staleness of a secondary in order to be considered for read operations.
* <p>
* The maximum staleness feature is designed to prevent badly-lagging servers from being selected. The staleness estimate is imprecise
* and shouldn't be used to try to select "up-to-date" secondaries.
* </p>
* <p>
* The driver estimates the staleness of each secondary, based on lastWriteDate values provided in server isMaster responses,
* and selects only those secondaries whose staleness is less than or equal to maxStaleness.
* </p>
* @param timeUnit the time unit in which to return the value
* @return the maximum acceptable staleness in the given time unit, or null if the value is not set
* @mongodb.server.release 3.4
* @since 3.4
*/
public Long getMaxStaleness(final TimeUnit timeUnit) {
notNull("timeUnit", timeUnit);
if (maxStalenessMS == null) {
return null;
}
return timeUnit.convert(maxStalenessMS, TimeUnit.MILLISECONDS);
}
@Override
public String toString() {
return "ReadPreference{"
+ "name=" + getName()
+ (tagSetList.isEmpty() ? "" : ", tagSetList=" + tagSetList)
+ (maxStalenessMS == null ? "" : ", maxStalenessMS=" + maxStalenessMS)
+ '}';
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
TaggableReadPreference that = (TaggableReadPreference) o;
if (maxStalenessMS != null ? !maxStalenessMS.equals(that.maxStalenessMS) : that.maxStalenessMS != null) {
return false;
}
if (!tagSetList.equals(that.tagSetList)) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = tagSetList.hashCode();
result = 31 * result + getName().hashCode();
result = 31 * result + (maxStalenessMS != null ? maxStalenessMS.hashCode() : 0);
return result;
}
@Override
@SuppressWarnings("deprecation")
protected List<ServerDescription> chooseForNonReplicaSet(final ClusterDescription clusterDescription) {
return selectFreshServers(clusterDescription, clusterDescription.getAny());
}
protected static ClusterDescription copyClusterDescription(final ClusterDescription clusterDescription,
final List<ServerDescription> selectedServers) {
return new ClusterDescription(clusterDescription.getConnectionMode(),
clusterDescription.getType(),
selectedServers,
clusterDescription.getClusterSettings(),
clusterDescription.getServerSettings());
}
protected List<ServerDescription> selectFreshServers(final ClusterDescription clusterDescription,
final List<ServerDescription> servers) {
if (getMaxStaleness(MILLISECONDS) == null) {
return servers;
}
if (clusterDescription.getServerSettings() == null) {
throw new MongoConfigurationException("heartbeat frequency must be provided in cluster description");
}
if (!serversAreAllThreeDotFour(clusterDescription)) {
throw new MongoConfigurationException("Servers must all be at least version 3.4 when max staleness is configured");
}
if (clusterDescription.getType() != ClusterType.REPLICA_SET) {
return servers;
}
long heartbeatFrequencyMS = clusterDescription.getServerSettings().getHeartbeatFrequency(MILLISECONDS);
if (getMaxStaleness(MILLISECONDS) < Math.max(SMALLEST_MAX_STALENESS_MS, heartbeatFrequencyMS + IDLE_WRITE_PERIOD_MS)) {
if (SMALLEST_MAX_STALENESS_MS > heartbeatFrequencyMS + IDLE_WRITE_PERIOD_MS){
throw new MongoConfigurationException(format("Max staleness (%d sec) must be at least 90 seconds",
getMaxStaleness(SECONDS)));
} else {
throw new MongoConfigurationException(format("Max staleness (%d ms) must be at least the heartbeat period (%d ms) "
+ "plus the idle write period (%d ms)",
getMaxStaleness(MILLISECONDS), heartbeatFrequencyMS, IDLE_WRITE_PERIOD_MS));
}
}
List<ServerDescription> freshServers = new ArrayList<ServerDescription>(servers.size());
ServerDescription primary = findPrimary(clusterDescription);
if (primary != null) {
for (ServerDescription cur : servers) {
if (cur.isPrimary()) {
freshServers.add(cur);
} else {
if (getStalenessOfSecondaryRelativeToPrimary(primary, cur, heartbeatFrequencyMS) <= getMaxStaleness(MILLISECONDS)) {
freshServers.add(cur);
}
}
}
} else {
ServerDescription mostUpdateToDateSecondary = findMostUpToDateSecondary(clusterDescription);
for (ServerDescription cur : servers) {
if (mostUpdateToDateSecondary.getLastWriteDate().getTime() - cur.getLastWriteDate().getTime() + heartbeatFrequencyMS
<= getMaxStaleness(MILLISECONDS)) {
freshServers.add(cur);
}
}
}
return freshServers;
}
private long getStalenessOfSecondaryRelativeToPrimary(final ServerDescription primary, final ServerDescription serverDescription,
final long heartbeatFrequencyMS) {
return primary.getLastWriteDate().getTime()
+ (serverDescription.getLastUpdateTime(MILLISECONDS) - primary.getLastUpdateTime(MILLISECONDS))
- serverDescription.getLastWriteDate().getTime() + heartbeatFrequencyMS;
}
private ServerDescription findPrimary(final ClusterDescription clusterDescription) {
for (ServerDescription cur : clusterDescription.getServerDescriptions()) {
if (cur.isPrimary()) {
return cur;
}
}
return null;
}
private ServerDescription findMostUpToDateSecondary(final ClusterDescription clusterDescription) {
ServerDescription mostUpdateToDateSecondary = null;
for (ServerDescription cur : clusterDescription.getServerDescriptions()) {
if (cur.isSecondary()) {
if (mostUpdateToDateSecondary == null
|| cur.getLastWriteDate().getTime() > mostUpdateToDateSecondary.getLastWriteDate().getTime()) {
mostUpdateToDateSecondary = cur;
}
}
}
return mostUpdateToDateSecondary;
}
private boolean serversAreAllThreeDotFour(final ClusterDescription clusterDescription) {
for (ServerDescription cur : clusterDescription.getServerDescriptions()) {
if (cur.isOk() && cur.getMaxWireVersion() < 5) {
return false;
}
}
return true;
}
/**
* Read from secondary
*/
static class SecondaryReadPreference extends TaggableReadPreference {
SecondaryReadPreference() {
}
SecondaryReadPreference(final List<TagSet> tagSetList, final Long maxStaleness, final TimeUnit timeUnit) {
super(tagSetList, maxStaleness, timeUnit);
}
@Override
public String getName() {
return "secondary";
}
@Override
@SuppressWarnings("deprecation")
protected List<ServerDescription> chooseForReplicaSet(final ClusterDescription clusterDescription) {
List<ServerDescription> selectedServers = selectFreshServers(clusterDescription, clusterDescription.getSecondaries());
if (!getTagSetList().isEmpty()) {
ClusterDescription nonStaleClusterDescription = copyClusterDescription(clusterDescription, selectedServers);
selectedServers = Collections.emptyList();
for (final TagSet tagSet : getTagSetList()) {
List<ServerDescription> servers = nonStaleClusterDescription.getSecondaries(tagSet);
if (!servers.isEmpty()) {
selectedServers = servers;
break;
}
}
}
return selectedServers;
}
}
/**
* Read from secondary if available, otherwise from primary, irrespective of tags.
*/
static class SecondaryPreferredReadPreference extends SecondaryReadPreference {
SecondaryPreferredReadPreference() {
}
SecondaryPreferredReadPreference(final List<TagSet> tagSetList, final Long maxStaleness, final TimeUnit timeUnit) {
super(tagSetList, maxStaleness, timeUnit);
}
@Override
public String getName() {
return "secondaryPreferred";
}
@Override
@SuppressWarnings("deprecation")
protected List<ServerDescription> chooseForReplicaSet(final ClusterDescription clusterDescription) {
List<ServerDescription> selectedServers = super.chooseForReplicaSet(clusterDescription);
if (selectedServers.isEmpty()) {
selectedServers = clusterDescription.getPrimaries();
}
return selectedServers;
}
}
/**
* Read from nearest node respective of tags.
*/
static class NearestReadPreference extends TaggableReadPreference {
NearestReadPreference() {
}
NearestReadPreference(final List<TagSet> tagSetList, final Long maxStaleness, final TimeUnit timeUnit) {
super(tagSetList, maxStaleness, timeUnit);
}
@Override
public String getName() {
return "nearest";
}
@Override
@SuppressWarnings("deprecation")
public List<ServerDescription> chooseForReplicaSet(final ClusterDescription clusterDescription) {
List<ServerDescription> selectedServers = selectFreshServers(clusterDescription, clusterDescription.getAnyPrimaryOrSecondary());
if (!getTagSetList().isEmpty()) {
ClusterDescription nonStaleClusterDescription = copyClusterDescription(clusterDescription, selectedServers);
selectedServers = Collections.emptyList();
for (final TagSet tagSet : getTagSetList()) {
List<ServerDescription> servers = nonStaleClusterDescription.getAnyPrimaryOrSecondary(tagSet);
if (!servers.isEmpty()) {
selectedServers = servers;
break;
}
}
}
return selectedServers;
}
}
/**
* Read from primary if available, otherwise a secondary.
*/
static class PrimaryPreferredReadPreference extends SecondaryReadPreference {
PrimaryPreferredReadPreference() {
}
PrimaryPreferredReadPreference(final List<TagSet> tagSetList, final Long maxStaleness, final TimeUnit timeUnit) {
super(tagSetList, maxStaleness, timeUnit);
}
@Override
public String getName() {
return "primaryPreferred";
}
@Override
@SuppressWarnings("deprecation")
protected List<ServerDescription> chooseForReplicaSet(final ClusterDescription clusterDescription) {
List<ServerDescription> selectedServers = selectFreshServers(clusterDescription, clusterDescription.getPrimaries());
if (selectedServers.isEmpty()) {
selectedServers = super.chooseForReplicaSet(clusterDescription);
}
return selectedServers;
}
}
private BsonArray tagsListToBsonArray() {
BsonArray bsonArray = new BsonArray();
for (TagSet tagSet : tagSetList) {
bsonArray.add(toDocument(tagSet));
}
return bsonArray;
}
private BsonDocument toDocument(final TagSet tagSet) {
BsonDocument document = new BsonDocument();
for (Tag tag : tagSet) {
document.put(tag.getName(), new BsonString(tag.getValue()));
}
return document;
}
}