Skip to content

Commit

Permalink
feat:add Redis hashtag in elevated limit keys (#74)
Browse files Browse the repository at this point in the history
  • Loading branch information
pubalokta authored Jun 28, 2024
1 parent 5f8eafb commit f3f6c75
Show file tree
Hide file tree
Showing 7 changed files with 294 additions and 167 deletions.
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,31 @@ When ERL is triggered, it will keep activated for the `erl_activation_period_sec

The amount of minutes per month allowed in ERL mode is defined by: `quota_per_calendar_month * erl_activation_period_seconds / 60`.

The overrides in ERL work the same way as for the regular bucket. Both size and per_interval are mandatory when specifying an override.
The overrides in ERL work the same way as for the regular bucket. Both size and per_interval are mandatory when specifying an override.

### Use of Redis hash tags
In order to comply with [Redis clustering best practices](https://redis.io/blog/redis-clustering-best-practices-with-keys/) when using multi-key operations,
we always add a hash tag to both `erl_quota_key` and `erl_is_active_key`, so all keys within the multi-key operation resolve to the same
hash slot.

In order to do this, we follow [Redis' hash-tag rules](https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec/#hash-tags) on what to identify as a hash tag.

Basically, what constitutes a hashtag is text within curly braces. e.g. "{tag}".

Additionally, we added 2 extra rules:
- If either the `type` or `key` arguments provided within the call to the takeElevated method contain a hashtag, we use that one to hash-tag the ERL keys
- If the previous rule is not met, we use the entire `type:key` as hashtag.

Examples:

| Situation | Call | Identified Hashtag | main_key | erl_is_active_key | erl_quota_key |
|----------------------------|---------------------------------------------------------------|---------------------------|-------------------------------------|------------------------------------------|-----------------------------------------|
| No hashtag provided | `limitd.takeElevated('bucketName', 'some-key')` | `bucketName:some-key` | `bucketName:some-key` | `ERLActiveKey:{bucketName:some-key}` | `ERLQuotaKey:{bucketName:some-key}` |
| Single hashtag | `limitd.takeElevated('bucketName', '{some-key}')` | `some-key` | `bucketName:{some-key}` | `ERLActiveKey:{some-key}` | `ERLQuotaKey:{some-key}` |
| Multiple hashtags | `limitd.takeElevated('bucketName', '{some-key}{anotherkey}')` | `some-key` | `bucketName:{some-key}{anotherkey}` | `ERLActiveKey:{some-key}` | `ERLQuotaKey:{some-key}` |
| Curly brace within hashtag | `limitd.takeElevated('bucketName', '{{some-key}')` | `{some-key` | `bucketName:{{some-key}` | `ERLActiveKey:{{some-key}` | `ERLQuotaKey:{{some-key}` |
| Empty hashtag | `limitd.takeElevated('bucketName', '{}{some-key}')` | `bucketName:{}{some-key}` | `bucketName:{}{some-key}` | `ERLActiveKey:{bucketName:{}{some-key}}` | `ERLQuotaKey:{bucketName:{}{some-key}}` |


## Breaking changes from `Limitdb`

Expand Down
2 changes: 1 addition & 1 deletion lib/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ class LimitDBRedis extends EventEmitter {
}

this._doTake(params, callback, (key, bucketKeyConfig, count) => {
const elevated_limits = resolveElevatedParams(erlParams, bucketKeyConfig);
const elevated_limits = resolveElevatedParams(erlParams, bucketKeyConfig, key, this.prefix);
const erl_quota_expiration = calculateQuotaExpiration(elevated_limits);
this.redis.takeElevated(key, elevated_limits.erl_is_active_key, elevated_limits.erl_quota_key,
bucketKeyConfig.ms_per_interval || 0,
Expand Down
42 changes: 28 additions & 14 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,31 +178,44 @@ function endOfMonthTimestamp() {
return Date.UTC(curDate.getUTCFullYear(), curDate.getUTCMonth() + 1, 1, 0, 0, 0, 0);
}

function resolveElevatedParams(erlParams, bucketKeyConfig) {
function resolveElevatedParams(erlParams, bucketKeyConfig, key, prefix) {
// provide default values for elevated_limits unless the bucketKeyConfig has them
return {
const elevatedLimits = {
ms_per_interval: bucketKeyConfig.ms_per_interval,
size: bucketKeyConfig.size,
erl_activation_period_seconds: 0,
erl_quota: 0,
erl_quota_interval: 'quota_per_calendar_month',
erl_is_active_key: 'defaultActiveKey',
erl_quota_key: 'defaultQuotaKey',
erl_is_active_key: 'ERLActiveKey',
erl_quota_key: 'ERLQuotaKey',
...erlParams,
...bucketKeyConfig.elevated_limits,
erl_configured_for_bucket: !!(erlParams && bucketKeyConfig.elevated_limits?.erl_configured_for_bucket),
};

elevatedLimits.erl_is_active_key = replicateHashtag(key, prefix, elevatedLimits.erl_is_active_key)
elevatedLimits.erl_quota_key = replicateHashtag(key, prefix, elevatedLimits.erl_quota_key)

return elevatedLimits;
}

class LimitdRedisConfigurationError extends Error {
constructor(msg, extra) {
super();
this.name = this.constructor.name;
this.message = msg;
Error.captureStackTrace(this, this.constructor);
if (extra) {
this.extra = extra;
}
function replicateHashtag(baseKey, prefix, key) {
const prefixedBaseKey = key + `:{${prefix}${baseKey}}`;
const idxOpenBrace = baseKey.indexOf('{')
if (idxOpenBrace < 0) {
return prefixedBaseKey;
}

const idxCloseBrace = baseKey.indexOf('}', idxOpenBrace)
if ( idxCloseBrace <= idxOpenBrace ) {
return prefixedBaseKey;
}

let hashtag = baseKey.slice(idxOpenBrace+1, idxCloseBrace);
if (hashtag.length > 0) {
return key + `:{${hashtag}}`;
} else {
return prefixedBaseKey;
}
}

Expand All @@ -218,5 +231,6 @@ module.exports = {
getERLParams,
endOfMonthTimestamp,
calculateQuotaExpiration,
resolveElevatedParams
resolveElevatedParams,
replicateHashtag
};
9 changes: 0 additions & 9 deletions lib/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,6 @@ function validateERLParams(params) {
return new LimitdRedisValidationError('elevated_limits object is required for elevated limits', { code: 107 });
}

// redis' way of knowing whether erl is active or not
if (typeof params.erl_is_active_key !== 'string') {
return new LimitdRedisValidationError('erl_is_active_key is required for elevated limits', { code: 108 });
}

if (typeof params.erl_quota_key !== 'string') {
return new LimitdRedisValidationError('erl_quota_key is required for elevated limits', { code: 110 });
}

if (typeof params.erl_activation_period_seconds !== 'number') {
return new LimitdRedisValidationError('erl_activation_period_seconds is required for elevated limits', { code: 111 });
}
Expand Down
Loading

0 comments on commit f3f6c75

Please sign in to comment.