From c6233568937a6b71148f153f0111a0b1609e4469 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Sun, 2 Apr 2023 21:27:12 -0400 Subject: [PATCH 01/30] FAAS metadata commit --- src/cmap/connect.ts | 7 +- src/cmap/connection.ts | 5 +- src/cmap/connection_pool.ts | 2 +- src/cmap/handshake/client_metadata.ts | 124 ++++ src/cmap/handshake/faas_provider.ts | 84 +++ src/connection_string.ts | 4 +- src/index.ts | 8 +- src/mongo_client.ts | 17 +- src/sdam/topology.ts | 9 +- src/utils.ts | 85 +-- .../connection.test.ts | 6 +- test/integration/mongodb-handshake/.gitkeep | 0 .../mongodb-handshake.prose.test.ts | 104 +++ .../node-specific/topology.test.js | 2 +- test/mongodb.ts | 2 + test/tools/cmap_spec_runner.ts | 11 +- .../cmap/handshake/client_metadata.test.ts | 602 ++++++++++++++---- test/unit/connection_string.test.ts | 34 + test/unit/sdam/topology.test.js | 2 +- 19 files changed, 905 insertions(+), 203 deletions(-) create mode 100644 src/cmap/handshake/client_metadata.ts create mode 100644 src/cmap/handshake/faas_provider.ts create mode 100644 test/integration/mongodb-handshake/.gitkeep create mode 100644 test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts diff --git a/src/cmap/connect.ts b/src/cmap/connect.ts index 7b2866adf2..324c6f7485 100644 --- a/src/cmap/connect.ts +++ b/src/cmap/connect.ts @@ -17,7 +17,7 @@ import { MongoRuntimeError, needsRetryableWriteLabel } from '../error'; -import { Callback, ClientMetadata, HostAddress, ns } from '../utils'; +import { Callback, HostAddress, ns } from '../utils'; import { AuthContext, AuthProvider } from './auth/auth_provider'; import { GSSAPI } from './auth/gssapi'; import { MongoCR } from './auth/mongocr'; @@ -28,6 +28,7 @@ import { AuthMechanism } from './auth/providers'; import { ScramSHA1, ScramSHA256 } from './auth/scram'; import { X509 } from './auth/x509'; import { CommandOptions, Connection, ConnectionOptions, CryptoConnection } from './connection'; +import type { TruncatedClientMetadata } from './handshake/client_metadata'; import { MAX_SUPPORTED_SERVER_VERSION, MAX_SUPPORTED_WIRE_VERSION, @@ -192,7 +193,7 @@ export interface HandshakeDocument extends Document { ismaster?: boolean; hello?: boolean; helloOk?: boolean; - client: ClientMetadata; + client: TruncatedClientMetadata; compression: string[]; saslSupportedMechs?: string; loadBalanced?: boolean; @@ -213,7 +214,7 @@ export async function prepareHandshakeDocument( const handshakeDoc: HandshakeDocument = { [serverApi?.version ? 'hello' : LEGACY_HELLO_COMMAND]: 1, helloOk: true, - client: options.metadata, + client: options.truncatedClientMetadata, compression: compressors }; diff --git a/src/cmap/connection.ts b/src/cmap/connection.ts index 464f86d9b3..8ffaa54718 100644 --- a/src/cmap/connection.ts +++ b/src/cmap/connection.ts @@ -29,7 +29,6 @@ import { applySession, ClientSession, updateSessionFromResponse } from '../sessi import { calculateDurationInMs, Callback, - ClientMetadata, HostAddress, maxWireVersion, MongoDBNamespace, @@ -46,6 +45,7 @@ import { } from './command_monitoring_events'; import { BinMsg, Msg, Query, Response, WriteProtocolMessageType } from './commands'; import type { Stream } from './connect'; +import type { ClientMetadata, TruncatedClientMetadata } from './handshake/client_metadata'; import { MessageStream, OperationDescription } from './message_stream'; import { StreamDescription, StreamDescriptionOptions } from './stream_description'; import { getReadPreference, isSharded } from './wire_protocol/shared'; @@ -128,6 +128,9 @@ export interface ConnectionOptions socketTimeoutMS?: number; cancellationToken?: CancellationToken; metadata: ClientMetadata; + + /** @internal */ + truncatedClientMetadata: TruncatedClientMetadata; } /** @internal */ diff --git a/src/cmap/connection_pool.ts b/src/cmap/connection_pool.ts index e3d4228135..4acc1551e5 100644 --- a/src/cmap/connection_pool.ts +++ b/src/cmap/connection_pool.ts @@ -227,7 +227,7 @@ export class ConnectionPool extends TypedEventEmitter { waitQueueTimeoutMS: options.waitQueueTimeoutMS ?? 0, minPoolSizeCheckFrequencyMS: options.minPoolSizeCheckFrequencyMS ?? 100, autoEncrypter: options.autoEncrypter, - metadata: options.metadata + truncatedClientMetadata: options.truncatedClientMetadata }); if (this.options.minPoolSize > this.options.maxPoolSize) { diff --git a/src/cmap/handshake/client_metadata.ts b/src/cmap/handshake/client_metadata.ts new file mode 100644 index 0000000000..e436bb722d --- /dev/null +++ b/src/cmap/handshake/client_metadata.ts @@ -0,0 +1,124 @@ +import { calculateObjectSize } from 'bson'; +import * as os from 'os'; + +import type { MongoOptions } from '../../mongo_client'; +import { deepCopy, DeepPartial } from '../../utils'; +import { applyFaasEnvMetadata } from './faas_provider'; + +/** + * @public + * @see https://github.com/mongodb/specifications/blob/master/source/mongodb-handshake/handshake.rst#hello-command + */ +export interface ClientMetadata { + driver: { + name: string; + version: string; + }; + os: { + type: string; + name: NodeJS.Platform; + architecture: string; + version: string; + }; + platform: string; + application?: { + name: string; + }; + + /** Data containing information about the environment, if the driver is running in a FAAS environment. */ + env?: { + name: 'aws.lambda' | 'gcp.func' | 'azure.func' | 'vercel'; + timeout_sec?: number; + memory_mb?: number; + region?: string; + url?: string; + }; +} + +/** @internal */ +export type TruncatedClientMetadata = DeepPartial; + +/** + * @internal + * truncates the client metadata according to the priority outlined here + * https://github.com/mongodb/specifications/blob/master/source/mongodb-handshake/handshake.rst#limitations + */ +export function truncateClientMetadata(metadata: ClientMetadata): TruncatedClientMetadata { + const copiedMetadata: TruncatedClientMetadata = deepCopy(metadata); + const truncations: Array<(arg0: TruncatedClientMetadata) => void> = [ + m => delete m.platform, + m => { + if (m.env) { + m.env = { name: m.env.name }; + } + }, + m => { + if (m.os) { + m.os = { type: m.os.type }; + } + }, + m => delete m.env, + m => delete m.os, + m => delete m.driver, + m => delete m.application + ]; + + for (const truncation of truncations) { + if (calculateObjectSize(copiedMetadata) <= 512) { + return copiedMetadata; + } + truncation(copiedMetadata); + } + + return copiedMetadata; +} + +/** @public */ +export interface ClientMetadataOptions { + driverInfo?: { + name?: string; + version?: string; + platform?: string; + }; + appName?: string; +} + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const NODE_DRIVER_VERSION = require('../../../package.json').version; + +export function makeClientMetadata( + options: Pick +): ClientMetadata { + const name = options.driverInfo.name ? `nodejs|${options.driverInfo.name}` : 'nodejs'; + const version = options.driverInfo.version + ? `${NODE_DRIVER_VERSION}|${options.driverInfo.version}` + : NODE_DRIVER_VERSION; + const platform = options.driverInfo.platform + ? `Node.js ${process.version}, ${os.endianness()}|${options.driverInfo.platform}` + : `Node.js ${process.version}, ${os.endianness()}`; + + const metadata: ClientMetadata = { + driver: { + name, + version + }, + os: { + type: os.type(), + name: process.platform, + architecture: process.arch, + version: os.release() + }, + platform + }; + + if (options.appName) { + // MongoDB requires the appName not exceed a byte length of 128 + const name = + Buffer.byteLength(options.appName, 'utf8') <= 128 + ? options.appName + : Buffer.from(options.appName, 'utf8').subarray(0, 128).toString('utf8'); + metadata.application = { name }; + } + + return applyFaasEnvMetadata(metadata); +} diff --git a/src/cmap/handshake/faas_provider.ts b/src/cmap/handshake/faas_provider.ts new file mode 100644 index 0000000000..ce9df91af0 --- /dev/null +++ b/src/cmap/handshake/faas_provider.ts @@ -0,0 +1,84 @@ +import { identity } from '../../utils'; +import type { ClientMetadata } from './client_metadata'; + +export type FAASProvider = 'aws' | 'gcp' | 'azure' | 'vercel' | 'none'; + +export function determineCloudProvider(): FAASProvider { + const awsPresent = process.env.AWS_EXECUTION_ENV || process.env.AWS_LAMBDA_RUNTIME_API; + const azurePresent = process.env.FUNCTIONS_WORKER_RUNTIME; + const gcpPresent = process.env.K_SERVICE || process.env.FUNCTION_NAME; + const vercelPresent = process.env.VERCEL; + + const numberOfProvidersPresent = [awsPresent, azurePresent, gcpPresent, vercelPresent].filter( + identity + ).length; + + if (numberOfProvidersPresent !== 1) { + return 'none'; + } + + if (awsPresent) return 'aws'; + if (azurePresent) return 'azure'; + if (gcpPresent) return 'gcp'; + return 'vercel'; +} + +function applyAzureMetadata(m: ClientMetadata): ClientMetadata { + m.env = { name: 'azure.func' }; + return m; +} + +function applyGCPMetadata(m: ClientMetadata): ClientMetadata { + m.env = { name: 'gcp.func' }; + + const memory_mb = Number.parseInt(process.env.FUNCTION_MEMORY_MB ?? ''); + if (!Number.isNaN(memory_mb)) { + m.env.memory_mb = memory_mb; + } + const timeout_sec = Number.parseInt(process.env.FUNCTION_TIMEOUT_SEC ?? ''); + if (!Number.isNaN(timeout_sec)) { + m.env.timeout_sec = timeout_sec; + } + if (process.env.FUNCTION_REGION) { + m.env.region = process.env.FUNCTION_REGION; + } + + return m; +} + +function applyAWSMetadata(m: ClientMetadata): ClientMetadata { + m.env = { name: 'aws.lambda' }; + if (process.env.AWS_REGION) { + m.env.region = process.env.AWS_REGION; + } + const memory_mb = Number.parseInt(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE ?? ''); + if (!Number.isNaN(memory_mb)) { + m.env.memory_mb = memory_mb; + } + return m; +} + +function applyVercelMetadata(m: ClientMetadata): ClientMetadata { + m.env = { name: 'vercel' }; + if (process.env.VERCEL_URL) { + m.env.url = process.env.VERCEL_URL; + } + if (process.env.VERCEL_REGION) { + m.env.region = process.env.VERCEL_REGION; + } + return m; +} + +export function applyFaasEnvMetadata(metadata: ClientMetadata): ClientMetadata { + const handlerMap: Record ClientMetadata> = { + aws: applyAWSMetadata, + gcp: applyGCPMetadata, + azure: applyAzureMetadata, + vercel: applyVercelMetadata, + none: identity + }; + const cloudProvider = determineCloudProvider(); + + const faasMetadataProvider = handlerMap[cloudProvider]; + return faasMetadataProvider(metadata); +} diff --git a/src/connection_string.ts b/src/connection_string.ts index 9b55437273..fe8cf90e6b 100644 --- a/src/connection_string.ts +++ b/src/connection_string.ts @@ -6,6 +6,7 @@ import { URLSearchParams } from 'url'; import type { Document } from './bson'; import { MongoCredentials } from './cmap/auth/mongo_credentials'; import { AUTH_MECHS_AUTH_SRC_EXTERNAL, AuthMechanism } from './cmap/auth/providers'; +import { makeClientMetadata, truncateClientMetadata } from './cmap/handshake/client_metadata'; import { Compressor, CompressorName } from './cmap/wire_protocol/compression'; import { Encrypter } from './encrypter'; import { @@ -32,7 +33,6 @@ import { emitWarningOnce, HostAddress, isRecord, - makeClientMetadata, parseInteger, setDifference } from './utils'; @@ -543,6 +543,8 @@ export function parseOptions( ); mongoOptions.metadata = makeClientMetadata(mongoOptions); + Object.freeze(mongoOptions.metadata); + mongoOptions.truncatedClientMetadata = truncateClientMetadata(mongoOptions.metadata); return mongoOptions; } diff --git a/src/index.ts b/src/index.ts index fac3c9c95b..da39a758ba 100644 --- a/src/index.ts +++ b/src/index.ts @@ -238,6 +238,11 @@ export type { WaitQueueMember, WithConnectionCallback } from './cmap/connection_pool'; +export type { + ClientMetadata, + ClientMetadataOptions, + TruncatedClientMetadata +} from './cmap/handshake/client_metadata'; export type { MessageStream, MessageStreamOptions, @@ -463,8 +468,7 @@ export type { Transaction, TransactionOptions, TxnState } from './transactions'; export type { BufferPool, Callback, - ClientMetadata, - ClientMetadataOptions, + DeepPartial, EventEmitterWithState, HostAddress, List, diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 885c980fbf..d5b2511a7b 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -8,6 +8,7 @@ import type { AuthMechanismProperties, MongoCredentials } from './cmap/auth/mong import type { AuthMechanism } from './cmap/auth/providers'; import type { LEGAL_TCP_SOCKET_OPTIONS, LEGAL_TLS_SOCKET_OPTIONS } from './cmap/connect'; import type { Connection } from './cmap/connection'; +import type { ClientMetadata, TruncatedClientMetadata } from './cmap/handshake/client_metadata'; import type { CompressorName } from './cmap/wire_protocol/compression'; import { parseOptions, resolveSRVRecord } from './connection_string'; import { MONGO_CLIENT_EVENTS } from './constants'; @@ -24,7 +25,7 @@ import { readPreferenceServerSelector } from './sdam/server_selection'; import type { SrvPoller } from './sdam/srv_polling'; import { Topology, TopologyEvents } from './sdam/topology'; import { ClientSession, ClientSessionOptions, ServerSessionPool } from './sessions'; -import { ClientMetadata, HostAddress, MongoDBNamespace, ns, resolveOptions } from './utils'; +import { HostAddress, MongoDBNamespace, ns, resolveOptions } from './utils'; import type { W, WriteConcern, WriteConcernSettings } from './write_concern'; /** @public */ @@ -711,12 +712,24 @@ export interface MongoOptions compressors: CompressorName[]; writeConcern: WriteConcern; dbName: string; - metadata: ClientMetadata; autoEncrypter?: AutoEncrypter; proxyHost?: string; proxyPort?: number; proxyUsername?: string; proxyPassword?: string; + + metadata: ClientMetadata; + + /** + * @internal + * `metadata` truncated to be less than 512 bytes, if necessary, to attach to handshakes. + * `metadata` is left untouched because it is public and to provide users a document they + * inspect to confirm their metadata was parsed correctly. + * + * If `metadata` `<=` 512 bytes, these fields are the same but the driver only uses `truncatedMetadata`. + */ + truncatedClientMetadata: TruncatedClientMetadata; + /** @internal */ connectionType?: typeof Connection; diff --git a/src/sdam/topology.ts b/src/sdam/topology.ts index f545cb8847..66465e43ef 100644 --- a/src/sdam/topology.ts +++ b/src/sdam/topology.ts @@ -5,6 +5,7 @@ import type { BSONSerializeOptions, Document } from '../bson'; import type { MongoCredentials } from '../cmap/auth/mongo_credentials'; import type { ConnectionEvents, DestroyOptions } from '../cmap/connection'; import type { CloseOptions, ConnectionPoolEvents } from '../cmap/connection_pool'; +import type { ClientMetadata, TruncatedClientMetadata } from '../cmap/handshake/client_metadata'; import { DEFAULT_OPTIONS, FEATURE_FLAGS } from '../connection_string'; import { CLOSE, @@ -37,7 +38,6 @@ import type { ClientSession } from '../sessions'; import type { Transaction } from '../transactions'; import { Callback, - ClientMetadata, EventEmitterWithState, HostAddress, List, @@ -138,15 +138,14 @@ export interface TopologyOptions extends BSONSerializeOptions, ServerOptions { /** The name of the replica set to connect to */ replicaSet?: string; srvHost?: string; - /** @internal */ srvPoller?: SrvPoller; /** Indicates that a client should directly connect to a node without attempting to discover its topology type */ directConnection: boolean; loadBalanced: boolean; metadata: ClientMetadata; + truncatedClientMetadata: TruncatedClientMetadata; /** MongoDB server API version */ serverApi?: ServerApi; - /** @internal */ [featureFlag: symbol]: any; } @@ -661,8 +660,8 @@ export class Topology extends TypedEventEmitter { if (typeof callback === 'function') callback(undefined, true); } - get clientMetadata(): ClientMetadata { - return this.s.options.metadata; + get clientMetadata(): TruncatedClientMetadata { + return this.s.options.truncatedClientMetadata; } isConnected(): boolean { diff --git a/src/utils.ts b/src/utils.ts index dee4bc51ae..1793352f06 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,5 @@ import * as crypto from 'crypto'; import type { SrvRecord } from 'dns'; -import * as os from 'os'; import { URL } from 'url'; import { Document, ObjectId, resolveBSONOptions } from './bson'; @@ -20,7 +19,7 @@ import { MongoRuntimeError } from './error'; import type { Explain } from './explain'; -import type { MongoClient, MongoOptions } from './mongo_client'; +import type { MongoClient } from './mongo_client'; import type { CommandOperationOptions, OperationParent } from './operations/command'; import type { Hint, OperationOptions } from './operations/operation'; import { ReadConcern } from './read_concern'; @@ -39,6 +38,13 @@ export type Callback = (error?: AnyError, result?: T) => void; export type AnyOptions = Document; +/** @internal */ +export type DeepPartial = T extends object + ? { + [P in keyof T]?: DeepPartial; + } + : T; + export const ByteUtils = { toLocalBufferType(this: void, buffer: Buffer | Uint8Array): Buffer { return Buffer.isBuffer(buffer) @@ -513,77 +519,6 @@ export function makeStateMachine(stateTable: StateTable): StateTransitionFunctio }; } -/** - * @public - * @see https://github.com/mongodb/specifications/blob/master/source/mongodb-handshake/handshake.rst#hello-command - */ -export interface ClientMetadata { - driver: { - name: string; - version: string; - }; - os: { - type: string; - name: NodeJS.Platform; - architecture: string; - version: string; - }; - platform: string; - application?: { - name: string; - }; -} - -/** @public */ -export interface ClientMetadataOptions { - driverInfo?: { - name?: string; - version?: string; - platform?: string; - }; - appName?: string; -} - -// eslint-disable-next-line @typescript-eslint/no-var-requires -const NODE_DRIVER_VERSION = require('../package.json').version; - -export function makeClientMetadata( - options: Pick -): ClientMetadata { - const name = options.driverInfo.name ? `nodejs|${options.driverInfo.name}` : 'nodejs'; - const version = options.driverInfo.version - ? `${NODE_DRIVER_VERSION}|${options.driverInfo.version}` - : NODE_DRIVER_VERSION; - const platform = options.driverInfo.platform - ? `Node.js ${process.version}, ${os.endianness()}|${options.driverInfo.platform}` - : `Node.js ${process.version}, ${os.endianness()}`; - - const metadata: ClientMetadata = { - driver: { - name, - version - }, - os: { - type: os.type(), - name: process.platform, - architecture: process.arch, - version: os.release() - }, - platform - }; - - if (options.appName) { - // MongoDB requires the appName not exceed a byte length of 128 - const name = - Buffer.byteLength(options.appName, 'utf8') <= 128 - ? options.appName - : Buffer.from(options.appName, 'utf8').subarray(0, 128).toString('utf8'); - metadata.application = { name }; - } - - return metadata; -} - /** @internal */ export function now(): number { const hrtime = process.hrtime(); @@ -1277,3 +1212,7 @@ export function parseUnsignedInteger(value: unknown): number | null { return parsedInt != null && parsedInt >= 0 ? parsedInt : null; } + +export function identity(obj: T): T { + return obj; +} diff --git a/test/integration/connection-monitoring-and-pooling/connection.test.ts b/test/integration/connection-monitoring-and-pooling/connection.test.ts index 7ad5bb5c59..7591aa7291 100644 --- a/test/integration/connection-monitoring-and-pooling/connection.test.ts +++ b/test/integration/connection-monitoring-and-pooling/connection.test.ts @@ -36,7 +36,7 @@ describe('Connection', function () { const connectOptions: Partial = { connectionType: Connection, ...this.configuration.options, - metadata: makeClientMetadata({ driverInfo: {} }) + truncatedClientMetadata: makeClientMetadata({ driverInfo: {} }) }; connect(connectOptions as any as ConnectionOptions, (err, conn) => { @@ -60,7 +60,7 @@ describe('Connection', function () { connectionType: Connection, monitorCommands: true, ...this.configuration.options, - metadata: makeClientMetadata({ driverInfo: {} }) + truncatedClientMetadata: makeClientMetadata({ driverInfo: {} }) }; connect(connectOptions as any as ConnectionOptions, (err, conn) => { @@ -92,7 +92,7 @@ describe('Connection', function () { const connectOptions: Partial = { connectionType: Connection, ...this.configuration.options, - metadata: makeClientMetadata({ driverInfo: {} }) + truncatedClientMetadata: makeClientMetadata({ driverInfo: {} }) }; connect(connectOptions as any as ConnectionOptions, (err, conn) => { diff --git a/test/integration/mongodb-handshake/.gitkeep b/test/integration/mongodb-handshake/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts b/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts new file mode 100644 index 0000000000..a97c25358b --- /dev/null +++ b/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts @@ -0,0 +1,104 @@ +import { expect } from 'chai'; + +import { determineCloudProvider, FAASProvider, MongoClient } from '../../mongodb'; + +context('FAAS Environment Prose Tests', function () { + let client: MongoClient; + + afterEach(async function () { + await client?.close(); + }); + + type EnvironmentVariables = Array<[string, string]>; + const tests: Array<{ + context: string; + expectedProvider: FAASProvider; + env: EnvironmentVariables; + }> = [ + { + context: '1. Valid AWS', + expectedProvider: 'aws', + env: [ + ['AWS_EXECUTION_ENV', 'AWS_Lambda_java8'], + ['AWS_REGION', 'us-east-2'], + ['AWS_LAMBDA_FUNCTION_MEMORY_SIZE', '1024'] + ] + }, + { + context: '2. Valid Azure', + expectedProvider: 'azure', + env: [['FUNCTIONS_WORKER_RUNTIME', 'node']] + }, + { + context: '3. Valid GCP', + expectedProvider: 'gcp', + env: [ + ['K_SERVICE', 'servicename'], + ['FUNCTION_MEMORY_MB', '1024'], + ['FUNCTION_TIMEOUT_SEC', '60'], + ['FUNCTION_REGION', 'us-central1'] + ] + }, + { + context: '4. Valid Vercel', + expectedProvider: 'vercel', + env: [ + ['VERCEL', '1'], + ['VERCEL_URL', '*.vercel.app'], + ['VERCEL_REGION', 'cdg1'] + ] + }, + { + expectedProvider: 'none', + context: '5. Invalid - multiple providers', + env: [ + ['AWS_EXECUTION_ENV', 'AWS_Lambda_java8'], + ['FUNCTIONS_WORKER_RUNTIME', 'node'] + ] + }, + { + expectedProvider: 'aws', + context: '6. Invalid - long string', + env: [ + ['AWS_EXECUTION_ENV', 'AWS_Lambda_java8'], + ['AWS_REGION', 'a'.repeat(1024)] + ] + }, + { + expectedProvider: 'aws', + context: '7. Invalid - wrong types', + env: [ + ['AWS_EXECUTION_ENV', 'AWS_Lambda_java8'], + ['AWS_LAMBDA_FUNCTION_MEMORY_SIZE', 'big'] + ] + } + ]; + + for (const { context: name, env, expectedProvider } of tests) { + context(name, function () { + before(() => { + for (const [key, value] of env) { + process.env[key] = value; + } + }); + after(() => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const [key, _] of env) { + delete process.env[key]; + } + }); + + before(`metadata confirmation test for ${name}`, function () { + expect(determineCloudProvider()).to.equal( + expectedProvider, + 'determined the wrong cloud provider' + ); + }); + + it('runs a hello successfully', async function () { + client = this.configuration.newClient({ serverSelectionTimeoutMS: 3000 }); + await client.connect(); + }); + }); + } +}); diff --git a/test/integration/node-specific/topology.test.js b/test/integration/node-specific/topology.test.js index ee4393efcd..ced960c939 100644 --- a/test/integration/node-specific/topology.test.js +++ b/test/integration/node-specific/topology.test.js @@ -7,7 +7,7 @@ describe('Topology', function () { metadata: { requires: { apiVersion: false, topology: '!load-balanced' } }, // apiVersion not supported by newTopology() test: function (done) { const topology = this.configuration.newTopology({ - metadata: makeClientMetadata({ driverInfo: {} }) + truncatedClientMetadata: makeClientMetadata({ driverInfo: {} }) }); const states = []; diff --git a/test/mongodb.ts b/test/mongodb.ts index dee4e204a3..79f621d496 100644 --- a/test/mongodb.ts +++ b/test/mongodb.ts @@ -122,6 +122,8 @@ export * from '../src/cmap/connection'; export * from '../src/cmap/connection_pool'; export * from '../src/cmap/connection_pool_events'; export * from '../src/cmap/errors'; +export * from '../src/cmap/handshake/client_metadata'; +export * from '../src/cmap/handshake/faas_provider'; export * from '../src/cmap/message_stream'; export * from '../src/cmap/metrics'; export * from '../src/cmap/stream_description'; diff --git a/test/tools/cmap_spec_runner.ts b/test/tools/cmap_spec_runner.ts index 7f21a8bc34..99da831840 100644 --- a/test/tools/cmap_spec_runner.ts +++ b/test/tools/cmap_spec_runner.ts @@ -370,7 +370,10 @@ async function runCmapTest(test: CmapTest, threadContext: ThreadContext) { delete poolOptions.backgroundThreadIntervalMS; } - const metadata = makeClientMetadata({ appName: poolOptions.appName, driverInfo: {} }); + const truncatedClientMetadata = makeClientMetadata({ + appName: poolOptions.appName, + driverInfo: {} + }); delete poolOptions.appName; const operations = test.operations; @@ -382,7 +385,11 @@ async function runCmapTest(test: CmapTest, threadContext: ThreadContext) { const mainThread = threadContext.getThread(MAIN_THREAD_KEY); mainThread.start(); - threadContext.createPool({ ...poolOptions, metadata, minPoolSizeCheckFrequencyMS }); + threadContext.createPool({ + ...poolOptions, + truncatedClientMetadata, + minPoolSizeCheckFrequencyMS + }); // yield control back to the event loop so that the ConnectionPoolCreatedEvent // has a chance to be fired before any synchronously-emitted events from // the queued operations diff --git a/test/unit/cmap/handshake/client_metadata.test.ts b/test/unit/cmap/handshake/client_metadata.test.ts index f75c9cecfe..c827588a91 100644 --- a/test/unit/cmap/handshake/client_metadata.test.ts +++ b/test/unit/cmap/handshake/client_metadata.test.ts @@ -1,150 +1,536 @@ import { expect } from 'chai'; import * as os from 'os'; -import { makeClientMetadata } from '../../../mongodb'; +import { + ClientMetadata, + determineCloudProvider, + FAASProvider, + makeClientMetadata, + truncateClientMetadata, + TruncatedClientMetadata +} from '../../../mongodb'; // eslint-disable-next-line @typescript-eslint/no-var-requires const NODE_DRIVER_VERSION = require('../../../../package.json').version; -describe('makeClientMetadata()', () => { - context('when driverInfo.platform is provided', () => { - it('appends driverInfo.platform to the platform field', () => { - const options = { - driverInfo: { platform: 'myPlatform' } - }; - const metadata = makeClientMetadata(options); - expect(metadata).to.deep.equal({ - driver: { - name: 'nodejs', - version: NODE_DRIVER_VERSION - }, - os: { - type: os.type(), - name: process.platform, - architecture: process.arch, - version: os.release() - }, - platform: `Node.js ${process.version}, ${os.endianness()}|myPlatform` +describe('client metadata module', () => { + describe('determineCloudProvider()', function () { + const tests: Array<[string, FAASProvider]> = [ + ['AWS_EXECUTION_ENV', 'aws'], + ['AWS_LAMBDA_RUNTIME_API', 'aws'], + ['FUNCTIONS_WORKER_RUNTIME', 'azure'], + ['K_SERVICE', 'gcp'], + ['FUNCTION_NAME', 'gcp'], + ['VERCEL', 'vercel'] + ]; + for (const [envVariable, provider] of tests) { + context(`when ${envVariable} is in the environment`, () => { + before(() => { + process.env[envVariable] = 'non empty string'; + }); + after(() => { + delete process.env[envVariable]; + }); + it('determines the correct provider', () => { + expect(determineCloudProvider()).to.equal(provider); + }); }); - }); - }); + } - context('when driverInfo.name is provided', () => { - it('appends driverInfo.name to the driver.name field', () => { - const options = { - driverInfo: { name: 'myName' } - }; - const metadata = makeClientMetadata(options); - expect(metadata).to.deep.equal({ - driver: { - name: 'nodejs|myName', - version: NODE_DRIVER_VERSION - }, - os: { - type: os.type(), - name: process.platform, - architecture: process.arch, - version: os.release() - }, - platform: `Node.js ${process.version}, ${os.endianness()}` + context('when there is no FAAS provider data in the env', () => { + it('parses no FAAS provider', () => { + expect(determineCloudProvider()).to.equal('none'); }); }); - }); - context('when driverInfo.version is provided', () => { - it('appends driverInfo.version to the version field', () => { - const options = { - driverInfo: { version: 'myVersion' } - }; - const metadata = makeClientMetadata(options); - expect(metadata).to.deep.equal({ - driver: { - name: 'nodejs', - version: `${NODE_DRIVER_VERSION}|myVersion` - }, - os: { - type: os.type(), - name: process.platform, - architecture: process.arch, - version: os.release() - }, - platform: `Node.js ${process.version}, ${os.endianness()}` + context('when there is data from multiple cloud providers in the env', () => { + before(() => { + process.env.AWS_EXECUTION_ENV = 'non-empty-string'; + process.env.FUNCTIONS_WORKER_RUNTIME = 'non-empty-string'; + }); + after(() => { + delete process.env.AWS_EXECUTION_ENV; + delete process.env.FUNCTIONS_WORKER_RUNTIME; + }); + it('parses no FAAS provider', () => { + expect(determineCloudProvider()).to.equal('none'); }); }); }); - context('when no custom driverInfo is provided', () => { - const metadata = makeClientMetadata({ driverInfo: {} }); + describe('makeClientMetadata()', () => { + context('when no FAAS environment is detected', () => { + it('does not append FAAS metadata', () => { + const metadata = makeClientMetadata({ driverInfo: {} }); + expect(metadata).not.to.have.property( + 'env', + 'faas metadata applied in a non-faas environment' + ); + expect(metadata).to.deep.equal({ + driver: { + name: 'nodejs', + version: NODE_DRIVER_VERSION + }, + os: { + type: os.type(), + name: process.platform, + architecture: process.arch, + version: os.release() + }, + platform: `Node.js ${process.version}, ${os.endianness()}` + }); + }); + }); + context('when driverInfo.platform is provided', () => { + it('appends driverInfo.platform to the platform field', () => { + const options = { + driverInfo: { platform: 'myPlatform' } + }; + const metadata = makeClientMetadata(options); + expect(metadata).to.deep.equal({ + driver: { + name: 'nodejs', + version: NODE_DRIVER_VERSION + }, + os: { + type: os.type(), + name: process.platform, + architecture: process.arch, + version: os.release() + }, + platform: `Node.js ${process.version}, ${os.endianness()}|myPlatform` + }); + }); + }); - it('does not append the driver info to the metadata', () => { - expect(metadata).to.deep.equal({ - driver: { - name: 'nodejs', - version: NODE_DRIVER_VERSION - }, - os: { - type: os.type(), - name: process.platform, - architecture: process.arch, - version: os.release() - }, - platform: `Node.js ${process.version}, ${os.endianness()}` + context('when driverInfo.name is provided', () => { + it('appends driverInfo.name to the driver.name field', () => { + const options = { + driverInfo: { name: 'myName' } + }; + const metadata = makeClientMetadata(options); + expect(metadata).to.deep.equal({ + driver: { + name: 'nodejs|myName', + version: NODE_DRIVER_VERSION + }, + os: { + type: os.type(), + name: process.platform, + architecture: process.arch, + version: os.release() + }, + platform: `Node.js ${process.version}, ${os.endianness()}` + }); }); }); - it('does not set the application field', () => { - expect(metadata).not.to.have.property('application'); + context('when driverInfo.version is provided', () => { + it('appends driverInfo.version to the version field', () => { + const options = { + driverInfo: { version: 'myVersion' } + }; + const metadata = makeClientMetadata(options); + expect(metadata).to.deep.equal({ + driver: { + name: 'nodejs', + version: `${NODE_DRIVER_VERSION}|myVersion` + }, + os: { + type: os.type(), + name: process.platform, + architecture: process.arch, + version: os.release() + }, + platform: `Node.js ${process.version}, ${os.endianness()}` + }); + }); }); - }); - context('when app name is provided', () => { - context('when the app name is over 128 bytes', () => { - const longString = 'a'.repeat(300); - const options = { - appName: longString, - driverInfo: {} - }; - const metadata = makeClientMetadata(options); - - it('truncates the application name to <=128 bytes', () => { - expect(metadata.application?.name).to.be.a('string'); - // the above assertion fails if `metadata.application?.name` is undefined, so - // we can safely assert that it exists - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(Buffer.byteLength(metadata.application!.name, 'utf8')).to.equal(128); + context('when no custom driverInto is provided', () => { + const metadata = makeClientMetadata({ driverInfo: {} }); + + it('does not append the driver info to the metadata', () => { + expect(metadata).to.deep.equal({ + driver: { + name: 'nodejs', + version: NODE_DRIVER_VERSION + }, + os: { + type: os.type(), + name: process.platform, + architecture: process.arch, + version: os.release() + }, + platform: `Node.js ${process.version}, ${os.endianness()}` + }); + }); + + it('does not set the application field', () => { + expect(metadata).not.to.have.property('application'); }); }); - context( - 'TODO(NODE-5150): fix appName truncation when multi-byte unicode charaters straddle byte 128', - () => { - const longString = '€'.repeat(300); + context('when app name is provided', () => { + context('when the app name is over 128 bytes', () => { + const longString = 'a'.repeat(300); const options = { appName: longString, driverInfo: {} }; const metadata = makeClientMetadata(options); - it('truncates the application name to 129 bytes', () => { + it('truncates the application name to <=128 bytes', () => { expect(metadata.application?.name).to.be.a('string'); // the above assertion fails if `metadata.application?.name` is undefined, so // we can safely assert that it exists // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(Buffer.byteLength(metadata.application!.name, 'utf8')).to.equal(129); + expect(Buffer.byteLength(metadata.application!.name, 'utf8')).to.equal(128); }); - } - ); + }); + + context( + 'TODO(NODE-5150): fix appName truncation when multi-byte unicode charaters straddle byte 128', + () => { + const longString = '€'.repeat(300); + const options = { + appName: longString, + driverInfo: {} + }; + const metadata = makeClientMetadata(options); + + it('truncates the application name to 129 bytes', () => { + expect(metadata.application?.name).to.be.a('string'); + // the above assertion fails if `metadata.application?.name` is undefined, so + // we can safely assert that it exists + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expect(Buffer.byteLength(metadata.application!.name, 'utf8')).to.equal(129); + }); + } + ); - context('when the app name is under 128 bytes', () => { - const options = { - appName: 'myApplication', - driverInfo: {} - }; - const metadata = makeClientMetadata(options); + context('when the app name is under 128 bytes', () => { + const options = { + appName: 'myApplication', + driverInfo: {} + }; + const metadata = makeClientMetadata(options); - it('sets the application name to the value', () => { - expect(metadata.application?.name).to.equal('myApplication'); + it('sets the application name to the value', () => { + expect(metadata.application?.name).to.equal('myApplication'); + }); }); }); }); + + describe('FAAS metadata application to handshake', () => { + const tests = { + aws: [ + { + context: 'no additional metadata', + env: [['AWS_EXECUTION_ENV', 'non-empty string']], + outcome: { + name: 'aws.lambda' + } + }, + { + context: 'AWS_REGION provided', + env: [ + ['AWS_EXECUTION_ENV', 'non-empty string'], + ['AWS_REGION', 'non-null'] + ], + outcome: { + name: 'aws.lambda', + region: 'non-null' + } + }, + { + context: 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE provided', + env: [ + ['AWS_EXECUTION_ENV', 'non-empty string'], + ['AWS_LAMBDA_FUNCTION_MEMORY_SIZE', '3'] + ], + outcome: { + name: 'aws.lambda', + memory_mb: 3 + } + } + ], + azure: [ + { + context: 'no additional metadata', + env: [['FUNCTIONS_WORKER_RUNTIME', 'non-empty']], + outcome: { + name: 'azure.func' + } + } + ], + gcp: [ + { + context: 'no additional metadata', + env: [['FUNCTION_NAME', 'non-empty']], + outcome: { + name: 'gcp.func' + } + }, + { + context: 'FUNCTION_MEMORY_MB provided', + env: [ + ['FUNCTION_NAME', 'non-empty'], + ['FUNCTION_MEMORY_MB', '1024'] + ], + outcome: { + name: 'gcp.func', + memory_mb: 1024 + } + }, + { + context: 'FUNCTION_REGION provided', + env: [ + ['FUNCTION_NAME', 'non-empty'], + ['FUNCTION_REGION', 'region'] + ], + outcome: { + name: 'gcp.func', + region: 'region' + } + } + ], + vercel: [ + { + context: 'no additional metadata', + env: [['VERCEL', 'non-empty']], + outcome: { + name: 'vercel' + } + }, + { + context: 'VERCEL_URL provided', + env: [ + ['VERCEL', 'non-empty'], + ['VERCEL_URL', 'provided-url'] + ], + outcome: { + name: 'vercel', + url: 'provided-url' + } + }, + { + context: 'VERCEL_REGION provided', + env: [ + ['VERCEL', 'non-empty'], + ['VERCEL_REGION', 'region'] + ], + outcome: { + name: 'vercel', + region: 'region' + } + } + ] + }; + + for (const [provider, _tests] of Object.entries(tests)) { + context(provider, () => { + for (const { context, env: _env, outcome } of _tests) { + it(context, () => { + for (const [k, v] of _env) { + if (v != null) { + process.env[k] = v; + } + } + + const { env } = makeClientMetadata({ driverInfo: {} }); + expect(env).to.deep.equal(outcome); + + for (const [k] of _env) { + delete process.env[k]; + } + }); + } + }); + } + + context('when a numeric FAAS env variable is not numerically parsable', () => { + before(() => { + process.env['AWS_EXECUTION_ENV'] = 'non-empty-string'; + process.env['AWS_LAMBDA_FUNCTION_MEMORY_SIZE'] = 'not numeric'; + }); + + after(() => { + delete process.env['AWS_EXECUTION_ENV']; + delete process.env['AWS_LAMBDA_FUNCTION_MEMORY_SIZE']; + }); + + it('does not attach it to the metadata', () => { + expect(makeClientMetadata({ driverInfo: {} })).not.to.have.nested.property('aws.memory_mb'); + }); + }); + }); + + describe('metadata truncation order', function () { + const longDocument = 'a'.repeat(512); + + const tests: Array<[string, ClientMetadata, TruncatedClientMetadata]> = [ + [ + 'only removes platform first', + { + driver: { name: 'nodejs', version: '5.1.0' }, + os: { + type: 'Darwin', + name: 'darwin', + architecture: 'x64', + version: '21.6.0' + }, + platform: longDocument, + application: { name: 'applicationName' }, + env: { name: 'aws.lambda' } + }, + { + driver: { name: 'nodejs', version: '5.1.0' }, + os: { + type: 'Darwin', + name: 'darwin', + architecture: 'x64', + version: '21.6.0' + }, + application: { name: 'applicationName' }, + env: { name: 'aws.lambda' } + } + ], + [ + 'truncates environment metadata after platform', + { + driver: { name: 'nodejs', version: '5.1.0' }, + os: { + type: 'Darwin', + name: 'darwin', + architecture: 'x64', + version: '21.6.0' + }, + platform: 'Node.js v16.17.0, LE', + application: { name: 'applicationName' }, + env: { + name: 'aws.lambda', + region: longDocument + } + }, + { + driver: { name: 'nodejs', version: '5.1.0' }, + os: { + type: 'Darwin', + name: 'darwin', + architecture: 'x64', + version: '21.6.0' + }, + application: { name: 'applicationName' }, + env: { name: 'aws.lambda' } + } + ], + [ + 'truncates os metadata after env metadata', + { + driver: { name: 'nodejs', version: '5.1.0' }, + os: { + type: 'Darwin', + name: 'darwin', + architecture: longDocument, + version: '21.6.0' + }, + platform: 'Node.js v16.17.0, LE', + application: { name: 'applicationName' }, + env: { name: 'aws.lambda' } + }, + { + driver: { name: 'nodejs', version: '5.1.0' }, + os: { type: 'Darwin' }, + application: { name: 'applicationName' }, + env: { name: 'aws.lambda' } + } + ], + [ + 'removes env after truncating os metadata', + { + driver: { name: 'nodejs', version: '5.1.0' }, + os: { + type: 'Darwin', + name: 'darwin', + architecture: 'x64', + version: '21.6.0' + }, + platform: 'Node.js v16.17.0, LE', + application: { name: 'applicationName' }, + env: { + name: longDocument as any + } + }, + { + driver: { name: 'nodejs', version: '5.1.0' }, + os: { type: 'Darwin' }, + application: { name: 'applicationName' } + } + ], + [ + 'removes os after removing env', + { + driver: { name: 'nodejs', version: '5.1.0' }, + os: { + type: longDocument, + name: 'darwin', + architecture: 'x64', + version: '21.6.0' + }, + platform: 'Node.js v16.17.0, LE', + application: { name: 'applicationName' }, + env: { name: 'aws.lambda' } + }, + { + application: { name: 'applicationName' }, + driver: { name: 'nodejs', version: '5.1.0' } + } + ], + [ + 'removes driver after removing env', + { + driver: { + name: longDocument, + version: '5.1.0' + }, + os: { + type: 'Darwin', + name: 'darwin', + architecture: 'x64', + version: '21.6.0' + }, + platform: 'Node.js v16.17.0, LE', + application: { name: 'applicationName' }, + env: { name: 'aws.lambda' } + }, + { application: { name: 'applicationName' } } + ], + [ + 'returns nothing when everything is too large (should never happen)', + { + driver: { name: 'nodejs', version: '5.1.0' }, + os: { + type: 'Darwin', + name: 'darwin', + architecture: 'x64', + version: '21.6.0' + }, + platform: 'Node.js v16.17.0, LE', + application: { + name: longDocument + }, + env: { name: 'aws.lambda' } + }, + {} + ] + ]; + + for (const [description, input, expected] of tests) { + it(description, function () { + expect(truncateClientMetadata(input)).to.deep.equal(expected); + }); + } + }); }); diff --git a/test/unit/connection_string.test.ts b/test/unit/connection_string.test.ts index 4a33259bb9..9d2ac852c2 100644 --- a/test/unit/connection_string.test.ts +++ b/test/unit/connection_string.test.ts @@ -641,4 +641,38 @@ describe('Connection String', function () { ]); }); }); + + context('when the metadata is >512 bytes', () => { + it('truncates the metadata', () => { + const client = new MongoClient('mongodb://localhost:27017', { + appName: 'my app', + driverInfo: { name: 'a'.repeat(512) } + }); + expect(client.options.truncatedClientMetadata).to.deep.equal({ + application: { name: 'my app' } + }); + }); + + it('preserves the untruncated metadata on the `metadata` property', () => { + const client = new MongoClient('mongodb://localhost:27017', { + appName: 'my app', + driverInfo: { name: 'a'.repeat(512) } + }); + console.error(client.options.metadata); + expect(client.options.metadata).to.deep.equal({ + driver: { + name: 'nodejs|mongodb-legacy|aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + version: '5.1.0|5.0.0' + }, + os: { + type: 'Darwin', + name: 'darwin', + architecture: 'x64', + version: '21.6.0' + }, + platform: 'Node.js v16.17.0, LE', + application: { name: 'my app' } + }); + }); + }); }); diff --git a/test/unit/sdam/topology.test.js b/test/unit/sdam/topology.test.js index 8f9dd6e984..244df35296 100644 --- a/test/unit/sdam/topology.test.js +++ b/test/unit/sdam/topology.test.js @@ -36,7 +36,7 @@ describe('Topology (unit)', function () { it('should correctly pass appname', function (done) { const server = new Topology([`localhost:27017`], { - metadata: makeClientMetadata({ + truncatedClientMetadata: makeClientMetadata({ appName: 'My application name', driverInfo: {} }) From 1b86b2df429bbfa2d3561786a8ffae914413c131 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Mon, 3 Apr 2023 08:30:52 -0400 Subject: [PATCH 02/30] fix lint --- test/unit/connection_string.test.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/test/unit/connection_string.test.ts b/test/unit/connection_string.test.ts index 9d2ac852c2..c5cdf3f533 100644 --- a/test/unit/connection_string.test.ts +++ b/test/unit/connection_string.test.ts @@ -1,5 +1,6 @@ import { expect } from 'chai'; import * as dns from 'dns'; +import * as os from 'os'; import * as sinon from 'sinon'; import { @@ -658,19 +659,22 @@ describe('Connection String', function () { appName: 'my app', driverInfo: { name: 'a'.repeat(512) } }); - console.error(client.options.metadata); + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const NODE_DRIVER_VERSION = require('../../package.json').version; + expect(client.options.metadata).to.deep.equal({ driver: { name: 'nodejs|mongodb-legacy|aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - version: '5.1.0|5.0.0' + version: `${NODE_DRIVER_VERSION}|5.0.0` }, os: { - type: 'Darwin', - name: 'darwin', - architecture: 'x64', - version: '21.6.0' + type: os.type(), + name: process.platform, + architecture: process.arch, + version: os.release() }, - platform: 'Node.js v16.17.0, LE', + platform: `Node.js ${process.version}, ${os.endianness()}`, application: { name: 'my app' } }); }); From ed44549f0511c20d00180b90838594cb4416d3cd Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Mon, 3 Apr 2023 08:43:52 -0400 Subject: [PATCH 03/30] misc fixes --- src/utils.ts | 5 +++++ test/unit/cmap/handshake/client_metadata.test.ts | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/utils.ts b/src/utils.ts index 1793352f06..6f2bf1261b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1213,6 +1213,11 @@ export function parseUnsignedInteger(value: unknown): number | null { return parsedInt != null && parsedInt >= 0 ? parsedInt : null; } +/** + * returns the object that was provided + * + * @internal + */ export function identity(obj: T): T { return obj; } diff --git a/test/unit/cmap/handshake/client_metadata.test.ts b/test/unit/cmap/handshake/client_metadata.test.ts index c827588a91..3aa295fedf 100644 --- a/test/unit/cmap/handshake/client_metadata.test.ts +++ b/test/unit/cmap/handshake/client_metadata.test.ts @@ -368,6 +368,16 @@ describe('client metadata module', () => { }); describe('metadata truncation order', function () { + /** + * These tests demonstrate that the order in which metadata truncation occurs is spec + * compliant. There are tests in `connection_string.test.ts` that demonstrate that when + * the metadata is greater than 512 bytes, the metadata is truncated. + * + * Together, these tests demonstrate that + * - truncation happens in the correct order + * - truncation occurs when necessary + */ + const longDocument = 'a'.repeat(512); const tests: Array<[string, ClientMetadata, TruncatedClientMetadata]> = [ From da62ec4dd6ca60d897205e0a60b88a69fc644d03 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Mon, 3 Apr 2023 12:30:10 -0400 Subject: [PATCH 04/30] remove truncated client metadata --- src/cmap/connect.ts | 6 +-- src/cmap/connection.ts | 5 +-- src/cmap/connection_pool.ts | 2 +- src/cmap/handshake/client_metadata.ts | 37 ++++--------------- src/connection_string.ts | 2 - src/index.ts | 6 +-- src/mongo_client.ts | 12 +----- src/sdam/topology.ts | 7 ++-- .../connection.test.ts | 6 +-- .../node-specific/topology.test.js | 2 +- test/tools/cmap_spec_runner.ts | 4 +- .../cmap/handshake/client_metadata.test.ts | 5 +-- test/unit/connection_string.test.ts | 2 +- test/unit/sdam/topology.test.js | 2 +- 14 files changed, 28 insertions(+), 70 deletions(-) diff --git a/src/cmap/connect.ts b/src/cmap/connect.ts index 324c6f7485..7789527f9d 100644 --- a/src/cmap/connect.ts +++ b/src/cmap/connect.ts @@ -28,7 +28,7 @@ import { AuthMechanism } from './auth/providers'; import { ScramSHA1, ScramSHA256 } from './auth/scram'; import { X509 } from './auth/x509'; import { CommandOptions, Connection, ConnectionOptions, CryptoConnection } from './connection'; -import type { TruncatedClientMetadata } from './handshake/client_metadata'; +import type { ClientMetadata } from './handshake/client_metadata'; import { MAX_SUPPORTED_SERVER_VERSION, MAX_SUPPORTED_WIRE_VERSION, @@ -193,7 +193,7 @@ export interface HandshakeDocument extends Document { ismaster?: boolean; hello?: boolean; helloOk?: boolean; - client: TruncatedClientMetadata; + client: ClientMetadata; compression: string[]; saslSupportedMechs?: string; loadBalanced?: boolean; @@ -214,7 +214,7 @@ export async function prepareHandshakeDocument( const handshakeDoc: HandshakeDocument = { [serverApi?.version ? 'hello' : LEGACY_HELLO_COMMAND]: 1, helloOk: true, - client: options.truncatedClientMetadata, + client: options.metadata, compression: compressors }; diff --git a/src/cmap/connection.ts b/src/cmap/connection.ts index 8ffaa54718..66e80aa95b 100644 --- a/src/cmap/connection.ts +++ b/src/cmap/connection.ts @@ -45,7 +45,7 @@ import { } from './command_monitoring_events'; import { BinMsg, Msg, Query, Response, WriteProtocolMessageType } from './commands'; import type { Stream } from './connect'; -import type { ClientMetadata, TruncatedClientMetadata } from './handshake/client_metadata'; +import type { ClientMetadata } from './handshake/client_metadata'; import { MessageStream, OperationDescription } from './message_stream'; import { StreamDescription, StreamDescriptionOptions } from './stream_description'; import { getReadPreference, isSharded } from './wire_protocol/shared'; @@ -128,9 +128,6 @@ export interface ConnectionOptions socketTimeoutMS?: number; cancellationToken?: CancellationToken; metadata: ClientMetadata; - - /** @internal */ - truncatedClientMetadata: TruncatedClientMetadata; } /** @internal */ diff --git a/src/cmap/connection_pool.ts b/src/cmap/connection_pool.ts index 4acc1551e5..e3d4228135 100644 --- a/src/cmap/connection_pool.ts +++ b/src/cmap/connection_pool.ts @@ -227,7 +227,7 @@ export class ConnectionPool extends TypedEventEmitter { waitQueueTimeoutMS: options.waitQueueTimeoutMS ?? 0, minPoolSizeCheckFrequencyMS: options.minPoolSizeCheckFrequencyMS ?? 100, autoEncrypter: options.autoEncrypter, - truncatedClientMetadata: options.truncatedClientMetadata + metadata: options.metadata }); if (this.options.minPoolSize > this.options.maxPoolSize) { diff --git a/src/cmap/handshake/client_metadata.ts b/src/cmap/handshake/client_metadata.ts index e436bb722d..e77203dfca 100644 --- a/src/cmap/handshake/client_metadata.ts +++ b/src/cmap/handshake/client_metadata.ts @@ -2,7 +2,7 @@ import { calculateObjectSize } from 'bson'; import * as os from 'os'; import type { MongoOptions } from '../../mongo_client'; -import { deepCopy, DeepPartial } from '../../utils'; +import { deepCopy } from '../../utils'; import { applyFaasEnvMetadata } from './faas_provider'; /** @@ -35,40 +35,19 @@ export interface ClientMetadata { }; } -/** @internal */ -export type TruncatedClientMetadata = DeepPartial; - /** * @internal * truncates the client metadata according to the priority outlined here * https://github.com/mongodb/specifications/blob/master/source/mongodb-handshake/handshake.rst#limitations */ -export function truncateClientMetadata(metadata: ClientMetadata): TruncatedClientMetadata { - const copiedMetadata: TruncatedClientMetadata = deepCopy(metadata); - const truncations: Array<(arg0: TruncatedClientMetadata) => void> = [ - m => delete m.platform, - m => { - if (m.env) { - m.env = { name: m.env.name }; - } - }, - m => { - if (m.os) { - m.os = { type: m.os.type }; - } - }, - m => delete m.env, - m => delete m.os, - m => delete m.driver, - m => delete m.application - ]; +export function truncateClientMetadata(metadata: ClientMetadata): ClientMetadata { + const copiedMetadata: ClientMetadata = deepCopy(metadata); - for (const truncation of truncations) { - if (calculateObjectSize(copiedMetadata) <= 512) { - return copiedMetadata; - } - truncation(copiedMetadata); - } + // if () + + // 1. Truncate ``platform``. + // 2. Omit fields from ``env`` except ``env.name``. + // 3. Omit the ``env`` document entirely. return copiedMetadata; } diff --git a/src/connection_string.ts b/src/connection_string.ts index fe8cf90e6b..9c4f67c6ad 100644 --- a/src/connection_string.ts +++ b/src/connection_string.ts @@ -543,8 +543,6 @@ export function parseOptions( ); mongoOptions.metadata = makeClientMetadata(mongoOptions); - Object.freeze(mongoOptions.metadata); - mongoOptions.truncatedClientMetadata = truncateClientMetadata(mongoOptions.metadata); return mongoOptions; } diff --git a/src/index.ts b/src/index.ts index da39a758ba..c945fbbcd7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -238,11 +238,7 @@ export type { WaitQueueMember, WithConnectionCallback } from './cmap/connection_pool'; -export type { - ClientMetadata, - ClientMetadataOptions, - TruncatedClientMetadata -} from './cmap/handshake/client_metadata'; +export type { ClientMetadata, ClientMetadataOptions } from './cmap/handshake/client_metadata'; export type { MessageStream, MessageStreamOptions, diff --git a/src/mongo_client.ts b/src/mongo_client.ts index d5b2511a7b..651e4b00e9 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -8,7 +8,7 @@ import type { AuthMechanismProperties, MongoCredentials } from './cmap/auth/mong import type { AuthMechanism } from './cmap/auth/providers'; import type { LEGAL_TCP_SOCKET_OPTIONS, LEGAL_TLS_SOCKET_OPTIONS } from './cmap/connect'; import type { Connection } from './cmap/connection'; -import type { ClientMetadata, TruncatedClientMetadata } from './cmap/handshake/client_metadata'; +import type { ClientMetadata } from './cmap/handshake/client_metadata'; import type { CompressorName } from './cmap/wire_protocol/compression'; import { parseOptions, resolveSRVRecord } from './connection_string'; import { MONGO_CLIENT_EVENTS } from './constants'; @@ -720,16 +720,6 @@ export interface MongoOptions metadata: ClientMetadata; - /** - * @internal - * `metadata` truncated to be less than 512 bytes, if necessary, to attach to handshakes. - * `metadata` is left untouched because it is public and to provide users a document they - * inspect to confirm their metadata was parsed correctly. - * - * If `metadata` `<=` 512 bytes, these fields are the same but the driver only uses `truncatedMetadata`. - */ - truncatedClientMetadata: TruncatedClientMetadata; - /** @internal */ connectionType?: typeof Connection; diff --git a/src/sdam/topology.ts b/src/sdam/topology.ts index 66465e43ef..9a4629b50e 100644 --- a/src/sdam/topology.ts +++ b/src/sdam/topology.ts @@ -5,7 +5,7 @@ import type { BSONSerializeOptions, Document } from '../bson'; import type { MongoCredentials } from '../cmap/auth/mongo_credentials'; import type { ConnectionEvents, DestroyOptions } from '../cmap/connection'; import type { CloseOptions, ConnectionPoolEvents } from '../cmap/connection_pool'; -import type { ClientMetadata, TruncatedClientMetadata } from '../cmap/handshake/client_metadata'; +import type { ClientMetadata } from '../cmap/handshake/client_metadata'; import { DEFAULT_OPTIONS, FEATURE_FLAGS } from '../connection_string'; import { CLOSE, @@ -143,7 +143,6 @@ export interface TopologyOptions extends BSONSerializeOptions, ServerOptions { directConnection: boolean; loadBalanced: boolean; metadata: ClientMetadata; - truncatedClientMetadata: TruncatedClientMetadata; /** MongoDB server API version */ serverApi?: ServerApi; [featureFlag: symbol]: any; @@ -660,8 +659,8 @@ export class Topology extends TypedEventEmitter { if (typeof callback === 'function') callback(undefined, true); } - get clientMetadata(): TruncatedClientMetadata { - return this.s.options.truncatedClientMetadata; + get clientMetadata(): ClientMetadata { + return this.s.options.metadata; } isConnected(): boolean { diff --git a/test/integration/connection-monitoring-and-pooling/connection.test.ts b/test/integration/connection-monitoring-and-pooling/connection.test.ts index 7591aa7291..7ad5bb5c59 100644 --- a/test/integration/connection-monitoring-and-pooling/connection.test.ts +++ b/test/integration/connection-monitoring-and-pooling/connection.test.ts @@ -36,7 +36,7 @@ describe('Connection', function () { const connectOptions: Partial = { connectionType: Connection, ...this.configuration.options, - truncatedClientMetadata: makeClientMetadata({ driverInfo: {} }) + metadata: makeClientMetadata({ driverInfo: {} }) }; connect(connectOptions as any as ConnectionOptions, (err, conn) => { @@ -60,7 +60,7 @@ describe('Connection', function () { connectionType: Connection, monitorCommands: true, ...this.configuration.options, - truncatedClientMetadata: makeClientMetadata({ driverInfo: {} }) + metadata: makeClientMetadata({ driverInfo: {} }) }; connect(connectOptions as any as ConnectionOptions, (err, conn) => { @@ -92,7 +92,7 @@ describe('Connection', function () { const connectOptions: Partial = { connectionType: Connection, ...this.configuration.options, - truncatedClientMetadata: makeClientMetadata({ driverInfo: {} }) + metadata: makeClientMetadata({ driverInfo: {} }) }; connect(connectOptions as any as ConnectionOptions, (err, conn) => { diff --git a/test/integration/node-specific/topology.test.js b/test/integration/node-specific/topology.test.js index ced960c939..ee4393efcd 100644 --- a/test/integration/node-specific/topology.test.js +++ b/test/integration/node-specific/topology.test.js @@ -7,7 +7,7 @@ describe('Topology', function () { metadata: { requires: { apiVersion: false, topology: '!load-balanced' } }, // apiVersion not supported by newTopology() test: function (done) { const topology = this.configuration.newTopology({ - truncatedClientMetadata: makeClientMetadata({ driverInfo: {} }) + metadata: makeClientMetadata({ driverInfo: {} }) }); const states = []; diff --git a/test/tools/cmap_spec_runner.ts b/test/tools/cmap_spec_runner.ts index 99da831840..6292eaae77 100644 --- a/test/tools/cmap_spec_runner.ts +++ b/test/tools/cmap_spec_runner.ts @@ -370,7 +370,7 @@ async function runCmapTest(test: CmapTest, threadContext: ThreadContext) { delete poolOptions.backgroundThreadIntervalMS; } - const truncatedClientMetadata = makeClientMetadata({ + const metadata = makeClientMetadata({ appName: poolOptions.appName, driverInfo: {} }); @@ -387,7 +387,7 @@ async function runCmapTest(test: CmapTest, threadContext: ThreadContext) { threadContext.createPool({ ...poolOptions, - truncatedClientMetadata, + metadata, minPoolSizeCheckFrequencyMS }); // yield control back to the event loop so that the ConnectionPoolCreatedEvent diff --git a/test/unit/cmap/handshake/client_metadata.test.ts b/test/unit/cmap/handshake/client_metadata.test.ts index 3aa295fedf..80bebd2ba3 100644 --- a/test/unit/cmap/handshake/client_metadata.test.ts +++ b/test/unit/cmap/handshake/client_metadata.test.ts @@ -6,8 +6,7 @@ import { determineCloudProvider, FAASProvider, makeClientMetadata, - truncateClientMetadata, - TruncatedClientMetadata + truncateClientMetadata } from '../../../mongodb'; // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -380,7 +379,7 @@ describe('client metadata module', () => { const longDocument = 'a'.repeat(512); - const tests: Array<[string, ClientMetadata, TruncatedClientMetadata]> = [ + const tests: Array<[string, ClientMetadata, ClientMetadata]> = [ [ 'only removes platform first', { diff --git a/test/unit/connection_string.test.ts b/test/unit/connection_string.test.ts index c5cdf3f533..2dfc3debe2 100644 --- a/test/unit/connection_string.test.ts +++ b/test/unit/connection_string.test.ts @@ -649,7 +649,7 @@ describe('Connection String', function () { appName: 'my app', driverInfo: { name: 'a'.repeat(512) } }); - expect(client.options.truncatedClientMetadata).to.deep.equal({ + expect(client.options.metadata).to.deep.equal({ application: { name: 'my app' } }); }); diff --git a/test/unit/sdam/topology.test.js b/test/unit/sdam/topology.test.js index 244df35296..8f9dd6e984 100644 --- a/test/unit/sdam/topology.test.js +++ b/test/unit/sdam/topology.test.js @@ -36,7 +36,7 @@ describe('Topology (unit)', function () { it('should correctly pass appname', function (done) { const server = new Topology([`localhost:27017`], { - truncatedClientMetadata: makeClientMetadata({ + metadata: makeClientMetadata({ appName: 'My application name', driverInfo: {} }) From a98a423fcf03c1d72a6af08efc52756fa2654614 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Mon, 3 Apr 2023 14:30:17 -0400 Subject: [PATCH 05/30] chore: everything but truncation --- src/cmap/handshake/faas_provider.ts | 29 ++++++++++++------- src/index.ts | 1 - src/utils.ts | 7 ----- .../mongodb-handshake.prose.test.ts | 11 +++++-- .../cmap/handshake/client_metadata.test.ts | 10 +++---- 5 files changed, 31 insertions(+), 27 deletions(-) diff --git a/src/cmap/handshake/faas_provider.ts b/src/cmap/handshake/faas_provider.ts index ce9df91af0..ee5b8d5049 100644 --- a/src/cmap/handshake/faas_provider.ts +++ b/src/cmap/handshake/faas_provider.ts @@ -3,11 +3,18 @@ import type { ClientMetadata } from './client_metadata'; export type FAASProvider = 'aws' | 'gcp' | 'azure' | 'vercel' | 'none'; -export function determineCloudProvider(): FAASProvider { - const awsPresent = process.env.AWS_EXECUTION_ENV || process.env.AWS_LAMBDA_RUNTIME_API; - const azurePresent = process.env.FUNCTIONS_WORKER_RUNTIME; - const gcpPresent = process.env.K_SERVICE || process.env.FUNCTION_NAME; - const vercelPresent = process.env.VERCEL; +function isNonEmptyString(s: string | undefined): s is string { + return typeof s === 'string' && s.length > 0; +} + +export function determineFAASProvider(): FAASProvider { + const awsPresent = + isNonEmptyString(process.env.AWS_EXECUTION_ENV) || + isNonEmptyString(process.env.AWS_LAMBDA_RUNTIME_API); + const azurePresent = isNonEmptyString(process.env.FUNCTIONS_WORKER_RUNTIME); + const gcpPresent = + isNonEmptyString(process.env.K_SERVICE) || isNonEmptyString(process.env.FUNCTION_NAME); + const vercelPresent = isNonEmptyString(process.env.VERCEL); const numberOfProvidersPresent = [awsPresent, azurePresent, gcpPresent, vercelPresent].filter( identity @@ -39,7 +46,7 @@ function applyGCPMetadata(m: ClientMetadata): ClientMetadata { if (!Number.isNaN(timeout_sec)) { m.env.timeout_sec = timeout_sec; } - if (process.env.FUNCTION_REGION) { + if (isNonEmptyString(process.env.FUNCTION_REGION)) { m.env.region = process.env.FUNCTION_REGION; } @@ -48,7 +55,7 @@ function applyGCPMetadata(m: ClientMetadata): ClientMetadata { function applyAWSMetadata(m: ClientMetadata): ClientMetadata { m.env = { name: 'aws.lambda' }; - if (process.env.AWS_REGION) { + if (isNonEmptyString(process.env.AWS_REGION)) { m.env.region = process.env.AWS_REGION; } const memory_mb = Number.parseInt(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE ?? ''); @@ -60,10 +67,10 @@ function applyAWSMetadata(m: ClientMetadata): ClientMetadata { function applyVercelMetadata(m: ClientMetadata): ClientMetadata { m.env = { name: 'vercel' }; - if (process.env.VERCEL_URL) { + if (isNonEmptyString(process.env.VERCEL_URL)) { m.env.url = process.env.VERCEL_URL; } - if (process.env.VERCEL_REGION) { + if (isNonEmptyString(process.env.VERCEL_REGION)) { m.env.region = process.env.VERCEL_REGION; } return m; @@ -77,8 +84,8 @@ export function applyFaasEnvMetadata(metadata: ClientMetadata): ClientMetadata { vercel: applyVercelMetadata, none: identity }; - const cloudProvider = determineCloudProvider(); + const faasProvider = determineFAASProvider(); - const faasMetadataProvider = handlerMap[cloudProvider]; + const faasMetadataProvider = handlerMap[faasProvider]; return faasMetadataProvider(metadata); } diff --git a/src/index.ts b/src/index.ts index c945fbbcd7..057047684e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -464,7 +464,6 @@ export type { Transaction, TransactionOptions, TxnState } from './transactions'; export type { BufferPool, Callback, - DeepPartial, EventEmitterWithState, HostAddress, List, diff --git a/src/utils.ts b/src/utils.ts index 6f2bf1261b..7434c7e659 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -38,13 +38,6 @@ export type Callback = (error?: AnyError, result?: T) => void; export type AnyOptions = Document; -/** @internal */ -export type DeepPartial = T extends object - ? { - [P in keyof T]?: DeepPartial; - } - : T; - export const ByteUtils = { toLocalBufferType(this: void, buffer: Buffer | Uint8Array): Buffer { return Buffer.isBuffer(buffer) diff --git a/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts b/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts index a97c25358b..9f81bf78d9 100644 --- a/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts +++ b/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; -import { determineCloudProvider, FAASProvider, MongoClient } from '../../mongodb'; +import { determineFAASProvider, FAASProvider, MongoClient } from '../../mongodb'; context('FAAS Environment Prose Tests', function () { let client: MongoClient; @@ -89,14 +89,19 @@ context('FAAS Environment Prose Tests', function () { }); before(`metadata confirmation test for ${name}`, function () { - expect(determineCloudProvider()).to.equal( + expect(determineFAASProvider()).to.equal( expectedProvider, 'determined the wrong cloud provider' ); }); it('runs a hello successfully', async function () { - client = this.configuration.newClient({ serverSelectionTimeoutMS: 3000 }); + client = this.configuration.newClient({ + // if the handshake is not truncated, the `hello`s fail and the client does + // not connect. Lowering the server selection timeout causes the tests + // to fail more quickly in that scenario. + serverSelectionTimeoutMS: 3000 + }); await client.connect(); }); }); diff --git a/test/unit/cmap/handshake/client_metadata.test.ts b/test/unit/cmap/handshake/client_metadata.test.ts index 80bebd2ba3..412debdf3f 100644 --- a/test/unit/cmap/handshake/client_metadata.test.ts +++ b/test/unit/cmap/handshake/client_metadata.test.ts @@ -3,7 +3,7 @@ import * as os from 'os'; import { ClientMetadata, - determineCloudProvider, + determineFAASProvider, FAASProvider, makeClientMetadata, truncateClientMetadata @@ -12,7 +12,7 @@ import { // eslint-disable-next-line @typescript-eslint/no-var-requires const NODE_DRIVER_VERSION = require('../../../../package.json').version; -describe('client metadata module', () => { +describe.only('client metadata module', () => { describe('determineCloudProvider()', function () { const tests: Array<[string, FAASProvider]> = [ ['AWS_EXECUTION_ENV', 'aws'], @@ -31,14 +31,14 @@ describe('client metadata module', () => { delete process.env[envVariable]; }); it('determines the correct provider', () => { - expect(determineCloudProvider()).to.equal(provider); + expect(determineFAASProvider()).to.equal(provider); }); }); } context('when there is no FAAS provider data in the env', () => { it('parses no FAAS provider', () => { - expect(determineCloudProvider()).to.equal('none'); + expect(determineFAASProvider()).to.equal('none'); }); }); @@ -52,7 +52,7 @@ describe('client metadata module', () => { delete process.env.FUNCTIONS_WORKER_RUNTIME; }); it('parses no FAAS provider', () => { - expect(determineCloudProvider()).to.equal('none'); + expect(determineFAASProvider()).to.equal('none'); }); }); }); From ef58d3f87cbb86728eff9663579e1b2589b35772 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Mon, 3 Apr 2023 14:47:47 -0400 Subject: [PATCH 06/30] chore: misc updates --- src/cmap/handshake/client_metadata.ts | 23 +++- .../cmap/handshake/client_metadata.test.ts | 125 ++---------------- test/unit/connection_string.test.ts | 37 ------ 3 files changed, 24 insertions(+), 161 deletions(-) diff --git a/src/cmap/handshake/client_metadata.ts b/src/cmap/handshake/client_metadata.ts index e77203dfca..5cab4bf12a 100644 --- a/src/cmap/handshake/client_metadata.ts +++ b/src/cmap/handshake/client_metadata.ts @@ -2,7 +2,6 @@ import { calculateObjectSize } from 'bson'; import * as os from 'os'; import type { MongoOptions } from '../../mongo_client'; -import { deepCopy } from '../../utils'; import { applyFaasEnvMetadata } from './faas_provider'; /** @@ -41,15 +40,25 @@ export interface ClientMetadata { * https://github.com/mongodb/specifications/blob/master/source/mongodb-handshake/handshake.rst#limitations */ export function truncateClientMetadata(metadata: ClientMetadata): ClientMetadata { - const copiedMetadata: ClientMetadata = deepCopy(metadata); - - // if () + if (calculateObjectSize(metadata) <= 512) { + return metadata; + } + // 1. Truncate ``platform``. + // no-op - we don't truncate because the `platform` field is essentially a fixed length in Node + // and there isn't anything we can truncate that without removing useful information. - // 1. Truncate ``platform``. // 2. Omit fields from ``env`` except ``env.name``. + if (metadata.env) { + metadata.env = { name: metadata.env.name }; + } + if (calculateObjectSize(metadata) <= 512) { + return metadata; + } + // 3. Omit the ``env`` document entirely. + delete metadata.env; - return copiedMetadata; + return metadata; } /** @public */ @@ -99,5 +108,5 @@ export function makeClientMetadata( metadata.application = { name }; } - return applyFaasEnvMetadata(metadata); + return truncateClientMetadata(applyFaasEnvMetadata(metadata)); } diff --git a/test/unit/cmap/handshake/client_metadata.test.ts b/test/unit/cmap/handshake/client_metadata.test.ts index 412debdf3f..e5a2e24159 100644 --- a/test/unit/cmap/handshake/client_metadata.test.ts +++ b/test/unit/cmap/handshake/client_metadata.test.ts @@ -12,7 +12,7 @@ import { // eslint-disable-next-line @typescript-eslint/no-var-requires const NODE_DRIVER_VERSION = require('../../../../package.json').version; -describe.only('client metadata module', () => { +describe('client metadata module', () => { describe('determineCloudProvider()', function () { const tests: Array<[string, FAASProvider]> = [ ['AWS_EXECUTION_ENV', 'aws'], @@ -366,48 +366,12 @@ describe.only('client metadata module', () => { }); }); - describe('metadata truncation order', function () { - /** - * These tests demonstrate that the order in which metadata truncation occurs is spec - * compliant. There are tests in `connection_string.test.ts` that demonstrate that when - * the metadata is greater than 512 bytes, the metadata is truncated. - * - * Together, these tests demonstrate that - * - truncation happens in the correct order - * - truncation occurs when necessary - */ - + describe('metadata truncation', function () { const longDocument = 'a'.repeat(512); const tests: Array<[string, ClientMetadata, ClientMetadata]> = [ [ - 'only removes platform first', - { - driver: { name: 'nodejs', version: '5.1.0' }, - os: { - type: 'Darwin', - name: 'darwin', - architecture: 'x64', - version: '21.6.0' - }, - platform: longDocument, - application: { name: 'applicationName' }, - env: { name: 'aws.lambda' } - }, - { - driver: { name: 'nodejs', version: '5.1.0' }, - os: { - type: 'Darwin', - name: 'darwin', - architecture: 'x64', - version: '21.6.0' - }, - application: { name: 'applicationName' }, - env: { name: 'aws.lambda' } - } - ], - [ - 'truncates environment metadata after platform', + 'removes extra fields in `env` first', { driver: { name: 'nodejs', version: '5.1.0' }, os: { @@ -418,10 +382,7 @@ describe.only('client metadata module', () => { }, platform: 'Node.js v16.17.0, LE', application: { name: 'applicationName' }, - env: { - name: 'aws.lambda', - region: longDocument - } + env: { name: 'aws.lambda', region: longDocument } }, { driver: { name: 'nodejs', version: '5.1.0' }, @@ -431,33 +392,13 @@ describe.only('client metadata module', () => { architecture: 'x64', version: '21.6.0' }, - application: { name: 'applicationName' }, - env: { name: 'aws.lambda' } - } - ], - [ - 'truncates os metadata after env metadata', - { - driver: { name: 'nodejs', version: '5.1.0' }, - os: { - type: 'Darwin', - name: 'darwin', - architecture: longDocument, - version: '21.6.0' - }, platform: 'Node.js v16.17.0, LE', application: { name: 'applicationName' }, env: { name: 'aws.lambda' } - }, - { - driver: { name: 'nodejs', version: '5.1.0' }, - os: { type: 'Darwin' }, - application: { name: 'applicationName' }, - env: { name: 'aws.lambda' } - } + }) ], [ - 'removes env after truncating os metadata', + 'removes `env` entirely next', { driver: { name: 'nodejs', version: '5.1.0' }, os: { @@ -472,67 +413,17 @@ describe.only('client metadata module', () => { name: longDocument as any } }, - { - driver: { name: 'nodejs', version: '5.1.0' }, - os: { type: 'Darwin' }, - application: { name: 'applicationName' } - } - ], - [ - 'removes os after removing env', { driver: { name: 'nodejs', version: '5.1.0' }, - os: { - type: longDocument, - name: 'darwin', - architecture: 'x64', - version: '21.6.0' - }, - platform: 'Node.js v16.17.0, LE', - application: { name: 'applicationName' }, - env: { name: 'aws.lambda' } - }, - { - application: { name: 'applicationName' }, - driver: { name: 'nodejs', version: '5.1.0' } - } - ], - [ - 'removes driver after removing env', - { - driver: { - name: longDocument, - version: '5.1.0' - }, os: { type: 'Darwin', name: 'darwin', architecture: 'x64', version: '21.6.0' }, - platform: 'Node.js v16.17.0, LE', application: { name: 'applicationName' }, - env: { name: 'aws.lambda' } - }, - { application: { name: 'applicationName' } } - ], - [ - 'returns nothing when everything is too large (should never happen)', - { - driver: { name: 'nodejs', version: '5.1.0' }, - os: { - type: 'Darwin', - name: 'darwin', - architecture: 'x64', - version: '21.6.0' - }, - platform: 'Node.js v16.17.0, LE', - application: { - name: longDocument - }, - env: { name: 'aws.lambda' } - }, - {} + platform: 'Node.js v16.17.0, LE' + } ] ]; diff --git a/test/unit/connection_string.test.ts b/test/unit/connection_string.test.ts index 2dfc3debe2..e85f379cf8 100644 --- a/test/unit/connection_string.test.ts +++ b/test/unit/connection_string.test.ts @@ -642,41 +642,4 @@ describe('Connection String', function () { ]); }); }); - - context('when the metadata is >512 bytes', () => { - it('truncates the metadata', () => { - const client = new MongoClient('mongodb://localhost:27017', { - appName: 'my app', - driverInfo: { name: 'a'.repeat(512) } - }); - expect(client.options.metadata).to.deep.equal({ - application: { name: 'my app' } - }); - }); - - it('preserves the untruncated metadata on the `metadata` property', () => { - const client = new MongoClient('mongodb://localhost:27017', { - appName: 'my app', - driverInfo: { name: 'a'.repeat(512) } - }); - - // eslint-disable-next-line @typescript-eslint/no-var-requires - const NODE_DRIVER_VERSION = require('../../package.json').version; - - expect(client.options.metadata).to.deep.equal({ - driver: { - name: 'nodejs|mongodb-legacy|aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - version: `${NODE_DRIVER_VERSION}|5.0.0` - }, - os: { - type: os.type(), - name: process.platform, - architecture: process.arch, - version: os.release() - }, - platform: `Node.js ${process.version}, ${os.endianness()}`, - application: { name: 'my app' } - }); - }); - }); }); From 51ae18b6ce42c2dec3b6f644c5480a567398bf61 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Mon, 3 Apr 2023 14:59:04 -0400 Subject: [PATCH 07/30] misc changes --- src/connection_string.ts | 2 +- src/mongo_client.ts | 3 +-- test/unit/cmap/handshake/client_metadata.test.ts | 2 +- test/unit/connection_string.test.ts | 1 - 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/connection_string.ts b/src/connection_string.ts index 9c4f67c6ad..c7ba4e028c 100644 --- a/src/connection_string.ts +++ b/src/connection_string.ts @@ -6,7 +6,7 @@ import { URLSearchParams } from 'url'; import type { Document } from './bson'; import { MongoCredentials } from './cmap/auth/mongo_credentials'; import { AUTH_MECHS_AUTH_SRC_EXTERNAL, AuthMechanism } from './cmap/auth/providers'; -import { makeClientMetadata, truncateClientMetadata } from './cmap/handshake/client_metadata'; +import { makeClientMetadata } from './cmap/handshake/client_metadata'; import { Compressor, CompressorName } from './cmap/wire_protocol/compression'; import { Encrypter } from './encrypter'; import { diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 651e4b00e9..21bf61618a 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -712,14 +712,13 @@ export interface MongoOptions compressors: CompressorName[]; writeConcern: WriteConcern; dbName: string; + metadata: ClientMetadata; autoEncrypter?: AutoEncrypter; proxyHost?: string; proxyPort?: number; proxyUsername?: string; proxyPassword?: string; - metadata: ClientMetadata; - /** @internal */ connectionType?: typeof Connection; diff --git a/test/unit/cmap/handshake/client_metadata.test.ts b/test/unit/cmap/handshake/client_metadata.test.ts index e5a2e24159..0c904a07e8 100644 --- a/test/unit/cmap/handshake/client_metadata.test.ts +++ b/test/unit/cmap/handshake/client_metadata.test.ts @@ -395,7 +395,7 @@ describe('client metadata module', () => { platform: 'Node.js v16.17.0, LE', application: { name: 'applicationName' }, env: { name: 'aws.lambda' } - }) + } ], [ 'removes `env` entirely next', diff --git a/test/unit/connection_string.test.ts b/test/unit/connection_string.test.ts index e85f379cf8..4a33259bb9 100644 --- a/test/unit/connection_string.test.ts +++ b/test/unit/connection_string.test.ts @@ -1,6 +1,5 @@ import { expect } from 'chai'; import * as dns from 'dns'; -import * as os from 'os'; import * as sinon from 'sinon'; import { From 6853019d3fd4fd91682d7bc3f6ebdcfc42b59136 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Mon, 3 Apr 2023 15:44:22 -0400 Subject: [PATCH 08/30] use `Int32` --- src/cmap/handshake/client_metadata.ts | 6 +++--- src/cmap/handshake/faas_provider.ts | 20 ++++++++++--------- .../cmap/handshake/client_metadata.test.ts | 5 +++-- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/cmap/handshake/client_metadata.ts b/src/cmap/handshake/client_metadata.ts index 5cab4bf12a..c19e8b3776 100644 --- a/src/cmap/handshake/client_metadata.ts +++ b/src/cmap/handshake/client_metadata.ts @@ -1,4 +1,4 @@ -import { calculateObjectSize } from 'bson'; +import { calculateObjectSize, Int32 } from 'bson'; import * as os from 'os'; import type { MongoOptions } from '../../mongo_client'; @@ -27,8 +27,8 @@ export interface ClientMetadata { /** Data containing information about the environment, if the driver is running in a FAAS environment. */ env?: { name: 'aws.lambda' | 'gcp.func' | 'azure.func' | 'vercel'; - timeout_sec?: number; - memory_mb?: number; + timeout_sec?: Int32; + memory_mb?: Int32; region?: string; url?: string; }; diff --git a/src/cmap/handshake/faas_provider.ts b/src/cmap/handshake/faas_provider.ts index ee5b8d5049..c1bfdc9ad1 100644 --- a/src/cmap/handshake/faas_provider.ts +++ b/src/cmap/handshake/faas_provider.ts @@ -1,3 +1,5 @@ +import { Int32 } from 'bson'; + import { identity } from '../../utils'; import type { ClientMetadata } from './client_metadata'; @@ -38,13 +40,13 @@ function applyAzureMetadata(m: ClientMetadata): ClientMetadata { function applyGCPMetadata(m: ClientMetadata): ClientMetadata { m.env = { name: 'gcp.func' }; - const memory_mb = Number.parseInt(process.env.FUNCTION_MEMORY_MB ?? ''); - if (!Number.isNaN(memory_mb)) { - m.env.memory_mb = memory_mb; + const memory_mb = Number(process.env.FUNCTION_MEMORY_MB); + if (Number.isInteger(memory_mb)) { + m.env.memory_mb = new Int32(memory_mb); } - const timeout_sec = Number.parseInt(process.env.FUNCTION_TIMEOUT_SEC ?? ''); - if (!Number.isNaN(timeout_sec)) { - m.env.timeout_sec = timeout_sec; + const timeout_sec = Number(process.env.FUNCTION_TIMEOUT_SEC); + if (Number.isInteger(timeout_sec)) { + m.env.timeout_sec = new Int32(timeout_sec); } if (isNonEmptyString(process.env.FUNCTION_REGION)) { m.env.region = process.env.FUNCTION_REGION; @@ -58,9 +60,9 @@ function applyAWSMetadata(m: ClientMetadata): ClientMetadata { if (isNonEmptyString(process.env.AWS_REGION)) { m.env.region = process.env.AWS_REGION; } - const memory_mb = Number.parseInt(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE ?? ''); - if (!Number.isNaN(memory_mb)) { - m.env.memory_mb = memory_mb; + const memory_mb = Number(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE); + if (Number.isInteger(memory_mb)) { + m.env.memory_mb = new Int32(memory_mb); } return m; } diff --git a/test/unit/cmap/handshake/client_metadata.test.ts b/test/unit/cmap/handshake/client_metadata.test.ts index 0c904a07e8..42d9cf5ca7 100644 --- a/test/unit/cmap/handshake/client_metadata.test.ts +++ b/test/unit/cmap/handshake/client_metadata.test.ts @@ -5,6 +5,7 @@ import { ClientMetadata, determineFAASProvider, FAASProvider, + Int32, makeClientMetadata, truncateClientMetadata } from '../../../mongodb'; @@ -251,7 +252,7 @@ describe('client metadata module', () => { ], outcome: { name: 'aws.lambda', - memory_mb: 3 + memory_mb: new Int32(3) } } ], @@ -280,7 +281,7 @@ describe('client metadata module', () => { ], outcome: { name: 'gcp.func', - memory_mb: 1024 + memory_mb: new Int32(1024) } }, { From cc1bf2256b9d176808bf1b7a8b478a13395b3c54 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Tue, 4 Apr 2023 17:44:21 -0400 Subject: [PATCH 09/30] refactor(NODE-4696): use an additive approach toward metadata limit --- src/cmap/handshake/client_metadata.ts | 129 ++++++++++-------- src/cmap/handshake/faas_env.ts | 69 ++++++++++ src/cmap/handshake/faas_provider.ts | 93 ------------- src/utils.ts | 9 -- test/mongodb.ts | 2 +- .../cmap/handshake/client_metadata.test.ts | 124 ++++++----------- 6 files changed, 188 insertions(+), 238 deletions(-) create mode 100644 src/cmap/handshake/faas_env.ts delete mode 100644 src/cmap/handshake/faas_provider.ts diff --git a/src/cmap/handshake/client_metadata.ts b/src/cmap/handshake/client_metadata.ts index c19e8b3776..d11c326476 100644 --- a/src/cmap/handshake/client_metadata.ts +++ b/src/cmap/handshake/client_metadata.ts @@ -1,8 +1,9 @@ -import { calculateObjectSize, Int32 } from 'bson'; import * as os from 'os'; +import * as process from 'process'; +import { BSON, Int32 } from '../../bson'; import type { MongoOptions } from '../../mongo_client'; -import { applyFaasEnvMetadata } from './faas_provider'; +import { getFAASEnv } from './faas_env'; /** * @public @@ -15,15 +16,14 @@ export interface ClientMetadata { }; os: { type: string; - name: NodeJS.Platform; - architecture: string; - version: string; + name?: NodeJS.Platform; + architecture?: string; + version?: string; }; platform: string; application?: { name: string; }; - /** Data containing information about the environment, if the driver is running in a FAAS environment. */ env?: { name: 'aws.lambda' | 'gcp.func' | 'azure.func' | 'vercel'; @@ -34,33 +34,6 @@ export interface ClientMetadata { }; } -/** - * @internal - * truncates the client metadata according to the priority outlined here - * https://github.com/mongodb/specifications/blob/master/source/mongodb-handshake/handshake.rst#limitations - */ -export function truncateClientMetadata(metadata: ClientMetadata): ClientMetadata { - if (calculateObjectSize(metadata) <= 512) { - return metadata; - } - // 1. Truncate ``platform``. - // no-op - we don't truncate because the `platform` field is essentially a fixed length in Node - // and there isn't anything we can truncate that without removing useful information. - - // 2. Omit fields from ``env`` except ``env.name``. - if (metadata.env) { - metadata.env = { name: metadata.env.name }; - } - if (calculateObjectSize(metadata) <= 512) { - return metadata; - } - - // 3. Omit the ``env`` document entirely. - delete metadata.env; - - return metadata; -} - /** @public */ export interface ClientMetadataOptions { driverInfo?: { @@ -74,39 +47,83 @@ export interface ClientMetadataOptions { // eslint-disable-next-line @typescript-eslint/no-var-requires const NODE_DRIVER_VERSION = require('../../../package.json').version; -export function makeClientMetadata( - options: Pick -): ClientMetadata { +/** @internal */ +class LimitedSizeDocument extends Map { + private static MAX_SIZE = 512; + + private get bsonByteLength() { + return BSON.serialize(this).byteLength; + } + + /** Only adds key/value if the bsonByteLength is less than or equal to MAX_SIZE */ + public ifFitsSits(key: string, value: Record | string): boolean { + if (this.bsonByteLength >= LimitedSizeDocument.MAX_SIZE) { + return false; + } + + this.set(key, value); + + if (this.bsonByteLength >= LimitedSizeDocument.MAX_SIZE) { + this.delete(key); + return false; + } + + return true; + } +} + +type MakeClientMetadataOptions = Pick; +export function makeClientMetadata(options: MakeClientMetadataOptions): ClientMetadata { + const metadataDocument = new LimitedSizeDocument(); + + // Add app name first, it must be sent + if (typeof options.appName === 'string' && options.appName.length > 0) { + const name = + Buffer.byteLength(options.appName, 'utf8') <= 128 + ? options.appName + : Buffer.from(options.appName, 'utf8').subarray(0, 128).toString('utf8'); + metadataDocument.ifFitsSits('application', { name }); + } + + // Driver info goes next, we're not going to be at the limit yet, max bytes used ~128 const name = options.driverInfo.name ? `nodejs|${options.driverInfo.name}` : 'nodejs'; const version = options.driverInfo.version ? `${NODE_DRIVER_VERSION}|${options.driverInfo.version}` : NODE_DRIVER_VERSION; + + metadataDocument.ifFitsSits('driver', { name, version }); + + // Platform likely to make it in, depending on driverInfo.name length const platform = options.driverInfo.platform ? `Node.js ${process.version}, ${os.endianness()}|${options.driverInfo.platform}` : `Node.js ${process.version}, ${os.endianness()}`; - const metadata: ClientMetadata = { - driver: { - name, - version - }, - os: { - type: os.type(), - name: process.platform, - architecture: process.arch, - version: os.release() - }, - platform + metadataDocument.ifFitsSits('platform', platform); + + const osInfo = { + type: os.type(), + name: process.platform, + architecture: process.arch, + version: os.release() }; - if (options.appName) { - // MongoDB requires the appName not exceed a byte length of 128 - const name = - Buffer.byteLength(options.appName, 'utf8') <= 128 - ? options.appName - : Buffer.from(options.appName, 'utf8').subarray(0, 128).toString('utf8'); - metadata.application = { name }; + if (!metadataDocument.ifFitsSits('os', osInfo)) { + // Could not add full OS info, add only type + metadataDocument.ifFitsSits('os', { type: osInfo.type }); + } else { + // full OS data was able to fit, try FAAS + const faasEnv = getFAASEnv(); + if (faasEnv != null) { + if (!metadataDocument.ifFitsSits('env', faasEnv)) { + metadataDocument.ifFitsSits('env', { name: faasEnv.get('name') }); + } + } } - return truncateClientMetadata(applyFaasEnvMetadata(metadata)); + return BSON.deserialize(BSON.serialize(metadataDocument), { + promoteLongs: false, + promoteBuffers: false, + promoteValues: false, + useBigInt64: false + }) as ClientMetadata; } diff --git a/src/cmap/handshake/faas_env.ts b/src/cmap/handshake/faas_env.ts new file mode 100644 index 0000000000..493b1d03ac --- /dev/null +++ b/src/cmap/handshake/faas_env.ts @@ -0,0 +1,69 @@ +import * as process from 'process'; + +import { Int32 } from '../../bson'; + +function isNonEmptyString(s: string | undefined): s is string { + return typeof s === 'string' && s.length > 0; +} + +export function getFAASEnv(): Map | null { + const awsPresent = + isNonEmptyString(process.env.AWS_EXECUTION_ENV) || + isNonEmptyString(process.env.AWS_LAMBDA_RUNTIME_API); + const azurePresent = isNonEmptyString(process.env.FUNCTIONS_WORKER_RUNTIME); + const gcpPresent = + isNonEmptyString(process.env.K_SERVICE) || isNonEmptyString(process.env.FUNCTION_NAME); + const vercelPresent = isNonEmptyString(process.env.VERCEL); + + const faasEnv = new Map(); + + if (awsPresent && !(azurePresent || gcpPresent || vercelPresent)) { + faasEnv.set('name', 'aws.lambda'); + + if (isNonEmptyString(process.env.AWS_REGION)) { + faasEnv.set('region', process.env.AWS_REGION); + } + + const memory_mb = Number(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE); + if (Number.isInteger(memory_mb)) { + faasEnv.set('memory_mb', new Int32(memory_mb)); + } + + return faasEnv; + } else if (azurePresent && !(awsPresent || gcpPresent || vercelPresent)) { + faasEnv.set('name', 'azure.func'); + return faasEnv; + } else if (gcpPresent && !(awsPresent || azurePresent || vercelPresent)) { + faasEnv.set('name', 'gcp.func'); + + if (isNonEmptyString(process.env.FUNCTION_REGION)) { + faasEnv.set('region', process.env.FUNCTION_REGION); + } + + const memory_mb = Number(process.env.FUNCTION_MEMORY_MB); + if (Number.isInteger(memory_mb)) { + faasEnv.set('memory_mb', new Int32(memory_mb)); + } + + const timeout_sec = Number(process.env.FUNCTION_TIMEOUT_SEC); + if (Number.isInteger(timeout_sec)) { + faasEnv.set('timeout_sec', new Int32(timeout_sec)); + } + + return faasEnv; + } else if (vercelPresent && !(awsPresent || azurePresent || gcpPresent)) { + faasEnv.set('name', 'vercel'); + + if (isNonEmptyString(process.env.VERCEL_URL)) { + faasEnv.set('url', process.env.VERCEL_URL); + } + + if (isNonEmptyString(process.env.VERCEL_REGION)) { + faasEnv.set('region', process.env.VERCEL_REGION); + } + + return faasEnv; + } else { + return null; + } +} diff --git a/src/cmap/handshake/faas_provider.ts b/src/cmap/handshake/faas_provider.ts deleted file mode 100644 index c1bfdc9ad1..0000000000 --- a/src/cmap/handshake/faas_provider.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Int32 } from 'bson'; - -import { identity } from '../../utils'; -import type { ClientMetadata } from './client_metadata'; - -export type FAASProvider = 'aws' | 'gcp' | 'azure' | 'vercel' | 'none'; - -function isNonEmptyString(s: string | undefined): s is string { - return typeof s === 'string' && s.length > 0; -} - -export function determineFAASProvider(): FAASProvider { - const awsPresent = - isNonEmptyString(process.env.AWS_EXECUTION_ENV) || - isNonEmptyString(process.env.AWS_LAMBDA_RUNTIME_API); - const azurePresent = isNonEmptyString(process.env.FUNCTIONS_WORKER_RUNTIME); - const gcpPresent = - isNonEmptyString(process.env.K_SERVICE) || isNonEmptyString(process.env.FUNCTION_NAME); - const vercelPresent = isNonEmptyString(process.env.VERCEL); - - const numberOfProvidersPresent = [awsPresent, azurePresent, gcpPresent, vercelPresent].filter( - identity - ).length; - - if (numberOfProvidersPresent !== 1) { - return 'none'; - } - - if (awsPresent) return 'aws'; - if (azurePresent) return 'azure'; - if (gcpPresent) return 'gcp'; - return 'vercel'; -} - -function applyAzureMetadata(m: ClientMetadata): ClientMetadata { - m.env = { name: 'azure.func' }; - return m; -} - -function applyGCPMetadata(m: ClientMetadata): ClientMetadata { - m.env = { name: 'gcp.func' }; - - const memory_mb = Number(process.env.FUNCTION_MEMORY_MB); - if (Number.isInteger(memory_mb)) { - m.env.memory_mb = new Int32(memory_mb); - } - const timeout_sec = Number(process.env.FUNCTION_TIMEOUT_SEC); - if (Number.isInteger(timeout_sec)) { - m.env.timeout_sec = new Int32(timeout_sec); - } - if (isNonEmptyString(process.env.FUNCTION_REGION)) { - m.env.region = process.env.FUNCTION_REGION; - } - - return m; -} - -function applyAWSMetadata(m: ClientMetadata): ClientMetadata { - m.env = { name: 'aws.lambda' }; - if (isNonEmptyString(process.env.AWS_REGION)) { - m.env.region = process.env.AWS_REGION; - } - const memory_mb = Number(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE); - if (Number.isInteger(memory_mb)) { - m.env.memory_mb = new Int32(memory_mb); - } - return m; -} - -function applyVercelMetadata(m: ClientMetadata): ClientMetadata { - m.env = { name: 'vercel' }; - if (isNonEmptyString(process.env.VERCEL_URL)) { - m.env.url = process.env.VERCEL_URL; - } - if (isNonEmptyString(process.env.VERCEL_REGION)) { - m.env.region = process.env.VERCEL_REGION; - } - return m; -} - -export function applyFaasEnvMetadata(metadata: ClientMetadata): ClientMetadata { - const handlerMap: Record ClientMetadata> = { - aws: applyAWSMetadata, - gcp: applyGCPMetadata, - azure: applyAzureMetadata, - vercel: applyVercelMetadata, - none: identity - }; - const faasProvider = determineFAASProvider(); - - const faasMetadataProvider = handlerMap[faasProvider]; - return faasMetadataProvider(metadata); -} diff --git a/src/utils.ts b/src/utils.ts index 7434c7e659..4581c777b1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1205,12 +1205,3 @@ export function parseUnsignedInteger(value: unknown): number | null { return parsedInt != null && parsedInt >= 0 ? parsedInt : null; } - -/** - * returns the object that was provided - * - * @internal - */ -export function identity(obj: T): T { - return obj; -} diff --git a/test/mongodb.ts b/test/mongodb.ts index 79f621d496..6a5cdf77ca 100644 --- a/test/mongodb.ts +++ b/test/mongodb.ts @@ -123,7 +123,7 @@ export * from '../src/cmap/connection_pool'; export * from '../src/cmap/connection_pool_events'; export * from '../src/cmap/errors'; export * from '../src/cmap/handshake/client_metadata'; -export * from '../src/cmap/handshake/faas_provider'; +export * from '../src/cmap/handshake/faas_env'; export * from '../src/cmap/message_stream'; export * from '../src/cmap/metrics'; export * from '../src/cmap/stream_description'; diff --git a/test/unit/cmap/handshake/client_metadata.test.ts b/test/unit/cmap/handshake/client_metadata.test.ts index 42d9cf5ca7..b3046bc09d 100644 --- a/test/unit/cmap/handshake/client_metadata.test.ts +++ b/test/unit/cmap/handshake/client_metadata.test.ts @@ -1,26 +1,21 @@ import { expect } from 'chai'; import * as os from 'os'; +import * as process from 'process'; +import * as sinon from 'sinon'; -import { - ClientMetadata, - determineFAASProvider, - FAASProvider, - Int32, - makeClientMetadata, - truncateClientMetadata -} from '../../../mongodb'; +import { getFAASEnv, Int32, makeClientMetadata } from '../../../mongodb'; // eslint-disable-next-line @typescript-eslint/no-var-requires const NODE_DRIVER_VERSION = require('../../../../package.json').version; describe('client metadata module', () => { describe('determineCloudProvider()', function () { - const tests: Array<[string, FAASProvider]> = [ - ['AWS_EXECUTION_ENV', 'aws'], - ['AWS_LAMBDA_RUNTIME_API', 'aws'], - ['FUNCTIONS_WORKER_RUNTIME', 'azure'], - ['K_SERVICE', 'gcp'], - ['FUNCTION_NAME', 'gcp'], + const tests: Array<[string, string]> = [ + ['AWS_EXECUTION_ENV', 'aws.lambda'], + ['AWS_LAMBDA_RUNTIME_API', 'aws.lambda'], + ['FUNCTIONS_WORKER_RUNTIME', 'azure.func'], + ['K_SERVICE', 'gcp.func'], + ['FUNCTION_NAME', 'gcp.func'], ['VERCEL', 'vercel'] ]; for (const [envVariable, provider] of tests) { @@ -32,14 +27,14 @@ describe('client metadata module', () => { delete process.env[envVariable]; }); it('determines the correct provider', () => { - expect(determineFAASProvider()).to.equal(provider); + expect(getFAASEnv()?.get('name')).to.equal(provider); }); }); } context('when there is no FAAS provider data in the env', () => { it('parses no FAAS provider', () => { - expect(determineFAASProvider()).to.equal('none'); + expect(getFAASEnv()).to.be.null; }); }); @@ -53,7 +48,7 @@ describe('client metadata module', () => { delete process.env.FUNCTIONS_WORKER_RUNTIME; }); it('parses no FAAS provider', () => { - expect(determineFAASProvider()).to.equal('none'); + expect(getFAASEnv()).to.be.null; }); }); }); @@ -368,70 +363,41 @@ describe('client metadata module', () => { }); describe('metadata truncation', function () { - const longDocument = 'a'.repeat(512); + beforeEach(() => { + sinon.stub(process, 'env').get(() => ({ + AWS_EXECUTION_ENV: 'iLoveJavaScript', + AWS_REGION: 'a'.repeat(512) + })); + }); - const tests: Array<[string, ClientMetadata, ClientMetadata]> = [ - [ - 'removes extra fields in `env` first', - { - driver: { name: 'nodejs', version: '5.1.0' }, - os: { - type: 'Darwin', - name: 'darwin', - architecture: 'x64', - version: '21.6.0' - }, - platform: 'Node.js v16.17.0, LE', - application: { name: 'applicationName' }, - env: { name: 'aws.lambda', region: longDocument } - }, - { - driver: { name: 'nodejs', version: '5.1.0' }, - os: { - type: 'Darwin', - name: 'darwin', - architecture: 'x64', - version: '21.6.0' - }, - platform: 'Node.js v16.17.0, LE', - application: { name: 'applicationName' }, - env: { name: 'aws.lambda' } - } - ], - [ - 'removes `env` entirely next', - { - driver: { name: 'nodejs', version: '5.1.0' }, - os: { - type: 'Darwin', - name: 'darwin', - architecture: 'x64', - version: '21.6.0' - }, - platform: 'Node.js v16.17.0, LE', - application: { name: 'applicationName' }, - env: { - name: longDocument as any - } - }, - { - driver: { name: 'nodejs', version: '5.1.0' }, - os: { - type: 'Darwin', - name: 'darwin', - architecture: 'x64', - version: '21.6.0' - }, - application: { name: 'applicationName' }, - platform: 'Node.js v16.17.0, LE' - } - ] - ]; + afterEach(() => { + sinon.restore(); + }); - for (const [description, input, expected] of tests) { - it(description, function () { - expect(truncateClientMetadata(input)).to.deep.equal(expected); + context('when faas region is too large', () => { + it('only includes env.name', () => { + const metadata = makeClientMetadata({ driverInfo: {} }); + expect(metadata).to.not.have.nested.property('env.region'); + expect(metadata).to.have.nested.property('env.name', 'aws.lambda'); + expect(metadata.env).to.have.all.keys('name'); }); - } + }); + + context('when os information is too large', () => { + beforeEach(() => { + sinon.stub(os, 'release').returns('a'.repeat(512)); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('only includes env.name', () => { + const metadata = makeClientMetadata({ driverInfo: {} }); + expect(metadata).to.not.have.property('env'); + expect(metadata).to.have.nested.property('os.type', os.type()); + expect(metadata.os).to.have.all.keys('type'); + }); + }); }); }); From 21dc9e3506e41ab2ffe01be4d264db402124bd11 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Wed, 5 Apr 2023 11:46:31 -0400 Subject: [PATCH 10/30] test: env omission --- src/cmap/handshake/client_metadata.ts | 45 ++++++++------- src/cmap/handshake/faas_env.ts | 57 +++++++++++-------- .../cmap/handshake/client_metadata.test.ts | 47 +++++++++------ 3 files changed, 88 insertions(+), 61 deletions(-) diff --git a/src/cmap/handshake/client_metadata.ts b/src/cmap/handshake/client_metadata.ts index d11c326476..a76b8cc211 100644 --- a/src/cmap/handshake/client_metadata.ts +++ b/src/cmap/handshake/client_metadata.ts @@ -56,7 +56,7 @@ class LimitedSizeDocument extends Map { } /** Only adds key/value if the bsonByteLength is less than or equal to MAX_SIZE */ - public ifFitsSits(key: string, value: Record | string): boolean { + public setIfFits(key: string, value: Record | string): boolean { if (this.bsonByteLength >= LimitedSizeDocument.MAX_SIZE) { return false; } @@ -82,23 +82,29 @@ export function makeClientMetadata(options: MakeClientMetadataOptions): ClientMe Buffer.byteLength(options.appName, 'utf8') <= 128 ? options.appName : Buffer.from(options.appName, 'utf8').subarray(0, 128).toString('utf8'); - metadataDocument.ifFitsSits('application', { name }); + metadataDocument.setIfFits('application', { name }); } // Driver info goes next, we're not going to be at the limit yet, max bytes used ~128 - const name = options.driverInfo.name ? `nodejs|${options.driverInfo.name}` : 'nodejs'; - const version = options.driverInfo.version - ? `${NODE_DRIVER_VERSION}|${options.driverInfo.version}` - : NODE_DRIVER_VERSION; + const name = + typeof options.driverInfo.name === 'string' && options.driverInfo.name.length > 0 + ? `nodejs|${options.driverInfo.name}` + : 'nodejs'; - metadataDocument.ifFitsSits('driver', { name, version }); + const version = + typeof options.driverInfo.version === 'string' && options.driverInfo.version.length > 0 + ? `${NODE_DRIVER_VERSION}|${options.driverInfo.version}` + : NODE_DRIVER_VERSION; + + metadataDocument.setIfFits('driver', { name, version }); // Platform likely to make it in, depending on driverInfo.name length - const platform = options.driverInfo.platform - ? `Node.js ${process.version}, ${os.endianness()}|${options.driverInfo.platform}` - : `Node.js ${process.version}, ${os.endianness()}`; + const platform = + typeof options.driverInfo.platform === 'string' && options.driverInfo.platform.length > 0 + ? `Node.js ${process.version}, ${os.endianness()}|${options.driverInfo.platform}` + : `Node.js ${process.version}, ${os.endianness()}`; - metadataDocument.ifFitsSits('platform', platform); + metadataDocument.setIfFits('platform', platform); const osInfo = { type: os.type(), @@ -107,16 +113,15 @@ export function makeClientMetadata(options: MakeClientMetadataOptions): ClientMe version: os.release() }; - if (!metadataDocument.ifFitsSits('os', osInfo)) { + if (!metadataDocument.setIfFits('os', osInfo)) { // Could not add full OS info, add only type - metadataDocument.ifFitsSits('os', { type: osInfo.type }); - } else { - // full OS data was able to fit, try FAAS - const faasEnv = getFAASEnv(); - if (faasEnv != null) { - if (!metadataDocument.ifFitsSits('env', faasEnv)) { - metadataDocument.ifFitsSits('env', { name: faasEnv.get('name') }); - } + metadataDocument.setIfFits('os', { type: osInfo.type }); + } + + const faasEnv = getFAASEnv(); + if (faasEnv != null) { + if (!metadataDocument.setIfFits('env', faasEnv)) { + metadataDocument.setIfFits('env', { name: faasEnv.get('name') }); } } diff --git a/src/cmap/handshake/faas_env.ts b/src/cmap/handshake/faas_env.ts index 493b1d03ac..842e32f71f 100644 --- a/src/cmap/handshake/faas_env.ts +++ b/src/cmap/handshake/faas_env.ts @@ -2,63 +2,72 @@ import * as process from 'process'; import { Int32 } from '../../bson'; -function isNonEmptyString(s: string | undefined): s is string { - return typeof s === 'string' && s.length > 0; -} - export function getFAASEnv(): Map | null { - const awsPresent = - isNonEmptyString(process.env.AWS_EXECUTION_ENV) || - isNonEmptyString(process.env.AWS_LAMBDA_RUNTIME_API); - const azurePresent = isNonEmptyString(process.env.FUNCTIONS_WORKER_RUNTIME); - const gcpPresent = - isNonEmptyString(process.env.K_SERVICE) || isNonEmptyString(process.env.FUNCTION_NAME); - const vercelPresent = isNonEmptyString(process.env.VERCEL); + const { + AWS_EXECUTION_ENV = '', + AWS_LAMBDA_RUNTIME_API = '', + FUNCTIONS_WORKER_RUNTIME = '', + K_SERVICE = '', + FUNCTION_NAME = '', + VERCEL = '', + AWS_LAMBDA_FUNCTION_MEMORY_SIZE = '', + AWS_REGION = '', + FUNCTION_MEMORY_MB = '', + FUNCTION_REGION = '', + FUNCTION_TIMEOUT_SEC = '', + VERCEL_URL = '', + VERCEL_REGION = '' + } = process.env; + + const isAWSFaaS = AWS_EXECUTION_ENV.length > 0 || AWS_LAMBDA_RUNTIME_API.length > 0; + const isAzureFaaS = FUNCTIONS_WORKER_RUNTIME.length > 0; + const isGCPFaaS = K_SERVICE.length > 0 || FUNCTION_NAME.length > 0; + const isVercelFaaS = VERCEL.length > 0; const faasEnv = new Map(); - if (awsPresent && !(azurePresent || gcpPresent || vercelPresent)) { + if (isAWSFaaS && !(isAzureFaaS || isGCPFaaS || isVercelFaaS)) { faasEnv.set('name', 'aws.lambda'); - if (isNonEmptyString(process.env.AWS_REGION)) { - faasEnv.set('region', process.env.AWS_REGION); + if (AWS_REGION.length > 0) { + faasEnv.set('region', AWS_REGION); } - const memory_mb = Number(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE); + const memory_mb = Number(AWS_LAMBDA_FUNCTION_MEMORY_SIZE); if (Number.isInteger(memory_mb)) { faasEnv.set('memory_mb', new Int32(memory_mb)); } return faasEnv; - } else if (azurePresent && !(awsPresent || gcpPresent || vercelPresent)) { + } else if (isAzureFaaS && !(isAWSFaaS || isGCPFaaS || isVercelFaaS)) { faasEnv.set('name', 'azure.func'); return faasEnv; - } else if (gcpPresent && !(awsPresent || azurePresent || vercelPresent)) { + } else if (isGCPFaaS && !(isAWSFaaS || isAzureFaaS || isVercelFaaS)) { faasEnv.set('name', 'gcp.func'); - if (isNonEmptyString(process.env.FUNCTION_REGION)) { - faasEnv.set('region', process.env.FUNCTION_REGION); + if (FUNCTION_REGION.length > 0) { + faasEnv.set('region', FUNCTION_REGION); } - const memory_mb = Number(process.env.FUNCTION_MEMORY_MB); + const memory_mb = Number(FUNCTION_MEMORY_MB); if (Number.isInteger(memory_mb)) { faasEnv.set('memory_mb', new Int32(memory_mb)); } - const timeout_sec = Number(process.env.FUNCTION_TIMEOUT_SEC); + const timeout_sec = Number(FUNCTION_TIMEOUT_SEC); if (Number.isInteger(timeout_sec)) { faasEnv.set('timeout_sec', new Int32(timeout_sec)); } return faasEnv; - } else if (vercelPresent && !(awsPresent || azurePresent || gcpPresent)) { + } else if (isVercelFaaS && !(isAWSFaaS || isAzureFaaS || isGCPFaaS)) { faasEnv.set('name', 'vercel'); - if (isNonEmptyString(process.env.VERCEL_URL)) { + if (VERCEL_URL.length > 0) { faasEnv.set('url', process.env.VERCEL_URL); } - if (isNonEmptyString(process.env.VERCEL_REGION)) { + if (VERCEL_REGION.length > 0) { faasEnv.set('region', process.env.VERCEL_REGION); } diff --git a/test/unit/cmap/handshake/client_metadata.test.ts b/test/unit/cmap/handshake/client_metadata.test.ts index b3046bc09d..d337744cd0 100644 --- a/test/unit/cmap/handshake/client_metadata.test.ts +++ b/test/unit/cmap/handshake/client_metadata.test.ts @@ -363,18 +363,16 @@ describe('client metadata module', () => { }); describe('metadata truncation', function () { - beforeEach(() => { - sinon.stub(process, 'env').get(() => ({ - AWS_EXECUTION_ENV: 'iLoveJavaScript', - AWS_REGION: 'a'.repeat(512) - })); - }); - - afterEach(() => { - sinon.restore(); - }); + afterEach(() => sinon.restore()); context('when faas region is too large', () => { + beforeEach('1. Omit fields from `env` except `env.name`.', () => { + sinon.stub(process, 'env').get(() => ({ + AWS_EXECUTION_ENV: 'iLoveJavaScript', + AWS_REGION: 'a'.repeat(512) + })); + }); + it('only includes env.name', () => { const metadata = makeClientMetadata({ driverInfo: {} }); expect(metadata).to.not.have.nested.property('env.region'); @@ -384,20 +382,35 @@ describe('client metadata module', () => { }); context('when os information is too large', () => { - beforeEach(() => { + beforeEach('2. Omit fields from `os` except `os.type`.', () => { + sinon.stub(process, 'env').get(() => ({ + AWS_EXECUTION_ENV: 'iLoveJavaScript', + AWS_REGION: 'abc' + })); sinon.stub(os, 'release').returns('a'.repeat(512)); }); - afterEach(() => { - sinon.restore(); - }); - it('only includes env.name', () => { const metadata = makeClientMetadata({ driverInfo: {} }); - expect(metadata).to.not.have.property('env'); - expect(metadata).to.have.nested.property('os.type', os.type()); + expect(metadata).to.have.property('env'); + expect(metadata).to.have.nested.property('env.region', 'abc'); expect(metadata.os).to.have.all.keys('type'); }); }); + + context('when there is no space for FaaS env', () => { + beforeEach('3. Omit the `env` document entirely.', () => { + sinon.stub(process, 'env').get(() => ({ + AWS_EXECUTION_ENV: 'iLoveJavaScript', + AWS_REGION: 'abc' + })); + sinon.stub(os, 'type').returns('a'.repeat(50)); + }); + + it('omits the faas env', () => { + const metadata = makeClientMetadata({ driverInfo: { name: 'a'.repeat(350) } }); + expect(metadata).to.not.have.property('env'); + }); + }); }); }); From b8108a7ff01d8f97b4bec43d4dd79156550388ea Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Wed, 5 Apr 2023 11:48:00 -0400 Subject: [PATCH 11/30] fix --- src/cmap/handshake/faas_env.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cmap/handshake/faas_env.ts b/src/cmap/handshake/faas_env.ts index 842e32f71f..d3705c7edb 100644 --- a/src/cmap/handshake/faas_env.ts +++ b/src/cmap/handshake/faas_env.ts @@ -64,11 +64,11 @@ export function getFAASEnv(): Map | null { faasEnv.set('name', 'vercel'); if (VERCEL_URL.length > 0) { - faasEnv.set('url', process.env.VERCEL_URL); + faasEnv.set('url', VERCEL_URL); } if (VERCEL_REGION.length > 0) { - faasEnv.set('region', process.env.VERCEL_REGION); + faasEnv.set('region', VERCEL_REGION); } return faasEnv; From 4be609fbce5a91b431ff8ed6d09f65483e429675 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Wed, 5 Apr 2023 12:43:30 -0400 Subject: [PATCH 12/30] test: make sure there's a failure with too large a document --- .../mongodb-handshake.prose.test.ts | 59 +++++++++++++++---- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts b/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts index 9f81bf78d9..a0ebb0c5a1 100644 --- a/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts +++ b/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts @@ -1,8 +1,15 @@ import { expect } from 'chai'; +import * as sinon from 'sinon'; -import { determineFAASProvider, FAASProvider, MongoClient } from '../../mongodb'; +import { + Connection, + getFAASEnv, + LEGACY_HELLO_COMMAND, + MongoClient, + MongoServerSelectionError +} from '../../mongodb'; -context('FAAS Environment Prose Tests', function () { +describe.only('FAAS Environment Prose Tests', function () { let client: MongoClient; afterEach(async function () { @@ -12,12 +19,12 @@ context('FAAS Environment Prose Tests', function () { type EnvironmentVariables = Array<[string, string]>; const tests: Array<{ context: string; - expectedProvider: FAASProvider; + expectedProvider: string | undefined; env: EnvironmentVariables; }> = [ { context: '1. Valid AWS', - expectedProvider: 'aws', + expectedProvider: 'aws.lambda', env: [ ['AWS_EXECUTION_ENV', 'AWS_Lambda_java8'], ['AWS_REGION', 'us-east-2'], @@ -26,12 +33,12 @@ context('FAAS Environment Prose Tests', function () { }, { context: '2. Valid Azure', - expectedProvider: 'azure', + expectedProvider: 'azure.func', env: [['FUNCTIONS_WORKER_RUNTIME', 'node']] }, { context: '3. Valid GCP', - expectedProvider: 'gcp', + expectedProvider: 'gcp.func', env: [ ['K_SERVICE', 'servicename'], ['FUNCTION_MEMORY_MB', '1024'], @@ -49,7 +56,7 @@ context('FAAS Environment Prose Tests', function () { ] }, { - expectedProvider: 'none', + expectedProvider: undefined, context: '5. Invalid - multiple providers', env: [ ['AWS_EXECUTION_ENV', 'AWS_Lambda_java8'], @@ -57,7 +64,7 @@ context('FAAS Environment Prose Tests', function () { ] }, { - expectedProvider: 'aws', + expectedProvider: 'aws.lambda', context: '6. Invalid - long string', env: [ ['AWS_EXECUTION_ENV', 'AWS_Lambda_java8'], @@ -65,7 +72,7 @@ context('FAAS Environment Prose Tests', function () { ] }, { - expectedProvider: 'aws', + expectedProvider: 'aws.lambda', context: '7. Invalid - wrong types', env: [ ['AWS_EXECUTION_ENV', 'AWS_Lambda_java8'], @@ -82,14 +89,13 @@ context('FAAS Environment Prose Tests', function () { } }); after(() => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - for (const [key, _] of env) { + for (const [key] of env) { delete process.env[key]; } }); - before(`metadata confirmation test for ${name}`, function () { - expect(determineFAASProvider()).to.equal( + it(`metadata confirmation test for ${name}`, function () { + expect(getFAASEnv()?.get('name')).to.equal( expectedProvider, 'determined the wrong cloud provider' ); @@ -106,4 +112,31 @@ context('FAAS Environment Prose Tests', function () { }); }); } + + context('when hello is too large', () => { + before(() => { + sinon.stub(Connection.prototype, 'command').callsFake(function (ns, cmd, options, callback) { + const command = Connection.prototype.command.wrappedMethod.bind(this); + + if (cmd.hello || cmd[LEGACY_HELLO_COMMAND]) { + return command( + ns, + { ...cmd, client: { driver: { name: 'a'.repeat(1000) } } }, + options, + callback + ); + } + return command(ns, cmd, options, callback); + }); + }); + + after(() => sinon.restore()); + + it('client fails to connect with an error relating to size', async function () { + client = this.configuration.newClient({ serverSelectionTimeoutMS: 2000 }); + const error = await client.connect().catch(error => error); + expect(error).to.be.instanceOf(MongoServerSelectionError); + expect(error).to.match(/client metadata document must be less/); + }); + }); }); From 70da1d6bb5909a219141394c270cfe37c4d0c8b4 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Wed, 5 Apr 2023 12:51:14 -0400 Subject: [PATCH 13/30] fix: tests --- src/cmap/handshake/faas_env.ts | 18 ++++----- .../mongodb-handshake.prose.test.ts | 38 +------------------ .../mongodb-handshake.test.ts | 36 ++++++++++++++++++ 3 files changed, 47 insertions(+), 45 deletions(-) create mode 100644 test/integration/mongodb-handshake/mongodb-handshake.test.ts diff --git a/src/cmap/handshake/faas_env.ts b/src/cmap/handshake/faas_env.ts index d3705c7edb..434a1bb0fe 100644 --- a/src/cmap/handshake/faas_env.ts +++ b/src/cmap/handshake/faas_env.ts @@ -33,9 +33,11 @@ export function getFAASEnv(): Map | null { faasEnv.set('region', AWS_REGION); } - const memory_mb = Number(AWS_LAMBDA_FUNCTION_MEMORY_SIZE); - if (Number.isInteger(memory_mb)) { - faasEnv.set('memory_mb', new Int32(memory_mb)); + if ( + AWS_LAMBDA_FUNCTION_MEMORY_SIZE.length > 0 && + Number.isInteger(+AWS_LAMBDA_FUNCTION_MEMORY_SIZE) + ) { + faasEnv.set('memory_mb', new Int32(AWS_LAMBDA_FUNCTION_MEMORY_SIZE)); } return faasEnv; @@ -49,14 +51,12 @@ export function getFAASEnv(): Map | null { faasEnv.set('region', FUNCTION_REGION); } - const memory_mb = Number(FUNCTION_MEMORY_MB); - if (Number.isInteger(memory_mb)) { - faasEnv.set('memory_mb', new Int32(memory_mb)); + if (FUNCTION_MEMORY_MB.length > 0 && Number.isInteger(+FUNCTION_MEMORY_MB)) { + faasEnv.set('memory_mb', new Int32(FUNCTION_MEMORY_MB)); } - const timeout_sec = Number(FUNCTION_TIMEOUT_SEC); - if (Number.isInteger(timeout_sec)) { - faasEnv.set('timeout_sec', new Int32(timeout_sec)); + if (FUNCTION_TIMEOUT_SEC.length > 0 && Number.isInteger(+FUNCTION_TIMEOUT_SEC)) { + faasEnv.set('timeout_sec', new Int32(FUNCTION_TIMEOUT_SEC)); } return faasEnv; diff --git a/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts b/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts index a0ebb0c5a1..d51ec137c2 100644 --- a/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts +++ b/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts @@ -1,15 +1,8 @@ import { expect } from 'chai'; -import * as sinon from 'sinon'; -import { - Connection, - getFAASEnv, - LEGACY_HELLO_COMMAND, - MongoClient, - MongoServerSelectionError -} from '../../mongodb'; +import { getFAASEnv, MongoClient } from '../../mongodb'; -describe.only('FAAS Environment Prose Tests', function () { +describe('FAAS Environment Prose Tests', function () { let client: MongoClient; afterEach(async function () { @@ -112,31 +105,4 @@ describe.only('FAAS Environment Prose Tests', function () { }); }); } - - context('when hello is too large', () => { - before(() => { - sinon.stub(Connection.prototype, 'command').callsFake(function (ns, cmd, options, callback) { - const command = Connection.prototype.command.wrappedMethod.bind(this); - - if (cmd.hello || cmd[LEGACY_HELLO_COMMAND]) { - return command( - ns, - { ...cmd, client: { driver: { name: 'a'.repeat(1000) } } }, - options, - callback - ); - } - return command(ns, cmd, options, callback); - }); - }); - - after(() => sinon.restore()); - - it('client fails to connect with an error relating to size', async function () { - client = this.configuration.newClient({ serverSelectionTimeoutMS: 2000 }); - const error = await client.connect().catch(error => error); - expect(error).to.be.instanceOf(MongoServerSelectionError); - expect(error).to.match(/client metadata document must be less/); - }); - }); }); diff --git a/test/integration/mongodb-handshake/mongodb-handshake.test.ts b/test/integration/mongodb-handshake/mongodb-handshake.test.ts new file mode 100644 index 0000000000..366a0eb0d4 --- /dev/null +++ b/test/integration/mongodb-handshake/mongodb-handshake.test.ts @@ -0,0 +1,36 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; + +import { Connection, LEGACY_HELLO_COMMAND, MongoServerSelectionError } from '../../mongodb'; + +describe('MongoDB Handshake Node tests', () => { + let client; + + context('when hello is too large', () => { + before(() => { + sinon.stub(Connection.prototype, 'command').callsFake(function (ns, cmd, options, callback) { + // @ts-expect-error: sinon will place wrappedMethod there + const command = Connection.prototype.command.wrappedMethod.bind(this); + + if (cmd.hello || cmd[LEGACY_HELLO_COMMAND]) { + return command( + ns, + { ...cmd, client: { driver: { name: 'a'.repeat(1000) } } }, + options, + callback + ); + } + return command(ns, cmd, options, callback); + }); + }); + + after(() => sinon.restore()); + + it('client fails to connect with an error relating to size', async function () { + client = this.configuration.newClient({ serverSelectionTimeoutMS: 2000 }); + const error = await client.connect().catch(error => error); + expect(error).to.be.instanceOf(MongoServerSelectionError); + expect(error).to.match(/client metadata document must be less/); + }); + }); +}); From 871d20853f02e560d1a5d4410220d018ccf94a67 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Wed, 5 Apr 2023 13:21:56 -0400 Subject: [PATCH 14/30] fix: lb mode error --- .../mongodb-handshake/mongodb-handshake.test.ts | 13 +++++++++++-- test/unit/cmap/handshake/client_metadata.test.ts | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/test/integration/mongodb-handshake/mongodb-handshake.test.ts b/test/integration/mongodb-handshake/mongodb-handshake.test.ts index 366a0eb0d4..c6636a2298 100644 --- a/test/integration/mongodb-handshake/mongodb-handshake.test.ts +++ b/test/integration/mongodb-handshake/mongodb-handshake.test.ts @@ -1,7 +1,12 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; -import { Connection, LEGACY_HELLO_COMMAND, MongoServerSelectionError } from '../../mongodb'; +import { + Connection, + LEGACY_HELLO_COMMAND, + MongoServerError, + MongoServerSelectionError +} from '../../mongodb'; describe('MongoDB Handshake Node tests', () => { let client; @@ -29,7 +34,11 @@ describe('MongoDB Handshake Node tests', () => { it('client fails to connect with an error relating to size', async function () { client = this.configuration.newClient({ serverSelectionTimeoutMS: 2000 }); const error = await client.connect().catch(error => error); - expect(error).to.be.instanceOf(MongoServerSelectionError); + if (this.configuration.isLoadBalanced) { + expect(error).to.be.instanceOf(MongoServerError); + } else { + expect(error).to.be.instanceOf(MongoServerSelectionError); + } expect(error).to.match(/client metadata document must be less/); }); }); diff --git a/test/unit/cmap/handshake/client_metadata.test.ts b/test/unit/cmap/handshake/client_metadata.test.ts index d337744cd0..286af2f182 100644 --- a/test/unit/cmap/handshake/client_metadata.test.ts +++ b/test/unit/cmap/handshake/client_metadata.test.ts @@ -9,7 +9,7 @@ import { getFAASEnv, Int32, makeClientMetadata } from '../../../mongodb'; const NODE_DRIVER_VERSION = require('../../../../package.json').version; describe('client metadata module', () => { - describe('determineCloudProvider()', function () { + describe('getFAASEnv()', function () { const tests: Array<[string, string]> = [ ['AWS_EXECUTION_ENV', 'aws.lambda'], ['AWS_LAMBDA_RUNTIME_API', 'aws.lambda'], From 6bccf1ef2e9d434eedff811582a94dc11fe85eec Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Wed, 5 Apr 2023 18:27:29 -0400 Subject: [PATCH 15/30] test: os type omission, env omission, merge faas logic --- src/cmap/handshake/client_metadata.ts | 205 +++++++++++++----- src/cmap/handshake/faas_env.ts | 78 ------- src/mongo_client.ts | 18 +- test/mongodb.ts | 1 - .../cmap/handshake/client_metadata.test.ts | 150 ++++++++++--- 5 files changed, 289 insertions(+), 163 deletions(-) delete mode 100644 src/cmap/handshake/faas_env.ts diff --git a/src/cmap/handshake/client_metadata.ts b/src/cmap/handshake/client_metadata.ts index a76b8cc211..a0bb2e9a6b 100644 --- a/src/cmap/handshake/client_metadata.ts +++ b/src/cmap/handshake/client_metadata.ts @@ -2,8 +2,11 @@ import * as os from 'os'; import * as process from 'process'; import { BSON, Int32 } from '../../bson'; +import { MongoInvalidArgumentError } from '../../error'; import type { MongoOptions } from '../../mongo_client'; -import { getFAASEnv } from './faas_env'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const NODE_DRIVER_VERSION = require('../../../package.json').version; /** * @public @@ -24,7 +27,7 @@ export interface ClientMetadata { application?: { name: string; }; - /** Data containing information about the environment, if the driver is running in a FAAS environment. */ + /** FaaS environment information */ env?: { name: 'aws.lambda' | 'gcp.func' | 'azure.func' | 'vercel'; timeout_sec?: Int32; @@ -44,91 +47,181 @@ export interface ClientMetadataOptions { appName?: string; } -// eslint-disable-next-line @typescript-eslint/no-var-requires -const NODE_DRIVER_VERSION = require('../../../package.json').version; - /** @internal */ -class LimitedSizeDocument extends Map { - private static MAX_SIZE = 512; +export class LimitedSizeDocument { + private document = new Map(); + constructor(private maxSize: number) {} - private get bsonByteLength() { - return BSON.serialize(this).byteLength; + private getBsonByteLength() { + return BSON.serialize(this.document).byteLength; } - /** Only adds key/value if the bsonByteLength is less than or equal to MAX_SIZE */ - public setIfFits(key: string, value: Record | string): boolean { - if (this.bsonByteLength >= LimitedSizeDocument.MAX_SIZE) { - return false; - } + /** Only adds key/value if the bsonByteLength is less than MAX_SIZE */ + public ifFitsSets(key: string, value: Record | string): boolean { + this.document.set(key, value); - this.set(key, value); - - if (this.bsonByteLength >= LimitedSizeDocument.MAX_SIZE) { - this.delete(key); + if (this.getBsonByteLength() > this.maxSize) { + this.document.delete(key); return false; } return true; } + + toObject(): ClientMetadata { + return BSON.deserialize(BSON.serialize(this.document), { + promoteLongs: false, + promoteBuffers: false, + promoteValues: false, + useBigInt64: false + }) as ClientMetadata; + } } type MakeClientMetadataOptions = Pick; +/** + * From the specs: + * Implementors SHOULD cumulatively update fields in the following order until the document is under the size limit: + * 1. Omit fields from `env` except `env.name`. + * 2. Omit fields from `os` except `os.type`. + * 3. Omit the `env` document entirely. + * 4. Truncate `platform`. -- special we do not truncate this field + */ export function makeClientMetadata(options: MakeClientMetadataOptions): ClientMetadata { - const metadataDocument = new LimitedSizeDocument(); + const metadataDocument = new LimitedSizeDocument(512); + const { appName = '' } = options; // Add app name first, it must be sent - if (typeof options.appName === 'string' && options.appName.length > 0) { + if (appName.length > 0) { const name = - Buffer.byteLength(options.appName, 'utf8') <= 128 + Buffer.byteLength(appName, 'utf8') <= 128 ? options.appName - : Buffer.from(options.appName, 'utf8').subarray(0, 128).toString('utf8'); - metadataDocument.setIfFits('application', { name }); + : Buffer.from(appName, 'utf8').subarray(0, 128).toString('utf8'); + metadataDocument.ifFitsSets('application', { name }); } - // Driver info goes next, we're not going to be at the limit yet, max bytes used ~128 - const name = - typeof options.driverInfo.name === 'string' && options.driverInfo.name.length > 0 - ? `nodejs|${options.driverInfo.name}` - : 'nodejs'; + const { name = '', version = '', platform = '' } = options.driverInfo; - const version = - typeof options.driverInfo.version === 'string' && options.driverInfo.version.length > 0 - ? `${NODE_DRIVER_VERSION}|${options.driverInfo.version}` - : NODE_DRIVER_VERSION; + const driverInfo = { + name: name.length > 0 ? `nodejs|${name}` : 'nodejs', + version: version.length > 0 ? `${NODE_DRIVER_VERSION}|${version}` : NODE_DRIVER_VERSION + }; - metadataDocument.setIfFits('driver', { name, version }); + if (!metadataDocument.ifFitsSets('driver', driverInfo)) { + throw new MongoInvalidArgumentError('driverInfo name and version exceed limit of 512 bytes'); + } - // Platform likely to make it in, depending on driverInfo.name length - const platform = - typeof options.driverInfo.platform === 'string' && options.driverInfo.platform.length > 0 - ? `Node.js ${process.version}, ${os.endianness()}|${options.driverInfo.platform}` + const platformInfo = + platform.length > 0 + ? `Node.js ${process.version}, ${os.endianness()}|${platform}` : `Node.js ${process.version}, ${os.endianness()}`; - metadataDocument.setIfFits('platform', platform); - - const osInfo = { - type: os.type(), - name: process.platform, - architecture: process.arch, - version: os.release() - }; + if (!metadataDocument.ifFitsSets('platform', platformInfo)) { + throw new MongoInvalidArgumentError('driverInfo platform exceeds the limit of 512 bytes'); + } - if (!metadataDocument.setIfFits('os', osInfo)) { - // Could not add full OS info, add only type - metadataDocument.setIfFits('os', { type: osInfo.type }); + // Note: order matters, os.type is last so it will be removed last if we're at maxSize + const osInfo = new Map() + .set('name', process.platform) + .set('architecture', process.arch) + .set('version', os.release()) + .set('type', os.type()); + + if (!metadataDocument.ifFitsSets('os', osInfo)) { + for (const key of osInfo.keys()) { + osInfo.delete(key); + if (osInfo.size === 0) break; + if (metadataDocument.ifFitsSets('os', osInfo)) break; + } } const faasEnv = getFAASEnv(); if (faasEnv != null) { - if (!metadataDocument.setIfFits('env', faasEnv)) { - metadataDocument.setIfFits('env', { name: faasEnv.get('name') }); + if (!metadataDocument.ifFitsSets('env', faasEnv)) { + for (const key of faasEnv.keys()) { + faasEnv.delete(key); + if (faasEnv.size === 0) break; + if (metadataDocument.ifFitsSets('env', faasEnv)) break; + } } } - return BSON.deserialize(BSON.serialize(metadataDocument), { - promoteLongs: false, - promoteBuffers: false, - promoteValues: false, - useBigInt64: false - }) as ClientMetadata; + return metadataDocument.toObject(); +} + +/** + * Collects FaaS metadata. + * - `name` MUST be the last key in the Map returned. + */ +export function getFAASEnv(): Map | null { + const { + AWS_EXECUTION_ENV = '', + AWS_LAMBDA_RUNTIME_API = '', + FUNCTIONS_WORKER_RUNTIME = '', + K_SERVICE = '', + FUNCTION_NAME = '', + VERCEL = '', + AWS_LAMBDA_FUNCTION_MEMORY_SIZE = '', + AWS_REGION = '', + FUNCTION_MEMORY_MB = '', + FUNCTION_REGION = '', + FUNCTION_TIMEOUT_SEC = '', + VERCEL_URL = '', + VERCEL_REGION = '' + } = process.env; + + const isAWSFaaS = AWS_EXECUTION_ENV.length > 0 || AWS_LAMBDA_RUNTIME_API.length > 0; + const isAzureFaaS = FUNCTIONS_WORKER_RUNTIME.length > 0; + const isGCPFaaS = K_SERVICE.length > 0 || FUNCTION_NAME.length > 0; + const isVercelFaaS = VERCEL.length > 0; + + // Note: order matters, name must always be the last key + const faasEnv = new Map(); + + if (isAWSFaaS && !(isAzureFaaS || isGCPFaaS || isVercelFaaS)) { + if (AWS_REGION.length > 0) { + faasEnv.set('region', AWS_REGION); + } + + if ( + AWS_LAMBDA_FUNCTION_MEMORY_SIZE.length > 0 && + Number.isInteger(+AWS_LAMBDA_FUNCTION_MEMORY_SIZE) + ) { + faasEnv.set('memory_mb', new Int32(AWS_LAMBDA_FUNCTION_MEMORY_SIZE)); + } + + faasEnv.set('name', 'aws.lambda'); + return faasEnv; + } else if (isAzureFaaS && !(isAWSFaaS || isGCPFaaS || isVercelFaaS)) { + faasEnv.set('name', 'azure.func'); + return faasEnv; + } else if (isGCPFaaS && !(isAWSFaaS || isAzureFaaS || isVercelFaaS)) { + if (FUNCTION_REGION.length > 0) { + faasEnv.set('region', FUNCTION_REGION); + } + + if (FUNCTION_MEMORY_MB.length > 0 && Number.isInteger(+FUNCTION_MEMORY_MB)) { + faasEnv.set('memory_mb', new Int32(FUNCTION_MEMORY_MB)); + } + + if (FUNCTION_TIMEOUT_SEC.length > 0 && Number.isInteger(+FUNCTION_TIMEOUT_SEC)) { + faasEnv.set('timeout_sec', new Int32(FUNCTION_TIMEOUT_SEC)); + } + + faasEnv.set('name', 'gcp.func'); + return faasEnv; + } else if (isVercelFaaS && !(isAWSFaaS || isAzureFaaS || isGCPFaaS)) { + if (VERCEL_URL.length > 0) { + faasEnv.set('url', VERCEL_URL); + } + + if (VERCEL_REGION.length > 0) { + faasEnv.set('region', VERCEL_REGION); + } + + faasEnv.set('name', 'vercel'); + return faasEnv; + } else { + return null; + } } diff --git a/src/cmap/handshake/faas_env.ts b/src/cmap/handshake/faas_env.ts deleted file mode 100644 index 434a1bb0fe..0000000000 --- a/src/cmap/handshake/faas_env.ts +++ /dev/null @@ -1,78 +0,0 @@ -import * as process from 'process'; - -import { Int32 } from '../../bson'; - -export function getFAASEnv(): Map | null { - const { - AWS_EXECUTION_ENV = '', - AWS_LAMBDA_RUNTIME_API = '', - FUNCTIONS_WORKER_RUNTIME = '', - K_SERVICE = '', - FUNCTION_NAME = '', - VERCEL = '', - AWS_LAMBDA_FUNCTION_MEMORY_SIZE = '', - AWS_REGION = '', - FUNCTION_MEMORY_MB = '', - FUNCTION_REGION = '', - FUNCTION_TIMEOUT_SEC = '', - VERCEL_URL = '', - VERCEL_REGION = '' - } = process.env; - - const isAWSFaaS = AWS_EXECUTION_ENV.length > 0 || AWS_LAMBDA_RUNTIME_API.length > 0; - const isAzureFaaS = FUNCTIONS_WORKER_RUNTIME.length > 0; - const isGCPFaaS = K_SERVICE.length > 0 || FUNCTION_NAME.length > 0; - const isVercelFaaS = VERCEL.length > 0; - - const faasEnv = new Map(); - - if (isAWSFaaS && !(isAzureFaaS || isGCPFaaS || isVercelFaaS)) { - faasEnv.set('name', 'aws.lambda'); - - if (AWS_REGION.length > 0) { - faasEnv.set('region', AWS_REGION); - } - - if ( - AWS_LAMBDA_FUNCTION_MEMORY_SIZE.length > 0 && - Number.isInteger(+AWS_LAMBDA_FUNCTION_MEMORY_SIZE) - ) { - faasEnv.set('memory_mb', new Int32(AWS_LAMBDA_FUNCTION_MEMORY_SIZE)); - } - - return faasEnv; - } else if (isAzureFaaS && !(isAWSFaaS || isGCPFaaS || isVercelFaaS)) { - faasEnv.set('name', 'azure.func'); - return faasEnv; - } else if (isGCPFaaS && !(isAWSFaaS || isAzureFaaS || isVercelFaaS)) { - faasEnv.set('name', 'gcp.func'); - - if (FUNCTION_REGION.length > 0) { - faasEnv.set('region', FUNCTION_REGION); - } - - if (FUNCTION_MEMORY_MB.length > 0 && Number.isInteger(+FUNCTION_MEMORY_MB)) { - faasEnv.set('memory_mb', new Int32(FUNCTION_MEMORY_MB)); - } - - if (FUNCTION_TIMEOUT_SEC.length > 0 && Number.isInteger(+FUNCTION_TIMEOUT_SEC)) { - faasEnv.set('timeout_sec', new Int32(FUNCTION_TIMEOUT_SEC)); - } - - return faasEnv; - } else if (isVercelFaaS && !(isAWSFaaS || isAzureFaaS || isGCPFaaS)) { - faasEnv.set('name', 'vercel'); - - if (VERCEL_URL.length > 0) { - faasEnv.set('url', VERCEL_URL); - } - - if (VERCEL_REGION.length > 0) { - faasEnv.set('region', VERCEL_REGION); - } - - return faasEnv; - } else { - return null; - } -} diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 21bf61618a..3916b8cceb 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -364,6 +364,7 @@ export class MongoClient extends TypedEventEmitter { }; } + /** {@inheritdoc MongoOptions} */ get options(): Readonly { return Object.freeze({ ...this[kOptions] }); } @@ -661,7 +662,22 @@ export class MongoClient extends TypedEventEmitter { } /** - * Mongo Client Options + * Parsed Mongo Client Options. + * + * User supplied options are documented by `MongoClientOptions`. + * + * **NOTE:** The client's options parsing is subject to change to support new features. + * This type is provided to aid with inspection of options after parsing, it should not be relied upon programmatically. + * + * Options are sourced from: + * - connection string + * - options object passed to the MongoClient constructor + * - file system (ex. tls settings) + * - environment variables + * - DNS SRV records and TXT records + * + * Not all options may be present after client construction as some are obtained from asynchronous operations. + * * @public */ export interface MongoOptions diff --git a/test/mongodb.ts b/test/mongodb.ts index 6a5cdf77ca..b209ffbdf2 100644 --- a/test/mongodb.ts +++ b/test/mongodb.ts @@ -123,7 +123,6 @@ export * from '../src/cmap/connection_pool'; export * from '../src/cmap/connection_pool_events'; export * from '../src/cmap/errors'; export * from '../src/cmap/handshake/client_metadata'; -export * from '../src/cmap/handshake/faas_env'; export * from '../src/cmap/message_stream'; export * from '../src/cmap/metrics'; export * from '../src/cmap/stream_description'; diff --git a/test/unit/cmap/handshake/client_metadata.test.ts b/test/unit/cmap/handshake/client_metadata.test.ts index 286af2f182..9023d6cc99 100644 --- a/test/unit/cmap/handshake/client_metadata.test.ts +++ b/test/unit/cmap/handshake/client_metadata.test.ts @@ -3,12 +3,37 @@ import * as os from 'os'; import * as process from 'process'; import * as sinon from 'sinon'; -import { getFAASEnv, Int32, makeClientMetadata } from '../../../mongodb'; - -// eslint-disable-next-line @typescript-eslint/no-var-requires -const NODE_DRIVER_VERSION = require('../../../../package.json').version; +import { version as NODE_DRIVER_VERSION } from '../../../../package.json'; +import { + getFAASEnv, + Int32, + LimitedSizeDocument, + makeClientMetadata, + MongoInvalidArgumentError, + ObjectId +} from '../../../mongodb'; describe('client metadata module', () => { + describe('class LimitedSizeDocument', () => { + // For the sake of testing the size limiter features + // We test document: { _id: ObjectId() } + // 4 bytes + 1 type byte + 4 bytes for key + 12 bytes Oid + 1 null term byte + // = 22 bytes + + it('sets key and value that fit within maxSize', () => { + const doc = new LimitedSizeDocument(22); + expect(doc.ifFitsSets('_id', new ObjectId())).to.be.true; + expect(doc.toObject()).to.have.all.keys('_id'); + }); + + it('ignores ifFitsSets that are over size', () => { + const doc = new LimitedSizeDocument(22); + expect(doc.ifFitsSets('_id', new ObjectId())).to.be.true; + expect(doc.ifFitsSets('_id2', '')).to.be.false; + expect(doc.toObject()).to.have.all.keys('_id'); + }); + }); + describe('getFAASEnv()', function () { const tests: Array<[string, string]> = [ ['AWS_EXECUTION_ENV', 'aws.lambda'], @@ -76,6 +101,7 @@ describe('client metadata module', () => { }); }); }); + context('when driverInfo.platform is provided', () => { it('appends driverInfo.platform to the platform field', () => { const options = { @@ -289,6 +315,17 @@ describe('client metadata module', () => { name: 'gcp.func', region: 'region' } + }, + { + context: 'FUNCTION_TIMEOUT_SEC provided', + env: [ + ['FUNCTION_NAME', 'non-empty'], + ['FUNCTION_TIMEOUT_SEC', '12345'] + ], + outcome: { + name: 'gcp.func', + timeout_sec: new Int32(12345) + } } ], vercel: [ @@ -324,22 +361,41 @@ describe('client metadata module', () => { ] }; - for (const [provider, _tests] of Object.entries(tests)) { + for (const [provider, testsForEnv] of Object.entries(tests)) { context(provider, () => { - for (const { context, env: _env, outcome } of _tests) { - it(context, () => { - for (const [k, v] of _env) { + for (const { context, env: faasVariables, outcome } of testsForEnv) { + const setupEnv = () => { + for (const [k, v] of faasVariables) { if (v != null) { process.env[k] = v; } } + }; - const { env } = makeClientMetadata({ driverInfo: {} }); - expect(env).to.deep.equal(outcome); - - for (const [k] of _env) { + const cleanupEnv = () => { + for (const [k] of faasVariables) { delete process.env[k]; } + }; + + it(context, () => { + try { + setupEnv(); + const { env } = makeClientMetadata({ driverInfo: {} }); + expect(env).to.deep.equal(outcome); + } finally { + cleanupEnv(); + } + }); + + it('always places name as the last key', () => { + try { + setupEnv(); + const keys = Array.from(getFAASEnv()?.keys() ?? []); + expect(keys).to.have.property(`${keys.length - 1}`, 'name'); + } finally { + cleanupEnv(); + } }); } }); @@ -347,13 +403,13 @@ describe('client metadata module', () => { context('when a numeric FAAS env variable is not numerically parsable', () => { before(() => { - process.env['AWS_EXECUTION_ENV'] = 'non-empty-string'; - process.env['AWS_LAMBDA_FUNCTION_MEMORY_SIZE'] = 'not numeric'; + process.env.AWS_EXECUTION_ENV = 'non-empty-string'; + process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE = '123not numeric'; }); after(() => { - delete process.env['AWS_EXECUTION_ENV']; - delete process.env['AWS_LAMBDA_FUNCTION_MEMORY_SIZE']; + delete process.env.AWS_EXECUTION_ENV; + delete process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE; }); it('does not attach it to the metadata', () => { @@ -365,6 +421,29 @@ describe('client metadata module', () => { describe('metadata truncation', function () { afterEach(() => sinon.restore()); + context('when driverInfo is too large', () => { + it('throws an error relating to name', () => { + expect(() => makeClientMetadata({ driverInfo: { name: 'a'.repeat(512) } })).to.throw( + MongoInvalidArgumentError, + /name/ + ); + }); + + it('throws an error relating to version', () => { + expect(() => makeClientMetadata({ driverInfo: { version: 'a'.repeat(512) } })).to.throw( + MongoInvalidArgumentError, + /version/ + ); + }); + + it('throws an error relating to platform', () => { + expect(() => makeClientMetadata({ driverInfo: { platform: 'a'.repeat(512) } })).to.throw( + MongoInvalidArgumentError, + /platform/ + ); + }); + }); + context('when faas region is too large', () => { beforeEach('1. Omit fields from `env` except `env.name`.', () => { sinon.stub(process, 'env').get(() => ({ @@ -382,19 +461,36 @@ describe('client metadata module', () => { }); context('when os information is too large', () => { - beforeEach('2. Omit fields from `os` except `os.type`.', () => { - sinon.stub(process, 'env').get(() => ({ - AWS_EXECUTION_ENV: 'iLoveJavaScript', - AWS_REGION: 'abc' - })); - sinon.stub(os, 'release').returns('a'.repeat(512)); + context('release too large', () => { + beforeEach('2. Omit fields from `os` except `os.type`.', () => { + sinon.stub(process, 'env').get(() => ({ + AWS_EXECUTION_ENV: 'iLoveJavaScript', + AWS_REGION: 'abc' + })); + sinon.stub(os, 'release').returns('a'.repeat(512)); + }); + + it('only includes env.name', () => { + const metadata = makeClientMetadata({ driverInfo: {} }); + expect(metadata).to.have.property('env'); + expect(metadata).to.have.nested.property('env.region', 'abc'); + expect(metadata.os).to.have.all.keys('type'); + }); }); - it('only includes env.name', () => { - const metadata = makeClientMetadata({ driverInfo: {} }); - expect(metadata).to.have.property('env'); - expect(metadata).to.have.nested.property('env.region', 'abc'); - expect(metadata.os).to.have.all.keys('type'); + context('os.type too large', () => { + beforeEach(() => { + sinon.stub(process, 'env').get(() => ({ + AWS_EXECUTION_ENV: 'iLoveJavaScript', + AWS_REGION: 'abc' + })); + sinon.stub(os, 'type').returns('a'.repeat(512)); + }); + + it('omits os information', () => { + const metadata = makeClientMetadata({ driverInfo: {} }); + expect(metadata).to.not.have.property('os'); + }); }); }); From 01b91405fc4cd3ecccdc3afd9757e2207c69fafd Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Thu, 6 Apr 2023 09:52:44 -0400 Subject: [PATCH 16/30] inheritdoc unsupported --- src/mongo_client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 3916b8cceb..6709699f0a 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -364,7 +364,7 @@ export class MongoClient extends TypedEventEmitter { }; } - /** {@inheritdoc MongoOptions} */ + /** @see MongoOptions */ get options(): Readonly { return Object.freeze({ ...this[kOptions] }); } From 757cc0b952c6215af1651b3a09ca7c19112121d6 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Fri, 7 Apr 2023 09:57:58 -0400 Subject: [PATCH 17/30] perf: only serialize new element for fits check --- src/cmap/handshake/client_metadata.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/cmap/handshake/client_metadata.ts b/src/cmap/handshake/client_metadata.ts index a0bb2e9a6b..1eaa0ff408 100644 --- a/src/cmap/handshake/client_metadata.ts +++ b/src/cmap/handshake/client_metadata.ts @@ -50,21 +50,24 @@ export interface ClientMetadataOptions { /** @internal */ export class LimitedSizeDocument { private document = new Map(); + /** BSON overhead: Int32 + Null byte */ + private documentSize = 5; constructor(private maxSize: number) {} - private getBsonByteLength() { - return BSON.serialize(this.document).byteLength; - } - /** Only adds key/value if the bsonByteLength is less than MAX_SIZE */ public ifFitsSets(key: string, value: Record | string): boolean { - this.document.set(key, value); + // The BSON byteLength of the new element is the same as serializing it to it's own document + // subtracting the document size int32 and the null terminator. + const newElementSize = BSON.serialize(new Map().set(key, value)).byteLength - 5; - if (this.getBsonByteLength() > this.maxSize) { - this.document.delete(key); + if (newElementSize + this.documentSize > this.maxSize) { return false; } + this.documentSize += newElementSize; + + this.document.set(key, value); + return true; } From 8f28668ccb2eca622273760e951e0ffdf5dd3dfd Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Fri, 7 Apr 2023 15:29:05 -0400 Subject: [PATCH 18/30] fix: vercel having aws env vars --- src/cmap/handshake/client_metadata.ts | 56 ++++++++++--------- .../cmap/handshake/client_metadata.test.ts | 49 +++++++++++----- 2 files changed, 67 insertions(+), 38 deletions(-) diff --git a/src/cmap/handshake/client_metadata.ts b/src/cmap/handshake/client_metadata.ts index 1eaa0ff408..aec8dd7519 100644 --- a/src/cmap/handshake/client_metadata.ts +++ b/src/cmap/handshake/client_metadata.ts @@ -55,8 +55,8 @@ export class LimitedSizeDocument { constructor(private maxSize: number) {} /** Only adds key/value if the bsonByteLength is less than MAX_SIZE */ - public ifFitsSets(key: string, value: Record | string): boolean { - // The BSON byteLength of the new element is the same as serializing it to it's own document + public ifItFitsItSits(key: string, value: Record | string): boolean { + // The BSON byteLength of the new element is the same as serializing it to its own document // subtracting the document size int32 and the null terminator. const newElementSize = BSON.serialize(new Map().set(key, value)).byteLength - 5; @@ -100,7 +100,7 @@ export function makeClientMetadata(options: MakeClientMetadataOptions): ClientMe Buffer.byteLength(appName, 'utf8') <= 128 ? options.appName : Buffer.from(appName, 'utf8').subarray(0, 128).toString('utf8'); - metadataDocument.ifFitsSets('application', { name }); + metadataDocument.ifItFitsItSits('application', { name }); } const { name = '', version = '', platform = '' } = options.driverInfo; @@ -110,7 +110,7 @@ export function makeClientMetadata(options: MakeClientMetadataOptions): ClientMe version: version.length > 0 ? `${NODE_DRIVER_VERSION}|${version}` : NODE_DRIVER_VERSION }; - if (!metadataDocument.ifFitsSets('driver', driverInfo)) { + if (!metadataDocument.ifItFitsItSits('driver', driverInfo)) { throw new MongoInvalidArgumentError('driverInfo name and version exceed limit of 512 bytes'); } @@ -119,7 +119,7 @@ export function makeClientMetadata(options: MakeClientMetadataOptions): ClientMe ? `Node.js ${process.version}, ${os.endianness()}|${platform}` : `Node.js ${process.version}, ${os.endianness()}`; - if (!metadataDocument.ifFitsSets('platform', platformInfo)) { + if (!metadataDocument.ifItFitsItSits('platform', platformInfo)) { throw new MongoInvalidArgumentError('driverInfo platform exceeds the limit of 512 bytes'); } @@ -130,21 +130,21 @@ export function makeClientMetadata(options: MakeClientMetadataOptions): ClientMe .set('version', os.release()) .set('type', os.type()); - if (!metadataDocument.ifFitsSets('os', osInfo)) { + if (!metadataDocument.ifItFitsItSits('os', osInfo)) { for (const key of osInfo.keys()) { osInfo.delete(key); if (osInfo.size === 0) break; - if (metadataDocument.ifFitsSets('os', osInfo)) break; + if (metadataDocument.ifItFitsItSits('os', osInfo)) break; } } const faasEnv = getFAASEnv(); if (faasEnv != null) { - if (!metadataDocument.ifFitsSets('env', faasEnv)) { + if (!metadataDocument.ifItFitsItSits('env', faasEnv)) { for (const key of faasEnv.keys()) { faasEnv.delete(key); if (faasEnv.size === 0) break; - if (metadataDocument.ifFitsSets('env', faasEnv)) break; + if (metadataDocument.ifItFitsItSits('env', faasEnv)) break; } } } @@ -181,7 +181,20 @@ export function getFAASEnv(): Map | null { // Note: order matters, name must always be the last key const faasEnv = new Map(); - if (isAWSFaaS && !(isAzureFaaS || isGCPFaaS || isVercelFaaS)) { + if (isVercelFaaS && !(isAzureFaaS || isGCPFaaS)) { + if (VERCEL_URL.length > 0) { + faasEnv.set('url', VERCEL_URL); + } + + if (VERCEL_REGION.length > 0) { + faasEnv.set('region', VERCEL_REGION); + } + + faasEnv.set('name', 'vercel'); + return faasEnv; + } + + if (isAWSFaaS && !(isAzureFaaS || isGCPFaaS)) { if (AWS_REGION.length > 0) { faasEnv.set('region', AWS_REGION); } @@ -195,10 +208,14 @@ export function getFAASEnv(): Map | null { faasEnv.set('name', 'aws.lambda'); return faasEnv; - } else if (isAzureFaaS && !(isAWSFaaS || isGCPFaaS || isVercelFaaS)) { + } + + if (isAzureFaaS && !isGCPFaaS) { faasEnv.set('name', 'azure.func'); return faasEnv; - } else if (isGCPFaaS && !(isAWSFaaS || isAzureFaaS || isVercelFaaS)) { + } + + if (isGCPFaaS) { if (FUNCTION_REGION.length > 0) { faasEnv.set('region', FUNCTION_REGION); } @@ -213,18 +230,7 @@ export function getFAASEnv(): Map | null { faasEnv.set('name', 'gcp.func'); return faasEnv; - } else if (isVercelFaaS && !(isAWSFaaS || isAzureFaaS || isGCPFaaS)) { - if (VERCEL_URL.length > 0) { - faasEnv.set('url', VERCEL_URL); - } - - if (VERCEL_REGION.length > 0) { - faasEnv.set('region', VERCEL_REGION); - } - - faasEnv.set('name', 'vercel'); - return faasEnv; - } else { - return null; } + + return null; } diff --git a/test/unit/cmap/handshake/client_metadata.test.ts b/test/unit/cmap/handshake/client_metadata.test.ts index 9023d6cc99..6dd34ec27b 100644 --- a/test/unit/cmap/handshake/client_metadata.test.ts +++ b/test/unit/cmap/handshake/client_metadata.test.ts @@ -22,14 +22,14 @@ describe('client metadata module', () => { it('sets key and value that fit within maxSize', () => { const doc = new LimitedSizeDocument(22); - expect(doc.ifFitsSets('_id', new ObjectId())).to.be.true; + expect(doc.ifItFitsItSits('_id', new ObjectId())).to.be.true; expect(doc.toObject()).to.have.all.keys('_id'); }); - it('ignores ifFitsSets that are over size', () => { + it('ignores ifItFitsItSits that are over size', () => { const doc = new LimitedSizeDocument(22); - expect(doc.ifFitsSets('_id', new ObjectId())).to.be.true; - expect(doc.ifFitsSets('_id2', '')).to.be.false; + expect(doc.ifItFitsItSits('_id', new ObjectId())).to.be.true; + expect(doc.ifItFitsItSits('_id2', '')).to.be.false; expect(doc.toObject()).to.have.all.keys('_id'); }); }); @@ -64,16 +64,39 @@ describe('client metadata module', () => { }); context('when there is data from multiple cloud providers in the env', () => { - before(() => { - process.env.AWS_EXECUTION_ENV = 'non-empty-string'; - process.env.FUNCTIONS_WORKER_RUNTIME = 'non-empty-string'; - }); - after(() => { - delete process.env.AWS_EXECUTION_ENV; - delete process.env.FUNCTIONS_WORKER_RUNTIME; + context('unrelated environments', () => { + before(() => { + // aws + process.env.AWS_EXECUTION_ENV = 'non-empty-string'; + // azure + process.env.FUNCTIONS_WORKER_RUNTIME = 'non-empty-string'; + }); + after(() => { + delete process.env.AWS_EXECUTION_ENV; + delete process.env.FUNCTIONS_WORKER_RUNTIME; + }); + it('parses no FAAS provider', () => { + expect(getFAASEnv()).to.be.null; + }); }); - it('parses no FAAS provider', () => { - expect(getFAASEnv()).to.be.null; + + context('vercel and aws which share env variables', () => { + before(() => { + // vercel + process.env.VERCEL = 'non-empty-string'; + // aws + process.env.AWS_EXECUTION_ENV = 'non-empty-string'; + process.env.AWS_LAMBDA_RUNTIME_API = 'non-empty-string'; + }); + after(() => { + delete process.env.VERCEL; + delete process.env.AWS_EXECUTION_ENV; + delete process.env.AWS_LAMBDA_RUNTIME_API; + }); + + it('parses vercel', () => { + expect(getFAASEnv()?.get('name')).to.equal('vercel'); + }); }); }); }); From beba14adfad40a03cd72b59a1eb5edd28908fb88 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Fri, 7 Apr 2023 15:41:05 -0400 Subject: [PATCH 19/30] fix: gcp bool --- src/cmap/handshake/client_metadata.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/cmap/handshake/client_metadata.ts b/src/cmap/handshake/client_metadata.ts index aec8dd7519..66272a52b3 100644 --- a/src/cmap/handshake/client_metadata.ts +++ b/src/cmap/handshake/client_metadata.ts @@ -192,9 +192,7 @@ export function getFAASEnv(): Map | null { faasEnv.set('name', 'vercel'); return faasEnv; - } - - if (isAWSFaaS && !(isAzureFaaS || isGCPFaaS)) { + } else if (isAWSFaaS && !(isAzureFaaS || isGCPFaaS || isVercelFaaS)) { if (AWS_REGION.length > 0) { faasEnv.set('region', AWS_REGION); } @@ -208,14 +206,10 @@ export function getFAASEnv(): Map | null { faasEnv.set('name', 'aws.lambda'); return faasEnv; - } - - if (isAzureFaaS && !isGCPFaaS) { + } else if (isAzureFaaS && !(isGCPFaaS || isAWSFaaS || isVercelFaaS)) { faasEnv.set('name', 'azure.func'); return faasEnv; - } - - if (isGCPFaaS) { + } else if (isGCPFaaS && !(isAzureFaaS || isAWSFaaS || isVercelFaaS)) { if (FUNCTION_REGION.length > 0) { faasEnv.set('region', FUNCTION_REGION); } From d82591dfdc35a890442b0c1f7ca7392db5d076b6 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Fri, 7 Apr 2023 15:51:16 -0400 Subject: [PATCH 20/30] style: early return = no else --- src/cmap/handshake/client_metadata.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/cmap/handshake/client_metadata.ts b/src/cmap/handshake/client_metadata.ts index 66272a52b3..bf3de0f8b3 100644 --- a/src/cmap/handshake/client_metadata.ts +++ b/src/cmap/handshake/client_metadata.ts @@ -192,7 +192,9 @@ export function getFAASEnv(): Map | null { faasEnv.set('name', 'vercel'); return faasEnv; - } else if (isAWSFaaS && !(isAzureFaaS || isGCPFaaS || isVercelFaaS)) { + } + + if (isAWSFaaS && !(isAzureFaaS || isGCPFaaS || isVercelFaaS)) { if (AWS_REGION.length > 0) { faasEnv.set('region', AWS_REGION); } @@ -206,10 +208,14 @@ export function getFAASEnv(): Map | null { faasEnv.set('name', 'aws.lambda'); return faasEnv; - } else if (isAzureFaaS && !(isGCPFaaS || isAWSFaaS || isVercelFaaS)) { + } + + if (isAzureFaaS && !(isGCPFaaS || isAWSFaaS || isVercelFaaS)) { faasEnv.set('name', 'azure.func'); return faasEnv; - } else if (isGCPFaaS && !(isAzureFaaS || isAWSFaaS || isVercelFaaS)) { + } + + if (isGCPFaaS && !(isAzureFaaS || isAWSFaaS || isVercelFaaS)) { if (FUNCTION_REGION.length > 0) { faasEnv.set('region', FUNCTION_REGION); } From 85a455192e578eda0b7985f2f6df1e9011c13963 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Fri, 7 Apr 2023 15:53:32 -0400 Subject: [PATCH 21/30] chore: add comment about vercel --- src/cmap/handshake/client_metadata.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cmap/handshake/client_metadata.ts b/src/cmap/handshake/client_metadata.ts index bf3de0f8b3..2cde498dfc 100644 --- a/src/cmap/handshake/client_metadata.ts +++ b/src/cmap/handshake/client_metadata.ts @@ -181,6 +181,7 @@ export function getFAASEnv(): Map | null { // Note: order matters, name must always be the last key const faasEnv = new Map(); + // When isVercelFaaS is true so is isAWSFaaS; Vercel inherits the AWS env if (isVercelFaaS && !(isAzureFaaS || isGCPFaaS)) { if (VERCEL_URL.length > 0) { faasEnv.set('url', VERCEL_URL); From d20c3330877a742d23ca1179414c09428929a503 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Mon, 10 Apr 2023 11:22:30 -0400 Subject: [PATCH 22/30] fix: update error message --- src/cmap/handshake/client_metadata.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/cmap/handshake/client_metadata.ts b/src/cmap/handshake/client_metadata.ts index 2cde498dfc..bd1dc00765 100644 --- a/src/cmap/handshake/client_metadata.ts +++ b/src/cmap/handshake/client_metadata.ts @@ -111,7 +111,9 @@ export function makeClientMetadata(options: MakeClientMetadataOptions): ClientMe }; if (!metadataDocument.ifItFitsItSits('driver', driverInfo)) { - throw new MongoInvalidArgumentError('driverInfo name and version exceed limit of 512 bytes'); + throw new MongoInvalidArgumentError( + 'Unable to include driverInfo name and version, metadata cannot exceed 512 bytes' + ); } const platformInfo = @@ -120,7 +122,9 @@ export function makeClientMetadata(options: MakeClientMetadataOptions): ClientMe : `Node.js ${process.version}, ${os.endianness()}`; if (!metadataDocument.ifItFitsItSits('platform', platformInfo)) { - throw new MongoInvalidArgumentError('driverInfo platform exceeds the limit of 512 bytes'); + throw new MongoInvalidArgumentError( + 'Unable to include driverInfo platform, metadata cannot exceed 512 bytes' + ); } // Note: order matters, os.type is last so it will be removed last if we're at maxSize From 69c01d7a8025223df917a1b9613c5863d672a834 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Mon, 10 Apr 2023 11:41:50 -0400 Subject: [PATCH 23/30] fix: rm vercel_url --- src/cmap/handshake/client_metadata.ts | 5 ----- .../mongodb-handshake/mongodb-handshake.prose.test.ts | 1 - test/unit/cmap/handshake/client_metadata.test.ts | 11 ----------- 3 files changed, 17 deletions(-) diff --git a/src/cmap/handshake/client_metadata.ts b/src/cmap/handshake/client_metadata.ts index bd1dc00765..0c00af9a8e 100644 --- a/src/cmap/handshake/client_metadata.ts +++ b/src/cmap/handshake/client_metadata.ts @@ -173,7 +173,6 @@ export function getFAASEnv(): Map | null { FUNCTION_MEMORY_MB = '', FUNCTION_REGION = '', FUNCTION_TIMEOUT_SEC = '', - VERCEL_URL = '', VERCEL_REGION = '' } = process.env; @@ -187,10 +186,6 @@ export function getFAASEnv(): Map | null { // When isVercelFaaS is true so is isAWSFaaS; Vercel inherits the AWS env if (isVercelFaaS && !(isAzureFaaS || isGCPFaaS)) { - if (VERCEL_URL.length > 0) { - faasEnv.set('url', VERCEL_URL); - } - if (VERCEL_REGION.length > 0) { faasEnv.set('region', VERCEL_REGION); } diff --git a/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts b/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts index d51ec137c2..498a257c2b 100644 --- a/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts +++ b/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts @@ -44,7 +44,6 @@ describe('FAAS Environment Prose Tests', function () { expectedProvider: 'vercel', env: [ ['VERCEL', '1'], - ['VERCEL_URL', '*.vercel.app'], ['VERCEL_REGION', 'cdg1'] ] }, diff --git a/test/unit/cmap/handshake/client_metadata.test.ts b/test/unit/cmap/handshake/client_metadata.test.ts index 6dd34ec27b..3a69cc0b30 100644 --- a/test/unit/cmap/handshake/client_metadata.test.ts +++ b/test/unit/cmap/handshake/client_metadata.test.ts @@ -359,17 +359,6 @@ describe('client metadata module', () => { name: 'vercel' } }, - { - context: 'VERCEL_URL provided', - env: [ - ['VERCEL', 'non-empty'], - ['VERCEL_URL', 'provided-url'] - ], - outcome: { - name: 'vercel', - url: 'provided-url' - } - }, { context: 'VERCEL_REGION provided', env: [ From 5cc2efb775bfd0a3c064db15cf602938e958f50c Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Tue, 11 Apr 2023 17:34:13 -0400 Subject: [PATCH 24/30] titles Co-authored-by: Daria Pardue --- .../mongodb-handshake/mongodb-handshake.prose.test.ts | 2 +- test/integration/mongodb-handshake/mongodb-handshake.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts b/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts index 498a257c2b..ba4c714f35 100644 --- a/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts +++ b/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import { getFAASEnv, MongoClient } from '../../mongodb'; -describe('FAAS Environment Prose Tests', function () { +describe('Handshake Prose Tests', function () { let client: MongoClient; afterEach(async function () { diff --git a/test/integration/mongodb-handshake/mongodb-handshake.test.ts b/test/integration/mongodb-handshake/mongodb-handshake.test.ts index c6636a2298..fa4b717792 100644 --- a/test/integration/mongodb-handshake/mongodb-handshake.test.ts +++ b/test/integration/mongodb-handshake/mongodb-handshake.test.ts @@ -8,7 +8,7 @@ import { MongoServerSelectionError } from '../../mongodb'; -describe('MongoDB Handshake Node tests', () => { +describe('MongoDB Handshake', () => { let client; context('when hello is too large', () => { @@ -31,7 +31,7 @@ describe('MongoDB Handshake Node tests', () => { after(() => sinon.restore()); - it('client fails to connect with an error relating to size', async function () { + it('should fail with an error relating to size', async function () { client = this.configuration.newClient({ serverSelectionTimeoutMS: 2000 }); const error = await client.connect().catch(error => error); if (this.configuration.isLoadBalanced) { From 6c82746d9bcea54e44ff42a4bca7673c4879fa07 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Tue, 11 Apr 2023 17:35:23 -0400 Subject: [PATCH 25/30] undo cmap change --- test/tools/cmap_spec_runner.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/test/tools/cmap_spec_runner.ts b/test/tools/cmap_spec_runner.ts index 6292eaae77..7f21a8bc34 100644 --- a/test/tools/cmap_spec_runner.ts +++ b/test/tools/cmap_spec_runner.ts @@ -370,10 +370,7 @@ async function runCmapTest(test: CmapTest, threadContext: ThreadContext) { delete poolOptions.backgroundThreadIntervalMS; } - const metadata = makeClientMetadata({ - appName: poolOptions.appName, - driverInfo: {} - }); + const metadata = makeClientMetadata({ appName: poolOptions.appName, driverInfo: {} }); delete poolOptions.appName; const operations = test.operations; @@ -385,11 +382,7 @@ async function runCmapTest(test: CmapTest, threadContext: ThreadContext) { const mainThread = threadContext.getThread(MAIN_THREAD_KEY); mainThread.start(); - threadContext.createPool({ - ...poolOptions, - metadata, - minPoolSizeCheckFrequencyMS - }); + threadContext.createPool({ ...poolOptions, metadata, minPoolSizeCheckFrequencyMS }); // yield control back to the event loop so that the ConnectionPoolCreatedEvent // has a chance to be fired before any synchronously-emitted events from // the queued operations From 83f5e6c6964aecce859fc8df9febc997d69d99e8 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Tue, 11 Apr 2023 17:42:49 -0400 Subject: [PATCH 26/30] titles2 Co-authored-by: Daria Pardue --- test/unit/cmap/handshake/client_metadata.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/unit/cmap/handshake/client_metadata.test.ts b/test/unit/cmap/handshake/client_metadata.test.ts index 3a69cc0b30..6834965537 100644 --- a/test/unit/cmap/handshake/client_metadata.test.ts +++ b/test/unit/cmap/handshake/client_metadata.test.ts @@ -14,19 +14,19 @@ import { } from '../../../mongodb'; describe('client metadata module', () => { - describe('class LimitedSizeDocument', () => { + describe('new LimitedSizeDocument()', () => { // For the sake of testing the size limiter features // We test document: { _id: ObjectId() } // 4 bytes + 1 type byte + 4 bytes for key + 12 bytes Oid + 1 null term byte // = 22 bytes - it('sets key and value that fit within maxSize', () => { + it('allows setting a key and value that fit within maxSize', () => { const doc = new LimitedSizeDocument(22); expect(doc.ifItFitsItSits('_id', new ObjectId())).to.be.true; expect(doc.toObject()).to.have.all.keys('_id'); }); - it('ignores ifItFitsItSits that are over size', () => { + it('ignores attempts to set key-value pairs that are over size', () => { const doc = new LimitedSizeDocument(22); expect(doc.ifItFitsItSits('_id', new ObjectId())).to.be.true; expect(doc.ifItFitsItSits('_id2', '')).to.be.false; @@ -58,7 +58,7 @@ describe('client metadata module', () => { } context('when there is no FAAS provider data in the env', () => { - it('parses no FAAS provider', () => { + it('returns null', () => { expect(getFAASEnv()).to.be.null; }); }); @@ -75,7 +75,7 @@ describe('client metadata module', () => { delete process.env.AWS_EXECUTION_ENV; delete process.env.FUNCTIONS_WORKER_RUNTIME; }); - it('parses no FAAS provider', () => { + it('returns null, () => { expect(getFAASEnv()).to.be.null; }); }); From 6c4d67d53ba3731265ea6ccb9569994705a3566c Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Tue, 11 Apr 2023 17:44:32 -0400 Subject: [PATCH 27/30] lint --- test/unit/cmap/handshake/client_metadata.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/cmap/handshake/client_metadata.test.ts b/test/unit/cmap/handshake/client_metadata.test.ts index 6834965537..279c02805e 100644 --- a/test/unit/cmap/handshake/client_metadata.test.ts +++ b/test/unit/cmap/handshake/client_metadata.test.ts @@ -75,7 +75,7 @@ describe('client metadata module', () => { delete process.env.AWS_EXECUTION_ENV; delete process.env.FUNCTIONS_WORKER_RUNTIME; }); - it('returns null, () => { + it('returns null', () => { expect(getFAASEnv()).to.be.null; }); }); From 2c9b4c4c015fd837442a4a5ae4ff9e5b0abfbb6b Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Tue, 11 Apr 2023 17:47:19 -0400 Subject: [PATCH 28/30] moved throwing tests --- .../cmap/handshake/client_metadata.test.ts | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/test/unit/cmap/handshake/client_metadata.test.ts b/test/unit/cmap/handshake/client_metadata.test.ts index 279c02805e..c37ad63a0a 100644 --- a/test/unit/cmap/handshake/client_metadata.test.ts +++ b/test/unit/cmap/handshake/client_metadata.test.ts @@ -125,6 +125,29 @@ describe('client metadata module', () => { }); }); + context('when driverInfo is too large', () => { + it('throws an error relating to name', () => { + expect(() => makeClientMetadata({ driverInfo: { name: 'a'.repeat(512) } })).to.throw( + MongoInvalidArgumentError, + /name/ + ); + }); + + it('throws an error relating to version', () => { + expect(() => makeClientMetadata({ driverInfo: { version: 'a'.repeat(512) } })).to.throw( + MongoInvalidArgumentError, + /version/ + ); + }); + + it('throws an error relating to platform', () => { + expect(() => makeClientMetadata({ driverInfo: { platform: 'a'.repeat(512) } })).to.throw( + MongoInvalidArgumentError, + /platform/ + ); + }); + }); + context('when driverInfo.platform is provided', () => { it('appends driverInfo.platform to the platform field', () => { const options = { @@ -433,29 +456,6 @@ describe('client metadata module', () => { describe('metadata truncation', function () { afterEach(() => sinon.restore()); - context('when driverInfo is too large', () => { - it('throws an error relating to name', () => { - expect(() => makeClientMetadata({ driverInfo: { name: 'a'.repeat(512) } })).to.throw( - MongoInvalidArgumentError, - /name/ - ); - }); - - it('throws an error relating to version', () => { - expect(() => makeClientMetadata({ driverInfo: { version: 'a'.repeat(512) } })).to.throw( - MongoInvalidArgumentError, - /version/ - ); - }); - - it('throws an error relating to platform', () => { - expect(() => makeClientMetadata({ driverInfo: { platform: 'a'.repeat(512) } })).to.throw( - MongoInvalidArgumentError, - /platform/ - ); - }); - }); - context('when faas region is too large', () => { beforeEach('1. Omit fields from `env` except `env.name`.', () => { sinon.stub(process, 'env').get(() => ({ From f7b1ea364077ef29ffe2d96cebea223e190b931d Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Tue, 11 Apr 2023 18:03:11 -0400 Subject: [PATCH 29/30] fix before/after inside loop --- .../cmap/handshake/client_metadata.test.ts | 51 ++++++------------- 1 file changed, 16 insertions(+), 35 deletions(-) diff --git a/test/unit/cmap/handshake/client_metadata.test.ts b/test/unit/cmap/handshake/client_metadata.test.ts index c37ad63a0a..0157abf3d6 100644 --- a/test/unit/cmap/handshake/client_metadata.test.ts +++ b/test/unit/cmap/handshake/client_metadata.test.ts @@ -2,6 +2,7 @@ import { expect } from 'chai'; import * as os from 'os'; import * as process from 'process'; import * as sinon from 'sinon'; +import { inspect } from 'util'; import { version as NODE_DRIVER_VERSION } from '../../../../package.json'; import { @@ -14,6 +15,8 @@ import { } from '../../../mongodb'; describe('client metadata module', () => { + afterEach(() => sinon.restore()); + describe('new LimitedSizeDocument()', () => { // For the sake of testing the size limiter features // We test document: { _id: ObjectId() } @@ -397,43 +400,23 @@ describe('client metadata module', () => { }; for (const [provider, testsForEnv] of Object.entries(tests)) { - context(provider, () => { - for (const { context, env: faasVariables, outcome } of testsForEnv) { - const setupEnv = () => { - for (const [k, v] of faasVariables) { - if (v != null) { - process.env[k] = v; - } - } - }; - - const cleanupEnv = () => { - for (const [k] of faasVariables) { - delete process.env[k]; - } - }; + for (const { context: title, env: faasVariables, outcome } of testsForEnv) { + context(`${provider} - ${title}`, () => { + beforeEach(() => { + sinon.stub(process, 'env').get(() => Object.fromEntries(faasVariables)); + }); - it(context, () => { - try { - setupEnv(); - const { env } = makeClientMetadata({ driverInfo: {} }); - expect(env).to.deep.equal(outcome); - } finally { - cleanupEnv(); - } + it(`returns ${inspect(outcome)} under env property`, () => { + const { env } = makeClientMetadata({ driverInfo: {} }); + expect(env).to.deep.equal(outcome); }); - it('always places name as the last key', () => { - try { - setupEnv(); - const keys = Array.from(getFAASEnv()?.keys() ?? []); - expect(keys).to.have.property(`${keys.length - 1}`, 'name'); - } finally { - cleanupEnv(); - } + it('places name as the last key in map', () => { + const keys = Array.from(getFAASEnv()?.keys() ?? []); + expect(keys).to.have.property(`${keys.length - 1}`, 'name'); }); - } - }); + }); + } } context('when a numeric FAAS env variable is not numerically parsable', () => { @@ -454,8 +437,6 @@ describe('client metadata module', () => { }); describe('metadata truncation', function () { - afterEach(() => sinon.restore()); - context('when faas region is too large', () => { beforeEach('1. Omit fields from `env` except `env.name`.', () => { sinon.stub(process, 'env').get(() => ({ From ec0372fb1c730a149f3f081dae0a8622ac0b3d1d Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Wed, 12 Apr 2023 10:30:39 -0400 Subject: [PATCH 30/30] fix: test locations --- .../cmap/handshake/client_metadata.test.ts | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/test/unit/cmap/handshake/client_metadata.test.ts b/test/unit/cmap/handshake/client_metadata.test.ts index 0157abf3d6..83f51e89b1 100644 --- a/test/unit/cmap/handshake/client_metadata.test.ts +++ b/test/unit/cmap/handshake/client_metadata.test.ts @@ -128,30 +128,14 @@ describe('client metadata module', () => { }); }); - context('when driverInfo is too large', () => { - it('throws an error relating to name', () => { - expect(() => makeClientMetadata({ driverInfo: { name: 'a'.repeat(512) } })).to.throw( - MongoInvalidArgumentError, - /name/ - ); - }); - - it('throws an error relating to version', () => { - expect(() => makeClientMetadata({ driverInfo: { version: 'a'.repeat(512) } })).to.throw( - MongoInvalidArgumentError, - /version/ - ); - }); - - it('throws an error relating to platform', () => { + context('when driverInfo.platform is provided', () => { + it('throws an error if driverInfo.platform is too large', () => { expect(() => makeClientMetadata({ driverInfo: { platform: 'a'.repeat(512) } })).to.throw( MongoInvalidArgumentError, /platform/ ); }); - }); - context('when driverInfo.platform is provided', () => { it('appends driverInfo.platform to the platform field', () => { const options = { driverInfo: { platform: 'myPlatform' } @@ -174,6 +158,13 @@ describe('client metadata module', () => { }); context('when driverInfo.name is provided', () => { + it('throws an error if driverInfo.name is too large', () => { + expect(() => makeClientMetadata({ driverInfo: { name: 'a'.repeat(512) } })).to.throw( + MongoInvalidArgumentError, + /name/ + ); + }); + it('appends driverInfo.name to the driver.name field', () => { const options = { driverInfo: { name: 'myName' } @@ -196,6 +187,13 @@ describe('client metadata module', () => { }); context('when driverInfo.version is provided', () => { + it('throws an error if driverInfo.version is too large', () => { + expect(() => makeClientMetadata({ driverInfo: { version: 'a'.repeat(512) } })).to.throw( + MongoInvalidArgumentError, + /version/ + ); + }); + it('appends driverInfo.version to the version field', () => { const options = { driverInfo: { version: 'myVersion' }