diff --git a/src/cmap/connection.ts b/src/cmap/connection.ts index e7b6e52d222..ccee670fc05 100644 --- a/src/cmap/connection.ts +++ b/src/cmap/connection.ts @@ -97,6 +97,7 @@ export interface CommandOptions extends BSONSerializeOptions { session?: ClientSession; documentsReturnedIn?: string; noResponse?: boolean; + omitReadPreference?: boolean; // FIXME: NODE-2802 willRetryWrite?: boolean; diff --git a/src/operations/command.ts b/src/operations/command.ts index 70ae413d65a..fe6b854b01d 100644 --- a/src/operations/command.ts +++ b/src/operations/command.ts @@ -10,6 +10,7 @@ import type { Server } from '../sdam/server'; import type { BSONSerializeOptions, Document } from '../bson'; import type { ReadConcernLike } from './../read_concern'; import { Explain, ExplainOptions } from '../explain'; +import { MIN_SECONDARY_WRITE_WIRE_VERSION } from '../sdam/server_selection'; const SUPPORTS_WRITE_CONCERN_AND_COLLATION = 5; @@ -126,6 +127,12 @@ export abstract class CommandOperation extends AbstractOperation { Object.assign(cmd, { readConcern: this.readConcern }); } + if (this.trySecondaryWrite && serverWireVersion < MIN_SECONDARY_WRITE_WIRE_VERSION) { + /* eslint no-console: 0 */ + console.log('need to remove read preference from options'); + options.omitReadPreference = true; + } + if (options.collation && serverWireVersion < SUPPORTS_WRITE_CONCERN_AND_COLLATION) { callback( new MongoCompatibilityError( diff --git a/src/operations/operation.ts b/src/operations/operation.ts index 1142b6cf9e5..66198da456a 100644 --- a/src/operations/operation.ts +++ b/src/operations/operation.ts @@ -31,6 +31,7 @@ export interface OperationOptions extends BSONSerializeOptions { /** @internal Hints to `executeOperation` that this operation should not unpin on an ended transaction */ bypassPinningCheck?: boolean; + omitReadPreference?: boolean; } /** @internal */ @@ -49,7 +50,7 @@ export abstract class AbstractOperation { readPreference: ReadPreference; server!: Server; bypassPinningCheck: boolean; - trySecondaryWrite = false; + trySecondaryWrite: boolean; // BSON serialization options bsonOptions?: BSONSerializeOptions; @@ -73,6 +74,7 @@ export abstract class AbstractOperation { this.options = options; this.bypassPinningCheck = !!options.bypassPinningCheck; + this.trySecondaryWrite = false; } abstract execute(server: Server, session: ClientSession, callback: Callback): void; diff --git a/src/sdam/server.ts b/src/sdam/server.ts index e3f1fb40965..393e2a72ba5 100644 --- a/src/sdam/server.ts +++ b/src/sdam/server.ts @@ -299,6 +299,14 @@ export class Server extends TypedEventEmitter { // Clone the options const finalOptions = Object.assign({}, options, { wireProtocolCommand: false }); + // 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 + // with a non-primary read preference. In this case the effective read preference + // (primary) is not the same as the provided and must be removed completely. + if (finalOptions.omitReadPreference) { + delete finalOptions.readPreference; + } + // error if collation not supported if (collationNotSupported(this, cmd)) { callback(new MongoCompatibilityError(`Server ${this.name} does not support collation`)); diff --git a/src/sdam/server_selection.ts b/src/sdam/server_selection.ts index 95010d66f9d..a39304841e2 100644 --- a/src/sdam/server_selection.ts +++ b/src/sdam/server_selection.ts @@ -46,6 +46,8 @@ export function secondaryWritableServerSelector( /* eslint no-console: 0 */ console.log('select', readPreference, wireVersion); if (!readPreference || (wireVersion && wireVersion < MIN_SECONDARY_WRITE_WIRE_VERSION)) { + /* eslint no-console: 0 */ + console.log('force select primary'); return readPreferenceServerSelector(ReadPreference.primary); } return readPreferenceServerSelector(readPreference); diff --git a/test/functional/crud_spec.test.js b/test/functional/crud_spec.test.js index 4e2b8e27822..ceccf8e9e7d 100644 --- a/test/functional/crud_spec.test.js +++ b/test/functional/crud_spec.test.js @@ -424,7 +424,8 @@ describe('CRUD spec v1', function () { } }); -describe('CRUD unified', function () { +// https://jira.mongodb.org/browse/NODE-3655 +describe.only('CRUD unified', function () { for (const crudSpecTest of loadSpecTests('crud/unified')) { expect(crudSpecTest).to.exist; const testDescription = String(crudSpecTest.description); diff --git a/test/unit/sdam/server_selection.test.js b/test/unit/sdam/server_selection.test.js index 6e6c799ca31..346a17612ba 100644 --- a/test/unit/sdam/server_selection.test.js +++ b/test/unit/sdam/server_selection.test.js @@ -11,46 +11,107 @@ const { ServerDescription } = require('../../../src/sdam/server_description'); const { TopologyDescription } = require('../../../src/sdam/topology_description'); const { TopologyType } = require('../../../src/sdam/common'); -describe('ServerSelector', function () { +describe.only('server selection', function () { + const primary = new ServerDescription('127.0.0.1:27017', { + setName: 'test', + isWritablePrimary: true, + ok: 1 + }); + const secondary = new ServerDescription('127.0.0.1:27018', { + setName: 'test', + secondary: true, + ok: 1 + }); + const mongos = new ServerDescription('127.0.0.1:27019', { + msg: 'isdbgrid', + ok: 1 + }); + const loadBalancer = new ServerDescription('127.0.0.1:27020', { ok: 1 }, { loadBalanced: true }); + const single = new ServerDescription('127.0.0.1:27021', { + isWritablePrimary: true, + ok: 1 + }); + describe('#secondaryWritableServerSelector', function () { - const primary = new ServerDescription('127.0.0.1:27017', { - setName: 'test', - isWritablePrimary: true, - ok: 1 - }); - const secondary = new ServerDescription('127.0.0.1:27018', { - setName: 'test', - secondary: true, - ok: 1 - }); - const serverDescriptions = new Map(); - serverDescriptions.set('127.0.0.1:27017', primary); - serverDescriptions.set('127.0.0.1:27018', secondary); - - context('when the common server version is >= 5.0', function () { - const topologyDescription = new TopologyDescription( - TopologyType.ReplicaSetWithPrimary, - serverDescriptions, - 'test', - MIN_SECONDARY_WRITE_WIRE_VERSION, - new ObjectId(), - MIN_SECONDARY_WRITE_WIRE_VERSION - ); - - context('when a read preference is provided', function () { - const selector = secondaryWritableServerSelector( + context('when the topology is a replica set', function () { + const serverDescriptions = new Map(); + serverDescriptions.set('127.0.0.1:27017', primary); + serverDescriptions.set('127.0.0.1:27018', secondary); + + context('when the common server version is >= 5.0', function () { + const topologyDescription = new TopologyDescription( + TopologyType.ReplicaSetWithPrimary, + serverDescriptions, + 'test', MIN_SECONDARY_WRITE_WIRE_VERSION, - ReadPreference.secondary + new ObjectId(), + MIN_SECONDARY_WRITE_WIRE_VERSION ); - const server = selector(topologyDescription, Array.from(serverDescriptions.values())); - it('uses the provided read preference', function () { - expect(server).to.deep.equal([secondary]); + context('when a read preference is provided', function () { + const selector = secondaryWritableServerSelector( + MIN_SECONDARY_WRITE_WIRE_VERSION, + ReadPreference.secondary + ); + const server = selector(topologyDescription, Array.from(serverDescriptions.values())); + + it('uses the provided read preference', function () { + expect(server).to.deep.equal([secondary]); + }); + }); + + context('when a read preference is not provided', function () { + const selector = secondaryWritableServerSelector(MIN_SECONDARY_WRITE_WIRE_VERSION); + const server = selector(topologyDescription, Array.from(serverDescriptions.values())); + + it('selects a primary', function () { + expect(server).to.deep.equal([primary]); + }); }); }); - context('when a read preference is not provided', function () { - const selector = secondaryWritableServerSelector(MIN_SECONDARY_WRITE_WIRE_VERSION); + context('when the common server version is < 5.0', function () { + const topologyDescription = new TopologyDescription( + TopologyType.ReplicaSetWithPrimary, + serverDescriptions, + 'test', + MIN_SECONDARY_WRITE_WIRE_VERSION - 1, + new ObjectId(), + MIN_SECONDARY_WRITE_WIRE_VERSION - 1 + ); + + context('when a read preference is provided', function () { + const selector = secondaryWritableServerSelector( + MIN_SECONDARY_WRITE_WIRE_VERSION - 1, + ReadPreference.secondary + ); + const server = selector(topologyDescription, Array.from(serverDescriptions.values())); + + it('selects a primary', function () { + expect(server).to.deep.equal([primary]); + }); + }); + + context('when read preference is not provided', function () { + const selector = secondaryWritableServerSelector(MIN_SECONDARY_WRITE_WIRE_VERSION - 1); + const server = selector(topologyDescription, Array.from(serverDescriptions.values())); + + it('selects a primary', function () { + expect(server).to.deep.equal([primary]); + }); + }); + }); + + context('when a common wire version is not provided', function () { + const topologyDescription = new TopologyDescription( + TopologyType.ReplicaSetWithPrimary, + serverDescriptions, + 'test', + MIN_SECONDARY_WRITE_WIRE_VERSION, + new ObjectId(), + MIN_SECONDARY_WRITE_WIRE_VERSION + ); + const selector = secondaryWritableServerSelector(); const server = selector(topologyDescription, Array.from(serverDescriptions.values())); it('selects a primary', function () { @@ -59,52 +120,261 @@ describe('ServerSelector', function () { }); }); - context('when the common server version is < 5.0', function () { - const topologyDescription = new TopologyDescription( - TopologyType.ReplicaSetWithPrimary, - serverDescriptions, - 'test', - MIN_SECONDARY_WRITE_WIRE_VERSION - 1, - new ObjectId(), - MIN_SECONDARY_WRITE_WIRE_VERSION - 1 - ); - - context('when a read preference is provided', function () { - const selector = secondaryWritableServerSelector( + context('when the topology is sharded', function () { + const serverDescriptions = new Map(); + serverDescriptions.set('127.0.0.1:27019', mongos); + + context('when the common server version is >= 5.0', function () { + const topologyDescription = new TopologyDescription( + TopologyType.Sharded, + serverDescriptions, + 'test', + MIN_SECONDARY_WRITE_WIRE_VERSION, + new ObjectId(), + MIN_SECONDARY_WRITE_WIRE_VERSION + ); + + context('when a read preference is provided', function () { + const selector = secondaryWritableServerSelector( + MIN_SECONDARY_WRITE_WIRE_VERSION, + ReadPreference.secondary + ); + const server = selector(topologyDescription, Array.from(serverDescriptions.values())); + + it('selects a mongos', function () { + expect(server).to.deep.equal([mongos]); + }); + }); + + context('when a read preference is not provided', function () { + const selector = secondaryWritableServerSelector(MIN_SECONDARY_WRITE_WIRE_VERSION); + const server = selector(topologyDescription, Array.from(serverDescriptions.values())); + + it('selects a mongos', function () { + expect(server).to.deep.equal([mongos]); + }); + }); + }); + + context('when the common server version is < 5.0', function () { + const topologyDescription = new TopologyDescription( + TopologyType.Sharded, + serverDescriptions, + 'test', MIN_SECONDARY_WRITE_WIRE_VERSION - 1, - ReadPreference.secondary + new ObjectId(), + MIN_SECONDARY_WRITE_WIRE_VERSION - 1 ); + + context('when a read preference is provided', function () { + const selector = secondaryWritableServerSelector( + MIN_SECONDARY_WRITE_WIRE_VERSION - 1, + ReadPreference.secondary + ); + const server = selector(topologyDescription, Array.from(serverDescriptions.values())); + + it('selects a mongos', function () { + expect(server).to.deep.equal([mongos]); + }); + }); + + context('when read preference is not provided', function () { + const selector = secondaryWritableServerSelector(MIN_SECONDARY_WRITE_WIRE_VERSION - 1); + const server = selector(topologyDescription, Array.from(serverDescriptions.values())); + + it('selects a mongos', function () { + expect(server).to.deep.equal([mongos]); + }); + }); + }); + + context('when a common wire version is not provided', function () { + const topologyDescription = new TopologyDescription( + TopologyType.Sharded, + serverDescriptions, + 'test', + MIN_SECONDARY_WRITE_WIRE_VERSION, + new ObjectId(), + MIN_SECONDARY_WRITE_WIRE_VERSION + ); + const selector = secondaryWritableServerSelector(); const server = selector(topologyDescription, Array.from(serverDescriptions.values())); - it('selects a primary', function () { - expect(server).to.deep.equal([primary]); + it('selects a mongos', function () { + expect(server).to.deep.equal([mongos]); + }); + }); + }); + + context('when the topology is load balanced', function () { + const serverDescriptions = new Map(); + serverDescriptions.set('127.0.0.1:27020', loadBalancer); + + context('when the common server version is >= 5.0', function () { + const topologyDescription = new TopologyDescription( + TopologyType.LoadBalanced, + serverDescriptions, + 'test', + MIN_SECONDARY_WRITE_WIRE_VERSION, + new ObjectId(), + MIN_SECONDARY_WRITE_WIRE_VERSION + ); + + context('when a read preference is provided', function () { + const selector = secondaryWritableServerSelector( + MIN_SECONDARY_WRITE_WIRE_VERSION, + ReadPreference.secondary + ); + const server = selector(topologyDescription, Array.from(serverDescriptions.values())); + + it('selects a load balancer', function () { + expect(server).to.deep.equal([loadBalancer]); + }); + }); + + context('when a read preference is not provided', function () { + const selector = secondaryWritableServerSelector(MIN_SECONDARY_WRITE_WIRE_VERSION); + const server = selector(topologyDescription, Array.from(serverDescriptions.values())); + + it('selects a load balancer', function () { + expect(server).to.deep.equal([loadBalancer]); + }); }); }); - context('when read preference is not provided', function () { - const selector = secondaryWritableServerSelector(MIN_SECONDARY_WRITE_WIRE_VERSION - 1); + context('when the common server version is < 5.0', function () { + const topologyDescription = new TopologyDescription( + TopologyType.LoadBalanced, + serverDescriptions, + 'test', + MIN_SECONDARY_WRITE_WIRE_VERSION - 1, + new ObjectId(), + MIN_SECONDARY_WRITE_WIRE_VERSION - 1 + ); + + context('when a read preference is provided', function () { + const selector = secondaryWritableServerSelector( + MIN_SECONDARY_WRITE_WIRE_VERSION - 1, + ReadPreference.secondary + ); + const server = selector(topologyDescription, Array.from(serverDescriptions.values())); + + it('selects a load balancer', function () { + expect(server).to.deep.equal([loadBalancer]); + }); + }); + + context('when read preference is not provided', function () { + const selector = secondaryWritableServerSelector(MIN_SECONDARY_WRITE_WIRE_VERSION - 1); + const server = selector(topologyDescription, Array.from(serverDescriptions.values())); + + it('selects a load balancer', function () { + expect(server).to.deep.equal([loadBalancer]); + }); + }); + }); + + context('when a common wire version is not provided', function () { + const topologyDescription = new TopologyDescription( + TopologyType.LoadBalanced, + serverDescriptions, + 'test', + MIN_SECONDARY_WRITE_WIRE_VERSION, + new ObjectId(), + MIN_SECONDARY_WRITE_WIRE_VERSION + ); + const selector = secondaryWritableServerSelector(); const server = selector(topologyDescription, Array.from(serverDescriptions.values())); - it('selects a primary', function () { - expect(server).to.deep.equal([primary]); + it('selects a load balancer', function () { + expect(server).to.deep.equal([loadBalancer]); }); }); }); - context('when a common wire version is not provided', function () { - const topologyDescription = new TopologyDescription( - TopologyType.ReplicaSetWithPrimary, - serverDescriptions, - 'test', - MIN_SECONDARY_WRITE_WIRE_VERSION, - new ObjectId(), - MIN_SECONDARY_WRITE_WIRE_VERSION - ); - const selector = secondaryWritableServerSelector(); - const server = selector(topologyDescription, Array.from(serverDescriptions.values())); - - it('selects a primary', function () { - expect(server).to.deep.equal([primary]); + context('when the topology is single', function () { + const serverDescriptions = new Map(); + serverDescriptions.set('127.0.0.1:27020', single); + + context('when the common server version is >= 5.0', function () { + const topologyDescription = new TopologyDescription( + TopologyType.Single, + serverDescriptions, + 'test', + MIN_SECONDARY_WRITE_WIRE_VERSION, + new ObjectId(), + MIN_SECONDARY_WRITE_WIRE_VERSION + ); + + context('when a read preference is provided', function () { + const selector = secondaryWritableServerSelector( + MIN_SECONDARY_WRITE_WIRE_VERSION, + ReadPreference.secondary + ); + const server = selector(topologyDescription, Array.from(serverDescriptions.values())); + + it('selects a standalone', function () { + expect(server).to.deep.equal([single]); + }); + }); + + context('when a read preference is not provided', function () { + const selector = secondaryWritableServerSelector(MIN_SECONDARY_WRITE_WIRE_VERSION); + const server = selector(topologyDescription, Array.from(serverDescriptions.values())); + + it('selects a standalone', function () { + expect(server).to.deep.equal([single]); + }); + }); + }); + + context('when the common server version is < 5.0', function () { + const topologyDescription = new TopologyDescription( + TopologyType.Single, + serverDescriptions, + 'test', + MIN_SECONDARY_WRITE_WIRE_VERSION - 1, + new ObjectId(), + MIN_SECONDARY_WRITE_WIRE_VERSION - 1 + ); + + context('when a read preference is provided', function () { + const selector = secondaryWritableServerSelector( + MIN_SECONDARY_WRITE_WIRE_VERSION - 1, + ReadPreference.secondary + ); + const server = selector(topologyDescription, Array.from(serverDescriptions.values())); + + it('selects a standalone', function () { + expect(server).to.deep.equal([single]); + }); + }); + + context('when read preference is not provided', function () { + const selector = secondaryWritableServerSelector(MIN_SECONDARY_WRITE_WIRE_VERSION - 1); + const server = selector(topologyDescription, Array.from(serverDescriptions.values())); + + it('selects a standalone', function () { + expect(server).to.deep.equal([single]); + }); + }); + }); + + context('when a common wire version is not provided', function () { + const topologyDescription = new TopologyDescription( + TopologyType.Single, + serverDescriptions, + 'test', + MIN_SECONDARY_WRITE_WIRE_VERSION, + new ObjectId(), + MIN_SECONDARY_WRITE_WIRE_VERSION + ); + const selector = secondaryWritableServerSelector(); + const server = selector(topologyDescription, Array.from(serverDescriptions.values())); + + it('selects a standalone', function () { + expect(server).to.deep.equal([single]); + }); }); }); });