From 10abf03fe5e77710aaa4ca488815d70d1fdddea6 Mon Sep 17 00:00:00 2001 From: bailey Date: Wed, 19 Nov 2025 11:02:55 -0700 Subject: [PATCH 01/12] rename isSDAMUnrecoverableError to match spec's name --- src/error.ts | 8 +------- src/sdam/server.ts | 12 ++++++++++-- test/unit/error.test.ts | 29 ++++++++--------------------- 3 files changed, 19 insertions(+), 30 deletions(-) diff --git a/src/error.ts b/src/error.ts index 9822361e72..eda39b7dc3 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1528,13 +1528,7 @@ export function isNodeShuttingDownError(err: MongoError): boolean { * * @see https://github.com/mongodb/specifications/blob/master/source/server-discovery-and-monitoring/server-discovery-and-monitoring.md#not-writable-primary-and-node-is-recovering */ -export function isSDAMUnrecoverableError(error: MongoError): boolean { - // NOTE: null check is here for a strictly pre-CMAP world, a timeout or - // close event are considered unrecoverable - if (error instanceof MongoParseError || error == null) { - return true; - } - +export function isStateChangeError(error: MongoError): boolean { return isRecoveringError(error) || isNotWritablePrimaryError(error); } diff --git a/src/sdam/server.ts b/src/sdam/server.ts index f14188a0d5..24a5373a7b 100644 --- a/src/sdam/server.ts +++ b/src/sdam/server.ts @@ -22,12 +22,13 @@ import { import { type AnyError, isNodeShuttingDownError, - isSDAMUnrecoverableError, + isStateChangeError, MONGODB_ERROR_CODES, MongoError, MongoErrorLabel, MongoNetworkError, MongoNetworkTimeoutError, + MongoParseError, MongoRuntimeError, MongoServerClosedError, type MongoServerError, @@ -412,7 +413,14 @@ export class Server extends TypedEventEmitter { this.pool.clear({ serviceId: connection.serviceId }); } } else { - if (isSDAMUnrecoverableError(error)) { + // TODO: considering parse errors as SDAM unrecoverable errors seem + // questionable. What if the parse error only comes from an application connection, + // indicating some bytes were lost in transmission? It seems overkill to completely + // kill the server. + // Parse errors from monitoring connections are already handled because the + // error would be wrapped in a ServerHeartbeatFailedEvent, which would mark the + // server unknown and clear the pool. Can we remove this? + if (isStateChangeError(error) || error instanceof MongoParseError) { if (shouldHandleStateChangeError(this, error)) { const shouldClearPool = isNodeShuttingDownError(error); if (this.loadBalanced && connection && shouldClearPool) { diff --git a/test/unit/error.test.ts b/test/unit/error.test.ts index 32e90a0785..c5124c60f9 100644 --- a/test/unit/error.test.ts +++ b/test/unit/error.test.ts @@ -10,7 +10,7 @@ import * as importsFromErrorSrc from '../../src/error'; import { isResumableError, isRetryableReadError, - isSDAMUnrecoverableError, + isStateChangeError, LEGACY_NOT_PRIMARY_OR_SECONDARY_ERROR_MESSAGE, LEGACY_NOT_WRITABLE_PRIMARY_ERROR_MESSAGE, MONGODB_ERROR_CODES, @@ -211,26 +211,13 @@ describe('MongoErrors', () => { }); }); - describe('#isSDAMUnrecoverableError', function () { - context('when the error is a MongoParseError', function () { - it('returns true', function () { - const error = new MongoParseError(''); - expect(isSDAMUnrecoverableError(error)).to.be.true; - }); - }); - - context('when the error is null', function () { - it('returns true', function () { - expect(isSDAMUnrecoverableError(null)).to.be.true; - }); - }); - + describe('#isStateChangeError', function () { context('when the error has a "node is recovering" error code', function () { it('returns true', function () { const error = new MongoError(''); // Code for NotPrimaryOrSecondary error.code = 13436; - expect(isSDAMUnrecoverableError(error)).to.be.true; + expect(isStateChangeError(error)).to.be.true; }); }); @@ -239,7 +226,7 @@ describe('MongoErrors', () => { const error = new MongoError(''); // Code for NotWritablePrimary error.code = 10107; - expect(isSDAMUnrecoverableError(error)).to.be.true; + expect(isStateChangeError(error)).to.be.true; }); }); @@ -250,7 +237,7 @@ describe('MongoErrors', () => { // If the response includes an error code, it MUST be solely used to determine if error is a "node is recovering" or "not writable primary" error. const error = new MongoError(NODE_IS_RECOVERING_ERROR_MESSAGE.source); error.code = 555; - expect(isSDAMUnrecoverableError(error)).to.be.false; + expect(isStateChangeError(error)).to.be.false; }); } ); @@ -262,7 +249,7 @@ describe('MongoErrors', () => { const error = new MongoError( `this is ${LEGACY_NOT_WRITABLE_PRIMARY_ERROR_MESSAGE.source}.` ); - expect(isSDAMUnrecoverableError(error)).to.be.true; + expect(isStateChangeError(error)).to.be.true; }); } ); @@ -272,7 +259,7 @@ describe('MongoErrors', () => { function () { it('returns true', function () { const error = new MongoError(`the ${NODE_IS_RECOVERING_ERROR_MESSAGE} from an error`); - expect(isSDAMUnrecoverableError(error)).to.be.true; + expect(isStateChangeError(error)).to.be.true; }); } ); @@ -284,7 +271,7 @@ describe('MongoErrors', () => { const error = new MongoError( `this is ${LEGACY_NOT_PRIMARY_OR_SECONDARY_ERROR_MESSAGE}, so we have a problem ` ); - expect(isSDAMUnrecoverableError(error)).to.be.true; + expect(isStateChangeError(error)).to.be.true; }); } ); From d8dd4a2b68ccaede7a503a462c40de4c8e841fc5 Mon Sep 17 00:00:00 2001 From: bailey Date: Wed, 19 Nov 2025 11:11:17 -0700 Subject: [PATCH 02/12] remove shouldHandleStateChangeError --- src/sdam/server.ts | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/sdam/server.ts b/src/sdam/server.ts index 24a5373a7b..e608054736 100644 --- a/src/sdam/server.ts +++ b/src/sdam/server.ts @@ -392,9 +392,7 @@ export class Server extends TypedEventEmitter { return; } - const isStaleError = - error.connectionGeneration && error.connectionGeneration < this.pool.generation; - if (isStaleError) { + if (isStaleError(this, error)) { return; } @@ -421,19 +419,17 @@ export class Server extends TypedEventEmitter { // error would be wrapped in a ServerHeartbeatFailedEvent, which would mark the // server unknown and clear the pool. Can we remove this? if (isStateChangeError(error) || error instanceof MongoParseError) { - if (shouldHandleStateChangeError(this, error)) { - const shouldClearPool = isNodeShuttingDownError(error); - if (this.loadBalanced && connection && shouldClearPool) { - this.pool.clear({ serviceId: connection.serviceId }); - } + const shouldClearPool = isNodeShuttingDownError(error); + if (this.loadBalanced && connection && shouldClearPool) { + this.pool.clear({ serviceId: connection.serviceId }); + } - if (!this.loadBalanced) { - if (shouldClearPool) { - error.addErrorLabel(MongoErrorLabel.ResetPool); - } - markServerUnknown(this, error); - process.nextTick(() => this.requestCheck()); + if (!this.loadBalanced) { + if (shouldClearPool) { + error.addErrorLabel(MongoErrorLabel.ResetPool); } + markServerUnknown(this, error); + process.nextTick(() => this.requestCheck()); } } } @@ -568,12 +564,6 @@ function connectionIsStale(pool: ConnectionPool, connection: Connection) { return connection.generation !== pool.generation; } -function shouldHandleStateChangeError(server: Server, err: MongoError) { - const etv = err.topologyVersion; - const stv = server.description.topologyVersion; - return compareTopologyVersion(stv, etv) < 0; -} - function inActiveTransaction(session: ClientSession | undefined, cmd: Document) { return session && session.inTransaction() && !isTransactionCommand(cmd); } @@ -583,3 +573,15 @@ function inActiveTransaction(session: ClientSession | undefined, cmd: Document) function isRetryableWritesEnabled(topology: Topology) { return topology.s.options.retryWrites !== false; } + +function isStaleError(server: Server, error: MongoError): boolean { + const currentGeneration = server.pool.generation; + const generation = error.connectionGeneration; + + if (generation && generation < currentGeneration) { + return true; + } + + const currentTopologyVersion = server.description.topologyVersion; + return compareTopologyVersion(currentTopologyVersion, error.topologyVersion) >= 0; +} From 3870f7d3e889e257598612f435fc1250e43e56d9 Mon Sep 17 00:00:00 2001 From: bailey Date: Wed, 19 Nov 2025 13:15:56 -0700 Subject: [PATCH 03/12] swap ordering of logic in server --- src/sdam/server.ts | 59 ++++++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/src/sdam/server.ts b/src/sdam/server.ts index e608054736..44a2727cc1 100644 --- a/src/sdam/server.ts +++ b/src/sdam/server.ts @@ -401,37 +401,46 @@ export class Server extends TypedEventEmitter { const isNetworkTimeoutBeforeHandshakeError = error instanceof MongoNetworkError && error.beforeHandshake; const isAuthHandshakeError = error.hasErrorLabel(MongoErrorLabel.HandshakeError); - if (isNetworkNonTimeoutError || isNetworkTimeoutBeforeHandshakeError || isAuthHandshakeError) { - // In load balanced mode we never mark the server as unknown and always - // clear for the specific service id. + + // TODO: considering parse errors as SDAM unrecoverable errors seem + // questionable. What if the parse error only comes from an application connection, + // indicating some bytes were lost in transmission? It seems overkill to completely + // kill the server. + // Parse errors from monitoring connections are already handled because the + // error would be wrapped in a ServerHeartbeatFailedEvent, which would mark the + // server unknown and clear the pool. Can we remove this? + if (isStateChangeError(error) || error instanceof MongoParseError) { + const shouldClearPool = isNodeShuttingDownError(error); + + // from the SDAM spec: The driver MUST synchronize clearing the pool with updating the topology. + // In load balanced mode: there is no monitoring, so there is no topology to update. We simply clear the pool. + // For other topologies: the `ResetPool` label instructs the topology to clear the server's pool in `updateServer()`. + if (!this.loadBalanced) { + if (shouldClearPool) { + error.addErrorLabel(MongoErrorLabel.ResetPool); + } + markServerUnknown(this, error); + process.nextTick(() => this.requestCheck()); + return; + } + + if (connection && shouldClearPool) { + this.pool.clear({ serviceId: connection.serviceId }); + } + } else if ( + isNetworkNonTimeoutError || + isNetworkTimeoutBeforeHandshakeError || + isAuthHandshakeError + ) { + // from the SDAM spec: The driver MUST synchronize clearing the pool with updating the topology. + // In load balanced mode: there is no monitoring, so there is no topology to update. We simply clear the pool. + // For other topologies: the `ResetPool` label instructs the topology to clear the server's pool in `updateServer()`. if (!this.loadBalanced) { error.addErrorLabel(MongoErrorLabel.ResetPool); markServerUnknown(this, error); } else if (connection) { this.pool.clear({ serviceId: connection.serviceId }); } - } else { - // TODO: considering parse errors as SDAM unrecoverable errors seem - // questionable. What if the parse error only comes from an application connection, - // indicating some bytes were lost in transmission? It seems overkill to completely - // kill the server. - // Parse errors from monitoring connections are already handled because the - // error would be wrapped in a ServerHeartbeatFailedEvent, which would mark the - // server unknown and clear the pool. Can we remove this? - if (isStateChangeError(error) || error instanceof MongoParseError) { - const shouldClearPool = isNodeShuttingDownError(error); - if (this.loadBalanced && connection && shouldClearPool) { - this.pool.clear({ serviceId: connection.serviceId }); - } - - if (!this.loadBalanced) { - if (shouldClearPool) { - error.addErrorLabel(MongoErrorLabel.ResetPool); - } - markServerUnknown(this, error); - process.nextTick(() => this.requestCheck()); - } - } } } From 9a388d780bf8837b333fc65cf8d6e1ae268cec0a Mon Sep 17 00:00:00 2001 From: bailey Date: Wed, 19 Nov 2025 13:32:07 -0700 Subject: [PATCH 04/12] changes! --- src/cmap/connect.ts | 30 ++++++++++++++++++++++++------ src/error.ts | 4 +++- src/sdam/server.ts | 9 +++++++-- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/cmap/connect.ts b/src/cmap/connect.ts index 62db2f98d4..aeeda7a573 100644 --- a/src/cmap/connect.ts +++ b/src/cmap/connect.ts @@ -35,6 +35,11 @@ import { /** @public */ export type Stream = Socket | TLSSocket; +function applyBackpressureLabels(error: MongoError) { + error.addErrorLabel(MongoErrorLabel.SystemOverloadedError); + error.addErrorLabel(MongoErrorLabel.RetryableError); +} + export async function connect(options: ConnectionOptions): Promise { let connection: Connection | null = null; try { @@ -103,6 +108,8 @@ export async function performInitialHandshake( const authContext = new AuthContext(conn, credentials, options); conn.authContext = authContext; + // If we encounter an error preparing the handshake document, do NOT apply backpressure labels. Errors + // encountered building the handshake document are all client-side, and do not indicate an overloaded server. const handshakeDoc = await prepareHandshakeDocument(authContext); // @ts-expect-error: TODO(NODE-5141): The options need to be filtered properly, Connection options differ from Command options @@ -162,13 +169,19 @@ export async function performInitialHandshake( try { await provider.auth(authContext); - } catch (error) { - if (error instanceof MongoError) { - error.addErrorLabel(MongoErrorLabel.HandshakeError); - if (needsRetryableWriteLabel(error, response.maxWireVersion, conn.description.type)) { - error.addErrorLabel(MongoErrorLabel.RetryableWriteError); - } + } catch (cause) { + // If we encounter an error authenticating a connection, do NOT apply backpressure labels. + + const error = + cause instanceof MongoError + ? cause + : new MongoRuntimeError('unexpected error during authentication', cause); + + error.addErrorLabel(MongoErrorLabel.HandshakeError); + if (needsRetryableWriteLabel(error, response.maxWireVersion, conn.description.type)) { + error.addErrorLabel(MongoErrorLabel.RetryableWriteError); } + throw error; } } @@ -189,6 +202,9 @@ export async function performInitialHandshake( if (error instanceof MongoError) { error.addErrorLabel(MongoErrorLabel.HandshakeError); } + // If we encounter an error executing the initial handshake, apply backpressure labels. + applyBackpressureLabels(error); + throw error; } } @@ -424,6 +440,8 @@ export async function makeSocket(options: MakeConnectionOptions): Promise { error instanceof MongoNetworkError && !(error instanceof MongoNetworkTimeoutError); const isNetworkTimeoutBeforeHandshakeError = error instanceof MongoNetworkError && error.beforeHandshake; - const isAuthHandshakeError = error.hasErrorLabel(MongoErrorLabel.HandshakeError); + const isAuthOrEstablishmentHandshakeError = error.hasErrorLabel(MongoErrorLabel.HandshakeError); + const isSystemOverloadError = error.hasErrorLabel(MongoErrorLabel.SystemOverloadedError); // TODO: considering parse errors as SDAM unrecoverable errors seem // questionable. What if the parse error only comes from an application connection, @@ -430,8 +431,12 @@ export class Server extends TypedEventEmitter { } else if ( isNetworkNonTimeoutError || isNetworkTimeoutBeforeHandshakeError || - isAuthHandshakeError + isAuthOrEstablishmentHandshakeError ) { + // Do NOT clear the pool if we encounter a system overloaded error. + if (isSystemOverloadError) { + return; + } // from the SDAM spec: The driver MUST synchronize clearing the pool with updating the topology. // In load balanced mode: there is no monitoring, so there is no topology to update. We simply clear the pool. // For other topologies: the `ResetPool` label instructs the topology to clear the server's pool in `updateServer()`. From d3b0637dfdb11f54b3f31896c5ab55dc7895969b Mon Sep 17 00:00:00 2001 From: bailey Date: Wed, 19 Nov 2025 13:36:39 -0700 Subject: [PATCH 05/12] sync new spec tests --- .../pool-create-min-size-error.json | 2 +- .../pool-create-min-size-error.yml | 2 +- .../load-balancers/sdam-error-handling.json | 8 +- .../load-balancers/sdam-error-handling.yml | 11 +- .../backpressure-network-error-fail.json | 171 +++++++++++++++++ .../backpressure-network-error-fail.yml | 97 ++++++++++ .../backpressure-network-timeout-fail.json | 174 ++++++++++++++++++ .../backpressure-network-timeout-fail.yml | 100 ++++++++++ 8 files changed, 553 insertions(+), 12 deletions(-) create mode 100644 test/spec/server-discovery-and-monitoring/unified/backpressure-network-error-fail.json create mode 100644 test/spec/server-discovery-and-monitoring/unified/backpressure-network-error-fail.yml create mode 100644 test/spec/server-discovery-and-monitoring/unified/backpressure-network-timeout-fail.json create mode 100644 test/spec/server-discovery-and-monitoring/unified/backpressure-network-timeout-fail.yml diff --git a/test/spec/connection-monitoring-and-pooling/cmap-format/pool-create-min-size-error.json b/test/spec/connection-monitoring-and-pooling/cmap-format/pool-create-min-size-error.json index 509b2a2356..fe7489f401 100644 --- a/test/spec/connection-monitoring-and-pooling/cmap-format/pool-create-min-size-error.json +++ b/test/spec/connection-monitoring-and-pooling/cmap-format/pool-create-min-size-error.json @@ -17,7 +17,7 @@ "isMaster", "hello" ], - "closeConnection": true, + "errorCode": 91, "appName": "poolCreateMinSizeErrorTest" } }, diff --git a/test/spec/connection-monitoring-and-pooling/cmap-format/pool-create-min-size-error.yml b/test/spec/connection-monitoring-and-pooling/cmap-format/pool-create-min-size-error.yml index f43c4ee154..42cf6e32a3 100644 --- a/test/spec/connection-monitoring-and-pooling/cmap-format/pool-create-min-size-error.yml +++ b/test/spec/connection-monitoring-and-pooling/cmap-format/pool-create-min-size-error.yml @@ -11,7 +11,7 @@ failPoint: mode: { times: 50 } data: failCommands: ["isMaster","hello"] - closeConnection: true + errorCode: 91 appName: "poolCreateMinSizeErrorTest" poolOptions: minPoolSize: 1 diff --git a/test/spec/load-balancers/sdam-error-handling.json b/test/spec/load-balancers/sdam-error-handling.json index 4ab34b1fed..2107afe5b3 100644 --- a/test/spec/load-balancers/sdam-error-handling.json +++ b/test/spec/load-balancers/sdam-error-handling.json @@ -1,6 +1,6 @@ { "description": "state change errors are correctly handled", - "schemaVersion": "1.3", + "schemaVersion": "1.4", "runOnRequirements": [ { "topologies": [ @@ -263,7 +263,7 @@ "description": "errors during the initial connection hello are ignored", "runOnRequirements": [ { - "minServerVersion": "4.9" + "minServerVersion": "4.4.7" } ], "operations": [ @@ -282,7 +282,7 @@ "isMaster", "hello" ], - "closeConnection": true, + "errorCode": 11600, "appName": "lbSDAMErrorTestClient" } } @@ -297,7 +297,7 @@ } }, "expectError": { - "isClientError": true + "isError": true } } ], diff --git a/test/spec/load-balancers/sdam-error-handling.yml b/test/spec/load-balancers/sdam-error-handling.yml index e3d6d6a251..c5a69339e3 100644 --- a/test/spec/load-balancers/sdam-error-handling.yml +++ b/test/spec/load-balancers/sdam-error-handling.yml @@ -1,6 +1,6 @@ description: state change errors are correctly handled -schemaVersion: '1.3' +schemaVersion: '1.4' runOnRequirements: - topologies: [ load-balanced ] @@ -141,9 +141,8 @@ tests: # to the same mongos on which the failpoint is set. - description: errors during the initial connection hello are ignored runOnRequirements: - # Server version 4.9+ is needed to set a fail point on the initial - # connection handshake with the appName filter due to SERVER-49336. - - minServerVersion: '4.9' + # Require SERVER-49336 for failCommand + appName on the initial handshake. + - minServerVersion: '4.4.7' operations: - name: failPoint object: testRunner @@ -154,14 +153,14 @@ tests: mode: { times: 1 } data: failCommands: [isMaster, hello] - closeConnection: true + errorCode: 11600 appName: *singleClientAppName - name: insertOne object: *singleColl arguments: document: { x: 1 } expectError: - isClientError: true + isError: true expectEvents: - client: *singleClient eventType: cmap diff --git a/test/spec/server-discovery-and-monitoring/unified/backpressure-network-error-fail.json b/test/spec/server-discovery-and-monitoring/unified/backpressure-network-error-fail.json new file mode 100644 index 0000000000..8a8e65be0f --- /dev/null +++ b/test/spec/server-discovery-and-monitoring/unified/backpressure-network-error-fail.json @@ -0,0 +1,171 @@ +{ + "description": "backpressure-network-error-fail", + "schemaVersion": "1.17", + "runOnRequirements": [ + { + "minServerVersion": "4.4", + "serverless": "forbid", + "topologies": [ + "single", + "replicaset", + "sharded" + ] + } + ], + "createEntities": [ + { + "client": { + "id": "setupClient", + "useMultipleMongoses": false + } + } + ], + "initialData": [ + { + "collectionName": "backpressure-network-error-fail", + "databaseName": "sdam-tests", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + } + ] + } + ], + "tests": [ + { + "description": "apply backpressure on network connection errors during connection establishment", + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "useMultipleMongoses": false, + "observeEvents": [ + "serverHeartbeatSucceededEvent", + "serverDescriptionChangedEvent", + "poolReadyEvent", + "poolClearedEvent" + ], + "uriOptions": { + "retryWrites": false, + "heartbeatFrequencyMS": 10000, + "serverMonitoringMode": "poll", + "directConnection": true, + "appname": "backpressureNetworkErrorFailTest" + } + } + }, + { + "database": { + "id": "database", + "client": "client", + "databaseName": "sdam-tests" + } + }, + { + "collection": { + "id": "collection", + "database": "database", + "collectionName": "backpressure-network-error-fail" + } + } + ] + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "serverHeartbeatSucceededEvent": {} + }, + "count": 1 + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "serverDescriptionChangedEvent": {} + }, + "count": 1 + } + }, + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "setupClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": "alwaysOn", + "data": { + "failCommands": [ + "isMaster", + "hello" + ], + "appName": "backpressureNetworkErrorFailTest", + "closeConnection": true + } + } + } + }, + { + "name": "insertMany", + "object": "collection", + "arguments": { + "documents": [ + { + "_id": 3 + }, + { + "_id": 4 + } + ] + }, + "expectError": { + "isError": true, + "errorLabelsContain": [ + "SystemOverloadedError", + "RetryableError" + ] + } + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "cmap", + "events": [ + { + "poolReadyEvent": {} + } + ] + }, + { + "client": "client", + "eventType": "sdam", + "ignoreExtraEvents": false, + "events": [ + { + "serverHeartbeatSucceededEvent": {} + }, + { + "serverDescriptionChangedEvent": {} + } + ] + } + ] + } + ] +} diff --git a/test/spec/server-discovery-and-monitoring/unified/backpressure-network-error-fail.yml b/test/spec/server-discovery-and-monitoring/unified/backpressure-network-error-fail.yml new file mode 100644 index 0000000000..d2029490bf --- /dev/null +++ b/test/spec/server-discovery-and-monitoring/unified/backpressure-network-error-fail.yml @@ -0,0 +1,97 @@ +description: backpressure-network-error-fail +schemaVersion: "1.17" +runOnRequirements: + - minServerVersion: "4.4" + serverless: forbid + topologies: + - single + - replicaset + - sharded +createEntities: + - client: + id: setupClient + useMultipleMongoses: false +initialData: + - collectionName: backpressure-network-error-fail + databaseName: sdam-tests + documents: + - _id: 1 + - _id: 2 +tests: + - description: apply backpressure on network connection errors during connection establishment + operations: + - name: createEntities + object: testRunner + arguments: + entities: + - client: + id: client + useMultipleMongoses: false + observeEvents: + - serverHeartbeatSucceededEvent + - serverDescriptionChangedEvent + - poolReadyEvent + - poolClearedEvent + uriOptions: + retryWrites: false + heartbeatFrequencyMS: 10000 + serverMonitoringMode: poll + directConnection: true + appname: backpressureNetworkErrorFailTest + - database: + id: database + client: client + databaseName: sdam-tests + - collection: + id: collection + database: database + collectionName: backpressure-network-error-fail + - name: waitForEvent + object: testRunner + arguments: + client: client + event: + serverHeartbeatSucceededEvent: {} + count: 1 + - name: waitForEvent + object: testRunner + arguments: + client: client + event: + serverDescriptionChangedEvent: {} + count: 1 + - name: failPoint + object: testRunner + arguments: + client: setupClient + failPoint: + configureFailPoint: failCommand + mode: alwaysOn + data: + failCommands: + - isMaster + - hello + appName: backpressureNetworkErrorFailTest + closeConnection: true + - name: insertMany + object: collection + arguments: + documents: + - _id: 3 + - _id: 4 + expectError: + isError: true + errorLabelsContain: + - SystemOverloadedError + - RetryableError + expectEvents: + - client: client + eventType: cmap + events: + - poolReadyEvent: {} + - client: client + eventType: sdam + ignoreExtraEvents: false + events: + - serverHeartbeatSucceededEvent: {} + - serverDescriptionChangedEvent: {} diff --git a/test/spec/server-discovery-and-monitoring/unified/backpressure-network-timeout-fail.json b/test/spec/server-discovery-and-monitoring/unified/backpressure-network-timeout-fail.json new file mode 100644 index 0000000000..1c85f265da --- /dev/null +++ b/test/spec/server-discovery-and-monitoring/unified/backpressure-network-timeout-fail.json @@ -0,0 +1,174 @@ +{ + "description": "backpressure-network-timeout-error", + "schemaVersion": "1.17", + "runOnRequirements": [ + { + "minServerVersion": "4.4", + "serverless": "forbid", + "topologies": [ + "single", + "replicaset", + "sharded" + ] + } + ], + "createEntities": [ + { + "client": { + "id": "setupClient", + "useMultipleMongoses": false + } + } + ], + "initialData": [ + { + "collectionName": "backpressure-network-timeout-error", + "databaseName": "sdam-tests", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + } + ] + } + ], + "tests": [ + { + "description": "apply backpressure on network timeout error during connection establishment", + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "useMultipleMongoses": false, + "observeEvents": [ + "serverHeartbeatSucceededEvent", + "serverDescriptionChangedEvent", + "poolReadyEvent", + "poolClearedEvent" + ], + "uriOptions": { + "retryWrites": false, + "heartbeatFrequencyMS": 10000, + "appname": "backpressureNetworkTimeoutErrorTest", + "serverMonitoringMode": "poll", + "directConnection": true, + "connectTimeoutMS": 250, + "socketTimeoutMS": 250 + } + } + }, + { + "database": { + "id": "database", + "client": "client", + "databaseName": "sdam-tests" + } + }, + { + "collection": { + "id": "collection", + "database": "database", + "collectionName": "backpressure-network-timeout-error" + } + } + ] + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "serverHeartbeatSucceededEvent": {} + }, + "count": 1 + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "serverDescriptionChangedEvent": {} + }, + "count": 1 + } + }, + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "setupClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": "alwaysOn", + "data": { + "failCommands": [ + "isMaster", + "hello" + ], + "blockConnection": true, + "blockTimeMS": 500, + "appName": "backpressureNetworkTimeoutErrorTest" + } + } + } + }, + { + "name": "insertMany", + "object": "collection", + "arguments": { + "documents": [ + { + "_id": 3 + }, + { + "_id": 4 + } + ] + }, + "expectError": { + "isError": true, + "errorLabelsContain": [ + "SystemOverloadedError", + "RetryableError" + ] + } + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "cmap", + "events": [ + { + "poolReadyEvent": {} + } + ] + }, + { + "client": "client", + "eventType": "sdam", + "ignoreExtraEvents": false, + "events": [ + { + "serverHeartbeatSucceededEvent": {} + }, + { + "serverDescriptionChangedEvent": {} + } + ] + } + ] + } + ] +} diff --git a/test/spec/server-discovery-and-monitoring/unified/backpressure-network-timeout-fail.yml b/test/spec/server-discovery-and-monitoring/unified/backpressure-network-timeout-fail.yml new file mode 100644 index 0000000000..c5ceb22b76 --- /dev/null +++ b/test/spec/server-discovery-and-monitoring/unified/backpressure-network-timeout-fail.yml @@ -0,0 +1,100 @@ +description: backpressure-network-timeout-error +schemaVersion: "1.17" +runOnRequirements: + - minServerVersion: "4.4" + serverless: forbid + topologies: + - single + - replicaset + - sharded +createEntities: + - client: + id: setupClient + useMultipleMongoses: false +initialData: + - collectionName: backpressure-network-timeout-error + databaseName: sdam-tests + documents: + - _id: 1 + - _id: 2 +tests: + - description: apply backpressure on network timeout error during connection establishment + operations: + - name: createEntities + object: testRunner + arguments: + entities: + - client: + id: client + useMultipleMongoses: false + observeEvents: + - serverHeartbeatSucceededEvent + - serverDescriptionChangedEvent + - poolReadyEvent + - poolClearedEvent + uriOptions: + retryWrites: false + heartbeatFrequencyMS: 10000 + appname: backpressureNetworkTimeoutErrorTest + serverMonitoringMode: poll + directConnection: true + connectTimeoutMS: 250 + socketTimeoutMS: 250 + - database: + id: database + client: client + databaseName: sdam-tests + - collection: + id: collection + database: database + collectionName: backpressure-network-timeout-error + - name: waitForEvent + object: testRunner + arguments: + client: client + event: + serverHeartbeatSucceededEvent: {} + count: 1 + - name: waitForEvent + object: testRunner + arguments: + client: client + event: + serverDescriptionChangedEvent: {} + count: 1 + - name: failPoint + object: testRunner + arguments: + client: setupClient + failPoint: + configureFailPoint: failCommand + mode: alwaysOn + data: + failCommands: + - isMaster + - hello + blockConnection: true + blockTimeMS: 500 + appName: backpressureNetworkTimeoutErrorTest + - name: insertMany + object: collection + arguments: + documents: + - _id: 3 + - _id: 4 + expectError: + isError: true + errorLabelsContain: + - SystemOverloadedError + - RetryableError + expectEvents: + - client: client + eventType: cmap + events: + - poolReadyEvent: {} + - client: client + eventType: sdam + ignoreExtraEvents: false + events: + - serverHeartbeatSucceededEvent: {} + - serverDescriptionChangedEvent: {} From 7f54387f1293575fc138db04e356b3ba3ccce7e9 Mon Sep 17 00:00:00 2001 From: bailey Date: Wed, 19 Nov 2025 14:14:04 -0700 Subject: [PATCH 06/12] skip tests that are not updated --- .../server_discovery_and_monitoring.spec.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/integration/server-discovery-and-monitoring/server_discovery_and_monitoring.spec.test.ts b/test/integration/server-discovery-and-monitoring/server_discovery_and_monitoring.spec.test.ts index a6b9249621..a8d584509d 100644 --- a/test/integration/server-discovery-and-monitoring/server_discovery_and_monitoring.spec.test.ts +++ b/test/integration/server-discovery-and-monitoring/server_discovery_and_monitoring.spec.test.ts @@ -15,6 +15,14 @@ const skipTable: { pattern: string; reason: string }[] = [ { pattern: 'connect with serverMonitoringMode=auto >=4.4', reason: 'TODO(NODE-6045): Ensure that first server hearbeat does not report that it is awaited' + }, + { + pattern: 'Pool is cleared before connection is closed (handshake error)', + reason: 'Steve has not updated this test yet.' + }, + { + pattern: 'Pool is cleared on handshake error during minPoolSize population', + reason: 'Steve has not updated this test yet.' } ]; From e14a3ef29d61dd85fc2cee32aae542ca34eaf06b Mon Sep 17 00:00:00 2001 From: bailey Date: Wed, 19 Nov 2025 14:16:52 -0700 Subject: [PATCH 07/12] fix kerberos tests --- src/cmap/connect.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/cmap/connect.ts b/src/cmap/connect.ts index aeeda7a573..e9fecb26bf 100644 --- a/src/cmap/connect.ts +++ b/src/cmap/connect.ts @@ -169,17 +169,14 @@ export async function performInitialHandshake( try { await provider.auth(authContext); - } catch (cause) { - // If we encounter an error authenticating a connection, do NOT apply backpressure labels. - - const error = - cause instanceof MongoError - ? cause - : new MongoRuntimeError('unexpected error during authentication', cause); + } catch (error) { + // NOTE: If we encounter an error authenticating a connection, do NOT apply backpressure labels. - error.addErrorLabel(MongoErrorLabel.HandshakeError); - if (needsRetryableWriteLabel(error, response.maxWireVersion, conn.description.type)) { - error.addErrorLabel(MongoErrorLabel.RetryableWriteError); + if (error instanceof MongoError) { + error.addErrorLabel(MongoErrorLabel.HandshakeError); + if (needsRetryableWriteLabel(error, response.maxWireVersion, conn.description.type)) { + error.addErrorLabel(MongoErrorLabel.RetryableWriteError); + } } throw error; From c363bea9650a3f16ff67fefbb65216d6cddcbcd6 Mon Sep 17 00:00:00 2001 From: bailey Date: Wed, 19 Nov 2025 14:59:39 -0700 Subject: [PATCH 08/12] lint --- test/unit/error.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/unit/error.test.ts b/test/unit/error.test.ts index c5124c60f9..34428b0666 100644 --- a/test/unit/error.test.ts +++ b/test/unit/error.test.ts @@ -26,7 +26,6 @@ import { MongoNetworkError, MongoNetworkTimeoutError, MongoOperationTimeoutError, - MongoParseError, MongoRuntimeError, MongoServerError, MongoSystemError, From 2aa99d25f45acb466eeb98a48a5f5e02a0044cf3 Mon Sep 17 00:00:00 2001 From: bailey Date: Wed, 19 Nov 2025 14:59:45 -0700 Subject: [PATCH 09/12] add prose test --- ...ver_discovery_and_monitoring.prose.test.ts | 85 ++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/test/integration/server-discovery-and-monitoring/server_discovery_and_monitoring.prose.test.ts b/test/integration/server-discovery-and-monitoring/server_discovery_and_monitoring.prose.test.ts index b0ed7adb07..c049febef5 100644 --- a/test/integration/server-discovery-and-monitoring/server_discovery_and_monitoring.prose.test.ts +++ b/test/integration/server-discovery-and-monitoring/server_discovery_and_monitoring.prose.test.ts @@ -1,7 +1,11 @@ import { expect } from 'chai'; import { once } from 'events'; -import { type MongoClient } from '../../../src'; +import { + type ConnectionCheckOutFailedEvent, + type ConnectionPoolClearedEvent, + type MongoClient +} from '../../../src'; import { CONNECTION_POOL_CLEARED, CONNECTION_POOL_READY, @@ -187,4 +191,83 @@ describe('Server Discovery and Monitoring Prose Tests', function () { } }); }); + + context('Connection Pool Backpressure', function () { + let client: MongoClient; + let utilClient: MongoClient; + const checkoutFailedEvents: Array = []; + const poolClearedEvents: Array = []; + + const metadata: MongoDBMetadataUI = { + requires: { + mongodb: '>=7.0' + } + }; + + beforeEach(async function () { + client = this.configuration.newClient({}, { maxConnecting: 20 }); + + client.on('connectionCheckOutFailed', e => checkoutFailedEvents.push(e)); + client.on('connectionPoolCleared', e => poolClearedEvents.push(e)); + + await client.connect(); + + utilClient = this.configuration.newClient(); + await utilClient.connect(); + + const admin = utilClient.db('admin').admin(); + await admin.command({ + setParameter: 1, + ingressConnectionEstablishmentRateLimiterEnabled: true + }); + await admin.command({ + setParameter: 1, + ingressConnectionEstablishmentRatePerSec: 20 + }); + await admin.command({ + setParameter: 1, + ingressConnectionEstablishmentBurstCapacitySecs: 1 + }); + await admin.command({ + setParameter: 1, + ingressConnectionEstablishmentMaxQueueDepth: 1 + }); + + await utilClient.db('test').collection('test').insertOne({}); + }); + + afterEach(async function () { + const admin = utilClient.db('admin').admin(); + await admin.command({ + setParameter: 1, + ingressConnectionEstablishmentRateLimiterEnabled: false + }); + + await utilClient.close(); + await client.close(); + }); + + it('runs', metadata, async function () { + await Promise.allSettled( + Array.from({ length: 10 }).map(() => + client + .db('test') + .collection('test') + .findOne({ $where: 'function() { sleep(2000); return true; }' }) + ) + ); + + await Promise.allSettled( + Array.from({ length: 100 }).map(() => + client + .db('test') + .collection('test') + .findOne({ $where: 'function() { sleep(2000); return true; }' }) + ) + ); + + expect(poolClearedEvents).to.be.empty; + expect(checkoutFailedEvents.length).to.be.greaterThan(10); + }); + }); }); From 4c51c2a61bb31915eba50a0fa739a048eef3f03f Mon Sep 17 00:00:00 2001 From: bailey Date: Thu, 20 Nov 2025 12:32:23 -0700 Subject: [PATCH 10/12] new tests? --- ...rver_discovery_and_monitoring.spec.test.ts | 8 +- .../backpressure-network-error-fail.json | 35 +---- .../backpressure-network-error-fail.yml | 21 +-- .../backpressure-network-timeout-fail.json | 35 +---- .../backpressure-network-timeout-fail.yml | 21 +-- ...ure-pool-no-clear-min-pool-size-error.json | 118 ++++++++++++++ ...sure-pool-no-clear-min-pool-size-error.yml | 68 ++++++++ .../unified/pool-clear-checkout-error.json | 148 ------------------ .../unified/pool-clear-checkout-error.yml | 90 ----------- .../pool-clear-min-pool-size-error.json | 116 -------------- .../pool-clear-min-pool-size-error.yml | 11 +- 11 files changed, 197 insertions(+), 474 deletions(-) create mode 100644 test/spec/server-discovery-and-monitoring/unified/backpressure-pool-no-clear-min-pool-size-error.json create mode 100644 test/spec/server-discovery-and-monitoring/unified/backpressure-pool-no-clear-min-pool-size-error.yml diff --git a/test/integration/server-discovery-and-monitoring/server_discovery_and_monitoring.spec.test.ts b/test/integration/server-discovery-and-monitoring/server_discovery_and_monitoring.spec.test.ts index a8d584509d..b741e72323 100644 --- a/test/integration/server-discovery-and-monitoring/server_discovery_and_monitoring.spec.test.ts +++ b/test/integration/server-discovery-and-monitoring/server_discovery_and_monitoring.spec.test.ts @@ -17,12 +17,8 @@ const skipTable: { pattern: string; reason: string }[] = [ reason: 'TODO(NODE-6045): Ensure that first server hearbeat does not report that it is awaited' }, { - pattern: 'Pool is cleared before connection is closed (handshake error)', - reason: 'Steve has not updated this test yet.' - }, - { - pattern: 'Pool is cleared on handshake error during minPoolSize population', - reason: 'Steve has not updated this test yet.' + pattern: 'Pool is not cleared on handshake error during minPoolSize ', + reason: 'broken' } ]; diff --git a/test/spec/server-discovery-and-monitoring/unified/backpressure-network-error-fail.json b/test/spec/server-discovery-and-monitoring/unified/backpressure-network-error-fail.json index 8a8e65be0f..f41b76459c 100644 --- a/test/spec/server-discovery-and-monitoring/unified/backpressure-network-error-fail.json +++ b/test/spec/server-discovery-and-monitoring/unified/backpressure-network-error-fail.json @@ -49,15 +49,12 @@ "useMultipleMongoses": false, "observeEvents": [ "serverHeartbeatSucceededEvent", - "serverDescriptionChangedEvent", - "poolReadyEvent", "poolClearedEvent" ], "uriOptions": { "retryWrites": false, - "heartbeatFrequencyMS": 10000, + "heartbeatFrequencyMS": 1000000, "serverMonitoringMode": "poll", - "directConnection": true, "appname": "backpressureNetworkErrorFailTest" } } @@ -90,17 +87,6 @@ "count": 1 } }, - { - "name": "waitForEvent", - "object": "testRunner", - "arguments": { - "client": "client", - "event": { - "serverDescriptionChangedEvent": {} - }, - "count": 1 - } - }, { "name": "failPoint", "object": "testRunner", @@ -146,24 +132,7 @@ { "client": "client", "eventType": "cmap", - "events": [ - { - "poolReadyEvent": {} - } - ] - }, - { - "client": "client", - "eventType": "sdam", - "ignoreExtraEvents": false, - "events": [ - { - "serverHeartbeatSucceededEvent": {} - }, - { - "serverDescriptionChangedEvent": {} - } - ] + "events": [] } ] } diff --git a/test/spec/server-discovery-and-monitoring/unified/backpressure-network-error-fail.yml b/test/spec/server-discovery-and-monitoring/unified/backpressure-network-error-fail.yml index d2029490bf..54e3030211 100644 --- a/test/spec/server-discovery-and-monitoring/unified/backpressure-network-error-fail.yml +++ b/test/spec/server-discovery-and-monitoring/unified/backpressure-network-error-fail.yml @@ -29,14 +29,11 @@ tests: useMultipleMongoses: false observeEvents: - serverHeartbeatSucceededEvent - - serverDescriptionChangedEvent - - poolReadyEvent - poolClearedEvent uriOptions: retryWrites: false - heartbeatFrequencyMS: 10000 + heartbeatFrequencyMS: 1000000 serverMonitoringMode: poll - directConnection: true appname: backpressureNetworkErrorFailTest - database: id: database @@ -53,13 +50,6 @@ tests: event: serverHeartbeatSucceededEvent: {} count: 1 - - name: waitForEvent - object: testRunner - arguments: - client: client - event: - serverDescriptionChangedEvent: {} - count: 1 - name: failPoint object: testRunner arguments: @@ -87,11 +77,4 @@ tests: expectEvents: - client: client eventType: cmap - events: - - poolReadyEvent: {} - - client: client - eventType: sdam - ignoreExtraEvents: false - events: - - serverHeartbeatSucceededEvent: {} - - serverDescriptionChangedEvent: {} + events: [] diff --git a/test/spec/server-discovery-and-monitoring/unified/backpressure-network-timeout-fail.json b/test/spec/server-discovery-and-monitoring/unified/backpressure-network-timeout-fail.json index 1c85f265da..a97c7a329f 100644 --- a/test/spec/server-discovery-and-monitoring/unified/backpressure-network-timeout-fail.json +++ b/test/spec/server-discovery-and-monitoring/unified/backpressure-network-timeout-fail.json @@ -48,17 +48,14 @@ "id": "client", "useMultipleMongoses": false, "observeEvents": [ - "serverHeartbeatSucceededEvent", "serverDescriptionChangedEvent", - "poolReadyEvent", "poolClearedEvent" ], "uriOptions": { "retryWrites": false, - "heartbeatFrequencyMS": 10000, + "heartbeatFrequencyMS": 1000000, "appname": "backpressureNetworkTimeoutErrorTest", "serverMonitoringMode": "poll", - "directConnection": true, "connectTimeoutMS": 250, "socketTimeoutMS": 250 } @@ -81,17 +78,6 @@ ] } }, - { - "name": "waitForEvent", - "object": "testRunner", - "arguments": { - "client": "client", - "event": { - "serverHeartbeatSucceededEvent": {} - }, - "count": 1 - } - }, { "name": "waitForEvent", "object": "testRunner", @@ -149,24 +135,7 @@ { "client": "client", "eventType": "cmap", - "events": [ - { - "poolReadyEvent": {} - } - ] - }, - { - "client": "client", - "eventType": "sdam", - "ignoreExtraEvents": false, - "events": [ - { - "serverHeartbeatSucceededEvent": {} - }, - { - "serverDescriptionChangedEvent": {} - } - ] + "events": [] } ] } diff --git a/test/spec/server-discovery-and-monitoring/unified/backpressure-network-timeout-fail.yml b/test/spec/server-discovery-and-monitoring/unified/backpressure-network-timeout-fail.yml index c5ceb22b76..6a61eba3ad 100644 --- a/test/spec/server-discovery-and-monitoring/unified/backpressure-network-timeout-fail.yml +++ b/test/spec/server-discovery-and-monitoring/unified/backpressure-network-timeout-fail.yml @@ -28,16 +28,13 @@ tests: id: client useMultipleMongoses: false observeEvents: - - serverHeartbeatSucceededEvent - serverDescriptionChangedEvent - - poolReadyEvent - poolClearedEvent uriOptions: retryWrites: false - heartbeatFrequencyMS: 10000 + heartbeatFrequencyMS: 1000000 appname: backpressureNetworkTimeoutErrorTest serverMonitoringMode: poll - directConnection: true connectTimeoutMS: 250 socketTimeoutMS: 250 - database: @@ -48,13 +45,6 @@ tests: id: collection database: database collectionName: backpressure-network-timeout-error - - name: waitForEvent - object: testRunner - arguments: - client: client - event: - serverHeartbeatSucceededEvent: {} - count: 1 - name: waitForEvent object: testRunner arguments: @@ -90,11 +80,4 @@ tests: expectEvents: - client: client eventType: cmap - events: - - poolReadyEvent: {} - - client: client - eventType: sdam - ignoreExtraEvents: false - events: - - serverHeartbeatSucceededEvent: {} - - serverDescriptionChangedEvent: {} + events: [] diff --git a/test/spec/server-discovery-and-monitoring/unified/backpressure-pool-no-clear-min-pool-size-error.json b/test/spec/server-discovery-and-monitoring/unified/backpressure-pool-no-clear-min-pool-size-error.json new file mode 100644 index 0000000000..77c8bc0f3c --- /dev/null +++ b/test/spec/server-discovery-and-monitoring/unified/backpressure-pool-no-clear-min-pool-size-error.json @@ -0,0 +1,118 @@ +{ + "description": "backpressure-pool-not-cleared-on-min-pool-size-population-error", + "schemaVersion": "1.4", + "runOnRequirements": [ + { + "minServerVersion": "4.4", + "serverless": "forbid", + "topologies": [ + "single" + ] + } + ], + "createEntities": [ + { + "client": { + "id": "setupClient", + "useMultipleMongoses": false + } + } + ], + "tests": [ + { + "description": "Pool is not cleared on handshake error during minPoolSize population", + "operations": [ + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "observeEvents": [ + "serverDescriptionChangedEvent", + "connectionCreatedEvent", + "poolClearedEvent", + "connectionClosedEvent", + "connectionReadyEvent" + ], + "uriOptions": { + "appname": "authErrorTest", + "minPoolSize": 5, + "maxConnecting": 1, + "serverMonitoringMode": "poll", + "heartbeatFrequencyMS": 1000000 + } + } + } + ] + } + }, + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "setupClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "skip": 1 + }, + "data": { + "failCommands": [ + "hello", + "isMaster" + ], + "appName": "authErrorTest", + "closeConnection": true + } + } + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "serverDescriptionChangedEvent": {} + }, + "count": 1 + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "connectionClosedEvent": {} + }, + "count": 1 + } + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "cmap", + "events": [ + { + "connectionCreatedEvent": {} + }, + { + "connectionReadyEvent": {} + }, + { + "connectionCreatedEvent": {} + }, + { + "connectionClosedEvent": {} + } + ] + } + ] + } + ] +} diff --git a/test/spec/server-discovery-and-monitoring/unified/backpressure-pool-no-clear-min-pool-size-error.yml b/test/spec/server-discovery-and-monitoring/unified/backpressure-pool-no-clear-min-pool-size-error.yml new file mode 100644 index 0000000000..1db48ccaaa --- /dev/null +++ b/test/spec/server-discovery-and-monitoring/unified/backpressure-pool-no-clear-min-pool-size-error.yml @@ -0,0 +1,68 @@ +description: backpressure-pool-not-cleared-on-min-pool-size-population-error +schemaVersion: "1.4" +runOnRequirements: + - minServerVersion: "4.4" + serverless: forbid + topologies: + - single +createEntities: + - client: + id: setupClient + useMultipleMongoses: false +tests: + - description: Pool is not cleared on handshake error during minPoolSize population + operations: + - name: createEntities + object: testRunner + arguments: + entities: + - client: + id: client + observeEvents: + - serverDescriptionChangedEvent + - connectionCreatedEvent + - poolClearedEvent + - connectionClosedEvent + - connectionReadyEvent + uriOptions: + appname: authErrorTest + minPoolSize: 5 + maxConnecting: 1 + serverMonitoringMode: poll + heartbeatFrequencyMS: 1000000 + - name: failPoint + object: testRunner + arguments: + client: setupClient + failPoint: + configureFailPoint: failCommand + mode: + skip: 1 + data: + failCommands: + - hello + - isMaster + appName: authErrorTest + closeConnection: true + - name: waitForEvent + object: testRunner + arguments: + client: client + event: + serverDescriptionChangedEvent: {} + count: 1 + - name: waitForEvent + object: testRunner + arguments: + client: client + event: + connectionClosedEvent: {} + count: 1 + expectEvents: + - client: client + eventType: cmap + events: + - connectionCreatedEvent: {} + - connectionReadyEvent: {} + - connectionCreatedEvent: {} + - connectionClosedEvent: {} diff --git a/test/spec/server-discovery-and-monitoring/unified/pool-clear-checkout-error.json b/test/spec/server-discovery-and-monitoring/unified/pool-clear-checkout-error.json index 126ee54533..7e6c7c8df4 100644 --- a/test/spec/server-discovery-and-monitoring/unified/pool-clear-checkout-error.json +++ b/test/spec/server-discovery-and-monitoring/unified/pool-clear-checkout-error.json @@ -143,154 +143,6 @@ ] } ] - }, - { - "description": "Pool is cleared before connection is closed (handshake error)", - "runOnRequirements": [ - { - "topologies": [ - "single" - ] - } - ], - "operations": [ - { - "name": "createEntities", - "object": "testRunner", - "arguments": { - "entities": [ - { - "client": { - "id": "client", - "useMultipleMongoses": false, - "observeEvents": [ - "connectionCheckOutStartedEvent", - "poolClearedEvent", - "connectionClosedEvent", - "topologyDescriptionChangedEvent" - ], - "uriOptions": { - "retryWrites": false, - "appname": "authErrorTest", - "minPoolSize": 0, - "serverMonitoringMode": "poll", - "heartbeatFrequencyMS": 1000000 - } - } - }, - { - "database": { - "id": "database", - "client": "client", - "databaseName": "foo" - } - }, - { - "collection": { - "id": "collection", - "database": "database", - "collectionName": "bar" - } - } - ] - } - }, - { - "name": "waitForEvent", - "object": "testRunner", - "arguments": { - "client": "client", - "event": { - "topologyDescriptionChangedEvent": { - "previousDescription": { - "type": "Unknown" - }, - "newDescription": { - "type": "Single" - } - } - }, - "count": 1 - } - }, - { - "name": "failPoint", - "object": "testRunner", - "arguments": { - "client": "setupClient", - "failPoint": { - "configureFailPoint": "failCommand", - "mode": { - "times": 1 - }, - "data": { - "failCommands": [ - "hello", - "isMaster" - ], - "appName": "authErrorTest", - "closeConnection": true - } - } - } - }, - { - "name": "insertMany", - "object": "collection", - "arguments": { - "documents": [ - { - "_id": 3 - }, - { - "_id": 4 - } - ] - }, - "expectError": { - "isError": true - } - }, - { - "name": "waitForEvent", - "object": "testRunner", - "arguments": { - "client": "client", - "event": { - "poolClearedEvent": {} - }, - "count": 1 - } - }, - { - "name": "waitForEvent", - "object": "testRunner", - "arguments": { - "client": "client", - "event": { - "connectionClosedEvent": {} - }, - "count": 1 - } - } - ], - "expectEvents": [ - { - "client": "client", - "eventType": "cmap", - "events": [ - { - "connectionCheckOutStartedEvent": {} - }, - { - "poolClearedEvent": {} - }, - { - "connectionClosedEvent": {} - } - ] - } - ] } ] } diff --git a/test/spec/server-discovery-and-monitoring/unified/pool-clear-checkout-error.yml b/test/spec/server-discovery-and-monitoring/unified/pool-clear-checkout-error.yml index 8df74b6a6f..5f7c48b521 100644 --- a/test/spec/server-discovery-and-monitoring/unified/pool-clear-checkout-error.yml +++ b/test/spec/server-discovery-and-monitoring/unified/pool-clear-checkout-error.yml @@ -84,93 +84,3 @@ tests: - connectionCheckOutStartedEvent: {} - poolClearedEvent: {} - connectionClosedEvent: {} - - - description: Pool is cleared before connection is closed (handshake error) - runOnRequirements: - - topologies: [ single ] - operations: - - name: createEntities - object: testRunner - arguments: - entities: - - client: - id: &client client - useMultipleMongoses: false - observeEvents: - - connectionCheckOutStartedEvent - - poolClearedEvent - - connectionClosedEvent - - topologyDescriptionChangedEvent - uriOptions: - retryWrites: false - appname: authErrorTest - minPoolSize: 0 - # ensure that once we've connected to the server, the failCommand won't - # be triggered by monitors and will only be triggered by handshakes - serverMonitoringMode: poll - heartbeatFrequencyMS: 1000000 - - database: - id: &database database - client: *client - databaseName: foo - - collection: - id: &collection collection - database: *database - collectionName: bar - - name: waitForEvent - object: testRunner - arguments: - client: *client - event: - topologyDescriptionChangedEvent: - previousDescription: - type: "Unknown" - newDescription: - type: "Single" - count: 1 - - - name: failPoint - object: testRunner - arguments: - client: *setupClient - failPoint: - configureFailPoint: failCommand - mode: - times: 1 - data: - failCommands: - - hello - - isMaster - appName: authErrorTest - closeConnection: true - - - name: insertMany - object: *collection - arguments: - documents: - - _id: 3 - - _id: 4 - expectError: - isError: true - - name: waitForEvent - object: testRunner - arguments: - client: *client - event: - poolClearedEvent: {} - count: 1 - - name: waitForEvent - object: testRunner - arguments: - client: *client - event: - connectionClosedEvent: {} - count: 1 - expectEvents: - - client: *client - eventType: cmap - events: - - connectionCheckOutStartedEvent: {} - - poolClearedEvent: {} - - connectionClosedEvent: {} - diff --git a/test/spec/server-discovery-and-monitoring/unified/pool-clear-min-pool-size-error.json b/test/spec/server-discovery-and-monitoring/unified/pool-clear-min-pool-size-error.json index 11c6be5bc1..e36dd7aa61 100644 --- a/test/spec/server-discovery-and-monitoring/unified/pool-clear-min-pool-size-error.json +++ b/test/spec/server-discovery-and-monitoring/unified/pool-clear-min-pool-size-error.json @@ -109,122 +109,6 @@ ] } ] - }, - { - "description": "Pool is cleared on handshake error during minPoolSize population", - "operations": [ - { - "name": "createEntities", - "object": "testRunner", - "arguments": { - "entities": [ - { - "client": { - "id": "client", - "observeEvents": [ - "topologyDescriptionChangedEvent", - "connectionCreatedEvent", - "poolClearedEvent", - "connectionClosedEvent", - "connectionReadyEvent" - ], - "uriOptions": { - "appname": "authErrorTest", - "minPoolSize": 5, - "maxConnecting": 1, - "serverMonitoringMode": "poll", - "heartbeatFrequencyMS": 1000000 - } - } - } - ] - } - }, - { - "name": "waitForEvent", - "object": "testRunner", - "arguments": { - "client": "client", - "event": { - "topologyDescriptionChangedEvent": { - "previousDescription": { - "type": "Unknown" - }, - "newDescription": { - "type": "Single" - } - } - }, - "count": 1 - } - }, - { - "name": "failPoint", - "object": "testRunner", - "arguments": { - "client": "setupClient", - "failPoint": { - "configureFailPoint": "failCommand", - "mode": { - "times": 1 - }, - "data": { - "failCommands": [ - "hello", - "isMaster" - ], - "appName": "authErrorTest", - "closeConnection": true - } - } - } - }, - { - "name": "waitForEvent", - "object": "testRunner", - "arguments": { - "client": "client", - "event": { - "poolClearedEvent": {} - }, - "count": 1 - } - }, - { - "name": "waitForEvent", - "object": "testRunner", - "arguments": { - "client": "client", - "event": { - "connectionClosedEvent": {} - }, - "count": 1 - } - } - ], - "expectEvents": [ - { - "client": "client", - "eventType": "cmap", - "events": [ - { - "connectionCreatedEvent": {} - }, - { - "connectionReadyEvent": {} - }, - { - "connectionCreatedEvent": {} - }, - { - "poolClearedEvent": {} - }, - { - "connectionClosedEvent": {} - } - ] - } - ] } ] } diff --git a/test/spec/server-discovery-and-monitoring/unified/pool-clear-min-pool-size-error.yml b/test/spec/server-discovery-and-monitoring/unified/pool-clear-min-pool-size-error.yml index 7e7ef0c590..8b58f672d3 100644 --- a/test/spec/server-discovery-and-monitoring/unified/pool-clear-min-pool-size-error.yml +++ b/test/spec/server-discovery-and-monitoring/unified/pool-clear-min-pool-size-error.yml @@ -68,7 +68,7 @@ tests: - poolClearedEvent: {} - connectionClosedEvent: {} - - description: Pool is cleared on handshake error during minPoolSize population + - description: Pool is not cleared on handshake error during minPoolSize population operations: - name: createEntities object: testRunner @@ -118,13 +118,6 @@ tests: appName: authErrorTest closeConnection: true - - name: waitForEvent - object: testRunner - arguments: - client: *client - event: - poolClearedEvent: {} - count: 1 - name: waitForEvent object: testRunner arguments: @@ -139,6 +132,4 @@ tests: - connectionCreatedEvent: {} - connectionReadyEvent: {} - connectionCreatedEvent: {} - - poolClearedEvent: {} - connectionClosedEvent: {} - From d6293467466de05b5635624ffa93ee21f81ce41e Mon Sep 17 00:00:00 2001 From: bailey Date: Thu, 20 Nov 2025 13:46:20 -0700 Subject: [PATCH 11/12] full set of tests --- ...ver_discovery_and_monitoring.prose.test.ts | 66 ++++++++----------- ...rver_discovery_and_monitoring.spec.test.ts | 4 -- ...ure-pool-no-clear-min-pool-size-error.json | 20 ++---- 3 files changed, 33 insertions(+), 57 deletions(-) diff --git a/test/integration/server-discovery-and-monitoring/server_discovery_and_monitoring.prose.test.ts b/test/integration/server-discovery-and-monitoring/server_discovery_and_monitoring.prose.test.ts index c049febef5..718e69d0fd 100644 --- a/test/integration/server-discovery-and-monitoring/server_discovery_and_monitoring.prose.test.ts +++ b/test/integration/server-discovery-and-monitoring/server_discovery_and_monitoring.prose.test.ts @@ -12,6 +12,7 @@ import { SERVER_HEARTBEAT_FAILED, SERVER_HEARTBEAT_SUCCEEDED } from '../../../src/constants'; +import { sleep } from '../../tools/utils'; describe('Server Discovery and Monitoring Prose Tests', function () { context('Monitors sleep at least minHeartbeatFrequencyMS between checks', function () { @@ -194,28 +195,18 @@ describe('Server Discovery and Monitoring Prose Tests', function () { context('Connection Pool Backpressure', function () { let client: MongoClient; - let utilClient: MongoClient; const checkoutFailedEvents: Array = []; const poolClearedEvents: Array = []; - const metadata: MongoDBMetadataUI = { - requires: { - mongodb: '>=7.0' - } - }; - beforeEach(async function () { - client = this.configuration.newClient({}, { maxConnecting: 20 }); + client = this.configuration.newClient({}, { maxConnecting: 100 }); client.on('connectionCheckOutFailed', e => checkoutFailedEvents.push(e)); client.on('connectionPoolCleared', e => poolClearedEvents.push(e)); await client.connect(); - utilClient = this.configuration.newClient(); - await utilClient.connect(); - - const admin = utilClient.db('admin').admin(); + const admin = client.db('admin').admin(); await admin.command({ setParameter: 1, ingressConnectionEstablishmentRateLimiterEnabled: true @@ -233,41 +224,42 @@ describe('Server Discovery and Monitoring Prose Tests', function () { ingressConnectionEstablishmentMaxQueueDepth: 1 }); - await utilClient.db('test').collection('test').insertOne({}); + await client.db('test').collection('test').insertOne({}); }); afterEach(async function () { - const admin = utilClient.db('admin').admin(); + // give the time to recover from the connection storm before cleaning up. + await sleep(1000); + + const admin = client.db('admin').admin(); await admin.command({ setParameter: 1, ingressConnectionEstablishmentRateLimiterEnabled: false }); - await utilClient.close(); await client.close(); }); - it('runs', metadata, async function () { - await Promise.allSettled( - Array.from({ length: 10 }).map(() => - client - .db('test') - .collection('test') - .findOne({ $where: 'function() { sleep(2000); return true; }' }) - ) - ); - - await Promise.allSettled( - Array.from({ length: 100 }).map(() => - client - .db('test') - .collection('test') - .findOne({ $where: 'function() { sleep(2000); return true; }' }) - ) - ); - - expect(poolClearedEvents).to.be.empty; - expect(checkoutFailedEvents.length).to.be.greaterThan(10); - }); + it( + 'does not clear the pool when connections are closed due to connection storms', + { + requires: { + mongodb: '>=7.0' // rate limiting added in 7.0 + } + }, + async function () { + await Promise.allSettled( + Array.from({ length: 100 }).map(() => + client + .db('test') + .collection('test') + .findOne({ $where: 'function() { sleep(2000); return true; }' }) + ) + ); + + expect(poolClearedEvents).to.be.empty; + expect(checkoutFailedEvents.length).to.be.greaterThan(10); + } + ); }); }); diff --git a/test/integration/server-discovery-and-monitoring/server_discovery_and_monitoring.spec.test.ts b/test/integration/server-discovery-and-monitoring/server_discovery_and_monitoring.spec.test.ts index b741e72323..a6b9249621 100644 --- a/test/integration/server-discovery-and-monitoring/server_discovery_and_monitoring.spec.test.ts +++ b/test/integration/server-discovery-and-monitoring/server_discovery_and_monitoring.spec.test.ts @@ -15,10 +15,6 @@ const skipTable: { pattern: string; reason: string }[] = [ { pattern: 'connect with serverMonitoringMode=auto >=4.4', reason: 'TODO(NODE-6045): Ensure that first server hearbeat does not report that it is awaited' - }, - { - pattern: 'Pool is not cleared on handshake error during minPoolSize ', - reason: 'broken' } ]; diff --git a/test/spec/server-discovery-and-monitoring/unified/backpressure-pool-no-clear-min-pool-size-error.json b/test/spec/server-discovery-and-monitoring/unified/backpressure-pool-no-clear-min-pool-size-error.json index 77c8bc0f3c..c1156b3ec2 100644 --- a/test/spec/server-discovery-and-monitoring/unified/backpressure-pool-no-clear-min-pool-size-error.json +++ b/test/spec/server-discovery-and-monitoring/unified/backpressure-pool-no-clear-min-pool-size-error.json @@ -32,10 +32,7 @@ "id": "client", "observeEvents": [ "serverDescriptionChangedEvent", - "connectionCreatedEvent", - "poolClearedEvent", - "connectionClosedEvent", - "connectionReadyEvent" + "connectionClosedEvent" ], "uriOptions": { "appname": "authErrorTest", @@ -96,23 +93,14 @@ "expectEvents": [ { "client": "client", - "eventType": "cmap", + "eventType": "sdam", "events": [ { - "connectionCreatedEvent": {} - }, - { - "connectionReadyEvent": {} - }, - { - "connectionCreatedEvent": {} - }, - { - "connectionClosedEvent": {} + "serverDescriptionChangedEvent": {} } ] } ] } ] -} +} \ No newline at end of file From cc2215de17e9955e85d7733e79fbfe6ba637e66f Mon Sep 17 00:00:00 2001 From: bailey Date: Thu, 20 Nov 2025 14:32:43 -0700 Subject: [PATCH 12/12] up-to-date --- ...anged-on-min-pool-size-population-error.json} | 8 ++++---- ...hanged-on-min-pool-size-population-error.yml} | 16 +++++----------- 2 files changed, 9 insertions(+), 15 deletions(-) rename test/spec/server-discovery-and-monitoring/unified/{backpressure-pool-no-clear-min-pool-size-error.json => backpressure-server-description-unchanged-on-min-pool-size-population-error.json} (90%) rename test/spec/server-discovery-and-monitoring/unified/{backpressure-pool-no-clear-min-pool-size-error.yml => backpressure-server-description-unchanged-on-min-pool-size-population-error.yml} (75%) diff --git a/test/spec/server-discovery-and-monitoring/unified/backpressure-pool-no-clear-min-pool-size-error.json b/test/spec/server-discovery-and-monitoring/unified/backpressure-server-description-unchanged-on-min-pool-size-population-error.json similarity index 90% rename from test/spec/server-discovery-and-monitoring/unified/backpressure-pool-no-clear-min-pool-size-error.json rename to test/spec/server-discovery-and-monitoring/unified/backpressure-server-description-unchanged-on-min-pool-size-population-error.json index c1156b3ec2..35a49c1323 100644 --- a/test/spec/server-discovery-and-monitoring/unified/backpressure-pool-no-clear-min-pool-size-error.json +++ b/test/spec/server-discovery-and-monitoring/unified/backpressure-server-description-unchanged-on-min-pool-size-population-error.json @@ -1,6 +1,6 @@ { - "description": "backpressure-pool-not-cleared-on-min-pool-size-population-error", - "schemaVersion": "1.4", + "description": "backpressure-server-description-unchanged-on-min-pool-size-population-error", + "schemaVersion": "1.17", "runOnRequirements": [ { "minServerVersion": "4.4", @@ -20,7 +20,7 @@ ], "tests": [ { - "description": "Pool is not cleared on handshake error during minPoolSize population", + "description": "the server description is not changed on handshake error during minPoolSize population", "operations": [ { "name": "createEntities", @@ -103,4 +103,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/test/spec/server-discovery-and-monitoring/unified/backpressure-pool-no-clear-min-pool-size-error.yml b/test/spec/server-discovery-and-monitoring/unified/backpressure-server-description-unchanged-on-min-pool-size-population-error.yml similarity index 75% rename from test/spec/server-discovery-and-monitoring/unified/backpressure-pool-no-clear-min-pool-size-error.yml rename to test/spec/server-discovery-and-monitoring/unified/backpressure-server-description-unchanged-on-min-pool-size-population-error.yml index 1db48ccaaa..dd5029097d 100644 --- a/test/spec/server-discovery-and-monitoring/unified/backpressure-pool-no-clear-min-pool-size-error.yml +++ b/test/spec/server-discovery-and-monitoring/unified/backpressure-server-description-unchanged-on-min-pool-size-population-error.yml @@ -1,5 +1,5 @@ -description: backpressure-pool-not-cleared-on-min-pool-size-population-error -schemaVersion: "1.4" +description: backpressure-server-description-unchanged-on-min-pool-size-population-error +schemaVersion: "1.17" runOnRequirements: - minServerVersion: "4.4" serverless: forbid @@ -10,7 +10,7 @@ createEntities: id: setupClient useMultipleMongoses: false tests: - - description: Pool is not cleared on handshake error during minPoolSize population + - description: the server description is not changed on handshake error during minPoolSize population operations: - name: createEntities object: testRunner @@ -20,10 +20,7 @@ tests: id: client observeEvents: - serverDescriptionChangedEvent - - connectionCreatedEvent - - poolClearedEvent - connectionClosedEvent - - connectionReadyEvent uriOptions: appname: authErrorTest minPoolSize: 5 @@ -60,9 +57,6 @@ tests: count: 1 expectEvents: - client: client - eventType: cmap + eventType: sdam events: - - connectionCreatedEvent: {} - - connectionReadyEvent: {} - - connectionCreatedEvent: {} - - connectionClosedEvent: {} + - serverDescriptionChangedEvent: {}