Delay batch GET requests to server, JavaScript

南笙酒味 提交于 2019-12-23 19:11:47

问题


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:

  1. Every time I call getUrl, I save the parameters in a list.
  2. 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.
  3. 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.

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

  1. What am I doing wrong?
  2. 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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!