@atlaspack/core 2.13.2-canary.3657 → 2.13.2-canary.3658

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.
@@ -11,7 +11,7 @@ function _rust() {
11
11
  };
12
12
  return data;
13
13
  }
14
- var _WorkerPool = require("./WorkerPool");
14
+ var _NapiWorkerPool = require("./NapiWorkerPool");
15
15
  function _diagnostic() {
16
16
  const data = _interopRequireDefault(require("@atlaspack/diagnostic"));
17
17
  _diagnostic = function () {
@@ -23,39 +23,26 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
23
23
  class AtlaspackV3 {
24
24
  constructor({
25
25
  fs,
26
- nodeWorkers,
27
26
  packageManager,
28
27
  threads,
29
28
  lmdb,
29
+ napiWorkerPool = new _NapiWorkerPool.NapiWorkerPool(),
30
30
  ...options
31
31
  }) {
32
32
  options.logLevel = options.logLevel || 'error';
33
33
  options.defaultTargetOptions = options.defaultTargetOptions || {};
34
34
  // $FlowFixMe "engines" are readonly
35
35
  options.defaultTargetOptions.engines = options.defaultTargetOptions.engines || {};
36
- this._workerIds = [];
37
36
  this._internal = _rust().AtlaspackNapi.create({
38
37
  fs,
39
- nodeWorkers,
40
38
  packageManager,
41
39
  threads,
42
- options
40
+ options,
41
+ napiWorkerPool
43
42
  }, lmdb);
44
43
  }
45
44
  async buildAssetGraph() {
46
- const workerIds = [];
47
- let [graph, error] = await this._internal.buildAssetGraph({
48
- registerWorker: tx_worker => {
49
- // $FlowFixMe
50
- const workerId = _WorkerPool.workerPool.registerWorker(tx_worker);
51
- workerIds.push(workerId);
52
- }
53
- });
54
-
55
- // In the integration tests we keep the workers alive so they don't need to
56
- // be re-initialized for the next test
57
-
58
- _WorkerPool.workerPool.shutdown();
45
+ let [graph, error] = await this._internal.buildAssetGraph();
59
46
  if (error !== null) {
60
47
  throw new (_diagnostic().default)({
61
48
  diagnostic: error
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.NapiWorkerPool = void 0;
7
+ function _worker_threads() {
8
+ const data = require("worker_threads");
9
+ _worker_threads = function () {
10
+ return data;
11
+ };
12
+ return data;
13
+ }
14
+ function _path() {
15
+ const data = _interopRequireDefault(require("path"));
16
+ _path = function () {
17
+ return data;
18
+ };
19
+ return data;
20
+ }
21
+ function _process() {
22
+ const data = _interopRequireDefault(require("process"));
23
+ _process = function () {
24
+ return data;
25
+ };
26
+ return data;
27
+ }
28
+ function _rust() {
29
+ const data = require("@atlaspack/rust");
30
+ _rust = function () {
31
+ return data;
32
+ };
33
+ return data;
34
+ }
35
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
36
+ const WORKER_PATH = _path().default.join(__dirname, 'worker', 'index.js');
37
+ const ATLASPACK_NAPI_WORKERS = _process().default.env.ATLASPACK_NAPI_WORKERS && parseInt(_process().default.env.ATLASPACK_NAPI_WORKERS, 10);
38
+ class NapiWorkerPool {
39
+ #workers;
40
+ #napiWorkers;
41
+ #workerCount;
42
+ constructor({
43
+ workerCount
44
+ } = {
45
+ workerCount: undefined
46
+ }) {
47
+ this.#workerCount = workerCount ?? ATLASPACK_NAPI_WORKERS ?? (0, _rust().getAvailableThreads)();
48
+ if (!this.#workerCount) {
49
+ // TODO use main thread if workerCount is 0
50
+ }
51
+ this.#workers = [];
52
+ this.#napiWorkers = [];
53
+ for (let i = 0; i < this.#workerCount; i++) {
54
+ let worker = new (_worker_threads().Worker)(WORKER_PATH);
55
+ this.#workers.push(worker);
56
+ this.#napiWorkers.push(new Promise(res => worker.once('message', res)));
57
+ }
58
+ }
59
+ workerCount() {
60
+ return this.#workerCount;
61
+ }
62
+ getWorkers() {
63
+ return Promise.all(this.#napiWorkers);
64
+ }
65
+ shutdown() {
66
+ for (const worker of this.#workers) {
67
+ worker.terminate();
68
+ }
69
+ }
70
+ }
71
+ exports.NapiWorkerPool = NapiWorkerPool;
@@ -15,5 +15,12 @@ Object.defineProperty(exports, "FileSystemV3", {
15
15
  return _fs.FileSystemV3;
16
16
  }
17
17
  });
18
+ Object.defineProperty(exports, "NapiWorkerPool", {
19
+ enumerable: true,
20
+ get: function () {
21
+ return _NapiWorkerPool.NapiWorkerPool;
22
+ }
23
+ });
18
24
  var _fs = require("./fs");
19
- var _AtlaspackV = require("./AtlaspackV3");
25
+ var _AtlaspackV = require("./AtlaspackV3");
26
+ var _NapiWorkerPool = require("./NapiWorkerPool");
@@ -272,30 +272,9 @@ class AtlaspackWorker {
272
272
  JSON.stringify((await mutableAsset.getMap()).toVLQ()) : ''];
273
273
  });
274
274
  }
275
+
276
+ // Create napi worker and send it back to main thread
275
277
  exports.AtlaspackWorker = AtlaspackWorker;
276
278
  const worker = new AtlaspackWorker();
277
- _worker_threads().parentPort === null || _worker_threads().parentPort === void 0 || _worker_threads().parentPort.on('message', event => {
278
- if (event.type === 'registerWorker') {
279
- try {
280
- napi().registerWorker(event.tx_worker, worker);
281
- } catch (err) {
282
- // eslint-disable-next-line no-console
283
- console.error('Registering worker failed... This might mean atlaspack is getting shut-down before the worker registered', err);
284
- _worker_threads().parentPort === null || _worker_threads().parentPort === void 0 || _worker_threads().parentPort.postMessage({
285
- type: 'workerError',
286
- error: err
287
- });
288
- }
289
- _worker_threads().parentPort === null || _worker_threads().parentPort === void 0 || _worker_threads().parentPort.postMessage({
290
- type: 'workerRegistered'
291
- });
292
- } else if (event.type === 'probeStatus') {
293
- _worker_threads().parentPort.postMessage({
294
- type: 'status',
295
- status: 'ok'
296
- });
297
- }
298
- });
299
- _worker_threads().parentPort === null || _worker_threads().parentPort === void 0 || _worker_threads().parentPort.postMessage({
300
- type: 'workerLoaded'
301
- });
279
+ const napiWorker = napi().newNodejsWorker(worker);
280
+ _worker_threads().parentPort === null || _worker_threads().parentPort === void 0 || _worker_threads().parentPort.postMessage(napiWorker);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaspack/core",
3
- "version": "2.13.2-canary.3657+93732232c",
3
+ "version": "2.13.2-canary.3658+4812d0f74",
4
4
  "license": "(MIT OR Apache-2.0)",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -20,21 +20,21 @@
20
20
  "check-ts": "tsc --noEmit index.d.ts"
21
21
  },
22
22
  "dependencies": {
23
- "@atlaspack/build-cache": "2.13.2-canary.3657+93732232c",
24
- "@atlaspack/cache": "2.13.2-canary.3657+93732232c",
25
- "@atlaspack/diagnostic": "2.13.2-canary.3657+93732232c",
26
- "@atlaspack/events": "2.13.2-canary.3657+93732232c",
27
- "@atlaspack/feature-flags": "2.13.2-canary.3657+93732232c",
28
- "@atlaspack/fs": "2.13.2-canary.3657+93732232c",
29
- "@atlaspack/graph": "3.3.2-canary.3657+93732232c",
30
- "@atlaspack/logger": "2.13.2-canary.3657+93732232c",
31
- "@atlaspack/package-manager": "2.13.2-canary.3657+93732232c",
32
- "@atlaspack/plugin": "2.13.2-canary.3657+93732232c",
33
- "@atlaspack/profiler": "2.13.2-canary.3657+93732232c",
34
- "@atlaspack/rust": "2.13.2-canary.3657+93732232c",
35
- "@atlaspack/types": "2.13.2-canary.3657+93732232c",
36
- "@atlaspack/utils": "2.13.2-canary.3657+93732232c",
37
- "@atlaspack/workers": "2.13.2-canary.3657+93732232c",
23
+ "@atlaspack/build-cache": "2.13.2-canary.3658+4812d0f74",
24
+ "@atlaspack/cache": "2.13.2-canary.3658+4812d0f74",
25
+ "@atlaspack/diagnostic": "2.13.2-canary.3658+4812d0f74",
26
+ "@atlaspack/events": "2.13.2-canary.3658+4812d0f74",
27
+ "@atlaspack/feature-flags": "2.13.2-canary.3658+4812d0f74",
28
+ "@atlaspack/fs": "2.13.2-canary.3658+4812d0f74",
29
+ "@atlaspack/graph": "3.3.2-canary.3658+4812d0f74",
30
+ "@atlaspack/logger": "2.13.2-canary.3658+4812d0f74",
31
+ "@atlaspack/package-manager": "2.13.2-canary.3658+4812d0f74",
32
+ "@atlaspack/plugin": "2.13.2-canary.3658+4812d0f74",
33
+ "@atlaspack/profiler": "2.13.2-canary.3658+4812d0f74",
34
+ "@atlaspack/rust": "2.13.2-canary.3658+4812d0f74",
35
+ "@atlaspack/types": "2.13.2-canary.3658+4812d0f74",
36
+ "@atlaspack/utils": "2.13.2-canary.3658+4812d0f74",
37
+ "@atlaspack/workers": "2.13.2-canary.3658+4812d0f74",
38
38
  "@mischnic/json-sourcemap": "^0.1.0",
39
39
  "@parcel/source-map": "^2.1.1",
40
40
  "base-x": "^3.0.8",
@@ -66,5 +66,5 @@
66
66
  "browser": {
67
67
  "./src/serializerCore.js": "./src/serializerCore.browser.js"
68
68
  },
69
- "gitHead": "93732232c38c42c9357356bdfd8325cd0bc2536d"
69
+ "gitHead": "4812d0f7400af0f8416f1b7175ecb87700860a68"
70
70
  }
@@ -5,13 +5,13 @@ import {
5
5
  type Lmdb,
6
6
  type AtlaspackNapiOptions,
7
7
  } from '@atlaspack/rust';
8
- import {workerPool} from './WorkerPool';
8
+ import {NapiWorkerPool} from './NapiWorkerPool';
9
9
  import ThrowableDiagnostic from '@atlaspack/diagnostic';
10
10
  import type {Event} from '@parcel/watcher';
11
+ import type {NapiWorkerPool as INapiWorkerPool} from '@atlaspack/types';
11
12
 
12
13
  export type AtlaspackV3Options = {|
13
14
  fs?: AtlaspackNapiOptions['fs'],
14
- nodeWorkers?: number,
15
15
  packageManager?: AtlaspackNapiOptions['packageManager'],
16
16
  threads?: number,
17
17
  /**
@@ -19,6 +19,7 @@ export type AtlaspackV3Options = {|
19
19
  */
20
20
  lmdb: Lmdb,
21
21
  featureFlags?: {[string]: string | boolean},
22
+ napiWorkerPool?: INapiWorkerPool,
22
23
  ...AtlaspackNapiOptions['options'],
23
24
  |};
24
25
 
@@ -28,10 +29,10 @@ export class AtlaspackV3 {
28
29
 
29
30
  constructor({
30
31
  fs,
31
- nodeWorkers,
32
32
  packageManager,
33
33
  threads,
34
34
  lmdb,
35
+ napiWorkerPool = new NapiWorkerPool(),
35
36
  ...options
36
37
  }: AtlaspackV3Options) {
37
38
  options.logLevel = options.logLevel || 'error';
@@ -40,37 +41,20 @@ export class AtlaspackV3 {
40
41
  options.defaultTargetOptions.engines =
41
42
  options.defaultTargetOptions.engines || {};
42
43
 
43
- this._workerIds = [];
44
44
  this._internal = AtlaspackNapi.create(
45
45
  {
46
46
  fs,
47
- nodeWorkers,
48
47
  packageManager,
49
48
  threads,
50
49
  options,
50
+ napiWorkerPool,
51
51
  },
52
52
  lmdb,
53
53
  );
54
54
  }
55
55
 
56
56
  async buildAssetGraph(): Promise<any> {
57
- const workerIds = [];
58
-
59
- let [graph, error] = await this._internal.buildAssetGraph({
60
- registerWorker: (tx_worker) => {
61
- // $FlowFixMe
62
- const workerId = workerPool.registerWorker(tx_worker);
63
- workerIds.push(workerId);
64
- },
65
- });
66
-
67
- // In the integration tests we keep the workers alive so they don't need to
68
- // be re-initialized for the next test
69
- if (process.env.ATLASPACK_BUILD_ENV === 'test') {
70
- workerPool.releaseWorkers(workerIds);
71
- } else {
72
- workerPool.shutdown();
73
- }
57
+ let [graph, error] = await this._internal.buildAssetGraph();
74
58
 
75
59
  if (error !== null) {
76
60
  throw new ThrowableDiagnostic({
@@ -0,0 +1,53 @@
1
+ // @flow
2
+ import type {NapiWorkerPool as INapiWorkerPool} from '@atlaspack/types';
3
+ import {Worker} from 'worker_threads';
4
+ import path from 'path';
5
+ import process from 'process';
6
+ import type {Transferable} from '@atlaspack/rust';
7
+ import {getAvailableThreads} from '@atlaspack/rust';
8
+
9
+ const WORKER_PATH = path.join(__dirname, 'worker', 'index.js');
10
+ const ATLASPACK_NAPI_WORKERS =
11
+ process.env.ATLASPACK_NAPI_WORKERS &&
12
+ parseInt(process.env.ATLASPACK_NAPI_WORKERS, 10);
13
+
14
+ export type NapiWorkerPoolOptions = {|
15
+ workerCount?: number,
16
+ |};
17
+
18
+ export class NapiWorkerPool implements INapiWorkerPool {
19
+ #workers: Worker[];
20
+ #napiWorkers: Array<Promise<Transferable>>;
21
+ #workerCount: number;
22
+
23
+ constructor({workerCount}: NapiWorkerPoolOptions = {workerCount: undefined}) {
24
+ this.#workerCount =
25
+ workerCount ?? ATLASPACK_NAPI_WORKERS ?? getAvailableThreads();
26
+ if (!this.#workerCount) {
27
+ // TODO use main thread if workerCount is 0
28
+ }
29
+
30
+ this.#workers = [];
31
+ this.#napiWorkers = [];
32
+
33
+ for (let i = 0; i < this.#workerCount; i++) {
34
+ let worker = new Worker(WORKER_PATH);
35
+ this.#workers.push(worker);
36
+ this.#napiWorkers.push(new Promise((res) => worker.once('message', res)));
37
+ }
38
+ }
39
+
40
+ workerCount(): number {
41
+ return this.#workerCount;
42
+ }
43
+
44
+ getWorkers(): Promise<Array<Transferable>> {
45
+ return Promise.all(this.#napiWorkers);
46
+ }
47
+
48
+ shutdown(): void {
49
+ for (const worker of this.#workers) {
50
+ worker.terminate();
51
+ }
52
+ }
53
+ }
@@ -2,4 +2,6 @@
2
2
 
3
3
  export {FileSystemV3} from './fs';
4
4
  export {AtlaspackV3} from './AtlaspackV3';
5
+ export {NapiWorkerPool} from './NapiWorkerPool';
5
6
  export type * from './AtlaspackV3';
7
+ export type * from './NapiWorkerPool';
@@ -299,28 +299,10 @@ export class AtlaspackWorker {
299
299
  );
300
300
  }
301
301
 
302
+ // Create napi worker and send it back to main thread
302
303
  const worker = new AtlaspackWorker();
303
- parentPort?.on('message', (event) => {
304
- if (event.type === 'registerWorker') {
305
- try {
306
- napi.registerWorker(event.tx_worker, worker);
307
- } catch (err) {
308
- // eslint-disable-next-line no-console
309
- console.error(
310
- 'Registering worker failed... This might mean atlaspack is getting shut-down before the worker registered',
311
- err,
312
- );
313
- parentPort?.postMessage({type: 'workerError', error: err});
314
- }
315
- parentPort?.postMessage({type: 'workerRegistered'});
316
- } else if (event.type === 'probeStatus') {
317
- parentPort.postMessage({
318
- type: 'status',
319
- status: 'ok',
320
- });
321
- }
322
- });
323
- parentPort?.postMessage({type: 'workerLoaded'});
304
+ const napiWorker = napi.newNodejsWorker(worker);
305
+ parentPort?.postMessage(napiWorker);
324
306
 
325
307
  type ResolverState<T> = {|
326
308
  resolver: Resolver<T>,
@@ -1,171 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.WorkerPool = void 0;
7
- exports.waitForMessage = waitForMessage;
8
- exports.workerPool = void 0;
9
- function _path() {
10
- const data = _interopRequireDefault(require("path"));
11
- _path = function () {
12
- return data;
13
- };
14
- return data;
15
- }
16
- function _promises() {
17
- const data = require("timers/promises");
18
- _promises = function () {
19
- return data;
20
- };
21
- return data;
22
- }
23
- function _worker_threads() {
24
- const data = require("worker_threads");
25
- _worker_threads = function () {
26
- return data;
27
- };
28
- return data;
29
- }
30
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
31
- /* eslint-disable no-console */
32
- /*!
33
- * Atlaspack V3 delegates work to node.js worker threads.
34
- *
35
- * Starting-up each worker is relatively expensive, in particular when atlaspack
36
- * is running in development mode, in which case each worker will transpile the
37
- * project on startup.
38
- *
39
- * This "WorkerPool" mitigates this problem by reusing worker threads across
40
- * builds.
41
- */
42
- // $FlowFixMe Missing types
43
- const WORKER_PATH = _path().default.join(__dirname, 'worker', 'index.js');
44
- function waitForMessage(worker, type, signal) {
45
- return new Promise(resolve => {
46
- const onMessage = message => {
47
- if (message.type === type) {
48
- resolve(message);
49
- worker.off('message', onMessage);
50
- }
51
- };
52
- worker.on('message', onMessage);
53
- const onAbort = () => {
54
- signal.removeEventListener('abort', onAbort);
55
- worker.off('message', onMessage);
56
- };
57
- signal.addEventListener('abort', onAbort);
58
- });
59
- }
60
- class WorkerPool {
61
- #workerPool = [];
62
- #usedWorkers = new Set();
63
- #workerPath;
64
- constructor(workerPath = WORKER_PATH) {
65
- this.#workerPath = workerPath;
66
-
67
- // $FlowFixMe
68
- this[Symbol.dispose] = () => {
69
- this.shutdown();
70
- };
71
- }
72
-
73
- /**
74
- * Find a worker thread that is free to use or create a new one.
75
- *
76
- * Then register the `tx_worker` channel ID with the worker thread.
77
- */
78
- registerWorker(tx_worker) {
79
- const availableIndex = this.#workerPool.findIndex((worker, index) => !this.#usedWorkers.has(index));
80
- const [workerId, worker] = availableIndex !== -1 ? [availableIndex, this.#workerPool[availableIndex]] : this.#createWorker();
81
- this.#bootWorker(worker, tx_worker).catch(err => {
82
- // eslint-disable-next-line no-console
83
- console.error('Worker failed, retrying to create it...', err);
84
- this.#workerPool[workerId] = new (_worker_threads().Worker)(this.#workerPath, {
85
- workerData: {
86
- attempt: 2
87
- }
88
- });
89
- this.#bootWorker(this.#workerPool[workerId], tx_worker).catch(err => {
90
- console.error('Worker failed to start, the build may hang:', err);
91
- });
92
- });
93
- this.#usedWorkers.add(workerId);
94
- return workerId;
95
- }
96
-
97
- /**
98
- * Release a set of workers back into the pool for re-use
99
- */
100
- releaseWorkers(ids) {
101
- for (let id of ids) {
102
- this.#usedWorkers.delete(id);
103
- }
104
- }
105
-
106
- /**
107
- * Terminate all worker threads and reset state.
108
- */
109
- shutdown() {
110
- for (let worker of this.#workerPool) {
111
- worker.terminate();
112
- }
113
- this.#usedWorkers.clear();
114
- this.#workerPool = [];
115
- }
116
- getStats() {
117
- return {
118
- totalWorkers: this.#workerPool.length,
119
- workersInUse: this.#usedWorkers.size
120
- };
121
- }
122
-
123
- /**
124
- * Get the worker thread. Used for testing.
125
- */
126
- getWorker(workerId) {
127
- if (!this.#workerPool[workerId]) {
128
- throw new Error('Worker does not exist');
129
- }
130
- return this.#workerPool[workerId];
131
- }
132
- async #bootWorker(worker, tx_worker) {
133
- const controller = new AbortController();
134
- const signal = controller.signal;
135
- const workerError = new Promise((_, reject) => {
136
- const onError = err => {
137
- reject(err);
138
- };
139
- worker.once('error', onError);
140
- const onAbort = () => {
141
- signal.removeEventListener('abort', onAbort);
142
- worker.off('error', onError);
143
- };
144
- signal.addEventListener('abort', onAbort);
145
- });
146
- const workerReady = waitForMessage(worker, 'workerRegistered', signal);
147
- worker.postMessage({
148
- type: 'registerWorker',
149
- tx_worker
150
- });
151
- try {
152
- await Promise.race([(0, _promises().setTimeout)(5000, {
153
- signal
154
- }), workerError, workerReady]);
155
- } finally {
156
- controller.abort();
157
- }
158
- }
159
- #createWorker() {
160
- const worker = new (_worker_threads().Worker)(this.#workerPath, {
161
- workerData: {
162
- attempt: 1
163
- }
164
- });
165
- const workerId = this.#workerPool.length;
166
- this.#workerPool.push(worker);
167
- return [workerId, worker];
168
- }
169
- }
170
- exports.WorkerPool = WorkerPool;
171
- const workerPool = exports.workerPool = new WorkerPool();
@@ -1,171 +0,0 @@
1
- // @flow strict-local
2
-
3
- /* eslint-disable no-console */
4
-
5
- /*!
6
- * Atlaspack V3 delegates work to node.js worker threads.
7
- *
8
- * Starting-up each worker is relatively expensive, in particular when atlaspack
9
- * is running in development mode, in which case each worker will transpile the
10
- * project on startup.
11
- *
12
- * This "WorkerPool" mitigates this problem by reusing worker threads across
13
- * builds.
14
- */
15
- import path from 'path';
16
- // $FlowFixMe Missing types
17
- import {setTimeout} from 'timers/promises';
18
- import {Worker} from 'worker_threads';
19
-
20
- const WORKER_PATH = path.join(__dirname, 'worker', 'index.js');
21
-
22
- export function waitForMessage<T>(
23
- worker: Worker,
24
- type: string,
25
- signal: AbortSignal,
26
- ): Promise<T> {
27
- return new Promise((resolve) => {
28
- const onMessage = (message: T & {|type: string|}) => {
29
- if (message.type === type) {
30
- resolve(message);
31
- worker.off('message', onMessage);
32
- }
33
- };
34
-
35
- worker.on('message', onMessage);
36
-
37
- const onAbort = () => {
38
- signal.removeEventListener('abort', onAbort);
39
- worker.off('message', onMessage);
40
- };
41
-
42
- signal.addEventListener('abort', onAbort);
43
- });
44
- }
45
-
46
- export class WorkerPool {
47
- #workerPool: Worker[] = [];
48
- #usedWorkers: Set<number> = new Set();
49
- #workerPath: string;
50
-
51
- constructor(workerPath: string = WORKER_PATH) {
52
- this.#workerPath = workerPath;
53
-
54
- // $FlowFixMe
55
- this[Symbol.dispose] = () => {
56
- this.shutdown();
57
- };
58
- }
59
-
60
- /**
61
- * Find a worker thread that is free to use or create a new one.
62
- *
63
- * Then register the `tx_worker` channel ID with the worker thread.
64
- */
65
- registerWorker(tx_worker: number): number {
66
- const availableIndex = this.#workerPool.findIndex(
67
- (worker, index) => !this.#usedWorkers.has(index),
68
- );
69
-
70
- const [workerId, worker] =
71
- availableIndex !== -1
72
- ? [availableIndex, this.#workerPool[availableIndex]]
73
- : this.#createWorker();
74
-
75
- this.#bootWorker(worker, tx_worker).catch((err) => {
76
- // eslint-disable-next-line no-console
77
- console.error('Worker failed, retrying to create it...', err);
78
- this.#workerPool[workerId] = new Worker(this.#workerPath, {
79
- workerData: {attempt: 2},
80
- });
81
-
82
- this.#bootWorker(this.#workerPool[workerId], tx_worker).catch((err) => {
83
- console.error('Worker failed to start, the build may hang:', err);
84
- });
85
- });
86
-
87
- this.#usedWorkers.add(workerId);
88
-
89
- return workerId;
90
- }
91
-
92
- /**
93
- * Release a set of workers back into the pool for re-use
94
- */
95
- releaseWorkers(ids: number[]) {
96
- for (let id of ids) {
97
- this.#usedWorkers.delete(id);
98
- }
99
- }
100
-
101
- /**
102
- * Terminate all worker threads and reset state.
103
- */
104
- shutdown() {
105
- for (let worker of this.#workerPool) {
106
- worker.terminate();
107
- }
108
- this.#usedWorkers.clear();
109
- this.#workerPool = [];
110
- }
111
-
112
- getStats(): {|totalWorkers: number, workersInUse: number|} {
113
- return {
114
- totalWorkers: this.#workerPool.length,
115
- workersInUse: this.#usedWorkers.size,
116
- };
117
- }
118
-
119
- /**
120
- * Get the worker thread. Used for testing.
121
- */
122
- getWorker(workerId: number): Worker {
123
- if (!this.#workerPool[workerId]) {
124
- throw new Error('Worker does not exist');
125
- }
126
- return this.#workerPool[workerId];
127
- }
128
-
129
- async #bootWorker(worker: Worker, tx_worker: number): Promise<void> {
130
- const controller = new AbortController();
131
- const signal = controller.signal;
132
-
133
- const workerError = new Promise((_, reject) => {
134
- const onError = (err: Error) => {
135
- reject(err);
136
- };
137
-
138
- worker.once('error', onError);
139
-
140
- const onAbort = () => {
141
- signal.removeEventListener('abort', onAbort);
142
- worker.off('error', onError);
143
- };
144
-
145
- signal.addEventListener('abort', onAbort);
146
- });
147
-
148
- const workerReady = waitForMessage(worker, 'workerRegistered', signal);
149
-
150
- worker.postMessage({type: 'registerWorker', tx_worker});
151
-
152
- try {
153
- await Promise.race([
154
- setTimeout(5000, {signal}),
155
- workerError,
156
- workerReady,
157
- ]);
158
- } finally {
159
- controller.abort();
160
- }
161
- }
162
-
163
- #createWorker(): [number, Worker] {
164
- const worker = new Worker(this.#workerPath, {workerData: {attempt: 1}});
165
- const workerId = this.#workerPool.length;
166
- this.#workerPool.push(worker);
167
- return [workerId, worker];
168
- }
169
- }
170
-
171
- export const workerPool: WorkerPool = new WorkerPool();
@@ -1,147 +0,0 @@
1
- // @flow strict-local
2
-
3
- import path from 'path';
4
- import {Worker} from 'worker_threads';
5
- import {WorkerPool, waitForMessage} from '../../src/atlaspack-v3/WorkerPool';
6
- import assert from 'assert';
7
-
8
- function probeStatus(worker: Worker) {
9
- const response = waitForMessage(
10
- worker,
11
- 'status',
12
- new AbortController().signal,
13
- );
14
- worker.postMessage({type: 'probeStatus'});
15
- return response;
16
- }
17
-
18
- const flushPromises = () => new Promise((resolve) => setImmediate(resolve));
19
-
20
- describe('WorkerPool', () => {
21
- it('can create workers and will send them the tx_worker value', async () => {
22
- const workerPool = new WorkerPool(path.join(__dirname, 'worker.js'));
23
- const workerId = workerPool.registerWorker(0);
24
- const worker = workerPool.getWorker(workerId);
25
- const status = await probeStatus(worker);
26
-
27
- assert.deepEqual(status, {
28
- type: 'status',
29
- status: 'test-status-ok',
30
- receivedMessages: [
31
- {
32
- type: 'registerWorker',
33
- tx_worker: 0,
34
- },
35
- ],
36
- });
37
- });
38
-
39
- it('recreates a failing worker', async () => {
40
- const workerPool = new WorkerPool(path.join(__dirname, 'worker-error.js'));
41
- const workerId = workerPool.registerWorker(0);
42
- const failingWorker = workerPool.getWorker(workerId);
43
-
44
- await new Promise((resolve) => {
45
- failingWorker.on('error', resolve);
46
- });
47
-
48
- await flushPromises();
49
-
50
- const worker = workerPool.getWorker(workerId);
51
- const status = await probeStatus(worker);
52
-
53
- assert.deepEqual(status, {
54
- type: 'status',
55
- status: 'ok',
56
- });
57
- });
58
-
59
- it('when a worker is created, it is tracked', () => {
60
- const workerPool = new WorkerPool(path.join(__dirname, 'worker.js'));
61
- const w1 = workerPool.registerWorker(0);
62
- const w2 = workerPool.registerWorker(0);
63
- const w3 = workerPool.registerWorker(0);
64
- assert.notEqual(w1, w2);
65
- assert.notEqual(w2, w3);
66
- assert.notEqual(w1, w3);
67
-
68
- assert.deepEqual(workerPool.getStats(), {
69
- totalWorkers: 3,
70
- workersInUse: 3,
71
- });
72
- });
73
-
74
- it('workers can be released and will then be reused', async () => {
75
- const workerPool = new WorkerPool(path.join(__dirname, 'worker.js'));
76
- const worker1 = workerPool.registerWorker(0);
77
- workerPool.registerWorker(5);
78
- assert.deepEqual(workerPool.getStats(), {
79
- totalWorkers: 2,
80
- workersInUse: 2,
81
- });
82
-
83
- // Release the worker
84
- workerPool.releaseWorkers([worker1]);
85
- assert.deepEqual(workerPool.getStats(), {
86
- totalWorkers: 2,
87
- workersInUse: 1,
88
- });
89
-
90
- const worker3 = workerPool.registerWorker(33);
91
- assert.equal(worker1, worker3);
92
-
93
- const worker = workerPool.getWorker(worker3);
94
- const status = await probeStatus(worker);
95
- assert.deepEqual(status, {
96
- type: 'status',
97
- status: 'test-status-ok',
98
- receivedMessages: [
99
- {
100
- type: 'registerWorker',
101
- tx_worker: 0,
102
- },
103
- {
104
- type: 'registerWorker',
105
- tx_worker: 33,
106
- },
107
- ],
108
- });
109
- });
110
-
111
- describe('shutdown', () => {
112
- it('terminates all workers', async () => {
113
- const workerPool = new WorkerPool(path.join(__dirname, 'worker.js'));
114
- const worker1Id = workerPool.registerWorker(0);
115
- const worker2Id = workerPool.registerWorker(0);
116
- const worker1 = workerPool.getWorker(worker1Id);
117
- const worker2 = workerPool.getWorker(worker2Id);
118
-
119
- const worker1Exit = new Promise((resolve) => {
120
- worker1.on('exit', () => {
121
- resolve(null);
122
- });
123
- });
124
- const worker2Exit = new Promise((resolve) => {
125
- worker2.on('exit', () => {
126
- resolve(null);
127
- });
128
- });
129
-
130
- workerPool.shutdown();
131
- assert.throws(() => {
132
- workerPool.getWorker(worker1Id);
133
- });
134
- assert.throws(() => {
135
- workerPool.getWorker(worker2Id);
136
- });
137
-
138
- await worker1Exit;
139
- await worker2Exit;
140
-
141
- assert.deepEqual(workerPool.getStats(), {
142
- totalWorkers: 0,
143
- workersInUse: 0,
144
- });
145
- });
146
- });
147
- });