tasks: add completed flag

in some cases, the tasks are setting percent to 100 and crashing later
This commit is contained in:
Girish Ramakrishnan
2025-07-16 15:22:00 +02:00
parent 54c2e670e1
commit b42be9899e
5 changed files with 31 additions and 11 deletions
@@ -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');
};
+1
View File
@@ -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,
+4 -1
View File
@@ -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);
});
});
+14 -9
View File
@@ -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) {
+2 -1
View File
@@ -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`);