Skip to content

Commit b3d3626

Browse files
authored
feat: support using process.on to listen SIGTERM (#10)
1 parent eefc1b8 commit b3d3626

File tree

5 files changed

+112
-13
lines changed

5 files changed

+112
-13
lines changed

src/index.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export interface Options {
1010
label?: string;
1111
timeout?: number;
1212
beforeExit?: BeforeExit;
13+
sigterm?: 'always' | 'once';
1314
}
1415

1516
export function graceful(options: Options = {}) {
@@ -43,10 +44,23 @@ export function graceful(options: Options = {}) {
4344

4445
// https://github.com/eggjs/egg-cluster/blob/master/lib/agent_worker.js#L35
4546
// exit gracefully
46-
process.once('SIGTERM', () => {
47-
printLogLevels.info && logger.info('[%s] receive signal SIGTERM, exiting with code:0', label);
48-
exit(0);
49-
});
47+
if (options.sigterm === 'always') {
48+
let called = false;
49+
process.on('SIGTERM', () => {
50+
if (called) {
51+
printLogLevels.info && logger.info('[%s] receive signal SIGTERM again, waiting for exit', label);
52+
return;
53+
}
54+
called = true;
55+
printLogLevels.info && logger.info('[%s] receive signal SIGTERM, exiting with code:0', label);
56+
exit(0);
57+
});
58+
} else {
59+
process.once('SIGTERM', () => {
60+
printLogLevels.info && logger.info('[%s] receive signal SIGTERM, exiting with code:0', label);
61+
exit(0);
62+
});
63+
}
5064

5165
process.once('exit', code => {
5266
const level = code === 0 ? 'info' : 'error';

test/fixtures/child.cjs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,19 @@ http.createServer((req, res) => {
88
}).listen(8000);
99

1010
console.log(`Worker ${process.pid} started`);
11-
graceful({
11+
const options = {
1212
logger: console,
1313
label: 'test-child',
1414
logLevel: process.env.NODE_LOG_LEVEL,
15-
});
15+
};
16+
if (process.env.ALWAYS_ON_SIGTERM) {
17+
options.sigterm = 'always';
18+
options.beforeExit = async () => {
19+
await new Promise(r => setTimeout(r, 1000));
20+
console.log('exit after 1000ms');
21+
};
22+
}
23+
graceful(options);
1624

1725
// run again should work
1826
graceful();

test/fixtures/cluster.cjs

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,22 @@ if (cluster.isMaster) {
1515
});
1616

1717
process.once('SIGTERM', () => {
18-
for (const id in cluster.workers) {
19-
cluster.workers[id].process.kill('SIGTERM');
18+
const killWorkers = () => {
19+
for (const id in cluster.workers) {
20+
cluster.workers[id].process.kill('SIGTERM');
21+
}
22+
};
23+
24+
killWorkers();
25+
26+
if (process.env.ALWAYS_ON_SIGTERM) {
27+
setTimeout(killWorkers, 50);
28+
setTimeout(() => process.exit(0), 2100);
29+
} else {
30+
setTimeout(() => {
31+
process.exit(0);
32+
}, 100);
2033
}
21-
setTimeout(() => {
22-
process.exit(0);
23-
}, 100);
2434
});
2535
} else {
2636
// Workers can share any TCP connection
@@ -35,8 +45,16 @@ if (cluster.isMaster) {
3545

3646
console.log(`Worker ${process.pid} started`);
3747
const { graceful } = require('../..');
38-
graceful({
48+
const options = {
3949
label: 'app-worker-' + cluster.worker.id,
4050
logLevel: process.env.NODE_LOG_LEVEL,
41-
});
51+
};
52+
if (process.env.ALWAYS_ON_SIGTERM) {
53+
options.sigterm = 'always';
54+
options.beforeExit = async () => {
55+
await new Promise(r => setTimeout(r, 1000));
56+
console.log('exit after 1000ms');
57+
};
58+
}
59+
graceful(options);
4260
}

test/fixtures/master-sigterm.cjs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/* eslint-disable @typescript-eslint/no-var-requires */
2+
const childProcess = require('node:child_process');
3+
const path = require('node:path');
4+
5+
const childFile = path.join(__dirname, 'child.cjs');
6+
7+
const child = childProcess.fork(childFile);
8+
9+
process.once('SIGTERM', () => {
10+
child.kill('SIGTERM');
11+
setTimeout(() => child.kill('SIGTERM'), 50);
12+
setTimeout(() => process.exit(0), 1500);
13+
});
14+
15+
console.log('master fork %s done', childFile);

test/index.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,28 @@ describe('test/index.test.ts', () => {
8282
// no exit event on coverage mode
8383
// child.expect('stdout', /worker \d+ died, code 0, signal null/);
8484
});
85+
86+
it('should always listen sigterm work', async () => {
87+
const startFile = path.join(fixtures, 'cluster.cjs');
88+
const child: any = coffee.fork(startFile, [], {
89+
env: {
90+
...process.env,
91+
ALWAYS_ON_SIGTERM: 'Y',
92+
},
93+
})
94+
.debug();
95+
await sleep(waitStart);
96+
child.proc.kill('SIGTERM');
97+
await sleep(2200);
98+
if (process.platform !== 'win32') {
99+
// windows can't handle SIGTERM signal
100+
child.expect('stdout', /\[app-worker-\d\] receive signal SIGTERM, exiting with code:0/);
101+
child.expect('stdout', /\[app-worker-\d\] receive signal SIGTERM again, waiting for exit/);
102+
child.expect('stdout', /exit after 1000ms/);
103+
}
104+
child.expect('stdout', /\[app-worker-1\] exit with code:0/);
105+
child.expect('stdout', /\[app-worker-2\] exit with code:0/);
106+
});
85107
});
86108

87109
describe('child_process.fork', () => {
@@ -101,6 +123,28 @@ describe('test/index.test.ts', () => {
101123
child.expect('stderr', /\[test-child\] exit with code:110/);
102124
}
103125
});
126+
127+
it('should always listen sigterm work', async () => {
128+
const startFile = path.join(fixtures, 'master-sigterm.cjs');
129+
const child: any = coffee.fork(startFile, [], {
130+
env: {
131+
...process.env,
132+
ALWAYS_ON_SIGTERM: 'Y',
133+
},
134+
})
135+
.debug();
136+
await sleep(waitStart);
137+
// the worker exit by graceful-process
138+
child.proc.kill('SIGTERM');
139+
await sleep(2000);
140+
if (process.platform !== 'win32') {
141+
// windows can't handle SIGTERM signal
142+
child.expect('stdout', /\[test-child\] receive signal SIGTERM, exiting with code:0/);
143+
child.expect('stdout', /\[test-child\] receive signal SIGTERM again, waiting for exit/);
144+
child.expect('stdout', /exit after 1000ms/);
145+
child.expect('stdout', /\[test-child\] exit with code:0/);
146+
}
147+
});
104148
});
105149

106150
describe.skip('child_process.spawn', () => {

0 commit comments

Comments
 (0)