简体   繁体   English

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

[英]How do I rate limit requests losslessly using RxJS 5

I would like to use make a series of requests to a server, but the server has a hard rate limit of 10 request per second.我想向服务器发出一系列请求,但服务器的硬速率限制为每秒 10 个请求。 If I try to make the requests in a loop, it will hit the rate limit since all the requests will happen at the same time.如果我尝试循环发出请求,它将达到速率限制,因为所有请求将同时发生。

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

ReactiveX has lots of tools for modifying observable streams, but I can't seem to find the tools to implement rate limiting. ReactiveX 有很多用于修改可观察流的工具,但我似乎找不到实现速率限制的工具。 I tried adding a standard delay, but the requests still fire at the same time, just 100ms later than they did previously.我尝试添加标准延迟,但请求仍然同时触发,仅比之前晚 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);

The debounceTime and throttleTime operators are similar to what I'm looking for as well, but that is lossy instead of lossless. debounceTimethrottleTime运算符也与我正在寻找的类似,但这是有损而不是无损的。 I want to preserve every request that I make, instead of discarding the earlier ones.我想保留我提出的每一个请求,而不是丢弃之前的请求。

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

How do I make these requests to the server without exceeding the rate limit using ReactiveX and Observables?如何在不超过使用 ReactiveX 和 Observables 的速率限制的情况下向服务器发出这些请求?

The implementation in the OP's self answer (and in the linked blog ) always imposes a delay which is less than ideal. OP的自我回答 (以及链接的博客 )中的实现始终会施加延迟,该延迟不理想。

If the rate-limited service allows for 10 requests per second, it should be possible to make 10 requests in, say, 10 milliseconds, as long as the next request is not made for another 990 milliseconds. 如果速率限制服务每秒允许10个请求,则只要在另一个990毫秒内不发出下一个请求,就应该有可能在10毫秒内发出10个请求。

The implementation below applies a variable delay to ensure the limit is enforced and the delay is only applied to requests that would see the limit exceeded. 下面的实现应用了可变延迟,以确保强制执行限制,并且该延迟仅应用于将看到超过限制的请求。

 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> 

I wrote a library to do this, you set up the maximum number of requests per interval and it rate limits observables by delaying subscriptions. 我编写了一个库来执行此操作,您设置了每个间隔的最大请求数,并通过延迟订阅来限制可观察的值。 It's tested and with examples: https://github.com/ohjames/rxjs-ratelimiter 经过测试并带有示例: https : //github.com/ohjames/rxjs-ratelimiter

This blog post does a great job of explaining that RxJS is great at discarding events, and how they came to the answer, but ultimately, the code you're looking for is: 这篇博客很好地解释了RxJS非常适合丢弃事件,以及它们如何得出答案,但是最终,您正在寻找的代码是:

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

concatMap adds concatenates the newly created observable to the back of the observable stream. concatMap将新创建的可观察concatMap添加到可观察流的后面。 Additionally, using delay pushes back the event by 100ms, allowing 10 request to happen per second. 此外,使用delay会将事件后退100毫秒,从而允许每秒发生10个请求。 You can view the full JSBin here, which logs to the console instead of firing requests. 您可以在此处查看完整的JSBin,它将记录到控制台而不是触发请求。

Actually, there's an easier way to do this with the bufferTime() operator and its three arguments: 实际上,使用bufferTime()运算符及其三个参数有一种更简单的方法:

bufferTime(bufferTimeSpan, bufferCreationInterval, maxBufferSize)

This means we can use bufferTime(1000, null, 10) which means we'll emit a buffer of max 10 items or after max 1s. 这意味着我们可以使用bufferTime(1000, null, 10) ,这意味着我们将发出最多10个项目最多1s之后的缓冲区。 The null means we want to open a new buffer immediately after the current buffer is emitted. null表示我们要在发出当前缓冲区后立即打开一个新缓冲区。

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

See live demo: https://jsbin.com/mijepam/19/edit?js,console 观看现场演示: https : //jsbin.com/mijepam/19/edit?js,console

You can experiment with different initial delay. 您可以尝试不同的初始延迟。 With only 25ms the request will be sent in batches by 10: 仅25 25ms ,请求将按10批量发送:

[ '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' ]

But with .delay(175) we'll emit batches of less than 10 items because we're limited by the 1s delay. 但是使用.delay(175)我们将发出少于10个项目的批次,因为我们受到1s延迟的限制。

[ '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' ]

There's however one difference to what you might need. 但是,您可能需要的是一个区别。 This solution starts initially starts emitting values after 2s delay because of the .bufferTime(1000, ...) and delay(1000) . 由于.bufferTime(1000, ...)delay(1000) .bufferTime(1000, ...)此解决方案最初在2s延迟后开始发射值。 All other emissions happen after 1s. 所有其他发射在1s之后发生。

You could eventually use: 您最终可以使用:

.bufferTime(1000, null, 10)
.mergeAll()
.bufferCount(10)

This will always collect 10 items and only after that it'll perform the request. 这将始终收集10个项目,然后才执行请求。 This would be probably more efficient. 这可能会更有效率。

Go with Adam 's answer . 跟随亚当答案 However, bear in mind the traditional of().delay() will actually add a delay before every element. 但是,请记住,传统的of().delay()实际上会每个元素之前添加一个延迟。 In particular, this will delay the first element of your observable, as well as any element that wasn't actually rate limited. 特别是,这会延迟您可观察的第一个元素,以及实际上不受速率限制的任何元素。

Solution

You can work around this by having your concatMap return a stream of observables that immediately emit a value, but only complete after a given delay: 你可以让你的解决这个concatMap给定延迟后返回观测流立即 发出值,但只完成:

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

This is kind of a mouthful, so I'd create a function for it. 这有点麻烦,因此我将为其创建一个函数。 That said, since there's no use for this outside of actual rate limiting, you'd probably be better served just writing a rateLimit operator: 就是说,由于在实际的速率限制之外没有任何用处,所以最好只编写一个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);
  }));
}

Then: 然后:

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

Limitation 局限性

This will now create a delay after every element. 现在,这将每个元素之后产生延迟。 This means that if your source observable emits its last value then completes, your resulting rate-limited observable will have a little delay between itself between its last value, and completing. 这意味着,如果您的源可观察对象发出其最后一个值然后完成,则最终的速率受限的可观察对象之间在其最后一个值与完成之间会有一点延迟。

Updated cartant's answer as pipe-able operator for newer rxjs versions:将 carant 的答案更新为新 rxjs 版本的管道运算符:

function rateLimit(count: number, period: number) {
  return <ValueType>(source: Observable<ValueType>) => {
    return source.pipe
      ( scan((records, value) => {
          let now = Date.now();
          let 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.
            let firstRecord = records[0];
            let lastRecord = records[records.length - 1];
            let 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;
        }, [] as RateLimitRecord<ValueType>[])
    , concatMap((records) => {
        let lastRecord = records[records.length - 1];
        let observable = of(lastRecord.value);
        return lastRecord.delay ? observable.pipe(delay(lastRecord.delay)) : observable;
      }) );
  };
}

interface RateLimitRecord<ValueType> {
  delay: number;
  until: number;
  value: ValueType;
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM