简体   繁体   中英

Limit concurrency of promise being run

I'm looking for a promise function wrapper that can limit / throttle when a given promise is running so that only a set number of that promise is running at a given time.

In the case below delayPromise should never run concurrently, they should all run one at a time in a first-come-first-serve order.

import Promise from 'bluebird'

function _delayPromise (seconds, str) {
  console.log(str)
  return Promise.delay(seconds)
}

let delayPromise = limitConcurrency(_delayPromise, 1)

async function a() {
  await delayPromise(100, "a:a")
  await delayPromise(100, "a:b")
  await delayPromise(100, "a:c")
}

async function b() {
  await delayPromise(100, "b:a")
  await delayPromise(100, "b:b")
  await delayPromise(100, "b:c")
}

a().then(() => console.log('done'))

b().then(() => console.log('done'))

Any ideas on how to get a queue like this set up?

I have a "debounce" function from the wonderful Benjamin Gruenbaum . I need to modify this to throttle a promise based on it's own execution and not the delay.

export function promiseDebounce (fn, delay, count) {
  let working = 0
  let queue = []
  function work () {
    if ((queue.length === 0) || (working === count)) return
    working++
    Promise.delay(delay).tap(function () { working-- }).then(work)
    var next = queue.shift()
    next[2](fn.apply(next[0], next[1]))
  }
  return function debounced () {
    var args = arguments
    return new Promise(function (resolve) {
      queue.push([this, args, resolve])
      if (working < count) work()
    }.bind(this))
  }
}

I don't think there are any libraries to do this, but it's actually quite simple to implement yourself:

function queue(fn) { // limitConcurrency(fn, 1)
    var q = Promise.resolve();
    return function(x) {
        var p = q.then(function() {
            return fn(x);
        });
        q = p.reflect();
        return p;
    };
}

For multiple concurrent requests it gets a little trickier, but can be done as well.

function limitConcurrency(fn, n) {
    if (n == 1) return queue(fn); // optimisation
    var q = null;
    var active = [];
    function next(x) {
        return function() {
            var p = fn(x)
            active.push(p.reflect().then(function() {
                active.splice(active.indexOf(p), 1);
            })
            return [Promise.race(active), p];
        }
    }
    function fst(t) {
        return t[0];
    }
    function snd(t) {
        return t[1];
    }
    return function(x) {
        var put = next(x)
        if (active.length < n) {
            var r = put()
            q = fst(t);
            return snd(t);
        } else {
            var r = q.then(put);
            q = r.then(fst);
            return r.then(snd)
        }
    };
}

Btw, you might want to have a look at the actors model and CSP . They can simplify dealing with such things, there are a few JS libraries for them out there as well.

Example

import Promise from 'bluebird'

function sequential(fn) {
  var q = Promise.resolve();
  return (...args) => {
    const p = q.then(() => fn(...args))
    q = p.reflect()
    return p
  }
}

async function _delayPromise (seconds, str) {
  console.log(`${str} started`)
  await Promise.delay(seconds)
  console.log(`${str} ended`)
  return str
}

let delayPromise = sequential(_delayPromise)

async function a() {
  await delayPromise(100, "a:a")
  await delayPromise(200, "a:b")
  await delayPromise(300, "a:c")
}

async function b() {
  await delayPromise(400, "b:a")
  await delayPromise(500, "b:b")
  await delayPromise(600, "b:c")
}

a().then(() => console.log('done'))
b().then(() => console.log('done'))

// --> with sequential()

// $ babel-node test/t.js
// a:a started
// a:a ended
// b:a started
// b:a ended
// a:b started
// a:b ended
// b:b started
// b:b ended
// a:c started
// a:c ended
// b:c started
// done
// b:c ended
// done

// --> without calling sequential()

// $ babel-node test/t.js
// a:a started
// b:a started
// a:a ended
// a:b started
// a:b ended
// a:c started
// b:a ended
// b:b started
// a:c ended
// done
// b:b ended
// b:c started
// b:c ended
// done

Use the throttled-promise module:

https://www.npmjs.com/package/throttled-promise

var ThrottledPromise = require('throttled-promise'),
    promises = [
        new ThrottledPromise(function(resolve, reject) { ... }),
        new ThrottledPromise(function(resolve, reject) { ... }),
        new ThrottledPromise(function(resolve, reject) { ... })
    ];

// Run promises, but only 2 parallel
ThrottledPromise.all(promises, 2)
.then( ... )
.catch( ... );

I have the same problem. I wrote a library to implement it. Code is here . I created a queue to save all the promises. When you push some promises to the queue, the first several promises at the head of the queue would be popped and running. Once one promise is done, the next promise in the queue would also be popped and running. Again and again, until the queue has no Task . You can check the code for details. Hope this library would help you.

Advantages

  • you can define the amount of concurrent promises (near simultaneous requests)
  • consistent flow: once one promise resolve, another request start no need to guess the server capability
  • robust against data choke, if the server stop for a moment, it will just wait, and next tasks will not start just because the clock allowed
  • do not rely on a 3rd party module it is Vanila node.js

1st thing is to make https a promise, so we can use wait to retrieve data (removed from the example) 2nd create a promise scheduler that submit another request as any promise get resolved. 3rd make the calls

Limiting requests taking by limiting the amount of concurrent promises

const https = require('https')

function httpRequest(method, path, body = null) {
  const reqOpt = { 
    method: method,
    path: path,
    hostname: 'dbase.ez-mn.net', 
    headers: {
      "Content-Type": "application/json",
      "Cache-Control": "no-cache"
    }
  }
  if (method == 'GET') reqOpt.path = path + '&max=20000'
  if (body) reqOpt.headers['Content-Length'] = Buffer.byteLength(body);
  return new Promise((resolve, reject) => {
  const clientRequest = https.request(reqOpt, incomingMessage => {
      let response = {
          statusCode: incomingMessage.statusCode,
          headers: incomingMessage.headers,
          body: []
      };
      let chunks = ""
      incomingMessage.on('data', chunk => { chunks += chunk; });
      incomingMessage.on('end', () => {
          if (chunks) {
              try {
                  response.body = JSON.parse(chunks);
              } catch (error) {
                  reject(error)
              }
          }
          console.log(response)
          resolve(response);
      });
  });
  clientRequest.on('error', error => { reject(error); });
  if (body) { clientRequest.write(body)  }  
  clientRequest.end();

  });
}

    const asyncLimit = (fn, n) => {
      const pendingPromises = new Set();

  return async function(...args) {
    while (pendingPromises.size >= n) {
      await Promise.race(pendingPromises);
    }

    const p = fn.apply(this, args);
    const r = p.catch(() => {});
    pendingPromises.add(r);
    await r;
    pendingPromises.delete(r);
    return p;
  };
};
// httpRequest is the function that we want to rate the amount of requests
// in this case, we set 8 requests running while not blocking other tasks (concurrency)


let ratedhttpRequest = asyncLimit(httpRequest, 8);

// this is our datase and caller    
let process = async () => {
  patchData=[
      {path: '/rest/slots/80973975078587', body:{score:3}},
      {path: '/rest/slots/809739750DFA95', body:{score:5}},
      {path: '/rest/slots/AE0973750DFA96', body:{score:5}}]

  for (let i = 0; i < patchData.length; i++) {
    ratedhttpRequest('PATCH', patchData[i].path,  patchData[i].body)
  }
  console.log('completed')
}

process() 

If you don't want to use any plugins/dependencies you can use this solution.

Let's say your data is in an array called datas

  1. Create a function that will process your data in the datas array, lets call it processData()
  2. Create a function that will execute processData() one after another in a while loop until there are no data left on datas array, lets call that function bufferedExecution() .
  3. Create an array of size buffer_size
  4. Fill the array with bufferedExecution()
  5. And wait for it to resolve in Promise.all() or in Promise.allSettled()

Here is a working example, where data is numbers and operation waits for a while and return the square of the number, It also randomly rejects.

 const datas = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]; // this datas array should not contain undefined values for this code to work const buffer_size = 3; const finishedPromises = []; // change this function to your actual function that processes data async function processData(item) { return new Promise((resolve, reject) => { // wait for some time setTimeout(() => { // randomly resolve or reject if (Math.random() > 0.5) { resolve(item ** 2); } else { reject("error message"); } }, 1500); }); } // this function executes one function per loop, but magic happens when you // execute the function below, multiple times async function bufferedExecution(callback, i) { return new Promise(async (resolve, reject) => { // take first vale to process let next = datas.shift(); // check if there is a value, (undefined means you have reached the end of datas array) while (next != undefined) { // just to show which function is running (index of function in array) console.log(`running function id: ${i}`); let result; try { // process data with your function's callback result = await callback(next); // result finishes without error finishedPromises.push({ input: next, result: result, }); } catch (error) { // rejected, so adds error instead of result finishedPromises.push({ input: next, error: error, }); } // get next data from array and goes to next iteration next = datas.shift(); } // once all that is done finish it resolve(); }); } // here is where the magic happens // we run the bufferedExecution function n times where n is buffer size // bufferedExecution runs concurrently because of Promise.all()/Promise.allsettled() const buffer = new Array(buffer_size) .fill(null) .map((_, i) => bufferedExecution(processData, i + 1)); Promise.allSettled(buffer) .then(() => { console.log("all done"); console.log(finishedPromises); // you will have your results in finishedPromises array at this point // you can use input KEY to get the actual processed value // first check for error, if not get the results }) .catch((err) => { console.log(err); });

Output

// waits a while
running function id: 1
running function id: 2
running function id: 3
// waits a while
running function id: 1
running function id: 2
running function id: 3
// waits a while
running function id: 1
running function id: 2
running function id: 3
// waits a while
running function id: 1
running function id: 2
running function id: 3
// waits a while
running function id: 1
all done
[
  { input: 1, error: 'error message' },
  { input: 2, result: 4 },
  { input: 3, result: 9 },
  { input: 4, result: 16 },
  { input: 5, error: 'error message' },
  { input: 6, result: 36 },
  { input: 7, result: 49 },
  { input: 8, error: 'error message' },
  { input: 9, result: 81 },
  { input: 10, result: 100 },
  { input: 11, result: 121 },
  { input: 12, error: 'error message' },
  { input: 13, result: 169 }
]

The classic way of running async processes in series is to use async.js and use async.series() . If you prefer promise based code then there is a promise version of async.js : async-q

With async-q you can once again use series :

async.series([
    function(){return delayPromise(100, "a:a")},
    function(){return delayPromise(100, "a:b")},
    function(){return delayPromise(100, "a:c")}
])
.then(function(){
    console.log(done);
});

Running two of them at the same time will run a and b concurrently but within each they will be sequential:

// these two will run concurrently but each will run
// their array of functions sequentially:
async.series(a_array).then(()=>console.log('a done'));
async.series(b_array).then(()=>console.log('b done'));

If you want to run b after a then put it in the .then() :

async.series(a_array)
.then(()=>{
    console.log('a done');
    return async.series(b_array);
})
.then(()=>{
    console.log('b done');
});

If instead of running each sequentially you want to limit each to run a set number of processes concurrently then you can use parallelLimit() :

// Run two promises at a time:
async.parallelLimit(a_array,2)
.then(()=>console.log('done'));

Read up the async-q docs: https://github.com/dbushong/async-q/blob/master/READJSME.md

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM