├── .gitignore ├── LICENSE ├── README.md ├── project.janet ├── redis.c ├── redis.janet └── test └── redis.janet /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 andrewchambers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # janet-redis 2 | A janet redis library built with the official hiredis C library. 3 | 4 | Quick Example: 5 | ``` 6 | (import redis) 7 | 8 | (def r (redis/connect "localhost" 1337)) 9 | 10 | # Simple commands 11 | (redis/command r "SET" "FOOBAR" "BAZ") 12 | (redis/command r "GET" "FOOBAR") 13 | # "BAZ" 14 | 15 | # Command pipelining 16 | (redis/append r "PING") 17 | (redis/append r "PING") 18 | (redis/get-reply r) 19 | # "PONG" 20 | (redis/get-reply r) 21 | # "PONG" 22 | 23 | (redis/close r) 24 | ``` -------------------------------------------------------------------------------- /project.janet: -------------------------------------------------------------------------------- 1 | (declare-project 2 | :name "redis" 3 | :description "A Janet Redis library built with the official hiredis C library." 4 | :author "Andrew Chambers" 5 | :license "MIT" 6 | :url "https://github.com/andrewchambers/janet-redis" 7 | :repo "git+https://github.com/andrewchambers/janet-redis.git") 8 | 9 | (defn exec-slurp 10 | "Read stdout of subprocess and return it trimmed in a string." 11 | [& args] 12 | (def proc (os/spawn args :px {:out :pipe})) 13 | (def out (get proc :out)) 14 | (def buf @"") 15 | (ev/gather 16 | (:read out :all buf) 17 | (:wait proc)) 18 | (string/trimr buf)) 19 | 20 | (defn pkg-config [& what] 21 | (try 22 | (string/split " " (exec-slurp "pkg-config" ;what)) 23 | ([err] (error "pkg-config failed!")))) 24 | 25 | (declare-source 26 | :source ["redis.janet"]) 27 | 28 | (declare-native 29 | :name "_janet_redis" 30 | :cflags (pkg-config "hiredis" "--cflags") 31 | :lflags (pkg-config "hiredis" "--libs") 32 | :source ["redis.c"]) 33 | -------------------------------------------------------------------------------- /redis.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | typedef struct { 6 | redisContext *ctx; 7 | } Context; 8 | 9 | static void context_close(Context *ctx) { 10 | if (ctx->ctx) { 11 | redisFree(ctx->ctx); 12 | ctx->ctx = NULL; 13 | } 14 | } 15 | 16 | static int context_gc(void *p, size_t s) { 17 | (void)s; 18 | Context *ctx = (Context *)p; 19 | context_close(ctx); 20 | return 0; 21 | } 22 | 23 | static Janet jredis_close(int32_t argc, Janet *argv); 24 | 25 | static JanetMethod context_methods[] = { 26 | {"close", jredis_close}, /* So contexts can be used with 'with' */ 27 | {NULL, NULL}}; 28 | 29 | static int context_get(void *ptr, Janet key, Janet *out) { 30 | Context *p = (Context *)ptr; 31 | return janet_getmethod(janet_unwrap_keyword(key), context_methods, out); 32 | } 33 | 34 | static const JanetAbstractType redis_context_type = {"redis.context", 35 | context_gc, 36 | NULL, 37 | context_get, 38 | NULL, 39 | NULL, 40 | NULL, 41 | NULL, 42 | NULL, 43 | NULL}; 44 | 45 | static Janet jredis_connect(int32_t argc, Janet *argv) { 46 | janet_arity(argc, 1, 2); 47 | const char *u = janet_getcstring(argv, 0); 48 | 49 | int32_t port = 6379; 50 | 51 | if (argc >= 2) { 52 | port = janet_getinteger(argv, 1); 53 | } 54 | 55 | Context *ctx = 56 | (Context *)janet_abstract(&redis_context_type, sizeof(Context)); 57 | 58 | ctx->ctx = redisConnect(u, port); 59 | if (!ctx->ctx) 60 | janet_panic("redis connection failed"); 61 | 62 | if (ctx->ctx->err) 63 | janet_panicf("error connecting to redis server: %s", ctx->ctx->errstr); 64 | 65 | return janet_wrap_abstract(ctx); 66 | } 67 | 68 | static Janet jredis_connect_unix(int32_t argc, Janet *argv) { 69 | janet_fixarity(argc, 1); 70 | const char *u = janet_getcstring(argv, 0); 71 | 72 | Context *ctx = 73 | (Context *)janet_abstract(&redis_context_type, sizeof(Context)); 74 | 75 | ctx->ctx = redisConnectUnix(u); 76 | if (!ctx->ctx) 77 | janet_panic("redis connection failed"); 78 | 79 | if (ctx->ctx->err) 80 | janet_panicf("error connecting to redis server: %s", ctx->ctx->errstr); 81 | 82 | return janet_wrap_abstract(ctx); 83 | } 84 | 85 | static Janet jredis_reconnect(int32_t argc, Janet *argv) { 86 | janet_fixarity(argc, 1); 87 | Context *ctx = (Context *)janet_getabstract(argv, 0, &redis_context_type); 88 | 89 | if (!ctx->ctx) 90 | janet_panic("this connection was closed, unable to reconnect"); 91 | 92 | if (redisReconnect(ctx->ctx) != REDIS_OK) 93 | janet_panicf("error reconnecting to redis server: %s", ctx->ctx->errstr); 94 | 95 | return janet_wrap_nil(); 96 | } 97 | 98 | static Janet jredis_close(int32_t argc, Janet *argv) { 99 | janet_fixarity(argc, 1); 100 | Context *ctx = (Context *)janet_getabstract(argv, 0, &redis_context_type); 101 | context_close(ctx); 102 | return janet_wrap_nil(); 103 | } 104 | 105 | static void __ensure_ctx_ok(Context *ctx) { 106 | if (ctx->ctx == NULL) 107 | janet_panic("redis context is disconnected"); 108 | if (ctx->ctx->err) 109 | janet_panicf("context has previously encountered an error: '%s'", 110 | ctx->ctx->errstr); 111 | } 112 | 113 | typedef enum { 114 | DO_ACTION_SEND, 115 | DO_ACTION_APPEND, 116 | } do_action; 117 | 118 | static void *__do_x_command(do_action action, Context *ctx, int32_t argc, 119 | Janet *argv) { 120 | #define N_FAST_PATH 8 121 | 122 | const char *args[N_FAST_PATH]; 123 | size_t argl[N_FAST_PATH]; 124 | 125 | const char **args_p; 126 | size_t *argl_p; 127 | 128 | if (argc <= N_FAST_PATH) { 129 | args_p = &args[0]; 130 | argl_p = &argl[0]; 131 | } else { 132 | args_p = janet_smalloc(sizeof(char *) * argc); 133 | argl_p = janet_smalloc(sizeof(size_t) * argc); 134 | } 135 | 136 | for (int i = 0; i < argc; i++) { 137 | JanetByteView bv = janet_getbytes(argv, i); 138 | args_p[i] = bv.bytes; 139 | argl_p[i] = bv.len; 140 | } 141 | 142 | int had_error = 0; 143 | void *reply = NULL; 144 | 145 | switch (action) { 146 | case DO_ACTION_SEND: 147 | reply = redisCommandArgv(ctx->ctx, argc, args_p, argl_p); 148 | if (!reply) 149 | had_error = 1; 150 | break; 151 | case DO_ACTION_APPEND: 152 | if (redisAppendCommandArgv(ctx->ctx, argc, args_p, argl_p) != REDIS_OK) 153 | had_error = 1; 154 | break; 155 | } 156 | 157 | if (argc > N_FAST_PATH) { 158 | janet_sfree(args_p); 159 | janet_sfree(argl_p); 160 | } 161 | 162 | if (had_error) 163 | janet_panicf("%s", ctx->ctx->errstr); 164 | 165 | return reply; 166 | 167 | #undef N_FAST_PATH 168 | } 169 | 170 | static Janet reply_to_janet(redisReply *reply) { 171 | Janet v; 172 | switch (reply->type) { 173 | case REDIS_REPLY_STATUS: 174 | v = janet_stringv(reply->str, reply->len); 175 | break; 176 | case REDIS_REPLY_ERROR: 177 | v = janet_stringv(reply->str, reply->len); 178 | break; 179 | case REDIS_REPLY_INTEGER: 180 | v = janet_wrap_s64(reply->integer); 181 | break; 182 | case REDIS_REPLY_NIL: 183 | v = janet_wrap_nil(); 184 | break; 185 | case REDIS_REPLY_STRING: 186 | v = janet_stringv(reply->str, reply->len); 187 | break; 188 | case REDIS_REPLY_ARRAY: { 189 | JanetArray *a = janet_array(reply->elements); 190 | for (int i = 0; i < reply->elements; i++) 191 | janet_array_push(a, reply_to_janet(reply->element[i])); 192 | v = janet_wrap_array(a); 193 | break; 194 | } 195 | default: 196 | v = janet_wrap_nil(); 197 | break; 198 | } 199 | return v; 200 | } 201 | 202 | static Janet jredis_command(int32_t argc, Janet *argv) { 203 | if (argc < 1) 204 | janet_panic("expected at least a redis context"); 205 | 206 | Context *ctx = (Context *)janet_getabstract(argv, 0, &redis_context_type); 207 | __ensure_ctx_ok(ctx); 208 | argc--; 209 | argv++; 210 | redisReply *reply = 211 | (redisReply *)__do_x_command(DO_ACTION_SEND, ctx, argc, argv); 212 | 213 | Janet v = reply_to_janet(reply); 214 | int err_occured = reply->type == REDIS_REPLY_ERROR; 215 | freeReplyObject(reply); 216 | if (err_occured) 217 | janet_panicv(v); 218 | 219 | return v; 220 | } 221 | 222 | static Janet jredis_append(int32_t argc, Janet *argv) { 223 | if (argc < 1) 224 | janet_panic("expected at least a redis context"); 225 | 226 | Context *ctx = (Context *)janet_getabstract(argv, 0, &redis_context_type); 227 | __ensure_ctx_ok(ctx); 228 | argc--; 229 | argv++; 230 | __do_x_command(DO_ACTION_APPEND, ctx, argc, argv); 231 | return janet_wrap_nil(); 232 | } 233 | 234 | static Janet jredis_get_reply(int32_t argc, Janet *argv) { 235 | janet_fixarity(argc, 1); 236 | 237 | Context *ctx = (Context *)janet_getabstract(argv, 0, &redis_context_type); 238 | __ensure_ctx_ok(ctx); 239 | 240 | redisReply *reply; 241 | if (redisGetReply(ctx->ctx, (void **)&reply) != REDIS_OK) 242 | janet_panicf("error getting reply: %s", ctx->ctx->errstr); 243 | 244 | Janet v = reply_to_janet(reply); 245 | int err_occured = reply->type == REDIS_REPLY_ERROR; 246 | freeReplyObject(reply); 247 | if (err_occured) 248 | janet_panicv(v); 249 | 250 | return v; 251 | } 252 | 253 | static Janet jredis_error_message(int32_t argc, Janet *argv) { 254 | janet_fixarity(argc, 1); 255 | Context *ctx = (Context *)janet_getabstract(argv, 0, &redis_context_type); 256 | if (!ctx->ctx) 257 | janet_panic("connection closed"); 258 | if (ctx->ctx->err) 259 | return janet_cstringv(ctx->ctx->errstr); 260 | return janet_wrap_nil(); 261 | } 262 | 263 | static Janet jredis_error_code(int32_t argc, Janet *argv) { 264 | janet_fixarity(argc, 1); 265 | Context *ctx = (Context *)janet_getabstract(argv, 0, &redis_context_type); 266 | if (!ctx->ctx) 267 | janet_panic("connection closed"); 268 | switch (ctx->ctx->err) { 269 | case 0: 270 | return janet_wrap_nil(); 271 | case REDIS_ERR_IO: 272 | return janet_ckeywordv("REDIS_ERR_IO"); 273 | case REDIS_ERR_EOF: 274 | return janet_ckeywordv("REDIS_ERR_EOF"); 275 | case REDIS_ERR_PROTOCOL: 276 | return janet_ckeywordv("REDIS_ERR_PROTOCOL"); 277 | case REDIS_ERR_OTHER: 278 | default: 279 | return janet_ckeywordv("REDIS_ERR_OTHER"); 280 | } 281 | } 282 | 283 | static Janet jredis_set_timeout(int32_t argc, Janet *argv) { 284 | janet_arity(argc, 2, 3); 285 | Context *ctx = (Context *)janet_getabstract(argv, 0, &redis_context_type); 286 | __ensure_ctx_ok(ctx); 287 | struct timeval tv; 288 | 289 | tv.tv_sec = janet_getinteger64(argv, 1); 290 | tv.tv_usec = argc == 3 ? janet_getinteger64(argv, 2) : 0; 291 | 292 | if (redisSetTimeout(ctx->ctx, tv) != REDIS_OK) 293 | janet_panic("unable to set timeout"); 294 | 295 | return janet_wrap_abstract(ctx); 296 | } 297 | 298 | static Janet jredis_get_timeout(int32_t argc, Janet *argv) { 299 | janet_fixarity(argc, 1); 300 | Context *ctx = (Context *)janet_getabstract(argv, 0, &redis_context_type); 301 | __ensure_ctx_ok(ctx); 302 | 303 | struct timeval tv; 304 | socklen_t len = sizeof(tv); 305 | 306 | /* N.B. at the time of writing, hiredis does not have a function to get this 307 | value. This is basically a hack since it relies on us knowing the 308 | implementation of set timeout, but our unit tests should cover it. 309 | */ 310 | 311 | if (getsockopt(ctx->ctx->fd, SOL_SOCKET, SO_RCVTIMEO, &tv, &len) == -1) 312 | janet_panic("unable to get previously set timeout on socket"); 313 | 314 | Janet *t = janet_tuple_begin(2); 315 | t[0] = janet_wrap_number(tv.tv_sec); 316 | t[1] = janet_wrap_number(tv.tv_usec); 317 | return janet_wrap_tuple(janet_tuple_end(t)); 318 | } 319 | 320 | static const JanetReg cfuns[] = { 321 | {"connect", jredis_connect, 322 | "(redis/connect host & port)\n\n" 323 | "Connect to a redis server or raise an error."}, 324 | {"connect-unix", jredis_connect_unix, 325 | "(redis/connect-unix socket-path)\n\n" 326 | "Connect to a redis server or raise an error."}, 327 | {"reconnect", jredis_reconnect, 328 | "(redis/reconnect ctx)\n\n" 329 | "Reconnect a redis context."}, 330 | {"close", jredis_close, 331 | "(redis/close ctx)\n\n" 332 | "Close a redis context."}, 333 | {"command", jredis_command, 334 | "(redis/command ctx & params])\n\n" 335 | "Send a command and get the reply, raises an error on redis errors."}, 336 | {"append", jredis_append, 337 | "(redis/append ctx & params])\n\n" 338 | "Add a command to the pipline, raises an error on redis errors."}, 339 | {"set-timeout", jredis_set_timeout, 340 | "(redis/set-timeout ctx seconds &opt useconds])\n\n" 341 | "Set connection timeout."}, 342 | {"get-timeout", jredis_get_timeout, 343 | "(redis/get-timeout ctx])\n\n" 344 | "Get connection timeout that can be passed back to set-timeout."}, 345 | {"get-reply", jredis_get_reply, 346 | "(redis/get-reply ctx & params])\n\n" 347 | "Get the result of a redis command, raises an error on redis errors."}, 348 | {"error-message", jredis_error_message, 349 | "(redis/error-message ctx)\n\n" 350 | "Returns the last redis error or nil."}, 351 | {"error-code", jredis_error_code, "(redis/error-code ctx)\n\n"}, 352 | {NULL, NULL, NULL}}; 353 | 354 | JANET_MODULE_ENTRY(JanetTable *env) { janet_cfuns(env, "redis", cfuns); } 355 | -------------------------------------------------------------------------------- /redis.janet: -------------------------------------------------------------------------------- 1 | (import _janet_redis :prefix "" :export true) 2 | 3 | (defn pipeline 4 | [conn & forms] 5 | (each f forms 6 | (append conn ;f)) 7 | (def r @[]) 8 | (each f forms 9 | (array/push r (get-reply conn))) 10 | r) 11 | 12 | (defn multi 13 | [conn & forms] 14 | (as-> (array/concat @[@["MULTI"]] forms @[["EXEC"]]) _ 15 | (pipeline conn ;_) 16 | (array/pop _))) 17 | -------------------------------------------------------------------------------- /test/redis.janet: -------------------------------------------------------------------------------- 1 | (import sh) 2 | (import shlex) 3 | (import posix-spawn) 4 | (import ../redis :as r) 5 | 6 | (defn tmp-redis 7 | [] 8 | (def port 35543) 9 | 10 | (def d (sh/$<_ mktemp -d /tmp/janet-redis-test.tmp.XXXXX)) 11 | 12 | (def r (posix-spawn/spawn ["sh" "-c" 13 | (string 14 | "cd " (shlex/quote d) " ;" 15 | "exec redis-server --port " port " > /dev/null 2>&1")])) 16 | (os/sleep 0.5) 17 | 18 | @{:port port 19 | :d d 20 | :r r 21 | :connect 22 | (fn [self] 23 | (r/connect "localhost" (self :port))) 24 | :close 25 | (fn [self] 26 | (print "closing down server...") 27 | (:close (self :r)) 28 | (sh/$ rm -rf (self :d)))}) 29 | 30 | (with [tmp-redis-server (tmp-redis)] 31 | 32 | (var conn (:connect tmp-redis-server)) 33 | 34 | # Simple commands. 35 | (assert (= (r/command conn "PING") "PONG")) 36 | 37 | # Simple pipeline 38 | (assert (= (r/append conn "PING") nil)) 39 | (assert (= (r/get-reply conn) "PONG")) 40 | 41 | # Test slow path of many args command 42 | (var args @[]) 43 | (loop [i :range [0 64]] 44 | (array/push args (string "K" i) (string "V" i))) 45 | (r/command conn "HSET" "H" ;args) 46 | (assert (= (r/command conn "HGET" "H" "K1") "V1")) 47 | (assert (= (r/command conn "HGET" "H" "K60") "V60")) 48 | 49 | # Test slow path of many args append 50 | (set args @[]) 51 | (loop [i :range [0 64]] 52 | (array/push args (string "K" i) (string "V" i))) 53 | (r/append conn "HSET" "H2" ;args) 54 | (r/get-reply conn) 55 | (assert (= (r/command conn "HGET" "H2" "K1") "V1")) 56 | (assert (= (r/command conn "HGET" "H2" "K60") "V60")) 57 | 58 | # Test large pipeline. 59 | (loop [i :range [0 64]] 60 | (r/append conn "SET" (string "K" i) (string "V" i))) 61 | 62 | (loop [i :range [0 64]] 63 | (r/append conn "GET" (string "K" i))) 64 | 65 | (loop [i :range [0 64]] 66 | (assert (= (r/get-reply conn) "OK"))) 67 | 68 | (loop [i :range [0 64]] 69 | (assert (= (r/get-reply conn) (string "V" i)))) 70 | 71 | # Test multi. 72 | (assert 73 | (= 74 | (tuple ;(r/multi conn ["PING"] ["PING"])) 75 | ["PONG" "PONG"])) 76 | 77 | # Test errors 78 | (do 79 | (def [ok v] (protect (r/command conn "FOOCOMMAND"))) 80 | (assert (false? ok))) 81 | 82 | # Test server disconnect error 83 | (do 84 | (def conn (:connect tmp-redis-server)) 85 | (r/command conn "QUIT") 86 | (protect (r/command conn "PING")) 87 | (assert (= (r/error-code conn) :REDIS_ERR_EOF))) 88 | 89 | # test close. 90 | (do 91 | (def conn1 (:connect tmp-redis-server)) 92 | (def conn2 (:connect tmp-redis-server)) 93 | (r/close conn1) 94 | (:close conn2) 95 | (def [ok1 v] (protect (r/command conn1 "PING"))) 96 | (def [ok2 v] (protect (r/command conn2 "PING"))) 97 | (assert (false? ok1)) 98 | (assert (false? ok2))) 99 | 100 | # test get timeout and reconnect 101 | (do 102 | (def c (:connect tmp-redis-server)) 103 | (assert (= [0 0] (r/get-timeout c))) 104 | (r/set-timeout c 10 0) 105 | (assert (= [10 0] (r/get-timeout c))) 106 | (r/reconnect c) 107 | (assert (= [0 0] (r/get-timeout c)))) 108 | 109 | # test gc 110 | (do 111 | (var conn (:connect tmp-redis-server)) 112 | (set conn nil) 113 | (gccollect))) 114 | --------------------------------------------------------------------------------