mongodb 6.2.0 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cmap/command_monitoring_events.js +2 -2
- package/lib/cmap/command_monitoring_events.js.map +1 -1
- package/lib/cmap/commands.js +54 -11
- package/lib/cmap/commands.js.map +1 -1
- package/lib/cmap/connect.js +1 -1
- package/lib/cmap/connect.js.map +1 -1
- package/lib/cmap/connection.js +401 -3
- package/lib/cmap/connection.js.map +1 -1
- package/lib/cmap/errors.js +1 -1
- package/lib/cmap/errors.js.map +1 -1
- package/lib/cmap/message_stream.js +3 -10
- package/lib/cmap/message_stream.js.map +1 -1
- package/lib/cmap/wire_protocol/compression.js +57 -1
- package/lib/cmap/wire_protocol/compression.js.map +1 -1
- package/lib/connection_string.js +29 -3
- package/lib/connection_string.js.map +1 -1
- package/lib/error.js +4 -2
- package/lib/error.js.map +1 -1
- package/lib/gridfs/download.js.map +1 -1
- package/lib/gridfs/upload.js.map +1 -1
- package/lib/mongo_client.js +14 -0
- package/lib/mongo_client.js.map +1 -1
- package/lib/mongo_logger.js +16 -7
- package/lib/mongo_logger.js.map +1 -1
- package/lib/sdam/monitor.js +61 -38
- package/lib/sdam/monitor.js.map +1 -1
- package/lib/sdam/server.js +9 -13
- package/lib/sdam/server.js.map +1 -1
- package/lib/sdam/topology.js.map +1 -1
- package/lib/sessions.js +2 -2
- package/lib/sessions.js.map +1 -1
- package/lib/utils.js +15 -1
- package/lib/utils.js.map +1 -1
- package/mongodb.d.ts +52 -16
- package/package.json +2 -2
- package/src/cmap/command_monitoring_events.ts +5 -5
- package/src/cmap/commands.ts +65 -8
- package/src/cmap/connect.ts +1 -1
- package/src/cmap/connection.ts +543 -4
- package/src/cmap/errors.ts +1 -1
- package/src/cmap/message_stream.ts +7 -21
- package/src/cmap/wire_protocol/compression.ts +73 -0
- package/src/connection_string.ts +31 -3
- package/src/error.ts +6 -2
- package/src/gridfs/download.ts +4 -2
- package/src/gridfs/upload.ts +8 -2
- package/src/index.ts +13 -6
- package/src/mongo_client.ts +59 -2
- package/src/mongo_logger.ts +72 -12
- package/src/sdam/monitor.ts +76 -45
- package/src/sdam/server.ts +10 -15
- package/src/sdam/topology.ts +2 -0
- package/src/sessions.ts +3 -2
- package/src/utils.ts +17 -0
package/src/cmap/connection.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { once } from 'events';
|
|
2
|
+
import { on } from 'stream';
|
|
1
3
|
import { clearTimeout, setTimeout } from 'timers';
|
|
2
4
|
import { promisify } from 'util';
|
|
3
5
|
|
|
@@ -18,6 +20,7 @@ import {
|
|
|
18
20
|
MongoMissingDependencyError,
|
|
19
21
|
MongoNetworkError,
|
|
20
22
|
MongoNetworkTimeoutError,
|
|
23
|
+
MongoParseError,
|
|
21
24
|
MongoRuntimeError,
|
|
22
25
|
MongoServerError,
|
|
23
26
|
MongoWriteConcernError
|
|
@@ -27,6 +30,7 @@ import { type CancellationToken, TypedEventEmitter } from '../mongo_types';
|
|
|
27
30
|
import type { ReadPreferenceLike } from '../read_preference';
|
|
28
31
|
import { applySession, type ClientSession, updateSessionFromResponse } from '../sessions';
|
|
29
32
|
import {
|
|
33
|
+
BufferPool,
|
|
30
34
|
calculateDurationInMs,
|
|
31
35
|
type Callback,
|
|
32
36
|
HostAddress,
|
|
@@ -43,11 +47,19 @@ import {
|
|
|
43
47
|
CommandStartedEvent,
|
|
44
48
|
CommandSucceededEvent
|
|
45
49
|
} from './command_monitoring_events';
|
|
46
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
OpCompressedRequest,
|
|
52
|
+
OpMsgRequest,
|
|
53
|
+
type OpMsgResponse,
|
|
54
|
+
OpQueryRequest,
|
|
55
|
+
type OpQueryResponse,
|
|
56
|
+
type WriteProtocolMessageType
|
|
57
|
+
} from './commands';
|
|
47
58
|
import type { Stream } from './connect';
|
|
48
59
|
import type { ClientMetadata } from './handshake/client_metadata';
|
|
49
60
|
import { MessageStream, type OperationDescription } from './message_stream';
|
|
50
61
|
import { StreamDescription, type StreamDescriptionOptions } from './stream_description';
|
|
62
|
+
import { decompressResponse } from './wire_protocol/compression';
|
|
51
63
|
import { getReadPreference, isSharded } from './wire_protocol/shared';
|
|
52
64
|
|
|
53
65
|
/** @internal */
|
|
@@ -324,7 +336,7 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
|
|
|
324
336
|
}, 1).unref(); // No need for this timer to hold the event loop open
|
|
325
337
|
}
|
|
326
338
|
|
|
327
|
-
onMessage(message:
|
|
339
|
+
onMessage(message: OpMsgResponse | OpQueryResponse) {
|
|
328
340
|
const delayedTimeoutId = this[kDelayedTimeoutId];
|
|
329
341
|
if (delayedTimeoutId != null) {
|
|
330
342
|
clearTimeout(delayedTimeoutId);
|
|
@@ -540,8 +552,8 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
|
|
|
540
552
|
);
|
|
541
553
|
|
|
542
554
|
const message = shouldUseOpMsg
|
|
543
|
-
? new
|
|
544
|
-
: new
|
|
555
|
+
? new OpMsgRequest(ns.db, cmd, commandOptions)
|
|
556
|
+
: new OpQueryRequest(ns.db, cmd, commandOptions);
|
|
545
557
|
|
|
546
558
|
try {
|
|
547
559
|
write(this, message, commandOptions, callback);
|
|
@@ -757,3 +769,530 @@ function write(
|
|
|
757
769
|
operationDescription.cb();
|
|
758
770
|
}
|
|
759
771
|
}
|
|
772
|
+
|
|
773
|
+
/** in-progress connection layer */
|
|
774
|
+
|
|
775
|
+
/** @internal */
|
|
776
|
+
export class ModernConnection extends TypedEventEmitter<ConnectionEvents> {
|
|
777
|
+
id: number | '<monitor>';
|
|
778
|
+
address: string;
|
|
779
|
+
socketTimeoutMS: number;
|
|
780
|
+
monitorCommands: boolean;
|
|
781
|
+
/** Indicates that the connection (including underlying TCP socket) has been closed. */
|
|
782
|
+
closed: boolean;
|
|
783
|
+
lastHelloMS?: number;
|
|
784
|
+
serverApi?: ServerApi;
|
|
785
|
+
helloOk?: boolean;
|
|
786
|
+
commandAsync: (
|
|
787
|
+
ns: MongoDBNamespace,
|
|
788
|
+
cmd: Document,
|
|
789
|
+
options: CommandOptions | undefined
|
|
790
|
+
) => Promise<Document>;
|
|
791
|
+
/** @internal */
|
|
792
|
+
authContext?: AuthContext;
|
|
793
|
+
|
|
794
|
+
/**@internal */
|
|
795
|
+
[kDelayedTimeoutId]: NodeJS.Timeout | null;
|
|
796
|
+
/** @internal */
|
|
797
|
+
[kDescription]: StreamDescription;
|
|
798
|
+
/** @internal */
|
|
799
|
+
[kGeneration]: number;
|
|
800
|
+
/** @internal */
|
|
801
|
+
[kLastUseTime]: number;
|
|
802
|
+
/** @internal */
|
|
803
|
+
[kQueue]: Map<number, OperationDescription>;
|
|
804
|
+
/** @internal */
|
|
805
|
+
[kMessageStream]: MessageStream;
|
|
806
|
+
/** @internal */
|
|
807
|
+
socket: Stream;
|
|
808
|
+
/** @internal */
|
|
809
|
+
[kHello]: Document | null;
|
|
810
|
+
/** @internal */
|
|
811
|
+
[kClusterTime]: Document | null;
|
|
812
|
+
|
|
813
|
+
/** @event */
|
|
814
|
+
static readonly COMMAND_STARTED = COMMAND_STARTED;
|
|
815
|
+
/** @event */
|
|
816
|
+
static readonly COMMAND_SUCCEEDED = COMMAND_SUCCEEDED;
|
|
817
|
+
/** @event */
|
|
818
|
+
static readonly COMMAND_FAILED = COMMAND_FAILED;
|
|
819
|
+
/** @event */
|
|
820
|
+
static readonly CLUSTER_TIME_RECEIVED = CLUSTER_TIME_RECEIVED;
|
|
821
|
+
/** @event */
|
|
822
|
+
static readonly CLOSE = CLOSE;
|
|
823
|
+
/** @event */
|
|
824
|
+
static readonly MESSAGE = MESSAGE;
|
|
825
|
+
/** @event */
|
|
826
|
+
static readonly PINNED = PINNED;
|
|
827
|
+
/** @event */
|
|
828
|
+
static readonly UNPINNED = UNPINNED;
|
|
829
|
+
|
|
830
|
+
constructor(stream: Stream, options: ConnectionOptions) {
|
|
831
|
+
super();
|
|
832
|
+
|
|
833
|
+
this.commandAsync = promisify(
|
|
834
|
+
(
|
|
835
|
+
ns: MongoDBNamespace,
|
|
836
|
+
cmd: Document,
|
|
837
|
+
options: CommandOptions | undefined,
|
|
838
|
+
callback: Callback
|
|
839
|
+
) => this.command(ns, cmd, options, callback as any)
|
|
840
|
+
);
|
|
841
|
+
|
|
842
|
+
this.id = options.id;
|
|
843
|
+
this.address = streamIdentifier(stream, options);
|
|
844
|
+
this.socketTimeoutMS = options.socketTimeoutMS ?? 0;
|
|
845
|
+
this.monitorCommands = options.monitorCommands;
|
|
846
|
+
this.serverApi = options.serverApi;
|
|
847
|
+
this.closed = false;
|
|
848
|
+
this[kHello] = null;
|
|
849
|
+
this[kClusterTime] = null;
|
|
850
|
+
|
|
851
|
+
this[kDescription] = new StreamDescription(this.address, options);
|
|
852
|
+
this[kGeneration] = options.generation;
|
|
853
|
+
this[kLastUseTime] = now();
|
|
854
|
+
|
|
855
|
+
// setup parser stream and message handling
|
|
856
|
+
this[kQueue] = new Map();
|
|
857
|
+
this[kMessageStream] = new MessageStream({
|
|
858
|
+
...options,
|
|
859
|
+
maxBsonMessageSize: this.hello?.maxBsonMessageSize
|
|
860
|
+
});
|
|
861
|
+
this.socket = stream;
|
|
862
|
+
|
|
863
|
+
this[kDelayedTimeoutId] = null;
|
|
864
|
+
|
|
865
|
+
this[kMessageStream].on('message', message => this.onMessage(message));
|
|
866
|
+
this[kMessageStream].on('error', error => this.onError(error));
|
|
867
|
+
this.socket.on('close', () => this.onClose());
|
|
868
|
+
this.socket.on('timeout', () => this.onTimeout());
|
|
869
|
+
this.socket.on('error', () => {
|
|
870
|
+
/* ignore errors, listen to `close` instead */
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
// hook the message stream up to the passed in stream
|
|
874
|
+
this.socket.pipe(this[kMessageStream]);
|
|
875
|
+
this[kMessageStream].pipe(this.socket);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
get description(): StreamDescription {
|
|
879
|
+
return this[kDescription];
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
get hello(): Document | null {
|
|
883
|
+
return this[kHello];
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// the `connect` method stores the result of the handshake hello on the connection
|
|
887
|
+
set hello(response: Document | null) {
|
|
888
|
+
this[kDescription].receiveResponse(response);
|
|
889
|
+
this[kDescription] = Object.freeze(this[kDescription]);
|
|
890
|
+
|
|
891
|
+
// TODO: remove this, and only use the `StreamDescription` in the future
|
|
892
|
+
this[kHello] = response;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// Set the whether the message stream is for a monitoring connection.
|
|
896
|
+
set isMonitoringConnection(value: boolean) {
|
|
897
|
+
this[kMessageStream].isMonitoringConnection = value;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
get isMonitoringConnection(): boolean {
|
|
901
|
+
return this[kMessageStream].isMonitoringConnection;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
get serviceId(): ObjectId | undefined {
|
|
905
|
+
return this.hello?.serviceId;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
get loadBalanced(): boolean {
|
|
909
|
+
return this.description.loadBalanced;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
get generation(): number {
|
|
913
|
+
return this[kGeneration] || 0;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
set generation(generation: number) {
|
|
917
|
+
this[kGeneration] = generation;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
get idleTime(): number {
|
|
921
|
+
return calculateDurationInMs(this[kLastUseTime]);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
get clusterTime(): Document | null {
|
|
925
|
+
return this[kClusterTime];
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
get stream(): Stream {
|
|
929
|
+
return this.socket;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
get hasSessionSupport(): boolean {
|
|
933
|
+
return this.description.logicalSessionTimeoutMinutes != null;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
get supportsOpMsg(): boolean {
|
|
937
|
+
return (
|
|
938
|
+
this.description != null &&
|
|
939
|
+
maxWireVersion(this as any as Connection) >= 6 &&
|
|
940
|
+
!this.description.__nodejs_mock_server__
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
markAvailable(): void {
|
|
945
|
+
this[kLastUseTime] = now();
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
onError(error: Error) {
|
|
949
|
+
this.cleanup(true, error);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
onClose() {
|
|
953
|
+
const message = `connection ${this.id} to ${this.address} closed`;
|
|
954
|
+
this.cleanup(true, new MongoNetworkError(message));
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
onTimeout() {
|
|
958
|
+
this[kDelayedTimeoutId] = setTimeout(() => {
|
|
959
|
+
const message = `connection ${this.id} to ${this.address} timed out`;
|
|
960
|
+
const beforeHandshake = this.hello == null;
|
|
961
|
+
this.cleanup(true, new MongoNetworkTimeoutError(message, { beforeHandshake }));
|
|
962
|
+
}, 1).unref(); // No need for this timer to hold the event loop open
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
onMessage(message: OpMsgResponse | OpQueryResponse) {
|
|
966
|
+
const delayedTimeoutId = this[kDelayedTimeoutId];
|
|
967
|
+
if (delayedTimeoutId != null) {
|
|
968
|
+
clearTimeout(delayedTimeoutId);
|
|
969
|
+
this[kDelayedTimeoutId] = null;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const socketTimeoutMS = this.socket.timeout ?? 0;
|
|
973
|
+
this.socket.setTimeout(0);
|
|
974
|
+
|
|
975
|
+
// always emit the message, in case we are streaming
|
|
976
|
+
this.emit('message', message);
|
|
977
|
+
let operationDescription = this[kQueue].get(message.responseTo);
|
|
978
|
+
|
|
979
|
+
if (!operationDescription && this.isMonitoringConnection) {
|
|
980
|
+
// This is how we recover when the initial hello's requestId is not
|
|
981
|
+
// the responseTo when hello responses have been skipped:
|
|
982
|
+
|
|
983
|
+
// First check if the map is of invalid size
|
|
984
|
+
if (this[kQueue].size > 1) {
|
|
985
|
+
this.cleanup(true, new MongoRuntimeError(INVALID_QUEUE_SIZE));
|
|
986
|
+
} else {
|
|
987
|
+
// Get the first orphaned operation description.
|
|
988
|
+
const entry = this[kQueue].entries().next();
|
|
989
|
+
if (entry.value != null) {
|
|
990
|
+
const [requestId, orphaned]: [number, OperationDescription] = entry.value;
|
|
991
|
+
// If the orphaned operation description exists then set it.
|
|
992
|
+
operationDescription = orphaned;
|
|
993
|
+
// Remove the entry with the bad request id from the queue.
|
|
994
|
+
this[kQueue].delete(requestId);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
if (!operationDescription) {
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const callback = operationDescription.cb;
|
|
1004
|
+
|
|
1005
|
+
// SERVER-45775: For exhaust responses we should be able to use the same requestId to
|
|
1006
|
+
// track response, however the server currently synthetically produces remote requests
|
|
1007
|
+
// making the `responseTo` change on each response
|
|
1008
|
+
this[kQueue].delete(message.responseTo);
|
|
1009
|
+
if ('moreToCome' in message && message.moreToCome) {
|
|
1010
|
+
// If the operation description check above does find an orphaned
|
|
1011
|
+
// description and sets the operationDescription then this line will put one
|
|
1012
|
+
// back in the queue with the correct requestId and will resolve not being able
|
|
1013
|
+
// to find the next one via the responseTo of the next streaming hello.
|
|
1014
|
+
this[kQueue].set(message.requestId, operationDescription);
|
|
1015
|
+
this.socket.setTimeout(socketTimeoutMS);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
try {
|
|
1019
|
+
// Pass in the entire description because it has BSON parsing options
|
|
1020
|
+
message.parse(operationDescription);
|
|
1021
|
+
} catch (err) {
|
|
1022
|
+
// If this error is generated by our own code, it will already have the correct class applied
|
|
1023
|
+
// if it is not, then it is coming from a catastrophic data parse failure or the BSON library
|
|
1024
|
+
// in either case, it should not be wrapped
|
|
1025
|
+
callback(err);
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
if (message.documents[0]) {
|
|
1030
|
+
const document: Document = message.documents[0];
|
|
1031
|
+
const session = operationDescription.session;
|
|
1032
|
+
if (session) {
|
|
1033
|
+
updateSessionFromResponse(session, document);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
if (document.$clusterTime) {
|
|
1037
|
+
this[kClusterTime] = document.$clusterTime;
|
|
1038
|
+
this.emit(Connection.CLUSTER_TIME_RECEIVED, document.$clusterTime);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
if (document.writeConcernError) {
|
|
1042
|
+
callback(new MongoWriteConcernError(document.writeConcernError, document), document);
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
if (document.ok === 0 || document.$err || document.errmsg || document.code) {
|
|
1047
|
+
callback(new MongoServerError(document));
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
callback(undefined, message.documents[0]);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
destroy(options: DestroyOptions, callback?: Callback): void {
|
|
1056
|
+
if (this.closed) {
|
|
1057
|
+
process.nextTick(() => callback?.());
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
if (typeof callback === 'function') {
|
|
1061
|
+
this.once('close', () => process.nextTick(() => callback()));
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// load balanced mode requires that these listeners remain on the connection
|
|
1065
|
+
// after cleanup on timeouts, errors or close so we remove them before calling
|
|
1066
|
+
// cleanup.
|
|
1067
|
+
this.removeAllListeners(Connection.PINNED);
|
|
1068
|
+
this.removeAllListeners(Connection.UNPINNED);
|
|
1069
|
+
const message = `connection ${this.id} to ${this.address} closed`;
|
|
1070
|
+
this.cleanup(options.force, new MongoNetworkError(message));
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* A method that cleans up the connection. When `force` is true, this method
|
|
1075
|
+
* forcibly destroys the socket.
|
|
1076
|
+
*
|
|
1077
|
+
* If an error is provided, any in-flight operations will be closed with the error.
|
|
1078
|
+
*
|
|
1079
|
+
* This method does nothing if the connection is already closed.
|
|
1080
|
+
*/
|
|
1081
|
+
private cleanup(force: boolean, error?: Error): void {
|
|
1082
|
+
if (this.closed) {
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
this.closed = true;
|
|
1087
|
+
|
|
1088
|
+
const completeCleanup = () => {
|
|
1089
|
+
for (const op of this[kQueue].values()) {
|
|
1090
|
+
op.cb(error);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
this[kQueue].clear();
|
|
1094
|
+
|
|
1095
|
+
this.emit(Connection.CLOSE);
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
this.socket.removeAllListeners();
|
|
1099
|
+
this[kMessageStream].removeAllListeners();
|
|
1100
|
+
|
|
1101
|
+
this[kMessageStream].destroy();
|
|
1102
|
+
|
|
1103
|
+
if (force) {
|
|
1104
|
+
this.socket.destroy();
|
|
1105
|
+
completeCleanup();
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
if (!this.socket.writableEnded) {
|
|
1110
|
+
this.socket.end(() => {
|
|
1111
|
+
this.socket.destroy();
|
|
1112
|
+
completeCleanup();
|
|
1113
|
+
});
|
|
1114
|
+
} else {
|
|
1115
|
+
completeCleanup();
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
command(
|
|
1120
|
+
ns: MongoDBNamespace,
|
|
1121
|
+
command: Document,
|
|
1122
|
+
options: CommandOptions | undefined,
|
|
1123
|
+
callback: Callback
|
|
1124
|
+
): void {
|
|
1125
|
+
let cmd = { ...command };
|
|
1126
|
+
|
|
1127
|
+
const readPreference = getReadPreference(options);
|
|
1128
|
+
const session = options?.session;
|
|
1129
|
+
|
|
1130
|
+
let clusterTime = this.clusterTime;
|
|
1131
|
+
|
|
1132
|
+
if (this.serverApi) {
|
|
1133
|
+
const { version, strict, deprecationErrors } = this.serverApi;
|
|
1134
|
+
cmd.apiVersion = version;
|
|
1135
|
+
if (strict != null) cmd.apiStrict = strict;
|
|
1136
|
+
if (deprecationErrors != null) cmd.apiDeprecationErrors = deprecationErrors;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
if (this.hasSessionSupport && session) {
|
|
1140
|
+
if (
|
|
1141
|
+
session.clusterTime &&
|
|
1142
|
+
clusterTime &&
|
|
1143
|
+
session.clusterTime.clusterTime.greaterThan(clusterTime.clusterTime)
|
|
1144
|
+
) {
|
|
1145
|
+
clusterTime = session.clusterTime;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
const err = applySession(session, cmd, options);
|
|
1149
|
+
if (err) {
|
|
1150
|
+
return callback(err);
|
|
1151
|
+
}
|
|
1152
|
+
} else if (session?.explicit) {
|
|
1153
|
+
return callback(new MongoCompatibilityError('Current topology does not support sessions'));
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// if we have a known cluster time, gossip it
|
|
1157
|
+
if (clusterTime) {
|
|
1158
|
+
cmd.$clusterTime = clusterTime;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
if (
|
|
1162
|
+
// @ts-expect-error ModernConnections cannot be passed as connections
|
|
1163
|
+
isSharded(this) &&
|
|
1164
|
+
!this.supportsOpMsg &&
|
|
1165
|
+
readPreference &&
|
|
1166
|
+
readPreference.mode !== 'primary'
|
|
1167
|
+
) {
|
|
1168
|
+
cmd = {
|
|
1169
|
+
$query: cmd,
|
|
1170
|
+
$readPreference: readPreference.toJSON()
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const commandOptions: Document = Object.assign(
|
|
1175
|
+
{
|
|
1176
|
+
numberToSkip: 0,
|
|
1177
|
+
numberToReturn: -1,
|
|
1178
|
+
checkKeys: false,
|
|
1179
|
+
// This value is not overridable
|
|
1180
|
+
secondaryOk: readPreference.secondaryOk()
|
|
1181
|
+
},
|
|
1182
|
+
options
|
|
1183
|
+
);
|
|
1184
|
+
|
|
1185
|
+
const message = this.supportsOpMsg
|
|
1186
|
+
? new OpMsgRequest(ns.db, cmd, commandOptions)
|
|
1187
|
+
: new OpQueryRequest(ns.db, cmd, commandOptions);
|
|
1188
|
+
|
|
1189
|
+
try {
|
|
1190
|
+
write(this as any as Connection, message, commandOptions, callback);
|
|
1191
|
+
} catch (err) {
|
|
1192
|
+
callback(err);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const kDefaultMaxBsonMessageSize = 1024 * 1024 * 16 * 4;
|
|
1198
|
+
|
|
1199
|
+
/**
|
|
1200
|
+
* @internal
|
|
1201
|
+
*
|
|
1202
|
+
* This helper reads chucks of data out of a socket and buffers them until it has received a
|
|
1203
|
+
* full wire protocol message.
|
|
1204
|
+
*
|
|
1205
|
+
* By itself, produces an infinite async generator of wire protocol messages and consumers must end
|
|
1206
|
+
* the stream by calling `return` on the generator.
|
|
1207
|
+
*
|
|
1208
|
+
* Note that `for-await` loops call `return` automatically when the loop is exited.
|
|
1209
|
+
*/
|
|
1210
|
+
export async function* readWireProtocolMessages(
|
|
1211
|
+
connection: ModernConnection
|
|
1212
|
+
): AsyncGenerator<Buffer> {
|
|
1213
|
+
const bufferPool = new BufferPool();
|
|
1214
|
+
const maxBsonMessageSize = connection.hello?.maxBsonMessageSize ?? kDefaultMaxBsonMessageSize;
|
|
1215
|
+
for await (const [chunk] of on(connection.socket, 'data')) {
|
|
1216
|
+
bufferPool.append(chunk);
|
|
1217
|
+
const sizeOfMessage = bufferPool.getInt32();
|
|
1218
|
+
|
|
1219
|
+
if (sizeOfMessage == null) {
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
if (sizeOfMessage < 0) {
|
|
1224
|
+
throw new MongoParseError(`Invalid message size: ${sizeOfMessage}`);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
if (sizeOfMessage > maxBsonMessageSize) {
|
|
1228
|
+
throw new MongoParseError(
|
|
1229
|
+
`Invalid message size: ${sizeOfMessage}, max allowed: ${maxBsonMessageSize}`
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
if (sizeOfMessage > bufferPool.length) {
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
yield bufferPool.read(sizeOfMessage);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/**
|
|
1242
|
+
* @internal
|
|
1243
|
+
*
|
|
1244
|
+
* Writes an OP_MSG or OP_QUERY request to the socket, optionally compressing the command. This method
|
|
1245
|
+
* waits until the socket's buffer has emptied (the Nodejs socket `drain` event has fired).
|
|
1246
|
+
*/
|
|
1247
|
+
export async function writeCommand(
|
|
1248
|
+
connection: ModernConnection,
|
|
1249
|
+
command: WriteProtocolMessageType,
|
|
1250
|
+
options: Partial<Pick<OperationDescription, 'agreedCompressor' | 'zlibCompressionLevel'>>
|
|
1251
|
+
): Promise<void> {
|
|
1252
|
+
const drained = once(connection.socket, 'drain');
|
|
1253
|
+
const finalCommand =
|
|
1254
|
+
options.agreedCompressor === 'none' || !OpCompressedRequest.canCompress(command)
|
|
1255
|
+
? command
|
|
1256
|
+
: new OpCompressedRequest(command, {
|
|
1257
|
+
agreedCompressor: options.agreedCompressor ?? 'none',
|
|
1258
|
+
zlibCompressionLevel: options.zlibCompressionLevel ?? 0
|
|
1259
|
+
});
|
|
1260
|
+
const buffer = Buffer.concat(await finalCommand.toBin());
|
|
1261
|
+
connection.socket.push(buffer);
|
|
1262
|
+
await drained;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/**
|
|
1266
|
+
* @internal
|
|
1267
|
+
*
|
|
1268
|
+
* Returns an async generator that yields full wire protocol messages from the underlying socket. This function
|
|
1269
|
+
* yields messages until `moreToCome` is false or not present in a response, or the caller cancels the request
|
|
1270
|
+
* by calling `return` on the generator.
|
|
1271
|
+
*
|
|
1272
|
+
* Note that `for-await` loops call `return` automatically when the loop is exited.
|
|
1273
|
+
*/
|
|
1274
|
+
export async function* readMany(
|
|
1275
|
+
connection: ModernConnection
|
|
1276
|
+
): AsyncGenerator<OpMsgResponse | OpQueryResponse> {
|
|
1277
|
+
for await (const message of readWireProtocolMessages(connection)) {
|
|
1278
|
+
const response = await decompressResponse(message);
|
|
1279
|
+
yield response;
|
|
1280
|
+
|
|
1281
|
+
if (!('moreToCome' in response) || !response.moreToCome) {
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* @internal
|
|
1289
|
+
*
|
|
1290
|
+
* Reads a single wire protocol message out of a connection.
|
|
1291
|
+
*/
|
|
1292
|
+
export async function read(connection: ModernConnection): Promise<OpMsgResponse | OpQueryResponse> {
|
|
1293
|
+
for await (const value of readMany(connection)) {
|
|
1294
|
+
return value;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
throw new MongoRuntimeError('unable to read message off of connection');
|
|
1298
|
+
}
|
package/src/cmap/errors.ts
CHANGED
|
@@ -56,7 +56,7 @@ export class PoolClearedError extends MongoNetworkError {
|
|
|
56
56
|
super(errorMessage, pool.serverError ? { cause: pool.serverError } : undefined);
|
|
57
57
|
this.address = pool.address;
|
|
58
58
|
|
|
59
|
-
this.addErrorLabel(MongoErrorLabel.
|
|
59
|
+
this.addErrorLabel(MongoErrorLabel.PoolRequstedRetry);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
override get name(): string {
|
|
@@ -5,19 +5,13 @@ import { MongoDecompressionError, MongoParseError } from '../error';
|
|
|
5
5
|
import type { ClientSession } from '../sessions';
|
|
6
6
|
import { BufferPool, type Callback } from '../utils';
|
|
7
7
|
import {
|
|
8
|
-
BinMsg,
|
|
9
8
|
type MessageHeader,
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
OpCompressedRequest,
|
|
10
|
+
OpMsgResponse,
|
|
11
|
+
OpQueryResponse,
|
|
12
12
|
type WriteProtocolMessageType
|
|
13
13
|
} from './commands';
|
|
14
|
-
import {
|
|
15
|
-
compress,
|
|
16
|
-
Compressor,
|
|
17
|
-
type CompressorName,
|
|
18
|
-
decompress,
|
|
19
|
-
uncompressibleCommands
|
|
20
|
-
} from './wire_protocol/compression';
|
|
14
|
+
import { compress, Compressor, type CompressorName, decompress } from './wire_protocol/compression';
|
|
21
15
|
import { OP_COMPRESSED, OP_MSG } from './wire_protocol/constants';
|
|
22
16
|
|
|
23
17
|
const MESSAGE_HEADER_SIZE = 16;
|
|
@@ -85,7 +79,7 @@ export class MessageStream extends Duplex {
|
|
|
85
79
|
operationDescription: OperationDescription
|
|
86
80
|
): void {
|
|
87
81
|
const agreedCompressor = operationDescription.agreedCompressor ?? 'none';
|
|
88
|
-
if (agreedCompressor === 'none' || !canCompress(command)) {
|
|
82
|
+
if (agreedCompressor === 'none' || !OpCompressedRequest.canCompress(command)) {
|
|
89
83
|
const data = command.toBin();
|
|
90
84
|
this.push(Array.isArray(data) ? Buffer.concat(data) : data);
|
|
91
85
|
return;
|
|
@@ -128,14 +122,6 @@ export class MessageStream extends Duplex {
|
|
|
128
122
|
}
|
|
129
123
|
}
|
|
130
124
|
|
|
131
|
-
// Return whether a command contains an uncompressible command term
|
|
132
|
-
// Will return true if command contains no uncompressible command terms
|
|
133
|
-
function canCompress(command: WriteProtocolMessageType) {
|
|
134
|
-
const commandDoc = command instanceof Msg ? command.command : command.query;
|
|
135
|
-
const commandName = Object.keys(commandDoc)[0];
|
|
136
|
-
return !uncompressibleCommands.has(commandName);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
125
|
function processIncomingData(stream: MessageStream, callback: Callback<Buffer>): void {
|
|
140
126
|
const buffer = stream[kBuffer];
|
|
141
127
|
const sizeOfMessage = buffer.getInt32();
|
|
@@ -179,7 +165,7 @@ function processIncomingData(stream: MessageStream, callback: Callback<Buffer>):
|
|
|
179
165
|
return false;
|
|
180
166
|
};
|
|
181
167
|
|
|
182
|
-
let ResponseType = messageHeader.opCode === OP_MSG ?
|
|
168
|
+
let ResponseType = messageHeader.opCode === OP_MSG ? OpMsgResponse : OpQueryResponse;
|
|
183
169
|
if (messageHeader.opCode !== OP_COMPRESSED) {
|
|
184
170
|
const messageBody = message.subarray(MESSAGE_HEADER_SIZE);
|
|
185
171
|
|
|
@@ -205,7 +191,7 @@ function processIncomingData(stream: MessageStream, callback: Callback<Buffer>):
|
|
|
205
191
|
const compressedBuffer = message.slice(MESSAGE_HEADER_SIZE + 9);
|
|
206
192
|
|
|
207
193
|
// recalculate based on wrapped opcode
|
|
208
|
-
ResponseType = messageHeader.opCode === OP_MSG ?
|
|
194
|
+
ResponseType = messageHeader.opCode === OP_MSG ? OpMsgResponse : OpQueryResponse;
|
|
209
195
|
decompress(compressorID, compressedBuffer).then(
|
|
210
196
|
messageBody => {
|
|
211
197
|
if (messageBody.length !== messageHeader.length) {
|