redis/tests/modules/configaccess.c
Mincho Paskalev 15706f2e82
Module set/get config API (#14051)
# 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>
2025-07-03 13:46:33 +03:00

345 lines
12 KiB
C

#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;
}