diff --git a/migrations/20250716130216-tasks-add-completed.js b/migrations/20250716130216-tasks-add-completed.js new file mode 100644 index 000000000..bfd220280 --- /dev/null +++ b/migrations/20250716130216-tasks-add-completed.js @@ -0,0 +1,10 @@ +'use strict'; + +exports.up = async function (db) { + await db.runSql('ALTER TABLE tasks ADD COLUMN completed BOOLEAN DEFAULT false'); + await db.runSql('UPDATE tasks SET completed=? WHERE percent=?', [ true, 100 ]); +}; + +exports.down = async function (db) { + await db.runSql('ALTER TABLE tasks DROP COLUMN completed'); +}; diff --git a/migrations/schema.sql b/migrations/schema.sql index 33735a857..24f57fb14 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -251,6 +251,7 @@ CREATE TABLE IF NOT EXISTS tasks( type VARCHAR(32) NOT NULL, argsJson TEXT, pending BOOLEAN DEFAULT true, + completed BOOLEAN DEFAULT false, percent INTEGER DEFAULT 0, message TEXT, errorJson TEXT, diff --git a/src/routes/test/tasks-test.js b/src/routes/test/tasks-test.js index 90c17c27d..2eef288f5 100644 --- a/src/routes/test/tasks-test.js +++ b/src/routes/test/tasks-test.js @@ -31,6 +31,7 @@ describe('Tasks API', function () { expect(response.body.active).to.be(false); // finished expect(response.body.success).to.be(true); expect(response.body.result).to.be('ping'); + expect(response.body.completed).to.be(true); expect(response.body.error).to.be(null); }); @@ -72,10 +73,11 @@ describe('Tasks API', function () { .query({ access_token: owner.token }); expect(response.status).to.equal(200); - expect(response.body.percent).to.be(100); + expect(response.body.percent).to.not.be(100); expect(response.body.active).to.be(false); // finished expect(response.body.success).to.be(false); expect(response.body.result).to.be(null); + expect(response.body.completed).to.be(true); expect(response.body.error.message).to.contain('stopped'); }); @@ -94,6 +96,7 @@ describe('Tasks API', function () { expect(response.body.tasks[0].active).to.be(false); // finished expect(response.body.tasks[0].success).to.be(true); // finished expect(response.body.tasks[0].result).to.be('ping'); + expect(response.body.tasks[0].completed).to.be(true); expect(response.body.tasks[0].error).to.be(null); }); }); diff --git a/src/tasks.js b/src/tasks.js index 8280713b9..fa2cdd301 100644 --- a/src/tasks.js +++ b/src/tasks.js @@ -61,7 +61,7 @@ let gTasks = {}; // indexed by task id const START_TASK_CMD = path.join(__dirname, 'scripts/starttask.sh'); const STOP_TASK_CMD = path.join(__dirname, 'scripts/stoptask.sh'); -const TASKS_FIELDS = [ 'id', 'type', 'argsJson', 'percent', 'pending', 'message', 'errorJson', 'creationTime', 'resultJson', 'ts' ]; +const TASKS_FIELDS = [ 'id', 'type', 'argsJson', 'percent', 'pending', 'completed', 'message', 'errorJson', 'creationTime', 'resultJson', 'ts' ]; function postProcess(task) { assert.strictEqual(typeof task, 'object'); @@ -72,6 +72,9 @@ function postProcess(task) { task.id = String(task.id); + task.pending = !!task.pending; + task.completed = !!task.completed; + task.result = JSON.parse(task.resultJson); delete task.resultJson; @@ -84,14 +87,14 @@ function postProcess(task) { function updateStatus(result) { assert.strictEqual(typeof result, 'object'); + // result.pending - task is scheduled to run at some point + // result.completed - task finished and exit/crash was cleanly collected result.running = !!gTasks[result.id]; // running means actively running - result.active = result.running || !!result.pending; // active mean task is 'done' or not. at this point, clients can stop polling this task. - - // we rely on 'percent' to determine success. maybe this can become a db field - result.success = result.percent === 100 && !result.error; + result.active = result.running || result.pending; // active mean task is 'done' or not. at this point, clients can stop polling this task. + result.success = result.completed && !result.error; // if task has completed without an error // the error in db will be empty if we didn't get a chance to handle task exit - if (!result.active && result.percent !== 100 && !result.error) { + if (!result.active && !result.completed && !result.error) { result.error = { message: 'Task was stopped because the server restarted or crashed', code: exports.ECRASHED }; } @@ -104,6 +107,8 @@ async function get(id) { const result = await database.query(`SELECT ${TASKS_FIELDS} FROM tasks WHERE id = ?`, [ id ]); if (result.length === 0) return null; + console.log(result[0]); + return updateStatus(postProcess(result[0])); } @@ -135,7 +140,7 @@ async function setCompleted(id, task) { debug(`setCompleted - ${id}: ${JSON.stringify(task)}`); - await update(id, Object.assign({ percent: 100 }, task)); + await update(id, Object.assign({ completed: true }, task)); } async function setCompletedByType(type, task) { @@ -181,7 +186,7 @@ async function startTask(id, options) { const [getError, task] = await safe(get(id)); let taskError = null; - if (!getError && task.percent !== 100) { // taskworker crashed or was killed by us + if (!getError && !task.completed) { // taskworker crashed or was killed by us if (code === 0) { taskError = { message: `Task ${id} ${timedOut ? 'timed out' : 'stopped'}` , @@ -289,7 +294,7 @@ async function getLogs(task, options) { // removes all fields that are strictly private and should never be returned by API calls function removePrivateFields(task) { - return _.pick(task, ['id', 'type', 'percent', 'message', 'error', 'running', 'active', 'creationTime', 'result', 'ts', 'success']); + return _.pick(task, ['id', 'type', 'percent', 'message', 'error', 'running', 'completed', 'active', 'creationTime', 'result', 'ts', 'success']); } async function del(id) { diff --git a/src/taskworker.js b/src/taskworker.js index af37a9ca3..b387522ae 100755 --- a/src/taskworker.js +++ b/src/taskworker.js @@ -110,7 +110,8 @@ async function main() { 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 + error: runError ? JSON.parse(JSON.stringify(runError, Object.getOwnPropertyNames(runError))) : null, + percent: 100 }; debug(`Task took ${(new Date() - startTime)/1000} seconds`);