问题
Background
I am making a batch of HTTP GET requests to a server and I need to throttle them to avoid killing the poor server. For the purposes of my demo, this will be the GET method:
/*
* This function simulates a real HTTP GET request, that always takes 1 seconds
* to give a response. In this case, always gives the same response.
*/
let mockGet = function(url) {
return new Promise(fulfil => {
setTimeout(
url => {
fulfil({ url, data: "banana"});
},
1000, url);
});
};
I am using the mockGet
in several places and in different contexts, so now I want to instead use a getUrl
function that uses mockGet
but that throttles it to a reasonable pace.
By this I mean I need a function that when called multiple times will always execute in the given order, but with the given delay between different executions.
Research
My first tentative was to use libraries like underscorejs and lodash to achieve this:
- How to execute functions with delay, underscorejs/lodash
but it fell short, as they don't provide the functionality I am after.
Turns out, I need to save each invocation in a list, so I can later call it when it is convenient. Following this logic I found another answer:
- Call function multiple times in the same moment but execute different calls with delay in nodejs
Which kinda answer my questions, but has several problems I plan to fix. It has global variables, no separation of concerns, forces the user to know the inner mechanics ... I want something cleaner, something among the lines of underscorejs or lodash that hides all that behind an easy to use function.
Code
My take on this challenge was to use the factory pattern with Promises in order to return what I needed:
let getFactory = function(args) {
let {
throttleMs
} = args;
let argsList = [];
let processTask;
/*
* Every time this function is called, I add the url argument to a list of
* arguments. Then when the time comes, I take out the oldest argument and
* I run the mockGet function with it, effectively making a queue.
*/
let getUrl = function(url) {
argsList.push(url);
return new Promise(fulfil => {
if (processTask === undefined) {
processTask = setInterval(() => {
if (argsList.length === 0) {
clearInterval(processTask);
processTask = undefined;
}
else {
let arg = argsList.shift();
fulfil(mockGet(arg));
}
}, throttleMs);
}
});
};
return Object.freeze({
getUrl
});
};
This is the algorithm I am following:
- Every time I call
getUrl
, I save the parameters in a list. Then, I check to see if the
setInterval
timer has started.- If it has not, then I start with with the given delay, otherwise I do nothing.
Upon executing, the
setInterval
function checks for the queue of arguments.- If it is empty, I stop the
setInterval
timer. - If it has arguments, I remove the first one from the list, and I fulfil the Promise with the real
mockGet
function and with its result.
- If it is empty, I stop the
Problem
Although all the logic seems to be in place, this is not working ... yet. What happens is that when I use it:
/*
* All calls to any of its functions will have a separation of X ms, and will
* all be executed in the order they were called.
*/
let throttleFuns = getFactory({
throttleMs: 5000
});
throttleFuns.getUrl('http://www.bananas.pt')
.then(console.log);
throttleFuns.getUrl('http://www.fruits.es')
.then(console.log);
throttleFuns.getUrl('http://www.veggies.com')
.then(console.log);
// a ton of other calls in random places in code
Only the first response is printed, and the other ones are not.
Questions
- What am I doing wrong?
- How can I improve this code?
回答1:
The problem with your code is that you only call the fulfil
method of the first promise in getUrl
, but call that many times. The promise will be resolved when the first call to fulfil
occurs, subsequent calls will be ignored.
Using a chained promise queue is a simple way to schedule the calls:
function delay(ms, val) {
return new Promise(resolve => {
setTimeout(() => {
resolve(val);
}, ms);
});
}
class Scheduler {
constructor(interval) {
this.queue = Promise.resolve();
this.interval = interval;
}
submit(fn) {
const result = this.queue.then(fn);
this.queue = this.queue.then(() => delay(this.interval));
return result;
}
wrapFn(fn) {
const _this = this;
return function() {
const targetThis = this, targetArgs = arguments;
console.log("Submitted " + arguments[0]); // for demonstration
return _this.submit(() => fn.apply(targetThis, targetArgs));
}
}
}
function mockGet(url) {
console.log("Getting " + url);
return delay(100, "Resolved " + url);
}
const scheduler = new Scheduler(500);
const getUrl = scheduler.wrapFn(mockGet);
getUrl("A").then(x => console.log(x));
getUrl("B").then(x => console.log(x));
getUrl("C").then(x => console.log(x));
setTimeout(() => {
getUrl("D").then(x => console.log(x));
getUrl("E").then(x => console.log(x));
getUrl("F").then(x => console.log(x));
}, 3000);
EDIT: Above snippet was edited to wait for a constant amount of time instead of the last mockGet resolution. Although I don't really understand your goals here. If you have a single client, then you should be fine with serializing the api calls. If you hav lots of clients, then this solution will not help you, as the same amount of calls will be made in a time segment as without this throttling, just the load on your server will be spread instead of coming in bursts.
EDIT: Made the code object oriented to be simpler to modularize to meet some your clean code requirements.
回答2:
Solution
Delaying batch requests is exactly the same as delaying any asynchronous call. After a lot of tries and asking around, I finally came to the solution I was looking for in this question:
- How to delay execution of functions, JavaScript
There is an in depth explanation of the reasoning and why I picked the answer I did as well.
Queue vs Math
In the answer above, I compare to solution to this problem. One using queue logic, like I was attempting to do here, and one using Math.
Usually if you have a mathematic expression you end up needing less logic and the code is easier to maintain. This is the solution I picked.
// Seed our "last call at" value
let lastCall = Date.now();
let delayAsync = function(url) {
return new Promise(fulfil => {
// Delay by at least `delayMs`, but more if necessary from the last call
const now = Date.now();
const thisDelay = Math.max(delayMs, lastCall - now + 1 + delayMs);
lastCall = now + thisDelay;
setTimeout(() => {
// Fulfill our promise using the result of `asyncMock`'s promise
fulfil(asyncMock(url));
}, thisDelay);
});
};
However, since I was on the path of using queue logic, I decided to post my 2 cents as well, and I am quite proud of it.
For more information I strongly recommend you read the whole thing !
来源:https://stackoverflow.com/questions/42558149/delay-batch-get-requests-to-server-javascript