execute_operation.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.executeOperation = executeOperation;
  4. exports.autoConnect = autoConnect;
  5. const error_1 = require("../error");
  6. const read_preference_1 = require("../read_preference");
  7. const server_selection_1 = require("../sdam/server_selection");
  8. const timeout_1 = require("../timeout");
  9. const utils_1 = require("../utils");
  10. const aggregate_1 = require("./aggregate");
  11. const operation_1 = require("./operation");
  12. const MMAPv1_RETRY_WRITES_ERROR_CODE = error_1.MONGODB_ERROR_CODES.IllegalOperation;
  13. const MMAPv1_RETRY_WRITES_ERROR_MESSAGE = 'This MongoDB deployment does not support retryable writes. Please add retryWrites=false to your connection string.';
  14. /**
  15. * Executes the given operation with provided arguments.
  16. * @internal
  17. *
  18. * @remarks
  19. * Allows for a single point of entry to provide features such as implicit sessions, which
  20. * are required by the Driver Sessions specification in the event that a ClientSession is
  21. * not provided.
  22. *
  23. * The expectation is that this function:
  24. * - Connects the MongoClient if it has not already been connected, see {@link autoConnect}
  25. * - Creates a session if none is provided and cleans up the session it creates
  26. * - Tries an operation and retries under certain conditions, see {@link tryOperation}
  27. *
  28. * @typeParam T - The operation's type
  29. * @typeParam TResult - The type of the operation's result, calculated from T
  30. *
  31. * @param client - The MongoClient to execute this operation with
  32. * @param operation - The operation to execute
  33. */
  34. async function executeOperation(client, operation, timeoutContext) {
  35. if (!(operation instanceof operation_1.AbstractOperation)) {
  36. // TODO(NODE-3483): Extend MongoRuntimeError
  37. throw new error_1.MongoRuntimeError('This method requires a valid operation instance');
  38. }
  39. const topology = client.topology == null
  40. ? await (0, utils_1.abortable)(autoConnect(client), operation.options)
  41. : client.topology;
  42. // The driver sessions spec mandates that we implicitly create sessions for operations
  43. // that are not explicitly provided with a session.
  44. let session = operation.session;
  45. let owner;
  46. if (session == null) {
  47. owner = Symbol();
  48. session = client.startSession({ owner, explicit: false });
  49. }
  50. else if (session.hasEnded) {
  51. throw new error_1.MongoExpiredSessionError('Use of expired sessions is not permitted');
  52. }
  53. else if (session.snapshotEnabled && !topology.capabilities.supportsSnapshotReads) {
  54. throw new error_1.MongoCompatibilityError('Snapshot reads require MongoDB 5.0 or later');
  55. }
  56. else if (session.client !== client) {
  57. throw new error_1.MongoInvalidArgumentError('ClientSession must be from the same MongoClient');
  58. }
  59. operation.session ??= session;
  60. const readPreference = operation.readPreference ?? read_preference_1.ReadPreference.primary;
  61. const inTransaction = !!session?.inTransaction();
  62. const hasReadAspect = operation.hasAspect(operation_1.Aspect.READ_OPERATION);
  63. if (inTransaction &&
  64. !readPreference.equals(read_preference_1.ReadPreference.primary) &&
  65. (hasReadAspect || operation.commandName === 'runCommand')) {
  66. throw new error_1.MongoTransactionError(`Read preference in a transaction must be primary, not: ${readPreference.mode}`);
  67. }
  68. if (session?.isPinned && session.transaction.isCommitted && !operation.bypassPinningCheck) {
  69. session.unpin();
  70. }
  71. timeoutContext ??= timeout_1.TimeoutContext.create({
  72. session,
  73. serverSelectionTimeoutMS: client.s.options.serverSelectionTimeoutMS,
  74. waitQueueTimeoutMS: client.s.options.waitQueueTimeoutMS,
  75. timeoutMS: operation.options.timeoutMS
  76. });
  77. try {
  78. return await tryOperation(operation, {
  79. topology,
  80. timeoutContext,
  81. session,
  82. readPreference
  83. });
  84. }
  85. finally {
  86. if (session?.owner != null && session.owner === owner) {
  87. await session.endSession();
  88. }
  89. }
  90. }
  91. /**
  92. * Connects a client if it has not yet been connected
  93. * @internal
  94. */
  95. async function autoConnect(client) {
  96. if (client.topology == null) {
  97. if (client.s.hasBeenClosed) {
  98. throw new error_1.MongoNotConnectedError('Client must be connected before running operations');
  99. }
  100. client.s.options.__skipPingOnConnect = true;
  101. try {
  102. await client.connect();
  103. if (client.topology == null) {
  104. throw new error_1.MongoRuntimeError('client.connect did not create a topology but also did not throw');
  105. }
  106. return client.topology;
  107. }
  108. finally {
  109. delete client.s.options.__skipPingOnConnect;
  110. }
  111. }
  112. return client.topology;
  113. }
  114. /**
  115. * Executes an operation and retries as appropriate
  116. * @internal
  117. *
  118. * @remarks
  119. * Implements behaviour described in [Retryable Reads](https://github.com/mongodb/specifications/blob/master/source/retryable-reads/retryable-reads.md) and [Retryable
  120. * Writes](https://github.com/mongodb/specifications/blob/master/source/retryable-writes/retryable-writes.md) specification
  121. *
  122. * This function:
  123. * - performs initial server selection
  124. * - attempts to execute an operation
  125. * - retries the operation if it meets the criteria for a retryable read or a retryable write
  126. *
  127. * @typeParam T - The operation's type
  128. * @typeParam TResult - The type of the operation's result, calculated from T
  129. *
  130. * @param operation - The operation to execute
  131. * */
  132. async function tryOperation(operation, { topology, timeoutContext, session, readPreference }) {
  133. let selector;
  134. if (operation.hasAspect(operation_1.Aspect.MUST_SELECT_SAME_SERVER)) {
  135. // GetMore and KillCursor operations must always select the same server, but run through
  136. // server selection to potentially force monitor checks if the server is
  137. // in an unknown state.
  138. selector = (0, server_selection_1.sameServerSelector)(operation.server?.description);
  139. }
  140. else if (operation instanceof aggregate_1.AggregateOperation && operation.hasWriteStage) {
  141. // If operation should try to write to secondary use the custom server selector
  142. // otherwise provide the read preference.
  143. selector = (0, server_selection_1.secondaryWritableServerSelector)(topology.commonWireVersion, readPreference);
  144. }
  145. else {
  146. selector = readPreference;
  147. }
  148. let server = await topology.selectServer(selector, {
  149. session,
  150. operationName: operation.commandName,
  151. timeoutContext,
  152. signal: operation.options.signal
  153. });
  154. const hasReadAspect = operation.hasAspect(operation_1.Aspect.READ_OPERATION);
  155. const hasWriteAspect = operation.hasAspect(operation_1.Aspect.WRITE_OPERATION);
  156. const inTransaction = session?.inTransaction() ?? false;
  157. const willRetryRead = topology.s.options.retryReads && !inTransaction && operation.canRetryRead;
  158. const willRetryWrite = topology.s.options.retryWrites &&
  159. !inTransaction &&
  160. (0, utils_1.supportsRetryableWrites)(server) &&
  161. operation.canRetryWrite;
  162. const willRetry = operation.hasAspect(operation_1.Aspect.RETRYABLE) &&
  163. session != null &&
  164. ((hasReadAspect && willRetryRead) || (hasWriteAspect && willRetryWrite));
  165. if (hasWriteAspect && willRetryWrite && session != null) {
  166. operation.options.willRetryWrite = true;
  167. session.incrementTransactionNumber();
  168. }
  169. const maxTries = willRetry ? (timeoutContext.csotEnabled() ? Infinity : 2) : 1;
  170. let previousOperationError;
  171. let previousServer;
  172. for (let tries = 0; tries < maxTries; tries++) {
  173. if (previousOperationError) {
  174. if (hasWriteAspect && previousOperationError.code === MMAPv1_RETRY_WRITES_ERROR_CODE) {
  175. throw new error_1.MongoServerError({
  176. message: MMAPv1_RETRY_WRITES_ERROR_MESSAGE,
  177. errmsg: MMAPv1_RETRY_WRITES_ERROR_MESSAGE,
  178. originalError: previousOperationError
  179. });
  180. }
  181. if (operation.hasAspect(operation_1.Aspect.COMMAND_BATCHING) && !operation.canRetryWrite) {
  182. throw previousOperationError;
  183. }
  184. if (hasWriteAspect && !(0, error_1.isRetryableWriteError)(previousOperationError))
  185. throw previousOperationError;
  186. if (hasReadAspect && !(0, error_1.isRetryableReadError)(previousOperationError)) {
  187. throw previousOperationError;
  188. }
  189. if (previousOperationError instanceof error_1.MongoNetworkError &&
  190. operation.hasAspect(operation_1.Aspect.CURSOR_CREATING) &&
  191. session != null &&
  192. session.isPinned &&
  193. !session.inTransaction()) {
  194. session.unpin({ force: true, forceClear: true });
  195. }
  196. server = await topology.selectServer(selector, {
  197. session,
  198. operationName: operation.commandName,
  199. previousServer,
  200. signal: operation.options.signal
  201. });
  202. if (hasWriteAspect && !(0, utils_1.supportsRetryableWrites)(server)) {
  203. throw new error_1.MongoUnexpectedServerResponseError('Selected server does not support retryable writes');
  204. }
  205. }
  206. operation.server = server;
  207. try {
  208. // If tries > 0 and we are command batching we need to reset the batch.
  209. if (tries > 0 && operation.hasAspect(operation_1.Aspect.COMMAND_BATCHING)) {
  210. operation.resetBatch();
  211. }
  212. try {
  213. const result = await server.command(operation, timeoutContext);
  214. return operation.handleOk(result);
  215. }
  216. catch (error) {
  217. return operation.handleError(error);
  218. }
  219. }
  220. catch (operationError) {
  221. if (!(operationError instanceof error_1.MongoError))
  222. throw operationError;
  223. if (previousOperationError != null &&
  224. operationError.hasErrorLabel(error_1.MongoErrorLabel.NoWritesPerformed)) {
  225. throw previousOperationError;
  226. }
  227. previousServer = server.description;
  228. previousOperationError = operationError;
  229. // Reset timeouts
  230. timeoutContext.clear();
  231. }
  232. }
  233. throw (previousOperationError ??
  234. new error_1.MongoRuntimeError('Tried to propagate retryability error, but no error was found.'));
  235. }
  236. //# sourceMappingURL=execute_operation.js.map