preface

After more than three years away from the front end of the Web, I took some time today to reorder a previous open source library: priority-async-Queue, a Node.js-based asynchronous priority task queue. For those who are not familiar with version 1.0, we will focus on the original design and application scenarios of PAQ today.

What is 2.0 better than 1.0?

There are three main improvements:

  • 1.0 supports only single-concurrent task execution, while 2.0 supports multi-concurrent task execution. Users can set the number of concurrent tasks.
  • 2.0 added execution time statistics for each task, including task creation timecreatTime, the time when the task startsstartTime, and the end time of task executionendTime. Users can calculate the required task waiting time according to their requirements:startTime - createTimeOr the time it takes to perform a taskendTime - startTimeOr just write to a log file to record information about the execution time.
  • 1.0 supports execution only in node.js, while 2.0 provides browser-executable output files. Let the browser end encounter the same problem of the students, can be solved.

Paq design idea

The design idea of PAQ is actually very simple, consisting of four classes:

  • TaskIs to describe the execution logic and configuration parameters of each task to be executed (asynchronous/synchronous).
  • PriorityQueueIs the queue that controls the priority of each task to be executed (asynchronous/synchronous), has the basic properties of the queue, and does.
  • AsyncQueueA queue that controls the execution of each asynchronous/synchronous task in a strict order.
  • EventClasses that simulate event listening and event firing.

In order to ensure the normal operation of compatible browser environment, 2.0 removes the dependence on EventEmitter, a native event class of Node.js, and realizes the functions of simple event binding and triggering.

Here is a flowchart for PAQ 2.0:

The original intention of PAQ

When I first moved to game development, the department desperately needed a clustered packaging system to handle the huge packaging business. At that time, I was ordered to accept the task. Later, THE cluster packaging system I developed adopted the task scheduling, and the core underlying code architecture was similar to PAQ. Of course, the actual application will be much more complicated than PAQ, because the game packaging process is an extremely complex and tedious process, I just took the core of the general scheduling ideas to open source into a general library.

Just think, if a baler can only perform a single packaging task at the same time, then it is too waste of hardware resources. But limited by the NUMBER of CPU cores, hard disk space, memory capacity, data read and write speed and other factors, we can not rudely to the baler to add concurrent packaging tasks, so at this time, to control the number of concurrent baler is particularly important. We should ensure both efficiency and safety and reliability.

Paq Application scenario

First, we need to be clear that in most business scenarios, you probably don’t need PAQ. In PAQ without setting the number of concurrent tasks, tasks are executed in strict order by default and the number of concurrent tasks is always 1, which reduces the execution efficiency in most cases. Asynchronous tasks and event loops supported by JavaScript are designed to make full use of the characteristics of CPU’s multi-core in a single-threaded execution environment, so as to improve program execution efficiency.

For example, the maximum number of concurrent requests allowed by Chrome is 6, and the maximum number of concurrent requests allowed by FireFox is 4. There are some differences between browser versions. All in all, the major browsers have done a good job of load balancing for us when it comes to web requests. In node.js, the load balancing problem is something we developers need to solve ourselves.

If, in a short period of time, a large number of clients generate a large number of network requests, the capacity of the server must be limited. At this time, we need to use a data structure like a queue container to store these requests, and then according to the principle of first-in, first-out slowly provided to the server to process, the pressure will be reduced a lot. Speaking of this, many students with server experience, the first time will think of message queues. Yes, PAQ is a lot like a message queue, but it doesn’t follow the producer and consumer pattern. Therefore, PAQ cannot handle the scheduling of distributed and clustered services alone. It is better placed downstream of MQ.

Paq characteristics

1. Paq is smaller and easier to use.

Paq effective source code about 200 lines, Node.js environment is very small. But on the browser side, the paQ is also packed and compressed to 18KB, mainly due to the redundancy of ES6 syntax-compatibility code.

As a front-end developer, whether it is Web, mobile native or game development, the most painful thing is to be compatible with various user terminal operating environments and devices.

Here is the most basic paQ usage, out of the box:

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

paq.addTask(() = > {
  console.log('Helo World! ');
});

// Hello World!
Copy the code

Next, let’s look at a classic interview question from Byte.

class Scheduler {
  add(promiseCreator) {
    // Refine the Scheduler so that it has a concurrency of 2}}const timeout = (time) = > new Promise(resolve= > {
  setTimeout(resolve, time);
})

const scheduler = new Scheduler();

const addTask = (time, order) = > {
  scheduler.add(() = > timeout(time)).then(() = > console.log(order));
}

addTask(1000.1);
addTask(500.2);
addTask(300.3);
addTask(400.4);

// Request the output order
/ / 2
/ / 3
/ / 1
/ / 4
Copy the code

You can think a little bit about how to extend the Scheduler class to fulfill the requirements. For those of you who have seen or already know how to do this, take a look at how to easily implement this requirement using PAQ.

const PAQ = require('priority-async-queue');
// When instantiating paQ, set its concurrency to 2
const paq = new PAQ(2);

class Scheduler {
  add(promiseCreator) {
    return new Promise(resolve= > {
      paq.addTask({
        completed: (ctx, res) = >{ resolve(res); }},() = >promiseCreator()); }); }}...Copy the code

As for how to implement this requirement without using PAQ? If you are interested, you can share your own implementation in the comments section.

2. Paq is more consistent with Node.js development habits

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

// Chain call structure
paq.addTask(() = > {
  console.log('one');
}).addTask(() = > {
  console.log('two');
}).addTask(() = > {
  console.log('three');
});

// one
// two
// three

// Supports asynchronous operations like native async and Promise
paq.addTask(() = > {
  return new Promise(resolve= > {
    paq.sleep(1000).then(() = > {
      console.log('sleep 1s');
      resolve();
    });
  });
});
paq.addTask(async() = > {await paq.sleep(1000).then(() = > {
    console.log('sleep 1s too');
  });
});

// sleep 1s
// sleep 1s too
Copy the code

3. Flexible use

As long as paQ sets the concurrency high enough, or as high as the processing peak, it can perform unlimited concurrency similar to promise.all, but PAQ does not wait for all tasks to complete before moving on to the next step.

const PAQ = require('priority-async-queue');
// The concurrency limit is large enough
const paq = new PAQ(20);

const p1 = () = > paq.sleep(1000).then(() = > Promise.resolve('p1'));
const p2 = () = > paq.sleep(1000).then(() = > Promise.resolve('p2'));
const p3 = () = > paq.sleep(1000).then(() = > Promise.resolve('p3'));

paq.addTask(p1).addTask(p2).addTask(p3).on('completed'.(opt, result) = > {
  console.log(result);
});

Promise.all([p1(), p2(), p3()]).then(res= > {
  console.log(res);
});

// p1
// p2
// p3
// [ 'p1', 'p2', 'p3' ]
Copy the code

If paQ only handles the first task that returns the status, its usage is closer to that of promise.race.

const PAQ = require('priority-async-queue');
// The concurrency limit is large enough
const paq = new PAQ(100);

const p1 = () = > paq.sleep(3000).then(() = > Promise.resolve('p1'));
const p2 = () = > paq.sleep(2000).then(() = > Promise.resolve('p2'));
const p3 = () = > paq.sleep(1000).then(() = > Promise.resolve('p3'));

let isFirst = false;
paq.addTask(p1).addTask(p2).addTask(p3).on('completed'.(opt, result) = > {
  if(! isFirst) {// TODO handles only the first state change task
    console.log('paq: ' + result);
    isFirst = true; }});Promise.race([p1(), p2(), p3()]).then(res= > {
  console.log('race: ' + res);
});

// paq: p3
// race: p3
Copy the code

I won’t expand the usage of paq that approximates promise.allsettled and promise.any. Personally, in daily development can use native implementation, try to use native implementation, this paper is only an introduction, does not constitute the use of recommendations.

Learning is endless, jiansheng said: “the real master always with an apprentice’s heart”, so it is called: “Master Yi”. If we ordinary people insist on learning, although the final may not become a master, but at least will not be bad, right?