diff --git a/src/cmap/commands.ts b/src/cmap/commands.ts index 165e33a2f0..57a605cf24 100644 --- a/src/cmap/commands.ts +++ b/src/cmap/commands.ts @@ -1,7 +1,7 @@ import type { BSONSerializeOptions, Document, Long } from '../bson'; import * as BSON from '../bson'; import { MongoInvalidArgumentError, MongoRuntimeError } from '../error'; -import { ReadPreference } from '../read_preference'; +import { type ReadPreference } from '../read_preference'; import type { ClientSession } from '../sessions'; import type { CommandOptions } from './connection'; import { @@ -51,7 +51,6 @@ export interface OpQueryOptions extends CommandOptions { requestId?: number; moreToCome?: boolean; exhaustAllowed?: boolean; - readPreference?: ReadPreference; } /************************************************************** @@ -77,7 +76,6 @@ export class OpQueryRequest { awaitData: boolean; exhaust: boolean; partial: boolean; - documentsReturnedIn?: string; constructor(public databaseName: string, public query: Document, options: OpQueryOptions) { // Basic options needed to be passed in @@ -503,10 +501,6 @@ export class OpMsgRequest { // Basic options this.command.$db = databaseName; - if (options.readPreference && options.readPreference.mode !== ReadPreference.PRIMARY) { - this.command.$readPreference = options.readPreference.toJSON(); - } - // Ensure empty options this.options = options ?? {}; diff --git a/src/cmap/connection.ts b/src/cmap/connection.ts index e4036bb187..7ea71727e2 100644 --- a/src/cmap/connection.ts +++ b/src/cmap/connection.ts @@ -26,7 +26,8 @@ import type { ServerApi, SupportedNodeConnectionOptions } from '../mongo_client' import { type MongoClientAuthProviders } from '../mongo_client_auth_providers'; import { MongoLoggableComponent, type MongoLogger, SeverityLevel } from '../mongo_logger'; import { type CancellationToken, TypedEventEmitter } from '../mongo_types'; -import type { ReadPreferenceLike } from '../read_preference'; +import { ReadPreference, type ReadPreferenceLike } from '../read_preference'; +import { ServerType } from '../sdam/common'; import { applySession, type ClientSession, updateSessionFromResponse } from '../sessions'; import { BufferPool, @@ -83,6 +84,8 @@ export interface CommandOptions extends BSONSerializeOptions { willRetryWrite?: boolean; writeConcern?: WriteConcern; + + directConnection?: boolean; } /** @public */ @@ -371,16 +374,34 @@ export class Connection extends TypedEventEmitter { cmd.$clusterTime = clusterTime; } - if ( - isSharded(this) && - !this.supportsOpMsg && - readPreference && - readPreference.mode !== 'primary' - ) { - cmd = { - $query: cmd, - $readPreference: readPreference.toJSON() - }; + // For standalone, drivers MUST NOT set $readPreference. + if (this.description.type !== ServerType.Standalone) { + if ( + !isSharded(this) && + !this.description.loadBalanced && + this.supportsOpMsg && + options.directConnection === true && + readPreference?.mode === 'primary' + ) { + // For mongos and load balancers with 'primary' mode, drivers MUST NOT set $readPreference. + // For all other types with a direct connection, if the read preference is 'primary' + // (driver sets 'primary' as default if no read preference is configured), + // the $readPreference MUST be set to 'primaryPreferred' + // to ensure that any server type can handle the request. + cmd.$readPreference = ReadPreference.primaryPreferred.toJSON(); + } else if (isSharded(this) && !this.supportsOpMsg && readPreference?.mode !== 'primary') { + // When sending a read operation via OP_QUERY and the $readPreference modifier, + // the query MUST be provided using the $query modifier. + cmd = { + $query: cmd, + $readPreference: readPreference.toJSON() + }; + } else if (readPreference?.mode !== 'primary') { + // For mode 'primary', drivers MUST NOT set $readPreference. + // For all other read preference modes (i.e. 'secondary', 'primaryPreferred', ...), + // drivers MUST set $readPreference + cmd.$readPreference = readPreference.toJSON(); + } } const commandOptions = { @@ -389,8 +410,7 @@ export class Connection extends TypedEventEmitter { checkKeys: false, // This value is not overridable secondaryOk: readPreference.secondaryOk(), - ...options, - readPreference // ensure we pass in ReadPreference instance + ...options }; const message = this.supportsOpMsg diff --git a/src/cmap/stream_description.ts b/src/cmap/stream_description.ts index 85decc2a09..88411c92c4 100644 --- a/src/cmap/stream_description.ts +++ b/src/cmap/stream_description.ts @@ -22,7 +22,7 @@ export interface StreamDescriptionOptions { /** @public */ export class StreamDescription { address: string; - type: string; + type: ServerType; minWireVersion?: number; maxWireVersion?: number; maxBsonObjectSize: number; diff --git a/src/cmap/wire_protocol/shared.ts b/src/cmap/wire_protocol/shared.ts index 53375d8ec5..98e0149001 100644 --- a/src/cmap/wire_protocol/shared.ts +++ b/src/cmap/wire_protocol/shared.ts @@ -13,12 +13,8 @@ export interface ReadPreferenceOption { } export function getReadPreference(options?: ReadPreferenceOption): ReadPreference { - // Default to command version of the readPreference + // Default to command version of the readPreference. let readPreference = options?.readPreference ?? ReadPreference.primary; - // If we have an option readPreference override the command one - if (options?.readPreference) { - readPreference = options.readPreference; - } if (typeof readPreference === 'string') { readPreference = ReadPreference.fromString(readPreference); @@ -43,7 +39,7 @@ export function isSharded(topologyOrServer?: Topology | Server | Connection): bo } // NOTE: This is incredibly inefficient, and should be removed once command construction - // happens based on `Server` not `Topology`. + // happens based on `Server` not `Topology`. if (topologyOrServer.description && topologyOrServer.description instanceof TopologyDescription) { const servers: ServerDescription[] = Array.from(topologyOrServer.description.servers.values()); return servers.some((server: ServerDescription) => server.type === ServerType.Mongos); diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 57cff6753f..5c1d6e9cf4 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -821,6 +821,7 @@ export interface MongoOptions readPreference: ReadPreference; readConcern: ReadConcern; loadBalanced: boolean; + directConnection: boolean; serverApi: ServerApi; compressors: CompressorName[]; writeConcern: WriteConcern; diff --git a/src/sdam/server.ts b/src/sdam/server.ts index f1a0bf1d98..01b8bb3219 100644 --- a/src/sdam/server.ts +++ b/src/sdam/server.ts @@ -290,7 +290,10 @@ export class Server extends TypedEventEmitter { } // Clone the options - const finalOptions = Object.assign({}, options, { wireProtocolCommand: false }); + const finalOptions = Object.assign({}, options, { + wireProtocolCommand: false, + directConnection: this.topology.s.options.directConnection + }); // There are cases where we need to flag the read preference not to get sent in // the command, such as pre-5.0 servers attempting to perform an aggregate write diff --git a/test/integration/max-staleness/max_staleness.test.js b/test/integration/max-staleness/max_staleness.test.js index d4dbf41368..b93b343fee 100644 --- a/test/integration/max-staleness/max_staleness.test.js +++ b/test/integration/max-staleness/max_staleness.test.js @@ -18,7 +18,7 @@ describe('Max Staleness', function () { // Primary server states const serverIsPrimary = [Object.assign({}, defaultFields)]; server.setMessageHandler(request => { - var doc = request.document; + const doc = request.document; if (isHello(doc)) { request.reply(serverIsPrimary[0]); return; @@ -46,34 +46,25 @@ describe('Max Staleness', function () { metadata: { requires: { generators: true, - topology: 'single' + topology: 'replicaset' } }, - test: function (done) { - var self = this; + test: async function () { + const self = this; const configuration = this.configuration; const client = configuration.newClient( `mongodb://${test.server.uri()}/test?readPreference=secondary&maxStalenessSeconds=250`, { serverApi: null } // TODO(NODE-3807): remove resetting serverApi when the usage of mongodb mock server is removed ); - client.connect(function (err, client) { - expect(err).to.not.exist; - var db = client.db(self.configuration.db); - - db.collection('test') - .find({}) - .toArray(function (err) { - expect(err).to.not.exist; - expect(test.checkCommand).to.containSubset({ - $query: { find: 'test', filter: {} }, - $readPreference: { mode: 'secondary', maxStalenessSeconds: 250 } - }); - - client.close(done); - }); + await client.connect(); + const db = client.db(self.configuration.db); + await db.collection('test').find({}).toArray(); + expect(test.checkCommand).to.containSubset({ + $readPreference: { mode: 'secondary', maxStalenessSeconds: 250 } }); + await client.close(); } }); @@ -81,36 +72,27 @@ describe('Max Staleness', function () { metadata: { requires: { generators: true, - topology: 'single' + topology: 'replicaset' } }, - test: function (done) { + test: async function () { const configuration = this.configuration; const client = configuration.newClient(`mongodb://${test.server.uri()}/test`, { serverApi: null // TODO(NODE-3807): remove resetting serverApi when the usage of mongodb mock server is removed }); - client.connect(function (err, client) { - expect(err).to.not.exist; - // Get a db with a new readPreference - var db1 = client.db('test', { - readPreference: new ReadPreference('secondary', null, { maxStalenessSeconds: 250 }) - }); + await client.connect(); - db1 - .collection('test') - .find({}) - .toArray(function (err) { - expect(err).to.not.exist; - expect(test.checkCommand).to.containSubset({ - $query: { find: 'test', filter: {} }, - $readPreference: { mode: 'secondary', maxStalenessSeconds: 250 } - }); - - client.close(done); - }); + // Get a db with a new readPreference + const db1 = client.db('test', { + readPreference: new ReadPreference('secondary', null, { maxStalenessSeconds: 250 }) + }); + await db1.collection('test').find({}).toArray(); + expect(test.checkCommand).to.containSubset({ + $readPreference: { mode: 'secondary', maxStalenessSeconds: 250 } }); + await client.close(); } }); @@ -120,35 +102,31 @@ describe('Max Staleness', function () { metadata: { requires: { generators: true, - topology: 'single' + topology: 'replicaset' } }, - test: function (done) { - var self = this; + test: async function () { + const self = this; const configuration = this.configuration; const client = configuration.newClient(`mongodb://${test.server.uri()}/test`, { serverApi: null // TODO(NODE-3807): remove resetting serverApi when the usage of mongodb mock server is removed }); - client.connect(function (err, client) { - expect(err).to.not.exist; - var db = client.db(self.configuration.db); - // Get a db with a new readPreference - db.collection('test', { + await client.connect(); + const db = client.db(self.configuration.db); + + // Get a db with a new readPreference + await db + .collection('test', { readPreference: new ReadPreference('secondary', null, { maxStalenessSeconds: 250 }) }) - .find({}) - .toArray(function (err) { - expect(err).to.not.exist; - expect(test.checkCommand).to.containSubset({ - $query: { find: 'test', filter: {} }, - $readPreference: { mode: 'secondary', maxStalenessSeconds: 250 } - }); - - client.close(done); - }); + .find({}) + .toArray(); + expect(test.checkCommand).to.containSubset({ + $readPreference: { mode: 'secondary', maxStalenessSeconds: 250 } }); + await client.close(); } } ); @@ -157,35 +135,29 @@ describe('Max Staleness', function () { metadata: { requires: { generators: true, - topology: 'single' + topology: 'replicaset' } }, - test: function (done) { - var self = this; + test: async function () { + const self = this; const configuration = this.configuration; const client = configuration.newClient(`mongodb://${test.server.uri()}/test`, { serverApi: null // TODO(NODE-3807): remove resetting serverApi when the usage of mongodb mock server is removed }); - client.connect(function (err, client) { - expect(err).to.not.exist; - var db = client.db(self.configuration.db); - var readPreference = new ReadPreference('secondary', null, { maxStalenessSeconds: 250 }); - // Get a db with a new readPreference - db.collection('test') - .find({}) - .withReadPreference(readPreference) - .toArray(function (err) { - expect(err).to.not.exist; - expect(test.checkCommand).to.containSubset({ - $query: { find: 'test', filter: {} }, - $readPreference: { mode: 'secondary', maxStalenessSeconds: 250 } - }); - - client.close(done); - }); + await client.connect(); + const db = client.db(self.configuration.db); + const readPreference = new ReadPreference('secondary', null, { maxStalenessSeconds: 250 }); + + // Get a db with a new readPreference + await db.collection('test').find({}).withReadPreference(readPreference).toArray(); + + expect(test.checkCommand).to.containSubset({ + $query: { find: 'test', filter: {} }, + $readPreference: { mode: 'secondary', maxStalenessSeconds: 250 } }); + await client.close(); } }); }); diff --git a/test/integration/run-command/run_command.spec.test.ts b/test/integration/run-command/run_command.spec.test.ts index a4a8a522d5..c2ca5e91b5 100644 --- a/test/integration/run-command/run_command.spec.test.ts +++ b/test/integration/run-command/run_command.spec.test.ts @@ -2,10 +2,5 @@ import { loadSpecTests } from '../../spec'; import { runUnifiedSuite } from '../../tools/unified-spec-runner/runner'; describe('RunCommand spec', () => { - runUnifiedSuite(loadSpecTests('run-command'), test => { - if (test.description === 'does not attach $readPreference to given command on standalone') { - return 'TODO(NODE-5263): Do not send $readPreference to standalone servers'; - } - return false; - }); + runUnifiedSuite(loadSpecTests('run-command')); }); diff --git a/test/integration/server-selection/readpreference.test.js b/test/integration/server-selection/readpreference.test.ts similarity index 60% rename from test/integration/server-selection/readpreference.test.js rename to test/integration/server-selection/readpreference.test.ts index ba1e8602a8..1b837598b3 100644 --- a/test/integration/server-selection/readpreference.test.js +++ b/test/integration/server-selection/readpreference.test.ts @@ -1,16 +1,12 @@ -'use strict'; +import { expect } from 'chai'; -const { ReadPreference } = require('../../mongodb'); -const { Topology } = require('../../mongodb'); -const chai = require('chai'); -chai.use(require('chai-subset')); - -const { assert: test, setupDatabase } = require('../shared'); - -const expect = chai.expect; +import { ReadPreference, Topology } from '../../mongodb'; +import { assert as test, setupDatabase } from '../shared'; describe('ReadPreference', function () { let client; + let events; + beforeEach(async function () { client = this.configuration.newClient({ monitorCommands: true }); }); @@ -27,20 +23,19 @@ describe('ReadPreference', function () { metadata: { requires: { mongodb: '>=2.6.0', topology: ['single', 'ssl'] } }, test: function (done) { - var configuration = this.configuration; - var client = configuration.newClient(configuration.writeConcernMax(), { maxPoolSize: 1 }); + const configuration = this.configuration; + const client = configuration.newClient(configuration.writeConcernMax(), { maxPoolSize: 1 }); client.connect(function (err, client) { - var db = client.db(configuration.db); + const db = client.db(configuration.db); expect(err).to.not.exist; // Set read preference - var collection = db.collection('read_pref_1', { + const collection = db.collection('read_pref_1', { readPreference: ReadPreference.SECONDARY_PREFERRED }); // Save checkout function - var command = client.topology.command; + const command = client.topology.command; // Set up our checker method - client.topology.command = function () { - var args = Array.prototype.slice.call(arguments, 0); + client.topology.command = function (...args) { if (args[0] === 'integration_tests.$cmd') { test.equal(ReadPreference.SECONDARY_PREFERRED, args[2].readPreference.mode); } @@ -63,20 +58,19 @@ describe('ReadPreference', function () { metadata: { requires: { mongodb: '>=2.6.0', topology: ['single', 'ssl'] } }, test: function (done) { - var configuration = this.configuration; - var client = configuration.newClient(configuration.writeConcernMax(), { maxPoolSize: 1 }); + const configuration = this.configuration; + const client = configuration.newClient(configuration.writeConcernMax(), { maxPoolSize: 1 }); client.connect(function (err, client) { - var db = client.db(configuration.db); + const db = client.db(configuration.db); expect(err).to.not.exist; // Set read preference - var collection = db.collection('read_pref_1', { + const collection = db.collection('read_pref_1', { readPreference: ReadPreference.SECONDARY_PREFERRED }); // Save checkout function - var command = client.topology.command; + const command = client.topology.command; // Set up our checker method - client.topology.command = function () { - var args = Array.prototype.slice.call(arguments, 0); + client.topology.command = function (...args) { if (args[0] === 'integration_tests.$cmd') { test.equal(ReadPreference.SECONDARY_PREFERRED, args[2].readPreference.mode); } @@ -114,18 +108,17 @@ describe('ReadPreference', function () { metadata: { requires: { mongodb: '>=2.6.0', topology: ['single', 'ssl'] } }, test: function (done) { - var configuration = this.configuration; - var client = configuration.newClient( + const configuration = this.configuration; + const client = configuration.newClient( { w: 1, readPreference: 'secondary' }, { maxPoolSize: 1 } ); client.connect(function (err, client) { - var db = client.db(configuration.db); + const db = client.db(configuration.db); // Save checkout function - var command = client.topology.command; + const command = client.topology.command; // Set up our checker method - client.topology.command = function () { - var args = Array.prototype.slice.call(arguments, 0); + client.topology.command = function (...args) { if (args[0] === 'integration_tests.$cmd') { test.equal(ReadPreference.SECONDARY, args[2].readPreference.mode); } @@ -136,8 +129,7 @@ describe('ReadPreference', function () { db.command({ dbStats: true }, function (err) { expect(err).to.not.exist; - client.topology.command = function () { - var args = Array.prototype.slice.call(arguments, 0); + client.topology.command = function (...args) { if (args[0] === 'integration_tests.$cmd') { test.equal(ReadPreference.SECONDARY_PREFERRED, args[2].readPreference.mode); } @@ -159,13 +151,13 @@ describe('ReadPreference', function () { metadata: { requires: { mongodb: '>=2.6.0', topology: ['single', 'ssl'] } }, test: function (done) { - var configuration = this.configuration; - var client = configuration.newClient(configuration.writeConcernMax(), { maxPoolSize: 1 }); + const configuration = this.configuration; + const client = configuration.newClient(configuration.writeConcernMax(), { maxPoolSize: 1 }); client.connect(function (err, client) { - var db = client.db(configuration.db); + const db = client.db(configuration.db); expect(err).to.not.exist; // Create read preference object. - var mySecondaryPreferred = { mode: 'secondaryPreferred', tags: [] }; + const mySecondaryPreferred = { mode: 'secondaryPreferred', tags: [] }; db.command({ dbStats: true }, { readPreference: mySecondaryPreferred }, function (err) { expect(err).to.not.exist; client.close(done); @@ -178,13 +170,13 @@ describe('ReadPreference', function () { metadata: { requires: { mongodb: '>=2.6.0', topology: ['single', 'ssl'] } }, test: function (done) { - var configuration = this.configuration; - var client = configuration.newClient(configuration.writeConcernMax(), { maxPoolSize: 1 }); + const configuration = this.configuration; + const client = configuration.newClient(configuration.writeConcernMax(), { maxPoolSize: 1 }); client.connect(function (err, client) { - var db = client.db(configuration.db); + const db = client.db(configuration.db); expect(err).to.not.exist; // Create read preference object. - var mySecondaryPreferred = { mode: 'secondaryPreferred', tags: [] }; + const mySecondaryPreferred = { mode: 'secondaryPreferred', tags: [] }; db.listCollections({}, { readPreference: mySecondaryPreferred }).toArray(function (err) { expect(err).to.not.exist; client.close(done); @@ -197,14 +189,14 @@ describe('ReadPreference', function () { metadata: { requires: { mongodb: '>=2.6.0', topology: ['single', 'ssl'] } }, test: function (done) { - var configuration = this.configuration; - var client = configuration.newClient(configuration.writeConcernMax(), { maxPoolSize: 1 }); + const configuration = this.configuration; + const client = configuration.newClient(configuration.writeConcernMax(), { maxPoolSize: 1 }); client.connect(function (err, client) { - var db = client.db(configuration.db); + const db = client.db(configuration.db); expect(err).to.not.exist; // Create read preference object. - var mySecondaryPreferred = { mode: 'secondaryPreferred', tags: [] }; - var cursor = db.collection('test').find({}, { readPreference: mySecondaryPreferred }); + const mySecondaryPreferred = { mode: 'secondaryPreferred', tags: [] }; + const cursor = db.collection('test').find({}, { readPreference: mySecondaryPreferred }); cursor.toArray(function (err) { expect(err).to.not.exist; client.close(done); @@ -217,12 +209,12 @@ describe('ReadPreference', function () { metadata: { requires: { mongodb: '>=2.6.0', topology: ['single', 'ssl'] } }, test: function (done) { - var configuration = this.configuration; - var client = configuration.newClient(configuration.writeConcernMax(), { maxPoolSize: 1 }); + const configuration = this.configuration; + const client = configuration.newClient(configuration.writeConcernMax(), { maxPoolSize: 1 }); client.connect(function (err, client) { - var db = client.db(configuration.db); + const db = client.db(configuration.db); expect(err).to.not.exist; - var cursor = db + const cursor = db .collection('test', { readPreference: ReadPreference.SECONDARY_PREFERRED }) .listIndexes(); test.equal(cursor.readPreference.mode, 'secondaryPreferred'); @@ -247,15 +239,15 @@ describe('ReadPreference', function () { context('hedge', function () { it('should set hedge using [find option & empty hedge]', { - metadata: { requires: { mongodb: '>=3.6.0' } }, + metadata: { requires: { topology: 'replicaset', mongodb: '>=3.6.0' } }, test: function (done) { - const events = []; + events = []; client.on('commandStarted', event => { if (event.commandName === 'find') { events.push(event); } }); - const rp = new ReadPreference(ReadPreference.SECONDARY, null, { hedge: {} }); + const rp = new ReadPreference(ReadPreference.SECONDARY, undefined, { hedge: {} }); client .db(this.configuration.db) .collection('test') @@ -270,15 +262,15 @@ describe('ReadPreference', function () { }); it('should set hedge using [.withReadPreference & empty hedge] ', { - metadata: { requires: { mongodb: '>=3.6.0' } }, + metadata: { requires: { topology: 'replicaset', mongodb: '>=3.6.0' } }, test: function (done) { - const events = []; + events = []; client.on('commandStarted', event => { if (event.commandName === 'find') { events.push(event); } }); - const rp = new ReadPreference(ReadPreference.SECONDARY, null, { hedge: {} }); + const rp = new ReadPreference(ReadPreference.SECONDARY, undefined, { hedge: {} }); client .db(this.configuration.db) .collection('test') @@ -294,15 +286,17 @@ describe('ReadPreference', function () { }); it('should set hedge using [.withReadPreference & enabled hedge] ', { - metadata: { requires: { mongodb: '>=3.6.0' } }, + metadata: { requires: { topology: 'replicaset', mongodb: '>=3.6.0' } }, test: function (done) { - const events = []; + events = []; client.on('commandStarted', event => { if (event.commandName === 'find') { events.push(event); } }); - const rp = new ReadPreference(ReadPreference.SECONDARY, null, { hedge: { enabled: true } }); + const rp = new ReadPreference(ReadPreference.SECONDARY, undefined, { + hedge: { enabled: true } + }); client .db(this.configuration.db) .collection('test') @@ -318,15 +312,15 @@ describe('ReadPreference', function () { }); it('should set hedge using [.withReadPreference & disabled hedge] ', { - metadata: { requires: { mongodb: '>=3.6.0' } }, + metadata: { requires: { topology: 'replicaset', mongodb: '>=3.6.0' } }, test: function (done) { - const events = []; + events = []; client.on('commandStarted', event => { if (event.commandName === 'find') { events.push(event); } }); - const rp = new ReadPreference(ReadPreference.SECONDARY, null, { + const rp = new ReadPreference(ReadPreference.SECONDARY, undefined, { hedge: { enabled: false } }); client @@ -344,15 +338,15 @@ describe('ReadPreference', function () { }); it('should set hedge using [.withReadPreference & undefined hedge] ', { - metadata: { requires: { mongodb: '>=3.6.0' } }, + metadata: { requires: { topology: 'replicaset', mongodb: '>=3.6.0' } }, test: function (done) { - const events = []; + events = []; client.on('commandStarted', event => { if (event.commandName === 'find') { events.push(event); } }); - const rp = new ReadPreference(ReadPreference.SECONDARY, null); + const rp = new ReadPreference(ReadPreference.SECONDARY); client .db(this.configuration.db) .collection('test') @@ -437,13 +431,13 @@ describe('ReadPreference', function () { }); it('should respect readPreference from uri', { - metadata: { requires: { topology: 'replicaset', mongodb: '>=3.6' } }, + metadata: { requires: { topology: 'replicaset' } }, test: async function () { const client = this.configuration.newClient({ readPreference: 'secondary', monitorCommands: true }); - const events = []; + events = []; client.on('commandStarted', event => { if (event.commandName === 'find') { events.push(event); @@ -463,4 +457,149 @@ describe('ReadPreference', function () { await client.close(); } }); + + context('when connecting to a secondary in a replica set with a direct connection', function () { + context('when readPreference is primary', () => { + it('should attach a read preference of primaryPreferred to the read command for replicaset', { + metadata: { requires: { topology: 'replicaset' } }, + test: async function () { + if (this.configuration.topologyType !== 'ReplicaSetWithPrimary') { + this.skipReason = 'This test is supposed to run on the replicaset with primary'; + return this.skip(); + } + + let checkedPrimary = false; + + for (const server of this.configuration.options.hostAddresses) { + const { host, port } = server.toHostPort(); + const client = this.configuration.newClient( + { + readPreference: 'primary', + directConnection: true, + host, + port + }, + { + monitorCommands: true + } + ); + events = []; + client.on('commandStarted', event => { + if (event.commandName === 'find') { + events.push(event); + } + }); + + const admin = client.db().admin(); + const serverStatus = await admin.serverStatus(); + + if (server.toString() === serverStatus.repl.primary) { + await client.db('test').collection('test').findOne({ a: 1 }); + expect(events[0]).to.have.property('commandName', 'find'); + expect(events[0]).to.have.deep.nested.property('command.$readPreference', { + mode: 'primaryPreferred' + }); + checkedPrimary = true; + } + await client.close(); + } + expect(checkedPrimary).to.be.equal(true); + } + }); + + it('should not attach a read preference to the read command for standalone', { + metadata: { requires: { topology: 'single' } }, + test: async function () { + const client = this.configuration.newClient( + { + readPreference: 'primary', + directConnection: true + }, + { + monitorCommands: true + } + ); + events = []; + client.on('commandStarted', event => { + if (event.commandName === 'find') { + events.push(event); + } + }); + await client.db('test').collection('test').findOne({ a: 1 }); + expect(events[0]).to.have.property('commandName', 'find'); + expect(events[0]).to.not.have.deep.nested.property('command.$readPreference'); + await client.close(); + } + }); + }); + + context('when readPreference is secondary', () => { + it('should attach a read preference of secondary to the read command for replicaset', { + metadata: { requires: { topology: 'replicaset' } }, + test: async function () { + let checkedSecondary = false; + + for (const server of this.configuration.options.hostAddresses) { + const { host, port } = server.toHostPort(); + const client = this.configuration.newClient( + { + readPreference: 'secondary', + directConnection: true, + host, + port + }, + { + monitorCommands: true + } + ); + events = []; + client.on('commandStarted', event => { + if (event.commandName === 'find') { + events.push(event); + } + }); + + const admin = client.db().admin(); + const serverStatus = await admin.serverStatus(); + + if (serverStatus.repl.secondary) { + await client.db('test').collection('test').findOne({ a: 1 }); + expect(events[0]).to.have.property('commandName', 'find'); + expect(events[0]).to.have.deep.nested.property('command.$readPreference', { + mode: 'secondary' + }); + checkedSecondary = true; + } + await client.close(); + } + expect(checkedSecondary).to.be.equal(true); + } + }); + + it('should not attach a read preference to the read command for standalone', { + metadata: { requires: { topology: 'single' } }, + test: async function () { + const client = this.configuration.newClient( + { + readPreference: 'secondary', + directConnection: true + }, + { + monitorCommands: true + } + ); + events = []; + client.on('commandStarted', event => { + if (event.commandName === 'find') { + events.push(event); + } + }); + await client.db('test').collection('test').findOne({ a: 1 }); + expect(events[0]).to.have.property('commandName', 'find'); + expect(events[0]).to.not.have.deep.nested.property('command.$readPreference'); + await client.close(); + } + }); + }); + }); }); diff --git a/test/spec/run-command/runCommand.json b/test/spec/run-command/runCommand.json index 007e514bd7..fde9de92e6 100644 --- a/test/spec/run-command/runCommand.json +++ b/test/spec/run-command/runCommand.json @@ -229,7 +229,6 @@ { "topologies": [ "replicaset", - "sharded-replicaset", "load-balanced", "sharded" ] @@ -493,7 +492,7 @@ { "minServerVersion": "4.2", "topologies": [ - "sharded-replicaset", + "sharded", "load-balanced" ] } diff --git a/test/spec/run-command/runCommand.yml b/test/spec/run-command/runCommand.yml index eaa12eff23..9b0bf1ad63 100644 --- a/test/spec/run-command/runCommand.yml +++ b/test/spec/run-command/runCommand.yml @@ -119,7 +119,7 @@ tests: - description: attaches the provided $readPreference to given command runOnRequirements: # Exclude single topology, which is most likely a standalone server - - topologies: [ replicaset, sharded-replicaset, load-balanced, sharded ] + - topologies: [ replicaset, load-balanced, sharded ] operations: - name: runCommand object: *db @@ -250,7 +250,7 @@ tests: - minServerVersion: "4.0" topologies: [ replicaset ] - minServerVersion: "4.2" - topologies: [ sharded-replicaset, load-balanced ] + topologies: [ sharded, load-balanced ] operations: - name: withTransaction object: *session diff --git a/test/tools/runner/config.ts b/test/tools/runner/config.ts index 1ba511bdb4..ab2a4d519e 100644 --- a/test/tools/runner/config.ts +++ b/test/tools/runner/config.ts @@ -39,6 +39,10 @@ interface UrlOptions { useMultipleMongoses?: boolean; /** Parameters for configuring a proxy connection */ proxyURIParams?: ProxyParams; + /** Host overwriting the one provided in the url. */ + host?: string; + /** Port overwriting the one provided in the url. */ + port?: number; } function convertToConnStringMap(obj: Record) { @@ -174,11 +178,13 @@ export class TestConfiguration { const queryOptions = urlOrQueryOptions || {}; // Fall back. - let dbHost = serverOptions.host || this.options.host; + let dbHost = queryOptions.host || this.options.host; if (dbHost.indexOf('.sock') !== -1) { dbHost = qs.escape(dbHost); } - const dbPort = serverOptions.port || this.options.port; + delete queryOptions.host; + const dbPort = queryOptions.port || this.options.port; + delete queryOptions.port; if (this.options.authMechanism && !serverOptions.authMechanism) { Object.assign(queryOptions, { diff --git a/test/unit/sdam/monitor.test.ts b/test/unit/sdam/monitor.test.ts index c5eb826ddf..325b411de2 100644 --- a/test/unit/sdam/monitor.test.ts +++ b/test/unit/sdam/monitor.test.ts @@ -35,7 +35,8 @@ class MockServer { debug: function (_v: any, _x: any) { return; } - } + }, + options: {} } }; }