From ea36d4de17101f05b03d267a4afbae0f7b33a27c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20S=C3=B6derqvist?= Date: Tue, 14 Sep 2021 16:48:06 +0200 Subject: [PATCH] Modules: Add remaining list API functions (#8439) List functions operating on elements by index: * RM_ListGet * RM_ListSet * RM_ListInsert * RM_ListDelete Iteration is done using a simple for loop over indices. The index based functions use an internal iterator as an optimization. This is explained in the docs: ``` * Many of the list functions access elements by index. Since a list is in * essence a doubly-linked list, accessing elements by index is generally an * O(N) operation. However, if elements are accessed sequentially or with * indices close together, the functions are optimized to seek the index from * the previous index, rather than seeking from the ends of the list. * * This enables iteration to be done efficiently using a simple for loop: * * long n = RM_ValueLength(key); * for (long i = 0; i < n; i++) { * RedisModuleString *elem = RedisModule_ListGet(key, i); * // Do stuff... * } ``` --- runtest-moduleapi | 1 + src/module.c | 299 ++++++++++++++++++++++++++++++++-- src/quicklist.c | 19 ++- src/quicklist.h | 3 + src/redismodule.h | 9 + src/server.h | 2 + src/t_list.c | 21 +++ tests/modules/Makefile | 1 + tests/modules/list.c | 236 +++++++++++++++++++++++++++ tests/unit/moduleapi/list.tcl | 66 ++++++++ 10 files changed, 635 insertions(+), 22 deletions(-) create mode 100644 tests/modules/list.c create mode 100644 tests/unit/moduleapi/list.tcl diff --git a/runtest-moduleapi b/runtest-moduleapi index f9dbcce61..2dc835a84 100755 --- a/runtest-moduleapi +++ b/runtest-moduleapi @@ -36,6 +36,7 @@ $TCLSH tests/test_helper.tcl \ --single unit/moduleapi/defrag \ --single unit/moduleapi/hash \ --single unit/moduleapi/zset \ +--single unit/moduleapi/list \ --single unit/moduleapi/stream \ --single unit/moduleapi/datatype2 \ "${@}" diff --git a/src/module.c b/src/module.c index 8bf3cbccf..6d048e57f 100644 --- a/src/module.c +++ b/src/module.c @@ -184,6 +184,11 @@ struct RedisModuleKey { int mode; /* Opening mode. */ union { + struct { + /* List, use only if value->type == OBJ_LIST */ + listTypeEntry entry; /* Current entry in iteration. */ + long index; /* Current 0-based index in iteration. */ + } list; struct { /* Zset iterator, use only if value->type == OBJ_ZSET */ uint32_t type; /* REDISMODULE_ZSET_RANGE_* */ @@ -538,6 +543,17 @@ int moduleCreateEmptyKey(RedisModuleKey *key, int type) { return REDISMODULE_OK; } +/* Frees key->iter and sets it to NULL. */ +static void moduleFreeKeyIterator(RedisModuleKey *key) { + serverAssert(key->iter != NULL); + switch (key->value->type) { + case OBJ_LIST: listTypeReleaseIterator(key->iter); break; + case OBJ_STREAM: zfree(key->iter); break; + default: serverAssert(0); /* No key->iter for other types. */ + } + key->iter = NULL; +} + /* This function is called in low-level API implementation functions in order * to check if the value associated with the key remained empty after an * operation that removed elements from an aggregate data type. @@ -563,6 +579,7 @@ int moduleDelKeyIfEmpty(RedisModuleKey *key) { } if (isempty) { + if (key->iter) moduleFreeKeyIterator(key); dbDelete(key->db,key->key); key->value = NULL; return 1; @@ -2436,13 +2453,20 @@ static void moduleCloseKey(RedisModuleKey *key) { int signal = SHOULD_SIGNAL_MODIFIED_KEYS(key->ctx); if ((key->mode & REDISMODULE_WRITE) && signal) signalModifiedKey(key->ctx->client,key->db,key->key); - if (key->iter) zfree(key->iter); - RM_ZsetRangeStop(key); - if (key && key->value && key->value->type == OBJ_STREAM && - key->u.stream.signalready) { - /* One of more RM_StreamAdd() have been done. */ - signalKeyAsReady(key->db, key->key, OBJ_STREAM); + if (key->value) { + if (key->iter) moduleFreeKeyIterator(key); + switch (key->value->type) { + case OBJ_ZSET: + RM_ZsetRangeStop(key); + break; + case OBJ_STREAM: + if (key->u.stream.signalready) + /* One or more RM_StreamAdd() have been done. */ + signalKeyAsReady(key->db, key->key, OBJ_STREAM); + break; + } } + serverAssert(key->iter == NULL); decrRefCount(key->key); } @@ -2735,16 +2759,108 @@ int RM_StringTruncate(RedisModuleKey *key, size_t newlen) { /* -------------------------------------------------------------------------- * ## Key API for List type * + * Many of the list functions access elements by index. Since a list is in + * essence a doubly-linked list, accessing elements by index is generally an + * O(N) operation. However, if elements are accessed sequentially or with + * indices close together, the functions are optimized to seek the index from + * the previous index, rather than seeking from the ends of the list. + * + * This enables iteration to be done efficiently using a simple for loop: + * + * long n = RM_ValueLength(key); + * for (long i = 0; i < n; i++) { + * RedisModuleString *elem = RedisModule_ListGet(key, i); + * // Do stuff... + * } + * + * Note that after modifying a list using RM_ListPop, RM_ListSet or + * RM_ListInsert, the internal iterator is invalidated so the next operation + * will require a linear seek. + * + * Modifying a list in any another way, for examle using RM_Call(), while a key + * is open will confuse the internal iterator and may cause trouble if the key + * is used after such modifications. The key must be reopened in this case. + * * See also RM_ValueLength(), which returns the length of a list. * -------------------------------------------------------------------------- */ -/* Push an element into a list, on head or tail depending on 'where' argument. - * If the key pointer is about an empty key opened for writing, the key - * is created. On error (key opened for read-only operations or of the wrong - * type) REDISMODULE_ERR is returned, otherwise REDISMODULE_OK is returned. */ +/* Seeks the key's internal list iterator to the given index. On success, 1 is + * returned and key->iter, key->u.list.entry and key->u.list.index are set. On + * failure, 0 is returned and errno is set as required by the list API + * functions. */ +int moduleListIteratorSeek(RedisModuleKey *key, long index, int mode) { + if (!key) { + errno = EINVAL; + return 0; + } else if (!key->value || key->value->type != OBJ_LIST) { + errno = ENOTSUP; + return 0; + } if (!(key->mode & mode)) { + errno = EBADF; + return 0; + } + + long length = listTypeLength(key->value); + if (index < -length || index >= length) { + errno = EDOM; /* Invalid index */ + return 0; + } + + if (key->iter == NULL) { + /* No existing iterator. Create one. */ + key->iter = listTypeInitIterator(key->value, index, LIST_TAIL); + serverAssert(key->iter != NULL); + serverAssert(listTypeNext(key->iter, &key->u.list.entry)); + key->u.list.index = index; + return 1; + } + + /* There's an existing iterator. Make sure the requested index has the same + * sign as the iterator's index. */ + if (index < 0 && key->u.list.index >= 0) index += length; + else if (index >= 0 && key->u.list.index < 0) index -= length; + + if (index == key->u.list.index) return 1; /* We're done. */ + + /* Seek the iterator to the requested index. */ + unsigned char dir = key->u.list.index < index ? LIST_TAIL : LIST_HEAD; + listTypeSetIteratorDirection(key->iter, dir); + while (key->u.list.index != index) { + serverAssert(listTypeNext(key->iter, &key->u.list.entry)); + key->u.list.index += dir == LIST_HEAD ? -1 : 1; + } + return 1; +} + +/* Push an element into a list, on head or tail depending on 'where' argument + * (REDISMODULE_LIST_HEAD or REDISMODULE_LIST_TAIL). If the key refers to an + * empty key opened for writing, the key is created. On success, REDISMODULE_OK + * is returned. On failure, REDISMODULE_ERR is returned and `errno` is set as + * follows: + * + * - EINVAL if key or ele is NULL. + * - ENOTSUP if the key is of another type than list. + * - EBADF if the key is not opened for writing. + * + * Note: Before Redis 7.0, `errno` was not set by this function. */ int RM_ListPush(RedisModuleKey *key, int where, RedisModuleString *ele) { + if (!key || !ele) { + errno = EINVAL; + return REDISMODULE_ERR; + } else if (key->value != NULL && key->value->type != OBJ_LIST) { + errno = ENOTSUP; + return REDISMODULE_ERR; + } if (!(key->mode & REDISMODULE_WRITE)) { + errno = EBADF; + return REDISMODULE_ERR; + } + if (!(key->mode & REDISMODULE_WRITE)) return REDISMODULE_ERR; if (key->value && key->value->type != OBJ_LIST) return REDISMODULE_ERR; + if (key->iter) { + listTypeReleaseIterator(key->iter); + key->iter = NULL; + } if (key->value == NULL) moduleCreateEmptyKey(key,REDISMODULE_KEYTYPE_LIST); listTypePush(key->value, ele, (where == REDISMODULE_LIST_HEAD) ? LIST_HEAD : LIST_TAIL); @@ -2753,16 +2869,31 @@ int RM_ListPush(RedisModuleKey *key, int where, RedisModuleString *ele) { /* Pop an element from the list, and returns it as a module string object * that the user should be free with RM_FreeString() or by enabling - * automatic memory. 'where' specifies if the element should be popped from - * head or tail. The command returns NULL if: + * automatic memory. The `where` argument specifies if the element should be + * popped from the beginning or the end of the list (REDISMODULE_LIST_HEAD or + * REDISMODULE_LIST_TAIL). On failure, the command returns NULL and sets + * `errno` as follows: * - * 1. The list is empty. - * 2. The key was not open for writing. - * 3. The key is not a list. */ + * - EINVAL if key is NULL. + * - ENOTSUP if the key is empty or of another type than list. + * - EBADF if the key is not opened for writing. + * + * Note: Before Redis 7.0, `errno` was not set by this function. */ RedisModuleString *RM_ListPop(RedisModuleKey *key, int where) { - if (!(key->mode & REDISMODULE_WRITE) || - key->value == NULL || - key->value->type != OBJ_LIST) return NULL; + if (!key) { + errno = EINVAL; + return NULL; + } else if (key->value == NULL || key->value->type != OBJ_LIST) { + errno = ENOTSUP; + return NULL; + } else if (!(key->mode & REDISMODULE_WRITE)) { + errno = EBADF; + return NULL; + } + if (key->iter) { + listTypeReleaseIterator(key->iter); + key->iter = NULL; + } robj *ele = listTypePop(key->value, (where == REDISMODULE_LIST_HEAD) ? LIST_HEAD : LIST_TAIL); robj *decoded = getDecodedObject(ele); @@ -2772,6 +2903,134 @@ RedisModuleString *RM_ListPop(RedisModuleKey *key, int where) { return decoded; } +/* Returns the element at index `index` in the list stored at `key`, like the + * LINDEX command. The element should be free'd using RM_FreeString() or using + * automatic memory management. + * + * The index is zero-based, so 0 means the first element, 1 the second element + * and so on. Negative indices can be used to designate elements starting at the + * tail of the list. Here, -1 means the last element, -2 means the penultimate + * and so forth. + * + * When no value is found at the given key and index, NULL is returned and + * `errno` is set as follows: + * + * - EINVAL if key is NULL. + * - ENOTSUP if the key is not a list. + * - EBADF if the key is not opened for reading. + * - EDOM if the index is not a valid index in the list. + */ +RedisModuleString *RM_ListGet(RedisModuleKey *key, long index) { + if (moduleListIteratorSeek(key, index, REDISMODULE_READ)) { + robj *elem = listTypeGet(&key->u.list.entry); + robj *decoded = getDecodedObject(elem); + decrRefCount(elem); + autoMemoryAdd(key->ctx, REDISMODULE_AM_STRING, decoded); + return decoded; + } else { + return NULL; + } +} + +/* Replaces the element at index `index` in the list stored at `key`. + * + * The index is zero-based, so 0 means the first element, 1 the second element + * and so on. Negative indices can be used to designate elements starting at the + * tail of the list. Here, -1 means the last element, -2 means the penultimate + * and so forth. + * + * On success, REDISMODULE_OK is returned. On failure, REDISMODULE_ERR is + * returned and `errno` is set as follows: + * + * - EINVAL if key or value is NULL. + * - ENOTSUP if the key is not a list. + * - EBADF if the key is not opened for writing. + * - EDOM if the index is not a valid index in the list. + */ +int RM_ListSet(RedisModuleKey *key, long index, RedisModuleString *value) { + if (!value) { + errno = EINVAL; + return REDISMODULE_ERR; + } + if (moduleListIteratorSeek(key, index, REDISMODULE_WRITE)) { + listTypeReplace(&key->u.list.entry, value); + /* A note in quicklist.c forbids use of iterator after insert, so + * probably also after replace. */ + listTypeReleaseIterator(key->iter); + key->iter = NULL; + return REDISMODULE_OK; + } else { + return REDISMODULE_ERR; + } +} + +/* Inserts an element at the given index. + * + * The index is zero-based, so 0 means the first element, 1 the second element + * and so on. Negative indices can be used to designate elements starting at the + * tail of the list. Here, -1 means the last element, -2 means the penultimate + * and so forth. The index is the element's index after inserting it. + * + * On success, REDISMODULE_OK is returned. On failure, REDISMODULE_ERR is + * returned and `errno` is set as follows: + * + * - EINVAL if key or value is NULL. + * - ENOTSUP if the key of another type than list. + * - EBADF if the key is not opened for writing. + * - EDOM if the index is not a valid index in the list. + */ +int RM_ListInsert(RedisModuleKey *key, long index, RedisModuleString *value) { + if (!value) { + errno = EINVAL; + return REDISMODULE_ERR; + } else if (key != NULL && key->value == NULL && + (index == 0 || index == -1)) { + /* Insert in empty key => push. */ + return RM_ListPush(key, REDISMODULE_LIST_TAIL, value); + } else if (key != NULL && key->value != NULL && + key->value->type == OBJ_LIST && + (index == (long)listTypeLength(key->value) || index == -1)) { + /* Insert after the last element => push tail. */ + return RM_ListPush(key, REDISMODULE_LIST_TAIL, value); + } else if (key != NULL && key->value != NULL && + key->value->type == OBJ_LIST && + (index == 0 || index == -(long)listTypeLength(key->value) - 1)) { + /* Insert before the first element => push head. */ + return RM_ListPush(key, REDISMODULE_LIST_HEAD, value); + } + if (moduleListIteratorSeek(key, index, REDISMODULE_WRITE)) { + int where = index < 0 ? LIST_TAIL : LIST_HEAD; + listTypeInsert(&key->u.list.entry, value, where); + /* A note in quicklist.c forbids use of iterator after insert. */ + listTypeReleaseIterator(key->iter); + key->iter = NULL; + return REDISMODULE_OK; + } else { + return REDISMODULE_ERR; + } +} + +/* Removes an element at the given index. The index is 0-based. A negative index + * can also be used, counting from the end of the list. + * + * On success, REDISMODULE_OK is returned. On failure, REDISMODULE_ERR is + * returned and `errno` is set as follows: + * + * - EINVAL if key or value is NULL. + * - ENOTSUP if the key is not a list. + * - EBADF if the key is not opened for writing. + * - EDOM if the index is not a valid index in the list. + */ +int RM_ListDelete(RedisModuleKey *key, long index) { + if (moduleListIteratorSeek(key, index, REDISMODULE_WRITE)) { + listTypeDelete(key->iter, &key->u.list.entry); + moduleDelKeyIfEmpty(key); + return REDISMODULE_OK; + } else { + return REDISMODULE_ERR; + } +} + /* -------------------------------------------------------------------------- * ## Key API for Sorted Set type * @@ -9564,6 +9823,10 @@ void moduleRegisterCoreAPI(void) { REGISTER_API(ValueLength); REGISTER_API(ListPush); REGISTER_API(ListPop); + REGISTER_API(ListGet); + REGISTER_API(ListSet); + REGISTER_API(ListInsert); + REGISTER_API(ListDelete); REGISTER_API(StringToLongLong); REGISTER_API(StringToDouble); REGISTER_API(StringToLongDouble); diff --git a/src/quicklist.c b/src/quicklist.c index ec5409533..c63b35468 100644 --- a/src/quicklist.c +++ b/src/quicklist.c @@ -673,6 +673,15 @@ void quicklistDelEntry(quicklistIter *iter, quicklistEntry *entry) { * quicklistNext() will jump to the next node. */ } +/* Replace quicklist entry by 'data' with length 'sz'. */ +void quicklistReplaceEntry(quicklist *quicklist, quicklistEntry *entry, + void *data, int sz) { + /* quicklistNext() and quicklistIndex() provide an uncompressed node */ + entry->node->zl = ziplistReplace(entry->node->zl, entry->zi, data, sz); + quicklistNodeUpdateSz(entry->node); + quicklistCompress(quicklist, entry->node); +} + /* Replace quicklist entry at offset 'index' by 'data' with length 'sz'. * * Returns 1 if replace happened. @@ -681,10 +690,7 @@ int quicklistReplaceAtIndex(quicklist *quicklist, long index, void *data, int sz) { quicklistEntry entry; if (likely(quicklistIndex(quicklist, index, &entry))) { - /* quicklistIndex provides an uncompressed node */ - entry.node->zl = ziplistReplace(entry.node->zl, entry.zi, data, sz); - quicklistNodeUpdateSz(entry.node); - quicklistCompress(quicklist, entry.node); + quicklistReplaceEntry(quicklist, &entry, data, sz); return 1; } else { return 0; @@ -1189,6 +1195,11 @@ int quicklistNext(quicklistIter *iter, quicklistEntry *entry) { } } +/* Sets the direction of a quicklist iterator. */ +void quicklistSetDirection(quicklistIter *iter, int direction) { + iter->direction = direction; +} + /* Duplicate the quicklist. * On success a copy of the original quicklist is returned. * diff --git a/src/quicklist.h b/src/quicklist.h index b9c01757d..173d9a419 100644 --- a/src/quicklist.h +++ b/src/quicklist.h @@ -169,6 +169,8 @@ void quicklistInsertAfter(quicklist *quicklist, quicklistEntry *entry, void quicklistInsertBefore(quicklist *quicklist, quicklistEntry *entry, void *value, const size_t sz); void quicklistDelEntry(quicklistIter *iter, quicklistEntry *entry); +void quicklistReplaceEntry(quicklist *quicklist, quicklistEntry *entry, + void *data, int sz); int quicklistReplaceAtIndex(quicklist *quicklist, long index, void *data, int sz); int quicklistDelRange(quicklist *quicklist, const long start, const long stop); @@ -176,6 +178,7 @@ quicklistIter *quicklistGetIterator(const quicklist *quicklist, int direction); quicklistIter *quicklistGetIteratorAtIdx(const quicklist *quicklist, int direction, const long long idx); int quicklistNext(quicklistIter *iter, quicklistEntry *entry); +void quicklistSetDirection(quicklistIter *iter, int direction); void quicklistReleaseIterator(quicklistIter *iter); quicklist *quicklistDup(quicklist *orig); int quicklistIndex(const quicklist *quicklist, const long long index, diff --git a/src/redismodule.h b/src/redismodule.h index be2186bc4..dd491ef24 100644 --- a/src/redismodule.h +++ b/src/redismodule.h @@ -27,6 +27,7 @@ * Avoid touching the LRU/LFU of the key when opened. */ #define REDISMODULE_OPEN_KEY_NOTOUCH (1<<16) +/* List push and pop */ #define REDISMODULE_LIST_HEAD 0 #define REDISMODULE_LIST_TAIL 1 @@ -615,6 +616,10 @@ REDISMODULE_API int (*RedisModule_KeyType)(RedisModuleKey *kp) REDISMODULE_ATTR; REDISMODULE_API size_t (*RedisModule_ValueLength)(RedisModuleKey *kp) REDISMODULE_ATTR; REDISMODULE_API int (*RedisModule_ListPush)(RedisModuleKey *kp, int where, RedisModuleString *ele) REDISMODULE_ATTR; REDISMODULE_API RedisModuleString * (*RedisModule_ListPop)(RedisModuleKey *key, int where) REDISMODULE_ATTR; +REDISMODULE_API RedisModuleString * (*RedisModule_ListGet)(RedisModuleKey *key, long index) REDISMODULE_ATTR; +REDISMODULE_API int (*RedisModule_ListSet)(RedisModuleKey *key, long index, RedisModuleString *value) REDISMODULE_ATTR; +REDISMODULE_API int (*RedisModule_ListInsert)(RedisModuleKey *key, long index, RedisModuleString *value) REDISMODULE_ATTR; +REDISMODULE_API int (*RedisModule_ListDelete)(RedisModuleKey *key, long index) REDISMODULE_ATTR; REDISMODULE_API RedisModuleCallReply * (*RedisModule_Call)(RedisModuleCtx *ctx, const char *cmdname, const char *fmt, ...) REDISMODULE_ATTR; REDISMODULE_API const char * (*RedisModule_CallReplyProto)(RedisModuleCallReply *reply, size_t *len) REDISMODULE_ATTR; REDISMODULE_API void (*RedisModule_FreeCallReply)(RedisModuleCallReply *reply) REDISMODULE_ATTR; @@ -941,6 +946,10 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int REDISMODULE_GET_API(ValueLength); REDISMODULE_GET_API(ListPush); REDISMODULE_GET_API(ListPop); + REDISMODULE_GET_API(ListGet); + REDISMODULE_GET_API(ListSet); + REDISMODULE_GET_API(ListInsert); + REDISMODULE_GET_API(ListDelete); REDISMODULE_GET_API(StringToLongLong); REDISMODULE_GET_API(StringToDouble); REDISMODULE_GET_API(StringToLongDouble); diff --git a/src/server.h b/src/server.h index 4c95f69ff..905836c93 100644 --- a/src/server.h +++ b/src/server.h @@ -1988,9 +1988,11 @@ robj *listTypePop(robj *subject, int where); unsigned long listTypeLength(const robj *subject); listTypeIterator *listTypeInitIterator(robj *subject, long index, unsigned char direction); void listTypeReleaseIterator(listTypeIterator *li); +void listTypeSetIteratorDirection(listTypeIterator *li, unsigned char direction); int listTypeNext(listTypeIterator *li, listTypeEntry *entry); robj *listTypeGet(listTypeEntry *entry); void listTypeInsert(listTypeEntry *entry, robj *value, int where); +void listTypeReplace(listTypeEntry *entry, robj *value); int listTypeEqual(listTypeEntry *entry, robj *o); void listTypeDelete(listTypeIterator *iter, listTypeEntry *entry); void listTypeConvert(robj *subject, int enc); diff --git a/src/t_list.c b/src/t_list.c index fb63c1509..80715996e 100644 --- a/src/t_list.c +++ b/src/t_list.c @@ -103,6 +103,13 @@ listTypeIterator *listTypeInitIterator(robj *subject, long index, return li; } +/* Sets the direction of an iterator. */ +void listTypeSetIteratorDirection(listTypeIterator *li, unsigned char direction) { + li->direction = direction; + int dir = direction == LIST_HEAD ? AL_START_TAIL : AL_START_HEAD; + quicklistSetDirection(li->iter, dir); +} + /* Clean up the iterator. */ void listTypeReleaseIterator(listTypeIterator *li) { zfree(li->iter); @@ -159,6 +166,20 @@ void listTypeInsert(listTypeEntry *entry, robj *value, int where) { } } +/* Replaces entry at the current position of the iterator. */ +void listTypeReplace(listTypeEntry *entry, robj *value) { + if (entry->li->encoding == OBJ_ENCODING_QUICKLIST) { + value = getDecodedObject(value); + sds str = value->ptr; + size_t len = sdslen(str); + quicklistReplaceEntry((quicklist *)entry->entry.quicklist, + &entry->entry, str, len); + decrRefCount(value); + } else { + serverPanic("Unknown list encoding"); + } +} + /* Compare the given object with the entry at the current position. */ int listTypeEqual(listTypeEntry *entry, robj *o) { if (entry->li->encoding == OBJ_ENCODING_QUICKLIST) { diff --git a/tests/modules/Makefile b/tests/modules/Makefile index 6a6864921..c0fbf1d0f 100644 --- a/tests/modules/Makefile +++ b/tests/modules/Makefile @@ -40,6 +40,7 @@ TEST_MODULES = \ hash.so \ zset.so \ stream.so \ + list.so .PHONY: all diff --git a/tests/modules/list.c b/tests/modules/list.c new file mode 100644 index 000000000..fcbd446ce --- /dev/null +++ b/tests/modules/list.c @@ -0,0 +1,236 @@ +#include "redismodule.h" +#include +#include +#include + +/* LIST.GETALL key [REVERSE] */ +int list_getall(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + if (argc < 2 || argc > 3) return RedisModule_WrongArity(ctx); + int reverse = (argc == 3 && + !strcasecmp(RedisModule_StringPtrLen(argv[2], NULL), + "REVERSE")); + RedisModule_AutoMemory(ctx); + RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ); + if (RedisModule_KeyType(key) != REDISMODULE_KEYTYPE_LIST) { + return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE); + } + long n = RedisModule_ValueLength(key); + RedisModule_ReplyWithArray(ctx, n); + if (!reverse) { + for (long i = 0; i < n; i++) { + RedisModuleString *elem = RedisModule_ListGet(key, i); + RedisModule_ReplyWithString(ctx, elem); + RedisModule_FreeString(ctx, elem); + } + } else { + for (long i = -1; i >= -n; i--) { + RedisModuleString *elem = RedisModule_ListGet(key, i); + RedisModule_ReplyWithString(ctx, elem); + RedisModule_FreeString(ctx, elem); + } + } + + /* Test error condition: index out of bounds */ + assert(RedisModule_ListGet(key, n) == NULL); + assert(errno == EDOM); /* no more elements in list */ + + /* RedisModule_CloseKey(key); //implicit, done by auto memory */ + return REDISMODULE_OK; +} + +/* LIST.EDIT key [REVERSE] cmdstr [value ..] + * + * cmdstr is a string of the following characters: + * + * k -- keep + * d -- delete + * i -- insert value from args + * r -- replace with value from args + * + * The number of occurrences of "i" and "r" in cmdstr) should correspond to the + * number of args after cmdstr. + * + * The reply is the number of edits (inserts + replaces + deletes) performed. + */ +int list_edit(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + if (argc < 3) return RedisModule_WrongArity(ctx); + RedisModule_AutoMemory(ctx); + int argpos = 1; /* the next arg */ + + /* key */ + int keymode = REDISMODULE_READ | REDISMODULE_WRITE; + RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[argpos++], keymode); + if (RedisModule_KeyType(key) != REDISMODULE_KEYTYPE_LIST) { + return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE); + } + + /* REVERSE */ + int reverse = 0; + if (argc >= 4 && + !strcasecmp(RedisModule_StringPtrLen(argv[argpos], NULL), "REVERSE")) { + reverse = 1; + argpos++; + } + + /* cmdstr */ + size_t cmdstr_len; + const char *cmdstr = RedisModule_StringPtrLen(argv[argpos++], &cmdstr_len); + + /* validate cmdstr vs. argc */ + long num_req_args = 0; + long min_list_length = 0; + for (size_t cmdpos = 0; cmdpos < cmdstr_len; cmdpos++) { + char c = cmdstr[cmdpos]; + if (c == 'i' || c == 'r') num_req_args++; + if (c == 'd' || c == 'r' || c == 'k') min_list_length++; + } + if (argc < argpos + num_req_args) { + return RedisModule_ReplyWithError(ctx, "ERR too few args"); + } + if ((long)RedisModule_ValueLength(key) < min_list_length) { + return RedisModule_ReplyWithError(ctx, "ERR list too short"); + } + + /* Iterate over the chars in cmdstr (edit instructions) */ + long long num_edits = 0; + long index = reverse ? -1 : 0; + RedisModuleString *value; + + for (size_t cmdpos = 0; cmdpos < cmdstr_len; cmdpos++) { + switch (cmdstr[cmdpos]) { + case 'i': /* insert */ + value = argv[argpos++]; + assert(RedisModule_ListInsert(key, index, value) == REDISMODULE_OK); + index += reverse ? -1 : 1; + num_edits++; + break; + case 'd': /* delete */ + assert(RedisModule_ListDelete(key, index) == REDISMODULE_OK); + num_edits++; + break; + case 'r': /* replace */ + value = argv[argpos++]; + assert(RedisModule_ListSet(key, index, value) == REDISMODULE_OK); + index += reverse ? -1 : 1; + num_edits++; + break; + case 'k': /* keep */ + index += reverse ? -1 : 1; + break; + } + } + + RedisModule_ReplyWithLongLong(ctx, num_edits); + RedisModule_CloseKey(key); + return REDISMODULE_OK; +} + +/* Reply based on errno as set by the List API functions. */ +static int replyByErrno(RedisModuleCtx *ctx) { + switch (errno) { + case EDOM: + return RedisModule_ReplyWithError(ctx, "ERR index out of bounds"); + case ENOTSUP: + return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE); + default: assert(0); /* Can't happen */ + } +} + +/* LIST.GET key index */ +int list_get(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + if (argc != 3) return RedisModule_WrongArity(ctx); + long long index; + if (RedisModule_StringToLongLong(argv[2], &index) != REDISMODULE_OK) { + return RedisModule_ReplyWithError(ctx, "ERR index must be a number"); + } + RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ); + RedisModuleString *value = RedisModule_ListGet(key, index); + if (value) { + RedisModule_ReplyWithString(ctx, value); + RedisModule_FreeString(ctx, value); + } else { + replyByErrno(ctx); + } + RedisModule_CloseKey(key); + return REDISMODULE_OK; +} + +/* LIST.SET key index value */ +int list_set(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + if (argc != 4) return RedisModule_WrongArity(ctx); + long long index; + if (RedisModule_StringToLongLong(argv[2], &index) != REDISMODULE_OK) { + RedisModule_ReplyWithError(ctx, "ERR index must be a number"); + return REDISMODULE_OK; + } + RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE); + if (RedisModule_ListSet(key, index, argv[3]) == REDISMODULE_OK) { + RedisModule_ReplyWithSimpleString(ctx, "OK"); + } else { + replyByErrno(ctx); + } + RedisModule_CloseKey(key); + return REDISMODULE_OK; +} + +/* LIST.INSERT key index value + * + * If index is negative, value is inserted after, otherwise before the element + * at index. + */ +int list_insert(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + if (argc != 4) return RedisModule_WrongArity(ctx); + long long index; + if (RedisModule_StringToLongLong(argv[2], &index) != REDISMODULE_OK) { + RedisModule_ReplyWithError(ctx, "ERR index must be a number"); + return REDISMODULE_OK; + } + RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE); + if (RedisModule_ListInsert(key, index, argv[3]) == REDISMODULE_OK) { + RedisModule_ReplyWithSimpleString(ctx, "OK"); + } else { + replyByErrno(ctx); + } + RedisModule_CloseKey(key); + return REDISMODULE_OK; +} + +/* LIST.DELETE key index */ +int list_delete(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + if (argc != 3) return RedisModule_WrongArity(ctx); + long long index; + if (RedisModule_StringToLongLong(argv[2], &index) != REDISMODULE_OK) { + RedisModule_ReplyWithError(ctx, "ERR index must be a number"); + return REDISMODULE_OK; + } + RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE); + if (RedisModule_ListDelete(key, index) == REDISMODULE_OK) { + RedisModule_ReplyWithSimpleString(ctx, "OK"); + } else { + replyByErrno(ctx); + } + RedisModule_CloseKey(key); + return REDISMODULE_OK; +} + +int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + REDISMODULE_NOT_USED(argv); + REDISMODULE_NOT_USED(argc); + if (RedisModule_Init(ctx, "list", 1, REDISMODULE_APIVER_1) == REDISMODULE_OK && + RedisModule_CreateCommand(ctx, "list.getall", list_getall, "", + 1, 1, 1) == REDISMODULE_OK && + RedisModule_CreateCommand(ctx, "list.edit", list_edit, "", + 1, 1, 1) == REDISMODULE_OK && + RedisModule_CreateCommand(ctx, "list.get", list_get, "", + 1, 1, 1) == REDISMODULE_OK && + RedisModule_CreateCommand(ctx, "list.set", list_set, "", + 1, 1, 1) == REDISMODULE_OK && + RedisModule_CreateCommand(ctx, "list.insert", list_insert, "", + 1, 1, 1) == REDISMODULE_OK && + RedisModule_CreateCommand(ctx, "list.delete", list_delete, "", + 1, 1, 1) == REDISMODULE_OK) { + return REDISMODULE_OK; + } else { + return REDISMODULE_ERR; + } +} diff --git a/tests/unit/moduleapi/list.tcl b/tests/unit/moduleapi/list.tcl new file mode 100644 index 000000000..6094a0354 --- /dev/null +++ b/tests/unit/moduleapi/list.tcl @@ -0,0 +1,66 @@ +set testmodule [file normalize tests/modules/list.so] + +start_server {tags {"modules"}} { + r module load $testmodule + + test {Module list set, get, insert, delete} { + r del k + r rpush k x + # insert, set, get + r list.insert k 0 foo + r list.insert k -1 bar + r list.set k 1 xyz + assert_equal {foo xyz bar} [r list.getall k] + assert_equal {foo} [r list.get k 0] + assert_equal {xyz} [r list.get k 1] + assert_equal {bar} [r list.get k 2] + assert_equal {bar} [r list.get k -1] + assert_equal {foo} [r list.get k -3] + assert_error {ERR index out*} {r list.get k -4} + assert_error {ERR index out*} {r list.get k 3} + # remove + assert_error {ERR index out*} {r list.delete k -4} + assert_error {ERR index out*} {r list.delete k 3} + r list.delete k 0 + r list.delete k -1 + assert_equal {xyz} [r list.getall k] + # removing the last element deletes the list + r list.delete k 0 + assert_equal 0 [r exists k] + } + + test {Module list iteration} { + r del k + r rpush k x y z + assert_equal {x y z} [r list.getall k] + assert_equal {z y x} [r list.getall k REVERSE] + } + + test {Module list insert & delete} { + r del k + r rpush k x y z + r list.edit k ikikdi foo bar baz + r list.getall k + } {foo x bar y baz} + + test {Module list insert & delete, neg index} { + r del k + r rpush k x y z + r list.edit k REVERSE ikikdi foo bar baz + r list.getall k + } {baz y bar z foo} + + test {Module list set while iterating} { + r del k + r rpush k x y z + r list.edit k rkr foo bar + r list.getall k + } {foo y bar} + + test {Module list set while iterating, neg index} { + r del k + r rpush k x y z + r list.edit k reverse rkr foo bar + r list.getall k + } {bar y foo} +}