0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-27 22:49:56 -05:00
ghost/core/server/ghost-server.js
Naz 5c4e884070 Added support for graceful job shutdown and worker threads
refs #12402

- With bumped version of job-manager it offloads job procesing into separate worker thread. Having jobs run out of main Ghost process even loop allows for safe job execution, which does not block Ghost from serving requests or performing other functions without a delay
- Added experimental data access to 'testmode' jobs. This should serve as an illustration of how to access data from the job layer
2020-11-23 17:17:49 +13:00

324 lines
11 KiB
JavaScript

// # Ghost Server
// Handles the creation of an HTTP Server for Ghost
const debug = require('ghost-ignition').debug('server');
const Promise = require('bluebird');
const fs = require('fs-extra');
const path = require('path');
const _ = require('lodash');
const config = require('../shared/config');
const urlUtils = require('./../shared/url-utils');
const errors = require('@tryghost/errors');
const {events, i18n} = require('./lib/common');
const logging = require('../shared/logging');
const moment = require('moment');
const bootstrapSocket = require('@tryghost/bootstrap-socket');
const stoppable = require('stoppable');
/**
* ## GhostServer
*/
class GhostServer {
/**
* @constructor
* @param {Object} rootApp - parent express instance
*/
constructor(rootApp) {
this.rootApp = rootApp;
this.httpServer = null;
// Expose config module for use externally.
this.config = config;
// Tasks that should be run before the server exits
this.cleanupTasks = [];
}
/**
* ## Public API methods
*
* ### Start
* Starts the ghost server listening on the configured port.
* Alternatively you can pass in your own express instance and let Ghost
* start listening for you.
* @param {Object} externalApp - Optional express app instance.
* @return {Promise} Resolves once Ghost has started
*/
start(externalApp) {
debug('Starting...');
const self = this;
const rootApp = externalApp ? externalApp : self.rootApp;
let socketConfig;
const socketValues = {
path: path.join(config.get('paths').contentPath, config.get('env') + '.socket'),
permissions: '660'
};
return new Promise(function (resolve, reject) {
if (Object.prototype.hasOwnProperty.call(config.get('server'), 'socket')) {
socketConfig = config.get('server').socket;
if (_.isString(socketConfig)) {
socketValues.path = socketConfig;
} else if (_.isObject(socketConfig)) {
socketValues.path = socketConfig.path || socketValues.path;
socketValues.permissions = socketConfig.permissions || socketValues.permissions;
}
// Make sure the socket is gone before trying to create another
try {
fs.unlinkSync(socketValues.path);
} catch (e) {
// We can ignore this.
}
self.httpServer = rootApp.listen(socketValues.path);
fs.chmod(socketValues.path, socketValues.permissions);
config.set('server:socket', socketValues);
} else {
self.httpServer = rootApp.listen(
config.get('server').port,
config.get('server').host
);
}
self.httpServer.on('error', function (error) {
let ghostError;
if (error.errno === 'EADDRINUSE') {
ghostError = new errors.GhostError({
message: i18n.t('errors.httpServer.addressInUse.error'),
context: i18n.t('errors.httpServer.addressInUse.context', {port: config.get('server').port}),
help: i18n.t('errors.httpServer.addressInUse.help')
});
} else {
ghostError = new errors.GhostError({
message: i18n.t('errors.httpServer.otherError.error', {errorNumber: error.errno}),
context: i18n.t('errors.httpServer.otherError.context'),
help: i18n.t('errors.httpServer.otherError.help')
});
}
reject(ghostError);
});
self.httpServer.on('listening', function () {
debug('...Started');
self._logStartMessages();
// Debug logs output in testmode only
if (config.get('server:testmode')) {
// This is horrible and very temporary
const jobService = require('./services/jobs');
// Output how many connections are open every 5 seconds
const connectionInterval = setInterval(() => self.httpServer.getConnections(
(err, connections) => logging.warn(`${connections} connections currently open`)
), 5000);
// Output a notice when the server closes
self.httpServer.on('close', function () {
clearInterval(connectionInterval);
logging.warn('Server has fully closed');
});
// Output job queue length every 5 seconds
setInterval(() => {
logging.warn(`${jobService.queue.length()} jobs in the queue. Idle: ${jobService.queue.idle()}`);
logging.warn(`${Object.keys(jobService.bree.workers).length} workers registered. Scheduled jobs: ${Object.keys(jobService.bree.intervals).length}`);
}, 5000);
}
return GhostServer.announceServerReadiness()
.finally(() => {
resolve(self);
});
});
stoppable(self.httpServer, config.get('server:shutdownTimeout'));
// ensure that Ghost exits correctly on Ctrl+C and SIGTERM
process
.removeAllListeners('SIGINT').on('SIGINT', self.shutdown.bind(self))
.removeAllListeners('SIGTERM').on('SIGTERM', self.shutdown.bind(self));
});
}
/**
* ### Shutdown
* Stops the server, handles cleanup and exits the process = a full shutdown
* Called on SIGINT or SIGTERM
*/
async shutdown() {
try {
logging.warn(i18n.t('notices.httpServer.ghostIsShuttingDown'));
await this.stop();
process.exit(0);
} catch (error) {
logging.error(error);
process.exit(-1);
}
}
/**
* ### Stop
* Stops the server & handles cleanup, but does not exit the process
* Used in tests for quick start/stop actions
* Called by shutdown to handle server stop and cleanup before exiting
* @returns {Promise} Resolves once Ghost has stopped
*/
async stop() {
// If we never fully started, there's nothing to stop
if (this.httpServer === null) {
return;
}
try {
// We stop the server first so that no new long running requests or processes can be started
await this._stopServer();
// Do all of the cleanup tasks
await this._cleanup();
} finally {
// Wrap up
events.emit('server.stop');
this.httpServer = null;
this._logStopMessages();
}
}
/**
* ### Hammertime
* To be called after `stop`
*/
async hammertime() {
logging.info(i18n.t('notices.httpServer.cantTouchThis'));
}
registerCleanupTask(task) {
this.cleanupTasks.push(task);
}
/**
* ### Stop Server
* Does the work of stopping the server using stoppable
* This handles closing connections:
* - New connections are rejected
* - Idle connections are closed immediately
* - Active connections are allowed to complete in-flight requests before being closed
*
* If server.shutdownTimeout is reached, requests are terminated in-flight
*/
async _stopServer() {
return new Promise((resolve, reject) => {
this.httpServer.stop((err, status) => (err ? reject(err) : resolve(status)));
});
}
async _cleanup() {
// Wait for all cleanup tasks to finish
await Promise
.all(this.cleanupTasks.map(task => task()));
}
_onShutdownComplete() {
// Wrap up
events.emit('server.stop');
this.httpServer = null;
this._logStopMessages();
}
/**
* ### Log Start Messages
*/
_logStartMessages() {
logging.info(i18n.t('notices.httpServer.ghostIsRunningIn', {env: config.get('env')}));
if (config.get('env') === 'production') {
logging.info(i18n.t('notices.httpServer.yourBlogIsAvailableOn', {url: urlUtils.urlFor('home', true)}));
} else {
logging.info(i18n.t('notices.httpServer.listeningOn', {
host: config.get('server').socket || config.get('server').host,
port: config.get('server').port
}));
logging.info(i18n.t('notices.httpServer.urlConfiguredAs', {url: urlUtils.urlFor('home', true)}));
}
logging.info(i18n.t('notices.httpServer.ctrlCToShutDown'));
}
/**
* ### Log Stop Messages
* Private / internal API
*/
_logStopMessages() {
logging.warn(i18n.t('notices.httpServer.ghostHasShutdown'));
// Extra clear message for production mode
if (config.get('env') === 'production') {
logging.warn(i18n.t('notices.httpServer.yourBlogIsNowOffline'));
}
// Always output uptime
logging.warn(
i18n.t('notices.httpServer.ghostWasRunningFor'),
moment.duration(process.uptime(), 'seconds').humanize()
);
}
}
module.exports = GhostServer;
/**
* We call announce server readiness when the server is ready
* When the server is started, but not ready, it is only able to serve 503s
*
* If the server isn't able to reach readiness, announceServerReadiness is called with an error
* A status message, any error, and debug info are all passed to managing processes via IPC and the bootstrap socket
*/
let announceServerReadinessCalled = false;
const debugInfo = {
versions: process.versions,
platform: process.platform,
arch: process.arch,
release: process.release
};
module.exports.announceServerReadiness = function (error = null) {
// If we already announced readiness, we should not do it again
if (announceServerReadinessCalled) {
return Promise.resolve();
}
// Mark this function as called
announceServerReadinessCalled = true;
// Build our message
// - if there's no error then the server is ready
let message = {
started: true,
debug: debugInfo
};
// - if there's an error then the server is not ready, include the errors
if (error) {
message.started = false;
message.error = error;
} else {
events.emit('server.start');
}
// CASE: IPC communication to the CLI for local process manager
if (process.send) {
process.send(message);
}
// CASE: use bootstrap socket to communicate with CLI for systemd
let socketAddress = config.get('bootstrap-socket');
if (socketAddress) {
return bootstrapSocket.connectAndSend(socketAddress, logging, message);
}
return Promise.resolve();
};