Skip to content

Commit 75f445b

Browse files
committed
refactor(topology): use a wait queue for server selection
The server selection loop is better modeled as a queue of requests rather than manually rescheduling timeouts. This makes the loop more readable and eliminates dependencies on SDAM events. NODE-2398
1 parent 0f4ab38 commit 75f445b

File tree

7 files changed

+258
-255
lines changed

7 files changed

+258
-255
lines changed

lib/core/sdam/server_selection.js

Lines changed: 0 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,6 @@ const ServerType = require('./common').ServerType;
33
const TopologyType = require('./common').TopologyType;
44
const ReadPreference = require('../topologies/read_preference');
55
const MongoError = require('../error').MongoError;
6-
const calculateDurationInMs = require('../utils').calculateDurationInMs;
7-
const MongoServerSelectionError = require('../error').MongoServerSelectionError;
8-
9-
const common = require('./common');
10-
const STATE_CLOSED = common.STATE_CLOSED;
11-
const clearAndRemoveTimerFrom = common.clearAndRemoveTimerFrom;
126

137
// max staleness constants
148
const IDLE_WRITE_PERIOD = 10000;
@@ -246,90 +240,7 @@ function readPreferenceServerSelector(readPreference) {
246240
};
247241
}
248242

249-
/**
250-
* Selects servers using the provided selector
251-
*
252-
* @private
253-
* @param {Topology} topology The topology to select servers from
254-
* @param {function} selector The predicate used for selecting servers
255-
* @param {Number} timeout The max time we are willing wait for selection
256-
* @param {Number} start A high precision timestamp for the start of the selection process
257-
* @param {function} callback The callback used to convey errors or the resultant servers
258-
*/
259-
function selectServers(topology, selector, timeout, start, callback) {
260-
const duration = calculateDurationInMs(start);
261-
if (duration >= timeout) {
262-
return callback(
263-
new MongoServerSelectionError(`Server selection timed out after ${timeout} ms`),
264-
topology.description
265-
);
266-
}
267-
268-
// explicitly disallow selection if client is closed
269-
if (topology.s.state === STATE_CLOSED) {
270-
callback(new MongoError('Topology is closed, please connect'));
271-
return;
272-
}
273-
274-
// otherwise, attempt server selection
275-
const serverDescriptions = Array.from(topology.description.servers.values());
276-
let descriptions;
277-
278-
// support server selection by options with readPreference
279-
if (typeof selector === 'object') {
280-
const readPreference = selector.readPreference
281-
? selector.readPreference
282-
: ReadPreference.primary;
283-
284-
selector = readPreferenceServerSelector(readPreference);
285-
}
286-
287-
try {
288-
descriptions = selector
289-
? selector(topology.description, serverDescriptions)
290-
: serverDescriptions;
291-
} catch (e) {
292-
return callback(e, null);
293-
}
294-
295-
if (descriptions.length) {
296-
const servers = descriptions.map(description => topology.s.servers.get(description.address));
297-
return callback(null, servers);
298-
}
299-
300-
const retrySelection = () => {
301-
// ensure all server monitors attempt monitoring soon
302-
topology.s.servers.forEach(server => process.nextTick(() => server.requestCheck()));
303-
304-
const iterationTimer = setTimeout(() => {
305-
topology.removeListener('topologyDescriptionChanged', descriptionChangedHandler);
306-
callback(
307-
new MongoServerSelectionError(
308-
`Server selection timed out after ${timeout} ms`,
309-
topology.description
310-
)
311-
);
312-
}, timeout - duration);
313-
314-
const descriptionChangedHandler = () => {
315-
// successful iteration, clear the check timer
316-
clearAndRemoveTimerFrom(iterationTimer, topology.s.iterationTimers);
317-
318-
// topology description has changed due to monitoring, reattempt server selection
319-
selectServers(topology, selector, timeout, start, callback);
320-
};
321-
322-
// track this timer in case we need to clean it up outside this loop
323-
topology.s.iterationTimers.add(iterationTimer);
324-
325-
topology.once('topologyDescriptionChanged', descriptionChangedHandler);
326-
};
327-
328-
retrySelection();
329-
}
330-
331243
module.exports = {
332-
selectServers,
333244
writableServerSelector,
334245
readPreferenceServerSelector
335246
};

lib/core/sdam/topology.js

Lines changed: 104 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
'use strict';
2+
const Denque = require('denque');
23
const EventEmitter = require('events');
34
const ServerDescription = require('./server_description').ServerDescription;
45
const ServerType = require('./common').ServerType;
@@ -18,6 +19,7 @@ const isNodeShuttingDownError = require('../error').isNodeShuttingDownError;
1819
const maxWireVersion = require('../utils').maxWireVersion;
1920
const ClientSession = require('../sessions').ClientSession;
2021
const MongoError = require('../error').MongoError;
22+
const MongoServerSelectionError = require('../error').MongoServerSelectionError;
2123
const resolveClusterTime = require('../topologies/shared').resolveClusterTime;
2224
const SrvPoller = require('./srv_polling').SrvPoller;
2325
const getMMAPError = require('../topologies/shared').getMMAPError;
@@ -35,7 +37,7 @@ const clearAndRemoveTimerFrom = common.clearAndRemoveTimerFrom;
3537
const serverSelection = require('./server_selection');
3638
const readPreferenceServerSelector = serverSelection.readPreferenceServerSelector;
3739
const writableServerSelector = serverSelection.writableServerSelector;
38-
const selectServers = serverSelection.selectServers;
40+
// const selectServers = serverSelection.selectServers;
3941

4042
// Global state
4143
let globalTopologyCounter = 0;
@@ -74,6 +76,9 @@ const DEPRECATED_OPTIONS = new Set([
7476
'bufferMaxEntries'
7577
]);
7678

79+
const kCancelled = Symbol('cancelled');
80+
const kWaitQueue = Symbol('waitQueue');
81+
7782
/**
7883
* A container of server instances representing a connection to a MongoDB topology.
7984
*
@@ -140,6 +145,7 @@ class Topology extends EventEmitter {
140145
return result;
141146
}, new Map());
142147

148+
this[kWaitQueue] = new Denque();
143149
this.s = {
144150
// the id of this topology
145151
id: topologyId,
@@ -195,7 +201,6 @@ class Topology extends EventEmitter {
195201
clusterTime: null,
196202

197203
// timer management
198-
iterationTimers: new Set(),
199204
connectionTimers: new Set()
200205
};
201206

@@ -335,8 +340,7 @@ class Topology extends EventEmitter {
335340
return;
336341
}
337342

338-
// clear all existing monitor timers
339-
drainTimerQueue(this.s.iterationTimers);
343+
drainWaitQueue(this[kWaitQueue], new MongoError('Topology closed'));
340344
drainTimerQueue(this.s.connectionTimers);
341345

342346
if (this.s.srvPoller) {
@@ -416,26 +420,43 @@ class Topology extends EventEmitter {
416420
const transaction = session && session.transaction;
417421

418422
if (isSharded && transaction && transaction.server) {
419-
callback(null, transaction.server);
423+
callback(undefined, transaction.server);
420424
return;
421425
}
422426

423-
selectServers(
424-
this,
425-
selector,
426-
options.serverSelectionTimeoutMS,
427-
process.hrtime(),
428-
(err, servers) => {
429-
if (err) return callback(err);
430-
431-
const selectedServer = randomSelection(servers);
432-
if (isSharded && transaction && transaction.isActive) {
433-
transaction.pinServer(selectedServer);
434-
}
427+
// support server selection by options with readPreference
428+
let serverSelector = selector;
429+
if (typeof selector === 'object') {
430+
const readPreference = selector.readPreference
431+
? selector.readPreference
432+
: ReadPreference.primary;
435433

436-
callback(null, selectedServer);
437-
}
438-
);
434+
serverSelector = readPreferenceServerSelector(readPreference);
435+
}
436+
437+
const waitQueueMember = {
438+
serverSelector,
439+
transaction,
440+
callback
441+
};
442+
443+
const serverSelectionTimeoutMS = options.serverSelectionTimeoutMS;
444+
if (serverSelectionTimeoutMS) {
445+
waitQueueMember.timer = setTimeout(() => {
446+
waitQueueMember[kCancelled] = true;
447+
waitQueueMember.timer = undefined;
448+
const timeoutError = new MongoServerSelectionError(
449+
`Server selection timed out after ${serverSelectionTimeoutMS} ms`,
450+
this.description
451+
);
452+
453+
waitQueueMember.callback(timeoutError);
454+
}, serverSelectionTimeoutMS);
455+
}
456+
457+
// place the member at the front of the wait queue
458+
this[kWaitQueue].unshift(waitQueueMember);
459+
processWaitQueue(this);
439460
}
440461

441462
// Sessions related methods
@@ -545,6 +566,11 @@ class Topology extends EventEmitter {
545566
// update server list from updated descriptions
546567
updateServers(this, serverDescription);
547568

569+
// attempt to resolve any outstanding server selection attempts
570+
if (this[kWaitQueue].length > 0) {
571+
processWaitQueue(this);
572+
}
573+
548574
this.emit(
549575
'topologyDescriptionChanged',
550576
new events.TopologyDescriptionChangedEvent(
@@ -1012,6 +1038,64 @@ function srvPollingHandler(topology) {
10121038
};
10131039
}
10141040

1041+
function drainWaitQueue(queue, err) {
1042+
while (queue.length) {
1043+
const waitQueueMember = queue.pop();
1044+
clearTimeout(waitQueueMember.timer);
1045+
if (!waitQueueMember[kCancelled]) {
1046+
waitQueueMember.callback(err);
1047+
}
1048+
}
1049+
}
1050+
1051+
function processWaitQueue(topology) {
1052+
if (topology.s.state === STATE_CLOSED) {
1053+
drainWaitQueue(topology[kWaitQueue], new MongoError('Topology is closed, please connect'));
1054+
return;
1055+
}
1056+
1057+
const isSharded = topology.description.type === TopologyType.Sharded;
1058+
const serverDescriptions = Array.from(topology.description.servers.values());
1059+
for (let i = 0; i < topology[kWaitQueue].length; ++i) {
1060+
const waitQueueMember = topology[kWaitQueue].shift();
1061+
if (waitQueueMember[kCancelled]) {
1062+
continue;
1063+
}
1064+
1065+
let selectedDescriptions;
1066+
try {
1067+
const serverSelector = waitQueueMember.serverSelector;
1068+
selectedDescriptions = serverSelector
1069+
? serverSelector(topology.description, serverDescriptions)
1070+
: serverDescriptions;
1071+
} catch (e) {
1072+
clearTimeout(waitQueueMember.timer);
1073+
waitQueueMember.callback(e);
1074+
break;
1075+
}
1076+
1077+
if (selectedDescriptions.length === 0) {
1078+
topology[kWaitQueue].push(waitQueueMember);
1079+
break;
1080+
}
1081+
1082+
const selectedServerDescription = randomSelection(selectedDescriptions);
1083+
const selectedServer = topology.s.servers.get(selectedServerDescription.address);
1084+
const transaction = waitQueueMember.transaction;
1085+
if (isSharded && transaction && transaction.isActive) {
1086+
transaction.pinServer(selectedServer);
1087+
}
1088+
1089+
clearTimeout(waitQueueMember.timer);
1090+
waitQueueMember.callback(undefined, selectedServer);
1091+
}
1092+
1093+
if (topology[kWaitQueue].length > 0) {
1094+
// ensure all server monitors attempt monitoring soon
1095+
topology.s.servers.forEach(server => process.nextTick(() => server.requestCheck()));
1096+
}
1097+
}
1098+
10151099
/**
10161100
* A server opening SDAM monitoring event
10171101
*

test/functional/scram_sha_256.test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ describe('SCRAM-SHA-256 auth', function() {
180180
},
181181
authSource: this.configuration.db,
182182
authMechanism: 'SCRAM-SHA-1',
183-
serverSelectionTimeoutMS: 2000
183+
serverSelectionTimeoutMS: 100
184184
};
185185

186186
return withClient(
@@ -204,7 +204,7 @@ describe('SCRAM-SHA-256 auth', function() {
204204
password: 'pencil'
205205
},
206206
authSource: 'admin',
207-
serverSelectionTimeoutMS: 2000
207+
serverSelectionTimeoutMS: 100
208208
};
209209

210210
const badPasswordOptions = {
@@ -213,7 +213,7 @@ describe('SCRAM-SHA-256 auth', function() {
213213
password: 'pencil'
214214
},
215215
authSource: 'admin',
216-
serverSelectionTimeoutMS: 2000
216+
serverSelectionTimeoutMS: 100
217217
};
218218

219219
const getErrorMsg = options =>

0 commit comments

Comments
 (0)