Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(NODE-4696): include FAAS metadata in the mongodb handshake #3616

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/cmap/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -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;
Expand All @@ -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
};

Expand Down
5 changes: 4 additions & 1 deletion src/cmap/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import { applySession, ClientSession, updateSessionFromResponse } from '../sessi
import {
calculateDurationInMs,
Callback,
ClientMetadata,
HostAddress,
maxWireVersion,
MongoDBNamespace,
Expand All @@ -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';
Expand Down Expand Up @@ -128,6 +128,9 @@ export interface ConnectionOptions
socketTimeoutMS?: number;
cancellationToken?: CancellationToken;
metadata: ClientMetadata;

/** @internal */
truncatedClientMetadata: TruncatedClientMetadata;
}

/** @internal */
Expand Down
2 changes: 1 addition & 1 deletion src/cmap/connection_pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
waitQueueTimeoutMS: options.waitQueueTimeoutMS ?? 0,
minPoolSizeCheckFrequencyMS: options.minPoolSizeCheckFrequencyMS ?? 100,
autoEncrypter: options.autoEncrypter,
metadata: options.metadata
truncatedClientMetadata: options.truncatedClientMetadata
});

if (this.options.minPoolSize > this.options.maxPoolSize) {
Expand Down
124 changes: 124 additions & 0 deletions src/cmap/handshake/client_metadata.ts
Original file line number Diff line number Diff line change
@@ -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<ClientMetadata>;
baileympearson marked this conversation as resolved.
Show resolved Hide resolved

/**
* @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);
baileympearson marked this conversation as resolved.
Show resolved Hide resolved
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<MongoOptions, 'appName' | 'driverInfo'>
): 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);
}
84 changes: 84 additions & 0 deletions src/cmap/handshake/faas_provider.ts
Original file line number Diff line number Diff line change
@@ -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 {
nbbeeken marked this conversation as resolved.
Show resolved Hide resolved
const awsPresent = process.env.AWS_EXECUTION_ENV || process.env.AWS_LAMBDA_RUNTIME_API;
nbbeeken marked this conversation as resolved.
Show resolved Hide resolved
const azurePresent = process.env.FUNCTIONS_WORKER_RUNTIME;
const gcpPresent = process.env.K_SERVICE || process.env.FUNCTION_NAME;
nbbeeken marked this conversation as resolved.
Show resolved Hide resolved
const vercelPresent = process.env.VERCEL;

const numberOfProvidersPresent = [awsPresent, azurePresent, gcpPresent, vercelPresent].filter(
identity
).length;
Comment on lines +21 to +23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be an ifelse sequence instead?, we have the order encoded below with the early returns, I'd just add an else case to the end of that and return none.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, look closely at the logic. It purposefully returns none when there's more than one faas provider present


if (numberOfProvidersPresent !== 1) {
return 'none';
}

if (awsPresent) return 'aws';
if (azurePresent) return 'azure';
if (gcpPresent) return 'gcp';
return 'vercel';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should vercel come before aws? IIRC vercel runs on AWS lambda is it possible aws env vars are also defined in vercel?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comment above – they will never both be set. If they are, we explicitly determine none

}

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 ?? '');
nbbeeken marked this conversation as resolved.
Show resolved Hide resolved
if (!Number.isNaN(memory_mb)) {
m.env.memory_mb = memory_mb;
}
const timeout_sec = Number.parseInt(process.env.FUNCTION_TIMEOUT_SEC ?? '');
nbbeeken marked this conversation as resolved.
Show resolved Hide resolved
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<FAASProvider, (m: ClientMetadata) => ClientMetadata> = {
aws: applyAWSMetadata,
gcp: applyGCPMetadata,
azure: applyAzureMetadata,
vercel: applyVercelMetadata,
none: identity
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of adding a step for none, can't none do nothing to the input? maybe none could instead be represented by a null determineFAASProvider result that leads to returning the metadata input.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

none does do nothing to the input, that's what identity is. I chose this over using null for simplicity in typing and edge casing. It flows everything through the same path logically

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an early return based on null should narrow the key to be one of the acceptable strings, this way we can write less handling for the obvious case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That still requires handling for the obvious case, no? it's just that you've put it in a null check. I like this approach because there's no special casing values (or a "lack" of value here, indicated by none or null)

};
const cloudProvider = determineCloudProvider();

const faasMetadataProvider = handlerMap[cloudProvider];
return faasMetadataProvider(metadata);
}
4 changes: 3 additions & 1 deletion src/connection_string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -32,7 +33,6 @@ import {
emitWarningOnce,
HostAddress,
isRecord,
makeClientMetadata,
parseInteger,
setDifference
} from './utils';
Expand Down Expand Up @@ -543,6 +543,8 @@ export function parseOptions(
);

mongoOptions.metadata = makeClientMetadata(mongoOptions);
Object.freeze(mongoOptions.metadata);
baileympearson marked this conversation as resolved.
Show resolved Hide resolved
mongoOptions.truncatedClientMetadata = truncateClientMetadata(mongoOptions.metadata);

return mongoOptions;
}
Expand Down
8 changes: 6 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -463,8 +468,7 @@ export type { Transaction, TransactionOptions, TxnState } from './transactions';
export type {
BufferPool,
Callback,
ClientMetadata,
ClientMetadataOptions,
DeepPartial,
EventEmitterWithState,
HostAddress,
List,
Expand Down
17 changes: 15 additions & 2 deletions src/mongo_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 */
Expand Down Expand Up @@ -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;

Expand Down
Loading