From aa52bb188e3b2418c8da15600a544d195241cbf9 Mon Sep 17 00:00:00 2001 From: Nate Silva Date: Mon, 23 Oct 2017 16:34:08 -0700 Subject: [PATCH] add a `fastStart` option when using Redis --- README.md | 13 +++++++++++++ src/quota/quota.ts | 10 +++++++++- src/quota/redisQuotaManager.ts | 16 +++++++++++++--- test/redisQuotaManager.ts | 22 +++++++++++++++++++++- 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9be3a87..07ffe1a 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ The `Quota` configuration object passed to `pRateLimit` offers the following con * `rate`: how many API calls to allow over the interval period * `concurrency`: how many concurrent API calls to allow * `maxDelay`: the maximum amount of time to wait (in milliseconds) before rejecting an API request with `RateLimitTimeoutError` (default: `0`, no timeout) +* `fastStart` (Redis only): if true, immediately begin processing requests using the full quota, instead of waiting several seconds to discover other servers (default: `false`) If you only care about concurrency, you can omit `interval` and `rate`. @@ -71,6 +72,8 @@ If you don’t care about concurrency, you can omit the `concurrency` value. If you make an API request that would exceed rate limits, it’s queued and delayed until it can run within the rate limits. Setting `maxDelay` will cause the API request to fail if it’s delayed too long. +See the [Distributed rate limits](https://github.com/natesilva/p-ratelimit#distributed-rate-limits) section for a discussion of the `fastStart` option. + ## Distributed rate limits You can use Redis to coordinate a rate limit among a pool of servers. @@ -99,6 +102,16 @@ Each server that registers with a given `channelName` will be allotted `1/(numbe When a new server joins the pool, the quota is dynamically adjusted. If a server goes away, its quota is reallocated among the remaining servers within a few minutes. +### The `fastStart` option + +The `fastStart` option only applies when using distributed rate limits (Redis). + +If `fastStart` is `true`, the rate-limiter will immediately process API requests, up to the full quota. As peer servers are discovered, the quota is automatically adjusted downward. + +If `fastStart` is `false` (the default), the rate-limiter starts with a quota of `0`. All API requests are queued and no requests are processed yet. After several seconds, when the rate-limiter has discovered its peers, its true quota is calculated and it begins processing the queued requests. + +A `fastStart` value of `true` will begin processing requests immediately, but there’s a small chance it could briefly cause the shared rate limit to be exceeded. A value of `false` makes sure the limit is not exceeded, but your app may run slowly at first, as the first API calls may be delayed for a few seconds. + ## License MIT © Nate Silva diff --git a/src/quota/quota.ts b/src/quota/quota.ts index 9a75648..ba3c3ac 100644 --- a/src/quota/quota.ts +++ b/src/quota/quota.ts @@ -5,6 +5,14 @@ export interface Quota { rate?: number; /** number of concurrent API calls allowed */ concurrency?: number; - /** if a request is queued longer than this, it will be discarded and an error thrown (default: 0, disabled) */ + /** + * if a request is queued longer than this, it will be discarded and an error thrown + * (default: 0, disabled) + */ maxDelay?: number; + /** + * (Redis only): if true, immediately begin processing requests using the full quota, + * instead of waiting several seconds to discover other servers (default: false) + */ + fastStart?: boolean; } diff --git a/src/quota/redisQuotaManager.ts b/src/quota/redisQuotaManager.ts index e34d1bc..aaf6b41 100644 --- a/src/quota/redisQuotaManager.ts +++ b/src/quota/redisQuotaManager.ts @@ -10,7 +10,7 @@ export class RedisQuotaManager extends QuotaManager { private readonly pubSubClient: RedisClient; private readonly pingsReceived = new Map(); private readonly channelName: string; - private _ready = false; + private _ready: boolean; private heartbeatTimer: any = null; /** @@ -26,7 +26,14 @@ export class RedisQuotaManager extends QuotaManager { private readonly heartbeatInterval = 30000 ) { // start with 0 concurrency so jobs don’t run until we’re ready - super(Object.assign({}, channelQuota, { concurrency: 0 })); + super( + Object.assign( + {}, + channelQuota, + { concurrency: channelQuota.fastStart ? channelQuota.concurrency : 0 } + ) + ); + this._ready = Boolean(channelQuota.fastStart); this.channelName = `ratelimit-${channelName}`; this.pubSubClient = this.client.duplicate(); this.register(); @@ -44,7 +51,10 @@ export class RedisQuotaManager extends QuotaManager { this.ping(); - await sleep(3000); + if (!this.channelQuota.fastStart) { + await sleep(3000); + } + await this.updateQuota(); this._ready = true; diff --git a/test/redisQuotaManager.ts b/test/redisQuotaManager.ts index 9daa253..ac0fca0 100644 --- a/test/redisQuotaManager.ts +++ b/test/redisQuotaManager.ts @@ -134,7 +134,6 @@ test('RedisQuotaManager with undefined concurrency has zero concurrency before i t.is(qm.quota.concurrency, undefined); }); - test('maxDelay applies to RedisQuotaManager even before it’s ready', async t => { const client: RedisClient = redis.createClient(REDIS_PORT, REDIS_SERVER); const quota: Quota = { rate: 3, interval: 500, concurrency: 2, maxDelay: 250 }; @@ -144,3 +143,24 @@ test('maxDelay applies to RedisQuotaManager even before it’s ready', async t = await waitForReady(qm); t.is(qm.quota.maxDelay, 250); }); + +test('RedisQuotaManager with fastStart = true will process requests right away', + async t => +{ + const channelName = uniqueId(); + + const client: RedisClient = redis.createClient(REDIS_PORT, REDIS_SERVER); + const quota: Quota = { rate: 10, interval: 500, concurrency: 4, fastStart: true }; + const qm: RedisQuotaManager = new RedisQuotaManager(quota, channelName, client); + + const client2: RedisClient = redis.createClient(REDIS_PORT, REDIS_SERVER); + const qm2: RedisQuotaManager = new RedisQuotaManager(quota, channelName, client2); + + t.is(qm.quota.concurrency, quota.concurrency, 'starts with full concurrency quota'); + t.is(qm.quota.rate, quota.rate, 'starts with full rate quota'); + t.true(qm.ready, 'it’s ready immediately'); + // wait for peer discovery + await sleep(3000); + t.is(qm.quota.concurrency, Math.floor(quota.concurrency / 2), 'now has half the concurrency quota'); + t.is(qm.quota.rate, Math.floor(quota.rate / 2), 'now has half the rate quota'); +});