如何使用RxJS 5无损地对限制请求进行评级

时间:2017-02-16 20:31:44

标签: javascript rxjs reactive-programming

我想使用向服务器发出一系列请求,但服务器的硬速率限制为每秒10个请求。如果我尝试在循环中发出请求,它将达到速率限制,因为所有请求将同时发生。

for(let i = 0; i < 20; i++) {
  sendRequest();
}

ReactiveX有很多用于修改可观察流的工具,但我似乎无法找到实现速率限制的工具。我尝试添加标准延迟,但请求仍然会同时触发,比之前的时间晚100毫秒。

const queueRequest$ = new Rx.Subject<number>();

queueRequest$
  .delay(100)
  .subscribe(queueData => {
    console.log(queueData);
  });

const queueRequest = (id) => queueRequest$.next(id);

function fire20Requests() {
  for (let i=0; i<20; i++) {
    queueRequest(i);
  }
}

fire20Requests();
setTimeout(fire20Requests, 1000);
setTimeout(fire20Requests, 5000);

debounceTimethrottleTime运算符与我正在寻找的类似,但这是有损的而不是无损的。我想保留我提出的每一个请求,而不是丢弃之前的请求。

...
queueRequest$
  .debounceTime(100)
  .subscribe(queueData => {
    sendRequest();
  });
...

如何使用ReactiveX和Observables在不超过速率限制的情况下向服务器发出这些请求?

5 个答案:

答案 0 :(得分:3)

我写了一个库来执行此操作,您设置了每个间隔的最大请求数,并通过延迟订阅来限制可观察量。它已经过测试,并带有示例:https://github.com/ohjames/rxjs-ratelimiter

答案 1 :(得分:2)

OP的self answer(以及linked blog)中的实施总是会产生一个不太理想的延迟。

如果速率限制服务允许每秒10个请求,则应该可以在10毫秒内发出10个请求,只要下一个请求不是另外990毫秒。

下面的实现应用可变延迟来确保强制执行限制,并且延迟仅适用于超出限制的请求。

function rateLimit(source, count, period) {

  return source
    .scan((records, value) => {

      const now = Date.now();
      const since = now - period;

      // Keep a record of all values received within the last period.

      records = records.filter((record) => record.until > since);
      if (records.length >= count) {

        // until is the time until which the value should be delayed.

        const firstRecord = records[0];
        const lastRecord = records[records.length - 1];
        const until = firstRecord.until + (period * Math.floor(records.length / count));

        // concatMap is used below to guarantee the values are emitted
        // in the same order in which they are received, so the delays
        // are cumulative. That means the actual delay is the difference
        // between the until times.

        records.push({
          delay: (lastRecord.until < now) ?
            (until - now) :
            (until - lastRecord.until),
          until,
          value
        });
      } else {
        records.push({
          delay: 0,
          until: now,
          value
        });
      }
      return records;

    }, [])
    .concatMap((records) => {

      const lastRecord = records[records.length - 1];
      const observable = Rx.Observable.of(lastRecord.value);
      return lastRecord.delay ? observable.delay(lastRecord.delay) : observable;
    });
}

const start = Date.now();
rateLimit(
  Rx.Observable.range(1, 30),
  10,
  1000
).subscribe((value) => console.log(`${value} at T+${Date.now() - start}`));
<script src="https://unpkg.com/rxjs@5/bundles/Rx.min.js"></script>

答案 2 :(得分:2)

使用Adamanswer。但是,请记住,传统的of().delay()实际上会在每个元素之前 添加延迟。特别是,这会延迟可观察对象的第一个元素,以及实际上不受速率限制的任何元素。

解决方案

您可以通过以下方法解决此问题:让您的concatMap返回可观察的流,这些可观察的流立即发出 a 值,但仅在给定延迟后才能完成:< / p>

new Observable(sub => {
  sub.next(v);
  setTimeout(() => sub.complete(), delay);
})

这有点麻烦,所以我要为其创建一个函数。就是说,由于除了实际的速率限制之外没有其他用途,因此最好编写一个rateLimit运算符:

function rateLimit<T>(
    delay: number,
    scheduler: SchedulerLike = asyncScheduler): MonoTypeOperatorFunction<T> {
  return concatMap(v => new Observable(sub => {
    sub.next(v);
    scheduler.schedule(() => sub.complete(), delay);
  }));
}

然后:

queueRequest$.pipe(
    rateLimit(100),
  ).subscribe(...);

限制

现在,这将在每个元素之后 创建一个延迟。这意味着,如果您的源可观察对象发出其最后一个值然后完成,则最终的速率受限的可观察对象之间在其最后一个值和完成之间会有一点延迟。

答案 3 :(得分:1)

This blog post很好地解释了RxJS非常善于丢弃事件,以及他们如何得出答案,但最终,您正在寻找的代码是:

queueRequest$
  .concatMap(queueData => Rx.Observable.of(queueData).delay(100))
  .subscribe(() => {
    sendRequest();
  });

concatMap将新创建的observable连接到可观察流的后面。此外,使用delay将事件推回100毫秒,允许每秒发生10个请求。 You can view the full JSBin here, which logs to the console instead of firing requests.

答案 4 :(得分:1)

实际上,使用$this->fields_list = array( 'id_push' => array('title' => $this->l('ID')), 'shops' => array('title' => $this->l('Shop(s)'),'callback' => 'getShopName','type'=>'editable') ); 运算符及其三个参数有一种更简单的方法:

import re
s = 'fixed-string-123-456'
result = re.findall('(?<=fixed-string-)(\d+)-(.*)', s)
if result:
    print (result[0])
#('123', '456')

这意味着我们可以使用bufferTime(),这意味着我们会在最多1秒后发出最多10项的缓冲区。 bufferTime(bufferTimeSpan, bufferCreationInterval, maxBufferSize) 表示我们希望在发出当前缓冲区后立即打开一个新缓冲区。

bufferTime(1000, null, 10)

查看现场演示:https://jsbin.com/mijepam/19/edit?js,console

您可以尝试不同的初始延迟。仅使用null,请求将按批次10发送:

function mockRequest(val) {
  return Observable
    .of(val)
    .delay(100)
    .map(val => 'R' + val);
}

Observable
  .range(0, 55)
  .concatMap(val => Observable.of(val)
    .delay(25) // async source of values
    // .delay(175)
  )

  .bufferTime(1000, null, 10) // collect all items for 1s

  .concatMap(buffer => Observable
    .from(buffer) // make requests
    .delay(1000)  // delay this batch by 1s (rate-limit)
    .mergeMap(value => mockRequest(value)) // collect results regardless their initial order
    .toArray()
  )
  // .timestamp()
  .subscribe(val => console.log(val));

但是对于25ms,我们会发出少于10件的批次,因为我们受到1秒延迟的限制。

[ 'R0', 'R1', 'R2', 'R3', 'R4', 'R5', 'R6', 'R7', 'R8', 'R9' ]
[ 'R10', 'R11', 'R12', 'R13', 'R14', 'R15', 'R16', 'R17', 'R18', 'R19' ]
[ 'R20', 'R21', 'R22', 'R23', 'R24', 'R25', 'R26', 'R27', 'R28', 'R29' ]
[ 'R30', 'R31', 'R32', 'R33', 'R34', 'R35', 'R36', 'R37', 'R38', 'R39' ]
[ 'R40', 'R41', 'R42', 'R43', 'R44', 'R45', 'R46', 'R47', 'R48', 'R49' ]
[ 'R50', 'R51', 'R52', 'R53', 'R54' ]

然而,您可能需要的不同之处。由于.delay(175)[ 'R0', 'R1', 'R2', 'R3', 'R4' ] [ 'R5', 'R6', 'R7', 'R8', 'R9', 'R10' ] [ 'R11', 'R12', 'R13', 'R14', 'R15' ] [ 'R16', 'R17', 'R18', 'R19', 'R20', 'R21' ] [ 'R22', 'R23', 'R24', 'R25', 'R26', 'R27' ] [ 'R28', 'R29', 'R30', 'R31', 'R32' ] [ 'R33', 'R34', 'R35', 'R36', 'R37', 'R38' ] [ 'R39', 'R40', 'R41', 'R42', 'R43' ] [ 'R44', 'R45', 'R46', 'R47', 'R48', 'R49' ] [ 'R50', 'R51', 'R52', 'R53', 'R54' ] ,此解决方案最初开始在2秒延迟后开始发出值。所有其他排放发生在1秒后。

你最终可以使用:

.bufferTime(1000, ...)

这将始终收集10个项目,然后才会执行请求。这可能会更有效率。