index.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. /**
  2. * Copyright (c) 2017-present, Facebook, Inc. All rights reserved.
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. *
  7. *
  8. */
  9. 'use strict';
  10. Object.defineProperty(exports, "__esModule", {
  11. value: true
  12. });
  13. var _mergeStream;
  14. function _load_mergeStream() {
  15. return _mergeStream = _interopRequireDefault(require('merge-stream'));
  16. }
  17. var _os;
  18. function _load_os() {
  19. return _os = _interopRequireDefault(require('os'));
  20. }
  21. var _path;
  22. function _load_path() {
  23. return _path = _interopRequireDefault(require('path'));
  24. }
  25. var _types;
  26. function _load_types() {
  27. return _types = require('./types');
  28. }
  29. var _worker;
  30. function _load_worker() {
  31. return _worker = _interopRequireDefault(require('./worker'));
  32. }
  33. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  34. /* istanbul ignore next */
  35. const emptyMethod = () => {};
  36. /**
  37. * The Jest farm (publicly called "Worker") is a class that allows you to queue
  38. * methods across multiple child processes, in order to parallelize work. This
  39. * is done by providing an absolute path to a module that will be loaded on each
  40. * of the child processes, and bridged to the main process.
  41. *
  42. * Bridged methods are specified by using the "exposedMethods" property of the
  43. * options "object". This is an array of strings, where each of them corresponds
  44. * to the exported name in the loaded module.
  45. *
  46. * You can also control the amount of workers by using the "numWorkers" property
  47. * of the "options" object, and the settings passed to fork the process through
  48. * the "forkOptions" property. The amount of workers defaults to the amount of
  49. * CPUS minus one.
  50. *
  51. * Queueing calls can be done in two ways:
  52. * - Standard method: calls will be redirected to the first available worker,
  53. * so they will get executed as soon as they can.
  54. *
  55. * - Sticky method: if a "computeWorkerKey" method is provided within the
  56. * config, the resulting string of this method will be used as a key.
  57. * Everytime this key is returned, it is guaranteed that your job will be
  58. * processed by the same worker. This is specially useful if your workers are
  59. * caching results.
  60. */
  61. exports.default = class {
  62. constructor(workerPath) {
  63. let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
  64. const numWorkers = options.numWorkers || (_os || _load_os()).default.cpus().length - 1;
  65. const workers = new Array(numWorkers);
  66. const stdout = (0, (_mergeStream || _load_mergeStream()).default)();
  67. const stderr = (0, (_mergeStream || _load_mergeStream()).default)();
  68. if (!(_path || _load_path()).default.isAbsolute(workerPath)) {
  69. workerPath = require.resolve(workerPath);
  70. }
  71. const sharedWorkerOptions = {
  72. forkOptions: options.forkOptions || {},
  73. maxRetries: options.maxRetries || 3,
  74. workerPath
  75. };
  76. for (let i = 0; i < numWorkers; i++) {
  77. const workerOptions = Object.assign({}, sharedWorkerOptions, {
  78. workerId: i + 1
  79. });
  80. const worker = new (_worker || _load_worker()).default(workerOptions);
  81. const workerStdout = worker.getStdout();
  82. const workerStderr = worker.getStderr();
  83. if (workerStdout) {
  84. stdout.add(workerStdout);
  85. }
  86. if (workerStderr) {
  87. stderr.add(workerStderr);
  88. }
  89. workers[i] = worker;
  90. }
  91. let exposedMethods = options.exposedMethods;
  92. // If no methods list is given, try getting it by auto-requiring the module.
  93. if (!exposedMethods) {
  94. // $FlowFixMe: This has to be a dynamic require.
  95. const child = require(workerPath);
  96. exposedMethods = Object.keys(child).filter(name => typeof child[name] === 'function');
  97. if (typeof child === 'function') {
  98. exposedMethods.push('default');
  99. }
  100. }
  101. exposedMethods.forEach(name => {
  102. if (name.startsWith('_')) {
  103. return;
  104. }
  105. if (this.constructor.prototype.hasOwnProperty(name)) {
  106. throw new TypeError('Cannot define a method called ' + name);
  107. }
  108. // $FlowFixMe: dynamic extension of the class instance is expected.
  109. this[name] = this._makeCall.bind(this, name);
  110. });
  111. this._stdout = stdout;
  112. this._stderr = stderr;
  113. this._ending = false;
  114. this._cacheKeys = Object.create(null);
  115. this._options = options;
  116. this._workers = workers;
  117. this._offset = 0;
  118. }
  119. getStdout() {
  120. return this._stdout;
  121. }
  122. getStderr() {
  123. return this._stderr;
  124. }
  125. end() {
  126. if (this._ending) {
  127. throw new Error('Farm is ended, no more calls can be done to it');
  128. }
  129. const workers = this._workers;
  130. // We do not cache the request object here. If so, it would only be only
  131. // processed by one of the workers, and we want them all to close.
  132. for (let i = 0; i < workers.length; i++) {
  133. workers[i].send([(_types || _load_types()).CHILD_MESSAGE_END, false], emptyMethod);
  134. }
  135. this._ending = true;
  136. }
  137. // eslint-disable-next-line no-unclear-flowtypes
  138. _makeCall(method) {
  139. for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
  140. args[_key - 1] = arguments[_key];
  141. }
  142. if (this._ending) {
  143. throw new Error('Farm is ended, no more calls can be done to it');
  144. }
  145. return new Promise((resolve, reject) => {
  146. const computeWorkerKey = this._options.computeWorkerKey;
  147. const workers = this._workers;
  148. const length = workers.length;
  149. const cacheKeys = this._cacheKeys;
  150. const request = [(_types || _load_types()).CHILD_MESSAGE_CALL, false, method, args];
  151. let worker = null;
  152. let hash = null;
  153. if (computeWorkerKey) {
  154. hash = computeWorkerKey.apply(this, [method].concat(args));
  155. worker = hash == null ? null : cacheKeys[hash];
  156. }
  157. // Do not use a fat arrow since we need the "this" value, which points to
  158. // the worker that executed the call.
  159. function callback(error, result) {
  160. if (hash != null) {
  161. cacheKeys[hash] = this;
  162. }
  163. if (error) {
  164. reject(error);
  165. } else {
  166. resolve(result);
  167. }
  168. }
  169. // If a worker is pre-selected, use it...
  170. if (worker) {
  171. worker.send(request, callback);
  172. return;
  173. }
  174. // ... otherwise use all workers, so the first one available will pick it.
  175. for (let i = 0; i < length; i++) {
  176. workers[(i + this._offset) % length].send(request, callback);
  177. }
  178. this._offset++;
  179. });
  180. }
  181. };