Skip to content

Commit fd31d37

Browse files
Basic implementation done, need to UTR changes + sync spec tests still
synced new test files added support for error response added api docs made MongoServerError.errorResponse required + casted resulting type errors test(NODE-5992): fix env var restoration in tests (#4017) refactor(NODE-5903): add newline to stdio logging (#4018) fix(NODE-5985): throw Nodejs' certificate expired error when TLS fails to connect instead of `CERT_HAS_EXPIRED` (#4014) test(NODE-5962): gossip cluster time in utr (#4019) chore(NODE-5997): update saslprep to ^1.1.5 (#4023) feat(NODE-5968): container and Kubernetes awareness in client metadata (#4005) fix(NODE-5993): memory leak in the `Connection` class (#4022) added TODO(NODE-XXXX)
1 parent eab8f23 commit fd31d37

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1703
-145
lines changed

Diff for: package-lock.json

+4-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"email": "[email protected]"
2626
},
2727
"dependencies": {
28-
"@mongodb-js/saslprep": "^1.1.0",
28+
"@mongodb-js/saslprep": "^1.1.5",
2929
"bson": "^6.4.0",
3030
"mongodb-connection-string-url": "^3.0.0"
3131
},

Diff for: src/cmap/connect.ts

+3-8
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import {
2525
type ConnectionOptions,
2626
CryptoConnection
2727
} from './connection';
28-
import type { ClientMetadata } from './handshake/client_metadata';
2928
import {
3029
MAX_SUPPORTED_SERVER_VERSION,
3130
MAX_SUPPORTED_WIRE_VERSION,
@@ -183,7 +182,7 @@ export interface HandshakeDocument extends Document {
183182
ismaster?: boolean;
184183
hello?: boolean;
185184
helloOk?: boolean;
186-
client: ClientMetadata;
185+
client: Document;
187186
compression: string[];
188187
saslSupportedMechs?: string;
189188
loadBalanced?: boolean;
@@ -200,11 +199,12 @@ export async function prepareHandshakeDocument(
200199
const options = authContext.options;
201200
const compressors = options.compressors ? options.compressors : [];
202201
const { serverApi } = authContext.connection;
202+
const clientMetadata: Document = await options.extendedMetadata;
203203

204204
const handshakeDoc: HandshakeDocument = {
205205
[serverApi?.version || options.loadBalanced === true ? 'hello' : LEGACY_HELLO_COMMAND]: 1,
206206
helloOk: true,
207-
client: options.metadata,
207+
client: clientMetadata,
208208
compression: compressors
209209
};
210210

@@ -319,7 +319,6 @@ export async function makeSocket(options: MakeConnectionOptions): Promise<Stream
319319
const useTLS = options.tls ?? false;
320320
const noDelay = options.noDelay ?? true;
321321
const connectTimeoutMS = options.connectTimeoutMS ?? 30000;
322-
const rejectUnauthorized = options.rejectUnauthorized ?? true;
323322
const existingSocket = options.existingSocket;
324323

325324
let socket: Stream;
@@ -375,10 +374,6 @@ export async function makeSocket(options: MakeConnectionOptions): Promise<Stream
375374
return socket;
376375
} catch (error) {
377376
socket.destroy();
378-
if ('authorizationError' in socket && socket.authorizationError != null && rejectUnauthorized) {
379-
// TODO(NODE-5192): wrap this with a MongoError subclass
380-
throw socket.authorizationError;
381-
}
382377
throw error;
383378
} finally {
384379
socket.setTimeout(0);

Diff for: src/cmap/connection.ts

+27-43
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { type Readable, Transform, type TransformCallback } from 'stream';
22
import { clearTimeout, setTimeout } from 'timers';
3-
import { promisify } from 'util';
43

54
import type { BSONSerializeOptions, Document, ObjectId } from '../bson';
65
import type { AutoEncrypter } from '../client-side-encryption/auto_encrypter';
@@ -37,7 +36,7 @@ import {
3736
maxWireVersion,
3837
type MongoDBNamespace,
3938
now,
40-
promiseWithResolvers,
39+
once,
4140
uuidV4
4241
} from '../utils';
4342
import type { WriteConcern } from '../write_concern';
@@ -119,6 +118,8 @@ export interface ConnectionOptions
119118
cancellationToken?: CancellationToken;
120119
metadata: ClientMetadata;
121120
/** @internal */
121+
extendedMetadata: Promise<Document>;
122+
/** @internal */
122123
mongoLogger?: MongoLogger | undefined;
123124
}
124125

@@ -180,18 +181,18 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
180181
* Once connection is established, command logging can log events (if enabled)
181182
*/
182183
public established: boolean;
184+
/** Indicates that the connection (including underlying TCP socket) has been closed. */
185+
public closed = false;
183186

184187
private lastUseTime: number;
185188
private clusterTime: Document | null = null;
189+
private error: Error | null = null;
190+
private dataEvents: AsyncGenerator<Buffer, void, void> | null = null;
186191

187192
private readonly socketTimeoutMS: number;
188193
private readonly monitorCommands: boolean;
189194
private readonly socket: Stream;
190-
private readonly controller: AbortController;
191-
private readonly signal: AbortSignal;
192195
private readonly messageStream: Readable;
193-
private readonly socketWrite: (buffer: Uint8Array) => Promise<void>;
194-
private readonly aborted: Promise<never>;
195196

196197
/** @event */
197198
static readonly COMMAND_STARTED = COMMAND_STARTED;
@@ -211,6 +212,7 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
211212
constructor(stream: Stream, options: ConnectionOptions) {
212213
super();
213214

215+
this.socket = stream;
214216
this.id = options.id;
215217
this.address = streamIdentifier(stream, options);
216218
this.socketTimeoutMS = options.socketTimeoutMS ?? 0;
@@ -223,39 +225,12 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
223225
this.generation = options.generation;
224226
this.lastUseTime = now();
225227

226-
this.socket = stream;
227-
228-
// TODO: Remove signal from connection layer
229-
this.controller = new AbortController();
230-
const { signal } = this.controller;
231-
this.signal = signal;
232-
const { promise: aborted, reject } = promiseWithResolvers<never>();
233-
aborted.then(undefined, () => null); // Prevent unhandled rejection
234-
this.signal.addEventListener(
235-
'abort',
236-
function onAbort() {
237-
reject(signal.reason);
238-
},
239-
{ once: true }
240-
);
241-
this.aborted = aborted;
242-
243228
this.messageStream = this.socket
244229
.on('error', this.onError.bind(this))
245230
.pipe(new SizedMessageTransform({ connection: this }))
246231
.on('error', this.onError.bind(this));
247232
this.socket.on('close', this.onClose.bind(this));
248233
this.socket.on('timeout', this.onTimeout.bind(this));
249-
250-
const socketWrite = promisify(this.socket.write.bind(this.socket));
251-
this.socketWrite = async buffer => {
252-
return Promise.race([socketWrite(buffer), this.aborted]);
253-
};
254-
}
255-
256-
/** Indicates that the connection (including underlying TCP socket) has been closed. */
257-
public get closed(): boolean {
258-
return this.signal.aborted;
259234
}
260235

261236
public get hello() {
@@ -306,7 +281,7 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
306281
this.lastUseTime = now();
307282
}
308283

309-
public onError(error?: Error) {
284+
public onError(error: Error) {
310285
this.cleanup(error);
311286
}
312287

@@ -349,13 +324,15 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
349324
*
350325
* This method does nothing if the connection is already closed.
351326
*/
352-
private cleanup(error?: Error): void {
327+
private cleanup(error: Error): void {
353328
if (this.closed) {
354329
return;
355330
}
356331

357332
this.socket.destroy();
358-
this.controller.abort(error);
333+
this.error = error;
334+
this.dataEvents?.throw(error).then(undefined, () => null); // squash unhandled rejection
335+
this.closed = true;
359336
this.emit(Connection.CLOSE);
360337
}
361338

@@ -596,7 +573,7 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
596573
}
597574

598575
private throwIfAborted() {
599-
this.signal.throwIfAborted();
576+
if (this.error) throw this.error;
600577
}
601578

602579
/**
@@ -619,7 +596,8 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
619596

620597
const buffer = Buffer.concat(await finalCommand.toBin());
621598

622-
return this.socketWrite(buffer);
599+
if (this.socket.write(buffer)) return;
600+
return once(this.socket, 'drain');
623601
}
624602

625603
/**
@@ -632,13 +610,19 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
632610
* Note that `for-await` loops call `return` automatically when the loop is exited.
633611
*/
634612
private async *readMany(): AsyncGenerator<OpMsgResponse | OpQueryResponse> {
635-
for await (const message of onData(this.messageStream, { signal: this.signal })) {
636-
const response = await decompressResponse(message);
637-
yield response;
613+
try {
614+
this.dataEvents = onData(this.messageStream);
615+
for await (const message of this.dataEvents) {
616+
const response = await decompressResponse(message);
617+
yield response;
638618

639-
if (!response.moreToCome) {
640-
return;
619+
if (!response.moreToCome) {
620+
return;
621+
}
641622
}
623+
} finally {
624+
this.dataEvents = null;
625+
this.throwIfAborted();
642626
}
643627
}
644628
}

Diff for: src/cmap/connection_pool.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,7 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
233233
maxIdleTimeMS: options.maxIdleTimeMS ?? 0,
234234
waitQueueTimeoutMS: options.waitQueueTimeoutMS ?? 0,
235235
minPoolSizeCheckFrequencyMS: options.minPoolSizeCheckFrequencyMS ?? 100,
236-
autoEncrypter: options.autoEncrypter,
237-
metadata: options.metadata
236+
autoEncrypter: options.autoEncrypter
238237
});
239238

240239
if (this.options.minPoolSize > this.options.maxPoolSize) {

Diff for: src/cmap/handshake/client_metadata.ts

+54-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { promises as fs } from 'fs';
12
import * as os from 'os';
23
import * as process from 'process';
34

4-
import { BSON, Int32 } from '../../bson';
5+
import { BSON, type Document, Int32 } from '../../bson';
56
import { MongoInvalidArgumentError } from '../../error';
67
import type { MongoOptions } from '../../mongo_client';
78

@@ -71,13 +72,13 @@ export class LimitedSizeDocument {
7172
return true;
7273
}
7374

74-
toObject(): ClientMetadata {
75+
toObject(): Document {
7576
return BSON.deserialize(BSON.serialize(this.document), {
7677
promoteLongs: false,
7778
promoteBuffers: false,
7879
promoteValues: false,
7980
useBigInt64: false
80-
}) as ClientMetadata;
81+
});
8182
}
8283
}
8384

@@ -152,8 +153,57 @@ export function makeClientMetadata(options: MakeClientMetadataOptions): ClientMe
152153
}
153154
}
154155
}
156+
return metadataDocument.toObject() as ClientMetadata;
157+
}
158+
159+
let dockerPromise: Promise<boolean>;
160+
/** @internal */
161+
async function getContainerMetadata() {
162+
const containerMetadata: Record<string, any> = {};
163+
dockerPromise ??= fs.access('/.dockerenv').then(
164+
() => true,
165+
() => false
166+
);
167+
const isDocker = await dockerPromise;
168+
169+
const { KUBERNETES_SERVICE_HOST = '' } = process.env;
170+
const isKubernetes = KUBERNETES_SERVICE_HOST.length > 0 ? true : false;
171+
172+
if (isDocker) containerMetadata.runtime = 'docker';
173+
if (isKubernetes) containerMetadata.orchestrator = 'kubernetes';
174+
175+
return containerMetadata;
176+
}
177+
178+
/**
179+
* @internal
180+
* Re-add each metadata value.
181+
* Attempt to add new env container metadata, but keep old data if it does not fit.
182+
*/
183+
export async function addContainerMetadata(originalMetadata: ClientMetadata) {
184+
const containerMetadata = await getContainerMetadata();
185+
if (Object.keys(containerMetadata).length === 0) return originalMetadata;
186+
187+
const extendedMetadata = new LimitedSizeDocument(512);
188+
189+
const extendedEnvMetadata = { ...originalMetadata?.env, container: containerMetadata };
190+
191+
for (const [key, val] of Object.entries(originalMetadata)) {
192+
if (key !== 'env') {
193+
extendedMetadata.ifItFitsItSits(key, val);
194+
} else {
195+
if (!extendedMetadata.ifItFitsItSits('env', extendedEnvMetadata)) {
196+
// add in old data if newer / extended metadata does not fit
197+
extendedMetadata.ifItFitsItSits('env', val);
198+
}
199+
}
200+
}
201+
202+
if (!('env' in originalMetadata)) {
203+
extendedMetadata.ifItFitsItSits('env', extendedEnvMetadata);
204+
}
155205

156-
return metadataDocument.toObject();
206+
return extendedMetadata.toObject();
157207
}
158208

159209
/**

Diff for: src/cmap/wire_protocol/on_data.ts

+2-16
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,9 @@ type PendingPromises = Omit<
1616
* https://nodejs.org/api/events.html#eventsonemitter-eventname-options
1717
*
1818
* Returns an AsyncIterator that iterates each 'data' event emitted from emitter.
19-
* It will reject upon an error event or if the provided signal is aborted.
19+
* It will reject upon an error event.
2020
*/
21-
export function onData(emitter: EventEmitter, options: { signal: AbortSignal }) {
22-
const signal = options.signal;
23-
21+
export function onData(emitter: EventEmitter) {
2422
// Setup pending events and pending promise lists
2523
/**
2624
* When the caller has not yet called .next(), we store the
@@ -89,19 +87,8 @@ export function onData(emitter: EventEmitter, options: { signal: AbortSignal })
8987
emitter.on('data', eventHandler);
9088
emitter.on('error', errorHandler);
9189

92-
if (signal.aborted) {
93-
// If the signal is aborted, set up the first .next() call to be a rejection
94-
queueMicrotask(abortListener);
95-
} else {
96-
signal.addEventListener('abort', abortListener, { once: true });
97-
}
98-
9990
return iterator;
10091

101-
function abortListener() {
102-
errorHandler(signal.reason);
103-
}
104-
10592
function eventHandler(value: Buffer) {
10693
const promise = unconsumedPromises.shift();
10794
if (promise != null) promise.resolve({ value, done: false });
@@ -119,7 +106,6 @@ export function onData(emitter: EventEmitter, options: { signal: AbortSignal })
119106
// Adding event handlers
120107
emitter.off('data', eventHandler);
121108
emitter.off('error', errorHandler);
122-
signal.removeEventListener('abort', abortListener);
123109
finished = true;
124110
const doneResult = { value: undefined, done: finished } as const;
125111

0 commit comments

Comments
 (0)