延迟对服务器的批量 GET 请求,JavaScript

Delay batch GET requests to server, JavaScript

背景

我正在向服务器发出一批 HTTP GET 请求,我需要限制它们以避免杀死糟糕的服务器。出于我的演示目的,这将是 GET 方法:

/*
 *  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);
    });
};

我在几个地方和不同的上下文中使用 mockGet,所以现在我想改用 getUrl 函数,该函数使用 mockGet 但将其节流到合理的水平步伐。

我的意思是我需要一个函数,当被多次调用时,它总是按给定的顺序执行,但在不同的执行之间有给定的延迟。

研究

我的第一个尝试是使用像 underscorejs and lodash 这样的库来实现这个:

但它不够好,因为它们没有提供我想要的功能。

事实证明,我需要将每个调用保存在一个列表中,以便以后方便时调用它。按照这个逻辑,我找到了另一个答案:

这有点回答了我的问题,但有几个问题我打算解决。它有全局变量,没有关注点分离,迫使用户了解内部机制……我想要更简洁的东西,类似于 underscorejs 或 lodash 的东西,将所有这些隐藏在一个易于使用的函数后面。

代码

我对这个挑战的看法是使用带有 Promises 的工厂模式,以便 return 我需要的东西:

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
    });
};

这是我遵循的算法:

  1. 每次调用getUrl时,我都会将参数保存在一个列表中。
  2. 然后,我检查 setInterval 计时器是否已启动。

    • 如果没有,那么我从给定的延迟开始,否则我什么也不做。
  3. 执行时,setInterval 函数检查参数队列。

    • 如果它是空的,我会停止 setInterval 计时器。
    • 如果它有参数,我从列表中删除第一个,然后用真正的 mockGet 函数及其结果实现 Promise。

问题

虽然所有的逻辑似乎都已到位,但这还行不通……还没有。当我使用它时会发生什么:

    /*
     *  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

只打印第一个响应,其他不打印。

问题

  1. 我做错了什么?
  2. 我该如何改进这段代码?

您的代码存在问题,您只调用了 getUrl 中第一个承诺的 fulfil 方法,但调用了很多次。当第一次调用 fulfil 时,承诺将被解决,后续调用将被忽略。

使用链式承诺队列是安排调用的一种简单方法:

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);

编辑:上面的代码片段被编辑为等待恒定的时间量而不是最后一个 mockGet 解析。虽然我不太明白你在这里的目标。如果您只有一个客户端,那么序列化 api 调用应该没问题。如果您有很多客户端,那么此解决方案将无济于事,因为在没有此限制的情况下,在一个时间段内将进行相同数量的调用,只是服务器上的负载将分散而不是突发。

编辑:使面向对象的代码更易于模块化以满足您的一些简洁代码要求。

解决方案

延迟批处理请求与延迟任何异步调用完全相同。经过多次尝试和询问,我终于找到了我在这个问题中寻找的解决方案:

有对推理的深入解释以及为什么我也选择了答案。

队列与数学

在上面的回答中,我比较了这个问题的解决方案。一种使用队列逻辑,就像我在这里尝试做的那样,另一种使用 Math.

通常,如果您有一个数学表达式,您最终需要的逻辑会更少,代码也更容易维护。这是我选择的解决方案。

// 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);
  });
};

但是,由于我在使用队列逻辑的道路上,我决定 post 我的 2 美分,我为此感到非常自豪。

要了解更多信息,我强烈建议您阅读整篇文章!