Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface Options {
label?: string;
timeout?: number;
beforeExit?: BeforeExit;
sigterm?: 'always' | 'once';
}

export function graceful(options: Options = {}) {
Expand Down Expand Up @@ -43,10 +44,23 @@ export function graceful(options: Options = {}) {

// https://github.com/eggjs/egg-cluster/blob/master/lib/agent_worker.js#L35
// exit gracefully
process.once('SIGTERM', () => {
printLogLevels.info && logger.info('[%s] receive signal SIGTERM, exiting with code:0', label);
exit(0);
});
if (options.sigterm === 'always') {
let called = false;
process.on('SIGTERM', () => {
if (called) {
printLogLevels.info && logger.info('[%s] receive signal SIGTERM again, waiting for exit', label);
return;
}
called = true;
printLogLevels.info && logger.info('[%s] receive signal SIGTERM, exiting with code:0', label);
exit(0);
});
} else {
process.once('SIGTERM', () => {
printLogLevels.info && logger.info('[%s] receive signal SIGTERM, exiting with code:0', label);
exit(0);
});
}

process.once('exit', code => {
const level = code === 0 ? 'info' : 'error';
Expand Down
12 changes: 10 additions & 2 deletions test/fixtures/child.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,19 @@ http.createServer((req, res) => {
}).listen(8000);

console.log(`Worker ${process.pid} started`);
graceful({
const options = {
logger: console,
label: 'test-child',
logLevel: process.env.NODE_LOG_LEVEL,
});
};
if (process.env.ALWAYS_ON_SIGTERM) {
options.sigterm = 'always';
options.beforeExit = async () => {
await new Promise(r => setTimeout(r, 1000));
console.log('exit after 1000ms');
};
}
graceful(options);

// run again should work
graceful();
32 changes: 25 additions & 7 deletions test/fixtures/cluster.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,22 @@ if (cluster.isMaster) {
});

process.once('SIGTERM', () => {
for (const id in cluster.workers) {
cluster.workers[id].process.kill('SIGTERM');
const killWorkers = () => {
for (const id in cluster.workers) {
cluster.workers[id].process.kill('SIGTERM');
}
};

killWorkers();

if (process.env.ALWAYS_ON_SIGTERM) {
setTimeout(killWorkers, 50);
setTimeout(() => process.exit(0), 2100);
} else {
setTimeout(() => {
process.exit(0);
}, 100);
}
setTimeout(() => {
process.exit(0);
}, 100);
});
} else {
// Workers can share any TCP connection
Expand All @@ -35,8 +45,16 @@ if (cluster.isMaster) {

console.log(`Worker ${process.pid} started`);
const { graceful } = require('../..');
graceful({
const options = {
label: 'app-worker-' + cluster.worker.id,
logLevel: process.env.NODE_LOG_LEVEL,
});
};
if (process.env.ALWAYS_ON_SIGTERM) {
options.sigterm = 'always';
options.beforeExit = async () => {
await new Promise(r => setTimeout(r, 1000));
console.log('exit after 1000ms');
};
}
graceful(options);
}
15 changes: 15 additions & 0 deletions test/fixtures/master-sigterm.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const childProcess = require('node:child_process');
const path = require('node:path');

const childFile = path.join(__dirname, 'child.cjs');

const child = childProcess.fork(childFile);

process.once('SIGTERM', () => {
child.kill('SIGTERM');
setTimeout(() => child.kill('SIGTERM'), 50);
setTimeout(() => process.exit(0), 1500);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider passing the ALWAYS_ON_SIGTERM environment variable to the child process.

Since the test in index.test.ts sets this environment variable and the cluster implementation uses it, this script should probably propagate it to ensure consistent behavior.

-const child = childProcess.fork(childFile);
+const child = childProcess.fork(childFile, [], {
+  env: {
+    ...process.env,
+    ALWAYS_ON_SIGTERM: process.env.ALWAYS_ON_SIGTERM,
+  },
+});

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +9 to +13
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider using persistent SIGTERM listener when ALWAYS_ON_SIGTERM is set.

Since this PR adds support for always listening to SIGTERM, this test fixture should demonstrate that behavior when the environment variable is set.

-process.once('SIGTERM', () => {
+const sigtermHandler = () => {
   child.kill('SIGTERM');
   setTimeout(() => child.kill('SIGTERM'), 50);
   setTimeout(() => process.exit(0), 1500);
-});
+};
+
+if (process.env.ALWAYS_ON_SIGTERM === 'Y') {
+  process.on('SIGTERM', sigtermHandler);
+} else {
+  process.once('SIGTERM', sigtermHandler);
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
process.once('SIGTERM', () => {
child.kill('SIGTERM');
setTimeout(() => child.kill('SIGTERM'), 50);
setTimeout(() => process.exit(0), 1500);
});
const sigtermHandler = () => {
child.kill('SIGTERM');
setTimeout(() => child.kill('SIGTERM'), 50);
setTimeout(() => process.exit(0), 1500);
};
if (process.env.ALWAYS_ON_SIGTERM === 'Y') {
process.on('SIGTERM', sigtermHandler);
} else {
process.once('SIGTERM', sigtermHandler);
}


console.log('master fork %s done', childFile);
44 changes: 44 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,28 @@ describe('test/index.test.ts', () => {
// no exit event on coverage mode
// child.expect('stdout', /worker \d+ died, code 0, signal null/);
});

it('should always listen sigterm work', async () => {
const startFile = path.join(fixtures, 'cluster.cjs');
const child: any = coffee.fork(startFile, [], {
env: {
...process.env,
ALWAYS_ON_SIGTERM: 'Y',
},
})
.debug();
await sleep(waitStart);
child.proc.kill('SIGTERM');
await sleep(2200);
if (process.platform !== 'win32') {
// windows can't handle SIGTERM signal
child.expect('stdout', /\[app-worker-\d\] receive signal SIGTERM, exiting with code:0/);
child.expect('stdout', /\[app-worker-\d\] receive signal SIGTERM again, waiting for exit/);
child.expect('stdout', /exit after 1000ms/);
}
child.expect('stdout', /\[app-worker-1\] exit with code:0/);
child.expect('stdout', /\[app-worker-2\] exit with code:0/);
});
});

describe('child_process.fork', () => {
Expand All @@ -101,6 +123,28 @@ describe('test/index.test.ts', () => {
child.expect('stderr', /\[test-child\] exit with code:110/);
}
});

it('should always listen sigterm work', async () => {
const startFile = path.join(fixtures, 'master-sigterm.cjs');
const child: any = coffee.fork(startFile, [], {
env: {
...process.env,
ALWAYS_ON_SIGTERM: 'Y',
},
})
.debug();
await sleep(waitStart);
// the worker exit by graceful-process
child.proc.kill('SIGTERM');
await sleep(2000);
if (process.platform !== 'win32') {
// windows can't handle SIGTERM signal
child.expect('stdout', /\[test-child\] receive signal SIGTERM, exiting with code:0/);
child.expect('stdout', /\[test-child\] receive signal SIGTERM again, waiting for exit/);
child.expect('stdout', /exit after 1000ms/);
child.expect('stdout', /\[test-child\] exit with code:0/);
}
});
});

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