Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(3062): fix Dockerfile not to use npm start, and add shutdown method #143

Merged
merged 4 commits into from
Mar 22, 2024
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
8 changes: 7 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,11 @@ RUN ln -s /usr/src/app/node_modules/screwdriver-store/config /config
# Expose the web service port
EXPOSE 80

# Add Tini
ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
ENTRYPOINT ["/tini", "--"]

# Run the service
CMD [ "npm", "start" ]
CMD [ "node", "./bin/server" ]
102 changes: 102 additions & 0 deletions plugins/shutdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
'use strict';

const joi = require('joi');
const logger = require('screwdriver-logger');

const tasks = {};
const taskSchema = joi.object({
taskname: joi.string().required(),
task: joi.func().required(),
timeout: joi.number().integer()
});

/**
* Function to return promise timeout or resolution
* whichever happens first
* @param {function} fn
* @param {string} timeout
*/
function promiseTimeout(fn, timeout) {
return Promise.race([
Promise.resolve(fn),
new Promise(resolve => {
setTimeout(() => {
resolve(`Promise timed out after ${timeout} ms`);
}, timeout);
})
]);
}

/**
* Hapi plugin to handle server graceful shutdown
* @method register
* @param {Hapi.Server} server
*/
const shutdownPlugin = {
name: 'shutdown',
async register(server) {
const terminationGracePeriod = parseInt(process.env.TERMINATION_GRACE_PERIOD, 10) || 30;

const taskHandler = async () => {
try {
await Promise.all(
Object.keys(tasks).map(async key => {
logger.info(`shutdown-> executing task ${key}`);
const item = tasks[key];

await item.task();
})
);

return Promise.resolve();
} catch (err) {
logger.error('shutdown-> Error in taskHandler %s', err);
throw err;
}
};

const gracefulStop = async () => {
try {
logger.info('shutdown-> gracefully shutting down server');
await server.stop({
timeout: 5000
});
process.exit(0);
} catch (err) {
logger.error('shutdown-> error in graceful shutdown %s', err);
process.exit(1);
}
};

const onSigterm = async () => {
try {
logger.info('shutdown-> got SIGTERM; running triggers before shutdown');
const res = await promiseTimeout(taskHandler(), terminationGracePeriod * 1000);

if (res) {
logger.error(res);
}
await gracefulStop();
} catch (err) {
logger.error('shutdown-> Error in plugin %s', err);
process.exit(1);
}
};

// catch sigterm signal
process.on('SIGTERM', onSigterm);

server.expose('handler', task => {
const res = taskSchema.validate(task);

if (res.error) {
return res.error;
}
tasks[task.taskname] = task;

return '';
});
}
};

module.exports = shutdownPlugin;
74 changes: 74 additions & 0 deletions test/plugins/shutdown.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
'use strict';

const chai = require('chai');
const { assert } = chai;
const hapi = require('@hapi/hapi');
const sinon = require('sinon');

sinon.assert.expose(assert, { prefix: '' });

describe('test shutdown plugin', () => {
let plugin;
let server;

beforeEach(async () => {
/* eslint-disable global-require */
plugin = require('../../plugins/shutdown');
/* eslint-enable global-require */

server = new hapi.Server({
port: 1234
});

await server.register({ plugin });
});

afterEach(() => {
server = null;
});

it('registers the plugin', () => {
assert.isOk(server.registrations.shutdown);
});
});

describe('test graceful shutdown', () => {
before(() => {
sinon.stub(process, 'exit');
});

after(() => {
process.exit.restore();
});

it('should catch the SIGTERM signal', () => {
/* eslint-disable global-require */
const plugin = require('../../plugins/shutdown');
/* eslint-enable global-require */
const options = {
terminationGracePeriod: 30
};
let stopCalled = false;
const server = new hapi.Server({
port: 1234
});

server.log = () => {};
server.root = {
stop: () => {
stopCalled = true;
}
};
server.expose = sinon.stub();

plugin.register(server, options, () => {});

process.exit(1);
process.exit.callsFake(() => {
assert.isTrue(stopCalled);
});
assert(process.exit.isSinonProxy);
sinon.assert.called(process.exit);
sinon.assert.calledWith(process.exit, 1);
});
});