Skip to content

Commit ae4c94a

Browse files
authored
feat(NODE-5197): add server monitoring mode (#3899)
1 parent 08c9fb4 commit ae4c94a

18 files changed

+947
-2201
lines changed

src/connection_string.ts

+12
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
} from './mongo_logger';
3030
import { ReadConcern, type ReadConcernLevel } from './read_concern';
3131
import { ReadPreference, type ReadPreferenceMode } from './read_preference';
32+
import { ServerMonitoringMode } from './sdam/monitor';
3233
import type { TagSet } from './sdam/server_description';
3334
import {
3435
DEFAULT_PK_FACTORY,
@@ -1055,6 +1056,17 @@ export const OPTIONS = {
10551056
serializeFunctions: {
10561057
type: 'boolean'
10571058
},
1059+
serverMonitoringMode: {
1060+
default: 'auto',
1061+
transform({ values: [value] }) {
1062+
if (!Object.values(ServerMonitoringMode).includes(value as any)) {
1063+
throw new MongoParseError(
1064+
'serverMonitoringMode must be one of `auto`, `poll`, or `stream`'
1065+
);
1066+
}
1067+
return value;
1068+
}
1069+
},
10581070
serverSelectionTimeoutMS: {
10591071
default: 30000,
10601072
type: 'uint'

src/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,8 @@ export type {
489489
MonitorOptions,
490490
MonitorPrivate,
491491
RTTPinger,
492-
RTTPingerOptions
492+
RTTPingerOptions,
493+
ServerMonitoringMode
493494
} from './sdam/monitor';
494495
export type { Server, ServerEvents, ServerOptions, ServerPrivate } from './sdam/server';
495496
export type {

src/mongo_client.ts

+4
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { executeOperation } from './operations/execute_operation';
3232
import { RunAdminCommandOperation } from './operations/run_command';
3333
import type { ReadConcern, ReadConcernLevel, ReadConcernLike } from './read_concern';
3434
import { ReadPreference, type ReadPreferenceMode } from './read_preference';
35+
import type { ServerMonitoringMode } from './sdam/monitor';
3536
import type { TagSet } from './sdam/server_description';
3637
import { readPreferenceServerSelector } from './sdam/server_selection';
3738
import type { SrvPoller } from './sdam/srv_polling';
@@ -257,6 +258,8 @@ export interface MongoClientOptions extends BSONSerializeOptions, SupportedNodeC
257258
proxyUsername?: string;
258259
/** Configures a Socks5 proxy password when the proxy in proxyHost requires username/password authentication. */
259260
proxyPassword?: string;
261+
/** Instructs the driver monitors to use a specific monitoring mode */
262+
serverMonitoringMode?: ServerMonitoringMode;
260263

261264
/** @internal */
262265
srvPoller?: SrvPoller;
@@ -816,6 +819,7 @@ export interface MongoOptions
816819
proxyPort?: number;
817820
proxyUsername?: string;
818821
proxyPassword?: string;
822+
serverMonitoringMode: ServerMonitoringMode;
819823

820824
/** @internal */
821825
connectionType?: typeof Connection;

src/sdam/monitor.ts

+50-19
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { clearTimeout, setTimeout } from 'timers';
33
import { type Document, Long } from '../bson';
44
import { connect } from '../cmap/connect';
55
import { Connection, type ConnectionOptions } from '../cmap/connection';
6+
import { getFAASEnv } from '../cmap/handshake/client_metadata';
67
import { LEGACY_HELLO_COMMAND } from '../constants';
78
import { MongoError, MongoErrorLabel, MongoNetworkTimeoutError } from '../error';
89
import { CancellationToken, TypedEventEmitter } from '../mongo_types';
@@ -44,6 +45,16 @@ function isInCloseState(monitor: Monitor) {
4445
return monitor.s.state === STATE_CLOSED || monitor.s.state === STATE_CLOSING;
4546
}
4647

48+
/** @public */
49+
export const ServerMonitoringMode = Object.freeze({
50+
auto: 'auto',
51+
poll: 'poll',
52+
stream: 'stream'
53+
} as const);
54+
55+
/** @public */
56+
export type ServerMonitoringMode = (typeof ServerMonitoringMode)[keyof typeof ServerMonitoringMode];
57+
4758
/** @internal */
4859
export interface MonitorPrivate {
4960
state: string;
@@ -55,6 +66,7 @@ export interface MonitorOptions
5566
connectTimeoutMS: number;
5667
heartbeatFrequencyMS: number;
5768
minHeartbeatFrequencyMS: number;
69+
serverMonitoringMode: ServerMonitoringMode;
5870
}
5971

6072
/** @public */
@@ -73,9 +85,16 @@ export class Monitor extends TypedEventEmitter<MonitorEvents> {
7385
s: MonitorPrivate;
7486
address: string;
7587
options: Readonly<
76-
Pick<MonitorOptions, 'connectTimeoutMS' | 'heartbeatFrequencyMS' | 'minHeartbeatFrequencyMS'>
88+
Pick<
89+
MonitorOptions,
90+
| 'connectTimeoutMS'
91+
| 'heartbeatFrequencyMS'
92+
| 'minHeartbeatFrequencyMS'
93+
| 'serverMonitoringMode'
94+
>
7795
>;
7896
connectOptions: ConnectionOptions;
97+
isRunningInFaasEnv: boolean;
7998
[kServer]: Server;
8099
[kConnection]?: Connection;
81100
[kCancellationToken]: CancellationToken;
@@ -103,8 +122,10 @@ export class Monitor extends TypedEventEmitter<MonitorEvents> {
103122
this.options = Object.freeze({
104123
connectTimeoutMS: options.connectTimeoutMS ?? 10000,
105124
heartbeatFrequencyMS: options.heartbeatFrequencyMS ?? 10000,
106-
minHeartbeatFrequencyMS: options.minHeartbeatFrequencyMS ?? 500
125+
minHeartbeatFrequencyMS: options.minHeartbeatFrequencyMS ?? 500,
126+
serverMonitoringMode: options.serverMonitoringMode
107127
});
128+
this.isRunningInFaasEnv = getFAASEnv() != null;
108129

109130
const cancellationToken = this[kCancellationToken];
110131
// TODO: refactor this to pull it directly from the pool, requires new ConnectionPool integration
@@ -207,27 +228,38 @@ function resetMonitorState(monitor: Monitor) {
207228
monitor[kConnection] = undefined;
208229
}
209230

231+
function useStreamingProtocol(monitor: Monitor, topologyVersion: TopologyVersion | null): boolean {
232+
// If we have no topology version we always poll no matter
233+
// what the user provided, since the server does not support
234+
// the streaming protocol.
235+
if (topologyVersion == null) return false;
236+
237+
const serverMonitoringMode = monitor.options.serverMonitoringMode;
238+
if (serverMonitoringMode === ServerMonitoringMode.poll) return false;
239+
if (serverMonitoringMode === ServerMonitoringMode.stream) return true;
240+
241+
// If we are in auto mode, we need to figure out if we're in a FaaS
242+
// environment or not and choose the appropriate mode.
243+
if (monitor.isRunningInFaasEnv) return false;
244+
return true;
245+
}
246+
210247
function checkServer(monitor: Monitor, callback: Callback<Document | null>) {
211248
let start = now();
212249
const topologyVersion = monitor[kServer].description.topologyVersion;
213-
const isAwaitable = topologyVersion != null;
250+
const isAwaitable = useStreamingProtocol(monitor, topologyVersion);
214251
monitor.emit(
215252
Server.SERVER_HEARTBEAT_STARTED,
216253
new ServerHeartbeatStartedEvent(monitor.address, isAwaitable)
217254
);
218255

219-
function failureHandler(err: Error) {
256+
function failureHandler(err: Error, awaited: boolean) {
220257
monitor[kConnection]?.destroy({ force: true });
221258
monitor[kConnection] = undefined;
222259

223260
monitor.emit(
224261
Server.SERVER_HEARTBEAT_FAILED,
225-
new ServerHeartbeatFailedEvent(
226-
monitor.address,
227-
calculateDurationInMs(start),
228-
err,
229-
isAwaitable
230-
)
262+
new ServerHeartbeatFailedEvent(monitor.address, calculateDurationInMs(start), err, awaited)
231263
);
232264

233265
const error = !(err instanceof MongoError)
@@ -274,7 +306,7 @@ function checkServer(monitor: Monitor, callback: Callback<Document | null>) {
274306

275307
connection.command(ns('admin.$cmd'), cmd, options, (err, hello) => {
276308
if (err) {
277-
return failureHandler(err);
309+
return failureHandler(err, isAwaitable);
278310
}
279311

280312
if (!('isWritablePrimary' in hello)) {
@@ -286,15 +318,14 @@ function checkServer(monitor: Monitor, callback: Callback<Document | null>) {
286318
const duration =
287319
isAwaitable && rttPinger ? rttPinger.roundTripTime : calculateDurationInMs(start);
288320

289-
const awaited = isAwaitable && hello.topologyVersion != null;
290321
monitor.emit(
291322
Server.SERVER_HEARTBEAT_SUCCEEDED,
292-
new ServerHeartbeatSucceededEvent(monitor.address, duration, hello, awaited)
323+
new ServerHeartbeatSucceededEvent(monitor.address, duration, hello, isAwaitable)
293324
);
294325

295-
// if we are using the streaming protocol then we immediately issue another `started`
296-
// event, otherwise the "check" is complete and return to the main monitor loop
297-
if (awaited) {
326+
// If we are using the streaming protocol then we immediately issue another 'started'
327+
// event, otherwise the "check" is complete and return to the main monitor loop.
328+
if (isAwaitable) {
298329
monitor.emit(
299330
Server.SERVER_HEARTBEAT_STARTED,
300331
new ServerHeartbeatStartedEvent(monitor.address, true)
@@ -316,7 +347,7 @@ function checkServer(monitor: Monitor, callback: Callback<Document | null>) {
316347
if (err) {
317348
monitor[kConnection] = undefined;
318349

319-
failureHandler(err);
350+
failureHandler(err, false);
320351
return;
321352
}
322353

@@ -337,7 +368,7 @@ function checkServer(monitor: Monitor, callback: Callback<Document | null>) {
337368
monitor.address,
338369
calculateDurationInMs(start),
339370
conn.hello,
340-
false
371+
useStreamingProtocol(monitor, conn.hello?.topologyVersion)
341372
)
342373
);
343374

@@ -370,7 +401,7 @@ function monitorServer(monitor: Monitor) {
370401
}
371402

372403
// if the check indicates streaming is supported, immediately reschedule monitoring
373-
if (hello && hello.topologyVersion) {
404+
if (useStreamingProtocol(monitor, hello?.topologyVersion)) {
374405
setTimeout(() => {
375406
if (!isInCloseState(monitor)) {
376407
monitor[kMonitorId]?.wake();

src/sdam/topology.ts

+2
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import {
6565
TopologyDescriptionChangedEvent,
6666
TopologyOpeningEvent
6767
} from './events';
68+
import type { ServerMonitoringMode } from './monitor';
6869
import { Server, type ServerEvents, type ServerOptions } from './server';
6970
import { compareTopologyVersion, ServerDescription } from './server_description';
7071
import { readPreferenceServerSelector, type ServerSelector } from './server_selection';
@@ -143,6 +144,7 @@ export interface TopologyOptions extends BSONSerializeOptions, ServerOptions {
143144
directConnection: boolean;
144145
loadBalanced: boolean;
145146
metadata: ClientMetadata;
147+
serverMonitoringMode: ServerMonitoringMode;
146148
/** MongoDB server API version */
147149
serverApi?: ServerApi;
148150
[featureFlag: symbol]: any;

test/lambda/mongodb/app.mjs

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import * as assert from 'node:assert/strict';
2+
13
import { MongoClient } from 'mongodb';
24

35
// Creates the client that is cached for all requests, subscribes to
@@ -30,18 +32,21 @@ mongoClient.on('commandFailed', (event) => {
3032

3133
mongoClient.on('serverHeartbeatStarted', (event) => {
3234
console.log('serverHeartbeatStarted', event);
35+
assert.strictEqual(event.awaited, false);
3336
});
3437

3538
mongoClient.on('serverHeartbeatSucceeded', (event) => {
3639
heartbeatCount++;
3740
totalHeartbeatDuration += event.duration;
3841
console.log('serverHeartbeatSucceeded', event);
42+
assert.strictEqual(event.awaited, false);
3943
});
4044

4145
mongoClient.on('serverHeartbeatFailed', (event) => {
4246
heartbeatCount++;
4347
totalHeartbeatDuration += event.duration;
4448
console.log('serverHeartbeatFailed', event);
49+
assert.strictEqual(event.awaited, false);
4550
});
4651

4752
mongoClient.on('connectionCreated', (event) => {

0 commit comments

Comments
 (0)