mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-22 02:11:44 +03:00
1882278b5b
- 🛠 add bunyan and prettyjson, remove morgan - ✨ add logging module - GhostLogger class that handles setup of bunyan - PrettyStream for stdout - ✨ config for logging - @TODO: testing level fatal? - ✨ log each request via GhostLogger (express middleware) - @TODO: add errors to output - 🔥 remove errors.updateActiveTheme - we can read the value from config - 🔥 remove 15 helper functions in core/server/errors/index.js - all these functions get replaced by modules: 1. logging 2. error middleware handling for html/json 3. error creation (which will be part of PR #7477) - ✨ add express error handler for html/json - one true error handler for express responses - contains still some TODO's, but they are not high priority for first implementation/integration - this middleware only takes responsibility of either rendering html responses or return json error responses - 🎨 use new express error handler in middleware/index - 404 and 500 handling - 🎨 return error instead of error message in permissions/index.js - the rule for error handling should be: if you call a unit, this unit should return a custom Ghost error - 🎨 wrap serve static module - rule: if you call a module/unit, you should always wrap this error - it's always the same rule - so the caller never has to worry about what comes back - it's always a clear error instance - in this case: we return our notfounderror if serve static does not find the resource - this avoid having checks everywhere - 🎨 replace usages of errors/index.js functions and adapt tests - use logging.error, logging.warn - make tests green - remove some usages of logging and throwing api errors -> because when a request is involved, logging happens automatically - 🐛 return errorDetails to Ghost-Admin - errorDetails is used for Theme error handling - 🎨 use 500er error for theme is missing error in theme-handler - 🎨 extend file rotation to 1w
221 lines
5.9 KiB
JavaScript
221 lines
5.9 KiB
JavaScript
var util = require('util'),
|
|
moment = require('moment'),
|
|
request = require('superagent'),
|
|
SchedulingBase = require(__dirname + '/SchedulingBase'),
|
|
logging = require(__dirname + '/../logging');
|
|
|
|
/**
|
|
* allJobs is a sorted list by time attribute
|
|
*/
|
|
function SchedulingDefault(options) {
|
|
SchedulingBase.call(this, options);
|
|
|
|
this.runTimeoutInMs = 1000 * 60 * 5;
|
|
this.offsetInMinutes = 10;
|
|
this.beforePingInMs = -50;
|
|
this.retryTimeoutInMs = 1000 * 5;
|
|
|
|
this.allJobs = {};
|
|
this.deletedJobs = {};
|
|
}
|
|
|
|
util.inherits(SchedulingDefault, SchedulingBase);
|
|
|
|
/**
|
|
* add to list
|
|
*/
|
|
SchedulingDefault.prototype.schedule = function (object) {
|
|
this._addJob(object);
|
|
};
|
|
|
|
/**
|
|
* remove from list
|
|
* add to list
|
|
*/
|
|
SchedulingDefault.prototype.reschedule = function (object) {
|
|
this._deleteJob({time: object.extra.oldTime, url: object.url});
|
|
this._addJob(object);
|
|
};
|
|
|
|
/**
|
|
* remove from list
|
|
* deletion happens right before execution
|
|
*/
|
|
SchedulingDefault.prototype.unschedule = function (object) {
|
|
this._deleteJob(object);
|
|
};
|
|
|
|
/**
|
|
* check if there are new jobs which needs to be published in the next x minutes
|
|
* because allJobs is a sorted list, we don't have to iterate over all jobs, just until the offset is too big
|
|
*/
|
|
SchedulingDefault.prototype.run = function () {
|
|
var self = this,
|
|
timeout = null;
|
|
|
|
timeout = setTimeout(function () {
|
|
var times = Object.keys(self.allJobs),
|
|
nextJobs = {};
|
|
|
|
times.every(function (time) {
|
|
if (moment(Number(time)).diff(moment(), 'minutes') <= self.offsetInMinutes) {
|
|
nextJobs[time] = self.allJobs[time];
|
|
delete self.allJobs[time];
|
|
return true;
|
|
}
|
|
|
|
// break!
|
|
return false;
|
|
});
|
|
|
|
clearTimeout(timeout);
|
|
self._execute(nextJobs);
|
|
|
|
// recursive!
|
|
self.run();
|
|
}, self.runTimeoutInMs);
|
|
};
|
|
|
|
/**
|
|
* each timestamp key entry can have multiple jobs
|
|
*/
|
|
SchedulingDefault.prototype._addJob = function (object) {
|
|
var timestamp = moment(object.time).valueOf(),
|
|
keys = [],
|
|
sortedJobs = {},
|
|
instantJob = {},
|
|
i = 0;
|
|
|
|
// CASE: should have been already pinged or should be pinged soon
|
|
if (moment(timestamp).diff(moment(), 'minutes') < this.offsetInMinutes) {
|
|
instantJob[timestamp] = [object];
|
|
this._execute(instantJob);
|
|
return;
|
|
}
|
|
|
|
// CASE: are there jobs already scheduled for the same time?
|
|
if (!this.allJobs[timestamp]) {
|
|
this.allJobs[timestamp] = [];
|
|
}
|
|
|
|
this.allJobs[timestamp].push(object);
|
|
|
|
keys = Object.keys(this.allJobs);
|
|
keys.sort();
|
|
|
|
for (i = 0; i < keys.length; i = i + 1) {
|
|
sortedJobs[keys[i]] = this.allJobs[keys[i]];
|
|
}
|
|
|
|
this.allJobs = sortedJobs;
|
|
};
|
|
|
|
SchedulingDefault.prototype._deleteJob = function (object) {
|
|
if (!object.time) {
|
|
return;
|
|
}
|
|
|
|
var deleteKey = object.url + '_' + moment(object.time).valueOf();
|
|
|
|
if (!this.deletedJobs[deleteKey]) {
|
|
this.deletedJobs[deleteKey] = [];
|
|
}
|
|
|
|
this.deletedJobs[deleteKey].push(object);
|
|
};
|
|
|
|
/**
|
|
* ping jobs
|
|
* setTimeout is not accurate, but we can live with that fact and use setImmediate feature to qualify
|
|
* we don't want to use process.nextTick, this would block any I/O operation
|
|
*/
|
|
SchedulingDefault.prototype._execute = function (jobs) {
|
|
var keys = Object.keys(jobs),
|
|
self = this;
|
|
|
|
keys.forEach(function (timestamp) {
|
|
var timeout = null,
|
|
diff = moment(Number(timestamp)).diff(moment());
|
|
|
|
// awake a little before
|
|
timeout = setTimeout(function () {
|
|
clearTimeout(timeout);
|
|
|
|
(function retry() {
|
|
var immediate = setImmediate(function () {
|
|
clearImmediate(immediate);
|
|
|
|
if (moment().diff(moment(Number(timestamp))) <= self.beforePingInMs) {
|
|
return retry();
|
|
}
|
|
|
|
var toExecute = jobs[timestamp];
|
|
delete jobs[timestamp];
|
|
|
|
toExecute.forEach(function (job) {
|
|
var deleteKey = job.url + '_' + moment(job.time).valueOf();
|
|
|
|
if (self.deletedJobs[deleteKey]) {
|
|
if (self.deletedJobs[deleteKey].length === 1) {
|
|
delete self.deletedJobs[deleteKey];
|
|
} else {
|
|
self.deletedJobs[deleteKey].pop();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
self._pingUrl(job);
|
|
});
|
|
});
|
|
})();
|
|
}, diff - 200);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* - if we detect to publish a post in the past (case blog is down), we add a force flag
|
|
*/
|
|
SchedulingDefault.prototype._pingUrl = function (object) {
|
|
var url = object.url,
|
|
time = object.time,
|
|
httpMethod = object.extra ? object.extra.httpMethod : 'PUT',
|
|
tries = object.tries || 0,
|
|
maxTries = 30,
|
|
req = request[httpMethod.toLowerCase()](url),
|
|
self = this, timeout;
|
|
|
|
if (moment(time).isBefore(moment())) {
|
|
if (httpMethod === 'GET') {
|
|
req.query('force=true');
|
|
} else {
|
|
req.send({
|
|
force: true
|
|
});
|
|
}
|
|
}
|
|
|
|
req.end(function (err, response) {
|
|
if (err) {
|
|
// CASE: post/page was deleted already
|
|
if (response && response.status === 404) {
|
|
return;
|
|
}
|
|
|
|
// CASE: blog is in maintenance mode, retry
|
|
if (response && response.status === 503 && tries < maxTries) {
|
|
timeout = setTimeout(function pingAgain() {
|
|
clearTimeout(timeout);
|
|
|
|
object.tries = tries + 1;
|
|
self._pingUrl(object);
|
|
}, self.retryTimeoutInMs);
|
|
}
|
|
|
|
logging.error(err);
|
|
}
|
|
});
|
|
};
|
|
|
|
module.exports = SchedulingDefault;
|