mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-28 21:33:24 +03:00
Added comments for post scheduling
no issue - jsdoc - inline comments
This commit is contained in:
parent
5e33f0771d
commit
8f76827464
@ -6,18 +6,33 @@ const common = require('../../lib/common');
|
||||
const request = require('../../lib/request');
|
||||
|
||||
/**
|
||||
* allJobs is a sorted list by time attribute
|
||||
* @description Default post scheduling implementation.
|
||||
*
|
||||
* The default scheduler is used for all self-hosted blogs.
|
||||
* It is implemented with pure javascript (timers).
|
||||
*
|
||||
* "node-cron" did not perform well enough and we really just needed a simple time management.
|
||||
|
||||
* @param {Objec†} options
|
||||
* @constructor
|
||||
*/
|
||||
function SchedulingDefault(options) {
|
||||
SchedulingBase.call(this, options);
|
||||
|
||||
// NOTE: How often should the scheduler wake up?
|
||||
this.runTimeoutInMs = 1000 * 60 * 5;
|
||||
|
||||
// NOTE: An offset between now and past, which helps us choosing jobs which need to be executed soon.
|
||||
this.offsetInMinutes = 10;
|
||||
this.beforePingInMs = -50;
|
||||
this.retryTimeoutInMs = 1000 * 5;
|
||||
|
||||
// NOTE: Each scheduler implementation can decide whether to load scheduled posts on bootstrap or not.
|
||||
this.rescheduleOnBoot = true;
|
||||
|
||||
// NOTE: A sorted list of all scheduled jobs.
|
||||
this.allJobs = {};
|
||||
|
||||
this.deletedJobs = {};
|
||||
this.isRunning = false;
|
||||
}
|
||||
@ -25,15 +40,40 @@ function SchedulingDefault(options) {
|
||||
util.inherits(SchedulingDefault, SchedulingBase);
|
||||
|
||||
/**
|
||||
* add to list
|
||||
* @description Add a new job to the scheduler.
|
||||
*
|
||||
* A new job get's added when the post scheduler module receives a new model event e.g. "post.scheduled".
|
||||
*
|
||||
* @param {Object} object
|
||||
* {
|
||||
* time: [Number] A unix timestamp
|
||||
* url: [String] The full post/page API url to publish it.
|
||||
* extra: {
|
||||
* httpMethod: [String] The method of the target API endpoint.
|
||||
* oldTime: [Number] The previous published time.
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
SchedulingDefault.prototype.schedule = function (object) {
|
||||
this._addJob(object);
|
||||
};
|
||||
|
||||
/**
|
||||
* remove from list
|
||||
* add to list
|
||||
* @description Remove & schedule a job.
|
||||
*
|
||||
* This function is useful if the model layer detects a rescheduling event.
|
||||
* Rescheduling means: scheduled -> update published at.
|
||||
* To be able to delete the previous job we need the old published time.
|
||||
*
|
||||
* @param {Object} object
|
||||
* {
|
||||
* time: [Number] A unix timestamp
|
||||
* url: [String] The full post/page API url to publish it.
|
||||
* extra: {
|
||||
* httpMethod: [String] The method of the target API endpoint.
|
||||
* oldTime: [Number] The previous published time.
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
SchedulingDefault.prototype.reschedule = function (object) {
|
||||
this._deleteJob({time: object.extra.oldTime, url: object.url});
|
||||
@ -41,22 +81,36 @@ SchedulingDefault.prototype.reschedule = function (object) {
|
||||
};
|
||||
|
||||
/**
|
||||
* remove from list
|
||||
* deletion happens right before execution
|
||||
* @description Unschedule a job.
|
||||
*
|
||||
* Unscheduling means: scheduled -> draft.
|
||||
*
|
||||
* @param {Object} object
|
||||
* {
|
||||
* time: [Number] A unix timestamp
|
||||
* url: [String] The full post/page API url to publish it.
|
||||
* extra: {
|
||||
* httpMethod: [String] The method of the target API endpoint.
|
||||
* oldTime: [Number] The previous published time.
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
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
|
||||
* @description "run" is executed from outside (see post-scheduling module)
|
||||
*
|
||||
* This function will ensure that the scheduler will be kept alive while the blog is running.
|
||||
* It will run recursively and checks if there are new jobs which need to be executed in the next X minutes.
|
||||
*/
|
||||
SchedulingDefault.prototype.run = function () {
|
||||
const self = this;
|
||||
let timeout = null,
|
||||
recursiveRun;
|
||||
|
||||
// NOTE: Ensure the scheduler never runs twice.
|
||||
if (this.isRunning) {
|
||||
return;
|
||||
}
|
||||
@ -68,6 +122,7 @@ SchedulingDefault.prototype.run = function () {
|
||||
const times = Object.keys(self.allJobs),
|
||||
nextJobs = {};
|
||||
|
||||
// CASE: We stop till the offset is too big. We are only interested in jobs which need get executed soon.
|
||||
times.every(function (time) {
|
||||
if (moment(Number(time)).diff(moment(), 'minutes') <= self.offsetInMinutes) {
|
||||
nextJobs[time] = self.allJobs[time];
|
||||
@ -90,7 +145,9 @@ SchedulingDefault.prototype.run = function () {
|
||||
};
|
||||
|
||||
/**
|
||||
* each timestamp key entry can have multiple jobs
|
||||
* @description Add the actual job to "allJobs".
|
||||
* @param {Object} object
|
||||
* @private
|
||||
*/
|
||||
SchedulingDefault.prototype._addJob = function (object) {
|
||||
let timestamp = moment(object.time).valueOf(),
|
||||
@ -101,7 +158,7 @@ SchedulingDefault.prototype._addJob = function (object) {
|
||||
|
||||
// CASE: should have been already pinged or should be pinged soon
|
||||
if (moment(timestamp).diff(moment(), 'minutes') < this.offsetInMinutes) {
|
||||
debug('Imergency job', object.url, moment(object.time).format('YYYY-MM-DD HH:mm:ss'));
|
||||
debug('Emergency job', object.url, moment(object.time).format('YYYY-MM-DD HH:mm:ss'));
|
||||
|
||||
instantJob[timestamp] = [object];
|
||||
this._execute(instantJob);
|
||||
@ -126,6 +183,15 @@ SchedulingDefault.prototype._addJob = function (object) {
|
||||
this.allJobs = sortedJobs;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Delete the job.
|
||||
*
|
||||
* Keep a list of deleted jobs because it can happen that a job is already part of the next execution list,
|
||||
* but it got deleted meanwhile.
|
||||
*
|
||||
* @param {Object} object
|
||||
* @private
|
||||
*/
|
||||
SchedulingDefault.prototype._deleteJob = function (object) {
|
||||
const {url, time} = object;
|
||||
|
||||
@ -144,9 +210,20 @@ SchedulingDefault.prototype._deleteJob = function (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
|
||||
* @description The "execute" function will receive the next jobs which need execution.
|
||||
*
|
||||
* Based on "offsetInMinutes" we figure out which jobs need execution and the "execute" function will
|
||||
* ensure that
|
||||
*
|
||||
* The advantage of having a two step system (a general runner and an executor) is:
|
||||
* - accuracy
|
||||
* - setTimeout is limited to 24,3 days
|
||||
*
|
||||
* The execution of "setTimeout" is never guaranteed, therefor we've optimised the execution by using "setImmediate".
|
||||
* The executor will put each job to sleep using `setTimeout` with a threshold of 70ms. And "setImmediate" is then
|
||||
* used to detect the correct moment to trigger the URL.
|
||||
|
||||
* We can't use "process.nextTick" otherwise we will block I/O operations.
|
||||
*/
|
||||
SchedulingDefault.prototype._execute = function (jobs) {
|
||||
const keys = Object.keys(jobs),
|
||||
@ -156,7 +233,7 @@ SchedulingDefault.prototype._execute = function (jobs) {
|
||||
let timeout = null,
|
||||
diff = moment(Number(timestamp)).diff(moment());
|
||||
|
||||
// awake a little before
|
||||
// NOTE: awake a little before...
|
||||
timeout = setTimeout(function () {
|
||||
clearTimeout(timeout);
|
||||
|
||||
@ -164,6 +241,7 @@ SchedulingDefault.prototype._execute = function (jobs) {
|
||||
let immediate = setImmediate(function () {
|
||||
clearImmediate(immediate);
|
||||
|
||||
// CASE: It's not the time yet...
|
||||
if (moment().diff(moment(Number(timestamp))) <= self.beforePingInMs) {
|
||||
return retry();
|
||||
}
|
||||
@ -171,10 +249,12 @@ SchedulingDefault.prototype._execute = function (jobs) {
|
||||
const toExecute = jobs[timestamp];
|
||||
delete jobs[timestamp];
|
||||
|
||||
// CASE: each timestamp can have multiple jobs
|
||||
toExecute.forEach(function (job) {
|
||||
const {url, time} = job;
|
||||
const deleteKey = `${url}_${moment(time).valueOf()}`;
|
||||
|
||||
// CASE: Was the job already deleted in the meanwhile...?
|
||||
if (self.deletedJobs[deleteKey]) {
|
||||
if (self.deletedJobs[deleteKey].length === 1) {
|
||||
delete self.deletedJobs[deleteKey];
|
||||
@ -194,7 +274,10 @@ SchedulingDefault.prototype._execute = function (jobs) {
|
||||
};
|
||||
|
||||
/**
|
||||
* - if we detect to publish a post in the past (case blog is down), we add a force flag
|
||||
* @description Ping the job URL.
|
||||
* @param {Object} object
|
||||
* @return {Promise}
|
||||
* @private
|
||||
*/
|
||||
SchedulingDefault.prototype._pingUrl = function (object) {
|
||||
const {url, time} = object;
|
||||
@ -214,9 +297,10 @@ SchedulingDefault.prototype._pingUrl = function (object) {
|
||||
}
|
||||
};
|
||||
|
||||
// CASE: If we detect to publish a post in the past (case blog is down), we add a force flag
|
||||
if (moment(time).isBefore(moment())) {
|
||||
if (httpMethod === 'GET') {
|
||||
// @todo: rename to searchParams when updating to Got v10
|
||||
// @TODO: rename to searchParams when updating to Got v10
|
||||
options.query = 'force=true';
|
||||
} else {
|
||||
options.body = JSON.stringify({force: true});
|
||||
|
@ -1,8 +1,20 @@
|
||||
const postScheduling = require(__dirname + '/post-scheduling');
|
||||
|
||||
/**
|
||||
* scheduling modules:
|
||||
* - post scheduling: publish posts/pages when scheduled
|
||||
* @description Initialise all scheduler modules.
|
||||
*
|
||||
* We currently only support post-scheduling: publish posts/pages when scheduled.
|
||||
*
|
||||
* @param {Object} options
|
||||
* {
|
||||
* schedulerUrl: [String] Remote scheduler domain.
|
||||
* active: [String] Name of the custom scheduler.
|
||||
* apiUrl: [String] Target Ghost API url.
|
||||
* internalPath: [String] Folder path where to find the default scheduler.
|
||||
* contentPath: [String] Folder path where to find custom schedulers.
|
||||
* }
|
||||
*
|
||||
* @TODO: Simplify the passed in options.
|
||||
*/
|
||||
exports.init = function init(options) {
|
||||
options = options || {};
|
||||
|
@ -6,23 +6,40 @@ const Promise = require('bluebird'),
|
||||
urlService = require('../../../services/url'),
|
||||
_private = {};
|
||||
|
||||
/**
|
||||
* @description Normalize model data into scheduler notation.
|
||||
* @param {Object} options
|
||||
* @return {Object}
|
||||
*/
|
||||
_private.normalize = function normalize(options) {
|
||||
const {object, apiUrl, client} = options;
|
||||
const {model, apiUrl, client} = options;
|
||||
|
||||
return {
|
||||
time: moment(object.get('published_at')).valueOf(),
|
||||
url: `${urlService.utils.urlJoin(apiUrl, 'schedules', 'posts', object.get('id'))}?client_id=${client.get('slug')}&client_secret=${client.get('secret')}`,
|
||||
// NOTE: The scheduler expects a unix timestmap.
|
||||
time: moment(model.get('published_at')).valueOf(),
|
||||
// @TODO: We are still using API v0.1
|
||||
url: `${urlService.utils.urlJoin(apiUrl, 'schedules', 'posts', model.get('id'))}?client_id=${client.get('slug')}&client_secret=${client.get('secret')}`,
|
||||
extra: {
|
||||
httpMethod: 'PUT',
|
||||
oldTime: object.previous('published_at') ? moment(object.previous('published_at')).valueOf() : null
|
||||
oldTime: model.previous('published_at') ? moment(model.previous('published_at')).valueOf() : null
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Load the client credentials for v0.1 API.
|
||||
*
|
||||
* @TODO: Remove when we drop v0.1. API v2 uses integrations.
|
||||
* @return {Promise}
|
||||
*/
|
||||
_private.loadClient = function loadClient() {
|
||||
return models.Client.findOne({slug: 'ghost-scheduler'}, {columns: ['slug', 'secret']});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Load all scheduled posts from database.
|
||||
* @return {Promise}
|
||||
*/
|
||||
_private.loadScheduledPosts = function () {
|
||||
const api = require('../../../api');
|
||||
return api.schedules.getScheduledPosts()
|
||||
@ -31,6 +48,11 @@ _private.loadScheduledPosts = function () {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Initialise post scheduling.
|
||||
* @param {Object} options
|
||||
* @return {*}
|
||||
*/
|
||||
exports.init = function init(options = {}) {
|
||||
const {apiUrl} = options;
|
||||
let adapter = null,
|
||||
@ -51,9 +73,11 @@ exports.init = function init(options = {}) {
|
||||
})
|
||||
.then((_adapter) => {
|
||||
adapter = _adapter;
|
||||
|
||||
if (!adapter.rescheduleOnBoot) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return _private.loadScheduledPosts();
|
||||
})
|
||||
.then((scheduledPosts) => {
|
||||
@ -61,8 +85,8 @@ exports.init = function init(options = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
scheduledPosts.forEach((object) => {
|
||||
adapter.reschedule(_private.normalize({object, apiUrl, client}));
|
||||
scheduledPosts.forEach((model) => {
|
||||
adapter.reschedule(_private.normalize({model, apiUrl, client}));
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
@ -72,22 +96,22 @@ exports.init = function init(options = {}) {
|
||||
common.events.onMany([
|
||||
'post.scheduled',
|
||||
'page.scheduled'
|
||||
], (object) => {
|
||||
adapter.schedule(_private.normalize({object, apiUrl, client}));
|
||||
], (model) => {
|
||||
adapter.schedule(_private.normalize({model, apiUrl, client}));
|
||||
});
|
||||
|
||||
common.events.onMany([
|
||||
'post.rescheduled',
|
||||
'page.rescheduled'
|
||||
], (object) => {
|
||||
adapter.reschedule(_private.normalize({object, apiUrl, client}));
|
||||
], (model) => {
|
||||
adapter.reschedule(_private.normalize({model, apiUrl, client}));
|
||||
});
|
||||
|
||||
common.events.onMany([
|
||||
'post.unscheduled',
|
||||
'page.unscheduled'
|
||||
], (object) => {
|
||||
adapter.unschedule(_private.normalize({object, apiUrl, client}));
|
||||
], (model) => {
|
||||
adapter.unschedule(_private.normalize({model, apiUrl, client}));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -4,6 +4,21 @@ const _ = require('lodash'),
|
||||
common = require('../../lib/common'),
|
||||
cache = {};
|
||||
|
||||
/**
|
||||
* @description Create the scheduling adapter.
|
||||
*
|
||||
* This utility helps us to:
|
||||
*
|
||||
* - validate the scheduling config
|
||||
* - cache the target adapter to ensure singletons
|
||||
* - ensure the adapter can be instantiated
|
||||
* - have a centralised error handling
|
||||
* - detect if the adapter is inherited from the base adapter
|
||||
* - detect if the adapter has implemented the required functions
|
||||
*
|
||||
* @param {Object} options
|
||||
* @return {Promise}
|
||||
*/
|
||||
exports.createAdapter = function (options) {
|
||||
options = options || {};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user