5

How do I do something like $q.all but limiting how many promises are executed concurrently?

My question is just like How can I limit Q promise concurrency?

I want no more than 5 process spawned at a time

The accepted answer for that other question is a library written for promise wrapped to work with Q. But I'm interested specifically in a solution for Angular's $q rather than for Q.

Background: The problem being solved:
I have a bunch of files to download in 2 steps: a) Get URL b) download file.
The browser limits how many files can be retrieved concurrently, so when the straightforward use of promises with $q.all fires off all the downloads, only
N happen right away, e.g. 6 in Chrome, while the rest are delayed. (see Max parallel http connections in a browser?)
Problem is that the URLs have expiry, so by the time the browser executes the N+1th file download, the URL is no longer valid.
So I want to do something like throttled.all(6, promises) rather than $q.all(promise)

Community
  • 1
  • 1
Daryn
  • 4,791
  • 4
  • 39
  • 52

4 Answers4

2

If the resolving of 'all' Promises is irrelevant (like you are updating some Elements on the page and you don't care if it's progressive and not in one 'whoosh') I have created a simple TaskQueue Service. It will not wait for all Promises to be resolved but rather it will process all added Promises/Functions/Tasks it gets and with a max. concurrency of a configured limit value.

As I only found this and some other StackOverflows not helping with my problem I had. So I now give something back to the community I guess. Hope it helps somebody.

It uses modern JS stuff like const and lambda expressions, but you may simply let it compile down from a precompiler like babel if you just want the 'old' stuff.

https://gist.github.com/Blackskyliner/8b1bafed326044fa4f8b1ba2627d1117

It just simply processes its queue after Tasks, which are just anonymous functions, returning a promise or value, are added. It will adhere to a configurable 'maxConcurrentTasks' variable on the service.

If the Task returns a promise which returns a promise and so on it will always use the initial 'slot' within the queue. So it will free the 'slot' for an other added Task after the whole Task Promise chain is resolved (or rejected).

0

I think you could do something recursive.

This would allow you to use just $q. You would just have to have a list you would be using, which you need anyway for $q.all

self.MakeCall = function(theList, chunkSize){
   //Split list into what will be processed and what should be held
    var processList = theList.slice(0,chunkSize);
    var holdList = theList.slice(chunkSize);

    return $q.all(processList).then(function(result) {
      //If holdList is empty you are finished
      if(holdList <= 0) {
        return result;
      }
      else {
        //More items make recursive call to process next set
        return self.MakeCall(holdList, chunkSize);
      }
    });
  };
//Initialize Recursive call
self.MakeCall(self.contacts);
Daryn
  • 4,791
  • 4
  • 39
  • 52
Malkus
  • 3,686
  • 2
  • 24
  • 39
  • 1
    Not a bad start, @Malkus, thanks. It's not quite as concurrent as I was imagining, since your example breaks into chunks of size N, rather than always having N promises running... having said that, it sure is way more concurrent than a sequential chain! : ) – Daryn Dec 09 '15 at 22:45
0

Here is an alternate solution that will run concurrent Chunks. It is not quite as clean as the other answer, but it is closer to what you are looking for.

I left some markup used for validation.

self.list [...];

self.MakeCall = function(theList, chunkSize,methodTracker){
    //Set default chunkSize
    chunkSize = (chunkSize)?chunkSize:1;

    //Slice list using previously defined list 
    // so multiple instances can be runuse same list
    var processList = self.list.slice(0,chunkSize);
    self.list = self.list.slice(chunkSize);

    return $q.all(processList).then(function(result) {
      //If holdList is empty you are finished
      if(self.list <= 0) {
        console.debug("method last: " + methodTracker);
        return result;
      }
      else {
        //More items make recursive call to process next set
        console.debug("method: " + methodTracker);
        return self.MakeCall(self.list, chunkSize,methodTracker);
      }
    });
  };
  //Initialize Recursive call
  self.MakeCall(self.list,1,1).then(function() {
    console.warn('end 1');
  });
  //Initialize Second call that will run asynchronous with the first
  // this can be initialized as many times as you want concurrent threads
  self.MakeCall(self.list,1,2).then(function() {
    console.warn('end 2');
  });

I opted to put in another answer. I think the two are different enough that I didn't want to make changes to the other

Malkus
  • 3,686
  • 2
  • 24
  • 39
0

I tried a few different methods to do this but in the end settled on writing an extension to $q that extends its $q.all behaviour with $q.allLimit().

See the angular-q-limit module for more details.

The module works by creating a wrapping function which takes an array of Promises and executes them in the ordered sequence (limiting the concurrent execution to the limit) until all are exhausted. It then resolves its own promise with the return values of all in the same way as $q.all().

hash-bang
  • 492
  • 1
  • 4
  • 9
  • Please detail how it works instead of just pasting a link to your library. – Bergi Sep 01 '16 at 06:21
  • 1
    That's doesn't make sense. [It's impossible](http://stackoverflow.com/q/30823653/1048572) to sequence `all`. – Bergi Sep 01 '16 at 06:23
  • Apologies. I've added a few examples here - https://gist.github.com/hash-bang/652be06e3cf89f9d71fa2dfceb893719 - If you can be more specific about what it is you are trying to request I can write something up. – hash-bang Sep 01 '16 at 07:45
  • @bergi - The module works by creating a wrapping promise then executing the promises but staying under the limit. It doesn't override $q.all() and instead provides a meta function that does something similar to wrapping each promise in a .then() sequence chain. – hash-bang Sep 01 '16 at 07:47
  • How is the mechanism to sequence promises implemented? Just [edit] your answer. Some code of the `allLimit` implementation would be nice. It doesn't need to go into the details but at least convey the general idea. – Bergi Sep 01 '16 at 08:03
  • The `dynamicClosureLimit.js` example doesn't work, it creates all promises and runs all `get`s immediately. Notice that promises aren't "executed" (they're plain values), functions are. The `limitAllExample.js` looks better, but you're passing functions not promises in that array. This is correct, but your function shouldn't be named `all` then. – Bergi Sep 01 '16 at 08:07
  • @Bergi I've updated the main README on the linked module with better written examples rather than the rushed Gist. To answer your comment, the way they are written was intended to show specifically Angular based promises - Angular doing weird things with `Model.Method().$promise` wrappers as work arounds to its return values. I suppose you could wrap each item within its own `$q` container, remove the outer function wrapper or even play around with `$q.defer` elements if needed though. – hash-bang Sep 01 '16 at 09:11
  • Your documentation is still wrong, and the "Dynamic promise creation" example does not work. Your function does not take an array of promises, it does take an array of promise-returning functions. You can read your own code to confirm that :-) – Bergi Sep 01 '16 at 09:23