Skip to content

Commit b9636ee

Browse files
authored
feat(NODE-7009): add client metadata on demand (#4574)
1 parent af0bb5a commit b9636ee

File tree

13 files changed

+489
-59
lines changed

13 files changed

+489
-59
lines changed

src/cmap/connection_pool.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -610,13 +610,17 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
610610
}
611611

612612
private createConnection(callback: Callback<Connection>) {
613+
// Note that metadata and extendedMetadata may have changed on the client but have
614+
// been frozen here, so we pull the extendedMetadata promise always from the client
615+
// no mattter what options were set at the construction of the pool.
613616
const connectOptions: ConnectionOptions = {
614617
...this.options,
615618
id: this.connectionCounter.next().value,
616619
generation: this.generation,
617620
cancellationToken: this.cancellationToken,
618621
mongoLogger: this.mongoLogger,
619-
authProviders: this.server.topology.client.s.authProviders
622+
authProviders: this.server.topology.client.s.authProviders,
623+
extendedMetadata: this.server.topology.client.options.extendedMetadata
620624
};
621625

622626
this.pending++;

src/cmap/handshake/client_metadata.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as os from 'os';
22
import * as process from 'process';
33

44
import { BSON, type Document, Int32 } from '../../bson';
5-
import { MongoInvalidArgumentError } from '../../error';
5+
import { MongoInvalidArgumentError, MongoRuntimeError } from '../../error';
66
import type { MongoOptions } from '../../mongo_client';
77
import { fileIsAccessible } from '../../utils';
88

@@ -90,7 +90,10 @@ export class LimitedSizeDocument {
9090
}
9191
}
9292

93-
type MakeClientMetadataOptions = Pick<MongoOptions, 'appName' | 'driverInfo'>;
93+
type MakeClientMetadataOptions = Pick<
94+
MongoOptions,
95+
'appName' | 'driverInfo' | 'additionalDriverInfo'
96+
>;
9497
/**
9598
* From the specs:
9699
* Implementors SHOULD cumulatively update fields in the following order until the document is under the size limit:
@@ -119,6 +122,22 @@ export function makeClientMetadata(options: MakeClientMetadataOptions): ClientMe
119122
version: version.length > 0 ? `${NODE_DRIVER_VERSION}|${version}` : NODE_DRIVER_VERSION
120123
};
121124

125+
if (options.additionalDriverInfo == null) {
126+
throw new MongoRuntimeError(
127+
'Client options `additionalDriverInfo` must always default to an empty array'
128+
);
129+
}
130+
131+
// This is where we handle additional driver info added after client construction.
132+
for (const { name: n = '', version: v = '' } of options.additionalDriverInfo) {
133+
if (n.length > 0) {
134+
driverInfo.name = `${driverInfo.name}|${n}`;
135+
}
136+
if (v.length > 0) {
137+
driverInfo.version = `${driverInfo.version}|${v}`;
138+
}
139+
}
140+
122141
if (!metadataDocument.ifItFitsItSits('driver', driverInfo)) {
123142
throw new MongoInvalidArgumentError(
124143
'Unable to include driverInfo name and version, metadata cannot exceed 512 bytes'
@@ -130,6 +149,12 @@ export function makeClientMetadata(options: MakeClientMetadataOptions): ClientMe
130149
runtimeInfo = `${runtimeInfo}|${platform}`;
131150
}
132151

152+
for (const { platform: p = '' } of options.additionalDriverInfo) {
153+
if (p.length > 0) {
154+
runtimeInfo = `${runtimeInfo}|${p}`;
155+
}
156+
}
157+
133158
if (!metadataDocument.ifItFitsItSits('platform', runtimeInfo)) {
134159
throw new MongoInvalidArgumentError(
135160
'Unable to include driverInfo platform, metadata cannot exceed 512 bytes'

src/connection_string.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,9 @@ export function parseOptions(
535535
}
536536
);
537537

538+
// Set the default for the additional driver info.
539+
mongoOptions.additionalDriverInfo = [];
540+
538541
mongoOptions.metadata = makeClientMetadata(mongoOptions);
539542

540543
mongoOptions.extendedMetadata = addContainerMetadata(mongoOptions.metadata).then(

src/mongo_client.ts

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ import { type TokenCache } from './cmap/auth/mongodb_oidc/token_cache';
1414
import { AuthMechanism } from './cmap/auth/providers';
1515
import type { LEGAL_TCP_SOCKET_OPTIONS, LEGAL_TLS_SOCKET_OPTIONS } from './cmap/connect';
1616
import type { Connection } from './cmap/connection';
17-
import type { ClientMetadata } from './cmap/handshake/client_metadata';
17+
import {
18+
addContainerMetadata,
19+
type ClientMetadata,
20+
makeClientMetadata
21+
} from './cmap/handshake/client_metadata';
1822
import type { CompressorName } from './cmap/wire_protocol/compression';
1923
import { parseOptions, resolveSRVRecord } from './connection_string';
2024
import { MONGO_CLIENT_EVENTS } from './constants';
@@ -398,9 +402,31 @@ export class MongoClient extends TypedEventEmitter<MongoClientEvents> implements
398402
* The consolidate, parsed, transformed and merged options.
399403
*/
400404
public readonly options: Readonly<
401-
Omit<MongoOptions, 'monitorCommands' | 'ca' | 'crl' | 'key' | 'cert'>
405+
Omit<
406+
MongoOptions,
407+
| 'monitorCommands'
408+
| 'ca'
409+
| 'crl'
410+
| 'key'
411+
| 'cert'
412+
| 'driverInfo'
413+
| 'additionalDriverInfo'
414+
| 'metadata'
415+
| 'extendedMetadata'
416+
>
402417
> &
403-
Pick<MongoOptions, 'monitorCommands' | 'ca' | 'crl' | 'key' | 'cert'>;
418+
Pick<
419+
MongoOptions,
420+
| 'monitorCommands'
421+
| 'ca'
422+
| 'crl'
423+
| 'key'
424+
| 'cert'
425+
| 'driverInfo'
426+
| 'additionalDriverInfo'
427+
| 'metadata'
428+
| 'extendedMetadata'
429+
>;
404430

405431
constructor(url: string, options?: MongoClientOptions) {
406432
super();
@@ -459,6 +485,18 @@ export class MongoClient extends TypedEventEmitter<MongoClientEvents> implements
459485
await this.close();
460486
}
461487

488+
/**
489+
* Append metadata to the client metadata after instantiation.
490+
* @param driverInfo - Information about the application or library.
491+
*/
492+
appendMetadata(driverInfo: DriverInfo) {
493+
this.options.additionalDriverInfo.push(driverInfo);
494+
this.options.metadata = makeClientMetadata(this.options);
495+
this.options.extendedMetadata = addContainerMetadata(this.options.metadata)
496+
.then(undefined, squashError)
497+
.then(result => result ?? {}); // ensure Promise<Document>
498+
}
499+
462500
/** @internal */
463501
private checkForNonGenuineHosts() {
464502
const documentDBHostnames = this.options.hosts.filter((hostAddress: HostAddress) =>
@@ -1041,8 +1079,8 @@ export interface MongoOptions
10411079
dbName: string;
10421080
/** @deprecated - Will be made internal in a future major release. */
10431081
metadata: ClientMetadata;
1044-
/** @internal */
10451082
extendedMetadata: Promise<Document>;
1083+
additionalDriverInfo: DriverInfo[];
10461084
/** @internal */
10471085
autoEncrypter?: AutoEncrypter;
10481086
/** @internal */

test/integration/connection-monitoring-and-pooling/connection.test.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,10 @@ describe('Connection', function () {
4949
...commonConnectOptions,
5050
connectionType: Connection,
5151
...this.configuration.options,
52-
metadata: makeClientMetadata({ driverInfo: {} }),
53-
extendedMetadata: addContainerMetadata(makeClientMetadata({ driverInfo: {} }))
52+
metadata: makeClientMetadata({ driverInfo: {}, additionalDriverInfo: [] }),
53+
extendedMetadata: addContainerMetadata(
54+
makeClientMetadata({ driverInfo: {}, additionalDriverInfo: [] })
55+
)
5456
};
5557

5658
let conn;
@@ -72,8 +74,10 @@ describe('Connection', function () {
7274
connectionType: Connection,
7375
...this.configuration.options,
7476
monitorCommands: true,
75-
metadata: makeClientMetadata({ driverInfo: {} }),
76-
extendedMetadata: addContainerMetadata(makeClientMetadata({ driverInfo: {} }))
77+
metadata: makeClientMetadata({ driverInfo: {}, additionalDriverInfo: [] }),
78+
extendedMetadata: addContainerMetadata(
79+
makeClientMetadata({ driverInfo: {}, additionalDriverInfo: [] })
80+
)
7781
};
7882

7983
let conn;
@@ -104,8 +108,10 @@ describe('Connection', function () {
104108
connectionType: Connection,
105109
...this.configuration.options,
106110
monitorCommands: true,
107-
metadata: makeClientMetadata({ driverInfo: {} }),
108-
extendedMetadata: addContainerMetadata(makeClientMetadata({ driverInfo: {} }))
111+
metadata: makeClientMetadata({ driverInfo: {}, additionalDriverInfo: [] }),
112+
extendedMetadata: addContainerMetadata(
113+
makeClientMetadata({ driverInfo: {}, additionalDriverInfo: [] })
114+
)
109115
};
110116

111117
let conn;

test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
LEGACY_HELLO_COMMAND,
99
type MongoClient
1010
} from '../../mongodb';
11+
import { sleep } from '../../tools/utils';
1112

1213
type EnvironmentVariables = Array<[string, string]>;
1314

@@ -194,3 +195,161 @@ describe('Handshake Prose Tests', function () {
194195
});
195196
});
196197
});
198+
199+
describe('Client Metadata Update Prose Tests', function () {
200+
let client: MongoClient;
201+
202+
afterEach(async function () {
203+
await client?.close();
204+
sinon.restore();
205+
});
206+
207+
describe('Test 1: Test that the driver updates metadata', function () {
208+
let initialClientMetadata;
209+
let updatedClientMetadata;
210+
211+
const tests = [
212+
{ testCase: 1, name: 'framework', version: '2.0', platform: 'Framework Platform' },
213+
{ testCase: 2, name: 'framework', version: '2.0' },
214+
{ testCase: 3, name: 'framework', platform: 'Framework Platform' },
215+
{ testCase: 4, name: 'framework' }
216+
];
217+
218+
for (const { testCase, name, version, platform } of tests) {
219+
context(`Case: ${testCase}`, function () {
220+
// 1. Create a `MongoClient` instance with the following:
221+
// - `maxIdleTimeMS` set to `1ms`
222+
// - Wrapping library metadata:
223+
// | Field | Value |
224+
// | -------- | ---------------- |
225+
// | name | library |
226+
// | version | 1.2 |
227+
// | platform | Library Platform |
228+
// 2. Send a `ping` command to the server and verify that the command succeeds.
229+
// 3. Save intercepted `client` document as `initialClientMetadata`.
230+
// 4. Wait 5ms for the connection to become idle.
231+
beforeEach(async function () {
232+
client = this.configuration.newClient(
233+
{},
234+
{
235+
maxIdleTimeMS: 1,
236+
driverInfo: { name: 'library', version: '1.2', platform: 'Library Platform' }
237+
}
238+
);
239+
240+
sinon.stub(Connection.prototype, 'command').callsFake(async function (ns, cmd, options) {
241+
// @ts-expect-error: sinon will place wrappedMethod on the command method.
242+
const command = Connection.prototype.command.wrappedMethod.bind(this);
243+
244+
if (cmd.hello || cmd[LEGACY_HELLO_COMMAND]) {
245+
if (!initialClientMetadata) {
246+
initialClientMetadata = cmd.client;
247+
} else {
248+
updatedClientMetadata = cmd.client;
249+
}
250+
}
251+
return command(ns, cmd, options);
252+
});
253+
254+
await client.db('test').command({ ping: 1 });
255+
await sleep(5);
256+
});
257+
258+
it('appends the metadata', async function () {
259+
// 1. Append the `DriverInfoOptions` from the selected test case to the `MongoClient` metadata.
260+
// 2. Send a `ping` command to the server and verify:
261+
// - The command succeeds.
262+
// - The framework metadata is appended to the existing `DriverInfoOptions` in the `client.driver` fields of the `hello`
263+
// command, with values separated by a pipe `|`.
264+
client.appendMetadata({ name, version, platform });
265+
await client.db('test').command({ ping: 1 });
266+
267+
// Since we have our own driver metadata getting added, we really want to just
268+
// assert that the last driver info values are appended at the end.
269+
expect(updatedClientMetadata.driver.name).to.match(/^.*\|framework$/);
270+
expect(updatedClientMetadata.driver.version).to.match(
271+
new RegExp(`^.*\\|${version ? version : '1.2'}$`)
272+
);
273+
expect(updatedClientMetadata.platform).to.match(
274+
new RegExp(`^.*\\|${platform ? platform : 'Library Platform'}$`)
275+
);
276+
// - All other subfields in the client document remain unchanged from initialClientMetadata.
277+
// (Note os is the only one getting set in these tests)
278+
expect(updatedClientMetadata.os).to.deep.equal(initialClientMetadata.os);
279+
});
280+
});
281+
}
282+
});
283+
284+
describe('Test 2: Multiple Successive Metadata Updates', function () {
285+
let initialClientMetadata;
286+
let updatedClientMetadata;
287+
288+
const tests = [
289+
{ testCase: 1, name: 'framework', version: '2.0', platform: 'Framework Platform' },
290+
{ testCase: 2, name: 'framework', version: '2.0' },
291+
{ testCase: 3, name: 'framework', platform: 'Framework Platform' },
292+
{ testCase: 4, name: 'framework' }
293+
];
294+
295+
for (const { testCase, name, version, platform } of tests) {
296+
context(`Case: ${testCase}`, function () {
297+
// 1. Create a `MongoClient` instance with the following:
298+
// - `maxIdleTimeMS` set to `1ms`
299+
// 2. Append the following `DriverInfoOptions` to the `MongoClient` metadata:
300+
// | Field | Value |
301+
// | -------- | ---------------- |
302+
// | name | library |
303+
// | version | 1.2 |
304+
// | platform | Library Platform |
305+
// 3. Send a `ping` command to the server and verify that the command succeeds.
306+
// 4. Save intercepted `client` document as `updatedClientMetadata`.
307+
// 5. Wait 5ms for the connection to become idle.
308+
beforeEach(async function () {
309+
client = this.configuration.newClient({}, { maxIdleTimeMS: 1 });
310+
client.appendMetadata({ name: 'library', version: '1.2', platform: 'Library Platform' });
311+
312+
sinon.stub(Connection.prototype, 'command').callsFake(async function (ns, cmd, options) {
313+
// @ts-expect-error: sinon will place wrappedMethod on the command method.
314+
const command = Connection.prototype.command.wrappedMethod.bind(this);
315+
316+
if (cmd.hello || cmd[LEGACY_HELLO_COMMAND]) {
317+
if (!initialClientMetadata) {
318+
initialClientMetadata = cmd.client;
319+
} else {
320+
updatedClientMetadata = cmd.client;
321+
}
322+
}
323+
return command(ns, cmd, options);
324+
});
325+
326+
await client.db('test').command({ ping: 1 });
327+
await sleep(5);
328+
});
329+
330+
it('appends the metadata', async function () {
331+
// 1. Append the `DriverInfoOptions` from the selected test case to the `MongoClient` metadata.
332+
// 2. Send a `ping` command to the server and verify:
333+
// - The command succeeds.
334+
// - The framework metadata is appended to the existing `DriverInfoOptions` in the `client.driver` fields of the `hello`
335+
// command, with values separated by a pipe `|`.
336+
client.appendMetadata({ name, version, platform });
337+
await client.db('test').command({ ping: 1 });
338+
339+
// Since we have our own driver metadata getting added, we really want to just
340+
// assert that the last driver info values are appended at the end.
341+
expect(updatedClientMetadata.driver.name).to.match(/^.*\|framework$/);
342+
expect(updatedClientMetadata.driver.version).to.match(
343+
new RegExp(`^.*\\|${version ? version : '1.2'}$`)
344+
);
345+
expect(updatedClientMetadata.platform).to.match(
346+
new RegExp(`^.*\\|${platform ? platform : 'Library Platform'}$`)
347+
);
348+
// - All other subfields in the client document remain unchanged from initialClientMetadata.
349+
// (Note os is the only one getting set in these tests)
350+
expect(updatedClientMetadata.os).to.deep.equal(initialClientMetadata.os);
351+
});
352+
});
353+
}
354+
});
355+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { join } from 'path';
2+
3+
import { loadSpecTests } from '../../spec';
4+
import { runUnifiedSuite } from '../../tools/unified-spec-runner/runner';
5+
6+
describe('MongoDB Handshake Tests (Unified)', function () {
7+
runUnifiedSuite(loadSpecTests(join('mongodb-handshake')));
8+
});

0 commit comments

Comments
 (0)