2026-02-14 09:53:14 +01:00
|
|
|
import assert from 'node:assert';
|
|
|
|
|
import BoxError from './boxerror.js';
|
|
|
|
|
import debugModule from 'debug';
|
|
|
|
|
import fs from 'node:fs';
|
|
|
|
|
import locks from './locks.js';
|
|
|
|
|
import path from 'node:path';
|
|
|
|
|
import paths from './paths.js';
|
|
|
|
|
import safe from 'safetydance';
|
2026-02-14 15:43:24 +01:00
|
|
|
import scheduler from './scheduler.js';
|
2026-02-14 09:53:14 +01:00
|
|
|
import tasks from './tasks.js';
|
|
|
|
|
|
|
|
|
|
const debug = debugModule('box:apptaskmanager');
|
|
|
|
|
|
2019-08-28 15:00:55 -07:00
|
|
|
|
2024-06-27 14:34:29 +02:00
|
|
|
const gActiveTasks = {}; // indexed by app id
|
|
|
|
|
const gPendingTasks = [];
|
2024-12-09 14:36:19 +01:00
|
|
|
let gStarted = false;
|
2019-08-28 15:00:55 -07:00
|
|
|
|
|
|
|
|
const TASK_CONCURRENCY = 3;
|
2024-12-09 14:36:19 +01:00
|
|
|
const DRAIN_TIMER_SECS = 1000;
|
2019-08-28 15:00:55 -07:00
|
|
|
|
2024-12-09 14:36:19 +01:00
|
|
|
let gDrainTimerId = null;
|
2019-08-28 15:00:55 -07:00
|
|
|
|
2024-12-07 14:35:45 +01:00
|
|
|
async function drain() {
|
|
|
|
|
debug(`drain: ${gPendingTasks.length} apptasks pending`);
|
2019-08-28 15:00:55 -07:00
|
|
|
|
2024-12-07 14:35:45 +01:00
|
|
|
for (let i = 0; i < gPendingTasks.length; i++) {
|
|
|
|
|
const space = Object.keys(gActiveTasks).length - TASK_CONCURRENCY;
|
|
|
|
|
if (space == 0) {
|
|
|
|
|
debug('At concurrency limit, cannot drain anymore');
|
|
|
|
|
break;
|
|
|
|
|
}
|
2019-08-28 15:00:55 -07:00
|
|
|
|
2024-12-07 14:35:45 +01:00
|
|
|
const { appId, taskId, options, onFinished } = gPendingTasks[i];
|
2019-08-28 15:00:55 -07:00
|
|
|
|
2025-07-18 13:22:33 +02:00
|
|
|
// acquire lock _before_ the task. this prevents the task failing if it can't get a lock
|
|
|
|
|
const [lockError] = await safe(locks.acquire(`${locks.TYPE_APP_TASK_PREFIX}${appId}`));
|
2024-12-07 14:35:45 +01:00
|
|
|
if (lockError) continue;
|
2020-11-25 22:16:20 -08:00
|
|
|
|
2024-12-07 14:35:45 +01:00
|
|
|
gPendingTasks.splice(i, 1);
|
|
|
|
|
gActiveTasks[appId] = {};
|
2019-08-28 15:00:55 -07:00
|
|
|
|
2024-12-07 14:35:45 +01:00
|
|
|
const logFile = path.join(paths.LOG_DIR, appId, 'apptask.log');
|
2020-08-19 18:23:44 +02:00
|
|
|
|
2024-12-07 14:35:45 +01:00
|
|
|
if (!fs.existsSync(path.dirname(logFile))) safe.fs.mkdirSync(path.dirname(logFile)); // ensure directory
|
|
|
|
|
|
2025-01-02 10:10:40 +01:00
|
|
|
scheduler.suspendAppJobs(appId);
|
2019-08-28 15:00:55 -07:00
|
|
|
|
2025-06-17 18:54:12 +02:00
|
|
|
// background
|
2026-03-07 22:30:43 +05:30
|
|
|
let taskError = null, taskResult = null;
|
2025-06-17 18:54:12 +02:00
|
|
|
tasks.startTask(taskId, Object.assign(options, { logFile }))
|
2026-03-07 22:30:43 +05:30
|
|
|
.then((result) => { taskResult = result; })
|
|
|
|
|
.catch((error) => { taskError = error; })
|
2025-07-17 14:54:41 +02:00
|
|
|
.finally(async () => {
|
2025-06-17 18:54:12 +02:00
|
|
|
delete gActiveTasks[appId];
|
2026-03-07 22:30:43 +05:30
|
|
|
await safe(onFinished(taskError, taskResult), { debug }); // hasPendingTasks() can now return false
|
2025-07-18 13:22:33 +02:00
|
|
|
await locks.release(`${locks.TYPE_APP_TASK_PREFIX}${appId}`);
|
2025-07-18 18:11:56 +02:00
|
|
|
await locks.releaseByTaskId(taskId);
|
2025-06-17 18:54:12 +02:00
|
|
|
scheduler.resumeAppJobs(appId);
|
2025-07-17 14:54:41 +02:00
|
|
|
});
|
2024-12-07 14:35:45 +01:00
|
|
|
}
|
2019-08-28 15:00:55 -07:00
|
|
|
|
2024-12-09 14:36:19 +01:00
|
|
|
gDrainTimerId = null;
|
|
|
|
|
if (gPendingTasks.length) gDrainTimerId = setTimeout(drain, DRAIN_TIMER_SECS); // check for released locks
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function start() {
|
|
|
|
|
assert.strictEqual(gDrainTimerId, null);
|
|
|
|
|
assert.strictEqual(gStarted, false);
|
|
|
|
|
|
|
|
|
|
debug('started');
|
|
|
|
|
gStarted = true;
|
|
|
|
|
|
|
|
|
|
if (gPendingTasks.length) gDrainTimerId = setTimeout(drain, DRAIN_TIMER_SECS);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function scheduleTask(appId, taskId, options, onFinished) {
|
|
|
|
|
assert.strictEqual(typeof appId, 'string');
|
|
|
|
|
assert.strictEqual(typeof taskId, 'string');
|
|
|
|
|
assert.strictEqual(typeof options, 'object');
|
|
|
|
|
assert.strictEqual(typeof onFinished, 'function');
|
|
|
|
|
|
|
|
|
|
if (appId in gActiveTasks) {
|
|
|
|
|
onFinished(new BoxError(BoxError.CONFLICT, `Task for ${appId} is already active`));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tasks.update(taskId, { percent: 1, message: gStarted ? 'Queued' : 'Waiting for platform to initialize' });
|
|
|
|
|
gPendingTasks.push({ appId, taskId, options, onFinished });
|
|
|
|
|
|
|
|
|
|
if (gStarted && !gDrainTimerId) gDrainTimerId = setTimeout(drain, DRAIN_TIMER_SECS);
|
2019-08-28 15:00:55 -07:00
|
|
|
}
|
2026-02-14 15:43:24 +01:00
|
|
|
|
2026-03-07 22:30:43 +05:30
|
|
|
function hasPendingTasks() {
|
|
|
|
|
return Object.keys(gActiveTasks).length > 0 || gPendingTasks.length > 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 15:43:24 +01:00
|
|
|
export default {
|
|
|
|
|
start,
|
2026-03-07 22:30:43 +05:30
|
|
|
scheduleTask,
|
|
|
|
|
hasPendingTasks
|
2026-02-14 15:43:24 +01:00
|
|
|
};
|