Files
cloudron-box/src/scheduler.js

184 lines
6.0 KiB
JavaScript
Raw Normal View History

'use strict';
exports = module.exports = {
sync: sync
};
var appdb = require('./appdb.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
CronJob = require('cron').CronJob,
debug = require('debug')('box:src/scheduler'),
2015-10-19 22:42:13 -07:00
docker = require('./docker.js'),
2015-10-19 22:41:42 -07:00
paths = require('./paths.js'),
safe = require('safetydance'),
_ = require('underscore');
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
// appId -> { schedulerConfig (manifest), cronjobs, containerIds }
var gState = null; // null indicates that we will load state on first sync
2015-10-19 22:41:42 -07:00
function loadState() {
var state = safe.JSON.parse(safe.fs.readFileSync(paths.SCHEDULER_FILE, 'utf8'));
return state || { };
2015-10-19 22:41:42 -07:00
}
function saveState(state) {
2015-10-20 09:36:30 -07:00
// do not save cronJobs
var safeState = { };
for (var appId in state) {
safeState[appId] = {
schedulerConfig: state[appId].schedulerConfig,
containerIds: state[appId].containerIds
};
}
safe.fs.writeFileSync(paths.SCHEDULER_FILE, JSON.stringify(safeState, null, 4), 'utf8');
2015-10-19 22:41:42 -07:00
}
function sync(callback) {
assert(!callback || typeof callback === 'function');
callback = callback || NOOP_CALLBACK;
debug('Syncing');
if (gState === null) gState = loadState();
2015-10-19 22:41:42 -07:00
apps.getAll(function (error, allApps) {
if (error) return callback(error);
// stop tasks of apps that went away
var allAppIds = allApps.map(function (app) { return app.id; });
var removedAppIds = _.difference(Object.keys(gState), allAppIds);
2015-10-19 22:41:42 -07:00
async.eachSeries(removedAppIds, function (appId, iteratorDone) {
stopJobs(appId, gState[appId], iteratorDone);
2015-10-19 22:41:42 -07:00
}, function (error) {
2015-10-19 19:04:53 -07:00
if (error) debug('Error stopping jobs : %j', error);
gState = _.omit(gState, removedAppIds);
2015-10-19 22:41:42 -07:00
2015-10-19 19:04:53 -07:00
// start tasks of new apps
async.eachSeries(allApps, function (app, iteratorDone) {
var appState = gState[app.id] || null;
var schedulerConfig = app.manifest.addons.scheduler || null;
2015-10-20 00:02:25 -07:00
if (!appState && !schedulerConfig) return iteratorDone(); // nothing changed
if (appState && _.isEqual(appState.schedulerConfig, schedulerConfig)) return iteratorDone(); // nothing changed
stopJobs(app.id, appState, function (error) {
if (error) debug('Error stopping jobs for %s : %s', app.id, error.message);
if (!schedulerConfig) {
delete gState[app.id];
return iteratorDone();
}
gState[app.id] = {
schedulerConfig: schedulerConfig,
cronJobs: createCronJobs(app.id, schedulerConfig),
containerIds: { }
};
saveState(gState);
2015-10-19 19:04:53 -07:00
iteratorDone();
});
});
2015-10-19 22:41:42 -07:00
2015-10-19 19:04:53 -07:00
debug('Done syncing');
});
});
}
function killTask(containerId, callback) {
if (!containerId) return callback();
async.series([
docker.stopContainer.bind(null, containerId),
docker.deleteContainer.bind(null, containerId)
], function (error) {
if (error) debug('Failed to kill task with containerId %s : %s', containerId, error.message);
callback(error);
});
}
2015-10-19 22:41:42 -07:00
function stopJobs(appId, appState, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof appState, 'object');
debug('stopJobs for %s', appId);
if (!appState) return callback();
async.eachSeries(Object.keys(appState.schedulerConfig), function (taskName, iteratorDone) {
if (appState.cronJobs[taskName]) appState.cronJobs[taskName].stop(); // could be null across restarts
killTask(appState.containerIds[taskName], iteratorDone);
2015-10-19 22:41:42 -07:00
}, callback);
}
function createCronJobs(appId, schedulerConfig) {
2015-10-20 09:44:46 -07:00
assert.strictEqual(typeof appId, 'string');
assert(schedulerConfig && typeof schedulerConfig === 'object');
2015-10-20 09:44:46 -07:00
debug('creating cron jobs for app %s', appId);
2015-10-19 22:41:42 -07:00
var jobs = { };
Object.keys(schedulerConfig).forEach(function (taskName) {
var task = schedulerConfig[taskName];
2015-10-20 00:02:25 -07:00
debug('scheduling task for %s/%s @ 00 %s : %s', appId, taskName, task.schedule, task.command);
2015-10-19 22:41:42 -07:00
var cronJob = new CronJob({
cronTime: '00 ' + task.schedule, // at this point, the pattern has been validated
2015-10-19 22:41:42 -07:00
onTick: doTask.bind(null, appId, taskName),
start: true
});
jobs[taskName] = cronJob;
});
2015-10-19 22:41:42 -07:00
return jobs;
}
2015-10-19 22:41:42 -07:00
function doTask(appId, taskName, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof taskName, 'string');
assert(!callback || typeof callback === 'function');
callback = callback || NOOP_CALLBACK;
var appState = gState[appId];
2015-10-20 09:36:30 -07:00
debug('Executing task %s/%s', appId, taskName);
apps.get(appId, function (error, app) {
if (error) return callback(error);
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
debug('task %s skipped. app %s is not installed/running', taskName, app.id);
return callback();
}
2015-10-20 09:36:30 -07:00
if (appState.containerIds[taskName]) debug('task %s/%s is already running. killing it', appId, taskName);
killTask(appState.containerIds[taskName], function (error) {
2015-10-20 00:02:25 -07:00
if (error) return callback(error);
2015-10-19 22:41:42 -07:00
2015-10-20 09:36:30 -07:00
debug('Creating createSubcontainer for %s/%s : %s', app.id, taskName, gState[appId].schedulerConfig[taskName].command);
2015-10-20 00:02:25 -07:00
docker.createSubcontainer(app, [ '/bin/sh', '-c', gState[appId].schedulerConfig[taskName].command ], function (error, container) {
appState.containerIds[taskName] = container.id;
saveState(gState);
2015-10-20 00:02:25 -07:00
docker.startContainer(container.id, callback);
});
});
});
}