From b8bb2de068bc90655698efe918f53a685d024bdd Mon Sep 17 00:00:00 2001 From: "taoxueying.txy" Date: Fri, 13 Mar 2026 11:15:06 +0800 Subject: [PATCH] Add onlyConnectOriginalUrl connection string option to skip replica set topology discovery --- .../main/com/mongodb/ConnectionString.java | 19 ++++ .../mongodb/connection/ClusterSettings.java | 36 ++++++- .../AbstractMultiServerCluster.java | 5 + .../ConnectionStringSpecification.groovy | 21 ++++ .../ClusterSettingsSpecification.groovy | 48 +++++++++ .../MultiServerClusterSpecification.groovy | 102 ++++++++++++++++++ 6 files changed, 230 insertions(+), 1 deletion(-) diff --git a/driver-core/src/main/com/mongodb/ConnectionString.java b/driver-core/src/main/com/mongodb/ConnectionString.java index 659e8fd02aa..b2919c930f9 100644 --- a/driver-core/src/main/com/mongodb/ConnectionString.java +++ b/driver-core/src/main/com/mongodb/ConnectionString.java @@ -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; @@ -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"); @@ -724,6 +727,9 @@ private void translateOptions(final Map> optionsMap) { case "srvservicename": srvServiceName = value; break; + case "onlyconnectoriginalurl": + onlyConnectOriginalUrl = parseBoolean(value, "onlyconnectoriginalurl"); + break; default: break; } @@ -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. * diff --git a/driver-core/src/main/com/mongodb/connection/ClusterSettings.java b/driver-core/src/main/com/mongodb/connection/ClusterSettings.java index 01e5c140441..93d3846771b 100644 --- a/driver-core/src/main/com/mongodb/connection/ClusterSettings.java +++ b/driver-core/src/main/com/mongodb/connection/ClusterSettings.java @@ -58,6 +58,7 @@ public final class ClusterSettings { private final long localThresholdMS; private final long serverSelectionTimeoutMS; private final List clusterListeners; + private final boolean onlyConnectOriginalUrl; /** * Get a builder for this class. @@ -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 clusterListeners = new ArrayList<>(); + private boolean onlyConnectOriginalUrl = false; private Builder() { } @@ -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; } @@ -314,6 +317,19 @@ public Builder clusterListenerList(final List 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 * @@ -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; } @@ -540,6 +560,17 @@ public List 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) { @@ -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) @@ -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 @@ -582,6 +614,7 @@ public String toString() { + ", clusterListeners='" + clusterListeners + '\'' + ", serverSelectionTimeout='" + serverSelectionTimeoutMS + " ms" + '\'' + ", localThreshold='" + localThresholdMS + " ms" + '\'' + + ", onlyConnectOriginalUrl=" + onlyConnectOriginalUrl + '}'; } @@ -659,5 +692,6 @@ private ClusterSettings(final Builder builder) { serverSelector = builder.serverSelector; serverSelectionTimeoutMS = builder.serverSelectionTimeoutMS; clusterListeners = unmodifiableList(builder.clusterListeners); + onlyConnectOriginalUrl = builder.onlyConnectOriginalUrl; } } diff --git a/driver-core/src/main/com/mongodb/internal/connection/AbstractMultiServerCluster.java b/driver-core/src/main/com/mongodb/internal/connection/AbstractMultiServerCluster.java index 11abddbb97a..3d6f38b270c 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/AbstractMultiServerCluster.java +++ b/driver-core/src/main/com/mongodb/internal/connection/AbstractMultiServerCluster.java @@ -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 diff --git a/driver-core/src/test/unit/com/mongodb/ConnectionStringSpecification.groovy b/driver-core/src/test/unit/com/mongodb/ConnectionStringSpecification.groovy index 72fdf108698..778ee642009 100644 --- a/driver-core/src/test/unit/com/mongodb/ConnectionStringSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/ConnectionStringSpecification.groovy @@ -536,6 +536,7 @@ class ConnectionStringSpecification extends Specification { connectionString.getCompressorList() == [] connectionString.getRetryWritesValue() == null connectionString.getRetryReads() == null + connectionString.getOnlyConnectOriginalUrl() == null } @Unroll @@ -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 + } } diff --git a/driver-core/src/test/unit/com/mongodb/connection/ClusterSettingsSpecification.groovy b/driver-core/src/test/unit/com/mongodb/connection/ClusterSettingsSpecification.groovy index 36da5c61e2d..b079408818d 100644 --- a/driver-core/src/test/unit/com/mongodb/connection/ClusterSettingsSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/connection/ClusterSettingsSpecification.groovy @@ -43,6 +43,7 @@ class ClusterSettingsSpecification extends Specification { settings.clusterListeners == [] settings.srvMaxHosts == null settings.srvServiceName == 'mongodb' + settings.onlyConnectOriginalUrl == false } def 'should set all properties'() { @@ -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) diff --git a/driver-core/src/test/unit/com/mongodb/internal/connection/MultiServerClusterSpecification.groovy b/driver-core/src/test/unit/com/mongodb/internal/connection/MultiServerClusterSpecification.groovy index a3cf8104fd3..b1113d8812d 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/connection/MultiServerClusterSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/internal/connection/MultiServerClusterSpecification.groovy @@ -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]) + } }