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.
- package/lib/bson.js +33 -22
- package/lib/bson.js.map +1 -1
- package/lib/cmap/wire_protocol/on_demand/document.js +207 -0
- package/lib/cmap/wire_protocol/on_demand/document.js.map +1 -0
- package/lib/sdam/monitor.js +136 -43
- package/lib/sdam/monitor.js.map +1 -1
- package/lib/sdam/server.js +2 -8
- package/lib/sdam/server.js.map +1 -1
- package/lib/sdam/server_description.js +1 -0
- package/lib/sdam/server_description.js.map +1 -1
- package/lib/sdam/server_selection.js +1 -1
- package/lib/sdam/server_selection.js.map +1 -1
- package/mongodb.d.ts +4 -2
- package/package.json +2 -2
- package/src/bson.ts +13 -0
- package/src/cmap/wire_protocol/on_demand/document.ts +322 -0
- package/src/index.ts +1 -0
- package/src/sdam/monitor.ts +175 -62
- package/src/sdam/server.ts +2 -10
- package/src/sdam/server_description.ts +6 -1
- package/src/sdam/server_selection.ts +2 -3
package/src/sdam/monitor.ts
CHANGED
|
@@ -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
|
|
12
|
-
|
|
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.
|
|
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
|
-
|
|
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(
|
|
496
|
+
constructor(monitor: Monitor) {
|
|
467
497
|
this.connection = undefined;
|
|
468
|
-
this[kCancellationToken] =
|
|
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(
|
|
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
|
|
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
|
-
|
|
499
|
-
if (
|
|
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 (
|
|
505
|
-
|
|
532
|
+
if (this.connection == null) {
|
|
533
|
+
this.connection = conn;
|
|
506
534
|
}
|
|
507
535
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
() => measureRoundTripTime(
|
|
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
|
-
|
|
516
|
-
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
measureAndReschedule(connection);
|
|
521
|
-
},
|
|
567
|
+
connection.command(ns('admin.$cmd'), { [commandName]: 1 }, undefined).then(
|
|
568
|
+
() => this.measureAndReschedule(),
|
|
522
569
|
() => {
|
|
523
|
-
|
|
524
|
-
|
|
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
|
+
}
|
package/src/sdam/server.ts
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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;
|