Skip to content

Commit

Permalink
add a fastStart option when using Redis
Browse files Browse the repository at this point in the history
  • Loading branch information
natesilva committed Oct 23, 2017
1 parent 81c82fb commit aa52bb1
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 5 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,16 @@ 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`.

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.
Expand Down Expand Up @@ -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
10 changes: 9 additions & 1 deletion src/quota/quota.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
16 changes: 13 additions & 3 deletions src/quota/redisQuotaManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export class RedisQuotaManager extends QuotaManager {
private readonly pubSubClient: RedisClient;
private readonly pingsReceived = new Map<string, number>();
private readonly channelName: string;
private _ready = false;
private _ready: boolean;
private heartbeatTimer: any = null;

/**
Expand All @@ -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();
Expand All @@ -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;

Expand Down
22 changes: 21 additions & 1 deletion test/redisQuotaManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand All @@ -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');
});

0 comments on commit aa52bb1

Please sign in to comment.