简体   繁体   English

有条件活动的rxjs可观察速率限制器

[英]conditionally active rxjs observable rate limiter

I want to create a rate limiter for an observable that conditionally changes the way that it limits a value. 我想为有条件的可观察对象创建一个速率限制器,以有条件地更改其限制值的方式。 The use case is for a downloader that is continually receiving new urls to download. 该用例适用于不断接收新网址进行下载的下载程序。 I want to be able to queue up incoming urls and switch between two methods of queueing. 我希望能够排队输入URL,并在两种排队方法之间进行切换。 The two methods are by limiting a rate (eg no more than 10 requests every 2 seconds) and by number of concurrent requests (eg no more than 10 requests can be fired at a time). 两种方法分别是通过限制速率(例如,每2秒不超过10个请求)和并发请求的数量(例如,一次不能触发10个以上的请求)。

A simple rate limiter can be implemented like below (borrowed from here ): 可以像下面这样实现一个简单的速率限制器(从此处借用):

const rateLimit = (limit, rate, scheduler = asyncScheduler) => {
  let tokens = limit
  const tokenChanged = new BehaviorSubject(tokens)
  const consumeToken = () => tokenChanged.next(--tokens)
  const renewToken = () => tokenChanged.next(++tokens)
  const availableTokens = tokenChanged.pipe(filter(() => tokens > 0))
  return source =>
    source.pipe(
      mergeMap(val =>
        availableTokens.pipe(
          take(1),
          map(() => {
            consumeToken()
            timer(rate, scheduler).subscribe(renewToken)
            return val
          })
        )
      )
    )
}
const o = urlSource.pipe(
  rateLimit(10, 2000),
  mergeMap(downloadUrl)
)
o.toPromise()

And this is a simple concurrent limiter: 这是一个简单的并发限制器:

const o = urlSource.pipe(
  mergeMap(downloadUrl, maxConcurrent)
)
o.toPromise()

Finally, I can create this combined switcher to choose which type of limiter to use: 最后,我可以创建此组合切换器以选择要使用的限制器类型:

const toggleableLimiter = (func, limit, rate, concurrent, toggleObservable) => {
  let useRateLimiter = true
  toggleObservable.subscribe(() => (useRateLimiter = !useRateLimiter))
  const rateLimiter = rateLimit(limit, rate)
  return source => {
    const operators = useRateLimiter
      ? [rateLimiter, mergeMap(func)]
      : [mergeMap(func, concurrent)]
    return source.pipe(...operators)
  }
}

const e = new EventEmitter()
const toggler = fromEvent(e, 'toggle')
const o = urlSource.pipe(
    toggleableLimiter(downloadUrl, 2, 1000, 2, toggler)
)
o.toPromise()
// using rate limiter
e.emit('toggle')
// incoming values now use concurrent limiter

This all works as a decent solution to my problem. 所有这些都可以很好地解决我的问题。 I can toggle between two methods using an event emitter. 我可以使用事件发射器在两种方法之间切换。 The issue however, is that whatever has been passed to the toggleableLimiter before the event is emitted, will have to adhere to that limiter operator. 但是,问题在于, 发出事件之前传递给toggleableLimiter任何内容都必须遵守该限制器运算符。 What I want to know, is if I can conditionally keep values in a queue and choose how to limit the queued values on a whim. 我想知道的是,是否可以有条件地将值保留在队列中,并选择如何随心所欲地限制排队的值。

Ok! 好! I have a solution, unfortunately it involves handling the queue myself. 我有一个解决方案,不幸的是它涉及到我自己处理队列。 I believe this is a necessity due to the nature of observables and backpressure, I found a lot of discussion on the topic in this issue . 由于可观察性和背压的性质,我认为这是必要的,我在本期中对此主题进行了很多讨论。 In the past this could have been handled more simply with the controlled operator, but it is deprecated . 在过去,可以使用controlled运算符来更简单地处理此问题,但已弃用 Instead I simply used a timer observable and wrapped mergeMap 's concurrency control myself (though mergeMap still manages concurrency internally as a safety measure. 取而代之的是,我自己只是使用了一个可观察的timer并自己包装了mergeMap的并发控件(尽管mergeMap仍然在内部管理并发作为一种安全措施。

const rateLimitToggle = (func, limit, rate, maxConcurrent, toggler) => {
  const rateTimer = Rx.timer(0, rate).pipe(ops.mapTo(true))
  return source =>
    new Rx.Observable(subscriber => {
      const concurrentLimiter = new Rx.Subject()
      // stateful vars
      const queue = []
      let inProgress = 0
      let closed = false

      const enqueue = val => {
        queue.push(val)
        concurrentLimiter.next()
      }
      const dequeue = useRateLimit => {
        const availableSlots = useRateLimit ? limit : maxConcurrent - inProgress
        const numberToDequeue = Math.min(availableSlots, queue.length)
        const nextVals = queue.splice(0, numberToDequeue)
        inProgress += availableSlots
        return nextVals
      }

      Rx.merge(Rx.of(true), toggler)
        .pipe(
          ops.switchMap(useRateLimiter => (useRateLimiter ? rateTimer : concurrentLimiter)),
          ops.takeWhile(() => !closed || queue.length),
          ops.mergeMap(dequeue),
          ops.mergeMap(val => func(val), maxConcurrent)
        )
        .subscribe(val => {
          inProgress--
          concurrentLimiter.next()
          subscriber.next(val)
        })

      source.subscribe({
        next(val) {
          enqueue(val)
        },
        complete() {
          closed = true
        }
      })
    })
}

example usage: 用法示例:

const timeout = n => val => {
  console.log('started', val)
  return new Promise(resolve => setTimeout(() => resolve(val), n))
}

const emitter = new EventEmitter()
const toggler = Rx.fromEvent(emitter, 'useRateLimiter')
const downloader = Rx.range(0, 10).pipe(
  rateLimitToggle(timeout(1000), 2, 1000, 10, toggler)
)

downloader.subscribe(val => console.log('finished', val))
setTimeout(() => {
  console.log('now use concurrentLimiter')
  emitter.emit('useRateLimiter', false)
}, 2000)
/* outputs:

  started 0
  started 1
  started 2
  started 3 // 0 - 3 all executed under rateLimiter
  finished 0
  finished 1
  now use concurrentLimiter
  started 4 // 4 - 9 executed under concurrentLimiter
  started 5
  started 6
  started 7
  started 8
  started 9
  finished 2
  finished 3
  finished 4
  finished 5
  finished 6
  finished 7
  finished 8
  finished 9
*/

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

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