@travetto/worker 3.4.2 → 4.0.0-rc.1
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/README.md +1 -156
- package/__index__.ts +1 -4
- package/package.json +2 -2
- package/src/comm/channel.ts +6 -7
- package/src/comm/parent.ts +7 -8
- package/src/pool.ts +118 -151
- package/src/{input/async-iterator.ts → queue.ts} +28 -10
- package/src/support/barrier.ts +7 -3
- package/src/support/timeout.ts +2 -1
- package/src/input/iterable.ts +0 -73
- package/src/input/types.ts +0 -17
- package/src/util.ts +0 -35
package/README.md
CHANGED
|
@@ -16,162 +16,7 @@ yarn add @travetto/worker
|
|
|
16
16
|
This module provides the necessary primitives for handling dependent workers. A worker can be an individual actor or could be a pool of workers. Node provides ipc (inter-process communication) functionality out of the box. This module builds upon that by providing enhanced event management, richer process management, as well as constructs for orchestrating a conversation between two processes.
|
|
17
17
|
|
|
18
18
|
## Execution Pools
|
|
19
|
-
With respect to managing multiple executions, [WorkPool](https://github.com/travetto/travetto/tree/main/module/worker/src/pool.ts#
|
|
20
|
-
|
|
21
|
-
The only provided [WorkSet](https://github.com/travetto/travetto/tree/main/module/worker/src/input/types.ts#L4) is the [IterableWorkSet](https://github.com/travetto/travetto/tree/main/module/worker/src/input/iterable.ts#L11) which supports all `Iterable` and `Iterator` sources. Additionally, the module provides [ManualAsyncIterator](https://github.com/travetto/travetto/tree/main/module/worker/src/input/async-iterator.ts#L6) which allows for manual control of iteration, which is useful for event driven work loads.
|
|
22
|
-
|
|
23
|
-
Below is a pool that will convert images on demand, while queuing as needed.
|
|
24
|
-
|
|
25
|
-
**Code: Image processing queue, with a fixed batch/pool size**
|
|
26
|
-
```typescript
|
|
27
|
-
import { ExecUtil, ExecutionState } from '@travetto/base';
|
|
28
|
-
import { Worker, WorkPool, IterableWorkSet, ManualAsyncIterator } from '@travetto/worker';
|
|
29
|
-
|
|
30
|
-
class ImageProcessor implements Worker<string> {
|
|
31
|
-
active = false;
|
|
32
|
-
proc: ExecutionState;
|
|
33
|
-
|
|
34
|
-
get id(): number | undefined {
|
|
35
|
-
return this.proc.process.pid;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async destroy(): Promise<void> {
|
|
39
|
-
this.proc.process.kill();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async execute(path: string): Promise<void> {
|
|
43
|
-
this.active = true;
|
|
44
|
-
try {
|
|
45
|
-
this.proc = ExecUtil.spawn('convert images', [path]);
|
|
46
|
-
await this.proc;
|
|
47
|
-
} catch {
|
|
48
|
-
// Do nothing
|
|
49
|
-
}
|
|
50
|
-
this.active = false;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export class ImageCompressor extends WorkPool<string> {
|
|
55
|
-
|
|
56
|
-
pendingImages = new ManualAsyncIterator<string>();
|
|
57
|
-
|
|
58
|
-
constructor() {
|
|
59
|
-
super(async () => new ImageProcessor());
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
begin(): void {
|
|
63
|
-
this.process(new IterableWorkSet(this.pendingImages));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
convert(...images: string[]): void {
|
|
67
|
-
this.pendingImages.add(images);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
Once a pool is constructed, it can be shutdown by calling the `.shutdown()` method, and awaiting the result.
|
|
19
|
+
With respect to managing multiple executions, [WorkPool](https://github.com/travetto/travetto/tree/main/module/worker/src/pool.ts#L35) is provided to allow for concurrent operation, and processing of jobs concurrently. To manage the flow of jobs, [WorkQueue](https://github.com/travetto/travetto/tree/main/module/worker/src/queue.ts#L6) is provided to support a wide range of use cases. [WorkQueue](https://github.com/travetto/travetto/tree/main/module/worker/src/queue.ts#L6) allows for manual control of iteration, which is useful for event driven work loads.
|
|
73
20
|
|
|
74
21
|
## IPC Support
|
|
75
22
|
Within the `comm` package, there is support for two primary communication elements: [ChildCommChannel](https://github.com/travetto/travetto/tree/main/module/worker/src/comm/child.ts#L6) and [ParentCommChannel](https://github.com/travetto/travetto/tree/main/module/worker/src/comm/parent.ts#L10). Usually [ParentCommChannel](https://github.com/travetto/travetto/tree/main/module/worker/src/comm/parent.ts#L10) indicates it is the owner of the sub process. [ChildCommChannel](https://github.com/travetto/travetto/tree/main/module/worker/src/comm/child.ts#L6) indicates that it has been created/spawned/forked by the parent and will communicate back to it's parent. This generally means that a [ParentCommChannel](https://github.com/travetto/travetto/tree/main/module/worker/src/comm/parent.ts#L10) can be destroyed (i.e. killing the subprocess) where a [ChildCommChannel](https://github.com/travetto/travetto/tree/main/module/worker/src/comm/child.ts#L6) can only exit the process, but the channel cannot be destroyed.
|
|
76
|
-
|
|
77
|
-
### IPC as a Worker
|
|
78
|
-
A common pattern is to want to model a sub process as a worker, to be a valid candidate in a [WorkPool](https://github.com/travetto/travetto/tree/main/module/worker/src/pool.ts#L31). The [WorkUtil](https://github.com/travetto/travetto/tree/main/module/worker/src/util.ts#L14) class provides a utility to facilitate this desire.
|
|
79
|
-
|
|
80
|
-
**Code: Spawned Worker**
|
|
81
|
-
```typescript
|
|
82
|
-
import { ExecutionState } from '@travetto/base';
|
|
83
|
-
|
|
84
|
-
import { ParentCommChannel } from './comm/parent';
|
|
85
|
-
import { Worker } from './pool';
|
|
86
|
-
|
|
87
|
-
type Simple<V> = (ch: ParentCommChannel<V>) => Promise<unknown | void>;
|
|
88
|
-
type Param<V, X> = (ch: ParentCommChannel<V>, input: X) => Promise<unknown | void>;
|
|
89
|
-
|
|
90
|
-
const empty = async (): Promise<void> => { };
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Spawned worker
|
|
94
|
-
*/
|
|
95
|
-
export class WorkUtil {
|
|
96
|
-
/**
|
|
97
|
-
* Create a process channel worker from a given spawn config
|
|
98
|
-
*/
|
|
99
|
-
static spawnedWorker<V, X>(
|
|
100
|
-
worker: () => ExecutionState,
|
|
101
|
-
init: Simple<V>,
|
|
102
|
-
execute: Param<V, X>,
|
|
103
|
-
destroy: Simple<V> = empty): Worker<X> {
|
|
104
|
-
const channel = new ParentCommChannel<V>(worker());
|
|
105
|
-
return {
|
|
106
|
-
get id(): number | undefined { return channel.id; },
|
|
107
|
-
get active(): boolean { return channel.active; },
|
|
108
|
-
init: () => init(channel),
|
|
109
|
-
execute: inp => execute(channel, inp),
|
|
110
|
-
async destroy(): Promise<void> {
|
|
111
|
-
await destroy(channel);
|
|
112
|
-
await channel.destroy();
|
|
113
|
-
},
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
When creating your work, via process spawning, you will need to provide the script (and any other features you would like in `SpawnConfig`). Additionally you must, at a minimum, provide functionality to run whenever an input element is up for grabs in the input source. This method will be provided the communication channel ([ParentCommChannel](https://github.com/travetto/travetto/tree/main/module/worker/src/comm/parent.ts#L10)) and the input value. A simple example could look like:
|
|
120
|
-
|
|
121
|
-
**Code: Spawning Pool**
|
|
122
|
-
```typescript
|
|
123
|
-
import { ExecUtil } from '@travetto/base';
|
|
124
|
-
import { WorkPool, WorkUtil, IterableWorkSet } from '@travetto/worker';
|
|
125
|
-
|
|
126
|
-
export async function main(): Promise<void> {
|
|
127
|
-
const pool = new WorkPool(() =>
|
|
128
|
-
WorkUtil.spawnedWorker<{ data: number }, number>(
|
|
129
|
-
() => ExecUtil.spawn('trv', ['main', '@travetto/worker/doc/spawned.ts']),
|
|
130
|
-
ch => ch.once('ready'), // Wait for child to indicate it is ready
|
|
131
|
-
async (channel, inp) => {
|
|
132
|
-
const res = channel.once('response'); // Register response listener
|
|
133
|
-
channel.send('request', { data: inp }); // Send request
|
|
134
|
-
|
|
135
|
-
const { data } = await res; // Get answer
|
|
136
|
-
console.log('Request complete', { input: inp, output: data });
|
|
137
|
-
|
|
138
|
-
if (!(inp + inp === data)) {
|
|
139
|
-
// Ensure the answer is double the input
|
|
140
|
-
throw new Error(`Did not get the double: inp=${inp}, data=${data}`);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
)
|
|
144
|
-
);
|
|
145
|
-
await pool.process(new IterableWorkSet([1, 2, 3, 4, 5]));
|
|
146
|
-
}
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
**Code: Spawned Worker**
|
|
150
|
-
```typescript
|
|
151
|
-
import timers from 'timers/promises';
|
|
152
|
-
import { ChildCommChannel } from '@travetto/worker';
|
|
153
|
-
|
|
154
|
-
export async function main(): Promise<void> {
|
|
155
|
-
const exec = new ChildCommChannel<{ data: string }>();
|
|
156
|
-
|
|
157
|
-
exec.on('request', data =>
|
|
158
|
-
exec.send('response', { data: (data.data + data.data) })); // When data is received, return double
|
|
159
|
-
|
|
160
|
-
exec.send('ready'); // Indicate the child is ready to receive requests
|
|
161
|
-
|
|
162
|
-
for await (const _ of timers.setInterval(5000)) {
|
|
163
|
-
// Keep-alive
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
**Terminal: Output**
|
|
169
|
-
```bash
|
|
170
|
-
$ trv main doc/spawner.ts
|
|
171
|
-
|
|
172
|
-
Request complete { input: 1, output: 2 }
|
|
173
|
-
Request complete { input: 2, output: 4 }
|
|
174
|
-
Request complete { input: 3, output: 6 }
|
|
175
|
-
Request complete { input: 4, output: 8 }
|
|
176
|
-
Request complete { input: 5, output: 10 }
|
|
177
|
-
```
|
package/__index__.ts
CHANGED
|
@@ -2,11 +2,8 @@ export * from './src/comm/channel';
|
|
|
2
2
|
export * from './src/comm/child';
|
|
3
3
|
export * from './src/comm/parent';
|
|
4
4
|
export * from './src/comm/types';
|
|
5
|
-
export * from './src/input/iterable';
|
|
6
|
-
export * from './src/input/async-iterator';
|
|
7
|
-
export * from './src/input/types';
|
|
8
5
|
export * from './src/support/barrier';
|
|
9
6
|
export * from './src/support/timeout';
|
|
10
7
|
export * from './src/support/error';
|
|
8
|
+
export * from './src/queue';
|
|
11
9
|
export * from './src/pool';
|
|
12
|
-
export * from './src/util';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/worker",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0-rc.1",
|
|
4
4
|
"description": "Process management utilities, with a focus on inter-process communication",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"exec",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"directory": "module/worker"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@travetto/base": "^
|
|
28
|
+
"@travetto/base": "^4.0.0-rc.1",
|
|
29
29
|
"generic-pool": "^3.9.0"
|
|
30
30
|
},
|
|
31
31
|
"travetto": {
|
package/src/comm/channel.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { ChildProcess } from 'child_process';
|
|
2
|
-
import { EventEmitter } from 'events';
|
|
3
|
-
|
|
4
|
-
import { ExecUtil } from '@travetto/base';
|
|
1
|
+
import { ChildProcess } from 'node:child_process';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
5
3
|
|
|
6
4
|
/**
|
|
7
5
|
* Channel that represents communication between parent/child
|
|
@@ -56,8 +54,8 @@ export class ProcessCommChannel<T extends NodeJS.Process | ChildProcess, V = unk
|
|
|
56
54
|
console.debug('Sending', { pid: this.#parentId, id: this.id, eventType });
|
|
57
55
|
if (!this.#proc) {
|
|
58
56
|
throw new Error('this.proc was not defined');
|
|
59
|
-
} else if (this.#proc.send) {
|
|
60
|
-
this.#proc.send({ ...(data ?? {}), type: eventType });
|
|
57
|
+
} else if (this.#proc.send && this.#proc.connected) {
|
|
58
|
+
this.#proc.send({ ...(data ?? {}), type: eventType }, (err) => err && console.error(err));
|
|
61
59
|
} else {
|
|
62
60
|
throw new Error('this.proc.send was not defined');
|
|
63
61
|
}
|
|
@@ -92,7 +90,8 @@ export class ProcessCommChannel<T extends NodeJS.Process | ChildProcess, V = unk
|
|
|
92
90
|
if (this.#proc) {
|
|
93
91
|
console.debug('Killing', { pid: this.#parentId, id: this.id });
|
|
94
92
|
if (this.#proc !== process) {
|
|
95
|
-
|
|
93
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
94
|
+
(this.#proc as ChildProcess).kill();
|
|
96
95
|
}
|
|
97
96
|
this.#proc = undefined;
|
|
98
97
|
}
|
package/src/comm/parent.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ChildProcess } from 'child_process';
|
|
1
|
+
import { ChildProcess } from 'node:child_process';
|
|
2
2
|
|
|
3
|
-
import { ShutdownManager
|
|
3
|
+
import { ShutdownManager } from '@travetto/base';
|
|
4
4
|
|
|
5
5
|
import { ProcessCommChannel } from './channel';
|
|
6
6
|
|
|
@@ -9,13 +9,12 @@ import { ProcessCommChannel } from './channel';
|
|
|
9
9
|
*/
|
|
10
10
|
export class ParentCommChannel<U = unknown> extends ProcessCommChannel<ChildProcess, U> {
|
|
11
11
|
|
|
12
|
-
#complete:
|
|
12
|
+
#complete: Promise<void>;
|
|
13
13
|
|
|
14
|
-
constructor(
|
|
15
|
-
super(
|
|
16
|
-
ShutdownManager.
|
|
17
|
-
this.#complete =
|
|
18
|
-
.finally(() => { this.proc = undefined; });
|
|
14
|
+
constructor(proc: ChildProcess) {
|
|
15
|
+
super(proc);
|
|
16
|
+
ShutdownManager.onGracefulShutdown(() => this.destroy(), this);
|
|
17
|
+
this.#complete = new Promise<void>(r => proc.on('close', r)).finally(() => { this.proc = undefined; });
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
/**
|
package/src/pool.ts
CHANGED
|
@@ -1,211 +1,178 @@
|
|
|
1
|
-
import os from 'os';
|
|
2
1
|
import gp from 'generic-pool';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import timers from 'node:timers/promises';
|
|
3
4
|
|
|
4
|
-
import {
|
|
5
|
+
import { Env, Util } from '@travetto/base';
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
-
|
|
7
|
+
import { WorkQueue } from './queue';
|
|
8
|
+
|
|
9
|
+
type ItrSource<I> = Iterable<I> | AsyncIterable<I>;
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Worker definition
|
|
11
13
|
*/
|
|
12
|
-
export interface Worker<
|
|
13
|
-
active
|
|
14
|
-
id
|
|
14
|
+
export interface Worker<I, O = unknown> {
|
|
15
|
+
active?: boolean;
|
|
16
|
+
id?: unknown;
|
|
15
17
|
init?(): Promise<unknown>;
|
|
16
|
-
execute(input:
|
|
17
|
-
destroy(): Promise<void>;
|
|
18
|
+
execute(input: I, idx: number): Promise<O>;
|
|
19
|
+
destroy?(): Promise<void>;
|
|
18
20
|
release?(): unknown;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
type
|
|
22
|
-
|
|
23
|
-
onComplete?: (
|
|
23
|
+
type WorkerInput<I, O> = (() => Worker<I, O>) | ((input: I, inputIdx: number) => Promise<O>);
|
|
24
|
+
type WorkPoolConfig<I, O> = gp.Options & {
|
|
25
|
+
onComplete?: (output: O, input: I, finishIdx: number) => void;
|
|
26
|
+
onError?(ev: Error, input: I, finishIdx: number): (unknown | Promise<unknown>);
|
|
27
|
+
shutdown?: AbortSignal;
|
|
24
28
|
};
|
|
25
29
|
|
|
26
|
-
|
|
30
|
+
const isWorkerFactory = <I, O>(o: WorkerInput<I, O>): o is (() => Worker<I, O>) => o.length === 0;
|
|
27
31
|
|
|
28
32
|
/**
|
|
29
33
|
* Work pool support
|
|
30
34
|
*/
|
|
31
|
-
export class WorkPool
|
|
35
|
+
export class WorkPool {
|
|
32
36
|
|
|
33
|
-
static MAX_SIZE = os.
|
|
37
|
+
static MAX_SIZE = os.availableParallelism();
|
|
34
38
|
static DEFAULT_SIZE = Math.min(WorkPool.MAX_SIZE, 4);
|
|
35
39
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
#pool: gp.Pool<Worker<X>>;
|
|
40
|
-
/**
|
|
41
|
-
* Number of acquisitions in process
|
|
42
|
-
*/
|
|
43
|
-
#pendingAcquires = 0;
|
|
44
|
-
/**
|
|
45
|
-
* List of errors during processing
|
|
46
|
-
*/
|
|
47
|
-
#errors: Error[] = [];
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Error count during creation
|
|
51
|
-
*/
|
|
52
|
-
#createErrors = 0;
|
|
40
|
+
/** Build worker pool */
|
|
41
|
+
static #buildPool<I, O>(worker: WorkerInput<I, O>, opts?: WorkPoolConfig<I, O>): gp.Pool<Worker<I, O>> {
|
|
42
|
+
let pendingAcquires = 0;
|
|
53
43
|
|
|
54
|
-
|
|
55
|
-
* Are we tracing
|
|
56
|
-
*/
|
|
57
|
-
#trace: boolean;
|
|
44
|
+
const trace = /@travetto\/worker/.test(Env.DEBUG.val ?? '');
|
|
58
45
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
46
|
+
// Create the pool
|
|
47
|
+
const pool = gp.createPool({
|
|
48
|
+
async create() {
|
|
49
|
+
try {
|
|
50
|
+
pendingAcquires += 1;
|
|
51
|
+
const res = isWorkerFactory(worker) ? await worker() : { execute: worker };
|
|
52
|
+
res.id ??= Util.shortHash(`${Math.random()}`);
|
|
53
|
+
|
|
54
|
+
if (res.init) {
|
|
55
|
+
await res.init();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return res;
|
|
59
|
+
} finally {
|
|
60
|
+
pendingAcquires -= 1;
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
async destroy(x) {
|
|
64
|
+
if (trace) {
|
|
65
|
+
console.debug('Destroying', { pid: process.pid, worker: x.id });
|
|
66
|
+
}
|
|
67
|
+
return x.destroy?.();
|
|
68
|
+
},
|
|
69
|
+
validate: async (x: Worker<I, O>) => x.active ?? true
|
|
70
|
+
}, {
|
|
71
71
|
max: WorkPool.DEFAULT_SIZE,
|
|
72
72
|
min: 1,
|
|
73
73
|
evictionRunIntervalMillis: 5000,
|
|
74
74
|
...(opts ?? {}),
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
// Create the pool
|
|
78
|
-
this.#pool = gp.createPool({
|
|
79
|
-
create: () => this.#createAndTrack(getWorker, args),
|
|
80
|
-
destroy: x => this.destroy(x),
|
|
81
|
-
validate: async (x: Worker<X>) => x.active
|
|
82
|
-
}, args);
|
|
83
|
-
|
|
84
|
-
this.#shutdownCleanup = ShutdownManager.onShutdown(`worker.pool.${this.constructor.name}`, () => {
|
|
85
|
-
this.#shutdownCleanup = undefined;
|
|
86
|
-
this.shutdown();
|
|
87
75
|
});
|
|
88
76
|
|
|
89
|
-
this.#trace = !!GlobalEnv.debug?.includes('@travetto/worker');
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Creates and tracks new worker
|
|
94
|
-
*/
|
|
95
|
-
async #createAndTrack(getWorker: () => Promise<Worker<X>> | Worker<X>, opts: gp.Options): Promise<Worker<X>> {
|
|
96
|
-
try {
|
|
97
|
-
this.#pendingAcquires += 1;
|
|
98
|
-
const res = await getWorker();
|
|
99
|
-
|
|
100
|
-
if (res.init) {
|
|
101
|
-
await res.init();
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
this.#createErrors = 0; // Reset errors on success
|
|
105
77
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
await ShutdownManager.exit(1);
|
|
78
|
+
// Listen for shutdown
|
|
79
|
+
opts?.shutdown?.addEventListener('abort', async () => {
|
|
80
|
+
while (pendingAcquires) {
|
|
81
|
+
await timers.setTimeout(10);
|
|
111
82
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
}
|
|
83
|
+
await pool.drain();
|
|
84
|
+
await pool.clear();
|
|
85
|
+
});
|
|
117
86
|
|
|
118
|
-
|
|
119
|
-
* Destroy the worker
|
|
120
|
-
*/
|
|
121
|
-
async destroy(worker: Worker<X>): Promise<void> {
|
|
122
|
-
if (this.#trace) {
|
|
123
|
-
console.debug('Destroying', { pid: process.pid, worker: worker.id });
|
|
124
|
-
}
|
|
125
|
-
return worker.destroy();
|
|
87
|
+
return pool;
|
|
126
88
|
}
|
|
127
89
|
|
|
128
90
|
/**
|
|
129
|
-
*
|
|
91
|
+
* Process a given input source and worker, and fire on completion
|
|
130
92
|
*/
|
|
131
|
-
async
|
|
132
|
-
if (this.#trace) {
|
|
133
|
-
console.debug('Releasing', { pid: process.pid, worker: worker.id });
|
|
134
|
-
}
|
|
135
|
-
try {
|
|
136
|
-
if (worker.active) {
|
|
137
|
-
try {
|
|
138
|
-
await worker.release?.();
|
|
139
|
-
} catch { }
|
|
140
|
-
await this.#pool.release(worker);
|
|
141
|
-
} else {
|
|
142
|
-
await this.#pool.destroy(worker);
|
|
143
|
-
}
|
|
144
|
-
} catch { }
|
|
145
|
-
}
|
|
93
|
+
static async run<I, O>(workerFactory: WorkerInput<I, O>, src: ItrSource<I>, opts: WorkPoolConfig<I, O> = {}): Promise<void> {
|
|
146
94
|
|
|
147
|
-
|
|
148
|
-
* Process a given input source
|
|
149
|
-
*/
|
|
150
|
-
async process(src: WorkSet<X>, cfg: WorkPoolProcessConfig<X> = {}): Promise<void> {
|
|
95
|
+
const trace = /@travetto\/worker/.test(Env.DEBUG.val ?? '');
|
|
151
96
|
const pending = new Set<Promise<unknown>>();
|
|
152
|
-
|
|
97
|
+
const errors: Error[] = [];
|
|
98
|
+
let inputIdx = 0;
|
|
99
|
+
let finishIdx = 0;
|
|
153
100
|
|
|
154
|
-
|
|
155
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
156
|
-
cfg.onComplete({ value: undefined as X, idx: 0, total: src.size });
|
|
157
|
-
}
|
|
101
|
+
const pool = this.#buildPool(workerFactory, opts);
|
|
158
102
|
|
|
159
|
-
|
|
160
|
-
const worker =
|
|
161
|
-
|
|
103
|
+
for await (const nextInput of src) {
|
|
104
|
+
const worker = await pool.acquire()!;
|
|
105
|
+
|
|
106
|
+
if (trace) {
|
|
162
107
|
console.debug('Acquired', { pid: process.pid, worker: worker.id });
|
|
163
108
|
}
|
|
164
|
-
const nextInput = await src.next();
|
|
165
109
|
|
|
166
|
-
const completion = worker.execute(nextInput)
|
|
167
|
-
.
|
|
168
|
-
.
|
|
169
|
-
|
|
110
|
+
const completion = worker.execute(nextInput, inputIdx += 1)
|
|
111
|
+
.then(v => opts.onComplete?.(v, nextInput, finishIdx += 1))
|
|
112
|
+
.catch(err => {
|
|
113
|
+
errors.push(err);
|
|
114
|
+
opts?.onError?.(err, nextInput, finishIdx += 1);
|
|
115
|
+
}) // Catch error
|
|
116
|
+
.finally(async () => {
|
|
117
|
+
if (trace) {
|
|
118
|
+
console.debug('Releasing', { pid: process.pid, worker: worker.id });
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
if (worker.active ?? true) {
|
|
122
|
+
try {
|
|
123
|
+
await worker.release?.();
|
|
124
|
+
} catch { }
|
|
125
|
+
await pool.release(worker);
|
|
126
|
+
} else {
|
|
127
|
+
await pool.destroy(worker);
|
|
128
|
+
}
|
|
129
|
+
} catch { }
|
|
130
|
+
});
|
|
170
131
|
|
|
171
132
|
completion.finally(() => pending.delete(completion));
|
|
172
|
-
|
|
173
133
|
pending.add(completion);
|
|
174
134
|
}
|
|
175
135
|
|
|
176
|
-
|
|
177
|
-
await Promise.all(Array.from(pending));
|
|
136
|
+
await Promise.all(Array.from(pending));
|
|
178
137
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
} finally {
|
|
183
|
-
if (cfg.shutdownOnComplete !== false) {
|
|
184
|
-
await this.shutdown();
|
|
185
|
-
}
|
|
138
|
+
if (errors.length) {
|
|
139
|
+
throw errors[0];
|
|
186
140
|
}
|
|
187
141
|
}
|
|
188
142
|
|
|
189
143
|
/**
|
|
190
|
-
* Process a given input source as an async iterable
|
|
144
|
+
* Process a given input source as an async iterable
|
|
191
145
|
*/
|
|
192
|
-
|
|
193
|
-
const itr = new
|
|
194
|
-
const res = this.
|
|
146
|
+
static runStream<I, O>(worker: WorkerInput<I, O>, input: ItrSource<I>, opts?: WorkPoolConfig<I, O>): AsyncIterable<O> {
|
|
147
|
+
const itr = new WorkQueue<O>();
|
|
148
|
+
const res = this.run(worker, input, {
|
|
149
|
+
...opts,
|
|
150
|
+
onComplete: (ev, inp, finishIdx) => {
|
|
151
|
+
itr.add(ev);
|
|
152
|
+
opts?.onComplete?.(ev, inp, finishIdx);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
195
155
|
res.finally(() => itr.close());
|
|
196
156
|
return itr;
|
|
197
157
|
}
|
|
198
158
|
|
|
199
159
|
/**
|
|
200
|
-
*
|
|
160
|
+
* Process a given input source as an async iterable with progress information
|
|
201
161
|
*/
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
|
|
162
|
+
static runStreamProgress<I, O>(worker: WorkerInput<I, O>, input: ItrSource<I>, total: number, opts?: WorkPoolConfig<I, O>): AsyncIterable<{
|
|
163
|
+
idx: number;
|
|
164
|
+
value: O;
|
|
165
|
+
total: number;
|
|
166
|
+
}> {
|
|
167
|
+
const itr = new WorkQueue<{ idx: number, value: O, total: number }>();
|
|
168
|
+
const res = this.run(worker, input, {
|
|
169
|
+
...opts,
|
|
170
|
+
onComplete: (ev, inp, finishIdx) => {
|
|
171
|
+
itr.add({ value: ev, idx: finishIdx, total });
|
|
172
|
+
opts?.onComplete?.(ev, inp, finishIdx);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
res.finally(() => itr.close());
|
|
176
|
+
return itr;
|
|
210
177
|
}
|
|
211
|
-
}
|
|
178
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Util } from '@travetto/base';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* WorkQueue, a manual async iterator. Items are added manually, and consumed asynchronously
|
|
5
5
|
*/
|
|
6
|
-
export class
|
|
6
|
+
export class WorkQueue<X> implements AsyncIterator<X>, AsyncIterable<X> {
|
|
7
7
|
|
|
8
8
|
#queue: X[] = [];
|
|
9
9
|
#done = false;
|
|
@@ -17,6 +17,9 @@ export class ManualAsyncIterator<X> implements AsyncIterator<X>, AsyncIterable<X
|
|
|
17
17
|
this.#queue.push(...initial);
|
|
18
18
|
this.#size = this.#queue.length;
|
|
19
19
|
signal?.addEventListener('abort', () => this.close());
|
|
20
|
+
if (signal?.aborted) {
|
|
21
|
+
this.close();
|
|
22
|
+
}
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
// Allow for iteration
|
|
@@ -39,14 +42,20 @@ export class ManualAsyncIterator<X> implements AsyncIterator<X>, AsyncIterable<X
|
|
|
39
42
|
* Queue next event to fire
|
|
40
43
|
* @param {boolean} immediate Determines if item(s) should be append or prepended to the queue
|
|
41
44
|
*/
|
|
42
|
-
add(item: X
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
add(item: X, immediate = false): void {
|
|
46
|
+
this.#queue[immediate ? 'unshift' : 'push'](item);
|
|
47
|
+
this.#size += 1;
|
|
48
|
+
this.#ready.resolve();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Queue a list of data to stream
|
|
53
|
+
* @param {boolean} immediate Determines if item(s) should be append or prepended to the queue
|
|
54
|
+
*/
|
|
55
|
+
addAll(items: Iterable<X>): void {
|
|
56
|
+
const copy = [...items];
|
|
57
|
+
this.#queue.push(...copy);
|
|
58
|
+
this.#size += copy.length;
|
|
50
59
|
this.#ready.resolve();
|
|
51
60
|
}
|
|
52
61
|
|
|
@@ -58,6 +67,15 @@ export class ManualAsyncIterator<X> implements AsyncIterator<X>, AsyncIterable<X
|
|
|
58
67
|
this.#ready.resolve();
|
|
59
68
|
}
|
|
60
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Throw an error from the queue, rejecting and terminating immediately
|
|
72
|
+
*/
|
|
73
|
+
async throw(e?: Error): Promise<IteratorResult<X>> {
|
|
74
|
+
this.#done = true;
|
|
75
|
+
this.#ready.reject(e);
|
|
76
|
+
return { value: undefined, done: this.#done };
|
|
77
|
+
}
|
|
78
|
+
|
|
61
79
|
/**
|
|
62
80
|
* Get size, will change as items are added
|
|
63
81
|
*/
|
package/src/support/barrier.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { setTimeout } from 'timers/promises';
|
|
1
|
+
import { setTimeout } from 'node:timers/promises';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { TimeSpan, Util } from '@travetto/base';
|
|
4
4
|
|
|
5
5
|
import { Timeout } from './timeout';
|
|
6
6
|
|
|
@@ -19,7 +19,11 @@ export class Barrier {
|
|
|
19
19
|
static listenForUnhandled(): Promise<never> & { cancel: () => void } {
|
|
20
20
|
const uncaught = Util.resolvablePromise<never>();
|
|
21
21
|
const uncaughtWithCancel: typeof uncaught & { cancel?: () => void } = uncaught;
|
|
22
|
-
const
|
|
22
|
+
const onError = (err: Error): void => { setTimeout(1).then(() => uncaught.reject(err)); };
|
|
23
|
+
process.on('unhandledRejection', onError).on('uncaughtException', onError);
|
|
24
|
+
const cancel = (): void => {
|
|
25
|
+
process.off('unhandledRejection', onError).off('unhandledException', onError);
|
|
26
|
+
};
|
|
23
27
|
uncaughtWithCancel.cancel = (): void => {
|
|
24
28
|
cancel(); // Remove the handler
|
|
25
29
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
package/src/support/timeout.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { clearTimeout } from 'node:timers';
|
|
1
2
|
import { TimeSpan, TimeUtil, Util } from '@travetto/base';
|
|
2
3
|
import { ExecutionError } from './error';
|
|
3
4
|
|
|
@@ -6,7 +7,7 @@ import { ExecutionError } from './error';
|
|
|
6
7
|
*/
|
|
7
8
|
export class Timeout extends ExecutionError {
|
|
8
9
|
|
|
9
|
-
#id:
|
|
10
|
+
#id: ReturnType<typeof setTimeout> | undefined;
|
|
10
11
|
#promise = Util.resolvablePromise();
|
|
11
12
|
#duration: number;
|
|
12
13
|
|
package/src/input/iterable.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { WorkSet } from './types';
|
|
2
|
-
|
|
3
|
-
type Itr<T> = Iterator<T> | AsyncIterator<T>;
|
|
4
|
-
|
|
5
|
-
const hasAsyncItr = (o: unknown): o is AsyncIterable<unknown> => !!o && typeof o === 'object' && Symbol.asyncIterator in o;
|
|
6
|
-
const hasItr = (o: unknown): o is Iterable<unknown> => !!o && typeof o === 'object' && Symbol.iterator in o;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Basic input source given an iterable input
|
|
10
|
-
*/
|
|
11
|
-
export class IterableWorkSet<X> implements WorkSet<X> {
|
|
12
|
-
|
|
13
|
-
#src: Itr<X>;
|
|
14
|
-
#ondeck?: X;
|
|
15
|
-
#done = false;
|
|
16
|
-
#size?: number;
|
|
17
|
-
|
|
18
|
-
constructor(src: Iterable<X> | AsyncIterable<X> | (() => Generator<X>) | (() => AsyncGenerator<X>) | Itr<X>) {
|
|
19
|
-
if ('next' in src) {
|
|
20
|
-
this.#src = src;
|
|
21
|
-
} else {
|
|
22
|
-
if (hasAsyncItr(src)) {
|
|
23
|
-
this.#src = src[Symbol.asyncIterator]();
|
|
24
|
-
} else if (hasItr(src)) {
|
|
25
|
-
this.#src = src[Symbol.iterator]();
|
|
26
|
-
} else {
|
|
27
|
-
this.#src = src();
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
if (Array.isArray(src)) {
|
|
31
|
-
this.#size = src.length;
|
|
32
|
-
} else if (src instanceof Set) {
|
|
33
|
-
this.#size = src.size;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Fetch next item from the iterable
|
|
39
|
-
*/
|
|
40
|
-
async #primeNext(): Promise<void> {
|
|
41
|
-
const res = await this.#src.next();
|
|
42
|
-
this.#done = !!res.done;
|
|
43
|
-
this.#ondeck = res.value;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Determine if the iterable has more data
|
|
48
|
-
*/
|
|
49
|
-
async hasNext(): Promise<boolean> {
|
|
50
|
-
if (this.#ondeck === undefined) {
|
|
51
|
-
await this.#primeNext();
|
|
52
|
-
}
|
|
53
|
-
return !this.#done;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Fetch next item
|
|
58
|
-
*/
|
|
59
|
-
async next(): Promise<X> {
|
|
60
|
-
await this.hasNext();
|
|
61
|
-
|
|
62
|
-
const out = this.#ondeck!;
|
|
63
|
-
this.#ondeck = undefined;
|
|
64
|
-
return out;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Get size, if defined
|
|
69
|
-
*/
|
|
70
|
-
get size(): number | undefined {
|
|
71
|
-
return this.#size;
|
|
72
|
-
}
|
|
73
|
-
}
|
package/src/input/types.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Definition for an work set
|
|
3
|
-
*/
|
|
4
|
-
export interface WorkSet<X> {
|
|
5
|
-
/**
|
|
6
|
-
* Determines if there is more work to do
|
|
7
|
-
*/
|
|
8
|
-
hasNext(): boolean | Promise<boolean>;
|
|
9
|
-
/**
|
|
10
|
-
* Get next item
|
|
11
|
-
*/
|
|
12
|
-
next(): X | Promise<X>;
|
|
13
|
-
/**
|
|
14
|
-
* Total size of work set
|
|
15
|
-
*/
|
|
16
|
-
size?: number;
|
|
17
|
-
}
|
package/src/util.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { ExecutionState } from '@travetto/base';
|
|
2
|
-
|
|
3
|
-
import { ParentCommChannel } from './comm/parent';
|
|
4
|
-
import { Worker } from './pool';
|
|
5
|
-
|
|
6
|
-
type Simple<V> = (ch: ParentCommChannel<V>) => Promise<unknown | void>;
|
|
7
|
-
type Param<V, X> = (ch: ParentCommChannel<V>, input: X) => Promise<unknown | void>;
|
|
8
|
-
|
|
9
|
-
const empty = async (): Promise<void> => { };
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Spawned worker
|
|
13
|
-
*/
|
|
14
|
-
export class WorkUtil {
|
|
15
|
-
/**
|
|
16
|
-
* Create a process channel worker from a given spawn config
|
|
17
|
-
*/
|
|
18
|
-
static spawnedWorker<V, X>(
|
|
19
|
-
worker: () => ExecutionState,
|
|
20
|
-
init: Simple<V>,
|
|
21
|
-
execute: Param<V, X>,
|
|
22
|
-
destroy: Simple<V> = empty): Worker<X> {
|
|
23
|
-
const channel = new ParentCommChannel<V>(worker());
|
|
24
|
-
return {
|
|
25
|
-
get id(): number | undefined { return channel.id; },
|
|
26
|
-
get active(): boolean { return channel.active; },
|
|
27
|
-
init: () => init(channel),
|
|
28
|
-
execute: inp => execute(channel, inp),
|
|
29
|
-
async destroy(): Promise<void> {
|
|
30
|
-
await destroy(channel);
|
|
31
|
-
await channel.destroy();
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
}
|