mirror of
https://github.com/redis/redis.git
synced 2026-05-28 04:02:46 -04:00
GCRA Rate Limiter (#14826)
# What Implement rate limiting functionality via GCRA algorithm. Introduce a new command `GCRA` to facilitate it. The implementation is heavily based on the popular [redis-cell](https://github.com/brandur/redis-cell) module (by [brandur](https://github.com/brandur)) with small changes in the API. # Why Rate limiting is a very common use case of redis and GCRA is one of the most popular algorithms used because of its simplicity and speed. Currently rate limiting with GCRA is possible via lua scripts or even client libraries via the relatively recent `SET IFEQ`/`DIGEST` commands ([redis-py example](https://gist.github.com/minchopaskal/b7acd4550f7144b88e2d0f86568a0d7b)). Implementing it directly inside redis gives us even faster performance. # API ``` GCRA key max_burst requests_per_period period [NUM_REQUESTS count] ``` ## Description Rate limit via GCRA. `requests_per_period` are allowed per `period` at a sustained rate. Thus we have a minimum spacing(emission interval) of `period`/`requests_per_period` seconds between each request. `max_burst` allows for occasional spikes by granting up to `max_burst` additional requests to be consumed at once. See more in the [GCRA wiki](https://en.wikipedia.org/wiki/Generic_cell_rate_algorithm). ## Options **KEY** - key related to specific rate limiting case **MAX_BURST** - maximum number of tokens allowed as a burst (in addition to the sustained rate). Min: 0 **REQUESTS_PER_PERIOD** - number of requests allowed per PERIOD. Min: 1 **PERIOD** - period in seconds as floating point number used for calculating the sustained rate. Min: 1.0 **NUM_REQUESTS** - cost (or weight) of this rate-limiting request. A higher cost drains the allowance faster. Default: 1 ### Note In redis-cell module and most other modules that are based on it PERIOD is given in seconds as integer. We decided to use floating point for greater flexibility. Internally time periods are calculated in microsecond granularity. ## Reply Reply is identical to reply of redis-cell ``` 127.0.0.1:6379> GCRA <key> <max_burst> <requests_per_period> <period> NUM_REQUESTS <count> 1) <limited> # 0 or 1 2) <max-req-num> # max number of request. Always equal to max_burst+1 3) <num-avail-req> # number of requests available immediately 4) <reply-after> # number of seconds after which caller should retry. Always returns -1 if request isn't limited. 5) <full-burst-after> # number of seconds after which a full burst will be allowed ``` --------- Co-authored-by: debing.sun <debing.sun@redis.com>
This commit is contained in:
parent
44157ee35e
commit
31a4356ac0
6 changed files with 630 additions and 1 deletions
|
|
@ -382,7 +382,7 @@ endif
|
|||
|
||||
REDIS_SERVER_NAME=redis-server$(PROG_SUFFIX)
|
||||
REDIS_SENTINEL_NAME=redis-sentinel$(PROG_SUFFIX)
|
||||
REDIS_SERVER_OBJ=threads_mngr.o memory_prefetch.o adlist.o quicklist.o ae.o anet.o dict.o ebuckets.o eventnotifier.o iothread.o mstr.o entry.o kvstore.o fwtree.o estore.o server.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o pubsub.o multi.o debug.o sort.o intset.o syncio.o cluster.o cluster_asm.o cluster_legacy.o cluster_slot_stats.o crc16.o endianconv.o slowlog.o eval.o bio.o rio.o rand.o memtest.o syscheck.o crcspeed.o crccombine.o crc64.o bitops.o sentinel.o notify.o setproctitle.o blocked.o hyperloglog.o latency.o sparkline.o redis-check-rdb.o redis-check-aof.o geo.o lazyfree.o module.o evict.o expire.o geohash.o geohash_helper.o childinfo.o defrag.o siphash.o rax.o t_stream.o listpack.o localtime.o lolwut.o lolwut5.o lolwut6.o lolwut8.o acl.o tracking.o socket.o tls.o sha256.o timeout.o setcpuaffinity.o monotonic.o mt19937-64.o resp_parser.o call_reply.o script_lua.o script.o functions.o function_lua.o commands.o strl.o connection.o unix.o logreqres.o keymeta.o chk.o hotkeys.o
|
||||
REDIS_SERVER_OBJ=threads_mngr.o memory_prefetch.o adlist.o quicklist.o ae.o anet.o dict.o ebuckets.o eventnotifier.o iothread.o mstr.o entry.o kvstore.o fwtree.o estore.o server.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o pubsub.o multi.o debug.o sort.o intset.o syncio.o cluster.o cluster_asm.o cluster_legacy.o cluster_slot_stats.o crc16.o endianconv.o slowlog.o eval.o bio.o rio.o rand.o memtest.o syscheck.o crcspeed.o crccombine.o crc64.o bitops.o sentinel.o notify.o setproctitle.o blocked.o hyperloglog.o latency.o sparkline.o redis-check-rdb.o redis-check-aof.o geo.o lazyfree.o module.o evict.o expire.o geohash.o geohash_helper.o childinfo.o defrag.o siphash.o rax.o t_stream.o listpack.o localtime.o lolwut.o lolwut5.o lolwut6.o lolwut8.o acl.o tracking.o socket.o tls.o sha256.o timeout.o setcpuaffinity.o monotonic.o mt19937-64.o resp_parser.o call_reply.o script_lua.o script.o functions.o function_lua.o commands.o strl.o connection.o unix.o logreqres.o keymeta.o chk.o hotkeys.o gcra.o
|
||||
REDIS_CLI_NAME=redis-cli$(PROG_SUFFIX)
|
||||
REDIS_CLI_OBJ=anet.o adlist.o dict.o redis-cli.o zmalloc.o release.o ae.o redisassert.o crcspeed.o crccombine.o crc64.o siphash.o crc16.o monotonic.o cli_common.o mt19937-64.o strl.o cli_commands.o
|
||||
REDIS_BENCHMARK_NAME=redis-benchmark$(PROG_SUFFIX)
|
||||
|
|
|
|||
|
|
@ -11039,6 +11039,34 @@ struct COMMAND_ARG DIGEST_Args[] = {
|
|||
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/********** GCRA ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
/* GCRA history */
|
||||
#define GCRA_History NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
/* GCRA tips */
|
||||
#define GCRA_Tips NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_KEY_SPECS_TABLE
|
||||
/* GCRA key specs */
|
||||
keySpec GCRA_Keyspecs[1] = {
|
||||
{NULL,CMD_KEY_RW|CMD_KEY_ACCESS|CMD_KEY_UPDATE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}
|
||||
};
|
||||
#endif
|
||||
|
||||
/* GCRA argument table */
|
||||
struct COMMAND_ARG GCRA_Args[] = {
|
||||
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("max-burst",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("requests-per-period",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("period",ARG_TYPE_DOUBLE,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("count",ARG_TYPE_INTEGER,-1,"NUM_REQUESTS",NULL,NULL,CMD_ARG_OPTIONAL,0,NULL)},
|
||||
};
|
||||
|
||||
/********** GET ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
|
|
@ -11969,6 +11997,7 @@ struct COMMAND_STRUCT redisCommandTable[] = {
|
|||
{MAKE_CMD("decrby","Decrements a number from the integer value of a key. Uses 0 as initial value if the key doesn't exist.","O(1)","1.0.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,DECRBY_History,0,DECRBY_Tips,0,decrbyCommand,3,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_STRING,DECRBY_Keyspecs,1,NULL,2),.args=DECRBY_Args},
|
||||
{MAKE_CMD("delex","Conditionally removes the specified key based on value or digest comparison.","O(1) for IFEQ/IFNE, O(N) for IFDEQ/IFDNE where N is the length of the string value.","8.4.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,DELEX_History,0,DELEX_Tips,0,delexCommand,-2,CMD_WRITE|CMD_FAST,ACL_CATEGORY_STRING,DELEX_Keyspecs,1,delexGetKeys,2),.args=DELEX_Args},
|
||||
{MAKE_CMD("digest","Returns the XXH3 hash of a string value.","O(N) where N is the length of the string value.","8.4.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,DIGEST_History,0,DIGEST_Tips,0,digestCommand,2,CMD_READONLY|CMD_FAST,ACL_CATEGORY_STRING,DIGEST_Keyspecs,1,NULL,1),.args=DIGEST_Args},
|
||||
{MAKE_CMD("gcra","Rate limit via GCRA (Generic Cell Rate Algorithm).","O(1)","8.8.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,GCRA_History,0,GCRA_Tips,0,gcraCommand,-5,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_STRING,GCRA_Keyspecs,1,NULL,5),.args=GCRA_Args},
|
||||
{MAKE_CMD("get","Returns the string value of a key.","O(1)","1.0.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,GET_History,0,GET_Tips,0,getCommand,2,CMD_READONLY|CMD_FAST,ACL_CATEGORY_STRING,GET_Keyspecs,1,NULL,1),.args=GET_Args},
|
||||
{MAKE_CMD("getdel","Returns the string value of a key after deleting the key.","O(1)","6.2.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,GETDEL_History,0,GETDEL_Tips,0,getdelCommand,2,CMD_WRITE|CMD_FAST,ACL_CATEGORY_STRING,GETDEL_Keyspecs,1,NULL,1),.args=GETDEL_Args},
|
||||
{MAKE_CMD("getex","Returns the string value of a key after setting its expiration time.","O(1)","6.2.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,GETEX_History,0,GETEX_Tips,0,getexCommand,-2,CMD_WRITE|CMD_FAST,ACL_CATEGORY_STRING,GETEX_Keyspecs,1,NULL,2),.args=GETEX_Args},
|
||||
|
|
|
|||
92
src/commands/gcra.json
Normal file
92
src/commands/gcra.json
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
{
|
||||
"GCRA": {
|
||||
"summary": "Rate limit via GCRA (Generic Cell Rate Algorithm).",
|
||||
"complexity": "O(1)",
|
||||
"group": "string",
|
||||
"since": "8.8.0",
|
||||
"arity": -5,
|
||||
"function": "gcraCommand",
|
||||
"command_flags": [
|
||||
"WRITE",
|
||||
"DENYOOM",
|
||||
"FAST"
|
||||
],
|
||||
"acl_categories": [
|
||||
"STRING"
|
||||
],
|
||||
"key_specs": [
|
||||
{
|
||||
"flags": [
|
||||
"RW",
|
||||
"ACCESS",
|
||||
"UPDATE"
|
||||
],
|
||||
"begin_search": {
|
||||
"index": {
|
||||
"pos": 1
|
||||
}
|
||||
},
|
||||
"find_keys": {
|
||||
"range": {
|
||||
"lastkey": 0,
|
||||
"step": 1,
|
||||
"limit": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"reply_schema": {
|
||||
"type": "array",
|
||||
"minItems": 5,
|
||||
"maxItems": 5,
|
||||
"description": "Rate limiting result",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Limited: 0 if allowed, 1 if rate limited"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Max request number: always equal to max_burst+1"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Number of requests available immediately"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Retry after: seconds after which caller should retry. Always -1 if not limited"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Full burst after: seconds after which a full burst will be allowed"
|
||||
}
|
||||
]
|
||||
},
|
||||
"arguments": [
|
||||
{
|
||||
"name": "key",
|
||||
"type": "key",
|
||||
"key_spec_index": 0
|
||||
},
|
||||
{
|
||||
"name": "max-burst",
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"name": "requests-per-period",
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"name": "period",
|
||||
"type": "double"
|
||||
},
|
||||
{
|
||||
"name": "count",
|
||||
"type": "integer",
|
||||
"token": "NUM_REQUESTS",
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
241
src/gcra.c
Normal file
241
src/gcra.c
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
/*
|
||||
* Copyright (c) 2026-Present, Redis Ltd.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Licensed under your choice of (a) the Redis Source Available License 2.0
|
||||
* (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
|
||||
* GNU Affero General Public License v3 (AGPLv3).
|
||||
*/
|
||||
#include "server.h"
|
||||
#include <math.h>
|
||||
|
||||
/* GCRA algorithm for rate limiting.
|
||||
* Implementation is heavily based on the implementation of (redis-cell)
|
||||
* [https://github.com/brandur/redis-cell] by (brandur)[https://github.com/brandur].
|
||||
*
|
||||
* It is a leaky-bucket type algorithm but instead of periodically dripping, we
|
||||
* calculate the next time the bucket has capacity - called
|
||||
* Theoretical arrival time(TaT) by the algorithm. We allow requests at a
|
||||
* sustained rate (f.e 5 request per 10 seconds, i.e 1 request per 2 seconds)
|
||||
* but also allow bursts of multiple request at one time.
|
||||
*
|
||||
* Explanation of the algorithm follows using the leaky-bucket analogy.
|
||||
*
|
||||
* GCRA works by keeping track of the next TaT and updating it after requests
|
||||
* are allowed. Let T be the emission interval for a request - in the
|
||||
* leaky-bucket analogy this will be the period at which the bucket drips.
|
||||
* Using N requests will result in the (next TaT) = (current TaT) + N * T (time
|
||||
* needed to drain the bucket). To determine if a request can be allowed we can
|
||||
* calculate the time at which "the bucket dripped", which is TaT-T.
|
||||
* If this time is in the past the request is allowed, otherwise we wait and TaT
|
||||
* is not updated. This only accounts for 1 request though. In order to allow
|
||||
* bursts we can imagine a full burst fully filling an empty bucket, thus
|
||||
* we need to calculate the time after which "the bucket will completely drain"
|
||||
* the requests of the burst - this will be t = T * max_burst.
|
||||
* At last the allowance check will be:
|
||||
*
|
||||
* "now" >= TaT - (T + t)
|
||||
*
|
||||
* And in this case a picture is worth about 250 words:
|
||||
*
|
||||
* +-------------------+
|
||||
* | ALLOWED REQUEST |
|
||||
* +-------------------+
|
||||
*
|
||||
* +-----------+ +-----+ +-----+
|
||||
* | allow at | | now | | TaT |
|
||||
* | (past) | +-----+ +-----+
|
||||
* +-----------+ | |
|
||||
* | |
|
||||
* ---+-----------------------+----------+-----------> time
|
||||
* |//////////////////////////////////|
|
||||
* |//////////////////////////////////|
|
||||
* +----------------------------------+
|
||||
* | |
|
||||
* |<------------- t + T ------------>|
|
||||
*
|
||||
*
|
||||
* +------------------------------------------+
|
||||
* | T = Emission interval |
|
||||
* | t = Capacity of bucket |
|
||||
* | t + T = Delay variation tolerance |
|
||||
* | tat = Theoretical arrival time |
|
||||
* | now = Actual time of request |
|
||||
* +------------------------------------------+
|
||||
*
|
||||
* (ASCII art adapted from https://brandur.org/rate-limiting). */
|
||||
|
||||
/* GCRA key max_burst requests_per_period period [NUM_REQUESTS count]
|
||||
*
|
||||
* key: Key related to specific rate limiting case
|
||||
* max_burst: Maximum requests allowed as burst (in addition to sustained rate)
|
||||
* requests_per_period: Number of requests allowed per period
|
||||
* period: Period in seconds for calculating sustained rate
|
||||
* num_requests: Optional, cost of this request (default: 1)
|
||||
*/
|
||||
void gcraCommand(client *c) {
|
||||
robj *key = c->argv[1];
|
||||
|
||||
/* GCRA parameters */
|
||||
long max_burst;
|
||||
long requests_per_period;
|
||||
long num_requests = 1;
|
||||
double period;
|
||||
|
||||
/* Variables used in the reply */
|
||||
int limited; /* Whether or not the request was limited */
|
||||
long long remaining = 0; /* Number of requests available immediately */
|
||||
long long retry_after_s = -1; /* Time in seconds after which the caller can retry */
|
||||
long long reset_after_s = 0; /* Number of seconds after which a full burst will be allowed */
|
||||
|
||||
if (c->argc > 7) {
|
||||
addReplyErrorArity(c);
|
||||
return;
|
||||
}
|
||||
|
||||
if (getPositiveLongFromObjectOrReply(c, c->argv[2], &max_burst, NULL) != C_OK) {
|
||||
return;
|
||||
}
|
||||
if (likely(max_burst < LONG_MAX)) max_burst += 1;
|
||||
|
||||
if (getRangeLongFromObjectOrReply(c, c->argv[3], 1, LONG_MAX, &requests_per_period, NULL) != C_OK) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (getDoubleFromObjectOrReply(c, c->argv[4], &period, NULL) != C_OK) {
|
||||
return;
|
||||
}
|
||||
if (period <= 0 || period >= 1e12) {
|
||||
addReplyError(c, "period must be > 0 and < 1e12");
|
||||
return;
|
||||
}
|
||||
|
||||
if (c->argc >= 6) {
|
||||
if (strcasecmp("NUM_REQUESTS", c->argv[5]->ptr)) {
|
||||
addReplyErrorObject(c, shared.syntaxerr);
|
||||
return;
|
||||
}
|
||||
if (c->argc == 6) {
|
||||
addReplyError(c, "Missing NUM_REQUESTS value");
|
||||
return;
|
||||
}
|
||||
if (getRangeLongFromObjectOrReply(c, c->argv[6], 1, LONG_MAX, &num_requests, NULL) != C_OK) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ustime_t now = commandTimeSnapshot() * 1000;
|
||||
|
||||
long long tat_us, new_tat_us;
|
||||
dictEntryLink link;
|
||||
kvobj *kv = lookupKeyWriteWithLink(c->db, key, &link);
|
||||
if (checkType(c, kv, OBJ_STRING)) {
|
||||
return;
|
||||
}
|
||||
if (kv != NULL) {
|
||||
/* Note the value of the key may have been overwritten outside of the
|
||||
* GCRA command (f.e by calling SET). We don't try to catch such errors
|
||||
* as this would be possible only with a dedicated structures for GCRA,
|
||||
* while using STRING gives us all the benefits of a redis key -
|
||||
* replication, setting expiration, etc. */
|
||||
if (getLongLongFromObject(kv, &tat_us) != C_OK) {
|
||||
addReplyError(c, "Invalid GCRA key");
|
||||
return;
|
||||
}
|
||||
if (tat_us <= 0) {
|
||||
addReplyError(c, "Negative time is invalid value for GCRA");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
tat_us = now;
|
||||
}
|
||||
|
||||
/* microsecond accuracy */
|
||||
double period_us = period * 1000000.;
|
||||
|
||||
/* Emission interval is the minimum amount of time between requests.
|
||||
* Note on calculations:
|
||||
* Even if emission_interval_us becomes less than 1us, we assume it's min
|
||||
* 1ms. The API is already in seconds granularity so it is expected the user
|
||||
* won't need a submicrosecond accuracy. */
|
||||
long long emission_interval_us = (long long)(period_us / requests_per_period + 0.5);
|
||||
if (unlikely(emission_interval_us == 0)) emission_interval_us = 1;
|
||||
|
||||
/* overflow checks. In normal circumstances we shouldn't get these but the
|
||||
* user may have wrongfully specified very large values.
|
||||
* Note that all values are positive. */
|
||||
if (emission_interval_us > LLONG_MAX / num_requests) {
|
||||
addReplyError(c, "GCRA limiting uses microsecond accuracy. Combination of period, requests_per_period and num_requests would cause an overflow");
|
||||
return;
|
||||
}
|
||||
if (emission_interval_us > LLONG_MAX / max_burst) {
|
||||
addReplyError(c, "GCRA limiting uses microsecond accuracy. Combination of period, requests_per_period and max_burst would cause an overflow");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Max bursts give us an amount of requests we can use up at one time.
|
||||
* The variance will calculate the amount of time that many request need
|
||||
* to "refill the bucket". */
|
||||
long long variance_us = emission_interval_us * max_burst;
|
||||
|
||||
/* If a request is allowed the next TaT is after an emission_interval_us time.
|
||||
* Hence for multiple requests we multiple by their number. */
|
||||
long long increment_us = emission_interval_us * num_requests;
|
||||
|
||||
long long base_us = (now > tat_us) ? now : tat_us;
|
||||
if (LLONG_MAX - base_us < increment_us) {
|
||||
addReplyError(c, "GCRA limiting uses microsecond accuracy. Combination of period, requests_per_period and num_requests would cause an overflow");
|
||||
return;
|
||||
}
|
||||
new_tat_us = base_us + increment_us;
|
||||
|
||||
/* Calculate the time a request is allowed. This is TaT, but because we allow
|
||||
* a burst we move that time in the past. If the allow time is before the
|
||||
* time we ask (i.e now) we allow the request, otherwise we limit it and
|
||||
* calculate after how much time the user should retry. */
|
||||
long long allow_at = new_tat_us - variance_us;
|
||||
long long diff_us = now - allow_at;
|
||||
long long ttl_us;
|
||||
if (diff_us < 0) {
|
||||
limited = 1;
|
||||
/* NOTE: if increment is more than variance, then number of requests is
|
||||
* more than what is maximally allowed (i.e max_bursts + 1) so we leave
|
||||
* retry_after_s to -1 in this case, as it should never be retried. */
|
||||
if (increment_us <= variance_us) {
|
||||
retry_after_s = ceil((-diff_us) / 1000000.);
|
||||
}
|
||||
ttl_us = tat_us - now;
|
||||
} else {
|
||||
limited = 0;
|
||||
ttl_us = new_tat_us - now;
|
||||
robj *tatobj = createStringObjectFromLongLong(new_tat_us);
|
||||
setKeyByLink(c, c->db, key, &tatobj, kv ? SETKEY_ALREADY_EXIST : SETKEY_DOESNT_EXIST, &link);
|
||||
notifyKeyspaceEvent(NOTIFY_STRING,"set",key,c->db->id);
|
||||
|
||||
long long when = new_tat_us / 1000;
|
||||
kv = setExpireByLink(c, c->db, key->ptr, when, link);
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC,"expire",key,c->db->id);
|
||||
|
||||
/* Replicating the command directly would mess up TaT as we use
|
||||
* commandTimeSnapshot. We instead rewrite the command as SET with the
|
||||
* appropriate expire time. */
|
||||
robj *pexat_obj = createStringObjectFromLongLong(when);
|
||||
rewriteClientCommandVector(c, 5, shared.set, key, kv, shared.pxat, pexat_obj);
|
||||
decrRefCount(pexat_obj);
|
||||
|
||||
server.dirty++;
|
||||
}
|
||||
|
||||
long long next_us = variance_us - ttl_us;
|
||||
if (next_us > -emission_interval_us) {
|
||||
remaining = next_us / emission_interval_us;
|
||||
}
|
||||
reset_after_s = ceil(ttl_us / 1000000.);
|
||||
|
||||
addReplyArrayLen(c, 5);
|
||||
addReply(c, limited ? shared.cone : shared.czero);
|
||||
addReplyLongLong(c, max_burst);
|
||||
addReplyLongLong(c, remaining);
|
||||
addReplyLongLong(c, retry_after_s);
|
||||
addReplyLongLong(c, reset_after_s);
|
||||
}
|
||||
|
|
@ -4437,6 +4437,7 @@ void quitCommand(client *c);
|
|||
void resetCommand(client *c);
|
||||
void failoverCommand(client *c);
|
||||
void digestCommand(client *c);
|
||||
void gcraCommand(client *c);
|
||||
|
||||
#if defined(__GNUC__)
|
||||
void *calloc(size_t count, size_t size) __attribute__ ((deprecated));
|
||||
|
|
|
|||
266
tests/unit/gcra.tcl
Normal file
266
tests/unit/gcra.tcl
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
start_server {tags {"gcra" "external:skip"}} {
|
||||
test {GCRA - argument validation} {
|
||||
# Wrong number of arguments (too few)
|
||||
catch {r gcra} err
|
||||
assert_match "*wrong number of arguments*" $err
|
||||
catch {r gcra mykey} err
|
||||
assert_match "*wrong number of arguments*" $err
|
||||
catch {r gcra mykey 10} err
|
||||
assert_match "*wrong number of arguments*" $err
|
||||
catch {r gcra mykey 10 5} err
|
||||
assert_match "*wrong number of arguments*" $err
|
||||
|
||||
# max_burst must be non-negative integer
|
||||
catch {r gcra mykey -1 5 10} err
|
||||
assert_match "*out of range*" $err
|
||||
catch {r gcra mykey notanumber 5 10} err
|
||||
assert_match "*out of range*" $err
|
||||
|
||||
# tokens_per_period must be >= 1
|
||||
catch {r gcra mykey 10 0 10} err
|
||||
assert_match "*out of range*" $err
|
||||
catch {r gcra mykey 10 -1 10} err
|
||||
assert_match "*out of range*" $err
|
||||
catch {r gcra mykey 10 notanumber 10} err
|
||||
assert_match "*not an integer*" $err
|
||||
|
||||
# period must be positive
|
||||
catch {r gcra mykey 10 5 0} err
|
||||
assert_match "*period must be > 0*" $err
|
||||
catch {r gcra mykey 10 5 -1} err
|
||||
assert_match "*period must be > 0*" $err
|
||||
catch {r gcra mykey 10 5 notanumber} err
|
||||
assert_match "*not a valid float*" $err
|
||||
|
||||
# tokens (optional) must be >= 1
|
||||
catch {r gcra mykey 10 5 10 NUM_REQUESTS} err
|
||||
assert_match "*Missing NUM_REQUESTS value*" $err
|
||||
catch {r gcra mykey 10 5 10 NUM_REQUESTS 0} err
|
||||
assert_match "*out of range*" $err
|
||||
catch {r gcra mykey 10 5 10 NUM_REQUESTS -1} err
|
||||
assert_match "*out of range*" $err
|
||||
catch {r gcra mykey 10 5 10 NUM_REQUESTS notanumber} err
|
||||
assert_match "*not an integer*" $err
|
||||
|
||||
# Valid arguments with default tokens
|
||||
r del mykey
|
||||
set result [r gcra mykey 10 5 60]
|
||||
assert_equal 5 [llength $result]
|
||||
set limited [lindex $result 0]
|
||||
set max_req_num [lindex $result 1]
|
||||
assert_equal $limited 0
|
||||
assert_equal 11 $max_req_num
|
||||
|
||||
# Valid arguments with explicit tokens
|
||||
r del mykey
|
||||
set result [r gcra mykey 10 5 60 NUM_REQUESTS 2]
|
||||
assert_equal 5 [llength $result]
|
||||
assert_equal 11 [lindex $result 1]
|
||||
|
||||
# Period accepts fractional seconds
|
||||
r del mykey
|
||||
set result [r gcra mykey 10 5 0.5]
|
||||
assert_equal 5 [llength $result]
|
||||
}
|
||||
|
||||
test {GCRA - first request is allowed} {
|
||||
r del mykey
|
||||
set result [r gcra mykey 5 10 60]
|
||||
set limited [lindex $result 0]
|
||||
# First request should not be limited
|
||||
assert_equal 0 $limited
|
||||
}
|
||||
|
||||
test {GCRA - requests within burst are allowed} {
|
||||
r del mykey
|
||||
# max_burst=5, tokens_per_period=1, period=60
|
||||
# This allows burst of 6 requests (max_burst + 1)
|
||||
for {set i 0} {$i < 6} {incr i} {
|
||||
set result [r gcra mykey 5 1 60]
|
||||
set limited [lindex $result 0]
|
||||
assert_equal 0 $limited "Request $i should be allowed"
|
||||
}
|
||||
# Request 6 should be limited
|
||||
set result [r gcra mykey 5 1 60]
|
||||
set limited [lindex $result 0]
|
||||
assert_equal 1 $limited "Request 6 should be limited"
|
||||
}
|
||||
|
||||
test {GCRA - retry_after is positive when limited} {
|
||||
r del mykey
|
||||
# Exhaust burst
|
||||
for {set i 0} {$i < 3} {incr i} {
|
||||
r gcra mykey 2 1 60
|
||||
}
|
||||
# Next request should be limited with positive retry_after
|
||||
set result [r gcra mykey 2 1 60]
|
||||
set limited [lindex $result 0]
|
||||
set retry_after [lindex $result 3]
|
||||
assert_equal 1 $limited
|
||||
assert {$retry_after > 0}
|
||||
}
|
||||
|
||||
test {GCRA - retry_after is negative when allowed} {
|
||||
r del mykey
|
||||
set result [r gcra mykey 5 1 60]
|
||||
set limited [lindex $result 0]
|
||||
set retry_after [lindex $result 3]
|
||||
assert_equal 0 $limited
|
||||
assert {$retry_after < 0}
|
||||
}
|
||||
|
||||
test {GCRA - num_avail_req decreases with each request} {
|
||||
r del mykey
|
||||
set result1 [r gcra mykey 5 1 60]
|
||||
set avail1 [lindex $result1 2]
|
||||
|
||||
set result2 [r gcra mykey 5 1 60]
|
||||
set avail2 [lindex $result2 2]
|
||||
|
||||
assert {$avail2 < $avail1}
|
||||
}
|
||||
|
||||
test {GCRA - multiple tokens consumed per request} {
|
||||
r del mykey
|
||||
# max_burst=5, so 6 tokens available initially
|
||||
# Consume 1 token (default)
|
||||
set result1 [r gcra mykey 5 1 60]
|
||||
set avail1 [lindex $result1 2]
|
||||
assert_equal 5 $avail1
|
||||
|
||||
r del mykey
|
||||
# Consume 3 tokens from fresh state
|
||||
set result2 [r gcra mykey 5 1 60 NUM_REQUESTS 3]
|
||||
set avail2 [lindex $result2 2]
|
||||
assert_equal 3 $avail2
|
||||
}
|
||||
|
||||
test {GCRA - rate recovery over time} {
|
||||
r del mykey
|
||||
# max_burst=1, tokens_per_period=1, period=1 (1 token per second)
|
||||
# Exhaust: 2 allowed (burst+1), then limited
|
||||
r gcra mykey 1 1 1
|
||||
r gcra mykey 1 1 1
|
||||
set result [r gcra mykey 1 1 1]
|
||||
assert_equal 1 [lindex $result 0] "Should be limited"
|
||||
|
||||
# Wait for rate to recover
|
||||
after 1100
|
||||
|
||||
# Should be allowed again
|
||||
set result [r gcra mykey 1 1 1]
|
||||
assert_equal 0 [lindex $result 0] "Should be allowed after recovery"
|
||||
}
|
||||
|
||||
test {GCRA - full_burst_after indicates time to full recovery} {
|
||||
r del mykey
|
||||
# Consume some tokens
|
||||
r gcra mykey 5 1 60
|
||||
r gcra mykey 5 1 60
|
||||
|
||||
set result [r gcra mykey 5 1 60]
|
||||
set full_burst_after [lindex $result 4]
|
||||
|
||||
# full_burst_after should be positive (time until full burst available)
|
||||
# Since we've taken from the burst twice the reset time was incremented
|
||||
# by the rate twice
|
||||
assert {$full_burst_after >= 179}
|
||||
|
||||
r del mykey
|
||||
r gcra mykey 5 1 60
|
||||
set result [r gcra mykey 5 1 60 NUM_REQUESTS 4]
|
||||
set full_burst_after [lindex $result 4]
|
||||
assert {$full_burst_after >= 299}
|
||||
}
|
||||
|
||||
test {GCRA - different keys are independent} {
|
||||
r del key1
|
||||
r del key2
|
||||
|
||||
# Exhaust key1
|
||||
for {set i 0} {$i < 3} {incr i} {
|
||||
r gcra key1 2 1 60
|
||||
}
|
||||
set result1 [r gcra key1 2 1 60]
|
||||
assert_equal 1 [lindex $result1 0] "key1 should be limited"
|
||||
|
||||
# key2 should still be available
|
||||
set result2 [r gcra key2 2 1 60]
|
||||
assert_equal 0 [lindex $result2 0] "key2 should be allowed"
|
||||
}
|
||||
|
||||
test {GCRA - max_burst of 0 allows only sustained rate} {
|
||||
r del mykey
|
||||
# max_burst=0, tokens_per_period=1, period=1
|
||||
# Only 1 request allowed initially (0+1)
|
||||
set result [r gcra mykey 0 1 1]
|
||||
assert_equal 0 [lindex $result 0] "First request allowed"
|
||||
assert_equal 1 [lindex $result 1] "max_req_num should be 1"
|
||||
|
||||
# Second request should be limited
|
||||
set result [r gcra mykey 0 1 1]
|
||||
assert_equal 1 [lindex $result 0] "Second request limited"
|
||||
}
|
||||
|
||||
test {GCRA - wrong type error} {
|
||||
r del mykey
|
||||
r lpush mykey "value"
|
||||
catch {r gcra mykey 5 1 60} err
|
||||
assert_match "*WRONGTYPE*" $err
|
||||
|
||||
r del mykey
|
||||
r sadd mykey "value"
|
||||
catch {r gcra mykey 5 1 60} err
|
||||
assert_match "*WRONGTYPE*" $err
|
||||
}
|
||||
|
||||
test {GCRA - overflow} {
|
||||
r del mykey
|
||||
catch {r gcra mykey 1 1 86400 NUM_REQUESTS 200000000} err
|
||||
assert_match "*would cause an overflow*" $err
|
||||
|
||||
r del mykey
|
||||
catch {r gcra mykey 200000000 1 86400} err
|
||||
assert_match "*would cause an overflow*" $err
|
||||
|
||||
r del mykey
|
||||
catch {r gcra mykey 1 1 2147483647 NUM_REQUESTS 2147483647} err
|
||||
assert_match "*would cause an overflow*" $err
|
||||
}
|
||||
}
|
||||
|
||||
start_server {tags {"gcra repl" "external:skip"}} {
|
||||
set replica [srv 0 client]
|
||||
set replica_host [srv 0 host]
|
||||
set replica_port [srv 0 port]
|
||||
set replica_log [srv 0 stdout]
|
||||
|
||||
start_server {tags {}} {
|
||||
set master [srv 0 client]
|
||||
set master_host [srv 0 host]
|
||||
set master_port [srv 0 port]
|
||||
|
||||
$master flushdb
|
||||
$replica flushdb
|
||||
|
||||
$replica replicaof $master_host $master_port
|
||||
wait_for_condition 100 100 {
|
||||
[s -1 master_link_status] eq "up"
|
||||
} else {
|
||||
fail "Master <-> Replica didn't finish sync"
|
||||
}
|
||||
|
||||
set cmdinfo [$replica info commandstats]
|
||||
assert_equal [lsearch -glob $cmdinfo "cmdstat_gcra:*"] -1
|
||||
assert_equal [lsearch -glob $cmdinfo "cmdstat_set:*"] -1
|
||||
|
||||
$master del mykey
|
||||
$master gcra mykey 2 1 1000 NUM_REQUESTS 2
|
||||
|
||||
wait_for_ofs_sync $master $replica
|
||||
|
||||
set cmdinfo [$replica info commandstats]
|
||||
assert_equal [lsearch -glob $cmdinfo "cmdstat_gcra:*"] -1
|
||||
assert_morethan_equal [lsearch -glob $cmdinfo "cmdstat_set:*"] 0
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue