mongodb 3.2.5 → 3.3.0-beta2
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/HISTORY.md +0 -10
- package/index.js +4 -4
- package/lib/admin.js +56 -56
- package/lib/aggregation_cursor.js +7 -3
- package/lib/bulk/common.js +18 -13
- package/lib/change_stream.js +196 -89
- package/lib/collection.js +217 -169
- package/lib/command_cursor.js +17 -7
- package/lib/core/auth/auth_provider.js +158 -0
- package/lib/core/auth/defaultAuthProviders.js +29 -0
- package/lib/core/auth/gssapi.js +241 -0
- package/lib/core/auth/mongo_credentials.js +81 -0
- package/lib/core/auth/mongocr.js +51 -0
- package/lib/core/auth/plain.js +35 -0
- package/lib/core/auth/scram.js +293 -0
- package/lib/core/auth/sspi.js +131 -0
- package/lib/core/auth/x509.js +26 -0
- package/lib/core/connection/apm.js +236 -0
- package/lib/core/connection/command_result.js +36 -0
- package/lib/core/connection/commands.js +507 -0
- package/lib/core/connection/connect.js +370 -0
- package/lib/core/connection/connection.js +624 -0
- package/lib/core/connection/logger.js +246 -0
- package/lib/core/connection/msg.js +219 -0
- package/lib/core/connection/pool.js +1285 -0
- package/lib/core/connection/utils.js +57 -0
- package/lib/core/cursor.js +752 -0
- package/lib/core/error.js +186 -0
- package/lib/core/index.js +50 -0
- package/lib/core/sdam/monitoring.js +228 -0
- package/lib/core/sdam/server.js +467 -0
- package/lib/core/sdam/server_description.js +163 -0
- package/lib/core/sdam/server_selectors.js +244 -0
- package/lib/core/sdam/srv_polling.js +135 -0
- package/lib/core/sdam/topology.js +1151 -0
- package/lib/core/sdam/topology_description.js +408 -0
- package/lib/core/sessions.js +711 -0
- package/lib/core/tools/smoke_plugin.js +61 -0
- package/lib/core/topologies/mongos.js +1337 -0
- package/lib/core/topologies/read_preference.js +202 -0
- package/lib/core/topologies/replset.js +1507 -0
- package/lib/core/topologies/replset_state.js +1121 -0
- package/lib/core/topologies/server.js +984 -0
- package/lib/core/topologies/shared.js +453 -0
- package/lib/core/transactions.js +167 -0
- package/lib/core/uri_parser.js +631 -0
- package/lib/core/utils.js +165 -0
- package/lib/core/wireprotocol/command.js +170 -0
- package/lib/core/wireprotocol/compression.js +73 -0
- package/lib/core/wireprotocol/constants.js +13 -0
- package/lib/core/wireprotocol/get_more.js +86 -0
- package/lib/core/wireprotocol/index.js +18 -0
- package/lib/core/wireprotocol/kill_cursors.js +70 -0
- package/lib/core/wireprotocol/query.js +224 -0
- package/lib/core/wireprotocol/shared.js +115 -0
- package/lib/core/wireprotocol/write_command.js +50 -0
- package/lib/cursor.js +40 -46
- package/lib/db.js +141 -95
- package/lib/dynamic_loaders.js +32 -0
- package/lib/error.js +12 -10
- package/lib/gridfs/chunk.js +2 -2
- package/lib/gridfs/grid_store.js +31 -25
- package/lib/gridfs-stream/index.js +4 -4
- package/lib/gridfs-stream/upload.js +1 -1
- package/lib/mongo_client.js +37 -15
- package/lib/operations/add_user.js +96 -0
- package/lib/operations/aggregate.js +24 -13
- package/lib/operations/aggregate_operation.js +127 -0
- package/lib/operations/bulk_write.js +104 -0
- package/lib/operations/close.js +47 -0
- package/lib/operations/collection_ops.js +28 -287
- package/lib/operations/collections.js +55 -0
- package/lib/operations/command.js +120 -0
- package/lib/operations/command_v2.js +43 -0
- package/lib/operations/common_functions.js +372 -0
- package/lib/operations/{mongo_client_ops.js → connect.js} +185 -157
- package/lib/operations/count.js +72 -0
- package/lib/operations/count_documents.js +46 -0
- package/lib/operations/create_collection.js +118 -0
- package/lib/operations/create_index.js +92 -0
- package/lib/operations/create_indexes.js +61 -0
- package/lib/operations/cursor_ops.js +3 -4
- package/lib/operations/db_ops.js +15 -12
- package/lib/operations/delete_many.js +25 -0
- package/lib/operations/delete_one.js +25 -0
- package/lib/operations/distinct.js +85 -0
- package/lib/operations/drop.js +53 -0
- package/lib/operations/drop_index.js +42 -0
- package/lib/operations/drop_indexes.js +23 -0
- package/lib/operations/estimated_document_count.js +33 -0
- package/lib/operations/execute_db_admin_command.js +34 -0
- package/lib/operations/execute_operation.js +165 -0
- package/lib/operations/explain.js +23 -0
- package/lib/operations/find_and_modify.js +98 -0
- package/lib/operations/find_one.js +33 -0
- package/lib/operations/find_one_and_delete.js +16 -0
- package/lib/operations/find_one_and_replace.js +18 -0
- package/lib/operations/find_one_and_update.js +19 -0
- package/lib/operations/geo_haystack_search.js +79 -0
- package/lib/operations/has_next.js +40 -0
- package/lib/operations/index_exists.js +39 -0
- package/lib/operations/index_information.js +23 -0
- package/lib/operations/indexes.js +22 -0
- package/lib/operations/insert_many.js +63 -0
- package/lib/operations/insert_one.js +75 -0
- package/lib/operations/is_capped.js +19 -0
- package/lib/operations/list_indexes.js +66 -0
- package/lib/operations/map_reduce.js +189 -0
- package/lib/operations/next.js +32 -0
- package/lib/operations/operation.js +63 -0
- package/lib/operations/options_operation.js +32 -0
- package/lib/operations/profiling_level.js +31 -0
- package/lib/operations/re_index.js +28 -0
- package/lib/operations/remove_user.js +52 -0
- package/lib/operations/rename.js +61 -0
- package/lib/operations/replace_one.js +47 -0
- package/lib/operations/set_profiling_level.js +48 -0
- package/lib/operations/stats.js +45 -0
- package/lib/operations/to_array.js +68 -0
- package/lib/operations/update_many.js +29 -0
- package/lib/operations/update_one.js +44 -0
- package/lib/operations/validate_collection.js +40 -0
- package/lib/read_concern.js +55 -0
- package/lib/topologies/mongos.js +3 -3
- package/lib/topologies/native_topology.js +22 -2
- package/lib/topologies/replset.js +3 -3
- package/lib/topologies/server.js +4 -4
- package/lib/topologies/topology_base.js +6 -6
- package/lib/url_parser.js +4 -3
- package/lib/utils.js +46 -59
- package/lib/write_concern.js +66 -0
- package/package.json +15 -6
- package/lib/.DS_Store +0 -0
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const retrieveBSON = require('./connection/utils').retrieveBSON;
|
|
4
|
+
const EventEmitter = require('events');
|
|
5
|
+
const BSON = retrieveBSON();
|
|
6
|
+
const Binary = BSON.Binary;
|
|
7
|
+
const uuidV4 = require('./utils').uuidV4;
|
|
8
|
+
const MongoError = require('./error').MongoError;
|
|
9
|
+
const isRetryableError = require('././error').isRetryableError;
|
|
10
|
+
const MongoNetworkError = require('./error').MongoNetworkError;
|
|
11
|
+
const MongoWriteConcernError = require('./error').MongoWriteConcernError;
|
|
12
|
+
const Transaction = require('./transactions').Transaction;
|
|
13
|
+
const TxnState = require('./transactions').TxnState;
|
|
14
|
+
const isPromiseLike = require('./utils').isPromiseLike;
|
|
15
|
+
const ReadPreference = require('./topologies/read_preference');
|
|
16
|
+
const isTransactionCommand = require('./transactions').isTransactionCommand;
|
|
17
|
+
const resolveClusterTime = require('./topologies/shared').resolveClusterTime;
|
|
18
|
+
const isSharded = require('./wireprotocol/shared').isSharded;
|
|
19
|
+
const maxWireVersion = require('./utils').maxWireVersion;
|
|
20
|
+
|
|
21
|
+
const MAX_FOR_TRANSACTIONS = 7;
|
|
22
|
+
|
|
23
|
+
function assertAlive(session, callback) {
|
|
24
|
+
if (session.serverSession == null) {
|
|
25
|
+
const error = new MongoError('Cannot use a session that has ended');
|
|
26
|
+
if (typeof callback === 'function') {
|
|
27
|
+
callback(error, null);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Options to pass when creating a Client Session
|
|
39
|
+
* @typedef {Object} SessionOptions
|
|
40
|
+
* @property {boolean} [causalConsistency=true] Whether causal consistency should be enabled on this session
|
|
41
|
+
* @property {TransactionOptions} [defaultTransactionOptions] The default TransactionOptions to use for transactions started on this session.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A BSON document reflecting the lsid of a {@link ClientSession}
|
|
46
|
+
* @typedef {Object} SessionId
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A class representing a client session on the server
|
|
51
|
+
* WARNING: not meant to be instantiated directly.
|
|
52
|
+
* @class
|
|
53
|
+
* @hideconstructor
|
|
54
|
+
*/
|
|
55
|
+
class ClientSession extends EventEmitter {
|
|
56
|
+
/**
|
|
57
|
+
* Create a client session.
|
|
58
|
+
* WARNING: not meant to be instantiated directly
|
|
59
|
+
*
|
|
60
|
+
* @param {Topology} topology The current client's topology (Internal Class)
|
|
61
|
+
* @param {ServerSessionPool} sessionPool The server session pool (Internal Class)
|
|
62
|
+
* @param {SessionOptions} [options] Optional settings
|
|
63
|
+
* @param {Object} [clientOptions] Optional settings provided when creating a client in the porcelain driver
|
|
64
|
+
*/
|
|
65
|
+
constructor(topology, sessionPool, options, clientOptions) {
|
|
66
|
+
super();
|
|
67
|
+
|
|
68
|
+
if (topology == null) {
|
|
69
|
+
throw new Error('ClientSession requires a topology');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (sessionPool == null || !(sessionPool instanceof ServerSessionPool)) {
|
|
73
|
+
throw new Error('ClientSession requires a ServerSessionPool');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
options = options || {};
|
|
77
|
+
clientOptions = clientOptions || {};
|
|
78
|
+
|
|
79
|
+
this.topology = topology;
|
|
80
|
+
this.sessionPool = sessionPool;
|
|
81
|
+
this.hasEnded = false;
|
|
82
|
+
this.serverSession = sessionPool.acquire();
|
|
83
|
+
this.clientOptions = clientOptions;
|
|
84
|
+
|
|
85
|
+
this.supports = {
|
|
86
|
+
causalConsistency:
|
|
87
|
+
typeof options.causalConsistency !== 'undefined' ? options.causalConsistency : true
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
this.clusterTime = options.initialClusterTime;
|
|
91
|
+
|
|
92
|
+
this.operationTime = null;
|
|
93
|
+
this.explicit = !!options.explicit;
|
|
94
|
+
this.owner = options.owner;
|
|
95
|
+
this.defaultTransactionOptions = Object.assign({}, options.defaultTransactionOptions);
|
|
96
|
+
this.transaction = new Transaction();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The server id associated with this session
|
|
101
|
+
* @type {SessionId}
|
|
102
|
+
*/
|
|
103
|
+
get id() {
|
|
104
|
+
return this.serverSession.id;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Ends this session on the server
|
|
109
|
+
*
|
|
110
|
+
* @param {Object} [options] Optional settings. Currently reserved for future use
|
|
111
|
+
* @param {Function} [callback] Optional callback for completion of this operation
|
|
112
|
+
*/
|
|
113
|
+
endSession(options, callback) {
|
|
114
|
+
if (typeof options === 'function') (callback = options), (options = {});
|
|
115
|
+
options = options || {};
|
|
116
|
+
|
|
117
|
+
if (this.hasEnded) {
|
|
118
|
+
if (typeof callback === 'function') callback(null, null);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (this.serverSession && this.inTransaction()) {
|
|
123
|
+
this.abortTransaction(); // pass in callback?
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// mark the session as ended, and emit a signal
|
|
127
|
+
this.hasEnded = true;
|
|
128
|
+
this.emit('ended', this);
|
|
129
|
+
|
|
130
|
+
// release the server session back to the pool
|
|
131
|
+
this.sessionPool.release(this.serverSession);
|
|
132
|
+
this.serverSession = null;
|
|
133
|
+
|
|
134
|
+
// spec indicates that we should ignore all errors for `endSessions`
|
|
135
|
+
if (typeof callback === 'function') callback(null, null);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Advances the operationTime for a ClientSession.
|
|
140
|
+
*
|
|
141
|
+
* @param {Timestamp} operationTime the `BSON.Timestamp` of the operation type it is desired to advance to
|
|
142
|
+
*/
|
|
143
|
+
advanceOperationTime(operationTime) {
|
|
144
|
+
if (this.operationTime == null) {
|
|
145
|
+
this.operationTime = operationTime;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (operationTime.greaterThan(this.operationTime)) {
|
|
150
|
+
this.operationTime = operationTime;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Used to determine if this session equals another
|
|
156
|
+
* @param {ClientSession} session
|
|
157
|
+
* @return {boolean} true if the sessions are equal
|
|
158
|
+
*/
|
|
159
|
+
equals(session) {
|
|
160
|
+
if (!(session instanceof ClientSession)) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return this.id.id.buffer.equals(session.id.id.buffer);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Increment the transaction number on the internal ServerSession
|
|
169
|
+
*/
|
|
170
|
+
incrementTransactionNumber() {
|
|
171
|
+
this.serverSession.txnNumber++;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @returns {boolean} whether this session is currently in a transaction or not
|
|
176
|
+
*/
|
|
177
|
+
inTransaction() {
|
|
178
|
+
return this.transaction.isActive;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Starts a new transaction with the given options.
|
|
183
|
+
*
|
|
184
|
+
* @param {TransactionOptions} options Options for the transaction
|
|
185
|
+
*/
|
|
186
|
+
startTransaction(options) {
|
|
187
|
+
assertAlive(this);
|
|
188
|
+
if (this.inTransaction()) {
|
|
189
|
+
throw new MongoError('Transaction already in progress');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const topologyMaxWireVersion = maxWireVersion(this.topology);
|
|
193
|
+
if (
|
|
194
|
+
isSharded(this.topology) ||
|
|
195
|
+
(topologyMaxWireVersion != null && topologyMaxWireVersion < MAX_FOR_TRANSACTIONS)
|
|
196
|
+
) {
|
|
197
|
+
throw new MongoError('Transactions are not supported on sharded clusters in MongoDB < 4.2.');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// increment txnNumber
|
|
201
|
+
this.incrementTransactionNumber();
|
|
202
|
+
|
|
203
|
+
// create transaction state
|
|
204
|
+
this.transaction = new Transaction(
|
|
205
|
+
Object.assign({}, this.clientOptions, options || this.defaultTransactionOptions)
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
this.transaction.transition(TxnState.STARTING_TRANSACTION);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Commits the currently active transaction in this session.
|
|
213
|
+
*
|
|
214
|
+
* @param {Function} [callback] optional callback for completion of this operation
|
|
215
|
+
* @return {Promise} A promise is returned if no callback is provided
|
|
216
|
+
*/
|
|
217
|
+
commitTransaction(callback) {
|
|
218
|
+
if (typeof callback === 'function') {
|
|
219
|
+
endTransaction(this, 'commitTransaction', callback);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return new Promise((resolve, reject) => {
|
|
224
|
+
endTransaction(
|
|
225
|
+
this,
|
|
226
|
+
'commitTransaction',
|
|
227
|
+
(err, reply) => (err ? reject(err) : resolve(reply))
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Aborts the currently active transaction in this session.
|
|
234
|
+
*
|
|
235
|
+
* @param {Function} [callback] optional callback for completion of this operation
|
|
236
|
+
* @return {Promise} A promise is returned if no callback is provided
|
|
237
|
+
*/
|
|
238
|
+
abortTransaction(callback) {
|
|
239
|
+
if (typeof callback === 'function') {
|
|
240
|
+
endTransaction(this, 'abortTransaction', callback);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return new Promise((resolve, reject) => {
|
|
245
|
+
endTransaction(
|
|
246
|
+
this,
|
|
247
|
+
'abortTransaction',
|
|
248
|
+
(err, reply) => (err ? reject(err) : resolve(reply))
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* This is here to ensure that ClientSession is never serialized to BSON.
|
|
255
|
+
* @ignore
|
|
256
|
+
*/
|
|
257
|
+
toBSON() {
|
|
258
|
+
throw new Error('ClientSession cannot be serialized to BSON.');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* A user provided function to be run within a transaction
|
|
263
|
+
*
|
|
264
|
+
* @callback WithTransactionCallback
|
|
265
|
+
* @param {ClientSession} session The parent session of the transaction running the operation. This should be passed into each operation within the lambda.
|
|
266
|
+
* @returns {Promise} The resulting Promise of operations run within this transaction
|
|
267
|
+
*/
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Runs a provided lambda within a transaction, retrying either the commit operation
|
|
271
|
+
* or entire transaction as needed (and when the error permits) to better ensure that
|
|
272
|
+
* the transaction can complete successfully.
|
|
273
|
+
*
|
|
274
|
+
* IMPORTANT: This method requires the user to return a Promise, all lambdas that do not
|
|
275
|
+
* return a Promise will result in undefined behavior.
|
|
276
|
+
*
|
|
277
|
+
* @param {WithTransactionCallback} fn
|
|
278
|
+
* @param {TransactionOptions} [options] Optional settings for the transaction
|
|
279
|
+
*/
|
|
280
|
+
withTransaction(fn, options) {
|
|
281
|
+
const startTime = Date.now();
|
|
282
|
+
return attemptTransaction(this, startTime, fn, options);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const MAX_WITH_TRANSACTION_TIMEOUT = 120000;
|
|
287
|
+
const UNSATISFIABLE_WRITE_CONCERN_CODE = 100;
|
|
288
|
+
const UNKNOWN_REPL_WRITE_CONCERN_CODE = 79;
|
|
289
|
+
const NON_DETERMINISTIC_WRITE_CONCERN_ERRORS = new Set([
|
|
290
|
+
'CannotSatisfyWriteConcern',
|
|
291
|
+
'UnknownReplWriteConcern',
|
|
292
|
+
'UnsatisfiableWriteConcern'
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
function hasNotTimedOut(startTime, max) {
|
|
296
|
+
return Date.now() - startTime < max;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function isUnknownTransactionCommitResult(err) {
|
|
300
|
+
return (
|
|
301
|
+
!NON_DETERMINISTIC_WRITE_CONCERN_ERRORS.has(err.codeName) &&
|
|
302
|
+
err.code !== UNSATISFIABLE_WRITE_CONCERN_CODE &&
|
|
303
|
+
err.code !== UNKNOWN_REPL_WRITE_CONCERN_CODE
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function attemptTransactionCommit(session, startTime, fn, options) {
|
|
308
|
+
return session.commitTransaction().catch(err => {
|
|
309
|
+
if (err instanceof MongoError && hasNotTimedOut(startTime, MAX_WITH_TRANSACTION_TIMEOUT)) {
|
|
310
|
+
if (err.hasErrorLabel('UnknownTransactionCommitResult')) {
|
|
311
|
+
return attemptTransactionCommit(session, startTime, fn, options);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (err.hasErrorLabel('TransientTransactionError')) {
|
|
315
|
+
return attemptTransaction(session, startTime, fn, options);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
throw err;
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const USER_EXPLICIT_TXN_END_STATES = new Set([
|
|
324
|
+
TxnState.NO_TRANSACTION,
|
|
325
|
+
TxnState.TRANSACTION_COMMITTED,
|
|
326
|
+
TxnState.TRANSACTION_ABORTED
|
|
327
|
+
]);
|
|
328
|
+
|
|
329
|
+
function userExplicitlyEndedTransaction(session) {
|
|
330
|
+
return USER_EXPLICIT_TXN_END_STATES.has(session.transaction.state);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function attemptTransaction(session, startTime, fn, options) {
|
|
334
|
+
session.startTransaction(options);
|
|
335
|
+
|
|
336
|
+
let promise;
|
|
337
|
+
try {
|
|
338
|
+
promise = fn(session);
|
|
339
|
+
} catch (err) {
|
|
340
|
+
promise = Promise.reject(err);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!isPromiseLike(promise)) {
|
|
344
|
+
session.abortTransaction();
|
|
345
|
+
throw new TypeError('Function provided to `withTransaction` must return a Promise');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return promise
|
|
349
|
+
.then(() => {
|
|
350
|
+
if (userExplicitlyEndedTransaction(session)) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return attemptTransactionCommit(session, startTime, fn, options);
|
|
355
|
+
})
|
|
356
|
+
.catch(err => {
|
|
357
|
+
function maybeRetryOrThrow(err) {
|
|
358
|
+
if (
|
|
359
|
+
err instanceof MongoError &&
|
|
360
|
+
err.hasErrorLabel('TransientTransactionError') &&
|
|
361
|
+
hasNotTimedOut(startTime, MAX_WITH_TRANSACTION_TIMEOUT)
|
|
362
|
+
) {
|
|
363
|
+
return attemptTransaction(session, startTime, fn, options);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
throw err;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (session.transaction.isActive) {
|
|
370
|
+
return session.abortTransaction().then(() => maybeRetryOrThrow(err));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return maybeRetryOrThrow(err);
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function endTransaction(session, commandName, callback) {
|
|
378
|
+
if (!assertAlive(session, callback)) {
|
|
379
|
+
// checking result in case callback was called
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// handle any initial problematic cases
|
|
384
|
+
let txnState = session.transaction.state;
|
|
385
|
+
|
|
386
|
+
if (txnState === TxnState.NO_TRANSACTION) {
|
|
387
|
+
callback(new MongoError('No transaction started'));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (commandName === 'commitTransaction') {
|
|
392
|
+
if (
|
|
393
|
+
txnState === TxnState.STARTING_TRANSACTION ||
|
|
394
|
+
txnState === TxnState.TRANSACTION_COMMITTED_EMPTY
|
|
395
|
+
) {
|
|
396
|
+
// the transaction was never started, we can safely exit here
|
|
397
|
+
session.transaction.transition(TxnState.TRANSACTION_COMMITTED_EMPTY);
|
|
398
|
+
callback(null, null);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (txnState === TxnState.TRANSACTION_ABORTED) {
|
|
403
|
+
callback(new MongoError('Cannot call commitTransaction after calling abortTransaction'));
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
} else {
|
|
407
|
+
if (txnState === TxnState.STARTING_TRANSACTION) {
|
|
408
|
+
// the transaction was never started, we can safely exit here
|
|
409
|
+
session.transaction.transition(TxnState.TRANSACTION_ABORTED);
|
|
410
|
+
callback(null, null);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (txnState === TxnState.TRANSACTION_ABORTED) {
|
|
415
|
+
callback(new MongoError('Cannot call abortTransaction twice'));
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (
|
|
420
|
+
txnState === TxnState.TRANSACTION_COMMITTED ||
|
|
421
|
+
txnState === TxnState.TRANSACTION_COMMITTED_EMPTY
|
|
422
|
+
) {
|
|
423
|
+
callback(new MongoError('Cannot call abortTransaction after calling commitTransaction'));
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// construct and send the command
|
|
429
|
+
const command = { [commandName]: 1 };
|
|
430
|
+
|
|
431
|
+
// apply a writeConcern if specified
|
|
432
|
+
let writeConcern;
|
|
433
|
+
if (session.transaction.options.writeConcern) {
|
|
434
|
+
writeConcern = Object.assign({}, session.transaction.options.writeConcern);
|
|
435
|
+
} else if (session.clientOptions && session.clientOptions.w) {
|
|
436
|
+
writeConcern = { w: session.clientOptions.w };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (txnState === TxnState.TRANSACTION_COMMITTED) {
|
|
440
|
+
writeConcern = Object.assign({ wtimeout: 10000 }, writeConcern, { w: 'majority' });
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (writeConcern) {
|
|
444
|
+
Object.assign(command, { writeConcern });
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function commandHandler(e, r) {
|
|
448
|
+
if (commandName === 'commitTransaction') {
|
|
449
|
+
session.transaction.transition(TxnState.TRANSACTION_COMMITTED);
|
|
450
|
+
|
|
451
|
+
if (
|
|
452
|
+
e &&
|
|
453
|
+
(e instanceof MongoNetworkError ||
|
|
454
|
+
e instanceof MongoWriteConcernError ||
|
|
455
|
+
isRetryableError(e))
|
|
456
|
+
) {
|
|
457
|
+
if (e.errorLabels) {
|
|
458
|
+
const idx = e.errorLabels.indexOf('TransientTransactionError');
|
|
459
|
+
if (idx !== -1) {
|
|
460
|
+
e.errorLabels.splice(idx, 1);
|
|
461
|
+
}
|
|
462
|
+
} else {
|
|
463
|
+
e.errorLabels = [];
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (isUnknownTransactionCommitResult(e)) {
|
|
467
|
+
e.errorLabels.push('UnknownTransactionCommitResult');
|
|
468
|
+
|
|
469
|
+
// per txns spec, must unpin session in this case
|
|
470
|
+
session.transaction.unpinServer();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
} else {
|
|
474
|
+
session.transaction.transition(TxnState.TRANSACTION_ABORTED);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
callback(e, r);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// The spec indicates that we should ignore all errors on `abortTransaction`
|
|
481
|
+
function transactionError(err) {
|
|
482
|
+
return commandName === 'commitTransaction' ? err : null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (
|
|
486
|
+
// Assumption here that commandName is "commitTransaction" or "abortTransaction"
|
|
487
|
+
session.transaction.recoveryToken &&
|
|
488
|
+
supportsRecoveryToken(session)
|
|
489
|
+
) {
|
|
490
|
+
command.recoveryToken = session.transaction.recoveryToken;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// send the command
|
|
494
|
+
session.topology.command('admin.$cmd', command, { session }, (err, reply) => {
|
|
495
|
+
if (err && isRetryableError(err)) {
|
|
496
|
+
// SPEC-1185: apply majority write concern when retrying commitTransaction
|
|
497
|
+
if (command.commitTransaction) {
|
|
498
|
+
// per txns spec, must unpin session in this case
|
|
499
|
+
session.transaction.unpinServer();
|
|
500
|
+
|
|
501
|
+
command.writeConcern = Object.assign({ wtimeout: 10000 }, command.writeConcern, {
|
|
502
|
+
w: 'majority'
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return session.topology.command('admin.$cmd', command, { session }, (_err, _reply) =>
|
|
507
|
+
commandHandler(transactionError(_err), _reply)
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
commandHandler(transactionError(err), reply);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function supportsRecoveryToken(session) {
|
|
516
|
+
const topology = session.topology;
|
|
517
|
+
return !!topology.s.options.useRecoveryToken;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Reflects the existence of a session on the server. Can be reused by the session pool.
|
|
522
|
+
* WARNING: not meant to be instantiated directly. For internal use only.
|
|
523
|
+
* @ignore
|
|
524
|
+
*/
|
|
525
|
+
class ServerSession {
|
|
526
|
+
constructor() {
|
|
527
|
+
this.id = { id: new Binary(uuidV4(), Binary.SUBTYPE_UUID) };
|
|
528
|
+
this.lastUse = Date.now();
|
|
529
|
+
this.txnNumber = 0;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Determines if the server session has timed out.
|
|
534
|
+
* @ignore
|
|
535
|
+
* @param {Date} sessionTimeoutMinutes The server's "logicalSessionTimeoutMinutes"
|
|
536
|
+
* @return {boolean} true if the session has timed out.
|
|
537
|
+
*/
|
|
538
|
+
hasTimedOut(sessionTimeoutMinutes) {
|
|
539
|
+
// Take the difference of the lastUse timestamp and now, which will result in a value in
|
|
540
|
+
// milliseconds, and then convert milliseconds to minutes to compare to `sessionTimeoutMinutes`
|
|
541
|
+
const idleTimeMinutes = Math.round(
|
|
542
|
+
(((Date.now() - this.lastUse) % 86400000) % 3600000) / 60000
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
return idleTimeMinutes > sessionTimeoutMinutes - 1;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Maintains a pool of Server Sessions.
|
|
551
|
+
* For internal use only
|
|
552
|
+
* @ignore
|
|
553
|
+
*/
|
|
554
|
+
class ServerSessionPool {
|
|
555
|
+
constructor(topology) {
|
|
556
|
+
if (topology == null) {
|
|
557
|
+
throw new Error('ServerSessionPool requires a topology');
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
this.topology = topology;
|
|
561
|
+
this.sessions = [];
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Ends all sessions in the session pool.
|
|
566
|
+
* @ignore
|
|
567
|
+
*/
|
|
568
|
+
endAllPooledSessions() {
|
|
569
|
+
if (this.sessions.length) {
|
|
570
|
+
this.topology.endSessions(this.sessions.map(session => session.id));
|
|
571
|
+
this.sessions = [];
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Acquire a Server Session from the pool.
|
|
577
|
+
* Iterates through each session in the pool, removing any stale sessions
|
|
578
|
+
* along the way. The first non-stale session found is removed from the
|
|
579
|
+
* pool and returned. If no non-stale session is found, a new ServerSession
|
|
580
|
+
* is created.
|
|
581
|
+
* @ignore
|
|
582
|
+
* @returns {ServerSession}
|
|
583
|
+
*/
|
|
584
|
+
acquire() {
|
|
585
|
+
const sessionTimeoutMinutes = this.topology.logicalSessionTimeoutMinutes;
|
|
586
|
+
while (this.sessions.length) {
|
|
587
|
+
const session = this.sessions.shift();
|
|
588
|
+
if (!session.hasTimedOut(sessionTimeoutMinutes)) {
|
|
589
|
+
return session;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return new ServerSession();
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Release a session to the session pool
|
|
598
|
+
* Adds the session back to the session pool if the session has not timed out yet.
|
|
599
|
+
* This method also removes any stale sessions from the pool.
|
|
600
|
+
* @ignore
|
|
601
|
+
* @param {ServerSession} session The session to release to the pool
|
|
602
|
+
*/
|
|
603
|
+
release(session) {
|
|
604
|
+
const sessionTimeoutMinutes = this.topology.logicalSessionTimeoutMinutes;
|
|
605
|
+
while (this.sessions.length) {
|
|
606
|
+
const session = this.sessions[this.sessions.length - 1];
|
|
607
|
+
if (session.hasTimedOut(sessionTimeoutMinutes)) {
|
|
608
|
+
this.sessions.pop();
|
|
609
|
+
} else {
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (!session.hasTimedOut(sessionTimeoutMinutes)) {
|
|
615
|
+
this.sessions.unshift(session);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Optionally decorate a command with sessions specific keys
|
|
622
|
+
*
|
|
623
|
+
* @param {ClientSession} session the session tracking transaction state
|
|
624
|
+
* @param {Object} command the command to decorate
|
|
625
|
+
* @param {Object} topology the topology for tracking the cluster time
|
|
626
|
+
* @param {Object} [options] Optional settings passed to calling operation
|
|
627
|
+
* @return {MongoError|null} An error, if some error condition was met
|
|
628
|
+
*/
|
|
629
|
+
function applySession(session, command, options) {
|
|
630
|
+
const serverSession = session.serverSession;
|
|
631
|
+
if (serverSession == null) {
|
|
632
|
+
// TODO: merge this with `assertAlive`, did not want to throw a try/catch here
|
|
633
|
+
return new MongoError('Cannot use a session that has ended');
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// mark the last use of this session, and apply the `lsid`
|
|
637
|
+
serverSession.lastUse = Date.now();
|
|
638
|
+
command.lsid = serverSession.id;
|
|
639
|
+
|
|
640
|
+
// first apply non-transaction-specific sessions data
|
|
641
|
+
const inTransaction = session.inTransaction() || isTransactionCommand(command);
|
|
642
|
+
const isRetryableWrite = options.willRetryWrite;
|
|
643
|
+
|
|
644
|
+
if (serverSession.txnNumber && (isRetryableWrite || inTransaction)) {
|
|
645
|
+
command.txnNumber = BSON.Long.fromNumber(serverSession.txnNumber);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// now attempt to apply transaction-specific sessions data
|
|
649
|
+
if (!inTransaction) {
|
|
650
|
+
if (session.transaction.state !== TxnState.NO_TRANSACTION) {
|
|
651
|
+
session.transaction.transition(TxnState.NO_TRANSACTION);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// TODO: the following should only be applied to read operation per spec.
|
|
655
|
+
// for causal consistency
|
|
656
|
+
if (session.supports.causalConsistency && session.operationTime) {
|
|
657
|
+
command.readConcern = command.readConcern || {};
|
|
658
|
+
Object.assign(command.readConcern, { afterClusterTime: session.operationTime });
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (options.readPreference && !options.readPreference.equals(ReadPreference.primary)) {
|
|
665
|
+
return new MongoError(
|
|
666
|
+
`Read preference in a transaction must be primary, not: ${options.readPreference.mode}`
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// `autocommit` must always be false to differentiate from retryable writes
|
|
671
|
+
command.autocommit = false;
|
|
672
|
+
|
|
673
|
+
if (session.transaction.state === TxnState.STARTING_TRANSACTION) {
|
|
674
|
+
session.transaction.transition(TxnState.TRANSACTION_IN_PROGRESS);
|
|
675
|
+
command.startTransaction = true;
|
|
676
|
+
|
|
677
|
+
const readConcern =
|
|
678
|
+
session.transaction.options.readConcern || session.clientOptions.readConcern;
|
|
679
|
+
if (readConcern) {
|
|
680
|
+
command.readConcern = readConcern;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (session.supports.causalConsistency && session.operationTime) {
|
|
684
|
+
command.readConcern = command.readConcern || {};
|
|
685
|
+
Object.assign(command.readConcern, { afterClusterTime: session.operationTime });
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function updateSessionFromResponse(session, document) {
|
|
691
|
+
if (document.$clusterTime) {
|
|
692
|
+
resolveClusterTime(session, document.$clusterTime);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (document.operationTime && session && session.supports.causalConsistency) {
|
|
696
|
+
session.advanceOperationTime(document.operationTime);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (document.recoveryToken && session && session.inTransaction()) {
|
|
700
|
+
session.transaction._recoveryToken = document.recoveryToken;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
module.exports = {
|
|
705
|
+
ClientSession,
|
|
706
|
+
ServerSession,
|
|
707
|
+
ServerSessionPool,
|
|
708
|
+
TxnState,
|
|
709
|
+
applySession,
|
|
710
|
+
updateSessionFromResponse
|
|
711
|
+
};
|