-
Notifications
You must be signed in to change notification settings - Fork 669
Expand file tree
/
Copy pathchild-pool.ts
More file actions
132 lines (110 loc) · 3.57 KB
/
Copy pathchild-pool.ts
File metadata and controls
132 lines (110 loc) · 3.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import * as path from 'path';
import { Child } from './child';
import { SandboxedOptions } from '../interfaces';
const CHILD_KILL_TIMEOUT = 30_000;
interface ChildPoolOpts extends SandboxedOptions {
mainFile?: string;
}
const supportCJS = () => {
return (
typeof require === 'function' &&
typeof module === 'object' &&
typeof module.exports === 'object'
);
};
export class ChildPool {
retained: { [key: number]: Child } = {};
free: { [key: string]: Child[] } = {};
private opts: ChildPoolOpts;
constructor({
mainFile = supportCJS()
? path.join(process.cwd(), 'dist/cjs/classes/main.js')
: path.join(process.cwd(), 'dist/esm/classes/main.js'),
useWorkerThreads,
workerForkOptions,
workerThreadsOptions,
}: ChildPoolOpts) {
this.opts = {
mainFile,
useWorkerThreads,
workerForkOptions,
workerThreadsOptions,
};
}
async retain(processFile: string): Promise<Child> {
let child = this.getFree(processFile).pop();
if (child) {
this.retained[child.pid] = child;
return child;
}
child = new Child(this.opts.mainFile, processFile, {
useWorkerThreads: this.opts.useWorkerThreads,
workerForkOptions: this.opts.workerForkOptions,
workerThreadsOptions: this.opts.workerThreadsOptions,
});
child.on('exit', this.remove.bind(this, child));
try {
await child.init();
// Check status here as well, in case the child exited before we could
// retain it.
if (child.exitCode !== null || child.signalCode !== null) {
throw new Error('Child exited before it could be retained');
}
this.retained[child.pid] = child;
return child;
} catch (err) {
console.error(err);
// A child that failed to initialize (or exited during init) must never
// be released back into the free pool, otherwise it becomes a "zombie"
// that is reused for every subsequent job and fails them instantly.
// Kill and remove it so a fresh child is forked on the next retain.
// The child also exits itself after a failed init (see ChildProcessor),
// so this is normally a no-op; log any kill failure instead of silently
// swallowing it so a lingering child would not go unnoticed.
if (child.childProcess || child.worker) {
try {
this.kill(child, 'SIGKILL').catch(killErr => {
console.error('Failed to kill child after init error:', killErr);
});
} catch (killErr) {
console.error('Failed to kill child after init error:', killErr);
}
}
throw err;
}
}
release(child: Child): void {
delete this.retained[child.pid];
this.getFree(child.processFile).push(child);
}
remove(child: Child): void {
delete this.retained[child.pid];
const free = this.getFree(child.processFile);
const childIndex = free.indexOf(child);
if (childIndex > -1) {
free.splice(childIndex, 1);
}
}
async kill(
child: Child,
signal: 'SIGTERM' | 'SIGKILL' = 'SIGKILL',
): Promise<void> {
this.remove(child);
return child.kill(signal, CHILD_KILL_TIMEOUT);
}
async clean(): Promise<void> {
const children = Object.values(this.retained).concat(this.getAllFree());
this.retained = {};
this.free = {};
await Promise.all(children.map(c => this.kill(c, 'SIGTERM')));
}
getFree(id: string): Child[] {
return (this.free[id] = this.free[id] || []);
}
getAllFree(): Child[] {
return Object.values(this.free).reduce(
(first, second) => first.concat(second),
[],
);
}
}