timeout.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.LegacyTimeoutContext = exports.CSOTTimeoutContext = exports.TimeoutContext = exports.Timeout = exports.TimeoutError = void 0;
  4. const timers_1 = require("timers");
  5. const error_1 = require("./error");
  6. const utils_1 = require("./utils");
  7. /** @internal */
  8. class TimeoutError extends Error {
  9. get name() {
  10. return 'TimeoutError';
  11. }
  12. constructor(message, options) {
  13. super(message, options);
  14. this.duration = options.duration;
  15. }
  16. static is(error) {
  17. return (error != null && typeof error === 'object' && 'name' in error && error.name === 'TimeoutError');
  18. }
  19. }
  20. exports.TimeoutError = TimeoutError;
  21. /**
  22. * @internal
  23. * This class is an abstraction over timeouts
  24. * The Timeout class can only be in the pending or rejected states. It is guaranteed not to resolve
  25. * if interacted with exclusively through its public API
  26. * */
  27. class Timeout extends Promise {
  28. get remainingTime() {
  29. if (this.timedOut)
  30. return 0;
  31. if (this.duration === 0)
  32. return Infinity;
  33. return this.start + this.duration - Math.trunc(performance.now());
  34. }
  35. get timeElapsed() {
  36. return Math.trunc(performance.now()) - this.start;
  37. }
  38. /** Create a new timeout that expires in `duration` ms */
  39. constructor(executor = () => null, options) {
  40. const duration = options?.duration ?? 0;
  41. const unref = !!options?.unref;
  42. const rejection = options?.rejection;
  43. if (duration < 0) {
  44. throw new error_1.MongoInvalidArgumentError('Cannot create a Timeout with a negative duration');
  45. }
  46. let reject;
  47. super((_, promiseReject) => {
  48. reject = promiseReject;
  49. executor(utils_1.noop, promiseReject);
  50. });
  51. this.ended = null;
  52. this.timedOut = false;
  53. this.cleared = false;
  54. this.duration = duration;
  55. this.start = Math.trunc(performance.now());
  56. if (rejection == null && this.duration > 0) {
  57. this.id = (0, timers_1.setTimeout)(() => {
  58. this.ended = Math.trunc(performance.now());
  59. this.timedOut = true;
  60. reject(new TimeoutError(`Expired after ${duration}ms`, { duration }));
  61. }, this.duration);
  62. if (typeof this.id.unref === 'function' && unref) {
  63. // Ensure we do not keep the Node.js event loop running
  64. this.id.unref();
  65. }
  66. }
  67. else if (rejection != null) {
  68. this.ended = Math.trunc(performance.now());
  69. this.timedOut = true;
  70. reject(rejection);
  71. }
  72. }
  73. /**
  74. * Clears the underlying timeout. This method is idempotent
  75. */
  76. clear() {
  77. (0, timers_1.clearTimeout)(this.id);
  78. this.id = undefined;
  79. this.timedOut = false;
  80. this.cleared = true;
  81. }
  82. throwIfExpired() {
  83. if (this.timedOut) {
  84. // This method is invoked when someone wants to throw immediately instead of await the result of this promise
  85. // Since they won't be handling the rejection from the promise (because we're about to throw here)
  86. // attach handling to prevent this from bubbling up to Node.js
  87. this.then(undefined, utils_1.squashError);
  88. throw new TimeoutError('Timed out', { duration: this.duration });
  89. }
  90. }
  91. static expires(duration, unref) {
  92. return new Timeout(undefined, { duration, unref });
  93. }
  94. static reject(rejection) {
  95. return new Timeout(undefined, { duration: 0, unref: true, rejection });
  96. }
  97. }
  98. exports.Timeout = Timeout;
  99. function isLegacyTimeoutContextOptions(v) {
  100. return (v != null &&
  101. typeof v === 'object' &&
  102. 'serverSelectionTimeoutMS' in v &&
  103. typeof v.serverSelectionTimeoutMS === 'number' &&
  104. 'waitQueueTimeoutMS' in v &&
  105. typeof v.waitQueueTimeoutMS === 'number');
  106. }
  107. function isCSOTTimeoutContextOptions(v) {
  108. return (v != null &&
  109. typeof v === 'object' &&
  110. 'serverSelectionTimeoutMS' in v &&
  111. typeof v.serverSelectionTimeoutMS === 'number' &&
  112. 'timeoutMS' in v &&
  113. typeof v.timeoutMS === 'number');
  114. }
  115. /** @internal */
  116. class TimeoutContext {
  117. static create(options) {
  118. if (options.session?.timeoutContext != null)
  119. return options.session?.timeoutContext;
  120. if (isCSOTTimeoutContextOptions(options))
  121. return new CSOTTimeoutContext(options);
  122. else if (isLegacyTimeoutContextOptions(options))
  123. return new LegacyTimeoutContext(options);
  124. else
  125. throw new error_1.MongoRuntimeError('Unrecognized options');
  126. }
  127. }
  128. exports.TimeoutContext = TimeoutContext;
  129. /** @internal */
  130. class CSOTTimeoutContext extends TimeoutContext {
  131. constructor(options) {
  132. super();
  133. this.minRoundTripTime = 0;
  134. this.start = Math.trunc(performance.now());
  135. this.timeoutMS = options.timeoutMS;
  136. this.serverSelectionTimeoutMS = options.serverSelectionTimeoutMS;
  137. this.socketTimeoutMS = options.socketTimeoutMS;
  138. this.clearServerSelectionTimeout = false;
  139. }
  140. get maxTimeMS() {
  141. return this.remainingTimeMS - this.minRoundTripTime;
  142. }
  143. get remainingTimeMS() {
  144. const timePassed = Math.trunc(performance.now()) - this.start;
  145. return this.timeoutMS <= 0 ? Infinity : this.timeoutMS - timePassed;
  146. }
  147. csotEnabled() {
  148. return true;
  149. }
  150. get serverSelectionTimeout() {
  151. // check for undefined
  152. if (typeof this._serverSelectionTimeout !== 'object' || this._serverSelectionTimeout?.cleared) {
  153. const { remainingTimeMS, serverSelectionTimeoutMS } = this;
  154. if (remainingTimeMS <= 0)
  155. return Timeout.reject(new error_1.MongoOperationTimeoutError(`Timed out in server selection after ${this.timeoutMS}ms`));
  156. const usingServerSelectionTimeoutMS = serverSelectionTimeoutMS !== 0 &&
  157. (0, utils_1.csotMin)(remainingTimeMS, serverSelectionTimeoutMS) === serverSelectionTimeoutMS;
  158. if (usingServerSelectionTimeoutMS) {
  159. this._serverSelectionTimeout = Timeout.expires(serverSelectionTimeoutMS);
  160. }
  161. else {
  162. if (remainingTimeMS > 0 && Number.isFinite(remainingTimeMS)) {
  163. this._serverSelectionTimeout = Timeout.expires(remainingTimeMS);
  164. }
  165. else {
  166. this._serverSelectionTimeout = null;
  167. }
  168. }
  169. }
  170. return this._serverSelectionTimeout;
  171. }
  172. get connectionCheckoutTimeout() {
  173. if (typeof this._connectionCheckoutTimeout !== 'object' ||
  174. this._connectionCheckoutTimeout?.cleared) {
  175. if (typeof this._serverSelectionTimeout === 'object') {
  176. // null or Timeout
  177. this._connectionCheckoutTimeout = this._serverSelectionTimeout;
  178. }
  179. else {
  180. throw new error_1.MongoRuntimeError('Unreachable. If you are seeing this error, please file a ticket on the NODE driver project on Jira');
  181. }
  182. }
  183. return this._connectionCheckoutTimeout;
  184. }
  185. get timeoutForSocketWrite() {
  186. const { remainingTimeMS } = this;
  187. if (!Number.isFinite(remainingTimeMS))
  188. return null;
  189. if (remainingTimeMS > 0)
  190. return Timeout.expires(remainingTimeMS);
  191. return Timeout.reject(new error_1.MongoOperationTimeoutError('Timed out before socket write'));
  192. }
  193. get timeoutForSocketRead() {
  194. const { remainingTimeMS } = this;
  195. if (!Number.isFinite(remainingTimeMS))
  196. return null;
  197. if (remainingTimeMS > 0)
  198. return Timeout.expires(remainingTimeMS);
  199. return Timeout.reject(new error_1.MongoOperationTimeoutError('Timed out before socket read'));
  200. }
  201. refresh() {
  202. this.start = Math.trunc(performance.now());
  203. this.minRoundTripTime = 0;
  204. this._serverSelectionTimeout?.clear();
  205. this._connectionCheckoutTimeout?.clear();
  206. }
  207. clear() {
  208. this._serverSelectionTimeout?.clear();
  209. this._connectionCheckoutTimeout?.clear();
  210. }
  211. /**
  212. * @internal
  213. * Throws a MongoOperationTimeoutError if the context has expired.
  214. * If the context has not expired, returns the `remainingTimeMS`
  215. **/
  216. getRemainingTimeMSOrThrow(message) {
  217. const { remainingTimeMS } = this;
  218. if (remainingTimeMS <= 0)
  219. throw new error_1.MongoOperationTimeoutError(message ?? `Expired after ${this.timeoutMS}ms`);
  220. return remainingTimeMS;
  221. }
  222. /**
  223. * @internal
  224. * This method is intended to be used in situations where concurrent operation are on the same deadline, but cannot share a single `TimeoutContext` instance.
  225. * Returns a new instance of `CSOTTimeoutContext` constructed with identical options, but setting the `start` property to `this.start`.
  226. */
  227. clone() {
  228. const timeoutContext = new CSOTTimeoutContext({
  229. timeoutMS: this.timeoutMS,
  230. serverSelectionTimeoutMS: this.serverSelectionTimeoutMS
  231. });
  232. timeoutContext.start = this.start;
  233. return timeoutContext;
  234. }
  235. refreshed() {
  236. return new CSOTTimeoutContext(this);
  237. }
  238. addMaxTimeMSToCommand(command, options) {
  239. if (options.omitMaxTimeMS)
  240. return;
  241. const maxTimeMS = this.remainingTimeMS - this.minRoundTripTime;
  242. if (maxTimeMS > 0 && Number.isFinite(maxTimeMS))
  243. command.maxTimeMS = maxTimeMS;
  244. }
  245. getSocketTimeoutMS() {
  246. return 0;
  247. }
  248. }
  249. exports.CSOTTimeoutContext = CSOTTimeoutContext;
  250. /** @internal */
  251. class LegacyTimeoutContext extends TimeoutContext {
  252. constructor(options) {
  253. super();
  254. this.options = options;
  255. this.clearServerSelectionTimeout = true;
  256. }
  257. csotEnabled() {
  258. return false;
  259. }
  260. get serverSelectionTimeout() {
  261. if (this.options.serverSelectionTimeoutMS != null && this.options.serverSelectionTimeoutMS > 0)
  262. return Timeout.expires(this.options.serverSelectionTimeoutMS);
  263. return null;
  264. }
  265. get connectionCheckoutTimeout() {
  266. if (this.options.waitQueueTimeoutMS != null && this.options.waitQueueTimeoutMS > 0)
  267. return Timeout.expires(this.options.waitQueueTimeoutMS);
  268. return null;
  269. }
  270. get timeoutForSocketWrite() {
  271. return null;
  272. }
  273. get timeoutForSocketRead() {
  274. return null;
  275. }
  276. refresh() {
  277. return;
  278. }
  279. clear() {
  280. return;
  281. }
  282. get maxTimeMS() {
  283. return null;
  284. }
  285. refreshed() {
  286. return new LegacyTimeoutContext(this.options);
  287. }
  288. addMaxTimeMSToCommand(_command, _options) {
  289. // No max timeMS is added to commands in legacy timeout mode.
  290. }
  291. getSocketTimeoutMS() {
  292. return this.options.socketTimeoutMS;
  293. }
  294. }
  295. exports.LegacyTimeoutContext = LegacyTimeoutContext;
  296. //# sourceMappingURL=timeout.js.map