Priority Asynchronous Queue Based on Node.js

Keywords: Javascript github Redis

Source of article: https://github.com/Checkson/b...

Preface

Unexpectedly, in my daily development, I encountered the need for a "priority asynchronous queue".The github has similar functionality, and the libraries that integrate redis include Kue, Bull, and so on.However, I am not satisfied with the pursuit of simplicity, ease and lightness.Based on the 28 Principles, I decided to implement a lightweight open source library with only the 20% functionality of the above two: priority-async-queue.

What is the name of an open source library so long?The reason is that I think developers just need to look at the library name to see what it does, and it's not ambiguous at all.However, for fluent wording, priority-async-queue is hereinafter referred to as "paq".

You may not need paq

Following the redux authors'line, first of all, we need to be clear that you may not need paq.

You need to use paq only if you encounter N asynchronous tasks that cannot be executed in parallel and can only be executed sequentially.

Simply put, if you need to perform N asynchronous tasks without resource race and occupation, data sharing, strict logical ordering, priority comparison, etc., paq may be unnecessary and will reduce execution efficiency and affect program performance.

paq design ideas

The paq is designed in three categories:

Task is the execution logic and configuration parameters that describe each task to be executed (asynchronous/synchronous).
PriorityQueue is a priority queue that controls the basic properties and operations of each task to be executed (asynchronous/synchronous).
AsyncQueue is a queue that controls the strict sequential execution of each pending (asynchronous/synchronous) task.
Here is the program flow chart for paq:

Basic concepts of paq and API

1. addTask

addTask is the core method that creates a task and adds it to the paq queue.

paq.addTask([options, ]callback);

options is an optional object that contains the following properties:

{
  id: undefined,          // Task id
  priority: 'normal',     // Task weights, for example: low, normal, mid, high, urgent
  context: null,          // Execution Task Context
  start: (ctx, options) => {}, // Callback that task will be executed
  completed: (ctx, res) => {}, // Callback after task execution is complete
  failed: (ctx, err) => {},    // Callback after task execution failure
  remove: (ctx, options) => {} // Callback after task is deleted
}

callback is a function that describes the logic of performing a task and contains two parameters: ctx and options:

  • ctx is the paq instance to which the task belongs.
  • Options is the final value of the options parameter for this task.

    paq.addTask((ctx, options) => {

     console.log(ctx === paq); // true

    });

2. removeTask

The removeTask method deletes tasks from a waiting column based on the task id.

paq.removeTask(taskId);

If the task is successfully deleted, it returns true.Otherwise, it will return false.

3. pause

The pause method is to pause the paq to continue the task.

paq.pause();

Note: However, you cannot pause the task currently executing because the progress of the asynchronous task cannot be temporarily detected.

4. isPause

isPause property that returns whether the current queue is paused.

paq.isPause; // return true or false.

5. resume

resume method, which restarts the paq queue to perform tasks.

paq.resume();

6. clearTask

The cleartTask method, which clears all tasks in the paq wait queue.

paq.clearTask();

paq usage

1. Basic usage

As soon as a task is added to the paq, it is automatically executed.

const PAQ = require('priority-async-queue');
const paq = new PAQ();

paq.addTask((ctx, options) => {
  console.log('This is a simple task!');
});

// This is a simple task!

2. Synchronize tasks

You can use paq Perform a series of synchronization tasks, such as:

const syncTask = (n) => {
  for (let i = 0; i < n; i++) {
    paq.addTask(() => {
      console.log('Step', i, 'sync');
      return i;
    });
  }
};

syncTask(3);

// Step 0 sync
// Step 1 sync
// Step 2 sync

3. Asynchronous Tasks

You can also use paq to perform a series of asynchronous tasks, such as:

const asyncTask = (n) => {
  for (let i = 0; i < n; i++) {
    paq.addTask(() => {
      return new Promise(resolve => {
        setTimeout(() => {
          console.log('Step', i, 'async');
          resolve(i);
        }, i * 1000);
      });
    });
  }
};

asyncTask(3);

// Step 0 async
// Step 1 async
// Step 2 async

4. Mixed tasks

You can even use paq to perform a series of synchronous and asynchronous staggered tasks, such as:

const mixTask = (n) => {
  asyncTask(n);
  syncTask(n);
  asyncTask(n);
};

mixTask(2);

// Step 0 async
// Step 1 async
// Step 0 sync
// Step 1 sync
// Step 0 async
// Step 1 async

5. Binding Execution Context

Sometimes if you need to specify a context to perform a task, for example:

const contextTask = (n) => {
  var testObj = {
    name: 'foo',
    sayName: (name) => {
      console.log(name);
    }
  };
  for (let i = 0; i < n; i++) {
    paq.addTask({ context: testObj }, function () {
      this.sayName(this.name + i);
    });
  }
};

contextTask(3);

// foo0
// foo1
// foo2

Note: this does not exist in the arrow function, or it is the context that points to its definition.

6. Delayed execution

paq also supports delayed execution of tasks, such as:

    const delayTask = (n) => {
      for (let i = 0; i < n; i++) {
        paq.addTask({ delay: 1000 * i }, () => {
          console.log('Step', i, 'sync');
          return i;
        });
      }
    };

delayTask(3);

// Step 0 sync
// Step 1 sync
// Step 2 sync

7. Priority

If the tasks you need to perform have weights, for example:

const priorityTask = (n) => {
for (let i = 0; i < n; i++) {

paq.addTask({ priority: i === n - 1 ? 'high' : 'normal' }, () => {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('Step', i, 'async');
      resolve(i);
    }, i * 1000);
  });
});

}
};

priorityTask(5);

// Step 0 async
// Step 4 async
// Step 1 async
// Step 2 async
// Step 3 async
The default priority mapping is as follows:

{
  "low": 1,
  "normal": 0, // default
  "mid": -1,
  "high": -2,
  "urgent": -3
}

8. Callback function

Sometimes you want to be able to do something when a task starts, completes, fails, or is deleted, for example

const callbackTask = (n) => {
  for (let i = 0; i < n; i++) {
    paq.addTask({
      id: i,
      start: (ctx, options) => {
        console.log('start running task id is', options.id);
      },
      completed: (ctx, res) => {
        console.log('complete, result is', res);
      },
      failed: (ctx, err) => {
        console.log(err);
      }
    }, () => {
      if (i < n / 2) {
        throw new Error(i + ' is too small!');
      }
      return i;
    });
  }
};

callbackTask(5);

// start running task id is 0
// Error: 0 is too small!
// start running task id is 1
// Error: 1 is too small!
// start running task id is 2
// Error: 2 is too small!
// start running task id is 3
// complete, result is 3
// start running task id is 4
// complete, result is 4

9. Delete Tasks

Sometimes you need to delete tasks, such as:

const removeTask = (n) => {
  for (let i = 0; i < n; i++) {
    paq.addTask({
      id: i,
      remove: (ctx, options) => {
        console.log('remove task id is', options.id);
      }
    }, () => {
      return new Promise(resolve => {
        setTimeout(() => {
          console.log('Step', i, 'async');
          resolve(i);
        }, i * 1000);
      });
    });
  }
  console.log(paq.removeTask(3));
  console.log(paq.removeTask(5));
};

removeTask(5);

// remove task id is 3
// true
// false
// Step 0 async
// Step 1 async
// Step 2 async
// Step 4 async

Note: You must assign IDS when creating tasks and delete tasks based on id s.

paq events

If you need to monitor the status of a paq queue, paq provides the following event listeners:

1. addTask

paq.on('addTask', (options) => {
  // Triggered when the queue adds a task.
});

2. startTask

paq.on('startTask', (options) => {
  // Triggered when a task in the queue is about to execute.
});

3. changeTask

paq.on('changeTask', (options) => {
  // Triggered when a task in the queue changes.
});

4. removeTask

paq.on('removeTask', (options) => {
  // Triggered when the queue remove a task.
});

5. completed

paq.on('completed', (options, result) => {
  // Triggered when the task execution in the queue is complete.
});

6. failed

paq.on('failed', (options, err) => {
  // Triggered when a task in the queue fails to execute.
});

Finally, I would like to add that if you encounter a need that Promise.all and Proise.race cannot solve, consider paq

Posted by weezil on Thu, 05 Sep 2019 19:06:40 -0700