Search code examples
javascriptnode.jspromisees6-promise

Why use process.nextTick to ensure correct execution of asynchronous tasks?


I am following an example of a book (Node.js design patterns) to implement a LIMITED PARALLEL EXECUTION algorithm in Node.js with Javascript.

First, I write a TaskQueue class that handles all the limiting and execution of the tasks.

class TaskQueue {
    constructor(concurrency) {
        this.concurrency = concurrency;
        this.running = 0;
        this.queue = [];
    }
    runTask(task) {
        return new Promise((resolve, reject) => {
            this.queue.push(() => {
                return task().then(resolve, reject);
            });
            process.nextTick(this.next.bind(this));
        });
    }
    next() {
        while (this.running < this.concurrency && this.queue.length) {
            const task = this.queue.shift();
            task().finally(() => {
                this.running--;
                this.next();
            });
            this.running++;
        }
    }
}

Then I have written a function that returns a promise that resolves after a random number of miliseconds and keeps record of the promise with an assigned number.

This function delegates the task to the queue, wrapping the promise in a function that will be executed on demand by the TaskQueue class.

function promiseResolveRandomValue(promiseNumber, queue) {
    return queue.runTask(() => {
        return new Promise(resolve => {
            const random = Math.round(Math.random()*1000);
            setTimeout(() => {
                console.log('Resolved promise number: ', promiseNumber, ' with delay of: ', random);
                resolve();
            }, random);
        });
    })
}

Finally I have used a map to execute the function over an array of ten numbers that will help us keep track of each promise.

const queue = new TaskQueue(2);
const array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const promises = array.map((number) => {
    return promiseResolveRandomValue(number, queue);
});

Promise.all(promises)
    .then(() => console.log('Finished'));

So far so good. The thing is that the author recommends this line of code to be executed with process.nextTick to avoid any Zalgo type of situation, but I don't see why, as this code executes exactly the same just invoking the next method of the class.

runTask(task) {
        return new Promise((resolve, reject) => {
            this.queue.push(() => {
                return task().then(resolve, reject);
            });
            process.nextTick(this.next.bind(this)); // <-------------------------------- WHY?
        });
    }

Could someone clarify why is it convenient to use the process.nextTick in some cases such as this one to avoid conflicts with synchronous and asynchronous code?


Solution

  • The Zalgo situation the author is referring to would be

    console.log("Before scheduling the task")
    queue.runTask(() => {
        console.log("The task actually starts");
        return new Promise(resolve => {
            // doesn't really matter:
            setTimeout(resolve, Math.random()*1000);
        });
    });
    console.log("Scheduled the task");
    

    If you didn't use nextTick, the logs could happen in a different order - the task might actually start before the runTask() function returns, depending on how many there are already in the queue.

    This might not be that unexpected - after all, we expect runTask to run the task - however if the task actually starts with some side effect synchronously, that might be unexpected. An example for such a side effect would be the next method itself - consider the following:

    queue.runTask(() => …);
    console.log(`Scheduled the task, there are now ${queue.queue.length} tasks in the queue`);
    

    Would you expect the length to always be at least one? Avoiding Zalgo can give such guarantees.

    (On the other hand, if we did log queue.running instead, expecting it to be >=1 immediately after calling runTask seems just as sensible to me, and would require starting next synchronously. Clear documentation trumps any intuition here.)