Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions driver-core/src/main/com/mongodb/ConnectionString.java
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ public class ConnectionString {
private String srvServiceName;
private Boolean directConnection;
private Boolean loadBalanced;
private Boolean onlyConnectOriginalUrl;
private ReadPreference readPreference;
private WriteConcern writeConcern;
private Boolean retryWrites;
Expand Down Expand Up @@ -569,6 +570,8 @@ public ConnectionString(final String connectionString, @Nullable final DnsClient
GENERAL_OPTIONS_KEYS.add("srvmaxhosts");
GENERAL_OPTIONS_KEYS.add("srvservicename");

GENERAL_OPTIONS_KEYS.add("onlyconnectoriginalurl");

COMPRESSOR_KEYS.add("compressors");
COMPRESSOR_KEYS.add("zlibcompressionlevel");

Expand Down Expand Up @@ -724,6 +727,9 @@ private void translateOptions(final Map<String, List<String>> optionsMap) {
case "srvservicename":
srvServiceName = value;
break;
case "onlyconnectoriginalurl":
onlyConnectOriginalUrl = parseBoolean(value, "onlyconnectoriginalurl");
break;
default:
break;
}
Expand Down Expand Up @@ -1406,6 +1412,19 @@ public Boolean isLoadBalanced() {
return loadBalanced;
}

/**
* Gets the value of the {@code onlyConnectOriginalUrl} property from the connection string.
* When true, the driver will only connect to the servers specified in the original connection string
* and will not add or remove servers based on replica set topology discovery.
*
* @return true if only the original URL hosts should be connected, or null if unset
* @since 5.7
*/
@Nullable
public Boolean getOnlyConnectOriginalUrl() {
return onlyConnectOriginalUrl;
}

/**
* Get the unparsed connection string.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public final class ClusterSettings {
private final long localThresholdMS;
private final long serverSelectionTimeoutMS;
private final List<ClusterListener> clusterListeners;
private final boolean onlyConnectOriginalUrl;

/**
* Get a builder for this class.
Expand Down Expand Up @@ -96,6 +97,7 @@ public static final class Builder {
private long serverSelectionTimeoutMS = MILLISECONDS.convert(30, TimeUnit.SECONDS);
private long localThresholdMS = MILLISECONDS.convert(15, MILLISECONDS);
private List<ClusterListener> clusterListeners = new ArrayList<>();
private boolean onlyConnectOriginalUrl = false;

private Builder() {
}
Expand All @@ -122,6 +124,7 @@ public Builder applySettings(final ClusterSettings clusterSettings) {
serverSelectionTimeoutMS = clusterSettings.serverSelectionTimeoutMS;
clusterListeners = new ArrayList<>(clusterSettings.clusterListeners);
serverSelector = clusterSettings.serverSelector;
onlyConnectOriginalUrl = clusterSettings.onlyConnectOriginalUrl;
return this;
}

Expand Down Expand Up @@ -314,6 +317,19 @@ public Builder clusterListenerList(final List<ClusterListener> clusterListeners)
return this;
}

/**
* Sets whether to only connect to the servers specified in the original connection string,
* skipping replica set topology discovery (no new hosts added or removed).
*
* @param onlyConnectOriginalUrl true to disable topology-based host management
* @return this
* @since 5.7
*/
public Builder onlyConnectOriginalUrl(final boolean onlyConnectOriginalUrl) {
this.onlyConnectOriginalUrl = onlyConnectOriginalUrl;
return this;
}

/**
* Takes the settings from the given {@code ConnectionString} and applies them to the builder
*
Expand Down Expand Up @@ -364,6 +380,10 @@ public Builder applyConnectionString(final ConnectionString connectionString) {
if (localThreshold != null) {
localThreshold(localThreshold, MILLISECONDS);
}
Boolean onlyConnectOriginalUrl = connectionString.getOnlyConnectOriginalUrl();
if (onlyConnectOriginalUrl != null) {
onlyConnectOriginalUrl(onlyConnectOriginalUrl);
}
return this;
}

Expand Down Expand Up @@ -540,6 +560,17 @@ public List<ClusterListener> getClusterListeners() {
return clusterListeners;
}

/**
* Gets whether the driver should only connect to the servers specified in the original connection string,
* skipping replica set topology discovery.
*
* @return true if only the original URL hosts should be connected
* @since 5.7
*/
public boolean isOnlyConnectOriginalUrl() {
return onlyConnectOriginalUrl;
}

@Override
public boolean equals(final Object o) {
if (this == o) {
Expand All @@ -551,6 +582,7 @@ public boolean equals(final Object o) {
ClusterSettings that = (ClusterSettings) o;
return localThresholdMS == that.localThresholdMS
&& serverSelectionTimeoutMS == that.serverSelectionTimeoutMS
&& onlyConnectOriginalUrl == that.onlyConnectOriginalUrl
&& Objects.equals(srvHost, that.srvHost)
&& Objects.equals(srvMaxHosts, that.srvMaxHosts)
&& srvServiceName.equals(that.srvServiceName)
Expand All @@ -565,7 +597,7 @@ public boolean equals(final Object o) {
@Override
public int hashCode() {
return Objects.hash(srvHost, srvMaxHosts, srvServiceName, hosts, mode, requiredClusterType, requiredReplicaSetName, serverSelector,
localThresholdMS, serverSelectionTimeoutMS, clusterListeners);
localThresholdMS, serverSelectionTimeoutMS, clusterListeners, onlyConnectOriginalUrl);
}

@Override
Expand All @@ -582,6 +614,7 @@ public String toString() {
+ ", clusterListeners='" + clusterListeners + '\''
+ ", serverSelectionTimeout='" + serverSelectionTimeoutMS + " ms" + '\''
+ ", localThreshold='" + localThresholdMS + " ms" + '\''
+ ", onlyConnectOriginalUrl=" + onlyConnectOriginalUrl
+ '}';
}

Expand Down Expand Up @@ -659,5 +692,6 @@ private ClusterSettings(final Builder builder) {
serverSelector = builder.serverSelector;
serverSelectionTimeoutMS = builder.serverSelectionTimeoutMS;
clusterListeners = unmodifiableList(builder.clusterListeners);
onlyConnectOriginalUrl = builder.onlyConnectOriginalUrl;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,11 @@ private boolean handleReplicaSetMemberChanged(final ServerDescription newDescrip
return true;
}

// When onlyConnectOriginalUrl is true, skip topology-based host management (no adding/removing hosts)
if (getSettings().isOnlyConnectOriginalUrl()) {
return true;
}

ensureServers(newDescription);

if (newDescription.getCanonicalAddress() != null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,7 @@ class ConnectionStringSpecification extends Specification {
connectionString.getCompressorList() == []
connectionString.getRetryWritesValue() == null
connectionString.getRetryReads() == null
connectionString.getOnlyConnectOriginalUrl() == null
}

@Unroll
Expand Down Expand Up @@ -845,4 +846,24 @@ class ConnectionStringSpecification extends Specification {
then:
connectionString.getRequiredReplicaSetName() == 'java'
}

def 'should parse onlyConnectOriginalUrl option correctly'() {
when:
def connectionString = new ConnectionString('mongodb://localhost:27017,localhost:27018/?onlyConnectOriginalUrl=true')

then:
connectionString.getOnlyConnectOriginalUrl() == true

when:
connectionString = new ConnectionString('mongodb://localhost:27017/?onlyConnectOriginalUrl=false')

then:
connectionString.getOnlyConnectOriginalUrl() == false

when:
connectionString = new ConnectionString('mongodb://localhost:27017/')

then:
connectionString.getOnlyConnectOriginalUrl() == null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class ClusterSettingsSpecification extends Specification {
settings.clusterListeners == []
settings.srvMaxHosts == null
settings.srvServiceName == 'mongodb'
settings.onlyConnectOriginalUrl == false
}

def 'should set all properties'() {
Expand Down Expand Up @@ -521,6 +522,53 @@ class ClusterSettingsSpecification extends Specification {
thrown(IllegalArgumentException)
}

def 'should support onlyConnectOriginalUrl via builder'() {
when:
def settings = ClusterSettings.builder()
.hosts([new ServerAddress('localhost:27017'), new ServerAddress('localhost:27018')])
.mode(ClusterConnectionMode.MULTIPLE)
.onlyConnectOriginalUrl(true)
.build()

then:
settings.onlyConnectOriginalUrl == true

when:
def copy = ClusterSettings.builder().applySettings(settings).build()

then:
copy.onlyConnectOriginalUrl == true
copy == settings
}

def 'should read onlyConnectOriginalUrl from connection string'() {
when:
def settings = ClusterSettings.builder()
.applyConnectionString(new ConnectionString(
'mongodb://localhost:27017,localhost:27018/?onlyConnectOriginalUrl=true'))
.build()

then:
settings.onlyConnectOriginalUrl == true

when:
settings = ClusterSettings.builder()
.applyConnectionString(new ConnectionString(
'mongodb://localhost:27017,localhost:27018/?onlyConnectOriginalUrl=false'))
.build()

then:
settings.onlyConnectOriginalUrl == false

when:
settings = ClusterSettings.builder()
.applyConnectionString(new ConnectionString('mongodb://localhost:27017,localhost:27018/'))
.build()

then:
settings.onlyConnectOriginalUrl == false
}

static class ServerAddressSubclass extends ServerAddress {
ServerAddressSubclass(final String host) {
super(host)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -523,4 +523,106 @@ class MultiServerClusterSpecification extends Specification {
def sendNotification(ServerAddress serverAddress, ServerType serverType) {
factory.sendNotification(serverAddress, serverType, [firstServer, secondServer, thirdServer])
}

// ---- onlyConnectOriginalUrl tests ----

def 'should not add new hosts when onlyConnectOriginalUrl is true and primary reports additional members'() {
given:
def cluster = new MultiServerCluster(CLUSTER_ID,
ClusterSettings.builder()
.mode(MULTIPLE)
.hosts([firstServer])
.onlyConnectOriginalUrl(true)
.build(),
factory, CLIENT_METADATA)

when:
// Primary reports firstServer, secondServer, thirdServer in its host list
factory.sendNotification(firstServer, REPLICA_SET_PRIMARY, [firstServer, secondServer, thirdServer])

then:
// Only firstServer should remain — no new hosts added
getAll(cluster.getCurrentDescription()) == factory.getDescriptions(firstServer)
}

def 'should not remove hosts when onlyConnectOriginalUrl is true and primary reports reduced member list'() {
given:
def cluster = new MultiServerCluster(CLUSTER_ID,
ClusterSettings.builder()
.mode(MULTIPLE)
.hosts([firstServer, secondServer, thirdServer])
.onlyConnectOriginalUrl(true)
.build(),
factory, CLIENT_METADATA)
factory.sendNotification(firstServer, REPLICA_SET_PRIMARY, [firstServer, secondServer, thirdServer])
factory.sendNotification(secondServer, REPLICA_SET_SECONDARY, [firstServer, secondServer, thirdServer])
factory.sendNotification(thirdServer, REPLICA_SET_SECONDARY, [firstServer, secondServer, thirdServer])

when:
// Primary now only reports firstServer and secondServer — thirdServer should NOT be removed
factory.sendNotification(firstServer, REPLICA_SET_PRIMARY, [firstServer, secondServer])

then:
getAll(cluster.getCurrentDescription()) == factory.getDescriptions(firstServer, secondServer, thirdServer)
!factory.getServer(thirdServer).isClosed()
}

def 'should still remove a server of wrong type when onlyConnectOriginalUrl is true'() {
given:
def cluster = new MultiServerCluster(CLUSTER_ID,
ClusterSettings.builder()
.requiredClusterType(REPLICA_SET)
.hosts([firstServer, secondServer])
.onlyConnectOriginalUrl(true)
.build(),
factory, CLIENT_METADATA)

when:
// secondServer is a shard router — must still be removed (wrong type check happens before the guard)
sendNotificationOnlyConnectOriginalUrl(secondServer, SHARD_ROUTER)

then:
cluster.getCurrentDescription().type == REPLICA_SET
getAll(cluster.getCurrentDescription()) == factory.getDescriptions(firstServer)
}

def 'should still reject a member from the wrong replica set when onlyConnectOriginalUrl is true'() {
given:
def cluster = new MultiServerCluster(CLUSTER_ID,
ClusterSettings.builder()
.mode(MULTIPLE)
.hosts([firstServer])
.requiredReplicaSetName('test1')
.onlyConnectOriginalUrl(true)
.build(),
factory, CLIENT_METADATA)

when:
factory.sendNotification(firstServer, REPLICA_SET_PRIMARY, [firstServer, secondServer, thirdServer], 'test2')

then:
// wrong setName — firstServer must be removed regardless of onlyConnectOriginalUrl
getAll(cluster.getCurrentDescription()) == [] as Set
}

def 'should add hosts normally when onlyConnectOriginalUrl is false'() {
given:
def cluster = new MultiServerCluster(CLUSTER_ID,
ClusterSettings.builder()
.mode(MULTIPLE)
.hosts([firstServer])
.onlyConnectOriginalUrl(false)
.build(),
factory, CLIENT_METADATA)

when:
factory.sendNotification(firstServer, REPLICA_SET_PRIMARY, [firstServer, secondServer, thirdServer])

then:
getAll(cluster.getCurrentDescription()) == factory.getDescriptions(firstServer, secondServer, thirdServer)
}

def sendNotificationOnlyConnectOriginalUrl(ServerAddress serverAddress, ServerType serverType) {
factory.sendNotification(serverAddress, serverType, [firstServer, secondServer])
}
}