Module set/get config API (#14051)
CI / test-ubuntu-latest (push) Waiting to run Details
CI / test-sanitizer-address (push) Waiting to run Details
CI / build-debian-old (push) Waiting to run Details
CI / build-macos-latest (push) Waiting to run Details
CI / build-32bit (push) Waiting to run Details
CI / build-libc-malloc (push) Waiting to run Details
CI / build-centos-jemalloc (push) Waiting to run Details
CI / build-old-chain-jemalloc (push) Waiting to run Details
Codecov / code-coverage (push) Waiting to run Details
External Server Tests / test-external-standalone (push) Waiting to run Details
External Server Tests / test-external-cluster (push) Waiting to run Details
External Server Tests / test-external-nodebug (push) Waiting to run Details
Spellcheck / Spellcheck (push) Waiting to run Details

# Problem

Some redis modules need to call `CONFIG GET/SET` commands. Server may be
ran with `rename-command CONFIG ""`(or something similar) which leads to
the module being unable to access the config.

# Solution

Added new API functions for use by modules
```
RedisModuleConfigIterator* RedisModule_GetConfigIterator(RedisModuleCtx *ctx, const char *pattern);
void RedisModule_ReleaseConfigIterator(RedisModuleCtx *ctx, RedisModuleConfigIterator *iter);
const char *RedisModule_ConfigIteratorNext(RedisModuleConfigIterator *iter);
int RedisModule_GetConfigType(const char *name, RedisModuleConfigType *res);
int RedisModule_GetBoolConfig(RedisModuleCtx *ctx, const char *name, int *res);
int RedisModule_GetConfig(RedisModuleCtx *ctx, const char *name, RedisModuleString **res);
int RedisModule_GetEnumConfig(RedisModuleCtx *ctx, const char *name, RedisModuleString **res);
int RedisModule_GetNumericConfig(RedisModuleCtx *ctx, const char *name, long long *res);
int RedisModule_SetBoolConfig(RedisModuleCtx *ctx, const char *name, int value, RedisModuleString **err);
int RedisModule_SetConfig(RedisModuleCtx *ctx, const char *name, RedisModuleString *value, RedisModuleString **err);
int RedisModule_SetEnumConfig(RedisModuleCtx *ctx, const char *name, RedisModuleString *value, RedisModuleString **err);
int RedisModule_SetNumericConfig(RedisModuleCtx *ctx, const char *name, long long value, RedisModuleString **err);
```

## Implementation

The work is mostly done inside `config.c` as I didn't want to expose the
config dict outside of it. That means each of these module functions has
a corresponding method in `config.c` that actually does the job. F.e
`RedisModule_SetEnumConfig` calls `moduleSetEnumConfig` which is
implemented in `config.c`

## Notes

Also, refactored `configSetCommand` and `restoreBackupConfig` functions
for the following reasons:
- code and logic is now way more clear in `configSetCommand`. Only
caveat here is removal of an optimization that skipped running apply
functions that already have ran in favour of code clarity.
- Both functions needlessly separated logic for module configs and
normal configs whereas no such separation is needed. This also had the
side effect of removing some allocations.
- `restoreBackupConfig` now has clearer interface and can be reused with
ease. One of the places I reused it is for the individual
`moduleSet*Config` functions, each of which needs the restoration
functionality but for a single config only.

## Future

Additionally, a couple considerations were made for potentially
extending the API in the future
- if need be an API for atomically setting multiple config values can be
added - `RedisModule_SetConfigsTranscationStart/End` or similar that can
be put around `RedisModule_Set*Config` calls.
- if performance is an issue an API
`RedisModule_GetConfigIteratorNextWithTypehint` or similar may be added
in order not to incur the additional cost of calling
`RedisModule_GetConfigType`.

---------

Co-authored-by: Oran Agra <oran@redislabs.com>
This commit is contained in:
Mincho Paskalev 2025-07-03 13:46:33 +03:00 committed by GitHub
parent 5b7eec4c81
commit 15706f2e82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 1248 additions and 74 deletions

View File

@ -58,4 +58,5 @@ $TCLSH tests/test_helper.tcl \
--single unit/moduleapi/rdbloadsave \
--single unit/moduleapi/crash \
--single unit/moduleapi/internalsecret \
--single unit/moduleapi/configaccess \
"${@}"

View File

@ -764,7 +764,12 @@ int performModuleConfigSetDefaultFromName(sds name, const char **err) {
return 0;
}
static void restoreBackupConfig(standardConfig **set_configs, sds *old_values, int count, apply_fn *apply_fns, list *module_configs) {
static int configNeedsApply(standardConfig *config) {
return ((config->flags & MODULE_CONFIG) && moduleConfigNeedsApply(config->privdata)) ||
config->interface.apply;
}
static void restoreBackupConfig(standardConfig **set_configs, sds *old_values, int count) {
int i;
const char *errstr = "unknown error";
/* Set all backup values */
@ -773,16 +778,19 @@ static void restoreBackupConfig(standardConfig **set_configs, sds *old_values, i
serverLog(LL_WARNING, "Failed restoring failed CONFIG SET command. Error setting %s to '%s': %s",
set_configs[i]->name, old_values[i], errstr);
}
/* Apply backup */
if (apply_fns) {
for (i = 0; i < count && apply_fns[i] != NULL; i++) {
if (!apply_fns[i](&errstr))
serverLog(LL_WARNING, "Failed applying restored failed CONFIG SET command: %s", errstr);
}
}
if (module_configs) {
if (!moduleConfigApplyConfig(module_configs, &errstr, NULL))
serverLog(LL_WARNING, "Failed applying restored failed CONFIG SET command: %s", errstr);
for (i = 0; i < count; i++) {
if (!configNeedsApply(set_configs[i])) continue;
int applyres = 0;
if (set_configs[i]->flags & MODULE_CONFIG)
applyres = moduleConfigApply(set_configs[i]->privdata, &errstr);
else
applyres = set_configs[i]->interface.apply(&errstr);
if (!applyres)
serverLog(LL_WARNING, "Failed applying restored CONFIG SET command. Error applying %s: %s",
set_configs[i]->name, errstr);
}
}
@ -795,14 +803,12 @@ void configSetCommand(client *c) {
const char *invalid_arg_name = NULL;
const char *err_arg_name = NULL;
standardConfig **set_configs; /* TODO: make this a dict for better performance */
list *module_configs_apply;
const char **config_names;
sds *new_values;
sds *old_values = NULL;
apply_fn *apply_fns; /* TODO: make this a set for better performance */
int config_count, i, j;
int invalid_args = 0, deny_loading_error = 0;
int *config_map_fns;
int *config_changed;
/* Make sure we have an even number of arguments: conf-val pairs */
if (c->argc & 1) {
@ -811,13 +817,11 @@ void configSetCommand(client *c) {
}
config_count = (c->argc - 2) / 2;
module_configs_apply = listCreate();
set_configs = zcalloc(sizeof(standardConfig*)*config_count);
config_names = zcalloc(sizeof(char*)*config_count);
new_values = zmalloc(sizeof(sds*)*config_count);
old_values = zcalloc(sizeof(sds*)*config_count);
apply_fns = zcalloc(sizeof(apply_fn)*config_count);
config_map_fns = zmalloc(sizeof(int)*config_count);
config_changed = zmalloc(sizeof(int)*config_count);
/* Find all relevant configs */
for (i = 0; i < config_count; i++) {
@ -883,46 +887,34 @@ void configSetCommand(client *c) {
for (i = 0; i < config_count; i++) {
int res = performInterfaceSet(set_configs[i], new_values[i], &errstr);
if (!res) {
restoreBackupConfig(set_configs, old_values, i+1, NULL, NULL);
restoreBackupConfig(set_configs, old_values, i+1);
err_arg_name = set_configs[i]->name;
goto err;
} else if (res == 1) {
/* A new value was set, if this config has an apply function then store it for execution later */
if (set_configs[i]->flags & MODULE_CONFIG) {
addModuleConfigApply(module_configs_apply, set_configs[i]->privdata);
} else if (set_configs[i]->interface.apply) {
/* Check if this apply function is already stored */
int exists = 0;
for (j = 0; apply_fns[j] != NULL && j <= i; j++) {
if (apply_fns[j] == set_configs[i]->interface.apply) {
exists = 1;
break;
}
}
/* Apply function not stored, store it */
if (!exists) {
apply_fns[j] = set_configs[i]->interface.apply;
config_map_fns[j] = i;
}
}
}
if (res == 1) config_changed[i] = 1;
else config_changed[i] = 0;
}
/* Apply all configs after being set */
for (i = 0; i < config_count && apply_fns[i] != NULL; i++) {
if (!apply_fns[i](&errstr)) {
serverLog(LL_WARNING, "Failed applying new configuration. Possibly related to new %s setting. Restoring previous settings.", set_configs[config_map_fns[i]]->name);
restoreBackupConfig(set_configs, old_values, config_count, apply_fns, NULL);
err_arg_name = set_configs[config_map_fns[i]]->name;
/* Apply all configs that need it */
for (i = 0; i < config_count; i++) {
if (!config_changed[i]) continue;
/* A new value was set, if this config has an apply function try to apply
* it and restore if apply fails. */
if (!configNeedsApply(set_configs[i])) continue;
int res = 0;
if (set_configs[i]->flags & MODULE_CONFIG)
res = moduleConfigApply(set_configs[i]->privdata, &errstr);
else
res = set_configs[i]->interface.apply(&errstr);
if (!res) {
restoreBackupConfig(set_configs, old_values, config_count);
err_arg_name = set_configs[i]->name;
goto err;
}
}
/* Apply all module configs that were set. */
if (!moduleConfigApplyConfig(module_configs_apply, &errstr, &err_arg_name)) {
serverLogRaw(LL_WARNING, "Failed applying new module configuration. Restoring previous settings.");
restoreBackupConfig(set_configs, old_values, config_count, apply_fns, module_configs_apply);
goto err;
}
RedisModuleConfigChangeV1 cc = {.num_changes = config_count, .config_names = config_names};
moduleFireServerEvent(REDISMODULE_EVENT_CONFIG, REDISMODULE_SUBEVENT_CONFIG_CHANGE, &cc);
@ -947,9 +939,7 @@ end:
for (i = 0; i < config_count; i++)
sdsfree(old_values[i]);
zfree(old_values);
zfree(apply_fns);
zfree(config_map_fns);
listRelease(module_configs_apply);
zfree(config_changed);
}
/*-----------------------------------------------------------------------------
@ -1826,9 +1816,7 @@ static void boolConfigInit(standardConfig *config) {
*config->data.yesno.config = config->data.yesno.default_value;
}
static int boolConfigSet(standardConfig *config, sds *argv, int argc, const char **err) {
UNUSED(argc);
int yn = yesnotoi(argv[0]);
static int boolConfigSetInternal(standardConfig *config, int yn, const char **err) {
if (yn == -1) {
*err = "argument must be 'yes' or 'no'";
return 0;
@ -1844,6 +1832,13 @@ static int boolConfigSet(standardConfig *config, sds *argv, int argc, const char
return 1;
}
return (config->flags & VOLATILE_CONFIG) ? 1 : 2;
}
static int boolConfigSet(standardConfig *config, sds *argv, int argc, const char **err) {
UNUSED(argc);
int yn = yesnotoi(argv[0]);
return boolConfigSetInternal(config, yn, err);
}
static sds boolConfigGet(standardConfig *config) {
@ -1874,12 +1869,11 @@ static void stringConfigInit(standardConfig *config) {
*config->data.string.config = (config->data.string.convert_empty_to_null && !config->data.string.default_value) ? NULL : zstrdup(config->data.string.default_value);
}
static int stringConfigSet(standardConfig *config, sds *argv, int argc, const char **err) {
UNUSED(argc);
if (config->data.string.is_valid_fn && !config->data.string.is_valid_fn(argv[0], err))
static int stringConfigSetInternal(standardConfig *config, char *str, const char **err) {
if (config->data.string.is_valid_fn && !config->data.string.is_valid_fn(str, err))
return 0;
char *prev = *config->data.string.config;
char *new = (config->data.string.convert_empty_to_null && !argv[0][0]) ? NULL : argv[0];
char *new = (config->data.string.convert_empty_to_null && !str[0]) ? NULL : str;
if (new != prev && (new == NULL || prev == NULL || strcmp(prev, new))) {
*config->data.string.config = new != NULL ? zstrdup(new) : NULL;
zfree(prev);
@ -1888,6 +1882,11 @@ static int stringConfigSet(standardConfig *config, sds *argv, int argc, const ch
return (config->flags & VOLATILE_CONFIG) ? 1 : 2;
}
static int stringConfigSet(standardConfig *config, sds *argv, int argc, const char **err) {
UNUSED(argc);
return stringConfigSetInternal(config, argv[0], err);
}
static sds stringConfigGet(standardConfig *config) {
return sdsnew(*config->data.string.config ? *config->data.string.config : "");
}
@ -2096,6 +2095,10 @@ static int numericBoundaryCheck(standardConfig *config, long long ll, const char
config->data.numeric.numeric_type == NUMERIC_TYPE_UINT ||
config->data.numeric.numeric_type == NUMERIC_TYPE_SIZE_T) {
/* Boundary check for unsigned types */
if (ll < 0) {
*err = "argument must be greater or equal to 0";
return 0;
}
unsigned long long ull = ll;
unsigned long long upper_bound = config->data.numeric.upper_bound;
unsigned long long lower_bound = config->data.numeric.lower_bound;
@ -2183,19 +2186,14 @@ static int numericParseString(standardConfig *config, sds value, const char **er
return 0;
}
static int numericConfigSet(standardConfig *config, sds *argv, int argc, const char **err) {
UNUSED(argc);
long long ll, prev = 0;
if (!numericParseString(config, argv[0], err, &ll))
return 0;
static int numericConfigSetInternal(standardConfig *config, long long ll, const char **err) {
if (!numericBoundaryCheck(config, ll, err))
return 0;
if (config->data.numeric.is_valid_fn && !config->data.numeric.is_valid_fn(ll, err))
return 0;
long long prev = 0;
GET_NUMERIC_TYPE(prev)
if (prev != ll) {
return setNumericType(config, ll, err);
@ -2204,6 +2202,16 @@ static int numericConfigSet(standardConfig *config, sds *argv, int argc, const c
return (config->flags & VOLATILE_CONFIG) ? 1 : 2;
}
static int numericConfigSet(standardConfig *config, sds *argv, int argc, const char **err) {
UNUSED(argc);
long long ll;
if (!numericParseString(config, argv[0], err, &ll))
return 0;
return numericConfigSetInternal(config, ll, err);
}
static sds numericConfigGet(standardConfig *config) {
char buf[128];
@ -3442,6 +3450,238 @@ void addModuleNumericConfig(sds name, sds alias, int flags, void *privdata, long
}
}
/*-----------------------------------------------------------------------------
* API for modules to access the config
*----------------------------------------------------------------------------*/
/* If a config with the given `name` does not exist or is not mutable, return
* NULL, else return the config. */
static standardConfig *getMutableConfig(client *c, const sds name, const char **errstr) {
standardConfig *config = lookupConfig(name);
if (!config) {
if (errstr) *errstr = "Config name not found";
return NULL;
}
if (config->flags & IMMUTABLE_CONFIG ||
(config->flags & PROTECTED_CONFIG &&
!allowProtectedAction(server.enable_protected_configs, c))) {
if (errstr) *errstr = config->flags & IMMUTABLE_CONFIG ? "Config is immutable" : "Config is protected";
return NULL;
}
if (server.loading && config->flags & DENY_LOADING_CONFIG) {
if (errstr) *errstr = "Config is not allowed during loading";
return NULL;
}
return config;
}
dictIterator *moduleGetConfigIterator(void) {
return dictGetIterator(configs);
}
const char *moduleConfigIteratorNext(dictIterator **iter, sds pattern, int is_glob, configType *typehint) {
if (*iter == NULL) return NULL;
standardConfig *config = NULL;
/* Special case for non-glob patterns - we only need to check if the config
* exists and return it. That save us iteration cycles. */
if (pattern && !is_glob) {
/* Release the iterator so we stop the iteration at this point */
dictReleaseIterator(*iter);
*iter = NULL;
dictEntry *de = dictFind(configs, pattern);
if (!de) return NULL;
config = dictGetVal(de);
if (typehint) *typehint = config->type;
return config->name;
}
dictEntry *de = NULL;
while ((de = dictNext(*iter)) != NULL) {
config = dictGetVal(de);
/* Note that hidden configs require an exact match (not a pattern) */
if (config->flags & HIDDEN_CONFIG) continue;
if (!pattern || stringmatch(pattern, config->name, 1))
break;
}
if (!de) return NULL;
if (typehint) *typehint = config->type;
return config->name;
}
int moduleGetConfigType(sds name, configType *res) {
standardConfig *config = lookupConfig(name);
if (!config) return 0;
if (res) *res = config->type;
return 1;
}
int moduleGetBoolConfig(sds name, int *res) {
standardConfig *config = lookupConfig(name);
if (!config) return 0;
if (config->type != BOOL_CONFIG) return 0;
if (res == NULL) return 1;
if (config->flags & MODULE_CONFIG)
*res = getModuleBoolConfig(config->privdata);
else
*res = *config->data.yesno.config;
return 1;
}
int moduleGetStringConfig(sds name, sds *res) {
standardConfig *config = lookupConfig(name);
if (!config) return 0;
if (res == NULL) return 1;
*res = config->interface.get(config);
return 1;
}
int moduleGetEnumConfig(sds name, sds *res) {
standardConfig *config = lookupConfig(name);
if (!config) return 0;
if (config->type != ENUM_CONFIG) return 0;
if (res != NULL) *res = enumConfigGet(config);
return 1;
}
int moduleGetNumericConfig(sds name, long long *res) {
standardConfig *config = lookupConfig(name);
if (!config) return 0;
if (config->type != NUMERIC_CONFIG) return 0;
if (res == NULL) return 1;
if (config->flags & MODULE_CONFIG)
*res = getModuleNumericConfig(config->privdata);
else
GET_NUMERIC_TYPE(*res);
return 1;
}
static int configApply(standardConfig *config, sds old_value, const char **err) {
if (!configNeedsApply(config)) return 1;
int res = 0;
if (config->flags & MODULE_CONFIG)
res = moduleConfigApply(config->privdata, err);
else
res = config->interface.apply(err);
if (res) return res;
/* Apply failed - restore old value and apply it again since we don't know
* the side effects of the failed apply. */
restoreBackupConfig(&config, &old_value, 1);
return res;
}
int moduleSetBoolConfig(client *c, sds name, int val, const char **err) {
standardConfig *config = getMutableConfig(c, name, err);
if (!config) return 0;
if (config->type != BOOL_CONFIG) return 0;
/* Sanitize input */
if (val != 0 && val != 1) val = -1;
sds old_value = config->interface.get(config);
int res = boolConfigSetInternal(config, val, err);
/* We can't be sure if value was changed but setting still failed so we need
* to restore the old value */
if (!res)
restoreBackupConfig(&config, &old_value, 1);
else
res = configApply(config, old_value, err);
if (old_value) sdsfree(old_value);
return res;
}
int moduleSetStringConfig(client *c, sds name, const char *val, const char **err) {
standardConfig *config = getMutableConfig(c, name, err);
if (!config) return 0;
sds old_value = config->interface.get(config);
int res = 0;
if (config->type == STRING_CONFIG)
res = stringConfigSetInternal(config, (char *)val, err);
else {
sds sdsval = sdsnew(val);
res = performInterfaceSet(config, sdsval, err);
sdsfree(sdsval);
}
/* We can't be sure if value was changed but setting still failed so we need
* to restore the old value */
if (!res)
restoreBackupConfig(&config, &old_value, 1);
else
res = configApply(config, old_value, err);
if (old_value) sdsfree(old_value);
return res;
}
int moduleSetEnumConfig(client *c, sds name, sds *vals, int vals_cnt, const char **err) {
standardConfig *config = getMutableConfig(c, name, err);
if (!config) return 0;
if (config->type != ENUM_CONFIG) return 0;
sds old_value = config->interface.get(config);
int res = enumConfigSet(config, vals, vals_cnt, err);
/* We can't be sure if value was changed but setting still failed so we need
* to restore the old value */
if (!res)
restoreBackupConfig(&config, &old_value, 1);
else
res = configApply(config, old_value, err);
if (old_value) sdsfree(old_value);
return res;
}
int moduleSetNumericConfig(client *c, sds name, long long val, const char **err) {
standardConfig *config = getMutableConfig(c, name, err);
if (config->type != NUMERIC_CONFIG) return 0;
sds old_value = config->interface.get(config);
int res = numericConfigSetInternal(config, val, err);
/* We can't be sure if value was changed but setting still failed so we need
* to restore the old value */
if (!res)
restoreBackupConfig(&config, &old_value, 1);
else
res = configApply(config, old_value, err);
if (old_value) sdsfree(old_value);
return res;
}
/*-----------------------------------------------------------------------------
* CONFIG HELP
*----------------------------------------------------------------------------*/

View File

@ -92,6 +92,7 @@ struct AutoMemEntry {
#define REDISMODULE_AM_FREED 3 /* Explicitly freed by user already. */
#define REDISMODULE_AM_DICT 4
#define REDISMODULE_AM_INFO 5
#define REDISMODULE_AM_CONFIG 6
/* The pool allocator block. Redis Modules can allocate memory via this special
* allocator that will automatically release it all once the callback returns.
@ -373,6 +374,12 @@ typedef struct RedisModuleServerInfoData {
rax *rax; /* parsed info data. */
} RedisModuleServerInfoData;
typedef struct RedisModuleConfigIterator {
dictIterator *di; /* Iterator for the configs dict. */
sds pattern; /* Pattern to filter configs by name. */
int is_glob; /* Is the pattern a glob-pattern or a fixed string? */
} RedisModuleConfigIterator;
/* Flags for moduleCreateArgvFromUserFormat(). */
#define REDISMODULE_ARGV_REPLICATE (1<<0)
#define REDISMODULE_ARGV_NO_AOF (1<<1)
@ -489,6 +496,7 @@ static void zsetKeyReset(RedisModuleKey *key);
static void moduleInitKeyTypeSpecific(RedisModuleKey *key);
void RM_FreeDict(RedisModuleCtx *ctx, RedisModuleDict *d);
void RM_FreeServerInfo(RedisModuleCtx *ctx, RedisModuleServerInfoData *data);
void RM_ConfigIteratorRelease(RedisModuleCtx *ctx, RedisModuleConfigIterator *iter);
/* Helpers for RM_SetCommandInfo. */
static int moduleValidateCommandInfo(const RedisModuleCommandInfo *info);
@ -2609,6 +2617,7 @@ void autoMemoryCollect(RedisModuleCtx *ctx) {
case REDISMODULE_AM_KEY: RM_CloseKey(ptr); break;
case REDISMODULE_AM_DICT: RM_FreeDict(NULL,ptr); break;
case REDISMODULE_AM_INFO: RM_FreeServerInfo(NULL,ptr); break;
case REDISMODULE_AM_CONFIG: RM_ConfigIteratorRelease(NULL, ptr); break;
}
}
ctx->flags |= REDISMODULE_CTX_AUTO_MEMORY;
@ -13013,26 +13022,46 @@ void addModuleConfigApply(list *module_configs, ModuleConfig *module_config) {
listAddNodeTail(module_configs, module_config);
}
/* Call apply on a module config. Assumes module_config->apply_fn != NULL! */
int moduleConfigApplyInternal(ModuleConfig *module_config, const char **err) {
RedisModuleCtx ctx;
RedisModuleString *error = NULL;
moduleCreateContext(&ctx, module_config->module, REDISMODULE_CTX_NONE);
if (module_config->apply_fn(&ctx, module_config->privdata, &error)) {
propagateErrorString(error, err);
moduleFreeContext(&ctx);
return 0;
}
moduleFreeContext(&ctx);
return 1;
}
/* Call apply on a single module config. */
int moduleConfigApply(ModuleConfig *module_config, const char **err) {
if (module_config->apply_fn == NULL) return 1;
return moduleConfigApplyInternal(module_config, err);
}
int moduleConfigNeedsApply(ModuleConfig *config) {
return config->apply_fn != NULL;
}
/* Call apply on all module configs specified in set, if an apply function was specified at registration time. */
int moduleConfigApplyConfig(list *module_configs, const char **err, const char **err_arg_name) {
if (!listLength(module_configs)) return 1;
listIter li;
listNode *ln;
ModuleConfig *module_config;
RedisModuleString *error = NULL;
RedisModuleCtx ctx;
listRewind(module_configs, &li);
while ((ln = listNext(&li))) {
module_config = listNodeValue(ln);
moduleCreateContext(&ctx, module_config->module, REDISMODULE_CTX_NONE);
if (module_config->apply_fn(&ctx, module_config->privdata, &error)) {
/* We know apply_fn is not NULL so skip the check */
if (!moduleConfigApplyInternal(module_config, err)) {
if (err_arg_name) *err_arg_name = module_config->name;
propagateErrorString(error, err);
moduleFreeContext(&ctx);
return 0;
}
moduleFreeContext(&ctx);
}
return 1;
}
@ -13513,6 +13542,286 @@ const char* RM_GetInternalSecret(RedisModuleCtx *ctx, size_t *len) {
return secret;
}
/* --------------------------------------------------------------------------
* ## Config access API
* -------------------------------------------------------------------------- */
/* Get an iterator to all configs.
* Optional `ctx` can be provided if use of auto-memory is desired.
* Optional `pattern` can be provided to filter configs by name. If `pattern` is
* NULL all configs will be returned.
*
* The returned iterator can be used to iterate over all configs using
* RedisModule_ConfigIteratorNext().
*
* Example usage:
* ```
* // Below is same as RedisModule_ConfigIteratorCreate(ctx, NULL)
* RedisModuleConfigIterator *iter = RedisModule_ConfigIteratorCreate(ctx, "*");
* const char *config_name = NULL;
* while ((config_name = RedisModule_ConfigIteratorNext(iter)) != NULL) {
* RedisModuleString *value = NULL;
* if (RedisModule_ConfigGet(ctx, config_name, &value) == REDISMODULE_OK) {
* // Do something with `value`...
* RedisModule_FreeString(ctx, value);
* }
* }
* RedisModule_ConfigIteratorRelease(ctx, iter);
*
* // Or optionally one can check the type to get the config value directly
* // via the appropriate API in case performance is of consideration
* iter = RedisModule_ConfigIteratorCreate(ctx, "*");
* while ((config_name = RedisModule_ConfigIteratorNext(iter)) != NULL) {
* RedisModuleConfigType type = RedisModule_ConfigGetType(config_name);
* if (type == REDISMODULE_CONFIG_TYPE_STRING) {
* RedisModuleString *value;
* RedisModule_ConfigGet(ctx, config_name, &value);
* // Do something with `value`...
* RedisModule_FreeString(ctx, value);
* } if (type == REDISMODULE_CONFIG_TYPE_NUMERIC) {
* long long value;
* RedisModule_ConfigGetNumeric(ctx, config_name, &value);
* // Do something with `value`...
* } else if (type == REDISMODULE_CONFIG_TYPE_BOOL) {
* int value;
* RedisModule_ConfigGetBool(ctx, config_name, &value);
* // Do something with `value`...
* } else if (type == REDISMODULE_CONFIG_TYPE_ENUM) {
* RedisModuleString *value;
* RedisModule_ConfigGetEnum(ctx, config_name, &value);
* // Do something with `value`...
* RedisModule_Free(value);
* }
* }
* RedisModule_ConfigIteratorRelease(ctx, iter);
* ```
*
* Returns a pointer to RedisModuleConfigIterator. Unless auto-memory is enabled
* the caller is responsible for freeing the iterator using
* RedisModule_ConfigIteratorRelease(). */
RedisModuleConfigIterator *RM_ConfigIteratorCreate(RedisModuleCtx *ctx, const char *pattern) {
RedisModuleConfigIterator *iter = RM_Alloc(sizeof(*iter));
iter->di = moduleGetConfigIterator();
if (pattern != NULL) {
iter->pattern = sdsnew(pattern);
iter->is_glob = (strpbrk(pattern, "*?[") != NULL);
} else
iter->pattern = NULL;
if (ctx != NULL) autoMemoryAdd(ctx,REDISMODULE_AM_CONFIG, iter);
return iter;
}
/* Release the iterator returned by RedisModule_ConfigIteratorCreate(). If auto-memory
* is enabled and manual release is needed one must pass the same RedisModuleCtx
* that was used to create the iterator. */
void RM_ConfigIteratorRelease(RedisModuleCtx *ctx, RedisModuleConfigIterator *iter) {
if (ctx != NULL) autoMemoryFreed(ctx,REDISMODULE_AM_CONFIG,iter);
if (iter->di) dictReleaseIterator(iter->di);
sdsfree(iter->pattern);
RM_Free(iter);
}
static RedisModuleConfigType convertToRedisModuleConfigType(configType type) {
switch (type) {
case BOOL_CONFIG:
return REDISMODULE_CONFIG_TYPE_BOOL;
case NUMERIC_CONFIG:
return REDISMODULE_CONFIG_TYPE_NUMERIC;
case STRING_CONFIG:
case SDS_CONFIG:
case SPECIAL_CONFIG:
return REDISMODULE_CONFIG_TYPE_STRING;
case ENUM_CONFIG:
return REDISMODULE_CONFIG_TYPE_ENUM;
default:
serverAssert(0);
break;
}
}
/* Get the type of a config as RedisModuleConfigType. One may use this in order
* to get or set the values of the config with the appropriate function if the
* generic RedisModule_ConfigGet and RedisModule_ConfigSet APIs are performing
* poorly.
*
* Intended usage of this function is when iteration over the configs is
* performed. See RedisModule_ConfigIteratorNext() for example usage. If setting
* or getting individual configs one can check the config type by hand in
* redis.conf (or via other sources if config is added by a module) and use the
* appropriate function without the need to call this function.
*
* Explanation of config types:
* - REDISMODULE_CONFIG_TYPE_BOOL: Config is a boolean. One can use RedisModule_Config(Get/Set)Bool
* - REDISMODULE_CONFIG_TYPE_NUMERIC: Config is a numeric value. One can use RedisModule_Config(Get/Set)Numeric
* - REDISMODULE_CONFIG_TYPE_STRING: Config is a string. One can use the generic RedisModule_Config(Get/Set)
* - REDISMODULE_CONFIG_TYPE_ENUM: Config is an enum. One can use RedisModule_Config(Get/Set)Enum
*
* If a config with the given name exists `res` is populated with its type, else
* REDISMODULE_ERR is returned. */
int RM_ConfigGetType(const char *name, RedisModuleConfigType *res) {
sds config_name = sdsnew(name);
configType type;
int ret = moduleGetConfigType(config_name, &type);
sdsfree(config_name);
if (!ret)
return REDISMODULE_ERR;
*res = convertToRedisModuleConfigType(type);
return REDISMODULE_OK;
}
/* Go to the next element of the config iterator.
*
* Returns the name of the next config, or NULL if there are no more configs.
* Returned string is non-owning and thus should not be freed.
* If a pattern was provided when creating the iterator, only configs matching
* the pattern will be returned.
*
* See RedisModule_ConfigIteratorCreate() for example usage. */
const char *RM_ConfigIteratorNext(RedisModuleConfigIterator *iter) {
return moduleConfigIteratorNext(&iter->di, iter->pattern, iter->is_glob, NULL);
}
/* Get the value of a config as a string. This function can be used to get the
* value of any config, regardless of its type.
*
* The string is allocated by the module and must be freed by the caller unless
* auto memory is enabled.
*
* If the config does not exist, REDISMODULE_ERR is returned, else REDISMODULE_OK
* is returned and `res` is populated with the value. */
int RM_ConfigGet(RedisModuleCtx *ctx, const char *name, RedisModuleString **res) {
sds config_name = sdsnew(name);
sds res_sds = NULL;
int ret = moduleGetStringConfig(config_name, &res_sds);
sdsfree(config_name);
if (ret)
*res = RM_CreateString(ctx, res_sds, sdslen(res_sds));
sdsfree(res_sds);
return ret ? REDISMODULE_OK : REDISMODULE_ERR;
}
/* Get the value of a bool config.
*
* If the config does not exist or is not a bool config, REDISMODULE_ERR is
* returned, else REDISMODULE_OK is returned and `res` is populated with the
* value. */
int RM_ConfigGetBool(RedisModuleCtx *ctx, const char *name, int *res) {
UNUSED(ctx);
sds config_name = sdsnew(name);
int ret = moduleGetBoolConfig(config_name, res);
sdsfree(config_name);
return ret ? REDISMODULE_OK : REDISMODULE_ERR;
}
/* Get the value of an enum config.
*
* If the config does not exist or is not an enum config, REDISMODULE_ERR is
* returned, else REDISMODULE_OK is returned and `res` is populated with the value.
* If the config has multiple arguments they are returned as a space-separated
* string. */
int RM_ConfigGetEnum(RedisModuleCtx *ctx, const char *name, RedisModuleString **res) {
sds config_name = sdsnew(name);
sds res_sds = NULL;
int ret = moduleGetEnumConfig(config_name, &res_sds);
sdsfree(config_name);
if (ret)
*res = RM_CreateString(ctx, res_sds, sdslen(res_sds));
sdsfree(res_sds);
return ret ? REDISMODULE_OK : REDISMODULE_ERR;
}
/* Get the value of a numeric config.
*
* If the config does not exist or is not a numeric config, REDISMODULE_ERR is
* returned, else REDISMODULE_OK is returned and `res` is populated with the
* value. */
int RM_ConfigGetNumeric(RedisModuleCtx *ctx, const char *name, long long *res) {
UNUSED(ctx);
sds config_name = sdsnew(name);
int ret = moduleGetNumericConfig(config_name, res);
sdsfree(config_name);
return ret ? REDISMODULE_OK : REDISMODULE_ERR;
}
/* Set the value of a config.
*
* This function can be used to set the value of any config, regardless of its
* type. If the config is multi-argument, the value must be a space-separated
* string.
*
* If the value failed to be set REDISMODULE_ERR will be returned and if `err`
* is not NULL, it will be populated with an error message. */
int RM_ConfigSet(RedisModuleCtx *ctx, const char *name, RedisModuleString *value, RedisModuleString **err) {
sds config_name = sdsnew(name);
const char *cerr = NULL;
const char *val = RM_StringPtrLen(value, NULL);
int res = moduleSetStringConfig(ctx->client, config_name, val, &cerr);
sdsfree(config_name);
if (err && cerr)
*err = RM_CreateString(ctx, cerr, strlen(cerr));
return (res == 0 ? REDISMODULE_ERR : REDISMODULE_OK);
}
/* Set the value of a bool config.
*
* See RedisModule_ConfigSet for return value. */
int RM_ConfigSetBool(RedisModuleCtx *ctx, const char *name, int value, RedisModuleString **err) {
const char *cerr = NULL;
sds config_name = sdsnew(name);
int res = moduleSetBoolConfig(ctx->client, config_name, value, &cerr);
sdsfree(config_name);
if (err && cerr)
*err = RM_CreateString(ctx, cerr, strlen(cerr));
return (res == 0 ? REDISMODULE_ERR : REDISMODULE_OK);
}
/* Set the value of an enum config.
*
* If the config is multi-argument the value parameter must be a space-separated
* string.
*
* See RedisModule_ConfigSet for return value. */
int RM_ConfigSetEnum(RedisModuleCtx *ctx, const char *name, RedisModuleString *value, RedisModuleString **err) {
sds config_name = sdsnew(name);
const char *cerr = NULL;
size_t len;
const char *val = RM_StringPtrLen(value, &len);
sds sds_val = sdsnewlen(val, len);
int vals_cnt = 0;
sds *vals = sdssplitlen(val, sdslen(sds_val), " ", 1, &vals_cnt);
int res = moduleSetEnumConfig(ctx->client, config_name, vals, vals_cnt, &cerr);
sdsfreesplitres(vals, vals_cnt);
sdsfree(sds_val);
sdsfree(config_name);
if (err && cerr)
*err = RM_CreateString(ctx, cerr, strlen(cerr));
return (res == 0 ? REDISMODULE_ERR : REDISMODULE_OK);
}
/* Set the value of a numeric config.
* If the value passed is meant to be a percentage, it should be passed as a
* negative value.
*
* See RedisModule_ConfigSet for return value. */
int RM_ConfigSetNumeric(RedisModuleCtx *ctx, const char *name, long long value, RedisModuleString **err) {
sds config_name = sdsnew(name);
const char *cerr = NULL;
int res = moduleSetNumericConfig(ctx->client, config_name, value, &cerr);
sdsfree(config_name);
if (err && cerr)
*err = RM_CreateString(ctx, cerr, strlen(cerr));
return (res == 0 ? REDISMODULE_ERR : REDISMODULE_OK);
}
/* Redis MODULE command.
*
* MODULE LIST
@ -14550,4 +14859,16 @@ void moduleRegisterCoreAPI(void) {
REGISTER_API(RdbLoad);
REGISTER_API(RdbSave);
REGISTER_API(GetInternalSecret);
REGISTER_API(ConfigIteratorCreate);
REGISTER_API(ConfigIteratorRelease);
REGISTER_API(ConfigIteratorNext);
REGISTER_API(ConfigGetType);
REGISTER_API(ConfigGet);
REGISTER_API(ConfigGetBool);
REGISTER_API(ConfigGetEnum);
REGISTER_API(ConfigGetNumeric);
REGISTER_API(ConfigSet);
REGISTER_API(ConfigSetBool);
REGISTER_API(ConfigSetEnum);
REGISTER_API(ConfigSetNumeric);
}

View File

@ -830,6 +830,13 @@ typedef enum {
REDISMODULE_ACL_LOG_CHANNEL /* Channel authorization failure */
} RedisModuleACLLogEntryReason;
typedef enum {
REDISMODULE_CONFIG_TYPE_STRING = 0,
REDISMODULE_CONFIG_TYPE_ENUM,
REDISMODULE_CONFIG_TYPE_NUMERIC,
REDISMODULE_CONFIG_TYPE_BOOL,
} RedisModuleConfigType;
/* Incomplete structures needed by both the core and modules. */
typedef struct RedisModuleIO RedisModuleIO;
typedef struct RedisModuleDigest RedisModuleDigest;
@ -891,6 +898,7 @@ typedef struct RedisModuleScanCursor RedisModuleScanCursor;
typedef struct RedisModuleUser RedisModuleUser;
typedef struct RedisModuleKeyOptCtx RedisModuleKeyOptCtx;
typedef struct RedisModuleRdbStream RedisModuleRdbStream;
typedef struct RedisModuleConfigIterator RedisModuleConfigIterator;
typedef int (*RedisModuleCmdFunc)(RedisModuleCtx *ctx, RedisModuleString **argv, int argc);
typedef void (*RedisModuleDisconnectFunc)(RedisModuleCtx *ctx, RedisModuleBlockedClient *bc);
@ -1333,6 +1341,18 @@ REDISMODULE_API void (*RedisModule_RdbStreamFree)(RedisModuleRdbStream *stream)
REDISMODULE_API int (*RedisModule_RdbLoad)(RedisModuleCtx *ctx, RedisModuleRdbStream *stream, int flags) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_RdbSave)(RedisModuleCtx *ctx, RedisModuleRdbStream *stream, int flags) REDISMODULE_ATTR;
REDISMODULE_API const char * (*RedisModule_GetInternalSecret)(RedisModuleCtx *ctx, size_t *len) REDISMODULE_ATTR;
REDISMODULE_API RedisModuleConfigIterator * (*RedisModule_ConfigIteratorCreate)(RedisModuleCtx *ctx, const char *pattern) REDISMODULE_ATTR;
REDISMODULE_API void (*RedisModule_ConfigIteratorRelease)(RedisModuleCtx *ctx, RedisModuleConfigIterator *iter) REDISMODULE_ATTR;
REDISMODULE_API const char * (*RedisModule_ConfigIteratorNext)(RedisModuleConfigIterator *iter) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ConfigGetType)(const char *name, RedisModuleConfigType *res) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ConfigGet)(RedisModuleCtx *ctx, const char *name, RedisModuleString **res) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ConfigGetBool)(RedisModuleCtx *ctx, const char *name, int *res) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ConfigGetEnum)(RedisModuleCtx *ctx, const char *name, RedisModuleString **res) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ConfigGetNumeric)(RedisModuleCtx *ctx, const char *name, long long *res) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ConfigSet)(RedisModuleCtx *ctx, const char *name, RedisModuleString *value, RedisModuleString **err) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ConfigSetBool)(RedisModuleCtx *ctx, const char *name, int value, RedisModuleString **err) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ConfigSetEnum)(RedisModuleCtx *ctx, const char *name, RedisModuleString *value, RedisModuleString **err) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ConfigSetNumeric)(RedisModuleCtx *ctx, const char *name, long long value, RedisModuleString **err) REDISMODULE_ATTR;
#define RedisModule_IsAOFClient(id) ((id) == UINT64_MAX)
@ -1708,6 +1728,18 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int
REDISMODULE_GET_API(RdbLoad);
REDISMODULE_GET_API(RdbSave);
REDISMODULE_GET_API(GetInternalSecret);
REDISMODULE_GET_API(ConfigIteratorCreate);
REDISMODULE_GET_API(ConfigIteratorRelease);
REDISMODULE_GET_API(ConfigIteratorNext);
REDISMODULE_GET_API(ConfigGetType);
REDISMODULE_GET_API(ConfigGet);
REDISMODULE_GET_API(ConfigGetBool);
REDISMODULE_GET_API(ConfigGetEnum);
REDISMODULE_GET_API(ConfigGetNumeric);
REDISMODULE_GET_API(ConfigSet);
REDISMODULE_GET_API(ConfigSetBool);
REDISMODULE_GET_API(ConfigSetEnum);
REDISMODULE_GET_API(ConfigSetNumeric);
if (RedisModule_IsModuleNameBusy && RedisModule_IsModuleNameBusy(name)) return REDISMODULE_ERR;
RedisModule_SetModuleAttribs(ctx,name,ver,apiver);

View File

@ -3609,7 +3609,9 @@ void addModuleStringConfig(sds name, sds alias, int flags, void *privdata, sds d
void addModuleEnumConfig(sds name, sds alias, int flags, void *privdata, int default_val, configEnum *enum_vals, int num_enum_vals);
void addModuleNumericConfig(sds name, sds alias, int flags, void *privdata, long long default_val, int conf_flags, long long lower, long long upper);
void addModuleConfigApply(list *module_configs, ModuleConfig *module_config);
int moduleConfigApply(ModuleConfig *module_config, const char **err);
int moduleConfigApplyConfig(list *module_configs, const char **err, const char **err_arg_name);
int moduleConfigNeedsApply(ModuleConfig *config);
int getModuleBoolConfig(ModuleConfig *module_config);
int setModuleBoolConfig(ModuleConfig *config, int val, const char **err);
sds getModuleStringConfig(ModuleConfig *module_config);
@ -3619,6 +3621,19 @@ int setModuleEnumConfig(ModuleConfig *config, int val, const char **err);
long long getModuleNumericConfig(ModuleConfig *module_config);
int setModuleNumericConfig(ModuleConfig *config, long long val, const char **err);
/* API for modules to access config values. */
dictIterator *moduleGetConfigIterator(void);
const char *moduleConfigIteratorNext(dictIterator **iter, sds pattern, int is_glob, configType *typehint);
int moduleGetConfigType(sds name, configType *res);
int moduleGetBoolConfig(sds name, int *res);
int moduleGetStringConfig(sds name, sds *res);
int moduleGetEnumConfig(sds name, sds *res);
int moduleGetNumericConfig(sds name, long long *res);
int moduleSetBoolConfig(client *c, sds name, int val, const char **err);
int moduleSetStringConfig(client *c, sds name, const char *val, const char **err);
int moduleSetEnumConfig(client *c, sds name, sds *vals, int vals_cnt, const char **err);
int moduleSetNumericConfig(client *c, sds name, long long val, const char **err);
/* db.c -- Keyspace access API */
void updateKeysizesHist(redisDb *db, int didx, uint32_t type, int64_t oldLen, int64_t newLen);
void dbgAssertKeysizesHist(redisDb *db);

View File

@ -82,7 +82,8 @@ TEST_MODULES = \
moduleauthtwo.so \
rdbloadsave.so \
crash.so \
internalsecret.so
internalsecret.so \
configaccess.so
.PHONY: all

View File

@ -0,0 +1,345 @@
#include "redismodule.h"
#include <assert.h>
#include <string.h>
/* See moduleconfigs.c for registering module configs. We need to register some
* module configs with our module in order to test the interaction between
* module configs and the RM_Get/Set*Config APIs. */
int configaccess_bool;
int getBoolConfigCommand(const char *name, void *privdata) {
REDISMODULE_NOT_USED(name);
return (*(int *)privdata);
}
int setBoolConfigCommand(const char *name, int new, void *privdata, RedisModuleString **err) {
REDISMODULE_NOT_USED(name);
REDISMODULE_NOT_USED(err);
*(int *)privdata = new;
return REDISMODULE_OK;
}
/* Test command for RM_GetConfigType */
int TestGetConfigType_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (argc != 2) {
return RedisModule_WrongArity(ctx);
}
size_t len;
const char *config_name = RedisModule_StringPtrLen(argv[1], &len);
RedisModuleConfigType type;
int res = RedisModule_ConfigGetType(config_name, &type);
if (res == REDISMODULE_ERR) {
RedisModule_ReplyWithError(ctx, "ERR Config does not exist");
return REDISMODULE_ERR;
}
const char *type_str;
switch (type) {
case REDISMODULE_CONFIG_TYPE_BOOL:
type_str = "bool";
break;
case REDISMODULE_CONFIG_TYPE_NUMERIC:
type_str = "numeric";
break;
case REDISMODULE_CONFIG_TYPE_STRING:
type_str = "string";
break;
case REDISMODULE_CONFIG_TYPE_ENUM:
type_str = "enum";
break;
default:
assert(0);
break;
}
RedisModule_ReplyWithSimpleString(ctx, type_str);
return REDISMODULE_OK;
}
/* Test command for config iteration */
int TestConfigIteration_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_NOT_USED(argv);
if (argc > 2) {
return RedisModule_WrongArity(ctx);
}
const char *pattern = NULL;
if (argc == 2) {
pattern = RedisModule_StringPtrLen(argv[1], NULL);
}
RedisModuleConfigIterator *iter = RedisModule_ConfigIteratorCreate(ctx, pattern);
if (!iter) {
RedisModule_ReplyWithError(ctx, "ERR Failed to get config iterator");
return REDISMODULE_ERR;
}
/* Start array reply for the configs */
RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN);
/* Iterate through the dictionary */
const char *config_name = NULL;
long count = 0;
while ((config_name = RedisModule_ConfigIteratorNext(iter)) != NULL) {
RedisModuleString *value = NULL;
RedisModule_ConfigGet(ctx, config_name, &value);
RedisModule_ReplyWithArray(ctx, 2);
RedisModule_ReplyWithStringBuffer(ctx, config_name, strlen(config_name));
RedisModule_ReplyWithString(ctx, value);
RedisModule_FreeString(ctx, value);
++count;
}
RedisModule_ReplySetArrayLength(ctx, count);
/* Free the iterator */
RedisModule_ConfigIteratorRelease(ctx, iter);
return REDISMODULE_OK;
}
/* Test command for RM_GetBoolConfig */
int TestGetBoolConfig_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (argc != 2) {
return RedisModule_WrongArity(ctx);
}
size_t len;
const char *config_name = RedisModule_StringPtrLen(argv[1], &len);
int value;
if (RedisModule_ConfigGetBool(ctx, config_name, &value) == REDISMODULE_ERR) {
RedisModule_ReplyWithError(ctx, "ERR Failed to get bool config");
return REDISMODULE_ERR;
}
RedisModule_ReplyWithLongLong(ctx, value);
return REDISMODULE_OK;
}
/* Test command for RM_GetNumericConfig */
int TestGetNumericConfig_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (argc != 2) {
return RedisModule_WrongArity(ctx);
}
size_t len;
const char *config_name = RedisModule_StringPtrLen(argv[1], &len);
long long value;
if (RedisModule_ConfigGetNumeric(ctx, config_name, &value) == REDISMODULE_ERR) {
RedisModule_ReplyWithError(ctx, "ERR Failed to get numeric config");
return REDISMODULE_ERR;
}
RedisModule_ReplyWithLongLong(ctx, value);
return REDISMODULE_OK;
}
/* Test command for RM_GetConfig */
int TestGetConfig_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (argc != 2) {
return RedisModule_WrongArity(ctx);
}
size_t len;
const char *config_name = RedisModule_StringPtrLen(argv[1], &len);
RedisModuleString *value;
if (RedisModule_ConfigGet(ctx, config_name, &value) == REDISMODULE_ERR) {
RedisModule_ReplyWithError(ctx, "ERR Failed to get string config");
return REDISMODULE_ERR;
}
RedisModule_ReplyWithString(ctx, value);
RedisModule_FreeString(ctx,value);
return REDISMODULE_OK;
}
/* Test command for RM_GetEnumConfig */
int TestGetEnumConfig_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (argc != 2) {
return RedisModule_WrongArity(ctx);
}
size_t len;
const char *config_name = RedisModule_StringPtrLen(argv[1], &len);
RedisModuleString *value;
if (RedisModule_ConfigGetEnum(ctx, config_name, &value) == REDISMODULE_ERR) {
RedisModule_ReplyWithError(ctx, "ERR Failed to get enum name config");
return REDISMODULE_ERR;
}
RedisModule_ReplyWithString(ctx, value);
RedisModule_Free(value);
return REDISMODULE_OK;
}
/* Test command for RM_SetBoolConfig */
int TestSetBoolConfig_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (argc != 3) {
return RedisModule_WrongArity(ctx);
}
size_t name_len, value_len;
const char *config_name = RedisModule_StringPtrLen(argv[1], &name_len);
const char *config_value = RedisModule_StringPtrLen(argv[2], &value_len);
int bool_value;
if (!strcasecmp(config_value, "yes")) {
bool_value = 1;
} else if (!strcasecmp(config_value, "no")) {
bool_value = 0;
} else {
bool_value = -1;
}
RedisModuleString *error = NULL;
int result = RedisModule_ConfigSetBool(ctx, config_name, bool_value, &error);
if (result == REDISMODULE_ERR) {
RedisModule_ReplyWithErrorFormat(ctx, "ERR Failed to set bool config %s: %s", config_name, RedisModule_StringPtrLen(error, NULL));
RedisModule_FreeString(ctx, error);
return REDISMODULE_ERR;
}
RedisModule_ReplyWithSimpleString(ctx, "OK");
return REDISMODULE_OK;
}
/* Test command for RM_SetNumericConfig */
int TestSetNumericConfig_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (argc != 3) {
return RedisModule_WrongArity(ctx);
}
size_t name_len;
const char *config_name = RedisModule_StringPtrLen(argv[1], &name_len);
long long value;
if (RedisModule_StringToLongLong(argv[2], &value) != REDISMODULE_OK) {
RedisModule_ReplyWithError(ctx, "ERR Invalid numeric value");
return REDISMODULE_ERR;
}
RedisModuleString *error = NULL;
int result = RedisModule_ConfigSetNumeric(ctx, config_name, value, &error);
if (result == REDISMODULE_OK) {
RedisModule_ReplyWithSimpleString(ctx, "OK");
} else {
RedisModule_ReplyWithErrorFormat(ctx, "ERR Failed to set numeric config %s: %s", config_name, RedisModule_StringPtrLen(error, NULL));
RedisModule_FreeString(ctx, error);
return REDISMODULE_ERR;
}
return REDISMODULE_OK;
}
/* Test command for RM_SetConfig */
int TestSetConfig_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (argc != 3) {
return RedisModule_WrongArity(ctx);
}
size_t name_len;
const char *config_name = RedisModule_StringPtrLen(argv[1], &name_len);
RedisModuleString *error = NULL;
int result = RedisModule_ConfigSet(ctx, config_name, argv[2], &error);
if (result == REDISMODULE_OK) {
RedisModule_ReplyWithSimpleString(ctx, "OK");
} else {
RedisModule_ReplyWithErrorFormat(ctx, "ERR Failed to set string config %s: %s", config_name, RedisModule_StringPtrLen(error, NULL));
RedisModule_FreeString(ctx, error);
return REDISMODULE_ERR;
}
return REDISMODULE_OK;
}
/* Test command for RM_SetEnumConfig with name */
int TestSetEnumConfig_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (argc < 3) {
return RedisModule_WrongArity(ctx);
}
const char *config_name = RedisModule_StringPtrLen(argv[1], NULL);
RedisModuleString *error = NULL;
int result = RedisModule_ConfigSetEnum(ctx, config_name, argv[2], &error);
if (result == REDISMODULE_OK) {
RedisModule_ReplyWithSimpleString(ctx, "OK");
} else {
RedisModule_ReplyWithErrorFormat(ctx, "ERR Failed to set enum config %s: %s", config_name, RedisModule_StringPtrLen(error, NULL));
RedisModule_FreeString(ctx, error);
return REDISMODULE_ERR;
}
return REDISMODULE_OK;
}
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
if (RedisModule_Init(ctx, "configaccess", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx, "configaccess.getconfigs",
TestConfigIteration_RedisCommand, "readonly", 0, 0, 0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx, "configaccess.getbool",
TestGetBoolConfig_RedisCommand, "readonly", 0, 0, 0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx, "configaccess.getnumeric",
TestGetNumericConfig_RedisCommand, "readonly", 0, 0, 0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx, "configaccess.get",
TestGetConfig_RedisCommand, "readonly", 0, 0, 0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx, "configaccess.getenum",
TestGetEnumConfig_RedisCommand, "readonly", 0, 0, 0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx, "configaccess.setbool",
TestSetBoolConfig_RedisCommand, "write", 0, 0, 0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx, "configaccess.setnumeric",
TestSetNumericConfig_RedisCommand, "write", 0, 0, 0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx, "configaccess.set",
TestSetConfig_RedisCommand, "write", 0, 0, 0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx, "configaccess.setenum",
TestSetEnumConfig_RedisCommand, "write", 0, 0, 0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx, "configaccess.getconfigtype", TestGetConfigType_RedisCommand, "readonly", 0, 0, 0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_RegisterBoolConfig(ctx, "bool", 1, REDISMODULE_CONFIG_DEFAULT,
getBoolConfigCommand, setBoolConfigCommand, NULL, &configaccess_bool) == REDISMODULE_ERR) {
RedisModule_Log(ctx, "warning", "Failed to register configaccess_bool");
return REDISMODULE_ERR;
}
RedisModule_Log(ctx, "debug", "Loading configaccess module configuration");
if (RedisModule_LoadConfigs(ctx) == REDISMODULE_ERR) {
RedisModule_Log(ctx, "warning", "Failed to load configaccess module configuration");
return REDISMODULE_ERR;
}
return REDISMODULE_OK;
}

View File

@ -0,0 +1,219 @@
set testmodule [file normalize tests/modules/configaccess.so]
set othermodule [file normalize tests/modules/moduleconfigs.so]
start_server {tags {"modules"}} {
r module load $testmodule
r module loadex $othermodule CONFIG moduleconfigs.mutable_bool yes
test {Test module config get with standard Redis configs} {
# Test getting standard Redis configs of different types
set maxmemory [r config get maxmemory]
assert_equal [lindex $maxmemory 1] [r configaccess.getnumeric maxmemory]
set port [r config get port]
assert_equal [lindex $port 1] [r configaccess.getnumeric port]
set appendonly [r config get appendonly]
assert_equal [string is true [lindex $appendonly 1]] [r configaccess.getbool appendonly]
# Test string config
set logfile [r config get logfile]
assert_equal [lindex $logfile 1] [r configaccess.get logfile]
# Test SDS config
set requirepass [r config get requirepass]
assert_equal [lindex $requirepass 1] [r configaccess.get requirepass]
# Test special config
set oom_score_adj_values [r config get oom-score-adj-values]
assert_equal [lindex $oom_score_adj_values 1] [r configaccess.get oom-score-adj-values]
set maxmemory_policy_name [r configaccess.getenum maxmemory-policy]
assert_equal [lindex [r config get maxmemory-policy] 1] $maxmemory_policy_name
# Test percent config
r config set maxmemory 100000
r configaccess.setnumeric maxmemory-clients -50
assert_equal [lindex [r config get maxmemory-clients] 1] 50%
# Test multi-argument enum config
r config set moduleconfigs.flags "one two four"
assert_equal "five two" [r configaccess.getenum moduleconfigs.flags]
# Test getting multi-argument enum config via generic get
r config set moduleconfigs.flags "two four"
assert_equal "two four" [r configaccess.get moduleconfigs.flags]
}
test {Test module config get with non-existent configs} {
# Test getting non-existent configs
catch {r configaccess.getnumeric nonexistent_config} err
assert_match "ERR*" $err
catch {r configaccess.getbool nonexistent_config} err
assert_match "ERR*" $err
catch {r configaccess.get nonexistent_config} err
assert_match "ERR*" $err
catch {r configaccess.getenum nonexistent_config} err
assert_match "ERR*" $err
}
test {Test module config set with standard Redis configs} {
# Test setting numeric config
set old_maxmemory_samples [r config get maxmemory-samples]
r configaccess.setnumeric maxmemory-samples 10
assert_equal "maxmemory-samples 10" [r config get maxmemory-samples]
r config set maxmemory-samples [lindex $old_maxmemory_samples 1]
# Test setting bool config
set old_protected_mode [r config get protected-mode]
r configaccess.setbool protected-mode no
assert_equal "protected-mode no" [r config get protected-mode]
r config set protected-mode [lindex $old_protected_mode 1]
# Test setting string config
set old_masteruser [r config get masteruser]
r configaccess.set masteruser "__newmasteruser__"
assert_equal "__newmasteruser__" [lindex [r config get masteruser] 1]
r config set masteruser [lindex $old_masteruser 1]
# Test setting enum config
set old_loglevel [r config get loglevel]
r config set loglevel "notice" ; # Set to some value we are sure is different than the one tested
r configaccess.setenum loglevel warning
assert_equal "loglevel warning" [r config get loglevel]
r config set loglevel [lindex $old_loglevel 1]
# Test setting multi-argument enum config
r config set moduleconfigs.flags "one two four"
assert_equal "moduleconfigs.flags {five two}" [r config get moduleconfigs.flags]
r configaccess.setenum moduleconfigs.flags "two four"
assert_equal "moduleconfigs.flags {two four}" [r config get moduleconfigs.flags]
# Test setting multi-argument enum config via generic set
r config set moduleconfigs.flags "one two four"
assert_equal "moduleconfigs.flags {five two}" [r config get moduleconfigs.flags]
r configaccess.set moduleconfigs.flags "two four"
assert_equal "moduleconfigs.flags {two four}" [r config get moduleconfigs.flags]
}
test {Test module config set with module configs} {
# Test setting module bool config
assert_equal "OK" [r configaccess.setbool configaccess.bool no]
assert_equal "configaccess.bool no" [r config get configaccess.bool]
# Test setting module bool config from another module
assert_equal "OK" [r configaccess.setbool moduleconfigs.mutable_bool no]
assert_equal "moduleconfigs.mutable_bool no" [r config get moduleconfigs.mutable_bool]
# Test setting module numeric config
assert_equal "OK" [r configaccess.setnumeric moduleconfigs.numeric 100]
assert_equal "moduleconfigs.numeric 100" [r config get moduleconfigs.numeric]
# Test setting module enum config
assert_equal "OK" [r configaccess.setenum moduleconfigs.enum "five"]
assert_equal "moduleconfigs.enum five" [r config get moduleconfigs.enum]
}
test {Test module config set with error cases} {
# Test setting a non-existent config
catch {r configaccess.setbool nonexistent_config yes} err
assert_match "*ERR*" $err
# Test setting a read-only config
catch {r configaccess.setbool moduleconfigs.immutable_bool yes} err
assert_match "*ERR*" $err
# Test setting an enum config with invalid value
catch {r configaccess.setenumname moduleconfigs.enum "invalid_value"} err
assert_match "*ERR*" $err
# Test setting a numeric config with out-of-range value
catch {r configaccess.setnumeric moduleconfigs.numeric 5000} err
assert_match "*ERR*" $err
}
test {Test module get all configs} {
# Get all configs using the module command
set all_configs [r configaccess.getconfigs]
# Verify the number of configs matches the number of configs returned
# by Redis's native CONFIG GET command.
set all_configs_std_pairs [llength [r config get *]]
# When comparing with the standard CONFIG GET command, we need to divide
# by 2 because the standard command returns a flattened array of
# key-value pairs whereas our testing command returns an array of pairs.
assert_equal [llength $all_configs] [expr $all_configs_std_pairs / 2]
# Verify all the configs are present in both replies.
foreach config_pair $all_configs {
assert_equal 2 [llength $config_pair]
set config_name [lindex $config_pair 0]
set config_value [lindex $config_pair 1]
# Verify that we can get this config using standard config get
set redis_config [r config get $config_name]
assert {[llength $redis_config] != 0}
assert_equal $config_value [lindex $redis_config 1]
}
# Test that module configs are also included
set found_module_config 0
foreach config_pair $all_configs {
set config_name [lindex $config_pair 0]
if {$config_name eq "configaccess.bool"} {
set found_module_config 1
break
}
}
assert {$found_module_config == 1}
# Test pattern matching
set moduleconfigs_count [r configaccess.getconfigs "moduleconfigs.*"]
assert_equal 7 [llength $moduleconfigs_count]
set memoryconfigs_count [r configaccess.getconfigs "*memory"]
assert_equal 3 [llength $memoryconfigs_count]
}
test {Test module config type detection} {
# Test getting config types for different config types
assert_equal "bool" [r configaccess.getconfigtype appendonly]
assert_equal "numeric" [r configaccess.getconfigtype port]
assert_equal "string" [r configaccess.getconfigtype logfile]
assert_equal "enum" [r configaccess.getconfigtype maxmemory-policy]
# Test with module config
assert_equal "bool" [r configaccess.getconfigtype configaccess.bool]
# Test with non-existent config
catch {r configaccess.getconfigtype nonexistent_config} err
assert_match "ERR Config does not exist" $err
}
test {Test config rollback on apply} {
set og_port [lindex [r config get port] 1]
set used_port [find_available_port $::baseport $::portcount]
# Run a dummy server on used_port so we know we can't configure redis to
# use it. It's ok for this to fail because that means used_port is invalid
# anyway
catch {set sockfd [socket -server dummy_accept -myaddr 127.0.0.1 $used_port]} e
if {$::verbose} { puts "dummy_accept: $e" }
# Try to listen on the used port, pass some more configs to make sure the
# returned failure message is for the first bad config and everything is rolled back.
assert_error "ERR Failed to set numeric config port: Unable to listen on this port*" {
eval "r configaccess.setnumeric port $used_port"
}
assert_equal [lindex [r config get port] 1] $og_port
close $sockfd
}
}