为承诺速率限制功能创建有效测试用例的问题

Issue creating valid test case for promise rate limit function

我正在尝试为下面的 promiseRateLimit 函数创建一个有效的测试用例。 promiseRateLimit 函数的工作方式是它使用 queue 来存储传入的承诺,并在它们之间放置一个 delay

import Promise from 'bluebird'

export default function promiseRateLimit (fn, delay, count) {
  let working = 0
  let queue = []
  function work () {
    if ((queue.length === 0) || (working === count)) return
    working++
    Promise.delay(delay).tap(() => working--).then(work)
    let {self, args, resolve} = queue.shift()
    resolve(fn.apply(self, args))
  }
  return function debounced (...args) {
    return new Promise(resolve => {
      queue.push({self: this, args, resolve})
      if (working < count) work()
    })
  }
}

下面是函数的一个例子。

async function main () {
  const example = (v) => Promise.delay(50)
  const exampleLimited = promiseRateLimit(example, 100, 1)
  const alpha = await exampleLimited('alpha')
  const beta = await exampleLimited('beta')
  const gamma = await exampleLimited('gamma')
  const epsilon = await exampleLimited('epsilon')
  const phi = await exampleLimited('phi')
}

example 承诺需要 50ms 到 运行,而 promiseRateLimit 函数将只允许 1 承诺每个 100ms。所以promise之间的间隔应该大于100ms

这是一个有时 returns 成功有时失败的完整测试:

import test from 'ava'
import Debug from 'debug'
import Promise from 'bluebird'
import promiseRateLimit from './index'
import {getIntervalsBetweenDates} from '../utilitiesForDates'
import {arraySum} from '../utilitiesForArrays'
import {filter} from 'lodash'

test('using async await', async (t) => {
  let timeLog = []
  let runCount = 0
  const example = (v) => Promise.delay(50)
    .then(() => timeLog.push(new Date))
    .then(() => runCount++)
    .then(() => v)
  const exampleLimited = promiseRateLimit(example, 100, 1, 'a')
  const alpha = await exampleLimited('alpha')
  const beta = await exampleLimited('beta')
  const gamma = await exampleLimited('gamma')
  const epsilon = await exampleLimited('epsilon')
  const phi = await exampleLimited('phi')
  const intervals = getIntervalsBetweenDates(timeLog)
  const invalidIntervals = filter(intervals, (interval) => interval < 100)
  const totalTime = arraySum(intervals)
  t.is(intervals.length, 4)
  t.deepEqual(invalidIntervals, [])
  t.deepEqual(totalTime >= 400, true)
  t.is(alpha, 'alpha')
  t.is(beta, 'beta')
  t.is(gamma, 'gamma')
  t.is(epsilon, 'epsilon')
  t.is(phi, 'phi')
})

我创建了一个 getIntervalsBetweenDates 函数,它简单地比较两个 unix 时间戳并获取日期数组之间的持续时间。

export function getIntervalsBetweenDates (dates) {
  let intervals = []
  dates.forEach((date, index) => {
    let nextDate = dates[index + 1]
    if (nextDate) intervals.push(nextDate - date)
  })
  return intervals
}

问题是上面的测试有时 returns 一个低于 delay 的区间。例如,如果 delay100ms,有时是一个区间 returns 98ms96ms。没有理由会发生这种情况。

有什么办法可以让上面的测试100%通过吗?我正在努力确保 delay 论点有效,并且承诺之间至少有那么多时间。

更新 2016-12-28 9:20am (EST)

这是完整的测试

import test from 'ava'
import Debug from 'debug'
import Promise from 'bluebird'
import promiseRateLimit from './index'
import {getIntervalsBetweenDates} from '../utilitiesForDates'
import {arraySum} from '../utilitiesForArrays'
import {filter} from 'lodash'

test('using async await', async (t) => {
  let timeLog = []
  let runCount = 0
  let bufferInterval = 100
  let promisesLength = 4
  const example = v => {
    timeLog.push(new Date)
    runCount++
    return Promise.delay(50, v)
  }
  const exampleLimited = promiseRateLimit(example, bufferInterval, 1)
  const alpha = await exampleLimited('alpha')
  const beta = await exampleLimited('beta')
  const gamma = await exampleLimited('gamma')
  const epsilon = await exampleLimited('epsilon')
  const phi = await exampleLimited('phi')
  const intervals = getIntervalsBetweenDates(timeLog)
  const invalidIntervals = filter(intervals, (interval) => interval < bufferInterval)
  const totalTime = arraySum(intervals)
  t.is(intervals.length, promisesLength)
  t.deepEqual(invalidIntervals, [])
  t.deepEqual(totalTime >= bufferInterval * promisesLength, true)
  t.is(alpha, 'alpha')
  t.is(beta, 'beta')
  t.is(gamma, 'gamma')
  t.is(epsilon, 'epsilon')
  t.is(phi, 'phi')
})

test('using Promise.all with 2 promises', async (t) => {
  let timeLog = []
  let runCount = 0
  let bufferInterval = 100
  let promisesLength = 1
  const example = v => {
    timeLog.push(new Date)
    runCount++
    return Promise.delay(50, v)
  }
  const exampleLimited = promiseRateLimit(example, bufferInterval, 1)
  const results = await Promise.all([exampleLimited('alpha'), exampleLimited('beta')])
  const intervals = getIntervalsBetweenDates(timeLog)
  const invalidIntervals = filter(intervals, (interval) => interval < bufferInterval)
  const totalTime = arraySum(intervals)
  t.is(intervals.length, promisesLength)
  t.deepEqual(invalidIntervals, [])
  t.deepEqual(totalTime >= bufferInterval * promisesLength, true)
})

test('using Promise.props with 4 promises', async (t) => {
  let timeLog = []
  let runCount = 0
  let bufferInterval = 100
  let promisesLength = 3
  const example = v => {
    timeLog.push(new Date)
    runCount++
    return Promise.delay(200, v)
  }
  const exampleLimited = promiseRateLimit(example, bufferInterval, 1)
  const results = await Promise.props({
    'alpha': exampleLimited('alpha'),
    'beta': exampleLimited('beta'),
    'gamma': exampleLimited('gamma'),
    'delta': exampleLimited('delta')
  })
  const intervals = getIntervalsBetweenDates(timeLog)
  const invalidIntervals = filter(intervals, (interval) => interval < bufferInterval)
  const totalTime = arraySum(intervals)
  t.is(intervals.length, promisesLength)
  t.deepEqual(invalidIntervals, [])
  t.deepEqual(totalTime >= bufferInterval * promisesLength, true)
  t.is(results.alpha, 'alpha')
  t.is(results.beta, 'beta')
  t.is(results.gamma, 'gamma')
  t.is(results.delta, 'delta')
})


test('using Promise.props with 12 promises', async (t) => {
  let timeLog = []
  let runCount = 0
  let bufferInterval = 100
  let promisesLength = 11
  const example = v => {
    timeLog.push(new Date)
    runCount++
    return Promise.delay(200, v)
  }
  const exampleLimited = promiseRateLimit(example, bufferInterval, 1)
  const results = await Promise.props({
    'a': exampleLimited('a'),
    'b': exampleLimited('b'),
    'c': exampleLimited('c'),
    'd': exampleLimited('d'),
    'e': exampleLimited('e'),
    'f': exampleLimited('f'),
    'g': exampleLimited('g'),
    'h': exampleLimited('h'),
    'i': exampleLimited('i'),
    'j': exampleLimited('j'),
    'k': exampleLimited('k'),
    'l': exampleLimited('l')
  })
  const intervals = getIntervalsBetweenDates(timeLog)
  console.log(intervals)
  const invalidIntervals = filter(intervals, (interval) => interval < bufferInterval)
  const totalTime = arraySum(intervals)
  t.is(intervals.length, promisesLength)
  t.deepEqual(invalidIntervals, [])
  t.deepEqual(totalTime >= bufferInterval * promisesLength, true)
})

即使 example 发生变化,我仍然遇到问题。

[ 99, 98, 105, 106, 119, 106, 105, 105, 101, 106, 100 ]

  2 passed
  2 failed

  using Promise.props with 4 promises

  t.deepEqual(invalidIntervals, [])
              |                    
              [99]                 

  Generator.next (<anonymous>)

  using Promise.props with 12 promises

  t.deepEqual(invalidIntervals, [])
              |                    
              [99,98]              

  Generator.next (<anonymous>)

setTimeout(在Promise.delay内部使用)不保证准确计时,它只保证回调被调用不在[=之前41=] 给定超时到期。实际时间将取决于机器负载、事件循环的速度和 possibly anything else.

事实上,Node.js docs 只说明

The callback will likely not be invoked in precisely delay milliseconds. Node.js makes no guarantees about the exact timing of when callbacks will fire, nor of their ordering. The callback will be called as close as possible to the time specified.

在您的测试中会发生的情况是 Promise.delay(50) 有时需要超过 50 毫秒(不多,但仍然如此),并且当下一个 Promise.delay(50) 更准时。

如果您只是立即记录 example 函数的调用时间,而不是在 大约 50 毫秒的人为延迟之后:

const example = v => {
  timeLog.push(new Date);
  runCount++;
  return Promise.delay(50, v)
};

为了解决 100 毫秒超时本身的不准确性,最简单的解决方案是给它一些可能为 5% 的余地(在您的情况下为 5 毫秒):

const invalidIntervals = filter(intervals, (interval) => interval < 100 * .95)
t.true(totalTime >= 400 * .95)

如果你想绝对确定延迟永远不会太短,你可以编写自己的函数:

Promise.delayAtLeast = function(delay, value) {
    const begin = Date.now()
    return Promise.delay(delay, value).then(function checkTime(v) {
        const duration = Date.now() - begin;
        return duration < delay
          ? Promise.delay(delay - duration, v).then(checkTime);
          : v;
    });
};

并在 promiseRateLimit.

中使用它