Skip to content

Commit 937c9c8

Browse files
alenakhineikabaileympearsonnbbeeken
authored
fix(NODE-5981): read preference not applied to commands properly (#4010)
Co-authored-by: Bailey Pearson <[email protected]> Co-authored-by: Neal Beeken <[email protected]>
1 parent 31f1eed commit 937c9c8

File tree

13 files changed

+309
-183
lines changed

13 files changed

+309
-183
lines changed

src/cmap/commands.ts

+1-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { BSONSerializeOptions, Document, Long } from '../bson';
22
import * as BSON from '../bson';
33
import { MongoInvalidArgumentError, MongoRuntimeError } from '../error';
4-
import { ReadPreference } from '../read_preference';
4+
import { type ReadPreference } from '../read_preference';
55
import type { ClientSession } from '../sessions';
66
import type { CommandOptions } from './connection';
77
import {
@@ -51,7 +51,6 @@ export interface OpQueryOptions extends CommandOptions {
5151
requestId?: number;
5252
moreToCome?: boolean;
5353
exhaustAllowed?: boolean;
54-
readPreference?: ReadPreference;
5554
}
5655

5756
/**************************************************************
@@ -77,7 +76,6 @@ export class OpQueryRequest {
7776
awaitData: boolean;
7877
exhaust: boolean;
7978
partial: boolean;
80-
documentsReturnedIn?: string;
8179

8280
constructor(public databaseName: string, public query: Document, options: OpQueryOptions) {
8381
// Basic options needed to be passed in
@@ -503,10 +501,6 @@ export class OpMsgRequest {
503501
// Basic options
504502
this.command.$db = databaseName;
505503

506-
if (options.readPreference && options.readPreference.mode !== ReadPreference.PRIMARY) {
507-
this.command.$readPreference = options.readPreference.toJSON();
508-
}
509-
510504
// Ensure empty options
511505
this.options = options ?? {};
512506

src/cmap/connection.ts

+33-13
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ import type { ServerApi, SupportedNodeConnectionOptions } from '../mongo_client'
2626
import { type MongoClientAuthProviders } from '../mongo_client_auth_providers';
2727
import { MongoLoggableComponent, type MongoLogger, SeverityLevel } from '../mongo_logger';
2828
import { type CancellationToken, TypedEventEmitter } from '../mongo_types';
29-
import type { ReadPreferenceLike } from '../read_preference';
29+
import { ReadPreference, type ReadPreferenceLike } from '../read_preference';
30+
import { ServerType } from '../sdam/common';
3031
import { applySession, type ClientSession, updateSessionFromResponse } from '../sessions';
3132
import {
3233
BufferPool,
@@ -83,6 +84,8 @@ export interface CommandOptions extends BSONSerializeOptions {
8384
willRetryWrite?: boolean;
8485

8586
writeConcern?: WriteConcern;
87+
88+
directConnection?: boolean;
8689
}
8790

8891
/** @public */
@@ -371,16 +374,34 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
371374
cmd.$clusterTime = clusterTime;
372375
}
373376

374-
if (
375-
isSharded(this) &&
376-
!this.supportsOpMsg &&
377-
readPreference &&
378-
readPreference.mode !== 'primary'
379-
) {
380-
cmd = {
381-
$query: cmd,
382-
$readPreference: readPreference.toJSON()
383-
};
377+
// For standalone, drivers MUST NOT set $readPreference.
378+
if (this.description.type !== ServerType.Standalone) {
379+
if (
380+
!isSharded(this) &&
381+
!this.description.loadBalanced &&
382+
this.supportsOpMsg &&
383+
options.directConnection === true &&
384+
readPreference?.mode === 'primary'
385+
) {
386+
// For mongos and load balancers with 'primary' mode, drivers MUST NOT set $readPreference.
387+
// For all other types with a direct connection, if the read preference is 'primary'
388+
// (driver sets 'primary' as default if no read preference is configured),
389+
// the $readPreference MUST be set to 'primaryPreferred'
390+
// to ensure that any server type can handle the request.
391+
cmd.$readPreference = ReadPreference.primaryPreferred.toJSON();
392+
} else if (isSharded(this) && !this.supportsOpMsg && readPreference?.mode !== 'primary') {
393+
// When sending a read operation via OP_QUERY and the $readPreference modifier,
394+
// the query MUST be provided using the $query modifier.
395+
cmd = {
396+
$query: cmd,
397+
$readPreference: readPreference.toJSON()
398+
};
399+
} else if (readPreference?.mode !== 'primary') {
400+
// For mode 'primary', drivers MUST NOT set $readPreference.
401+
// For all other read preference modes (i.e. 'secondary', 'primaryPreferred', ...),
402+
// drivers MUST set $readPreference
403+
cmd.$readPreference = readPreference.toJSON();
404+
}
384405
}
385406

386407
const commandOptions = {
@@ -389,8 +410,7 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
389410
checkKeys: false,
390411
// This value is not overridable
391412
secondaryOk: readPreference.secondaryOk(),
392-
...options,
393-
readPreference // ensure we pass in ReadPreference instance
413+
...options
394414
};
395415

396416
const message = this.supportsOpMsg

src/cmap/stream_description.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export interface StreamDescriptionOptions {
2222
/** @public */
2323
export class StreamDescription {
2424
address: string;
25-
type: string;
25+
type: ServerType;
2626
minWireVersion?: number;
2727
maxWireVersion?: number;
2828
maxBsonObjectSize: number;

src/cmap/wire_protocol/shared.ts

+2-6
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,8 @@ export interface ReadPreferenceOption {
1313
}
1414

1515
export function getReadPreference(options?: ReadPreferenceOption): ReadPreference {
16-
// Default to command version of the readPreference
16+
// Default to command version of the readPreference.
1717
let readPreference = options?.readPreference ?? ReadPreference.primary;
18-
// If we have an option readPreference override the command one
19-
if (options?.readPreference) {
20-
readPreference = options.readPreference;
21-
}
2218

2319
if (typeof readPreference === 'string') {
2420
readPreference = ReadPreference.fromString(readPreference);
@@ -43,7 +39,7 @@ export function isSharded(topologyOrServer?: Topology | Server | Connection): bo
4339
}
4440

4541
// NOTE: This is incredibly inefficient, and should be removed once command construction
46-
// happens based on `Server` not `Topology`.
42+
// happens based on `Server` not `Topology`.
4743
if (topologyOrServer.description && topologyOrServer.description instanceof TopologyDescription) {
4844
const servers: ServerDescription[] = Array.from(topologyOrServer.description.servers.values());
4945
return servers.some((server: ServerDescription) => server.type === ServerType.Mongos);

src/mongo_client.ts

+1
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,7 @@ export interface MongoOptions
821821
readPreference: ReadPreference;
822822
readConcern: ReadConcern;
823823
loadBalanced: boolean;
824+
directConnection: boolean;
824825
serverApi: ServerApi;
825826
compressors: CompressorName[];
826827
writeConcern: WriteConcern;

src/sdam/server.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,10 @@ export class Server extends TypedEventEmitter<ServerEvents> {
290290
}
291291

292292
// Clone the options
293-
const finalOptions = Object.assign({}, options, { wireProtocolCommand: false });
293+
const finalOptions = Object.assign({}, options, {
294+
wireProtocolCommand: false,
295+
directConnection: this.topology.s.options.directConnection
296+
});
294297

295298
// There are cases where we need to flag the read preference not to get sent in
296299
// the command, such as pre-5.0 servers attempting to perform an aggregate write

test/integration/max-staleness/max_staleness.test.js

+49-77
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ describe('Max Staleness', function () {
1818
// Primary server states
1919
const serverIsPrimary = [Object.assign({}, defaultFields)];
2020
server.setMessageHandler(request => {
21-
var doc = request.document;
21+
const doc = request.document;
2222
if (isHello(doc)) {
2323
request.reply(serverIsPrimary[0]);
2424
return;
@@ -46,71 +46,53 @@ describe('Max Staleness', function () {
4646
metadata: {
4747
requires: {
4848
generators: true,
49-
topology: 'single'
49+
topology: 'replicaset'
5050
}
5151
},
5252

53-
test: function (done) {
54-
var self = this;
53+
test: async function () {
54+
const self = this;
5555
const configuration = this.configuration;
5656
const client = configuration.newClient(
5757
`mongodb://${test.server.uri()}/test?readPreference=secondary&maxStalenessSeconds=250`,
5858
{ serverApi: null } // TODO(NODE-3807): remove resetting serverApi when the usage of mongodb mock server is removed
5959
);
6060

61-
client.connect(function (err, client) {
62-
expect(err).to.not.exist;
63-
var db = client.db(self.configuration.db);
64-
65-
db.collection('test')
66-
.find({})
67-
.toArray(function (err) {
68-
expect(err).to.not.exist;
69-
expect(test.checkCommand).to.containSubset({
70-
$query: { find: 'test', filter: {} },
71-
$readPreference: { mode: 'secondary', maxStalenessSeconds: 250 }
72-
});
73-
74-
client.close(done);
75-
});
61+
await client.connect();
62+
const db = client.db(self.configuration.db);
63+
await db.collection('test').find({}).toArray();
64+
expect(test.checkCommand).to.containSubset({
65+
$readPreference: { mode: 'secondary', maxStalenessSeconds: 250 }
7666
});
67+
await client.close();
7768
}
7869
});
7970

8071
it('should correctly set maxStalenessSeconds on Mongos query using db level readPreference', {
8172
metadata: {
8273
requires: {
8374
generators: true,
84-
topology: 'single'
75+
topology: 'replicaset'
8576
}
8677
},
8778

88-
test: function (done) {
79+
test: async function () {
8980
const configuration = this.configuration;
9081
const client = configuration.newClient(`mongodb://${test.server.uri()}/test`, {
9182
serverApi: null // TODO(NODE-3807): remove resetting serverApi when the usage of mongodb mock server is removed
9283
});
93-
client.connect(function (err, client) {
94-
expect(err).to.not.exist;
9584

96-
// Get a db with a new readPreference
97-
var db1 = client.db('test', {
98-
readPreference: new ReadPreference('secondary', null, { maxStalenessSeconds: 250 })
99-
});
85+
await client.connect();
10086

101-
db1
102-
.collection('test')
103-
.find({})
104-
.toArray(function (err) {
105-
expect(err).to.not.exist;
106-
expect(test.checkCommand).to.containSubset({
107-
$query: { find: 'test', filter: {} },
108-
$readPreference: { mode: 'secondary', maxStalenessSeconds: 250 }
109-
});
110-
111-
client.close(done);
112-
});
87+
// Get a db with a new readPreference
88+
const db1 = client.db('test', {
89+
readPreference: new ReadPreference('secondary', null, { maxStalenessSeconds: 250 })
90+
});
91+
await db1.collection('test').find({}).toArray();
92+
expect(test.checkCommand).to.containSubset({
93+
$readPreference: { mode: 'secondary', maxStalenessSeconds: 250 }
11394
});
95+
await client.close();
11496
}
11597
});
11698

@@ -120,35 +102,31 @@ describe('Max Staleness', function () {
120102
metadata: {
121103
requires: {
122104
generators: true,
123-
topology: 'single'
105+
topology: 'replicaset'
124106
}
125107
},
126108

127-
test: function (done) {
128-
var self = this;
109+
test: async function () {
110+
const self = this;
129111
const configuration = this.configuration;
130112
const client = configuration.newClient(`mongodb://${test.server.uri()}/test`, {
131113
serverApi: null // TODO(NODE-3807): remove resetting serverApi when the usage of mongodb mock server is removed
132114
});
133-
client.connect(function (err, client) {
134-
expect(err).to.not.exist;
135-
var db = client.db(self.configuration.db);
136115

137-
// Get a db with a new readPreference
138-
db.collection('test', {
116+
await client.connect();
117+
const db = client.db(self.configuration.db);
118+
119+
// Get a db with a new readPreference
120+
await db
121+
.collection('test', {
139122
readPreference: new ReadPreference('secondary', null, { maxStalenessSeconds: 250 })
140123
})
141-
.find({})
142-
.toArray(function (err) {
143-
expect(err).to.not.exist;
144-
expect(test.checkCommand).to.containSubset({
145-
$query: { find: 'test', filter: {} },
146-
$readPreference: { mode: 'secondary', maxStalenessSeconds: 250 }
147-
});
148-
149-
client.close(done);
150-
});
124+
.find({})
125+
.toArray();
126+
expect(test.checkCommand).to.containSubset({
127+
$readPreference: { mode: 'secondary', maxStalenessSeconds: 250 }
151128
});
129+
await client.close();
152130
}
153131
}
154132
);
@@ -157,35 +135,29 @@ describe('Max Staleness', function () {
157135
metadata: {
158136
requires: {
159137
generators: true,
160-
topology: 'single'
138+
topology: 'replicaset'
161139
}
162140
},
163141

164-
test: function (done) {
165-
var self = this;
142+
test: async function () {
143+
const self = this;
166144
const configuration = this.configuration;
167145
const client = configuration.newClient(`mongodb://${test.server.uri()}/test`, {
168146
serverApi: null // TODO(NODE-3807): remove resetting serverApi when the usage of mongodb mock server is removed
169147
});
170-
client.connect(function (err, client) {
171-
expect(err).to.not.exist;
172-
var db = client.db(self.configuration.db);
173-
var readPreference = new ReadPreference('secondary', null, { maxStalenessSeconds: 250 });
174148

175-
// Get a db with a new readPreference
176-
db.collection('test')
177-
.find({})
178-
.withReadPreference(readPreference)
179-
.toArray(function (err) {
180-
expect(err).to.not.exist;
181-
expect(test.checkCommand).to.containSubset({
182-
$query: { find: 'test', filter: {} },
183-
$readPreference: { mode: 'secondary', maxStalenessSeconds: 250 }
184-
});
185-
186-
client.close(done);
187-
});
149+
await client.connect();
150+
const db = client.db(self.configuration.db);
151+
const readPreference = new ReadPreference('secondary', null, { maxStalenessSeconds: 250 });
152+
153+
// Get a db with a new readPreference
154+
await db.collection('test').find({}).withReadPreference(readPreference).toArray();
155+
156+
expect(test.checkCommand).to.containSubset({
157+
$query: { find: 'test', filter: {} },
158+
$readPreference: { mode: 'secondary', maxStalenessSeconds: 250 }
188159
});
160+
await client.close();
189161
}
190162
});
191163
});

test/integration/run-command/run_command.spec.test.ts

+1-6
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,5 @@ import { loadSpecTests } from '../../spec';
22
import { runUnifiedSuite } from '../../tools/unified-spec-runner/runner';
33

44
describe('RunCommand spec', () => {
5-
runUnifiedSuite(loadSpecTests('run-command'), test => {
6-
if (test.description === 'does not attach $readPreference to given command on standalone') {
7-
return 'TODO(NODE-5263): Do not send $readPreference to standalone servers';
8-
}
9-
return false;
10-
});
5+
runUnifiedSuite(loadSpecTests('run-command'));
116
});

0 commit comments

Comments
 (0)