【问题标题】:Delay batch GET requests to server, JavaScript延迟批量 GET 请求到服务器,JavaScript
【发布时间】:2017-07-22 08:09:28
【问题描述】:

背景

我正在向服务器发出一批 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,所以现在我想改用一个使用mockGetgetUrl 函数,但这会将其限制在合理的速度。

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

研究

我的第一个尝试是使用 underscorejslodash 之类的库来实现:

但它失败了,因为它们没有提供我所追求的功能。

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

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

代码

我对这个挑战的看法是使用带有 Promises 的工厂模式来返回我需要的东西:

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. 如何改进此代码?

【问题讨论】:

  • 有趣的是你想​​通过调用来实现 - 如果这是某种长轮询,你应该切换到 websockets
  • 抱歉,不允许使用网络套接字:P

标签: javascript node.js ecmascript-6 promise throttling


【解决方案1】:

你的代码的问题是你只调用了getUrl中第一个promise的fulfil方法,但是调用了很多次。当第一次调用fulfil 时,promise 将被解析,后续调用将被忽略。

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

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

编辑:上面的 sn-p 被编辑为等待一段恒定的时间,而不是最后一个 mockGet 分辨率。虽然我不太了解您的目标。如果您有一个客户端,那么您应该可以序列化 api 调用。如果您有很多客户端,那么此解决方案将无济于事,因为在没有此限制的时间段内将进行相同数量的调用,只是您服务器上的负载将分散而不是突发。

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

【讨论】:

  • 在您的代码中,getUrl === mockGetgetUrlThrottled === getUrl。我对您的解决方案的问题是您根本没有限制任何东西。你只是序列化了这些调用,但我仍将几乎同时进行一百万个调用。
  • @Flame_Phoenix 也可以安排,看我的编辑
【解决方案2】:

解决方案

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

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

队列与数学

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

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

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

但是,由于我正在使用队列逻辑,因此我决定也发布我的 2 美分,我为此感到非常自豪。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-09-30
    • 2017-03-16
    • 1970-01-01
    • 2020-01-20
    相关资源
    最近更新 更多