Skip to content

Nordix/hiredis-cluster

Repository files navigation

Hiredis-cluster

Hiredis-cluster is a C client library for cluster deployments of the Redis database.

Hiredis-cluster is using Hiredis for the connections to each Redis node.

Hiredis-cluster is a fork of Hiredis-vip, with the following improvements:

  • The C library hiredis is an external dependency rather than a builtin part of the cluster client, meaning that the latest hiredis can be used.
  • Support for SSL/TLS introduced in Redis 6
  • Support for IPv6
  • Support authentication using AUTH
  • Uses CMake (3.11+) as the primary build system, but optionally Make can be used directly
  • Code style guide (using clang-format)
  • Improved testing
  • Memory leak corrections and allocation failure handling
  • Low-level API for sending commands to specific node

Features

  • Redis Cluster

    • Connect to a Redis cluster and run commands.
  • Multi-key commands

    • Support MSET, MGET and DEL.
    • Multi-key commands will be processed and sent to slot owning nodes. (This breaks the atomicity of the commands if the keys reside on different nodes so if atomicity is important, use these only with keys in the same cluster slot.)
  • Pipelining

    • Send multiple commands at once to speed up queries.
    • Supports multi-key commands described in above bullet.
  • Asynchronous API

    • Send commands asynchronously and let a callback handle the response.
    • Needs an external event loop system that can be attached using an adapter.
  • SSL/TLS

    • Connect to Redis nodes using SSL/TLS (supported from Redis 6)
  • IPv6

    • Handles clusters on IPv6 networks

Build instructions

Prerequisites:

Hiredis-cluster will be built as a shared library libhiredis_cluster.so and it depends on the hiredis shared library libhiredis.so.

When SSL/TLS support is enabled an extra library libhiredis_cluster_ssl.so is built, which depends on the hiredis SSL support library libhiredis_ssl.a.

A user project that needs SSL/TLS support should link to both libhiredis_cluster.so and libhiredis_cluster_ssl.so to enable the SSL/TLS configuration API.

$ mkdir build; cd build
$ cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DENABLE_SSL=ON ..
$ make

Build options

The following CMake options are available:

  • DOWNLOAD_HIREDIS
    • OFF CMake will search for an already installed hiredis (for example the the Debian package libhiredis-dev) for header files and linkage.
    • ON (default) hiredis will be downloaded from Github, built and installed locally in the build folder.
  • ENABLE_SSL
    • OFF (default)
    • ON Enable SSL/TLS support and build its tests (also affect hiredis when DOWNLOAD_HIREDIS=ON).
  • DISABLE_TESTS
    • OFF (default)
    • ON Disable compilation of tests (also affect hiredis when DOWNLOAD_HIREDIS=ON).
  • ENABLE_IPV6_TESTS
    • OFF (default)
    • ON Enable IPv6 tests. Requires that IPv6 is setup in Docker.
  • ENABLE_COVERAGE
    • OFF (default)
    • ON Compile using build flags that enables the GNU coverage tool gcov to provide test coverage information. This CMake option also enables a new build target coverage to generate a test coverage report using gcovr.
  • USE_SANITIZER Compile using a specific sanitizer that detect issues. The value of this option specifies which sanitizer to activate, but it depends on support in the compiler. Common option values are: address, thread, undefined, leak

Options needs to be set with the -D flag when generating makefiles, e.g.

cmake -DENABLE_SSL=ON -DUSE_SANITIZER=address ..

Build details

The build uses CMake's find_package to search for a hiredis installation. CMake will search for a hiredis installation in the default paths, searching for a file called hiredis-config.cmake. The default search path can be altered via CMAKE_PREFIX_PATH or as described in the CMake docs; a specific path can be set using a flag like: -Dhiredis_DIR:PATH=${MY_DIR}/hiredis/share/hiredis

See examples/using_cmake_separate/build.sh or examples/using_cmake_externalproject/build.sh for alternative CMake builds.

Extend the list of supported commands

The list of commands and the position of the first key in the command line is defined in cmddef.h which is included in this repo. It has been generated using the JSON files describing the syntax of each command in the Redis repository, which makes sure hiredis-cluster supports all commands in Redis, at least in terms of cluster routing. To add support for custom commands defined in Redis modules, you can regenerate cmddef.h using the script gencommands.py. Use the JSON files from Redis and any additional files on the same format as arguments to the script. For details, see the comments inside gencommands.py.

Alternative build using Makefile directly

When a simpler build setup is preferred a provided Makefile can be used directly when building. A benefit of this, instead of using CMake, is that it also provides a static library, a similar limitation exists in the CMake files in hiredis v1.0.0.

The only option that exists in the Makefile is to enable SSL/TLS support via USE_SSL=1

By default the hiredis library (and headers) installed on the system is used, but alternative installations can be used by defining the compiler flags CFLAGS and LDFLAGS.

See examples/using_make/build.sh for an example build using an alternative hiredis installation.

Build failures like hircluster_ssl.h:33:10: fatal error: hiredis/hiredis_ssl.h: No such file or directory indicates that hiredis is not installed on the system, or that a given CFLAGS is wrong. Use the previous mentioned build example as reference.

Running the tests

Prerequisites:

Some tests needs a Redis cluster and that can be setup by the make targets start/stop. The clusters will be setup using Docker and it may take a while for them to be ready and accepting requests. Run make start to start the clusters and then wait a few seconds before running make test. To stop the running cluster containers run make stop.

$ make start
$ make test
$ make stop

If you want to set up the Redis clusters manually they should run on localhost using following access ports:

Cluster type Access port
IPv4 7000
IPv4, authentication needed, password: secretword 7100
IPv6 7200
IPv4, using TLS/SSL 7300

Quick usage

Cluster synchronous API

Connecting

The function redisClusterContextInit is used to create a redisClusterContext. The context is where the state for connections is kept.

The function redisClusterSetOptionAddNodes is used to add one or many Redis Cluster addresses.

The functions redisClusterSetOptionUsername and redisClusterSetOptionPassword are used to configure authentication, causing the AUTH command to be sent on every new connection to Redis.

For more options, see the file hircluster.h.

The function redisClusterConnect2 is used to connect to the Redis Cluster.

The redisClusterContext struct has an integer err field that is non-zero when the connection is in an error state. The field errstr will contain a string with a description of the error. After trying to connect to Redis using redisClusterContext you should check the err field to see if establishing the connection was successful:

redisClusterContext *cc = redisClusterContextInit();
redisClusterSetOptionAddNodes(cc, "127.0.0.1:6379,127.0.0.1:6380");
redisClusterConnect2(cc);
if (cc != NULL && cc->err) {
    printf("Error: %s\n", cc->errstr);
    // handle error
}

Events per cluster context

There is a hook to get notified when certain events occur.

int redisClusterSetEventCallback(redisClusterContext *cc,
                                 void(fn)(const redisClusterContext *cc, int event,
                                          void *privdata),
                                 void *privdata);

The callback is called with event set to one of the following values:

  • HIRCLUSTER_EVENT_SLOTMAP_UPDATED when the slot mapping has been updated;
  • HIRCLUSTER_EVENT_READY when the slot mapping has been fetched for the first time and the client is ready to accept commands, useful when initiating the client with redisClusterAsyncConnect2() where a client is not immediately ready after a successful call;
  • HIRCLUSTER_EVENT_FREE_CONTEXT when the cluster context is being freed, so that the user can free the event privdata.

Events per connection

There is a hook to get notified about connect and reconnect attempts. This is useful for applying socket options or access endpoint information for a connection to a particular node. The callback is registered using the following function:

int redisClusterSetConnectCallback(redisClusterContext *cc,
                                   void(fn)(const redisContext *c, int status));

The callback is called just after connect, before TLS handshake and Redis authentication.

On successful connection, status is set to REDIS_OK and the redisContext (defined in hiredis.h) can be used, for example, to see which IP and port it's connected to or to set socket options directly on the file descriptor which can be accessed as c->fd.

On failed connection attempt, this callback is called with status set to REDIS_ERR. The err field in the redisContext can be used to find out the cause of the error.

Sending commands

The function redisClusterCommand takes a format similar to printf. In the simplest form it is used like:

reply = redisClusterCommand(clustercontext, "SET foo bar");

The specifier %s interpolates a string in the command, and uses strlen to determine the length of the string:

reply = redisClusterCommand(clustercontext, "SET foo %s", value);

Internally, hiredis-cluster splits the command in different arguments and will convert it to the protocol used to communicate with Redis. One or more spaces separates arguments, so you can use the specifiers anywhere in an argument:

reply = redisClusterCommand(clustercontext, "SET key:%s %s", myid, value);

Commands will be sent to the cluster node that the client perceives handling the given key. If the cluster topology has changed the Redis node might respond with a redirection error which the client will handle, update its slotmap and resend the command to correct node. The reply will in this case arrive from the correct node.

If a node is unreachable, for example if the command times out or if the connect times out, it can indicated that there has been a failover and the node is no longer part of the cluster. In this case, redisClusterCommand returns NULL and sets err and errstr on the cluster context, but additionally, hiredis cluster schedules a slotmap update to be performed when the next command is sent. That means that if you try the same command again, there is a good chance the command will be sent to another node and the command may succeed.

Sending multi-key commands

Hiredis-cluster supports mget/mset/del multi-key commands. The command will be splitted per slot and sent to correct Redis nodes.

Example:

reply = redisClusterCommand(clustercontext, "mget %s %s %s %s", key1, key2, key3, key4);

Sending commands to a specific node

When there is a need to send commands to a specific node, the following low-level API can be used.

reply = redisClusterCommandToNode(clustercontext, node, "DBSIZE");

This function handles printf like arguments similar to redisClusterCommand(), but will only attempt to send the command to the given node and will not perform redirects or retries.

If the command times out or the connection to the node fails, a slotmap update is scheduled to be performed when the next command is sent. redisClusterCommandToNode also performs a slotmap update if it has previously been scheduled.

Teardown

To disconnect and free the context the following function can be used:

void redisClusterFree(redisClusterContext *cc);

This function closes the sockets and deallocates the context.

Cluster pipelining

The function redisClusterGetReply is exported as part of the Hiredis API and can be used when a reply is expected on the socket. To pipeline commands, the only things that needs to be done is filling up the output buffer. For this cause, the following commands can be used that are identical to the redisClusterCommand family, apart from not returning a reply:

int redisClusterAppendCommand(redisClusterContext *cc, const char *format, ...);
int redisClusterAppendCommandArgv(redisClusterContext *cc, int argc, const char **argv);

/* Send a command to a specific cluster node */
int redisClusterAppendCommandToNode(redisClusterContext *cc, redisClusterNode *node,
                                    const char *format, ...);

After calling either function one or more times, redisClusterGetReply can be used to receive the subsequent replies. The return value for this function is either REDIS_OK or REDIS_ERR, where the latter means an error occurred while reading a reply. Just as with the other commands, the err field in the context can be used to find out what the cause of this error is.

void redisClusterReset(redisClusterContext *cc);

Warning: You must call redisClusterReset function after one pipelining anyway.

Warning: Calling redisClusterReset without pipelining first will reset all Redis connections.

The following examples shows a simple cluster pipeline:

redisReply *reply;
redisClusterAppendCommand(clusterContext,"SET foo bar");
redisClusterAppendCommand(clusterContext,"GET foo");
redisClusterGetReply(clusterContext,&reply); // reply for SET
freeReplyObject(reply);
redisClusterGetReply(clusterContext,&reply); // reply for GET
freeReplyObject(reply);
redisClusterReset(clusterContext);

Cluster asynchronous API

Hiredis-cluster comes with an asynchronous cluster API that works with many event systems. Currently there are adapters that enables support for libevent, libev, libuv, glib and Redis Event Library (ae). For usage examples, see the test programs with the different event libraries tests/ct_async_{libev,libuv,glib}.c.

The hiredis library has adapters for additional event systems that easily can be adapted for hiredis-cluster as well.

Connecting

There are two alternative ways to initiate a cluster client which also determines how the client behaves during the initial connect.

The first alternative is to use the function redisClusterAsyncConnect, which initially connects to the cluster in a blocking fashion and waits for the slotmap before returning. Any command sent by the user thereafter will create a new non-blocking connection, unless a non-blocking connection already exists to the destination. The function returns a pointer to a newly created redisClusterAsyncContext struct and its err field should be checked to make sure the initial slotmap update was successful.

// Insufficient error handling for brevity.
redisClusterAsyncContext *acc = redisClusterAsyncConnect("127.0.0.1:6379", HIRCLUSTER_FLAG_NULL);
if (acc->err) {
    printf("error: %s\n", acc->errstr);
    exit(1);
}

// Attach an event engine. In this example we use libevent.
struct event_base *base = event_base_new();
redisClusterLibeventAttach(acc, base);

The second alternative is to use redisClusterAsyncContextInit and redisClusterAsyncConnect2 which avoids the initial blocking connect. This connection alternative requires an attached event engine when redisClusterAsyncConnect2 is called, but the connect and the initial slotmap update is done in a non-blocking fashion.

This means that commands sent directly after redisClusterAsyncConnect2 may fail because the initial slotmap has not yet been retrieved and the client doesn't know which cluster node to send the command to. You may use the eventCallback to be notified when the slotmap is updated and the client is ready to accept commands. An crude example of using the eventCallback can be found in this testcase.

// Insufficient error handling for brevity.
redisClusterAsyncContext *acc = redisClusterAsyncContextInit();

// Add a cluster node address for the initial connect.
redisClusterSetOptionAddNodes(acc->cc, "127.0.0.1:6379");

// Attach an event engine. In this example we use libevent.
struct event_base *base = event_base_new();
redisClusterLibeventAttach(acc, base);

if (redisClusterAsyncConnect2(acc) != REDIS_OK) {
    printf("error: %s\n", acc->errstr);
    exit(1);
}

Events per cluster context

Use redisClusterSetEventCallback with acc->cc as the context to get notified when certain events occur.

Events per connection

Because the connections that will be created are non-blocking, the kernel is not able to instantly return if the specified host and port is able to accept a connection. Instead, use a connect callback to be notified when a connection is established or failed. Similarily, a disconnect callback can be used to be notified about a disconnected connection (either because of an error or per user request). The callbacks are installed using the following functions:

int redisClusterAsyncSetConnectCallback(redisClusterAsyncContext *acc,
                                        redisConnectCallback *fn);
int redisClusterAsyncSetDisonnectCallback(redisClusterAsyncContext *acc,
                                          redisConnectCallback *fn);

The callback functions should have the following prototype, aliased to redisConnectCallback:

void(const redisAsyncContext *ac, int status);

Alternatively, if hiredis >= v1.1.0 is used, you set a connect callback that will be passed a non-const redisAsyncContext* on invocation (e.g. to be able to set a push callback on it).

int redisClusterAsyncSetConnectCallbackNC(redisClusterAsyncContext *acc,
                                          redisConnectCallbackNC *fn);

The callback function should have the following prototype, aliased to redisConnectCallbackNC:

void(redisAsyncContext *ac, int status);

On a connection attempt, the status argument is set to REDIS_OK when the connection was successful. The file description of the connection socket can be retrieved from a redisAsyncContext as ac->c->fd. On a disconnect, the status argument is set to REDIS_OK when disconnection was initiated by the user, or REDIS_ERR when the disconnection was caused by an error. When it is REDIS_ERR, the err field in the context can be accessed to find out the cause of the error.

You don't need to reconnect in the disconnect callback. Hiredis-cluster will reconnect by itself when the next command for this Redis node is handled.

Setting the connect and disconnect callbacks can only be done once per context. For subsequent calls it will return REDIS_ERR.

Sending commands and their callbacks

In an asynchronous cluster context, commands are automatically pipelined due to the nature of an event loop. Therefore, unlike the synchronous API, there is only a single way to send commands. Because commands are sent to Redis Cluster asynchronously, issuing a command requires a callback function that is called when the reply is received. Reply callbacks should have the following prototype:

void(redisClusterAsyncContext *acc, void *reply, void *privdata);

The privdata argument can be used to carry arbitrary data to the callback from the point where the command is initially queued for execution.

The most commonly used functions to issue commands in an asynchronous context are:

int redisClusterAsyncCommand(redisClusterAsyncContext *acc,
                             redisClusterCallbackFn *fn,
                             void *privdata, const char *format, ...);
int redisClusterAsyncCommandArgv(redisClusterAsyncContext *acc,
                                 redisClusterCallbackFn *fn, void *privdata,
                                 int argc, const char **argv,
                                 const size_t *argvlen);
int redisClusterAsyncFormattedCommand(redisClusterAsyncContext *acc,
                                      redisClusterCallbackFn *fn,
                                      void *privdata, char *cmd, int len);

These functions works like their blocking counterparts. The return value is REDIS_OK when the command was successfully added to the output buffer and REDIS_ERR otherwise. When the connection is being disconnected per user-request, no new commands may be added to the output buffer and REDIS_ERR is returned.

If the reply for a command with a NULL callback is read, it is immediately freed. When the callback for a command is non-NULL, the memory is freed immediately following the callback: the reply is only valid for the duration of the callback.

All pending callbacks are called with a NULL reply when the context encountered an error.

Sending commands to a specific node

When there is a need to send commands to a specific node, the following low-level API can be used.

int redisClusterAsyncCommandToNode(redisClusterAsyncContext *acc,
                                   redisClusterNode *node,
                                   redisClusterCallbackFn *fn, void *privdata,
                                   const char *format, ...);
int redisClusterAsyncCommandArgvToNode(redisClusterAsyncContext *acc,
                                       redisClusterNode *node,
                                       redisClusterCallbackFn *fn,
                                       void *privdata, int argc,
                                       const char **argv,
                                       const size_t *argvlen);
int redisClusterAsyncFormattedCommandToNode(redisClusterAsyncContext *acc,
                                            redisClusterNode *node,
                                            redisClusterCallbackFn *fn,
                                            void *privdata, char *cmd, int len);

These functions will only attempt to send the command to a specific node and will not perform redirects or retries, but communication errors will trigger a slotmap update just like the commonly used API.

Disconnecting

Asynchronous cluster connections can be terminated using:

void redisClusterAsyncDisconnect(redisClusterAsyncContext *acc);

When this function is called, connections are not immediately terminated. Instead, new commands are no longer accepted and connections are only terminated when all pending commands have been written to a socket, their respective replies have been read and their respective callbacks have been executed. After this, the disconnection callback is executed with the REDIS_OK status and the context object is freed.

Using event library X

There are a few hooks that need to be set on the cluster context object after it is created. See the adapters/ directory for bindings to libevent and a range of other event libraries.

Other details

Cluster node iterator

A redisClusterNodeIterator can be used to iterate on all known master nodes in a cluster context. First it needs to be initiated using redisClusterInitNodeIterator() and then you can repeatedly call redisClusterNodeNext() to get the next node from the iterator.

void redisClusterInitNodeIterator(redisClusterNodeIterator *iter,
                                  redisClusterContext *cc);
redisClusterNode *redisClusterNodeNext(redisClusterNodeIterator *iter);

The iterator will handle changes due to slotmap updates by restarting the iteration, but on the new set of master nodes. There is no bookkeeping for already iterated nodes when a restart is triggered, which means that a node can be iterated over more than once depending on when the slotmap update happened and the change of cluster nodes.

Note that when redisClusterCommandToNode is called, a slotmap update can happen if it has been scheduled by the previous command, for example if the previous call to redisClusterCommandToNode timed out or the node wasn't reachable.

To detect when the slotmap has been updated, you can check if the iterator's slotmap version (iter.route_version) is equal to the current cluster context's slotmap version (cc->route_version). If it isn't, it means that the slotmap has been updated and the iterator will restart itself at the next call to redisClusterNodeNext.

Another way to detect that the slotmap has been updated is to register an event callback and look for the event HIRCLUSTER_EVENT_SLOTMAP_UPDATED.

Random number generator

This library uses random() while selecting a node used for requesting the cluster topology (slotmap). A user should seed the random number generator using srandom() to get less predictability in the node selection.

Allocator injection

Hiredis-cluster uses hiredis allocation structure with configurable allocation and deallocation functions. By default they just point to libc (malloc, calloc, realloc, etc).

Overriding

If you have your own allocator or if you expect an abort in out-of-memory cases, you can configure the used functions in the following way:

hiredisAllocFuncs myfuncs = {
    .mallocFn = my_malloc,
    .callocFn = my_calloc,
    .reallocFn = my_realloc,
    .strdupFn = my_strdup,
    .freeFn = my_free,
};

// Override allocators (function returns current allocators if needed)
hiredisAllocFuncs orig = hiredisSetAllocators(&myfuncs);

To reset the allocators to their default libc functions simply call:

hiredisResetAllocators();