Skip to main content
3 of 4
added 279 characters in body

Concurrent fire and forget async task queue

I had to do a lot of IO stuff on my server running Node JS where I did not really care about the result of the operations. The list of incoming tasks could be huge, so I had to limit the number of tasks that could run at the same time. I try to avoid third party libraries wherever I can, so I made my own method.

It seems to work fine, but could I still improve something?

const pending_tasks = [];
const running_tasks = [];
const identifiers = [];

let concurrency = 1;

/*
    Set how many tasks will run concurrently.
*/
function set_concurrency (max) {
    concurrency = max;
}

/*
    Check whether a task with the specified id is already in the queue.
    Only tasks that were added to the queue with their id parameter defined can be checked against.
*/
function in_queue (id) {
    if (!id) { return false; }

    return identifiers.includes(id);
};

/*
    Add a task to the list of running tasks.
*/
function fired (id, task) {
    if (id) { identifiers.push(id); }

    running_tasks.push(task);
}

/*
    Delete a task from the list of running tasks.
*/
function finished (id, task) {
    if (id) { identifiers.splice(identifiers.indexOf(id), 1); }

    running_tasks.splice(running_tasks.indexOf(task), 1);
}

/*
    Add a task to the queue.
*/
function pending (id, task, parameters) {
    pending_tasks.push({
        'id': id,
        'task': task,
        'parameters': parameters
    });
}

/*
    Clean up and queue logic after a task has finished.
*/
function next_task (id, task) {
    /*
        Delete the task from the queue.
    */
    finished(id, task);

    /*
        If there are still pending tasks in the queue ...
    */
    if (pending_tasks.length > 0) {
        /*
            ... and there are less tasks running than can run concurrently ...
        */
        if (running_tasks.length < concurrency) {
            /*
                ... get another task from the pending list ...
            */
            const pending_task = pending_tasks.shift();

            /*
                ... and start it.
            */
            fire_and_forget(
                pending_task['id'],
                pending_task['task'], 
                pending_task['parameters']
            );
        }
    }
}

/*
    Add tasks to the queue and have them execute without caring about the result.
    You should set the number of concurrent tasks with the 'set_concurreny (max)' function, if
    you wish for more than one task to be executed at a time.
    If a specific task should be unique and only have one instance of itself running, give it
    an id and check whether it is already in the queue with the 'in_queue (id) function.'
*/
async function fire_and_forget (id, task, parameters) {
    /*
        If an id is defined, check whether it is already in the queue.
        You should check this yourself before executing this function to avoid an exception.
    */
    if (in_queue(id)) { throw 'Task with id \'' + id + '\' is already in queue.'; }

    /*
        If there are less tasks running than can run concurrently, start this task immediately.
    */
    if (running_tasks.length < concurrency) {
        /*
            Create a promise and apply the parameters to the task to start it.
        */
        const promised_task = new Promise(function (resolve, reject) {
            task.apply(this, parameters)
            .then(() => { resolve(); })
            .catch(() => { reject(); });
        });

        /*
            The task has finished.
        */
        promised_task.then(() => {
            next_task(id, promised_task);
        }).catch(() => {
            next_task(id, promised_task);
        });

        /*
            Add the task to the list of running tasks.
        */
        fired(id, promised_task);
    }
    else {
        /*
            Add the task to the queue.
        */
        pending(id, task, parameters);
    }
};

module.exports.set_concurrency = set_concurrency;
module.exports.in_queue = in_queue;
module.exports.fire_and_forget = fire_and_forget;






/*
    TEST SCENARIO --- Remove code beneath for production
*/

// Dummy task
async function long_write_task(subject, body) {
    await new Promise((resolve) => setTimeout(resolve, 1000));

    console.log([subject, body]);
}

// Set concurrency to 3 tasks.
set_concurrency(3);

// Start 10 tasks.
for(let i = 0; i < 10; i++){
    // Dummy parameter list.
    const parameters = ['subject ' + i, 'body ' + i];

    // Check if id already queued.
    // Can't be if we just assign the loop iteration number as id, but is best practice.
    // If you don't care about running an identical task twice, leave id 'undefined'.
    if (!in_queue(i)) {
        fire_and_forget(i, long_write_task, parameters);
    }
}

EDITED CODE

const enqueued_ids = [];
const enqueued_tasks = [];

let running = 0;
let concurrency = 10;

/*
    Set how many tasks will run concurrently.
*/
function set_concurrency (max) {
    concurrency = max;
};

/*
    Check whether a task with the specified id is already in the queue.
*/
function in_queue (id) {
    if ([undefined, null].includes(id)) { return false; }

    return enqueued_ids.includes(id);
};

/*
    Add the id for a task to the list of enqueued ids.
*/
function enqueue_id (id) {
    if ([undefined, null].includes(id)) { return; }

    enqueued_ids.push(id);
};

/*
    Remove the id after the task has finished.
*/
function dequeue_id (id) {
    if ([undefined, null].includes(id)) { return; }

    enqueued_ids.splice(enqueued_ids.indexOf(id), 1);
};

/*
    Start a new task from the queue.
*/
function fire_task () {
    /*
        If there are pending tasks in the queue ...
    */
    if (enqueued_tasks.length > 0) {
        /*
            ... and there are less tasks running than can run concurrently ...
        */
        if (running < concurrency) {
            /*
                ... get a task from the queue ...
            */
            const enqueued_task = enqueued_tasks.shift();
            const id = enqueued_task['id'];
            const task = enqueued_task['task'];

            /*
                ... and start it.
            */
            const fired_task = new Promise(function (resolve, reject) {
                running++;

                task()
                .then(() => { resolve(); })
                .catch(() => { reject(); });
            });

            /*
                The task has finished.
            */
            fired_task.then(() => {
                running--;

                dequeue_id(id);
                fire_task();
            }).catch(() => {
                running--;

                dequeue_id(id);
                fire_task();
            });
        }
    }
};

/*
    Add tasks to the queue and have them execute without caring about the result.
    You should set the number of concurrent tasks with the 'set_concurreny (max)' function, if
    you wish for more than one task to be executed at a time.
    If a specific task should be unique and only have one instance of itself running, give it
    an id and check whether it is already in the queue with the 'in_queue (id)' function.
*/
async function fire_and_forget (id, task) {
    /*
        If an id is defined, check whether it is already in the queue.
        You should check this yourself before executing this function to avoid an exception.
    */
    if (in_queue(id)) { throw 'Task with id \'' + id + '\' is already in queue.'; }

    /*
        If the id is valid, add it to the list of enqueued ids.
    */
    enqueue_id(id);

    /*
        Bind the parameters to the task.
    */
    const bound_task = task.bind.apply(task, [null].concat([].slice.call(arguments).slice(2)));

    /*
        Add the task to the queue.
    */
    enqueued_tasks.push({
        'id': id,
        'task': bound_task
    });

    /*
        Start a new task from the queue.
    */
    fire_task();
};

module.exports.set_concurrency = set_concurrency;
module.exports.in_queue = in_queue;
module.exports.fire_and_forget = fire_and_forget;

/*
    TEST SCENARIO --- Remove code beneath for production
*/

// Dummy task
async function long_write_task(subject, body) {
    await new Promise((resolve) => setTimeout(resolve, 1000));

    console.log([subject, body]);
};


// Set concurrency to 3 tasks.
set_concurrency(3);

// Start 10 tasks.
for(let i = 0; i < 10; i++){
    // Check if id already queued.
    // Can't be if we just assign the loop iteration number as id, but is best practice.
    // If you don't care about running an identical task twice, leave id 'undefined'.
    if (!in_queue(i)) {
        fire_and_forget(i, long_write_task, 'subject ' + i, 'body ' + i);
    }
}