Files
cloudron-box/src/taskworker.js

123 lines
4.5 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
'use strict';
const apptask = require('./apptask.js'),
2021-07-14 11:07:19 -07:00
backupCleaner = require('./backupcleaner.js'),
backuptask = require('./backuptask.js'),
dashboard = require('./dashboard.js'),
database = require('./database.js'),
2021-08-13 17:22:28 -07:00
dns = require('./dns.js'),
dyndns = require('./dyndns.js'),
2019-10-25 15:58:11 -07:00
externalLdap = require('./externalldap.js'),
fs = require('fs'),
remove global lock Currently, the update/apptask/fullbackup/platformstart take a global lock and cannot run in parallel. This causes situations where when a user tries to trigger an apptask, it says "waiting for backup to finish..." etc The solution is to let them run in parallel. We need a lock at the app level as app operations running in parallel would be bad (tm). In addition, the update task needs a lock just for the update part. We also need multi-process locks. Running tasks as processes is core to our "kill" strategy. Various inter process locks were explored: * node's IPC mechanism with process.send(). But this only works for direct node.js children. taskworker is run via sudo and the IPC does not work. * File lock using O_EXCL. Basic ideas to create lock files. While file creation can be done atomically, it becomes complicated to clean up lock files when the tasks crash. We need a way to know what locks were held by the crashing task. flock and friends are not built-into node.js * sqlite/redis were options but introduce additional deps * Settled on MySQL based locking. Initial plan was to have row locks or table locks. Each row is a kind of lock. While implementing, it was found that we need many types of locks (and not just update lock and app locks). For example, we need locks for each task type, so that only one task type is active at a time. * Instead of rows, we can just lock table and have a json blob in it. This hit a road block that LOCK TABLE is per session and our db layer cannot handle this easily! i.e when issing two db.query() it might use two different connections from the pool. We have to expose the connection, release connection etc. * Next idea was atomic blob update of the blob checking if old blob was same. This approach, was finally refined into a version field. Phew!
2024-12-07 14:35:45 +01:00
locks = require('./locks.js'),
mailServer = require('./mailserver.js'),
net = require('net'),
2018-12-10 20:20:53 -08:00
reverseProxy = require('./reverseproxy.js'),
2021-07-12 23:35:30 -07:00
safe = require('safetydance'),
2022-10-12 10:26:21 +02:00
system = require('./system.js'),
tasks = require('./tasks.js'),
2022-04-15 17:40:46 -05:00
updater = require('./updater.js');
const TASKS = { // indexed by task type
2019-08-26 15:55:57 -07:00
app: apptask.run,
backup: backuptask.fullBackup,
2018-12-09 12:04:51 -08:00
update: updater.update,
checkCerts: reverseProxy.checkCerts,
2023-08-14 09:40:31 +05:30
prepareDashboardLocation: dashboard.prepareLocation,
2021-07-14 11:07:19 -07:00
cleanBackups: backupCleaner.run,
2019-10-25 15:58:11 -07:00
syncExternalLdap: externalLdap.sync,
changeMailLocation: mailServer.changeLocation,
2021-08-13 17:22:28 -07:00
syncDnsRecords: dns.syncDnsRecords,
syncDyndns: dyndns.sync,
2022-10-12 10:26:21 +02:00
updateDiskUsage: system.updateDiskUsage,
2018-12-10 21:05:46 -08:00
_identity: async (arg, progressCallback) => { progressCallback({}); return arg; },
_error: async (arg, progressCallback) => { progressCallback({}); throw new Error(`Failed for arg: ${arg}`); },
2022-04-28 18:03:36 -07:00
_crash: (arg) => { throw new Error(`Crashing for arg: ${arg}`); }, // the test looks for this debug string in the log file
2022-04-15 17:40:46 -05:00
_sleep: async (arg) => setTimeout(process.exit, arg)
};
if (process.argv.length !== 4) {
console.error('Pass the taskid and logfile as argument');
process.exit(1);
}
const taskId = process.argv[2];
const logFile = process.argv[3];
2021-08-30 22:01:34 -07:00
let logFd = null;
async function setupLogging() {
logFd = fs.openSync(logFile, 'a');
// we used to write using a stream before but it caches internally and there is no way to flush it when things crash
process.stdout.write = process.stderr.write = function (...args) {
const callback = typeof args[args.length-1] === 'function' ? args.pop() : function () {}; // callback is required for fs.write
fs.write.apply(fs, [logFd, ...args, callback]);
};
process.stdout.logFile = logFile; // used by update task
2021-08-30 22:01:34 -07:00
}
// happy eyeballs workaround. see box.js for detailed note
async function setupNetworking() {
net.setDefaultAutoSelectFamilyAttemptTimeout(2500);
}
2021-08-30 22:01:34 -07:00
// this is also used as the 'uncaughtException' handler which can only have synchronous functions
function exitSync(status) {
if (status.error) fs.write(logFd, status.error.stack + '\n', function () {});
fs.fsyncSync(logFd);
fs.closeSync(logFd);
process.exit(status.code);
}
// Main process starts here
2020-07-31 12:59:15 -07:00
const startTime = new Date();
2024-10-31 09:46:36 +01:00
async function main() {
2022-04-15 17:40:46 -05:00
try {
2024-10-31 09:46:36 +01:00
await setupLogging();
await setupNetworking();
await database.initialize();
remove global lock Currently, the update/apptask/fullbackup/platformstart take a global lock and cannot run in parallel. This causes situations where when a user tries to trigger an apptask, it says "waiting for backup to finish..." etc The solution is to let them run in parallel. We need a lock at the app level as app operations running in parallel would be bad (tm). In addition, the update task needs a lock just for the update part. We also need multi-process locks. Running tasks as processes is core to our "kill" strategy. Various inter process locks were explored: * node's IPC mechanism with process.send(). But this only works for direct node.js children. taskworker is run via sudo and the IPC does not work. * File lock using O_EXCL. Basic ideas to create lock files. While file creation can be done atomically, it becomes complicated to clean up lock files when the tasks crash. We need a way to know what locks were held by the crashing task. flock and friends are not built-into node.js * sqlite/redis were options but introduce additional deps * Settled on MySQL based locking. Initial plan was to have row locks or table locks. Each row is a kind of lock. While implementing, it was found that we need many types of locks (and not just update lock and app locks). For example, we need locks for each task type, so that only one task type is active at a time. * Instead of rows, we can just lock table and have a json blob in it. This hit a road block that LOCK TABLE is per session and our db layer cannot handle this easily! i.e when issing two db.query() it might use two different connections from the pool. We have to expose the connection, release connection etc. * Next idea was atomic blob update of the blob checking if old blob was same. This approach, was finally refined into a version field. Phew!
2024-12-07 14:35:45 +01:00
locks.setTaskId(taskId);
2024-10-31 09:46:36 +01:00
} catch (initError) {
2024-11-01 16:15:28 +01:00
console.error(initError);
return process.exit(50);
}
2024-10-31 09:46:36 +01:00
2024-11-01 16:15:28 +01:00
const debug = require('debug')('box:taskworker'); // require this here so that logging handler is already setup
2024-10-31 09:46:36 +01:00
2024-11-01 16:15:28 +01:00
process.on('SIGTERM', () => exitSync({ code: 0 })); // sent as timeout notification
2024-10-31 09:46:36 +01:00
2024-11-01 16:15:28 +01:00
// ensure we log task crashes with the task logs. neither console.log nor debug are sync for some reason
process.on('uncaughtException', (error) => exitSync({ error, code: 1 }));
2024-10-31 09:46:36 +01:00
2024-11-01 16:15:28 +01:00
debug(`Starting task ${taskId}. Logs are at ${logFile}`);
2024-10-31 09:46:36 +01:00
2024-11-01 16:15:28 +01:00
const [getError, task] = await safe(tasks.get(taskId));
if (getError) return exitSync({ error: getError, code: 50 });
if (!task) return exitSync({ error: new Error(`Task ${taskId} not found`), code: 50 });
2024-10-31 09:46:36 +01:00
2024-11-01 16:15:28 +01:00
async function progressCallback(progress) {
await safe(tasks.update(taskId, progress), { debug });
}
2024-10-31 09:46:36 +01:00
2024-11-01 16:15:28 +01:00
try {
2024-12-16 15:17:35 +01:00
debug(`Running task of type ${task.type}`);
2024-11-01 16:15:28 +01:00
const [runError, result] = await safe(TASKS[task.type].apply(null, task.args.concat(progressCallback)));
const progress = {
result: result || null,
error: runError ? JSON.parse(JSON.stringify(runError, Object.getOwnPropertyNames(runError))) : null
};
debug(`Task took ${(new Date() - startTime)/1000} seconds`);
await safe(tasks.setCompleted(taskId, progress));
exitSync({ error: runError, code: runError ? 50 : 0 });
} catch (error) {
exitSync({ error, code: 1 }); // do not call setCompleted() intentionally. the task code must be resilient enough to handle it
2021-07-12 23:35:30 -07:00
}
2024-10-31 09:46:36 +01:00
}
main();