mongodb 6.5.0-dev.20240328.sha.458cf6d → 6.5.0-dev.20240404.sha.0e3d6ea

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.
@@ -8,8 +8,14 @@ import { LEGACY_HELLO_COMMAND } from '../constants';
8
8
  import { MongoError, MongoErrorLabel, MongoNetworkTimeoutError } from '../error';
9
9
  import { MongoLoggableComponent } from '../mongo_logger';
10
10
  import { CancellationToken, TypedEventEmitter } from '../mongo_types';
11
- import type { Callback, EventEmitterWithState } from '../utils';
12
- import { calculateDurationInMs, makeStateMachine, now, ns } from '../utils';
11
+ import {
12
+ calculateDurationInMs,
13
+ type Callback,
14
+ type EventEmitterWithState,
15
+ makeStateMachine,
16
+ now,
17
+ ns
18
+ } from '../utils';
13
19
  import { ServerType, STATE_CLOSED, STATE_CLOSING } from './common';
14
20
  import {
15
21
  ServerHeartbeatFailedEvent,
@@ -25,8 +31,6 @@ const kServer = Symbol('server');
25
31
  const kMonitorId = Symbol('monitorId');
26
32
  /** @internal */
27
33
  const kCancellationToken = Symbol('cancellationToken');
28
- /** @internal */
29
- const kRoundTripTime = Symbol('roundTripTime');
30
34
 
31
35
  const STATE_IDLE = 'idle';
32
36
  const STATE_MONITORING = 'monitoring';
@@ -100,6 +104,8 @@ export class Monitor extends TypedEventEmitter<MonitorEvents> {
100
104
  rttPinger?: RTTPinger;
101
105
  /** @internal */
102
106
  override component = MongoLoggableComponent.TOPOLOGY;
107
+ /** @internal */
108
+ private rttSampler: RTTSampler;
103
109
 
104
110
  constructor(server: Server, options: MonitorOptions) {
105
111
  super();
@@ -121,6 +127,7 @@ export class Monitor extends TypedEventEmitter<MonitorEvents> {
121
127
  });
122
128
  this.isRunningInFaasEnv = getFAASEnv() != null;
123
129
  this.mongoLogger = this[kServer].topology.client?.mongoLogger;
130
+ this.rttSampler = new RTTSampler(10);
124
131
 
125
132
  const cancellationToken = this[kCancellationToken];
126
133
  // TODO: refactor this to pull it directly from the pool, requires new ConnectionPool integration
@@ -203,6 +210,26 @@ export class Monitor extends TypedEventEmitter<MonitorEvents> {
203
210
  this.emit('close');
204
211
  stateTransition(this, STATE_CLOSED);
205
212
  }
213
+
214
+ get roundTripTime(): number {
215
+ return this.rttSampler.average();
216
+ }
217
+
218
+ get minRoundTripTime(): number {
219
+ return this.rttSampler.min();
220
+ }
221
+
222
+ get latestRtt(): number {
223
+ return this.rttSampler.last ?? 0; // FIXME: Check if this is acceptable
224
+ }
225
+
226
+ addRttSample(rtt: number) {
227
+ this.rttSampler.addSample(rtt);
228
+ }
229
+
230
+ clearRttSamples() {
231
+ this.rttSampler.clear();
232
+ }
206
233
  }
207
234
 
208
235
  function resetMonitorState(monitor: Monitor) {
@@ -216,6 +243,8 @@ function resetMonitorState(monitor: Monitor) {
216
243
 
217
244
  monitor.connection?.destroy();
218
245
  monitor.connection = null;
246
+
247
+ monitor.clearRttSamples();
219
248
  }
220
249
 
221
250
  function useStreamingProtocol(monitor: Monitor, topologyVersion: TopologyVersion | null): boolean {
@@ -249,7 +278,6 @@ function checkServer(monitor: Monitor, callback: Callback<Document | null>) {
249
278
  function onHeartbeatFailed(err: Error) {
250
279
  monitor.connection?.destroy();
251
280
  monitor.connection = null;
252
-
253
281
  monitor.emitAndLogHeartbeat(
254
282
  Server.SERVER_HEARTBEAT_FAILED,
255
283
  monitor[kServer].topology.s.id,
@@ -275,11 +303,15 @@ function checkServer(monitor: Monitor, callback: Callback<Document | null>) {
275
303
  hello.isWritablePrimary = hello[LEGACY_HELLO_COMMAND];
276
304
  }
277
305
 
306
+ // NOTE: here we use the latestRtt as this measurement corresponds with the value
307
+ // obtained for this successful heartbeat
278
308
  const duration =
279
309
  isAwaitable && monitor.rttPinger
280
- ? monitor.rttPinger.roundTripTime
310
+ ? monitor.rttPinger.latestRtt ?? calculateDurationInMs(start)
281
311
  : calculateDurationInMs(start);
282
312
 
313
+ monitor.addRttSample(duration);
314
+
283
315
  monitor.emitAndLogHeartbeat(
284
316
  Server.SERVER_HEARTBEAT_SUCCEEDED,
285
317
  monitor[kServer].topology.s.id,
@@ -328,13 +360,7 @@ function checkServer(monitor: Monitor, callback: Callback<Document | null>) {
328
360
  : { socketTimeoutMS: connectTimeoutMS };
329
361
 
330
362
  if (isAwaitable && monitor.rttPinger == null) {
331
- monitor.rttPinger = new RTTPinger(
332
- monitor[kCancellationToken],
333
- Object.assign(
334
- { heartbeatFrequencyMS: monitor.options.heartbeatFrequencyMS },
335
- monitor.connectOptions
336
- )
337
- );
363
+ monitor.rttPinger = new RTTPinger(monitor);
338
364
  }
339
365
 
340
366
  // Record new start time before sending handshake
@@ -377,6 +403,8 @@ function checkServer(monitor: Monitor, callback: Callback<Document | null>) {
377
403
  connection.destroy();
378
404
  return;
379
405
  }
406
+ const duration = calculateDurationInMs(start);
407
+ monitor.addRttSample(duration);
380
408
 
381
409
  monitor.connection = connection;
382
410
  monitor.emitAndLogHeartbeat(
@@ -385,7 +413,7 @@ function checkServer(monitor: Monitor, callback: Callback<Document | null>) {
385
413
  connection.hello?.connectionId,
386
414
  new ServerHeartbeatSucceededEvent(
387
415
  monitor.address,
388
- calculateDurationInMs(start),
416
+ duration,
389
417
  connection.hello,
390
418
  useStreamingProtocol(monitor, connection.hello?.topologyVersion)
391
419
  )
@@ -458,23 +486,30 @@ export class RTTPinger {
458
486
  /** @internal */
459
487
  [kCancellationToken]: CancellationToken;
460
488
  /** @internal */
461
- [kRoundTripTime]: number;
462
- /** @internal */
463
489
  [kMonitorId]: NodeJS.Timeout;
490
+ /** @internal */
491
+ monitor: Monitor;
464
492
  closed: boolean;
493
+ /** @internal */
494
+ latestRtt?: number;
465
495
 
466
- constructor(cancellationToken: CancellationToken, options: RTTPingerOptions) {
496
+ constructor(monitor: Monitor) {
467
497
  this.connection = undefined;
468
- this[kCancellationToken] = cancellationToken;
469
- this[kRoundTripTime] = 0;
498
+ this[kCancellationToken] = monitor[kCancellationToken];
470
499
  this.closed = false;
500
+ this.monitor = monitor;
501
+ this.latestRtt = monitor.latestRtt;
471
502
 
472
- const heartbeatFrequencyMS = options.heartbeatFrequencyMS;
473
- this[kMonitorId] = setTimeout(() => measureRoundTripTime(this, options), heartbeatFrequencyMS);
503
+ const heartbeatFrequencyMS = monitor.options.heartbeatFrequencyMS;
504
+ this[kMonitorId] = setTimeout(() => this.measureRoundTripTime(), heartbeatFrequencyMS);
474
505
  }
475
506
 
476
507
  get roundTripTime(): number {
477
- return this[kRoundTripTime];
508
+ return this.monitor.roundTripTime;
509
+ }
510
+
511
+ get minRoundTripTime(): number {
512
+ return this.monitor.minRoundTripTime;
478
513
  }
479
514
 
480
515
  close(): void {
@@ -484,61 +519,60 @@ export class RTTPinger {
484
519
  this.connection?.destroy();
485
520
  this.connection = undefined;
486
521
  }
487
- }
488
-
489
- function measureRoundTripTime(rttPinger: RTTPinger, options: RTTPingerOptions) {
490
- const start = now();
491
- options.cancellationToken = rttPinger[kCancellationToken];
492
- const heartbeatFrequencyMS = options.heartbeatFrequencyMS;
493
-
494
- if (rttPinger.closed) {
495
- return;
496
- }
497
522
 
498
- function measureAndReschedule(conn?: Connection) {
499
- if (rttPinger.closed) {
523
+ private measureAndReschedule(start?: number, conn?: Connection) {
524
+ if (start == null) {
525
+ start = now();
526
+ }
527
+ if (this.closed) {
500
528
  conn?.destroy();
501
529
  return;
502
530
  }
503
531
 
504
- if (rttPinger.connection == null) {
505
- rttPinger.connection = conn;
532
+ if (this.connection == null) {
533
+ this.connection = conn;
506
534
  }
507
535
 
508
- rttPinger[kRoundTripTime] = calculateDurationInMs(start);
509
- rttPinger[kMonitorId] = setTimeout(
510
- () => measureRoundTripTime(rttPinger, options),
511
- heartbeatFrequencyMS
536
+ this.latestRtt = calculateDurationInMs(start);
537
+ this[kMonitorId] = setTimeout(
538
+ () => this.measureRoundTripTime(),
539
+ this.monitor.options.heartbeatFrequencyMS
512
540
  );
513
541
  }
514
542
 
515
- const connection = rttPinger.connection;
516
- if (connection == null) {
543
+ private measureRoundTripTime() {
544
+ const start = now();
545
+
546
+ if (this.closed) {
547
+ return;
548
+ }
549
+
550
+ const connection = this.connection;
551
+ if (connection == null) {
552
+ // eslint-disable-next-line github/no-then
553
+ connect(this.monitor.connectOptions).then(
554
+ connection => {
555
+ this.measureAndReschedule(start, connection);
556
+ },
557
+ () => {
558
+ this.connection = undefined;
559
+ }
560
+ );
561
+ return;
562
+ }
563
+
564
+ const commandName =
565
+ connection.serverApi?.version || connection.helloOk ? 'hello' : LEGACY_HELLO_COMMAND;
517
566
  // eslint-disable-next-line github/no-then
518
- connect(options).then(
519
- connection => {
520
- measureAndReschedule(connection);
521
- },
567
+ connection.command(ns('admin.$cmd'), { [commandName]: 1 }, undefined).then(
568
+ () => this.measureAndReschedule(),
522
569
  () => {
523
- rttPinger.connection = undefined;
524
- rttPinger[kRoundTripTime] = 0;
570
+ this.connection?.destroy();
571
+ this.connection = undefined;
572
+ return;
525
573
  }
526
574
  );
527
- return;
528
575
  }
529
-
530
- const commandName =
531
- connection.serverApi?.version || connection.helloOk ? 'hello' : LEGACY_HELLO_COMMAND;
532
- // eslint-disable-next-line github/no-then
533
- connection.command(ns('admin.$cmd'), { [commandName]: 1 }, undefined).then(
534
- () => measureAndReschedule(),
535
- () => {
536
- rttPinger.connection?.destroy();
537
- rttPinger.connection = undefined;
538
- rttPinger[kRoundTripTime] = 0;
539
- return;
540
- }
541
- );
542
576
  }
543
577
 
544
578
  /**
@@ -666,3 +700,82 @@ export class MonitorInterval {
666
700
  });
667
701
  };
668
702
  }
703
+
704
+ /** @internal
705
+ * This class implements the RTT sampling logic specified for [CSOT](https://github.com/mongodb/specifications/blob/bbb335e60cd7ea1e0f7cd9a9443cb95fc9d3b64d/source/client-side-operations-timeout/client-side-operations-timeout.md#drivers-use-minimum-rtt-to-short-circuit-operations)
706
+ *
707
+ * This is implemented as a [circular buffer](https://en.wikipedia.org/wiki/Circular_buffer) keeping
708
+ * the most recent `windowSize` samples
709
+ * */
710
+ export class RTTSampler {
711
+ /** Index of the next slot to be overwritten */
712
+ private writeIndex: number;
713
+ private length: number;
714
+ private rttSamples: Float64Array;
715
+
716
+ constructor(windowSize = 10) {
717
+ this.rttSamples = new Float64Array(windowSize);
718
+ this.length = 0;
719
+ this.writeIndex = 0;
720
+ }
721
+
722
+ /**
723
+ * Adds an rtt sample to the end of the circular buffer
724
+ * When `windowSize` samples have been collected, `addSample` overwrites the least recently added
725
+ * sample
726
+ */
727
+ addSample(sample: number) {
728
+ this.rttSamples[this.writeIndex++] = sample;
729
+ if (this.length < this.rttSamples.length) {
730
+ this.length++;
731
+ }
732
+
733
+ this.writeIndex %= this.rttSamples.length;
734
+ }
735
+
736
+ /**
737
+ * When \< 2 samples have been collected, returns 0
738
+ * Otherwise computes the minimum value samples contained in the buffer
739
+ */
740
+ min(): number {
741
+ if (this.length < 2) return 0;
742
+ let min = this.rttSamples[0];
743
+ for (let i = 1; i < this.length; i++) {
744
+ if (this.rttSamples[i] < min) min = this.rttSamples[i];
745
+ }
746
+
747
+ return min;
748
+ }
749
+
750
+ /**
751
+ * Returns mean of samples contained in the buffer
752
+ */
753
+ average(): number {
754
+ if (this.length === 0) return 0;
755
+ let sum = 0;
756
+ for (let i = 0; i < this.length; i++) {
757
+ sum += this.rttSamples[i];
758
+ }
759
+
760
+ return sum / this.length;
761
+ }
762
+
763
+ /**
764
+ * Returns most recently inserted element in the buffer
765
+ * Returns null if the buffer is empty
766
+ * */
767
+ get last(): number | null {
768
+ if (this.length === 0) return null;
769
+ return this.rttSamples[this.writeIndex === 0 ? this.length - 1 : this.writeIndex - 1];
770
+ }
771
+
772
+ /**
773
+ * Clear the buffer
774
+ * NOTE: this does not overwrite the data held in the internal array, just the pointers into
775
+ * this array
776
+ */
777
+ clear() {
778
+ this.length = 0;
779
+ this.writeIndex = 0;
780
+ }
781
+ }
@@ -175,7 +175,8 @@ export class Server extends TypedEventEmitter<ServerEvents> {
175
175
  this.emit(
176
176
  Server.DESCRIPTION_RECEIVED,
177
177
  new ServerDescription(this.description.hostAddress, event.reply, {
178
- roundTripTime: calculateRoundTripTime(this.description.roundTripTime, event.duration)
178
+ roundTripTime: this.monitor?.roundTripTime,
179
+ minRoundTripTime: this.monitor?.minRoundTripTime
179
180
  })
180
181
  );
181
182
 
@@ -467,15 +468,6 @@ export class Server extends TypedEventEmitter<ServerEvents> {
467
468
  }
468
469
  }
469
470
 
470
- function calculateRoundTripTime(oldRtt: number, duration: number): number {
471
- if (oldRtt === -1) {
472
- return duration;
473
- }
474
-
475
- const alpha = 0.2;
476
- return alpha * duration + (1 - alpha) * oldRtt;
477
- }
478
-
479
471
  function markServerUnknown(server: Server, error?: MongoError) {
480
472
  // Load balancer servers can never be marked unknown.
481
473
  if (server.loadBalanced) {
@@ -33,8 +33,10 @@ export interface ServerDescriptionOptions {
33
33
  /** An Error used for better reporting debugging */
34
34
  error?: MongoError;
35
35
 
36
- /** The round trip time to ping this server (in ms) */
36
+ /** The average round trip time to ping this server (in ms) */
37
37
  roundTripTime?: number;
38
+ /** The minimum round trip time to ping this server over the past 10 samples(in ms) */
39
+ minRoundTripTime?: number;
38
40
 
39
41
  /** If the client is in load balancing mode. */
40
42
  loadBalanced?: boolean;
@@ -58,6 +60,8 @@ export class ServerDescription {
58
60
  minWireVersion: number;
59
61
  maxWireVersion: number;
60
62
  roundTripTime: number;
63
+ /** The minimum measurement of the last 10 measurements of roundTripTime that have been collected */
64
+ minRoundTripTime: number;
61
65
  lastUpdateTime: number;
62
66
  lastWriteDate: number;
63
67
  me: string | null;
@@ -98,6 +102,7 @@ export class ServerDescription {
98
102
  this.minWireVersion = hello?.minWireVersion ?? 0;
99
103
  this.maxWireVersion = hello?.maxWireVersion ?? 0;
100
104
  this.roundTripTime = options?.roundTripTime ?? -1;
105
+ this.minRoundTripTime = options?.minRoundTripTime ?? 0;
101
106
  this.lastUpdateTime = now();
102
107
  this.lastWriteDate = hello?.lastWrite?.lastWriteDate ?? 0;
103
108
  this.error = options.error ?? null;
@@ -223,9 +223,8 @@ function latencyWindowReducer(
223
223
  servers: ServerDescription[]
224
224
  ): ServerDescription[] {
225
225
  const low = servers.reduce(
226
- (min: number, server: ServerDescription) =>
227
- min === -1 ? server.roundTripTime : Math.min(server.roundTripTime, min),
228
- -1
226
+ (min: number, server: ServerDescription) => Math.min(server.roundTripTime, min),
227
+ Infinity
229
228
  );
230
229
 
231
230
  const high = low + topologyDescription.localThresholdMS;