mongodb 7.0.0-dev.20251218.sha.f0af829f → 7.0.0-dev.20251219.sha.a4211e77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { MongoInvalidArgumentError } from '../error';
1
+ import { MongoInvalidArgumentError, MongoRuntimeError } from '../error';
2
2
  import { ReadPreference } from '../read_preference';
3
3
  import { ServerType, TopologyType } from './common';
4
4
  import type { ServerDescription, TagSet } from './server_description';
@@ -15,21 +15,52 @@ export const MIN_SECONDARY_WRITE_WIRE_VERSION = 13;
15
15
  export type ServerSelector = (
16
16
  topologyDescription: TopologyDescription,
17
17
  servers: ServerDescription[],
18
- deprioritized?: ServerDescription[]
18
+ deprioritized: DeprioritizedServers
19
19
  ) => ServerDescription[];
20
20
 
21
+ /** @internal */
22
+ export class DeprioritizedServers {
23
+ private deprioritized: Set<string> = new Set();
24
+
25
+ constructor(descriptions?: Iterable<ServerDescription>) {
26
+ for (const description of descriptions ?? []) {
27
+ this.add(description);
28
+ }
29
+ }
30
+
31
+ add({ address }: ServerDescription) {
32
+ this.deprioritized.add(address);
33
+ }
34
+
35
+ has({ address }: ServerDescription): boolean {
36
+ return this.deprioritized.has(address);
37
+ }
38
+ }
39
+
40
+ function filterDeprioritized(
41
+ candidates: ServerDescription[],
42
+ deprioritized: DeprioritizedServers
43
+ ): ServerDescription[] {
44
+ const filtered = candidates.filter(candidate => !deprioritized.has(candidate));
45
+
46
+ return filtered.length ? filtered : candidates;
47
+ }
48
+
21
49
  /**
22
50
  * Returns a server selector that selects for writable servers
23
51
  */
24
52
  export function writableServerSelector(): ServerSelector {
25
53
  return function writableServer(
26
54
  topologyDescription: TopologyDescription,
27
- servers: ServerDescription[]
55
+ servers: ServerDescription[],
56
+ deprioritized: DeprioritizedServers
28
57
  ): ServerDescription[] {
29
- return latencyWindowReducer(
30
- topologyDescription,
31
- servers.filter((s: ServerDescription) => s.isWritable)
58
+ const eligibleServers = filterDeprioritized(
59
+ servers.filter(({ isWritable }) => isWritable),
60
+ deprioritized
32
61
  );
62
+
63
+ return latencyWindowReducer(topologyDescription, eligibleServers);
33
64
  };
34
65
  }
35
66
 
@@ -39,8 +70,9 @@ export function writableServerSelector(): ServerSelector {
39
70
  */
40
71
  export function sameServerSelector(description?: ServerDescription): ServerSelector {
41
72
  return function sameServerSelector(
42
- topologyDescription: TopologyDescription,
43
- servers: ServerDescription[]
73
+ _topologyDescription: TopologyDescription,
74
+ servers: ServerDescription[],
75
+ _deprioritized: DeprioritizedServers
44
76
  ): ServerDescription[] {
45
77
  if (!description) return [];
46
78
  // Filter the servers to match the provided description only if
@@ -113,7 +145,7 @@ function maxStalenessReducer(
113
145
  primaryFilter
114
146
  )[0];
115
147
 
116
- return servers.reduce((result: ServerDescription[], server: ServerDescription) => {
148
+ return servers.filter((server: ServerDescription) => {
117
149
  const stalenessMS =
118
150
  server.lastUpdateTime -
119
151
  server.lastWriteDate -
@@ -122,12 +154,8 @@ function maxStalenessReducer(
122
154
 
123
155
  const staleness = stalenessMS / 1000;
124
156
  const maxStalenessSeconds = readPreference.maxStalenessSeconds ?? 0;
125
- if (staleness <= maxStalenessSeconds) {
126
- result.push(server);
127
- }
128
-
129
- return result;
130
- }, []);
157
+ return staleness <= maxStalenessSeconds;
158
+ });
131
159
  }
132
160
 
133
161
  if (topologyDescription.type === TopologyType.ReplicaSetNoPrimary) {
@@ -139,40 +167,38 @@ function maxStalenessReducer(
139
167
  s.lastWriteDate > max.lastWriteDate ? s : max
140
168
  );
141
169
 
142
- return servers.reduce((result: ServerDescription[], server: ServerDescription) => {
170
+ return servers.filter((server: ServerDescription) => {
143
171
  const stalenessMS =
144
172
  sMax.lastWriteDate - server.lastWriteDate + topologyDescription.heartbeatFrequencyMS;
145
173
 
146
174
  const staleness = stalenessMS / 1000;
147
175
  const maxStalenessSeconds = readPreference.maxStalenessSeconds ?? 0;
148
- if (staleness <= maxStalenessSeconds) {
149
- result.push(server);
150
- }
151
-
152
- return result;
153
- }, []);
176
+ return staleness <= maxStalenessSeconds;
177
+ });
154
178
  }
155
179
 
156
180
  return servers;
157
181
  }
158
182
 
159
183
  /**
160
- * Determines whether a server's tags match a given set of tags
184
+ * Determines whether a server's tags match a given set of tags.
185
+ *
186
+ * A tagset matches the server's tags if every k-v pair in the tagset
187
+ * is also in the server's tagset.
188
+ *
189
+ * Note that this does not requires that every k-v pair in the server's tagset is also
190
+ * in the client's tagset. The server's tagset is required only to be a superset of the
191
+ * client's tags.
192
+ *
193
+ * @see https://github.com/mongodb/specifications/blob/master/source/server-selection/server-selection.md#tag_sets
161
194
  *
162
195
  * @param tagSet - The requested tag set to match
163
196
  * @param serverTags - The server's tags
164
197
  */
165
198
  function tagSetMatch(tagSet: TagSet, serverTags: TagSet) {
166
- const keys = Object.keys(tagSet);
167
- const serverTagKeys = Object.keys(serverTags);
168
- for (let i = 0; i < keys.length; ++i) {
169
- const key = keys[i];
170
- if (serverTagKeys.indexOf(key) === -1 || serverTags[key] !== tagSet[key]) {
171
- return false;
172
- }
173
- }
174
-
175
- return true;
199
+ return Object.entries(tagSet).every(
200
+ ([key, value]) => serverTags[key] != null && serverTags[key] === value
201
+ );
176
202
  }
177
203
 
178
204
  /**
@@ -183,24 +209,17 @@ function tagSetMatch(tagSet: TagSet, serverTags: TagSet) {
183
209
  * @returns The list of servers matching the requested tags
184
210
  */
185
211
  function tagSetReducer(
186
- readPreference: ReadPreference,
212
+ { tags }: ReadPreference,
187
213
  servers: ServerDescription[]
188
214
  ): ServerDescription[] {
189
- if (
190
- readPreference.tags == null ||
191
- (Array.isArray(readPreference.tags) && readPreference.tags.length === 0)
192
- ) {
215
+ if (tags == null || tags.length === 0) {
216
+ // empty tag sets match all servers
193
217
  return servers;
194
218
  }
195
219
 
196
- for (let i = 0; i < readPreference.tags.length; ++i) {
197
- const tagSet = readPreference.tags[i];
198
- const serversMatchingTagset = servers.reduce(
199
- (matched: ServerDescription[], server: ServerDescription) => {
200
- if (tagSetMatch(tagSet, server.tags)) matched.push(server);
201
- return matched;
202
- },
203
- []
220
+ for (const tagSet of tags) {
221
+ const serversMatchingTagset = servers.filter((s: ServerDescription) =>
222
+ tagSetMatch(tagSet, s.tags)
204
223
  );
205
224
 
206
225
  if (serversMatchingTagset.length) {
@@ -231,10 +250,7 @@ function latencyWindowReducer(
231
250
  );
232
251
 
233
252
  const high = low + topologyDescription.localThresholdMS;
234
- return servers.reduce((result: ServerDescription[], server: ServerDescription) => {
235
- if (server.roundTripTime <= high && server.roundTripTime >= low) result.push(server);
236
- return result;
237
- }, []);
253
+ return servers.filter(server => server.roundTripTime <= high && server.roundTripTime >= low);
238
254
  }
239
255
 
240
256
  // filters
@@ -258,6 +274,107 @@ function loadBalancerFilter(server: ServerDescription): boolean {
258
274
  return server.type === ServerType.LoadBalancer;
259
275
  }
260
276
 
277
+ function isDeprioritizedFactory(
278
+ deprioritized: DeprioritizedServers
279
+ ): (server: ServerDescription) => boolean {
280
+ return server =>
281
+ // if any deprioritized servers equal the server, here we are.
282
+ !deprioritized.has(server);
283
+ }
284
+
285
+ function secondarySelector(
286
+ readPreference: ReadPreference,
287
+ topologyDescription: TopologyDescription,
288
+ servers: ServerDescription[],
289
+ deprioritized: DeprioritizedServers
290
+ ) {
291
+ const mode = readPreference.mode;
292
+ switch (mode) {
293
+ case 'primary':
294
+ // Note: no need to filter for deprioritized servers. A replica set has only one primary; that means that
295
+ // we are in one of two scenarios:
296
+ // 1. deprioritized servers is empty - return the primary.
297
+ // 2. deprioritized servers contains the primary - return the primary.
298
+ return servers.filter(primaryFilter);
299
+ case 'primaryPreferred': {
300
+ const primary = servers.filter(primaryFilter);
301
+
302
+ // If there is a primary and it is not deprioritized, use the primary. Otherwise,
303
+ // check for secondaries.
304
+ const eligiblePrimary = primary.filter(isDeprioritizedFactory(deprioritized));
305
+ if (eligiblePrimary.length) {
306
+ return eligiblePrimary;
307
+ }
308
+
309
+ // If we make it here, we either have:
310
+ // 1. a deprioritized primary
311
+ // 2. no eligible primary
312
+ // secondaries take precedence of deprioritized primaries.
313
+ const secondaries = tagSetReducer(
314
+ readPreference,
315
+ maxStalenessReducer(readPreference, topologyDescription, servers.filter(secondaryFilter))
316
+ );
317
+
318
+ const eligibleSecondaries = secondaries.filter(isDeprioritizedFactory(deprioritized));
319
+ if (eligibleSecondaries.length) {
320
+ return latencyWindowReducer(topologyDescription, eligibleSecondaries);
321
+ }
322
+
323
+ // if we make it here, we have no primaries or secondaries that not deprioritized.
324
+ // prefer the primary (which may not exist, if the topology has no primary).
325
+ // otherwise, return the secondaries (which also may not exist, but there is nothing else to check here).
326
+ return primary.length ? primary : latencyWindowReducer(topologyDescription, secondaries);
327
+ }
328
+ case 'nearest': {
329
+ const eligible = filterDeprioritized(
330
+ tagSetReducer(
331
+ readPreference,
332
+ maxStalenessReducer(readPreference, topologyDescription, servers.filter(nearestFilter))
333
+ ),
334
+ deprioritized
335
+ );
336
+ return latencyWindowReducer(topologyDescription, eligible);
337
+ }
338
+ case 'secondary':
339
+ case 'secondaryPreferred': {
340
+ const secondaries = tagSetReducer(
341
+ readPreference,
342
+ maxStalenessReducer(readPreference, topologyDescription, servers.filter(secondaryFilter))
343
+ );
344
+ const eligibleSecondaries = secondaries.filter(isDeprioritizedFactory(deprioritized));
345
+
346
+ if (eligibleSecondaries.length) {
347
+ return latencyWindowReducer(topologyDescription, eligibleSecondaries);
348
+ }
349
+
350
+ // we have no eligible secondaries, try for a primary if we can.
351
+ if (mode === ReadPreference.SECONDARY_PREFERRED) {
352
+ const primary = servers.filter(primaryFilter);
353
+
354
+ // unlike readPreference=primary, here we do filter for deprioritized servers.
355
+ // if the primary is deprioritized, deprioritized secondaries take precedence.
356
+ const eligiblePrimary = primary.filter(isDeprioritizedFactory(deprioritized));
357
+ if (eligiblePrimary.length) return eligiblePrimary;
358
+
359
+ // we have no eligible primary nor secondaries that have not been deprioritized
360
+ return secondaries.length
361
+ ? latencyWindowReducer(topologyDescription, secondaries)
362
+ : primary;
363
+ }
364
+
365
+ // return all secondaries in the latency window.
366
+ return latencyWindowReducer(topologyDescription, secondaries);
367
+ }
368
+
369
+ default: {
370
+ const _exhaustiveCheck: never = mode;
371
+ throw new MongoRuntimeError(
372
+ `unexpected readPreference=${mode} (should never happen). Please report a bug in the Node driver Jira project.`
373
+ );
374
+ }
375
+ }
376
+ }
377
+
261
378
  /**
262
379
  * Returns a function which selects servers based on a provided read preference
263
380
  *
@@ -271,53 +388,28 @@ export function readPreferenceServerSelector(readPreference: ReadPreference): Se
271
388
  return function readPreferenceServers(
272
389
  topologyDescription: TopologyDescription,
273
390
  servers: ServerDescription[],
274
- deprioritized: ServerDescription[] = []
391
+ deprioritized: DeprioritizedServers
275
392
  ): ServerDescription[] {
276
- if (topologyDescription.type === TopologyType.LoadBalanced) {
277
- return servers.filter(loadBalancerFilter);
278
- }
279
-
280
- if (topologyDescription.type === TopologyType.Unknown) {
281
- return [];
282
- }
283
-
284
- if (topologyDescription.type === TopologyType.Single) {
285
- return latencyWindowReducer(topologyDescription, servers.filter(knownFilter));
286
- }
287
-
288
- if (topologyDescription.type === TopologyType.Sharded) {
289
- const filtered = servers.filter(server => {
290
- return !deprioritized.includes(server);
291
- });
292
- const selectable = filtered.length > 0 ? filtered : deprioritized;
293
- return latencyWindowReducer(topologyDescription, selectable.filter(knownFilter));
294
- }
295
-
296
- const mode = readPreference.mode;
297
- if (mode === ReadPreference.PRIMARY) {
298
- return servers.filter(primaryFilter);
299
- }
300
-
301
- if (mode === ReadPreference.PRIMARY_PREFERRED) {
302
- const result = servers.filter(primaryFilter);
303
- if (result.length) {
304
- return result;
393
+ switch (topologyDescription.type) {
394
+ case 'Single':
395
+ return latencyWindowReducer(topologyDescription, servers.filter(knownFilter));
396
+ case 'ReplicaSetNoPrimary':
397
+ case 'ReplicaSetWithPrimary':
398
+ return secondarySelector(readPreference, topologyDescription, servers, deprioritized);
399
+ case 'Sharded': {
400
+ const selectable = filterDeprioritized(servers, deprioritized);
401
+ return latencyWindowReducer(topologyDescription, selectable.filter(knownFilter));
402
+ }
403
+ case 'Unknown':
404
+ return [];
405
+ case 'LoadBalanced':
406
+ return servers.filter(loadBalancerFilter);
407
+ default: {
408
+ const _exhaustiveCheck: never = topologyDescription.type;
409
+ throw new MongoRuntimeError(
410
+ `unexpected topology type: ${topologyDescription.type} (this should never happen). Please file a bug in the Node driver Jira project.`
411
+ );
305
412
  }
306
413
  }
307
-
308
- const filter = mode === ReadPreference.NEAREST ? nearestFilter : secondaryFilter;
309
- const selectedServers = latencyWindowReducer(
310
- topologyDescription,
311
- tagSetReducer(
312
- readPreference,
313
- maxStalenessReducer(readPreference, topologyDescription, servers.filter(filter))
314
- )
315
- );
316
-
317
- if (mode === ReadPreference.SECONDARY_PREFERRED && selectedServers.length === 0) {
318
- return servers.filter(primaryFilter);
319
- }
320
-
321
- return selectedServers;
322
414
  };
323
415
  }
@@ -70,7 +70,11 @@ import {
70
70
  import type { ServerMonitoringMode } from './monitor';
71
71
  import { Server, type ServerEvents, type ServerOptions } from './server';
72
72
  import { compareTopologyVersion, ServerDescription } from './server_description';
73
- import { readPreferenceServerSelector, type ServerSelector } from './server_selection';
73
+ import {
74
+ DeprioritizedServers,
75
+ readPreferenceServerSelector,
76
+ type ServerSelector
77
+ } from './server_selection';
74
78
  import {
75
79
  ServerSelectionFailedEvent,
76
80
  ServerSelectionStartedEvent,
@@ -105,7 +109,7 @@ export interface ServerSelectionRequest {
105
109
  cancelled: boolean;
106
110
  operationName: string;
107
111
  waitingLogged: boolean;
108
- previousServer?: ServerDescription;
112
+ deprioritizedServers: DeprioritizedServers;
109
113
  }
110
114
 
111
115
  /** @internal */
@@ -169,7 +173,9 @@ export interface SelectServerOptions {
169
173
  serverSelectionTimeoutMS?: number;
170
174
  session?: ClientSession;
171
175
  operationName: string;
172
- previousServer?: ServerDescription;
176
+
177
+ /** @internal */
178
+ deprioritizedServers: DeprioritizedServers;
173
179
  /**
174
180
  * @internal
175
181
  * TODO(NODE-6496): Make this required by making ChangeStream use LegacyTimeoutContext
@@ -455,7 +461,8 @@ export class Topology extends TypedEventEmitter<TopologyEvents> {
455
461
  const selectServerOptions = {
456
462
  operationName: 'handshake',
457
463
  ...options,
458
- timeoutContext
464
+ timeoutContext,
465
+ deprioritizedServers: new DeprioritizedServers()
459
466
  };
460
467
 
461
468
  try {
@@ -605,7 +612,7 @@ export class Topology extends TypedEventEmitter<TopologyEvents> {
605
612
  startTime: processTimeMS(),
606
613
  operationName: options.operationName,
607
614
  waitingLogged: false,
608
- previousServer: options.previousServer
615
+ deprioritizedServers: options.deprioritizedServers
609
616
  };
610
617
 
611
618
  const abortListener = addAbortListener(options.signal, function () {
@@ -957,13 +964,9 @@ function processWaitQueue(topology: Topology) {
957
964
  let selectedDescriptions;
958
965
  try {
959
966
  const serverSelector = waitQueueMember.serverSelector;
960
- const previousServer = waitQueueMember.previousServer;
967
+ const deprioritizedServers = waitQueueMember.deprioritizedServers;
961
968
  selectedDescriptions = serverSelector
962
- ? serverSelector(
963
- topology.description,
964
- serverDescriptions,
965
- previousServer ? [previousServer] : []
966
- )
969
+ ? serverSelector(topology.description, serverDescriptions, deprioritizedServers)
967
970
  : serverDescriptions;
968
971
  } catch (selectorError) {
969
972
  if (