mirror of
https://github.com/redis/redis.git
synced 2026-05-28 04:02:46 -04:00
Hold GCRA out of the release (#15191)
Some checks failed
CI / test-ubuntu-latest (push) Waiting to run
CI / test-sanitizer-address (push) Waiting to run
CI / build-debian-old (push) Waiting to run
CI / build-macos-latest (push) Waiting to run
CI / build-32bit (push) Waiting to run
CI / build-libc-malloc (push) Waiting to run
CI / build-centos-jemalloc (push) Waiting to run
CI / build-old-chain-jemalloc (push) Waiting to run
Codecov / code-coverage (push) Waiting to run
External Server Tests / test-external-standalone (push) Waiting to run
External Server Tests / test-external-cluster (push) Waiting to run
External Server Tests / test-external-nodebug (push) Waiting to run
Spellcheck / Spellcheck (push) Waiting to run
Reply-schemas linter / reply-schemas-linter (push) Has been cancelled
Some checks failed
CI / test-ubuntu-latest (push) Waiting to run
CI / test-sanitizer-address (push) Waiting to run
CI / build-debian-old (push) Waiting to run
CI / build-macos-latest (push) Waiting to run
CI / build-32bit (push) Waiting to run
CI / build-libc-malloc (push) Waiting to run
CI / build-centos-jemalloc (push) Waiting to run
CI / build-old-chain-jemalloc (push) Waiting to run
Codecov / code-coverage (push) Waiting to run
External Server Tests / test-external-standalone (push) Waiting to run
External Server Tests / test-external-cluster (push) Waiting to run
External Server Tests / test-external-nodebug (push) Waiting to run
Spellcheck / Spellcheck (push) Waiting to run
Reply-schemas linter / reply-schemas-linter (push) Has been cancelled
After introducing GCRA algorithm into redis https://github.com/redis/redis/pull/14826 and subsequent introduction of new RATE_LIMIT object type - https://github.com/redis/redis/pull/14905. It was internally decided not to introduce GCRA into the new release. As still no decision is made on whether it will be kept or not in the future, this PR only makes the code related to GCRA dead - commands are inaccessible and AOF/RDB load+save is disabled. --------- Co-authored-by: debing.sun <debing.sun@redis.com>
This commit is contained in:
parent
c3db5254b7
commit
2e46d2e735
25 changed files with 117 additions and 219 deletions
|
|
@ -2051,7 +2051,6 @@ latency-monitor-threshold 0
|
|||
# (Note: not included in the 'A' class)
|
||||
# c Type-changed events generated every time a key's type changes
|
||||
# (Note: not included in the 'A' class)
|
||||
# r rate limit event
|
||||
# S Subkeyspace events, published with __subkeyspace@<db>__:<key> prefix.
|
||||
# T Subkeyevent events, published with __subkeyevent@<db>__:<event> prefix.
|
||||
# I Subkeyspaceitem events, published per subkey with
|
||||
|
|
@ -2059,7 +2058,7 @@ latency-monitor-threshold 0
|
|||
# V Subkeyspaceevent events, published with
|
||||
# __subkeyspaceevent@<db>__:<event>|<key> prefix.
|
||||
# A Alias for g$lshzxetad, so that the "AKE" string means all the events
|
||||
# except key-miss, new key, overwritten, type-changed and rate-limit.
|
||||
# except key-miss, new key, overwritten and type-changed.
|
||||
#
|
||||
# The "notify-keyspace-events" takes as argument a string that is composed
|
||||
# of zero or multiple characters. The empty string means that notifications
|
||||
|
|
|
|||
|
|
@ -71,7 +71,9 @@ struct ACLCategoryItem {
|
|||
{"connection", ACL_CATEGORY_CONNECTION},
|
||||
{"transaction", ACL_CATEGORY_TRANSACTION},
|
||||
{"scripting", ACL_CATEGORY_SCRIPTING},
|
||||
#ifdef ENABLE_GCRA
|
||||
{"ratelimit", ACL_CATEGORY_RATE_LIMIT},
|
||||
#endif
|
||||
{NULL,0} /* Terminator. */
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2467,6 +2467,7 @@ int rewriteStreamObject(rio *r, robj *key, robj *o) {
|
|||
return 1;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_GCRA
|
||||
int rewriteGCRAObject(rio *r, robj *key, robj *o) {
|
||||
long long val;
|
||||
getLongLongFromGCRAObject(o, &val);
|
||||
|
|
@ -2478,6 +2479,7 @@ int rewriteGCRAObject(rio *r, robj *key, robj *o) {
|
|||
if (rioWriteBulkLongLong(r,val) == 0) return 0;
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Call the module type callback in order to rewrite a data type
|
||||
* that is exported by a module and is not handled by Redis itself.
|
||||
|
|
@ -2644,8 +2646,10 @@ int rewriteObject(rio *r, robj *key, robj *o, int dbid, long long expiretime) {
|
|||
if (rewriteHashObject(r,key,o) == 0) return C_ERR;
|
||||
} else if (o->type == OBJ_STREAM) {
|
||||
if (rewriteStreamObject(r,key,o) == 0) return C_ERR;
|
||||
#ifdef ENABLE_GCRA
|
||||
} else if (o->type == OBJ_GCRA) {
|
||||
if (rewriteGCRAObject(r,key,o) == 0) return C_ERR;
|
||||
#endif
|
||||
} else if (o->type == OBJ_ARRAY) {
|
||||
if (rewriteArrayObject(r,key,o) == 0) return C_ERR;
|
||||
} else if (o->type == OBJ_MODULE) {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@ const char *COMMAND_GROUP_STR[] = {
|
|||
"bitmap",
|
||||
"array",
|
||||
"module",
|
||||
#ifdef ENABLE_GCRA
|
||||
"rate_limit"
|
||||
#endif
|
||||
};
|
||||
|
||||
const char *commandGroupStr(int index) {
|
||||
|
|
@ -5910,59 +5912,6 @@ struct COMMAND_ARG UNSUBSCRIBE_Args[] = {
|
|||
{MAKE_ARG("channel",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL|CMD_ARG_MULTIPLE,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("tokens-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,"TOKENS",NULL,NULL,CMD_ARG_OPTIONAL,0,NULL)},
|
||||
};
|
||||
|
||||
/********** GCRASETVALUE ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
/* GCRASETVALUE history */
|
||||
#define GCRASETVALUE_History NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
/* GCRASETVALUE tips */
|
||||
#define GCRASETVALUE_Tips NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_KEY_SPECS_TABLE
|
||||
/* GCRASETVALUE key specs */
|
||||
keySpec GCRASETVALUE_Keyspecs[1] = {
|
||||
{NULL,CMD_KEY_OW|CMD_KEY_UPDATE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}
|
||||
};
|
||||
#endif
|
||||
|
||||
/* GCRASETVALUE argument table */
|
||||
struct COMMAND_ARG GCRASETVALUE_Args[] = {
|
||||
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("tat",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/********** EVAL ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
|
|
@ -12556,9 +12505,6 @@ struct COMMAND_STRUCT redisCommandTable[] = {
|
|||
{MAKE_CMD("subscribe","Listens for messages published to channels.","O(N) where N is the number of channels to subscribe to.","2.0.0",CMD_DOC_NONE,NULL,NULL,"pubsub",COMMAND_GROUP_PUBSUB,SUBSCRIBE_History,0,SUBSCRIBE_Tips,0,subscribeCommand,-2,CMD_PUBSUB|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL|CMD_DENYOOM,0,SUBSCRIBE_Keyspecs,0,NULL,1),.args=SUBSCRIBE_Args},
|
||||
{MAKE_CMD("sunsubscribe","Stops listening to messages posted to shard channels.","O(N) where N is the number of shard channels to unsubscribe.","7.0.0",CMD_DOC_NONE,NULL,NULL,"pubsub",COMMAND_GROUP_PUBSUB,SUNSUBSCRIBE_History,0,SUNSUBSCRIBE_Tips,0,sunsubscribeCommand,-1,CMD_PUBSUB|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE,0,SUNSUBSCRIBE_Keyspecs,1,NULL,1),.args=SUNSUBSCRIBE_Args},
|
||||
{MAKE_CMD("unsubscribe","Stops listening to messages posted to channels.","O(N) where N is the number of channels to unsubscribe.","2.0.0",CMD_DOC_NONE,NULL,NULL,"pubsub",COMMAND_GROUP_PUBSUB,UNSUBSCRIBE_History,0,UNSUBSCRIBE_Tips,0,unsubscribeCommand,-1,CMD_PUBSUB|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,0,UNSUBSCRIBE_Keyspecs,0,NULL,1),.args=UNSUBSCRIBE_Args},
|
||||
/* rate_limit */
|
||||
{MAKE_CMD("gcra","Rate limit via GCRA (Generic Cell Rate Algorithm).","O(1)","8.8.0",CMD_DOC_NONE,NULL,NULL,"rate_limit",COMMAND_GROUP_RATE_LIMIT,GCRA_History,0,GCRA_Tips,0,gcraCommand,-5,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_RATE_LIMIT,GCRA_Keyspecs,1,NULL,5),.args=GCRA_Args},
|
||||
{MAKE_CMD("gcrasetvalue","An internal command for recording a GCRA TAT value during AOF rewrite and replication.","O(1)","8.8.0",CMD_DOC_NONE,NULL,NULL,"rate_limit",COMMAND_GROUP_RATE_LIMIT,GCRASETVALUE_History,0,GCRASETVALUE_Tips,0,gcraSetValueCommand,3,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_RATE_LIMIT,GCRASETVALUE_Keyspecs,1,NULL,2),.args=GCRASETVALUE_Args},
|
||||
/* scripting */
|
||||
{MAKE_CMD("eval","Executes a server-side Lua script.","Depends on the script that is executed.","2.6.0",CMD_DOC_NONE,NULL,NULL,"scripting",COMMAND_GROUP_SCRIPTING,EVAL_History,0,EVAL_Tips,0,evalCommand,-3,CMD_NOSCRIPT|CMD_SKIP_MONITOR|CMD_MAY_REPLICATE|CMD_NO_MANDATORY_KEYS|CMD_STALE,ACL_CATEGORY_SCRIPTING,EVAL_Keyspecs,1,evalGetKeys,4),.args=EVAL_Args},
|
||||
{MAKE_CMD("evalsha","Executes a server-side Lua script by SHA1 digest.","Depends on the script that is executed.","2.6.0",CMD_DOC_NONE,NULL,NULL,"scripting",COMMAND_GROUP_SCRIPTING,EVALSHA_History,0,EVALSHA_Tips,0,evalShaCommand,-3,CMD_NOSCRIPT|CMD_SKIP_MONITOR|CMD_MAY_REPLICATE|CMD_NO_MANDATORY_KEYS|CMD_STALE,ACL_CATEGORY_SCRIPTING,EVALSHA_Keyspecs,1,evalGetKeys,4),.args=EVALSHA_Args},
|
||||
|
|
|
|||
|
|
@ -1,92 +0,0 @@
|
|||
{
|
||||
"GCRA": {
|
||||
"summary": "Rate limit via GCRA (Generic Cell Rate Algorithm).",
|
||||
"complexity": "O(1)",
|
||||
"group": "rate_limit",
|
||||
"since": "8.8.0",
|
||||
"arity": -5,
|
||||
"function": "gcraCommand",
|
||||
"command_flags": [
|
||||
"WRITE",
|
||||
"DENYOOM",
|
||||
"FAST"
|
||||
],
|
||||
"acl_categories": [
|
||||
"RATE_LIMIT"
|
||||
],
|
||||
"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 tokens: always equal to max_burst+1"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Number of tokens available immediately"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Retry after: seconds after which the 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": "tokens-per-period",
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"name": "period",
|
||||
"type": "double"
|
||||
},
|
||||
{
|
||||
"name": "count",
|
||||
"type": "integer",
|
||||
"token": "TOKENS",
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
{
|
||||
"GCRASETVALUE": {
|
||||
"summary": "An internal command for recording a GCRA TAT value during AOF rewrite and replication.",
|
||||
"complexity": "O(1)",
|
||||
"group": "rate_limit",
|
||||
"since": "8.8.0",
|
||||
"arity": 3,
|
||||
"function": "gcraSetValueCommand",
|
||||
"command_flags": [
|
||||
"WRITE",
|
||||
"DENYOOM",
|
||||
"FAST"
|
||||
],
|
||||
"acl_categories": [
|
||||
"RATE_LIMIT"
|
||||
],
|
||||
"key_specs": [
|
||||
{
|
||||
"flags": [
|
||||
"OW",
|
||||
"UPDATE"
|
||||
],
|
||||
"begin_search": {
|
||||
"index": {
|
||||
"pos": 1
|
||||
}
|
||||
},
|
||||
"find_keys": {
|
||||
"range": {
|
||||
"lastkey": 0,
|
||||
"step": 1,
|
||||
"limit": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"reply_schema": {
|
||||
"const": "OK"
|
||||
},
|
||||
"arguments": [
|
||||
{
|
||||
"name": "key",
|
||||
"type": "key",
|
||||
"key_spec_index": 0
|
||||
},
|
||||
{
|
||||
"name": "tat",
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -2972,7 +2972,11 @@ static int setConfigNotifyKeyspaceEventsOption(standardConfig *config, sds *argv
|
|||
}
|
||||
int flags = keyspaceEventsStringToFlags(argv[0]);
|
||||
if (flags == -1) {
|
||||
*err = "Invalid event class character. Use 'Ag$lshzxeKEtmdnocrSTIV'.";
|
||||
#ifdef ENABLE_GCRA
|
||||
*err = "Invalid event class character. Use 'Ag$lshzxeKEtmdnocraSTIV'.";
|
||||
#else
|
||||
*err = "Invalid event class character. Use 'Ag$lshzxeKEtmdnocaSTIV'.";
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
server.notify_keyspace_events = flags;
|
||||
|
|
|
|||
8
src/db.c
8
src/db.c
|
|
@ -1758,8 +1758,10 @@ char *obj_type_name[OBJ_TYPE_MAX] = {
|
|||
"hash",
|
||||
NULL, /* module type is special */
|
||||
"stream",
|
||||
"gcra",
|
||||
"array"
|
||||
"array",
|
||||
#ifdef ENABLE_GCRA
|
||||
"gcra"
|
||||
#endif
|
||||
};
|
||||
|
||||
/* Helper function to get type from a string in scan commands */
|
||||
|
|
@ -2434,7 +2436,9 @@ void copyCommand(client *c) {
|
|||
case OBJ_ZSET: newobj = zsetDup(o); break;
|
||||
case OBJ_HASH: newobj = hashTypeDup(o, &minHashExpire); break;
|
||||
case OBJ_STREAM: newobj = streamDup(o); break;
|
||||
#ifdef ENABLE_GCRA
|
||||
case OBJ_GCRA: newobj = gcraDup(o); break;
|
||||
#endif
|
||||
case OBJ_MODULE:
|
||||
newobj = moduleTypeDupOrReply(c, key, newkey, dst->id, o);
|
||||
if (!newobj) return;
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ void mixStringObjectDigest(unsigned char *digest, robj *o) {
|
|||
decrRefCount(o);
|
||||
}
|
||||
|
||||
#ifdef ENABLE_GCRA
|
||||
void mixGCRAObjectDigest(unsigned char *digest, robj *o) {
|
||||
char buf[LONG_STR_SIZE];
|
||||
long long val;
|
||||
|
|
@ -130,6 +131,7 @@ void mixGCRAObjectDigest(unsigned char *digest, robj *o) {
|
|||
int len = ll2string(buf, sizeof(buf), val);
|
||||
mixDigest(digest,buf,len);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* This function computes the digest of a data structure stored in the
|
||||
* object 'o'. It is the core of the DEBUG DIGEST command: when taking the
|
||||
|
|
@ -263,8 +265,10 @@ void xorObjectDigest(redisDb *db, robj *keyobj, unsigned char *digest, robj *o)
|
|||
}
|
||||
}
|
||||
streamIteratorStop(&si);
|
||||
#ifdef ENABLE_GCRA
|
||||
} else if (o->type == OBJ_GCRA) {
|
||||
mixGCRAObjectDigest(digest, o);
|
||||
#endif
|
||||
} else if (o->type == OBJ_MODULE) {
|
||||
RedisModuleDigest md = {{0},{0},keyobj,db->id};
|
||||
moduleValue *mv = o->ptr;
|
||||
|
|
@ -1327,9 +1331,11 @@ void serverLogObjectDebugInfo(const robj *o) {
|
|||
serverLog(LL_WARNING,"Skiplist level: %d", (int) ((const zset*)o->ptr)->zsl->level);
|
||||
} else if (o->type == OBJ_STREAM) {
|
||||
serverLog(LL_WARNING,"Stream size: %d", (int) streamLength(o));
|
||||
#ifdef ENABLE_GCRA
|
||||
} else if (o->type == OBJ_GCRA) {
|
||||
#if UINTPTR_MAX == 0xffffffffffffffff
|
||||
serverLog(LL_WARNING, "GCRA object: %lld", (long long)o->ptr);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1189,12 +1189,14 @@ void defragKey(defragKeysCtx *ctx, dictEntry *de, dictEntryLink link) {
|
|||
}
|
||||
} else if (ob->type == OBJ_STREAM) {
|
||||
defragStream(ctx, ob);
|
||||
#ifdef ENABLE_GCRA
|
||||
} else if (ob->type == OBJ_GCRA) {
|
||||
/* GCRA object is just an allocation to a long long value */
|
||||
#if UINTPTR_MAX == 0xffffffff
|
||||
void *newptr, *ptr = ob->ptr;
|
||||
if ((newptr = activeDefragAlloc(ptr)))
|
||||
ob->ptr = newptr;
|
||||
#endif
|
||||
#endif
|
||||
} else if (ob->type == OBJ_MODULE) {
|
||||
defragModule(ctx,db, ob);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
#include "server.h"
|
||||
#include <math.h>
|
||||
|
||||
#ifdef ENABLE_GCRA
|
||||
|
||||
/* 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].
|
||||
|
|
@ -278,3 +280,5 @@ robj *gcraDup(robj *o) {
|
|||
getLongLongFromGCRAObject(o, &val);
|
||||
return createGCRAObject(val);
|
||||
}
|
||||
|
||||
#endif /* ENABLE_GCRA */
|
||||
|
|
|
|||
|
|
@ -4254,7 +4254,9 @@ int RM_KeyType(RedisModuleKey *key) {
|
|||
case OBJ_HASH: return REDISMODULE_KEYTYPE_HASH;
|
||||
case OBJ_MODULE: return REDISMODULE_KEYTYPE_MODULE;
|
||||
case OBJ_STREAM: return REDISMODULE_KEYTYPE_STREAM;
|
||||
#ifdef ENABLE_GCRA
|
||||
case OBJ_GCRA: return REDISMODULE_KEYTYPE_GCRA;
|
||||
#endif
|
||||
case OBJ_ARRAY: return REDISMODULE_KEYTYPE_ARRAY;
|
||||
default: return REDISMODULE_KEYTYPE_EMPTY;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,9 @@ int keyspaceEventsStringToFlags(char *classes) {
|
|||
case 'n': flags |= NOTIFY_NEW; break;
|
||||
case 'o': flags |= NOTIFY_OVERWRITTEN; break;
|
||||
case 'c': flags |= NOTIFY_TYPE_CHANGED; break;
|
||||
#ifdef ENABLE_GCRA
|
||||
case 'r': flags |= NOTIFY_RATE_LIMIT; break;
|
||||
#endif
|
||||
case 'S': flags |= NOTIFY_SUBKEYSPACE; break;
|
||||
case 'T': flags |= NOTIFY_SUBKEYEVENT; break;
|
||||
case 'I': flags |= NOTIFY_SUBKEYSPACEITEM; break;
|
||||
|
|
@ -77,7 +79,9 @@ sds keyspaceEventsFlagsToString(int flags) {
|
|||
if (flags & NOTIFY_NEW) res = sdscatlen(res,"n",1);
|
||||
if (flags & NOTIFY_OVERWRITTEN) res = sdscatlen(res,"o",1);
|
||||
if (flags & NOTIFY_TYPE_CHANGED) res = sdscatlen(res,"c",1);
|
||||
#ifdef ENABLE_GCRA
|
||||
if (flags & NOTIFY_RATE_LIMIT) res = sdscatlen(res,"r",1);
|
||||
#endif
|
||||
}
|
||||
if (flags & NOTIFY_KEYSPACE) res = sdscatlen(res,"K",1);
|
||||
if (flags & NOTIFY_KEYEVENT) res = sdscatlen(res,"E",1);
|
||||
|
|
|
|||
20
src/object.c
20
src/object.c
|
|
@ -514,6 +514,7 @@ robj *createStreamObject(void) {
|
|||
return o;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_GCRA
|
||||
robj *createGCRAObject(long long value) {
|
||||
/* NOTE: for 32-bit systems we can't use integer encoding (as OBJ_STRING does)
|
||||
* as the GCRA object is a unixtime value in microseconds, which as of the
|
||||
|
|
@ -530,6 +531,7 @@ robj *createGCRAObject(long long value) {
|
|||
o->encoding = OBJ_ENCODING_INT;
|
||||
return o;
|
||||
}
|
||||
#endif
|
||||
|
||||
robj *createArrayObject(void) {
|
||||
redisArray *ar = arNew();
|
||||
|
|
@ -610,6 +612,7 @@ void freeStreamObject(robj *o) {
|
|||
freeStream(o->ptr);
|
||||
}
|
||||
|
||||
#ifdef ENABLE_GCRA
|
||||
void freeGCRAObject(robj *o) {
|
||||
#if UINTPTR_MAX == 0xffffffff
|
||||
zfree(o->ptr);
|
||||
|
|
@ -617,6 +620,7 @@ void freeGCRAObject(robj *o) {
|
|||
(void)o;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
void freeArrayObject(robj *o) {
|
||||
arFree(o->ptr);
|
||||
|
|
@ -673,7 +677,9 @@ void decrRefCount(robj *o) {
|
|||
case OBJ_HASH: freeHashObject(o); break;
|
||||
case OBJ_MODULE: freeModuleObject(o); break;
|
||||
case OBJ_STREAM: freeStreamObject(o); break;
|
||||
#ifdef ENABLE_GCRA
|
||||
case OBJ_GCRA: freeGCRAObject(o); break;
|
||||
#endif
|
||||
case OBJ_ARRAY: freeArrayObject(o); break;
|
||||
default: serverPanic("Unknown object type"); break;
|
||||
}
|
||||
|
|
@ -827,12 +833,14 @@ void dismissArrayObject(robj *o, size_t size_hint) {
|
|||
arDismiss(o->ptr, size_hint);
|
||||
}
|
||||
|
||||
#ifdef ENABLE_GCRA
|
||||
void dismissGCRAObject(robj *o, size_t size_hint) {
|
||||
/* GCRA is a single allocation of a long long thus way smaller than a
|
||||
* page-size. The dismiss mechanism is not needed for it - hence NOOP.*/
|
||||
(void)o;
|
||||
(void)size_hint;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* When creating a snapshot in a fork child process, the main process and child
|
||||
* process share the same physical memory pages, and if / when the parent
|
||||
|
|
@ -862,7 +870,9 @@ void dismissObject(robj *o, size_t size_hint) {
|
|||
case OBJ_ZSET: dismissZsetObject(o, size_hint); break;
|
||||
case OBJ_HASH: dismissHashObject(o, size_hint); break;
|
||||
case OBJ_STREAM: dismissStreamObject(o, size_hint); break;
|
||||
#ifdef ENABLE_GCRA
|
||||
case OBJ_GCRA: dismissGCRAObject(o, size_hint); break;
|
||||
#endif
|
||||
case OBJ_ARRAY: dismissArrayObject(o, size_hint); break;
|
||||
default: break;
|
||||
}
|
||||
|
|
@ -985,7 +995,9 @@ size_t getObjectLength(robj *o) {
|
|||
case OBJ_ZSET: return zsetLength(o);
|
||||
case OBJ_HASH: return hashTypeLength(o, 0);
|
||||
case OBJ_STREAM: return streamLength(o);
|
||||
#ifdef ENABLE_GCRA
|
||||
case OBJ_GCRA: return gcraObjectLength(o);
|
||||
#endif
|
||||
case OBJ_ARRAY: return arCount(o->ptr);
|
||||
default: return 0;
|
||||
}
|
||||
|
|
@ -1195,6 +1207,7 @@ int getLongLongFromObject(robj *o, long long *target) {
|
|||
return C_OK;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_GCRA
|
||||
int getLongLongFromGCRAObject(robj *o, long long *target) {
|
||||
long long res;
|
||||
serverAssertWithInfo(NULL, o, o->type == OBJ_GCRA);
|
||||
|
|
@ -1210,6 +1223,7 @@ int getLongLongFromGCRAObject(robj *o, long long *target) {
|
|||
*target = res;
|
||||
return C_OK;
|
||||
}
|
||||
#endif
|
||||
|
||||
int getLongLongFromObjectOrReply(client *c, robj *o, long long *target, const char *msg) {
|
||||
long long value;
|
||||
|
|
@ -1303,7 +1317,9 @@ size_t kvobjComputeSize(robj *key, kvobj *o, size_t sample_size, int dbid) {
|
|||
o->type == OBJ_ZSET ||
|
||||
o->type == OBJ_HASH ||
|
||||
o->type == OBJ_STREAM ||
|
||||
#ifdef ENABLE_GCRA
|
||||
o->type == OBJ_GCRA ||
|
||||
#endif
|
||||
o->type == OBJ_ARRAY)
|
||||
{
|
||||
return kvobjAllocSize(o);
|
||||
|
|
@ -1330,8 +1346,10 @@ size_t kvobjAllocSize(kvobj *o) {
|
|||
} else if (o->type == OBJ_STREAM) {
|
||||
stream *s = o->ptr;
|
||||
asize += s->alloc_size;
|
||||
#ifdef ENABLE_GCRA
|
||||
} else if (o->type == OBJ_GCRA) {
|
||||
asize += gcraTypeAllocSize(o);
|
||||
#endif
|
||||
} else if (o->type == OBJ_ARRAY) {
|
||||
redisArray *ar = o->ptr;
|
||||
asize += ar->alloc_size;
|
||||
|
|
@ -1341,6 +1359,7 @@ size_t kvobjAllocSize(kvobj *o) {
|
|||
return asize;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_GCRA
|
||||
size_t gcraTypeAllocSize(robj *o) {
|
||||
(void)o;
|
||||
#if UINTPTR_MAX == 0xffffffff
|
||||
|
|
@ -1357,6 +1376,7 @@ size_t gcraObjectLength(robj *o) {
|
|||
(void)o;
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Release data obtained with getMemoryOverheadData(). */
|
||||
void freeMemoryOverheadData(struct redisMemOverhead *mh) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* values of different logical types (strings, lists, sets, hashes, sorted sets,
|
||||
* streams, modules, ...). It contains:
|
||||
* - type: one of OBJ_STRING, OBJ_LIST, OBJ_SET, OBJ_ZSET, OBJ_HASH, OBJ_STREAM,
|
||||
* OBJ_GCRA, OBJ_MODULE, ...
|
||||
* OBJ_MODULE, ...
|
||||
* - encoding: an implementation detail of how the value is represented in
|
||||
* memory for the given type (see OBJ_ENCODING_* below). For example,
|
||||
* strings may be RAW/EMBSTR/INT, sets may be INTSET or HT, etc.
|
||||
|
|
|
|||
|
|
@ -722,8 +722,10 @@ int rdbSaveObjectType(rio *rdb, robj *o) {
|
|||
serverPanic("Unknown hash encoding");
|
||||
case OBJ_STREAM:
|
||||
return rdbSaveType(rdb,RDB_TYPE_STREAM_LISTPACKS_5);
|
||||
#ifdef ENABLE_GCRA
|
||||
case OBJ_GCRA:
|
||||
return rdbSaveType(rdb,RDB_TYPE_GCRA);
|
||||
#endif
|
||||
case OBJ_MODULE:
|
||||
return rdbSaveType(rdb,RDB_TYPE_MODULE_2);
|
||||
case OBJ_ARRAY:
|
||||
|
|
@ -1474,11 +1476,13 @@ ssize_t rdbSaveObject(rio *rdb, robj *o, robj *key, int dbid) {
|
|||
/* Save the all-time count of duplicate IIDs detected. */
|
||||
if ((n = rdbSaveLen(rdb,s->iids_duplicates)) == -1) return -1;
|
||||
nwritten += n;
|
||||
#ifdef ENABLE_GCRA
|
||||
} else if (o->type == OBJ_GCRA) {
|
||||
long long t;
|
||||
getLongLongFromGCRAObject(o, &t);
|
||||
if ((n = rdbSaveLen(rdb,t)) == -1) return -1;
|
||||
nwritten += n;
|
||||
#endif
|
||||
} else if (o->type == OBJ_MODULE) {
|
||||
/* Save a module-specific value. */
|
||||
RedisModuleIO io;
|
||||
|
|
@ -3769,6 +3773,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error)
|
|||
return NULL;
|
||||
}
|
||||
o = createModuleObject(mt, ptr);
|
||||
#ifdef ENABLE_GCRA
|
||||
} else if (rdbtype == RDB_TYPE_GCRA) {
|
||||
uint64_t time = rdbLoadLen(rdb, NULL);
|
||||
if (time == RDB_LENERR || time > LLONG_MAX) {
|
||||
|
|
@ -3776,6 +3781,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error)
|
|||
return NULL;
|
||||
}
|
||||
o = createGCRAObject((long long)time);
|
||||
#endif
|
||||
} else if (rdbtype == RDB_TYPE_ARRAY) {
|
||||
/* Load array value. We only persist elements and insert_idx - no
|
||||
* implementation details. Arrays use current ar_slice_size config. */
|
||||
|
|
|
|||
10
src/rdb.h
10
src/rdb.h
|
|
@ -80,12 +80,18 @@
|
|||
#define RDB_TYPE_HASH_LISTPACK_EX 25 /* Hash LP with HFEs. Attach min TTL at start */
|
||||
#define RDB_TYPE_STREAM_LISTPACKS_4 26 /* Stream with IDMP support */
|
||||
#define RDB_TYPE_STREAM_LISTPACKS_5 27 /* Stream with XNACK support (NACKed entries) */
|
||||
#define RDB_TYPE_GCRA 28 /* GCRA object */
|
||||
#define RDB_TYPE_ARRAY 29 /* Array data type */
|
||||
#define RDB_TYPE_ARRAY 28 /* Array data type */
|
||||
#ifdef ENABLE_GCRA
|
||||
#define RDB_TYPE_GCRA 29 /* GCRA object */
|
||||
#endif
|
||||
/* NOTE: WHEN ADDING NEW RDB TYPE, UPDATE rdbIsObjectType(), and rdb_type_string[] */
|
||||
|
||||
/* Test if a type is an object type. */
|
||||
#ifdef ENABLE_GCRA
|
||||
#define rdbIsObjectType(t) (((t) >= 0 && (t) <= 7) || ((t) >= 9 && (t) <= 29))
|
||||
#else
|
||||
#define rdbIsObjectType(t) (((t) >= 0 && (t) <= 7) || ((t) >= 9 && (t) <= 28))
|
||||
#endif
|
||||
|
||||
/* Special RDB opcodes (saved/loaded with rdbSaveType/rdbLoadType). */
|
||||
#define RDB_OPCODE_KEY_META 243 /* Key metadata (module metadata classes). */
|
||||
|
|
|
|||
|
|
@ -88,8 +88,10 @@ char *rdb_type_string[] = {
|
|||
"hash-listpack-md",
|
||||
"stream-v4",
|
||||
"stream-v5",
|
||||
"gcra",
|
||||
"array",
|
||||
#ifdef ENABLE_GCRA
|
||||
"gcra",
|
||||
#endif
|
||||
};
|
||||
|
||||
/* Show a few stats collected into 'rdbstate' */
|
||||
|
|
|
|||
|
|
@ -89,8 +89,7 @@ typedef long long ustime_t;
|
|||
#define REDISMODULE_KEYTYPE_ZSET 5
|
||||
#define REDISMODULE_KEYTYPE_MODULE 6
|
||||
#define REDISMODULE_KEYTYPE_STREAM 7
|
||||
#define REDISMODULE_KEYTYPE_GCRA 8
|
||||
#define REDISMODULE_KEYTYPE_ARRAY 9
|
||||
#define REDISMODULE_KEYTYPE_ARRAY 8
|
||||
|
||||
/* Reply types. */
|
||||
#define REDISMODULE_REPLY_UNKNOWN -1
|
||||
|
|
@ -249,18 +248,24 @@ This flag should not be used directly by the module.
|
|||
#define REDISMODULE_NOTIFY_OVERWRITTEN (1<<15) /* o, key overwrite notification */
|
||||
#define REDISMODULE_NOTIFY_TYPE_CHANGED (1<<16) /* c, key type changed notification */
|
||||
#define REDISMODULE_NOTIFY_KEY_TRIMMED (1<<17) /* module only key space notification, indicates a key trimmed during slot migration */
|
||||
#define REDISMODULE_NOTIFY_RATE_LIMIT (1<<18) /* r, rate limit event */
|
||||
|
||||
#define REDISMODULE_NOTIFY_SUBKEYSPACE (1<<19) /* S */
|
||||
#define REDISMODULE_NOTIFY_SUBKEYEVENT (1<<20) /* T */
|
||||
#define REDISMODULE_NOTIFY_SUBKEYSPACEITEM (1<<21) /* I */
|
||||
#define REDISMODULE_NOTIFY_SUBKEYSPACEEVENT (1<<22) /* V */
|
||||
#define REDISMODULE_NOTIFY_ARRAY (1<<23) /* a, array key space notification */
|
||||
#ifdef ENABLE_GCRA
|
||||
#define REDISMODULE_NOTIFY_RATE_LIMIT (1<<24) /* r, rate limit event */
|
||||
#endif
|
||||
|
||||
/* Next notification flag, must be updated when adding new flags above!
|
||||
This flag should not be used directly by the module.
|
||||
* Use RedisModule_GetKeyspaceNotificationFlagsAll instead. */
|
||||
#ifdef ENABLE_GCRA
|
||||
#define _REDISMODULE_NOTIFY_NEXT (1<<25)
|
||||
#else
|
||||
#define _REDISMODULE_NOTIFY_NEXT (1<<24)
|
||||
#endif
|
||||
|
||||
/* Delivery flags for RM_SubscribeToKeyspaceEventsWithSubkeys.
|
||||
* These are passed in the 'flags' parameter, not in 'types'. */
|
||||
|
|
|
|||
23
src/server.h
23
src/server.h
|
|
@ -288,8 +288,10 @@ extern int configOOMScoreAdjValuesDefaults[CONFIG_OOM_COUNT];
|
|||
#define ACL_CATEGORY_CONNECTION (1ULL<<18)
|
||||
#define ACL_CATEGORY_TRANSACTION (1ULL<<19)
|
||||
#define ACL_CATEGORY_SCRIPTING (1ULL<<20)
|
||||
#define ACL_CATEGORY_RATE_LIMIT (1ULL<<21)
|
||||
#define ACL_CATEGORY_ARRAY (1ULL<<22)
|
||||
#define ACL_CATEGORY_ARRAY (1ULL<<21)
|
||||
#ifdef ENABLE_GCRA
|
||||
#define ACL_CATEGORY_RATE_LIMIT (1ULL<<22)
|
||||
#endif
|
||||
|
||||
/* Key-spec flags *
|
||||
* -------------- */
|
||||
|
|
@ -798,12 +800,14 @@ typedef enum {
|
|||
#define NOTIFY_OVERWRITTEN (1<<15) /* o, key overwrite notification (Note: excluded from NOTIFY_ALL) */
|
||||
#define NOTIFY_TYPE_CHANGED (1<<16) /* c, key type changed notification (Note: excluded from NOTIFY_ALL) */
|
||||
#define NOTIFY_KEY_TRIMMED (1<<17) /* module only key space notification, indicates a key trimmed during slot migration */
|
||||
#define NOTIFY_RATE_LIMIT (1<<18) /* r, notify rate limit event (Note: excluded from NOTIFY_ALL)*/
|
||||
#define NOTIFY_SUBKEYSPACE (1<<19) /* S, subkey-level keyspace notification */
|
||||
#define NOTIFY_SUBKEYEVENT (1<<20) /* T, subkey-level keyevent notification */
|
||||
#define NOTIFY_SUBKEYSPACEITEM (1<<21) /* I, subkey-level notification per item: channel=key\nsubkey */
|
||||
#define NOTIFY_SUBKEYSPACEEVENT (1<<22) /* V, subkey-level notification: channel=event|key */
|
||||
#define NOTIFY_ARRAY (1<<23) /* a, array notification */
|
||||
#ifdef ENABLE_GCRA
|
||||
#define NOTIFY_RATE_LIMIT (1<<24) /* r, notify rate limit event (Note: excluded from NOTIFY_ALL)*/
|
||||
#endif
|
||||
#define NOTIFY_ALL (NOTIFY_GENERIC | NOTIFY_STRING | NOTIFY_LIST | NOTIFY_SET | NOTIFY_HASH | NOTIFY_ZSET | NOTIFY_EXPIRED | NOTIFY_EVICTED | NOTIFY_STREAM | NOTIFY_MODULE | NOTIFY_ARRAY) /* A flag */
|
||||
|
||||
/* Using the following macro you can run code inside serverCron() with the
|
||||
|
|
@ -866,11 +870,18 @@ typedef enum {
|
|||
* by a 64 bit module type ID, which has a 54 bits module-specific signature
|
||||
* in order to dispatch the loading to the right module, plus a 10 bits
|
||||
* encoding version. */
|
||||
/* Code related to GCRA is disabled by default.
|
||||
* Build with -DENABLE_GCRA to compile it back in. */
|
||||
|
||||
#define OBJ_MODULE 5 /* Module object. */
|
||||
#define OBJ_STREAM 6 /* Stream object. */
|
||||
#define OBJ_GCRA 7 /* GCRA object. */
|
||||
#define OBJ_ARRAY 8 /* Array object. */
|
||||
#define OBJ_ARRAY 7 /* Array object. */
|
||||
#ifdef ENABLE_GCRA
|
||||
#define OBJ_GCRA 8 /* GCRA object. */
|
||||
#define OBJ_TYPE_MAX 9 /* Maximum number of object types */
|
||||
#else
|
||||
#define OBJ_TYPE_MAX 8 /* Maximum number of object types */
|
||||
#endif
|
||||
|
||||
/* NOTE: adding a new object requires changes in the following places:
|
||||
* - rdb.c - save/load (also bump RDB_VERSION if needed)
|
||||
|
|
@ -2811,7 +2822,9 @@ typedef enum {
|
|||
COMMAND_GROUP_BITMAP,
|
||||
COMMAND_GROUP_ARRAY,
|
||||
COMMAND_GROUP_MODULE,
|
||||
#ifdef ENABLE_GCRA
|
||||
COMMAND_GROUP_RATE_LIMIT,
|
||||
#endif
|
||||
} redisCommandGroup;
|
||||
|
||||
typedef void redisCommandProc(client *c);
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -68,7 +68,9 @@ proc generate_types {} {
|
|||
# create other non-collection types
|
||||
r incr int
|
||||
r set string str
|
||||
if 0 {
|
||||
r gcra gcra 10 5 60000
|
||||
}
|
||||
|
||||
# create bigger objects with 10 items (more than a single ziplist / listpack)
|
||||
generate_collections big 10
|
||||
|
|
|
|||
|
|
@ -801,9 +801,12 @@ proc generate_fuzzy_traffic_on_key {key type duration} {
|
|||
set set_commands {SADD SCARD SDIFF SDIFFSTORE SINTER SINTERSTORE SISMEMBER SMEMBERS SMOVE SPOP SRANDMEMBER SREM SSCAN SUNION SUNIONSTORE}
|
||||
set stream_commands {XACK XADD XCLAIM XDEL XGROUP XINFO XLEN XPENDING XRANGE XREAD XREADGROUP XREVRANGE XTRIM XDELEX XACKDEL XNACK}
|
||||
set vset_commands {VADD VREM}
|
||||
set gcra_commands {GCRA}
|
||||
set array_commands {ARSET ARGET ARDEL ARCOUNT ARMSET ARMGET ARGETRANGE ARDELRANGE ARINFO}
|
||||
set commands [dict create string $string_commands hash $hash_commands zset $zset_commands list $list_commands set $set_commands stream $stream_commands vectorset $vset_commands gcra $gcra_commands array $array_commands]
|
||||
set commands [dict create string $string_commands hash $hash_commands zset $zset_commands list $list_commands set $set_commands stream $stream_commands vectorset $vset_commands array $array_commands]
|
||||
if 0 {
|
||||
set gcra_commands {GCRA}
|
||||
dict set commands gcra $gcra_commands
|
||||
}
|
||||
|
||||
set cmds [dict get $commands $type]
|
||||
set start_time [clock seconds]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
start_server {tags {"gcra" "external:skip"}} {
|
||||
if 0 {
|
||||
test {GCRA - argument validation} {
|
||||
# Wrong number of arguments (too few)
|
||||
catch {r gcra} err
|
||||
|
|
@ -236,8 +237,10 @@ start_server {tags {"gcra" "external:skip"}} {
|
|||
assert {[r pttl mykey] > 0}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start_server {tags {"gcra" "external:skip"}} {
|
||||
if 0 {
|
||||
test {GCRA - RDB save and reload preserves value} {
|
||||
r del mykey
|
||||
r gcra mykey 5 1 60
|
||||
|
|
@ -333,8 +336,10 @@ start_server {tags {"gcra" "external:skip"}} {
|
|||
assert_equal $digest_before $digest_after
|
||||
} {} {needs:debug}
|
||||
}
|
||||
}
|
||||
|
||||
start_server {tags {"gcra repl" "external:skip"}} {
|
||||
if 0 {
|
||||
set replica [srv 0 client]
|
||||
set replica_host [srv 0 host]
|
||||
set replica_port [srv 0 port]
|
||||
|
|
@ -368,3 +373,4 @@ start_server {tags {"gcra repl" "external:skip"}} {
|
|||
} {} {external:skip}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -606,7 +606,9 @@ const char *COMMAND_GROUP_STR[] = {
|
|||
"bitmap",
|
||||
"array",
|
||||
"module",
|
||||
#ifdef ENABLE_GCRA
|
||||
"rate_limit"
|
||||
#endif
|
||||
};
|
||||
|
||||
const char *commandGroupStr(int index) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue