home

Writing a Custom Redis Command In C - Part 1

Dec 20, 2012

Not too long ago, we found ourselves in need of a zdiff command in Redis. We could have implemented it in Lua, like much of our other code, but why not do it straight in Redis? One of the great things about Redis is how clean the codebase is; even for a very rusty C programmer like myself.

The first thing you'll want to do is define your command. At the bottom of redis.h add the function signature:

void xdiffCommand(redisClient *c);

In redis.c, we'll register our function. Near the top, you'll find a well-documented redisCommandTable. First we'll register our command, then we'll explain each part. So, at the bottom of this variable, add a new entry:

{"xdiff",xdiffCommand,5,"r",0,NULL,1,2,0,0,0},

The first value is the name of the command, followed by the actual function (the one we declared in redis.h). Next we have the number of arguments. We'll take five arguments: the name of the command itself ("xdiff"), a sorted set, a set we want to use to diff, an offset and a count.Next we provide flags that tell Redis about our command - like whether using it might grow memory, whether it's read-only or does writes and so on. The next value, 0, is used internally by Redis (Redis will store a numeric representation of the previous flags value here). The next four values, starting from NULL and ending with 0, are all about keys. We'll get back to the NULL, but the 1 and 2 tell Redis that the first key that we'll specify is the 1st parameter, and the last one is the 2nd parameter. The 0 is the step Redis should use to get the keys. For example, if we wanted a command that alternate between key1 value1 key2 value2 key3 value3, we'd use 1,5,1 - the step of 1 meaning to skip every other value. The last two 0s are used internally by Redis (they track statistics about your command). Back to the NULL, some commands make use of keys in a way that cannot be expressed by the simple start,stop,step pattern. For such commands, we specify a custom function that'll take care of identifying the keys of a specific call.

In case you aren't clear on what xdiff will do, we'll take a sorted set, and find the first count values that aren't also in a set, starting at a given offset. For example, we could use it to do something like:

zadd duncan:friends 101 murbella 100 leto 95 paul 85 teg 85 gurney 80 chaini 70 thufir 60 leto2

sadd family:atreides ghanima paul jessica leto leto2

xdiff duncan:friends family:atreides 1 2

#skips murbella since the offset is 1, and not 0
output: teg gurney

We'll add a new file, custom.c to the src folder (best to keep our code as separate as possible to make it easy to pull in changes from the main project). Next, in src/Makefile find REDIS_SERVER_OBJ and add custom.o at the end of the line. Finally, open our newly created file, custom.c, and add the following:

#include "redis.h"

void xdiffCommand(redisClient *c) {

}

You should now be able to compile Redis (via make).

The redisClient structure represents the context being executed - web programmers might think of it as both the request and the response. We won't deal with it directly; we will pass it to a number of existing functions though. For example, the first thing we'll want to do is add some validation to make sure all of our inputs are ok (since we told Redis that we expected 5 arguments, it'll take care of at least that basic amount of validation for us).

Starting simply, we can use the getLongFromObjectOrReply function to get both the offset and count arguments:

long offset, count;

if ((getLongFromObjectOrReply(c, c->argv[3], &offset, NULL) != REDIS_OK)) { return; }
if ((getLongFromObjectOrReply(c, c->argv[4], &count, NULL) != REDIS_OK)) { return; }

These two lines tell Redis to load the 3rd and 4th arguments into our offset and count variables as longs. If this fails, Redis will reply with an error message (or we can specify a custom error message as the 4th parameter). We could default our variables (say to 0 and 10), but let's simply exit our function instead.

We also want to load our sorted set and set (argv[1] and argv[2]) and want to make sure that they are of the correct types:

robj *zobj, *sobj;

  zobj = lookupKeyReadOrReply(c, c->argv[1], shared.czero);
  if (zobj == NULL || checkType(c, zobj, REDIS_ZSET)) { return; }

  sobj = lookupKeyReadOrReply(c, c->argv[2], shared.czero);
  if (sobj == NULL || checkType(c, sobj, REDIS_SET)) { return; }

Not too different than before. We define two redis objects, zobj and sobj and try to load them from the given arguments. If they are either null or of the wrong type, we exit. (This is probably the wrong behavior with respect to our second argument, the set. If the set is not present (null), the expected behavior would probably be to apply our paging (offset/count) to the entire zset without doing any diff).

The redisObjects (robj) is a fundamental building block. It wraps the underlying data structure (accessible via the void *ptr member) and provides a type, which checkType relies on, a reference count (int refcount) and and lru value (for expiration, I imagine), along with a couple other members (one of which we'll explore in detail in our next part).

The last bit, for today, is just to reply with a dummy answer when all our validation passes:

#include "redis.h"

void xdiffCommand(redisClient *c) {
  long offset, count;
  robj *zobj, *sobj;

  if ((getLongFromObjectOrReply(c, c->argv[3], &offset, NULL) != REDIS_OK)) { return; }
  if ((getLongFromObjectOrReply(c, c->argv[4], &count, NULL) != REDIS_OK)) { return; }

  zobj = lookupKeyReadOrReply(c, c->argv[1], shared.czero);
  if (zobj == NULL || checkType(c, zobj, REDIS_ZSET)) { return; }

  sobj = lookupKeyReadOrReply(c, c->argv[2], shared.czero);
  if (sobj == NULL || checkType(c, sobj, REDIS_SET)) { return; }

  addReplyLongLong(c, 9001);
}

You can go ahead and build the code. Next, run redis (./src/redis-server) and connect to it via the redis-cli client. You can try various arguments with xfind to make sure it's all working properly (don't worry, we'll add proper tests at some point).

In the next part, we'll build the core of xdiff.