Skip to content

Commit 41fbf1b

Browse files
committed
fix(NODE-7229): remove duplicate server selection when auto-connecting
1 parent c58b2cd commit 41fbf1b

File tree

16 files changed

+120
-1172
lines changed

16 files changed

+120
-1172
lines changed

src/operations/execute_operation.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,6 @@ export async function executeOperation<
8282
session = client.startSession({ owner, explicit: false });
8383
} else if (session.hasEnded) {
8484
throw new MongoExpiredSessionError('Use of expired sessions is not permitted');
85-
} else if (
86-
session.snapshotEnabled &&
87-
maxWireVersion(topology) < MIN_SUPPORTED_SNAPSHOT_READS_WIRE_VERSION
88-
) {
89-
throw new MongoCompatibilityError('Snapshot reads require MongoDB 5.0 or later');
9085
} else if (session.client !== client) {
9186
throw new MongoInvalidArgumentError('ClientSession must be from the same MongoClient');
9287
}
@@ -210,6 +205,13 @@ async function tryOperation<T extends AbstractOperation, TResult = ResultTypeFro
210205
signal: operation.options.signal
211206
});
212207

208+
if (
209+
session?.snapshotEnabled &&
210+
maxWireVersion(topology) < MIN_SUPPORTED_SNAPSHOT_READS_WIRE_VERSION
211+
) {
212+
throw new MongoCompatibilityError('Snapshot reads require MongoDB 5.0 or later');
213+
}
214+
213215
const hasReadAspect = operation.hasAspect(Aspect.READ_OPERATION);
214216
const hasWriteAspect = operation.hasAspect(Aspect.WRITE_OPERATION);
215217
const inTransaction = session?.inTransaction() ?? false;

src/sdam/topology.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -459,20 +459,14 @@ export class Topology extends TypedEventEmitter<TopologyEvents> {
459459
};
460460

461461
try {
462-
const server = await this.selectServer(
463-
readPreferenceServerSelector(readPreference),
464-
selectServerOptions
465-
);
466-
467462
const skipPingOnConnect = this.s.options.__skipPingOnConnect === true;
468463
if (!skipPingOnConnect) {
464+
const server = await this.selectServer(
465+
readPreferenceServerSelector(readPreference),
466+
selectServerOptions
467+
);
469468
const connection = await server.pool.checkOut({ timeoutContext: timeoutContext });
470469
server.pool.checkIn(connection);
471-
stateTransition(this, STATE_CONNECTED);
472-
this.emit(Topology.OPEN, this);
473-
this.emit(Topology.CONNECT, this);
474-
475-
return this;
476470
}
477471

478472
stateTransition(this, STATE_CONNECTED);

test/integration/node-specific/abort_signal.test.ts

Lines changed: 25 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import {
2929
findLast,
3030
sleep
3131
} from '../../tools/utils';
32+
import { Topology } from '../../../lib/sdam/topology';
33+
import { MongoServerSelectionError } from '../../../lib/error';
3234

3335
const failPointMetadata = { requires: { mongodb: '>=4.4' } };
3436

@@ -611,35 +613,22 @@ describe('AbortSignal support', () => {
611613
let client: MongoClient;
612614
let db: Db;
613615
let collection: Collection<{ a: number; ssn: string }>;
614-
const logs: Log[] = [];
615616
let connectStarted;
616617
let controller: AbortController;
617618
let signal: AbortSignal;
618619
let cursor: AbstractCursor<{ a: number }>;
619620

620621
describe('when connect succeeds', () => {
621622
beforeEach(async function () {
622-
logs.length = 0;
623-
624623
const promise = promiseWithResolvers<void>();
625624
connectStarted = promise.promise;
626625

627-
client = this.configuration.newClient(
628-
{},
629-
{
630-
mongodbLogComponentSeverities: { serverSelection: 'debug' },
631-
mongodbLogPath: {
632-
write: log => {
633-
if (log.c === 'serverSelection' && log.operation === 'handshake') {
634-
controller.abort();
635-
promise.resolve();
636-
}
637-
logs.push(log);
638-
}
639-
},
640-
serverSelectionTimeoutMS: 1000
641-
}
642-
);
626+
client = this.configuration.newClient({}, { serverSelectionTimeoutMS: 1000 });
627+
628+
client.once('open', () => {
629+
controller.abort();
630+
promise.resolve();
631+
});
643632
db = client.db('abortSignal');
644633
collection = db.collection('support');
645634

@@ -650,7 +639,6 @@ describe('AbortSignal support', () => {
650639
});
651640

652641
afterEach(async function () {
653-
logs.length = 0;
654642
await client?.close();
655643
});
656644

@@ -666,22 +654,18 @@ describe('AbortSignal support', () => {
666654

667655
describe('when connect fails', () => {
668656
beforeEach(async function () {
669-
logs.length = 0;
670-
671657
const promise = promiseWithResolvers<void>();
672658
connectStarted = promise.promise;
673659

660+
const selectServerStub = sinon
661+
.stub(Topology.prototype, 'selectServer')
662+
.callsFake(async function (...args) {
663+
controller.abort();
664+
promise.resolve();
665+
return selectServerStub.wrappedMethod.call(this, ...args);
666+
});
667+
674668
client = this.configuration.newClient('mongodb://iLoveJavaScript', {
675-
mongodbLogComponentSeverities: { serverSelection: 'debug' },
676-
mongodbLogPath: {
677-
write: log => {
678-
if (log.c === 'serverSelection' && log.operation === 'handshake') {
679-
controller.abort();
680-
promise.resolve();
681-
}
682-
logs.push(log);
683-
}
684-
},
685669
serverSelectionTimeoutMS: 200,
686670
maxPoolSize: 1
687671
});
@@ -695,18 +679,23 @@ describe('AbortSignal support', () => {
695679
});
696680

697681
afterEach(async function () {
698-
logs.length = 0;
682+
sinon.restore();
699683
await client?.close();
700684
});
701685

702-
it('escapes auto connect without interrupting it', async () => {
686+
it('server selection error is thrown before reaching signal abort state check', async () => {
703687
const toArray = cursor.toArray().catch(error => error);
704688
await connectStarted;
705-
expect(await toArray).to.be.instanceOf(DOMException);
689+
const findError = await toArray;
690+
expect(findError).to.be.instanceOf(MongoServerSelectionError);
691+
if (process.platform !== 'win32') {
692+
// linux / mac, unix in general will have this errno set,
693+
// which is generally helpful if this is kept elevated in the error message
694+
expect(findError).to.match(/ENOTFOUND/);
695+
}
706696
await sleep(500);
707697
expect(client.topology).to.exist;
708698
expect(client.topology.description).to.have.property('type', 'Unknown');
709-
expect(findLast(logs, l => l.message.includes('Server selection failed'))).to.exist;
710699
});
711700
});
712701
});

test/integration/node-specific/auto_connect.test.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
type Collection,
1010
MongoClient,
1111
MongoNotConnectedError,
12+
MongoOperationTimeoutError,
1213
ProfilingLevel,
1314
TopologyType
1415
} from '../../../src';
@@ -862,12 +863,17 @@ describe('When executing an operation for the first time', () => {
862863
sinon.restore();
863864
});
864865

865-
it('client.connect() takes as long as selectServer is delayed for and does not throw a timeout error', async function () {
866+
it('client.connect() takes as long as selectServer is delayed for and throws a timeout error', async function () {
866867
const start = performance.now();
867868
expect(client.topology).to.not.exist; // make sure not connected.
868-
const res = await client.db().collection('test').insertOne({ a: 1 }, { timeoutMS: 500 }); // auto-connect
869+
const error = await client
870+
.db()
871+
.collection('test')
872+
.insertOne({ a: 1 }, { timeoutMS: 500 })
873+
.catch(error => error);
869874
const end = performance.now();
870-
expect(res).to.have.property('acknowledged', true);
875+
expect(error).to.be.instanceOf(MongoOperationTimeoutError);
876+
expect(error).to.match(/Timed out during server selection/);
871877
expect(end - start).to.be.within(1000, 1500); // timeoutMS is 1000, did not apply.
872878
});
873879
}

test/integration/node-specific/mongo_client.test.ts

Lines changed: 24 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -339,29 +339,6 @@ describe('class MongoClient', function () {
339339
});
340340
});
341341

342-
it('throws ENOTFOUND error when connecting to non-existent host with no auth and loadBalanced=true', async function () {
343-
const configuration = this.configuration;
344-
const client = configuration.newClient(
345-
'mongodb://iLoveJavaScript:27017/test?loadBalanced=true',
346-
{ serverSelectionTimeoutMS: 100 }
347-
);
348-
349-
const error = await client.connect().catch(error => error);
350-
expect(error).to.be.instanceOf(MongoNetworkError); // not server selection like other topologies
351-
expect(error.message).to.match(/ENOTFOUND/);
352-
});
353-
354-
it('throws an error when srv is not a real record', async function () {
355-
const client = this.configuration.newClient('mongodb+srv://iLoveJavaScript/test', {
356-
serverSelectionTimeoutMS: 100
357-
});
358-
359-
const error = await client.connect().catch(error => error);
360-
expect(error).to.be.instanceOf(Error);
361-
expect(error.message).to.match(/ENOTFOUND/);
362-
});
363-
});
364-
365342
it('Should correctly pass through appname', async function () {
366343
const configuration = this.configuration;
367344
const options = {
@@ -511,13 +488,29 @@ describe('class MongoClient', function () {
511488
await client.close();
512489
});
513490

514-
it('creates topology and checks out connection', async function () {
515-
const checkoutStarted = once(client, 'connectionCheckOutStarted');
516-
await client.connect();
517-
const checkout = await checkoutStarted;
518-
expect(checkout).to.exist;
519-
expect(client).to.have.property('topology').that.is.instanceOf(Topology);
520-
});
491+
it(
492+
'creates topology and checks out connection when auth is enabled',
493+
{ requires: { auth: 'enabled' } },
494+
async function () {
495+
const checkoutStarted = once(client, 'connectionCheckOutStarted');
496+
await client.connect();
497+
const checkout = await checkoutStarted;
498+
expect(checkout).to.exist;
499+
expect(client).to.have.property('topology').that.is.instanceOf(Topology);
500+
}
501+
);
502+
503+
it(
504+
'checks out connection to confirm connectivity even when authentication is disabled',
505+
{ requires: { auth: 'disabled' } },
506+
async function () {
507+
const checkoutStarted = once(client, 'connectionCheckOutStarted');
508+
await client.connect();
509+
const checkout = await checkoutStarted;
510+
expect(checkout).to.exist;
511+
expect(client).to.have.property('topology').that.is.instanceOf(Topology);
512+
}
513+
);
521514

522515
it(
523516
'permits operations to be run after connect is called',
@@ -606,6 +599,7 @@ describe('class MongoClient', function () {
606599
expect(result).to.be.instanceOf(MongoServerSelectionError);
607600
expect(client).to.be.instanceOf(MongoClient);
608601
expect(client).to.have.property('topology').that.is.instanceOf(Topology);
602+
await client.close();
609603
}
610604
);
611605
});

test/integration/server-selection/server_selection.spec.test.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,19 @@ import * as path from 'path';
33
import { loadSpecTests } from '../../spec';
44
import { runUnifiedSuite } from '../../tools/unified-spec-runner/runner';
55

6-
describe.skip('Server Selection Unified Tests (Spec)', function () {
6+
describe('Server Selection Unified Tests (Spec)', function () {
77
const tests = loadSpecTests(path.join('server-selection', 'logging'));
88
runUnifiedSuite(tests, test => {
99
if (
1010
[
1111
'Failed bulkWrite operation: log messages have operationIds',
12-
'Successful bulkWrite operation: log messages have operationIds'
12+
'Failed client bulkWrite operation: log messages have operationIds',
13+
'Successful bulkWrite operation: log messages have operationIds',
14+
'Successful client bulkWrite operation: log messages have operationIds'
1315
].includes(test.description)
1416
) {
1517
return 'not applicable: operationId not supported';
1618
}
1719
return false;
1820
});
19-
}).skipReason =
20-
'TODO: unskip these tests - NODE-2471 (ping on connect) and NODE-5774 (duplicate server selection for bulkWrite and other wrapper operations';
21+
});

test/integration/server-selection/server_selection.test.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)