Redis module から HSCAN を呼んで配列を返す

Redis コマンド HSCAN を使う Redis module を作ります。高レベルAPIを呼ぶRedis moduleのサンプルです。

環境

ファイル構成

  • redismodule.h
  • helloscan.c

redismodule.h は RedisModulesSDK/redismodule.h at master · RedisLabs/RedisModulesSDK · GitHub からダウンロードする。

ソースコード

hellohscan.c:

// gcc -O2 -shared -fPIC hellohscan.c -o hellohscan.so

#include "redismodule.h"
#include <stdlib.h>
#include <string.h>

int HelloHscan_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
  // Check arg count
  if (argc != 2) return RedisModule_WrongArity(ctx);

  RedisModuleString* keyStr = argv[1];

  RedisModule_Log(ctx, "notice", "Before call()");
  RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN);
  RedisModule_ReplyWithStringBuffer(ctx, "[start]", strlen("[start]"));
  RedisModuleCallReply *reply = RedisModule_Call(ctx, "HSCAN", "sc", keyStr, "0");
  if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_ARRAY) {
    RedisModule_Log(ctx, "warning", "not array");
    return REDISMODULE_ERR;
  }
  RedisModule_Log(ctx, "notice", "After call()");
  size_t length0 = RedisModule_CallReplyLength(reply);
  if (length0 != 2) {
    RedisModule_Log(ctx, "warning", "length0 is NOT 2");
    return REDISMODULE_ERR;
  }
  RedisModuleCallReply *r0 = RedisModule_CallReplyArrayElement(reply, 0);
  if (RedisModule_CallReplyType(r0) == REDISMODULE_REPLY_STRING) {
    size_t len = 0;
    const char *s = RedisModule_CallReplyStringPtr(r0, &len);
    RedisModule_ReplyWithStringBuffer(ctx, s, len);
  } else {
    RedisModule_ReplyWithStringBuffer(ctx, "ERR", strlen("ERR"));
  }
  RedisModuleCallReply *r1 = RedisModule_CallReplyArrayElement(reply, 1);
  size_t length = RedisModule_CallReplyLength(r1);

  for (size_t i = 0; i < length; i++) {
    RedisModuleCallReply *r = RedisModule_CallReplyArrayElement(r1, i);
    if (RedisModule_CallReplyType(r) == REDISMODULE_REPLY_STRING) {
      size_t len = 0;
      const char *s = RedisModule_CallReplyStringPtr(r, &len);
      RedisModule_ReplyWithStringBuffer(ctx, s, len);
    } else {
      RedisModule_ReplyWithStringBuffer(ctx, "[non str]", strlen("[non str]"));
    }
  }
  RedisModule_FreeCallReply(reply);
  RedisModule_Log(ctx, "notice", "NOTICE!");
  RedisModule_ReplyWithLongLong(ctx, length);
  RedisModule_ReplyWithStringBuffer(ctx, "[end]", strlen("[end]"));
  RedisModule_ReplySetArrayLength(ctx, length + 4);
  return REDISMODULE_OK;
}

int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
  if (RedisModule_Init(ctx, "hellohscan", 1, REDISMODULE_APIVER_1)
    == REDISMODULE_ERR) {
    return REDISMODULE_ERR;
  }

  if (RedisModule_CreateCommand(ctx,"hellohscan",
    HelloHscan_RedisCommand, "readonly", 1, 1, 1) == REDISMODULE_ERR) {
    return REDISMODULE_ERR;
  }
  return REDISMODULE_OK;
}

実行結果

$ redis-cli
127.0.0.1:6379> module load /opt/hellohscan.so
OK
127.0.0.1:6379> hscan hello 0
1) "0"
2) (empty list or set)
127.0.0.1:6379> hset hello a a1
(integer) 1
127.0.0.1:6379> hset hello b b1
(integer) 1
127.0.0.1:6379> hscan hello 0
1) "0"
2) 1) "a"
   2) "a1"
   3) "b"
   4) "b1"
127.0.0.1:6379> hellohscan hello
1) "[start]"
2) "0"
3) "a"
4) "a1"
5) "b"
6) "b1"
7) (integer) 4
8) "[end]"
127.0.0.1:6379>

解説

int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
  if (RedisModule_Init(ctx, "hellohscan", 1, REDISMODULE_APIVER_1)
    == REDISMODULE_ERR) {
    return REDISMODULE_ERR;
  }
  • RedisModule_OnLoad(): Redis から module load 時に呼ばれる関数。モジュール開発者が実装する
  • RedisModule_Init(): 自身のモジュール名、利用する Redis module API version、自身のバージョンを設定する
    • ここで指定したモジュール名が module listmodule unload で使われる
  • RedisModule_Init は エラー時 REDISMODULE_ERR (1) 、正常時は REDISMODULE_OK (0) が返ってくる
  • 同じように、 RedisModule_OnLoad() ではエラー時 REDISMODULE_ERR (1) 、正常時は REDISMODULE_OK (0) を返す
  if (RedisModule_CreateCommand(ctx,"hellohscan",
    HelloHscan_RedisCommand, "readonly", 1, 1, 1) == REDISMODULE_ERR) {
    return REDISMODULE_ERR;
  }
  return REDISMODULE_OK;
  • RedisModule_CreateCommand() で、コマンドを登録する
  • "hellohscan": 登録したいコマンド名の文字列
  • HelloHscan_RedisCommand: このファイル中で定義している、コマンド処理を書いた関数
  • "readonly": Redisのデータを書き換えないコマンドの場合は readonly を指定する
  • 問題が無ければ RedisModule_Onload() から REDISMODULE_OK を返す
int HelloHscan_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {

コマンド実装の本体。

if (argc != 2) return RedisModule_WrongArity(ctx);

引数の数が期待通りでない場合は RedisModule_WrongArity() を呼ぶ。(Redis client に引数の数が違うというエラーが伝わる)

RedisModuleString* keyStr = argv[1];

argv[0] にはコマンド名が、argv[1] 以降にはコマンドに対するパラメータが入っている。今回は field を指定しているという想定。 argvなど、RedisModule が管理する文字列は RedisModuleString が使われる。 argvについてはメモリ解放のために RedisModuleString_Free() を呼ばなくてもいい。

  RedisModule_Log(ctx, "notice", "Before call()");

Redisログファイル (例: /var/log/redis/redis-server.log ) にログを出力する。 第二引数 levelStrには "debug", "verbose", "notice", "warning" を指定可能。デフォルトで "notice" と "warning" はログ出力された。

  RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN);
  RedisModule_ReplyWithStringBuffer(ctx, "[start]", strlen("[start]"));
...
  RedisModule_ReplySetArrayLength(ctx, length + 4);

ReplyWithArray はコマンドの戻りが配列の時に指定する。第2引数に要素数を指定するが、要素数が決められない時は REDISMODULE_POSTPONED_ARRAY_LEN を指定し、 すべての要素を設定し終わってから ReplySetArrayLength で要素数を指定する。 ReplyWithArray の後で ReplyWithStringBuffer を呼び出すと、配列の要素の先頭から指定していることになる。 ReplyWithStringBuffer は文字列バイナリの先頭ポインタと長さで値を設定する。

  RedisModuleCallReply *reply = RedisModule_Call(ctx, "HSCAN", "sc", keyStr, "0");
...
  RedisModule_FreeCallReply(reply);

RedisModule_Call() で高レベルAPI呼び出し (Call) を行っている。Callの戻り値の型は RedisModuleCallReply* となる。 第2引数がC文字列のコマンド名 (ここでは"HSCAN")、第3引数で、第4引数以降の型を指定する。 ”sc” だと、第4引数が s: RedisModuleString* 、第5引数が c: C文字列 (NULL終端文字列) という意味になる。 結果が不要になったら最後に FreeCallReply() を呼ぶ。 戻り値の内容はコマンドによって違い、CallReplyType() でRedisとしての型を取得できる。 HSCANの場合、要素数2の配列であり、リストの続きを表す文字列が第1要素と、実データの配列が第2要素に格納される。

配列の場合は CallReplyLength() で長さを取得でき、 CallReplyArrayElement() で配列の要素を取り出す。

インデックス 内容
0 カーソル REDISMODULE_REPLY_STRING
1 内容の配列 REDISMODULE_REPLY_ARRAY

カーソルが "0" の場合、最後まで走査できたことを示す。 "0" 以外の場合、HSCANの次回呼び出し時にカーソルを指定すると続きを取得できる。

  RedisModuleCallReply *r1 = RedisModule_CallReplyArrayElement(reply, 1);
  size_t length = RedisModule_CallReplyLength(r1);

  for (size_t i = 0; i < length; i++) {
    RedisModuleCallReply *r = RedisModule_CallReplyArrayElement(r1, i);
    if (RedisModule_CallReplyType(r) == REDISMODULE_REPLY_STRING) {
      size_t len = 0;
      const char *s = RedisModule_CallReplyStringPtr(r, &len);
      RedisModule_ReplyWithStringBuffer(ctx, s, len);

callReplyからの文字列の取得は const char *s = RedisModule_CallReplyStringPtr(r0, &len); で行う。 第2引数に与えたsize_t 変数に長さが格納され、文字列の先頭ポインタが返ってくる。Null終端されていないので そのままC文字列としては扱えない。

ここでは RedisModule_ReplyWithStringBuffer(ctx, s, len); として、取得した文字列をそのままコマンドの返却文字列として設定している。

ビルド

gcc -O2 -shared -fPIC hellohscan.c -o hellohscan.so

gccはインストール済みであること。 helloscan.so を適当な場所において redis から読み取らせる。 ( /tmp 直下は Redis から読み取れなかったので /opt に配置した )

実行

$ redis-cli
127.0.0.1:6379> module load /opt/hellohscan.so
...
127.0.0.1:6379> hellohscan hello
...
127.0.0.1:6379> module unload hellohscan

redis-cli を実行、 module load (helloscan.so のパス) でモジュールをロードする。 RedisModule_CreateCommand で定義したコマンド名・引数で実行する。 module unload (モジュール名) でアンロードする。

HSCANを内部で実行するRedis moduleを作ることができた。