Rate-limiting and count-limiting events in RxJS v5, but also allowing pass-through

烈酒焚心 提交于 2020-01-02 11:19:14

问题


I have a bunch of events to send up to a service. But the requests are rate limited and each request has a count limit:

  • 1 request per second: bufferTime(1000)
  • 100 event items per request: bufferCount(100)

The problem is, I am not sure how to combine them in a way that makes sense.

Allowing pass-through

Complicating this further, I need to make sure that events go through instantaneously if we don't hit either limit.

For example, I don't want it to actually wait for 100 event items before letting it go through if it's only one single event during a non-busy time.

Legacy API

I also found that there was a bufferWithTimeOrCount that existed in RxJS v4, although I am not sure how I'd use that even if I had it.

Test playground

Here is a JSBin I made for you to test your solution:

http://jsbin.com/fozexehiba/1/edit?js,console,output

Any help would be greatly appreciated.


回答1:


The bufferTime() operator takes three parameters which combines the functionality of bufferTime and bufferCount. See http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-bufferTime.

With .bufferTime(1000, null, 3) you can make a buffer every 1000ms or when it reaches 3 items. However, this means that it doesn't guarantee 1000ms delay between each buffer.

So you could use something like this which is pretty easy to use (buffers only 3 items for max 1000ms):

click$
  .scan((a, b) => a + 1, 0)
  .bufferTime(1000, null, 3)
  .filter(buffer => buffer.length > 0)
  .concatMap(buffer => Rx.Observable.of(buffer).delay(1000))
  .timestamp()
  .subscribe(console.log);

See live demo: http://jsbin.com/libazer/7/edit?js,console,output

The only difference to what you probably wanted is that the first emission might be delayed by more than 1000ms. This is because both bufferTime() and delay(1000) operators make a delay to ensure that there's always at least 1000ms gap.




回答2:


I hope this works for you.

Operator

events$
  .windowCount(10)
  .mergeMap(m => m.bufferTime(100))
  .concatMap(val => Rx.Observable.of(val).delay(100))
  .filter(f => f.length > 0)

Doc

  • .windowCount(number) : [ Rx Doc ]
  • .bufferTime(number) : [ Rx Doc ]

Demo

// test case
const mock = [8, 0, 2, 3, 30, 5, 6, 2, 2, 0, 0, 0, 1]

const tInterval = 100
const tCount = 10

Rx.Observable.interval(tInterval)
  .take(mock.length)
  .mergeMap(mm => Rx.Observable.range(0, mock[mm]))
  
  // start
  .windowCount(tCount)
  .mergeMap(m => m.bufferTime(tInterval))
  .concatMap(val => Rx.Observable.of(val).delay(tInterval))
  .filter(f => f.length > 0)
  // end

  .subscribe({
    next: (n) => console.log('Next: ', n),
    error: (e) => console.log('Error: ', e),
    complete: (c) => console.log('Completed'),
  })
<script src="https://unpkg.com/rxjs/bundles/Rx.min.js"></script>

Updated

After more testing. I found the answer above has some problem in extreme condition. I think they are caused by .window() and .concat(), and then I find a warning in the doc#concatMap.

Warning: if source values arrive endlessly and faster than their corresponding inner Observables can complete, it will result in memory issues as inner Observables amass in an unbounded buffer waiting for their turn to be subscribed to.

However, I thought the right way to limit the request rate possibly is, that we could limit the cycle time of requests. In your case, just limit there is only 1 request per 10 milliseconds. It is simpler and may be more efficient to control the requests.

Operator

const tInterval = 100
const tCount = 10
const tCircle = tInterval / tCount

const rxTimer = Rx.Observable.timer(tCircle).ignoreElements()

events$
  .concatMap(m => Rx.Observable.of(m).merge(rxTimer)) // more accurate than `.delay()`
  // .concatMap(m => Rx.Observable.of(m).delay(tCircle))

or

events$
  .zip(Rx.Observable.interval(tCircle), (x,y) => x)



回答3:


I've modified the answer I gave to this question to support your use case of adding a limited number of values (i.e. events) to pending requests.

The comments within should explain how it works.

Because you need to keep a record of the requests that have been made within the rate limit period, I don't believe that it's possible to use the bufferTime and bufferCount operators to do what you want - a scan is required so that you can maintain that state within the observable.

function rateLimit(source, period, valuesPerRequest, requestsPerPeriod = 1) {

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

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

      // Keep a record of all requests made within the last period. If the
      // number of requests made is below the limit, the value can be
      // included in an immediate request. Otherwise, it will need to be
      // included in a delayed request.

      requests = requests.filter((request) => request.until > since);
      if (requests.length >= requestsPerPeriod) {

        const leastRecentRequest = requests[0];
        const mostRecentRequest = requests[requests.length - 1];

        // If there is a request that has not yet been made, append the
        // value to that request if the number of values in that request's
        // is below the limit. Otherwise, another delayed request will be
        // required.

        if (
          (mostRecentRequest.until > now) &&
          (mostRecentRequest.values.length < valuesPerRequest)
        ) {

          mostRecentRequest.values.push(value);

        } else {

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

          const until = leastRecentRequest.until + (
            period * Math.floor(requests.length / requestsPerPeriod)
          );

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

          requests.push({
            delay: (mostRecentRequest.until < now) ?
              (until - now) :
              (until - mostRecentRequest.until),
            until,
            values: [value]
          });
        }

      } else {

        requests.push({
          delay: 0,
          until: now,
          values: [value]
        });
      }
      return requests;

    }, [])

    // Emit only the most recent request.

    .map((requests) => requests[requests.length - 1])

    // If multiple values are added to the request, it will be emitted
    // mulitple times. Use distinctUntilChanged so that concatMap receives
    // the request only once.

    .distinctUntilChanged()
    .concatMap((request) => {

      const observable = Rx.Observable.of(request.values);
      return request.delay ? observable.delay(request.delay) : observable;
    });
}

const start = Date.now();
rateLimit(
  Rx.Observable.range(1, 250),
  1000,
  100,
  1
).subscribe((values) => console.log(
  `Request with ${values.length} value(s) at T+${Date.now() - start}`
));
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://unpkg.com/rxjs@5/bundles/Rx.min.js"></script>


来源:https://stackoverflow.com/questions/43130007/rate-limiting-and-count-limiting-events-in-rxjs-v5-but-also-allowing-pass-throu

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!