Skip to content

Commit 2b7b936

Browse files
author
Thomas Reggi
authored
feat: support hedged reads
Adds driver support for server Hedged Reads. Adds `hedge` property to the `ReadPreference` class and exports it using the `.toJSON` method. Introduces `withMonitoredClient` a shared testing utility for testing command monitoring. Tests command sent to the server and verifies that `hedge` property was propagated. Tests are only run on `>=3.6.0` due to how prior versions send operations to the server. NODE-2510
1 parent 665b352 commit 2b7b936

File tree

6 files changed

+273
-25
lines changed

6 files changed

+273
-25
lines changed

lib/read_preference.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
* @param {object[]} [tags] A tag set used to target reads to members with the specified tag(s). tagSet is not available if using read preference mode primary.
1010
* @param {object} [options] Additional read preference options
1111
* @param {number} [options.maxStalenessSeconds] Max secondary read staleness in seconds, Minimum value is 90 seconds.
12+
* @param {object} [options.hedge] Server mode in which the same query is dispatched in parallel to multiple replica set members.
13+
* @param {boolean} [options.hedge.enabled] Explicitly enable or disable hedged reads.
1214
* @see https://docs.mongodb.com/manual/core/read-preference/
1315
* @returns {ReadPreference}
1416
*/
@@ -25,6 +27,7 @@ const ReadPreference = function(mode, tags, options) {
2527

2628
this.mode = mode;
2729
this.tags = tags;
30+
this.hedge = options && options.hedge;
2831

2932
options = options || {};
3033
if (options.maxStalenessSeconds != null) {
@@ -47,6 +50,10 @@ const ReadPreference = function(mode, tags, options) {
4750
if (this.maxStalenessSeconds) {
4851
throw new TypeError('Primary read preference cannot be combined with maxStalenessSeconds');
4952
}
53+
54+
if (this.hedge) {
55+
throw new TypeError('Primary read preference cannot be combined with hedge');
56+
}
5057
}
5158
};
5259

@@ -96,7 +103,8 @@ ReadPreference.fromOptions = function(options) {
96103
const mode = readPreference.mode || readPreference.preference;
97104
if (mode && typeof mode === 'string') {
98105
return new ReadPreference(mode, readPreference.tags, {
99-
maxStalenessSeconds: readPreference.maxStalenessSeconds
106+
maxStalenessSeconds: readPreference.maxStalenessSeconds,
107+
hedge: options.hedge
100108
});
101109
}
102110
}
@@ -160,6 +168,7 @@ ReadPreference.prototype.toJSON = function() {
160168
const readPreference = { mode: this.mode };
161169
if (Array.isArray(this.tags)) readPreference.tags = this.tags;
162170
if (this.maxStalenessSeconds) readPreference.maxStalenessSeconds = this.maxStalenessSeconds;
171+
if (this.hedge) readPreference.hedge = this.hedge;
163172
return readPreference;
164173
};
165174

lib/sdam/topology.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -958,7 +958,8 @@ function translateReadPreference(options) {
958958
const mode = r.mode || r.preference;
959959
if (mode && typeof mode === 'string') {
960960
options.readPreference = new ReadPreference(mode, r.tags, {
961-
maxStalenessSeconds: r.maxStalenessSeconds
961+
maxStalenessSeconds: r.maxStalenessSeconds,
962+
hedge: r.hedge
962963
});
963964
}
964965
} else if (!(r instanceof ReadPreference)) {

lib/utils.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ var translateReadPreference = function(options) {
2020
const mode = r.mode || r.preference;
2121
if (mode && typeof mode === 'string') {
2222
options.readPreference = new ReadPreference(mode, r.tags, {
23-
maxStalenessSeconds: r.maxStalenessSeconds
23+
maxStalenessSeconds: r.maxStalenessSeconds,
24+
hedge: r.hedge
2425
});
2526
}
2627
} else if (!(r instanceof ReadPreference)) {

test/functional/readpreference.test.js

+127-22
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
'use strict';
2-
var test = require('./shared').assert;
3-
var setupDatabase = require('./shared').setupDatabase;
2+
3+
const test = require('./shared').assert;
4+
const setupDatabase = require('./shared').setupDatabase;
5+
const withMonitoredClient = require('./shared').withMonitoredClient;
46
const expect = require('chai').expect;
57
const { ReadPreference } = require('../..');
68

@@ -81,7 +83,7 @@ describe('ReadPreference', function() {
8183
var client = configuration.newClient(configuration.writeConcernMax(), { poolSize: 1 });
8284
client.connect(function(err, client) {
8385
var db = client.db(configuration.db);
84-
test.equal(null, err);
86+
expect(err).to.not.exist;
8587
// Set read preference
8688
var collection = db.collection('read_pref_1', {
8789
readPreference: ReadPreference.SECONDARY_PREFERRED
@@ -100,7 +102,7 @@ describe('ReadPreference', function() {
100102

101103
// Execute count
102104
collection.count(function(err) {
103-
test.equal(null, err);
105+
expect(err).to.not.exist;
104106
client.topology.command = command;
105107

106108
client.close(done);
@@ -117,7 +119,7 @@ describe('ReadPreference', function() {
117119
var client = configuration.newClient(configuration.writeConcernMax(), { poolSize: 1 });
118120
client.connect(function(err, client) {
119121
var db = client.db(configuration.db);
120-
test.equal(null, err);
122+
expect(err).to.not.exist;
121123
// Set read preference
122124
var collection = db.collection('read_pref_1', {
123125
readPreference: ReadPreference.SECONDARY_PREFERRED
@@ -139,7 +141,7 @@ describe('ReadPreference', function() {
139141
collection.group([], {}, { count: 0 }, 'function (obj, prev) { prev.count++; }', function(
140142
err
141143
) {
142-
test.equal(null, err);
144+
expect(err).to.not.exist;
143145
client.topology.command = command;
144146

145147
client.close(done);
@@ -156,7 +158,7 @@ describe('ReadPreference', function() {
156158
var client = configuration.newClient(configuration.writeConcernMax(), { poolSize: 1 });
157159
client.connect(function(err, client) {
158160
var db = client.db(configuration.db);
159-
test.equal(null, err);
161+
expect(err).to.not.exist;
160162
// Set read preference
161163
var collection = db.collection('read_pref_1', {
162164
readPreference: ReadPreference.SECONDARY_PREFERRED
@@ -184,7 +186,7 @@ describe('ReadPreference', function() {
184186

185187
// Perform the map reduce
186188
collection.mapReduce(map, reduce, { out: { inline: 1 } }, function(/* err */) {
187-
// test.equal(null, err);
189+
// expect(err).to.not.exist;
188190

189191
// eslint-disable-line
190192
client.topology.command = command;
@@ -205,7 +207,7 @@ describe('ReadPreference', function() {
205207
var client = configuration.newClient(configuration.writeConcernMax(), { poolSize: 1 });
206208
client.connect(function(err, client) {
207209
var db = client.db(configuration.db);
208-
test.equal(null, err);
210+
expect(err).to.not.exist;
209211
// Set read preference
210212
var collection = db.collection('read_pref_1', {
211213
readPreference: ReadPreference.SECONDARY_PREFERRED
@@ -234,7 +236,7 @@ describe('ReadPreference', function() {
234236

235237
// Perform the map reduce
236238
collection.mapReduce(map, reduce, { out: 'inline' }, function(/* err */) {
237-
// test.equal(null, err);
239+
// expect(err).to.not.exist;
238240
client.topology.command = command;
239241
client.close(done);
240242
});
@@ -251,7 +253,7 @@ describe('ReadPreference', function() {
251253
var client = configuration.newClient(configuration.writeConcernMax(), { poolSize: 1 });
252254
client.connect(function(err, client) {
253255
var db = client.db(configuration.db);
254-
test.equal(null, err);
256+
expect(err).to.not.exist;
255257
// Set read preference
256258
var collection = db.collection('read_pref_1', {
257259
readPreference: ReadPreference.SECONDARY_PREFERRED
@@ -283,7 +285,7 @@ describe('ReadPreference', function() {
283285
var client = configuration.newClient(configuration.writeConcernMax(), { poolSize: 1 });
284286
client.connect(function(err, client) {
285287
var db = client.db(configuration.db);
286-
test.equal(null, err);
288+
expect(err).to.not.exist;
287289
// Set read preference
288290
var collection = db.collection('read_pref_1', {
289291
readPreference: ReadPreference.SECONDARY_PREFERRED
@@ -334,7 +336,7 @@ describe('ReadPreference', function() {
334336
var client = configuration.newClient(configuration.writeConcernMax(), { poolSize: 1 });
335337
client.connect(function(err, client) {
336338
var db = client.db(configuration.db);
337-
test.equal(null, err);
339+
expect(err).to.not.exist;
338340
// Set read preference
339341
var collection = db.collection('read_pref_1', {
340342
readPreference: ReadPreference.SECONDARY_PREFERRED
@@ -353,7 +355,7 @@ describe('ReadPreference', function() {
353355

354356
// Perform the map reduce
355357
collection.stats(function(/* err */) {
356-
// test.equal(null, err);
358+
// expect(err).to.not.exist;
357359
client.topology.command = command;
358360
client.close(done);
359361
});
@@ -382,7 +384,7 @@ describe('ReadPreference', function() {
382384
};
383385

384386
db.command({ dbStats: true }, function(err) {
385-
test.equal(null, err);
387+
expect(err).to.not.exist;
386388

387389
client.topology.command = function() {
388390
var args = Array.prototype.slice.call(arguments, 0);
@@ -394,7 +396,7 @@ describe('ReadPreference', function() {
394396
};
395397

396398
db.command({ dbStats: true }, { readPreference: 'secondaryPreferred' }, function(err) {
397-
test.equal(null, err);
399+
expect(err).to.not.exist;
398400
client.topology.command = command;
399401
client.close(done);
400402
});
@@ -411,11 +413,11 @@ describe('ReadPreference', function() {
411413
var client = configuration.newClient(configuration.writeConcernMax(), { poolSize: 1 });
412414
client.connect(function(err, client) {
413415
var db = client.db(configuration.db);
414-
test.equal(null, err);
416+
expect(err).to.not.exist;
415417
// Create read preference object.
416418
var mySecondaryPreferred = { mode: 'secondaryPreferred', tags: [] };
417419
db.command({ dbStats: true }, { readPreference: mySecondaryPreferred }, function(err) {
418-
test.equal(null, err);
420+
expect(err).to.not.exist;
419421
client.close(done);
420422
});
421423
});
@@ -430,11 +432,11 @@ describe('ReadPreference', function() {
430432
var client = configuration.newClient(configuration.writeConcernMax(), { poolSize: 1 });
431433
client.connect(function(err, client) {
432434
var db = client.db(configuration.db);
433-
test.equal(null, err);
435+
expect(err).to.not.exist;
434436
// Create read preference object.
435437
var mySecondaryPreferred = { mode: 'secondaryPreferred', tags: [] };
436438
db.listCollections({}, { readPreference: mySecondaryPreferred }).toArray(function(err) {
437-
test.equal(null, err);
439+
expect(err).to.not.exist;
438440
client.close(done);
439441
});
440442
});
@@ -449,12 +451,12 @@ describe('ReadPreference', function() {
449451
var client = configuration.newClient(configuration.writeConcernMax(), { poolSize: 1 });
450452
client.connect(function(err, client) {
451453
var db = client.db(configuration.db);
452-
test.equal(null, err);
454+
expect(err).to.not.exist;
453455
// Create read preference object.
454456
var mySecondaryPreferred = { mode: 'secondaryPreferred', tags: [] };
455457
var cursor = db.collection('test').find({}, { readPreference: mySecondaryPreferred });
456458
cursor.toArray(function(err) {
457-
test.equal(null, err);
459+
expect(err).to.not.exist;
458460
client.close(done);
459461
});
460462
});
@@ -492,4 +494,107 @@ describe('ReadPreference', function() {
492494
client.close(done);
493495
});
494496
});
497+
498+
context('hedge', function() {
499+
it('should set hedge using [find option & empty hedge]', {
500+
metadata: { requires: { mongodb: '>=3.6.0' } },
501+
test: withMonitoredClient(['find'], function(client, events, done) {
502+
const rp = new ReadPreference(ReadPreference.SECONDARY, null, { hedge: {} });
503+
client
504+
.db(this.configuration.db)
505+
.collection('test')
506+
.find({}, { readPreference: rp })
507+
.toArray(err => {
508+
expect(err).to.not.exist;
509+
const expected = { mode: ReadPreference.SECONDARY, hedge: {} };
510+
expect(events[0])
511+
.nested.property('command.$readPreference')
512+
.to.deep.equal(expected);
513+
done();
514+
});
515+
})
516+
});
517+
518+
it('should set hedge using [.setReadPreference & empty hedge] ', {
519+
metadata: { requires: { mongodb: '>=3.6.0' } },
520+
test: withMonitoredClient(['find'], function(client, events, done) {
521+
const rp = new ReadPreference(ReadPreference.SECONDARY, null, { hedge: {} });
522+
client
523+
.db(this.configuration.db)
524+
.collection('test')
525+
.find({})
526+
.setReadPreference(rp)
527+
.toArray(err => {
528+
expect(err).to.not.exist;
529+
const expected = { mode: ReadPreference.SECONDARY, hedge: {} };
530+
expect(events[0])
531+
.nested.property('command.$readPreference')
532+
.to.deep.equal(expected);
533+
done();
534+
});
535+
})
536+
});
537+
538+
it('should set hedge using [.setReadPreference & enabled hedge] ', {
539+
metadata: { requires: { mongodb: '>=3.6.0' } },
540+
test: withMonitoredClient(['find'], function(client, events, done) {
541+
const rp = new ReadPreference(ReadPreference.SECONDARY, null, { hedge: { enabled: true } });
542+
client
543+
.db(this.configuration.db)
544+
.collection('test')
545+
.find({})
546+
.setReadPreference(rp)
547+
.toArray(err => {
548+
expect(err).to.not.exist;
549+
const expected = { mode: ReadPreference.SECONDARY, hedge: { enabled: true } };
550+
expect(events[0])
551+
.nested.property('command.$readPreference')
552+
.to.deep.equal(expected);
553+
done();
554+
});
555+
})
556+
});
557+
558+
it('should set hedge using [.setReadPreference & disabled hedge] ', {
559+
metadata: { requires: { mongodb: '>=3.6.0' } },
560+
test: withMonitoredClient(['find'], function(client, events, done) {
561+
const rp = new ReadPreference(ReadPreference.SECONDARY, null, {
562+
hedge: { enabled: false }
563+
});
564+
client
565+
.db(this.configuration.db)
566+
.collection('test')
567+
.find({})
568+
.setReadPreference(rp)
569+
.toArray(err => {
570+
expect(err).to.not.exist;
571+
const expected = { mode: ReadPreference.SECONDARY, hedge: { enabled: false } };
572+
expect(events[0])
573+
.nested.property('command.$readPreference')
574+
.to.deep.equal(expected);
575+
done();
576+
});
577+
})
578+
});
579+
580+
it('should set hedge using [.setReadPreference & undefined hedge] ', {
581+
metadata: { requires: { mongodb: '>=3.6.0' } },
582+
test: withMonitoredClient(['find'], function(client, events, done) {
583+
const rp = new ReadPreference(ReadPreference.SECONDARY, null);
584+
client
585+
.db(this.configuration.db)
586+
.collection('test')
587+
.find({})
588+
.setReadPreference(rp)
589+
.toArray(err => {
590+
expect(err).to.not.exist;
591+
const expected = { mode: ReadPreference.SECONDARY };
592+
expect(events[0])
593+
.nested.property('command.$readPreference')
594+
.to.deep.equal(expected);
595+
done();
596+
});
597+
})
598+
});
599+
});
495600
});

test/functional/shared.js

+20
Original file line numberDiff line numberDiff line change
@@ -192,12 +192,32 @@ class EventCollector {
192192
}
193193
}
194194

195+
function withMonitoredClient(commands, callback) {
196+
if (!Object.prototype.hasOwnProperty.call(callback, 'prototype')) {
197+
throw new Error('withMonitoredClient callback can not be arrow function');
198+
}
199+
return function(done) {
200+
const configuration = this.configuration;
201+
const client = configuration.newClient({ monitorCommands: true });
202+
const events = [];
203+
client.on('commandStarted', filterForCommands(commands, events));
204+
client.connect((err, client) => {
205+
expect(err).to.not.exist;
206+
function _done(err) {
207+
client.close(err2 => done(err || err2));
208+
}
209+
callback.bind(this)(client, events, _done);
210+
});
211+
};
212+
}
213+
195214
module.exports = {
196215
connectToDb,
197216
setupDatabase,
198217
assert,
199218
delay,
200219
withClient,
220+
withMonitoredClient,
201221
filterForCommands,
202222
filterOutCommands,
203223
ignoreNsNotFound,

0 commit comments

Comments
 (0)