Skip to content

Commit

Permalink
Merge branch 'main' into NODE-6258-abortsignal
Browse files Browse the repository at this point in the history
  • Loading branch information
W-A-James authored Jan 24, 2025
2 parents f30dc05 + c1bcf0d commit 907a1a8
Show file tree
Hide file tree
Showing 14 changed files with 839 additions and 251 deletions.
54 changes: 0 additions & 54 deletions .evergreen/config.yml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .evergreen/generate_evergreen_tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ function makeTask({ mongoVersion, topology, tags = [], auth = 'auth' }) {
name: `test-${mongoVersion}-${topology}${auth === 'noauth' ? '-noauth' : ''}`,
tags: [mongoVersion, topology, ...tags],
commands: [
updateExpansions({ NPM_VERSION: 9, VERSION: mongoVersion, TOPOLOGY: topology, AUTH: auth }),
updateExpansions({ VERSION: mongoVersion, TOPOLOGY: topology, AUTH: auth }),
{ func: 'install dependencies' },
{ func: 'bootstrap mongo-orchestration' },
{ func: 'bootstrap kms servers' },
Expand Down
14 changes: 2 additions & 12 deletions .evergreen/install-dependencies.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,8 @@ set -o errexit # Exit the script with error if any of the commands fail
## a full nodejs version, in the format v<major>.<minor>.patch
export NODE_LTS_VERSION=${NODE_LTS_VERSION:-16}
# npm version can be defined in the environment for cases where we need to install
# a version lower than latest to support EOL Node versions.

# If NODE_LTS_VERSION is numeric and less than 18, default to 9, if less than 20, default to 10.
# Do not override if it is already set.
if [[ "$NODE_LTS_VERSION" =~ ^[0-9]+$ && "$NODE_LTS_VERSION" -lt 18 ]]; then
export NPM_VERSION=${NPM_VERSION:-9}
elif [[ "$NODE_LTS_VERSION" =~ ^[0-9]+$ && "$NODE_LTS_VERSION" -lt 20 ]]; then
export NPM_VERSION=${NPM_VERSION:-10}
else
export NPM_VERSION=${NPM_VERSION:-latest}
fi

# a version lower than latest to support EOL Node versions. When not provided will
# be handled by this script in drivers tools.
source $DRIVERS_TOOLS/.evergreen/install-node.sh

npm install "${NPM_OPTIONS}"
Expand Down
65 changes: 10 additions & 55 deletions src/cmap/command_monitoring_events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
LEGACY_HELLO_COMMAND,
LEGACY_HELLO_COMMAND_CAMEL_CASE
} from '../constants';
import { calculateDurationInMs, deepCopy } from '../utils';
import { calculateDurationInMs } from '../utils';
import {
DocumentSequence,
OpMsgRequest,
Expand Down Expand Up @@ -125,7 +125,7 @@ export class CommandSucceededEvent {
this.requestId = command.requestId;
this.commandName = commandName;
this.duration = calculateDurationInMs(started);
this.reply = maybeRedact(commandName, cmd, extractReply(command, reply));
this.reply = maybeRedact(commandName, cmd, extractReply(reply));
this.serverConnectionId = serverConnectionId;
}

Expand Down Expand Up @@ -214,7 +214,6 @@ const HELLO_COMMANDS = new Set(['hello', LEGACY_HELLO_COMMAND, LEGACY_HELLO_COMM

// helper methods
const extractCommandName = (commandDoc: Document) => Object.keys(commandDoc)[0];
const namespace = (command: OpQueryRequest) => command.ns;
const collectionName = (command: OpQueryRequest) => command.ns.split('.')[1];
const maybeRedact = (commandName: string, commandDoc: Document, result: Error | Document) =>
SENSITIVE_COMMANDS.has(commandName) ||
Expand Down Expand Up @@ -242,19 +241,10 @@ const LEGACY_FIND_OPTIONS_MAP = {
returnFieldSelector: 'projection'
} as const;

const OP_QUERY_KEYS = [
'tailable',
'oplogReplay',
'noCursorTimeout',
'awaitData',
'partial',
'exhaust'
] as const;

/** Extract the actual command from the query, possibly up-converting if it's a legacy format */
function extractCommand(command: WriteProtocolMessageType): Document {
if (command instanceof OpMsgRequest) {
const cmd = deepCopy(command.command);
const cmd = { ...command.command };
// For OP_MSG with payload type 1 we need to pull the documents
// array out of the document sequence for monitoring.
if (cmd.ops instanceof DocumentSequence) {
Expand All @@ -276,72 +266,37 @@ function extractCommand(command: WriteProtocolMessageType): Document {
result = { find: collectionName(command) };
Object.keys(LEGACY_FIND_QUERY_MAP).forEach(key => {
if (command.query[key] != null) {
result[LEGACY_FIND_QUERY_MAP[key]] = deepCopy(command.query[key]);
result[LEGACY_FIND_QUERY_MAP[key]] = { ...command.query[key] };
}
});
}

Object.keys(LEGACY_FIND_OPTIONS_MAP).forEach(key => {
const legacyKey = key as keyof typeof LEGACY_FIND_OPTIONS_MAP;
if (command[legacyKey] != null) {
result[LEGACY_FIND_OPTIONS_MAP[legacyKey]] = deepCopy(command[legacyKey]);
}
});

OP_QUERY_KEYS.forEach(key => {
if (command[key]) {
result[key] = command[key];
result[LEGACY_FIND_OPTIONS_MAP[legacyKey]] = command[legacyKey];
}
});

if (command.pre32Limit != null) {
result.limit = command.pre32Limit;
}

if (command.query.$explain) {
return { explain: result };
}
return result;
}

const clonedQuery: Record<string, unknown> = {};
const clonedCommand: Record<string, unknown> = {};
let clonedQuery: Record<string, unknown> = {};
const clonedCommand: Record<string, unknown> = { ...command };
if (command.query) {
for (const k in command.query) {
clonedQuery[k] = deepCopy(command.query[k]);
}
clonedQuery = { ...command.query };
clonedCommand.query = clonedQuery;
}

for (const k in command) {
if (k === 'query') continue;
clonedCommand[k] = deepCopy((command as unknown as Record<string, unknown>)[k]);
}
return command.query ? clonedQuery : clonedCommand;
}

function extractReply(command: WriteProtocolMessageType, reply?: Document) {
function extractReply(reply?: Document) {
if (!reply) {
return reply;
}

if (command instanceof OpMsgRequest) {
return deepCopy(reply.result ? reply.result : reply);
}

// is this a legacy find command?
if (command.query && command.query.$query != null) {
return {
ok: 1,
cursor: {
id: deepCopy(reply.cursorId),
ns: namespace(command),
firstBatch: deepCopy(reply.documents)
}
};
}

return deepCopy(reply.result ? reply.result : reply);
return reply.result ? reply.result : reply;
}

function extractConnectionDetails(connection: Connection) {
Expand Down
28 changes: 24 additions & 4 deletions src/connection_string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,27 @@ const LB_REPLICA_SET_ERROR = 'loadBalanced option not supported with a replicaSe
const LB_DIRECT_CONNECTION_ERROR =
'loadBalanced option not supported when directConnection is provided';

function retryDNSTimeoutFor(api: 'resolveSrv'): (a: string) => Promise<dns.SrvRecord[]>;
function retryDNSTimeoutFor(api: 'resolveTxt'): (a: string) => Promise<string[][]>;
function retryDNSTimeoutFor(
api: 'resolveSrv' | 'resolveTxt'
): (a: string) => Promise<dns.SrvRecord[] | string[][]> {
return async function dnsReqRetryTimeout(lookupAddress: string) {
try {
return await dns.promises[api](lookupAddress);
} catch (firstDNSError) {
if (firstDNSError.code === dns.TIMEOUT) {
return await dns.promises[api](lookupAddress);
} else {
throw firstDNSError;
}
}
};
}

const resolveSrv = retryDNSTimeoutFor('resolveSrv');
const resolveTxt = retryDNSTimeoutFor('resolveTxt');

/**
* Lookup a `mongodb+srv` connection string, combine the parts and reparse it as a normal
* connection string.
Expand All @@ -67,14 +88,13 @@ export async function resolveSRVRecord(options: MongoOptions): Promise<HostAddre
// Asynchronously start TXT resolution so that we do not have to wait until
// the SRV record is resolved before starting a second DNS query.
const lookupAddress = options.srvHost;
const txtResolutionPromise = dns.promises.resolveTxt(lookupAddress);
const txtResolutionPromise = resolveTxt(lookupAddress);

txtResolutionPromise.then(undefined, squashError); // rejections will be handled later

const hostname = `_${options.srvServiceName}._tcp.${lookupAddress}`;
// Resolve the SRV record and use the result as the list of hosts to connect to.
const addresses = await dns.promises.resolveSrv(
`_${options.srvServiceName}._tcp.${lookupAddress}`
);
const addresses = await resolveSrv(hostname);

if (addresses.length === 0) {
throw new MongoAPIError('No addresses found at host');
Expand Down
8 changes: 8 additions & 0 deletions src/mongo_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,10 @@ export class MongoClient extends TypedEventEmitter<MongoClientEvents> implements
* This means the time to setup the `MongoClient` does not count against `timeoutMS`.
* If you are using `timeoutMS` we recommend connecting your client explicitly in advance of any operation to avoid this inconsistent execution time.
*
* @remarks
* The driver will look up corresponding SRV and TXT records if the connection string starts with `mongodb+srv://`.
* If those look ups throw a DNS Timeout error, the driver will retry the look up once.
*
* @see docs.mongodb.org/manual/reference/connection-string/
*/
async connect(): Promise<this> {
Expand Down Expand Up @@ -727,6 +731,10 @@ export class MongoClient extends TypedEventEmitter<MongoClientEvents> implements
* @remarks
* The programmatically provided options take precedence over the URI options.
*
* @remarks
* The driver will look up corresponding SRV and TXT records if the connection string starts with `mongodb+srv://`.
* If those look ups throw a DNS Timeout error, the driver will retry the look up once.
*
* @see https://www.mongodb.com/docs/manual/reference/connection-string/
*/
static async connect(url: string, options?: MongoClientOptions): Promise<MongoClient> {
Expand Down
130 changes: 128 additions & 2 deletions src/mongo_logger.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
import { inspect, promisify } from 'util';
import { isUint8Array } from 'util/types';

import { type Document, EJSON, type EJSONOptions, type ObjectId } from './bson';
import {
type Binary,
type BSONRegExp,
type BSONSymbol,
type Code,
type DBRef,
type Decimal128,
type Document,
type Double,
EJSON,
type EJSONOptions,
type Int32,
type Long,
type MaxKey,
type MinKey,
type ObjectId,
type Timestamp
} from './bson';
import type { CommandStartedEvent } from './cmap/command_monitoring_events';
import type {
ConnectionCheckedInEvent,
Expand Down Expand Up @@ -413,6 +431,20 @@ export interface LogConvertible extends Record<string, any> {
toLog(): Record<string, any>;
}

type BSONObject =
| BSONRegExp
| BSONSymbol
| Code
| DBRef
| Decimal128
| Double
| Int32
| Long
| MaxKey
| MinKey
| ObjectId
| Timestamp
| Binary;
/** @internal */
export function stringifyWithMaxLen(
value: any,
Expand All @@ -421,13 +453,107 @@ export function stringifyWithMaxLen(
): string {
let strToTruncate = '';

let currentLength = 0;
const maxDocumentLengthEnsurer = function maxDocumentLengthEnsurer(key: string, value: any) {
if (currentLength >= maxDocumentLength) {
return undefined;
}
// Account for root document
if (key === '') {
// Account for starting brace
currentLength += 1;
return value;
}

// +4 accounts for 2 quotation marks, colon and comma after value
// Note that this potentially undercounts since it does not account for escape sequences which
// will have an additional backslash added to them once passed through JSON.stringify.
currentLength += key.length + 4;

if (value == null) return value;

switch (typeof value) {
case 'string':
// +2 accounts for quotes
// Note that this potentially undercounts similarly to the key length calculation
currentLength += value.length + 2;
break;
case 'number':
case 'bigint':
currentLength += String(value).length;
break;
case 'boolean':
currentLength += value ? 4 : 5;
break;
case 'object':
if (isUint8Array(value)) {
// '{"$binary":{"base64":"<base64 string>","subType":"XX"}}'
// This is an estimate based on the fact that the base64 is approximately 1.33x the length of
// the actual binary sequence https://en.wikipedia.org/wiki/Base64
currentLength += (22 + value.byteLength + value.byteLength * 0.33 + 18) | 0;
} else if ('_bsontype' in value) {
const v = value as BSONObject;
switch (v._bsontype) {
case 'Int32':
currentLength += String(v.value).length;
break;
case 'Double':
// Account for representing integers as <value>.0
currentLength +=
(v.value | 0) === v.value ? String(v.value).length + 2 : String(v.value).length;
break;
case 'Long':
currentLength += v.toString().length;
break;
case 'ObjectId':
// '{"$oid":"XXXXXXXXXXXXXXXXXXXXXXXX"}'
currentLength += 35;
break;
case 'MaxKey':
case 'MinKey':
// '{"$maxKey":1}' or '{"$minKey":1}'
currentLength += 13;
break;
case 'Binary':
// '{"$binary":{"base64":"<base64 string>","subType":"XX"}}'
// This is an estimate based on the fact that the base64 is approximately 1.33x the length of
// the actual binary sequence https://en.wikipedia.org/wiki/Base64
currentLength += (22 + value.position + value.position * 0.33 + 18) | 0;
break;
case 'Timestamp':
// '{"$timestamp":{"t":<t>,"i":<i>}}'
currentLength += 19 + String(v.t).length + 5 + String(v.i).length + 2;
break;
case 'Code':
// '{"$code":"<code>"}' or '{"$code":"<code>","$scope":<scope>}'
if (v.scope == null) {
currentLength += v.code.length + 10 + 2;
} else {
// Ignoring actual scope object, so this undercounts by a significant amount
currentLength += v.code.length + 10 + 11;
}
break;
case 'BSONRegExp':
// '{"$regularExpression":{"pattern":"<pattern>","options":"<options>"}}'
currentLength += 34 + v.pattern.length + 13 + v.options.length + 3;
break;
}
}
}
return value;
};

if (typeof value === 'string') {
strToTruncate = value;
} else if (typeof value === 'function') {
strToTruncate = value.name;
} else {
try {
strToTruncate = EJSON.stringify(value, options);
if (maxDocumentLength !== 0) {
strToTruncate = EJSON.stringify(value, maxDocumentLengthEnsurer, 0, options);
} else {
strToTruncate = EJSON.stringify(value, options);
}
} catch (e) {
strToTruncate = `Extended JSON serialization failed with: ${e.message}`;
}
Expand Down
Loading

0 comments on commit 907a1a8

Please sign in to comment.