1 module redis.redis; 2 3 import std.stdio; 4 import std.conv; 5 import std.traits; 6 import std.json; 7 import std.exception; 8 import std.variant; 9 import std.array; 10 import std.string; 11 import std.typetuple; 12 import std.exception; 13 14 import core.stdc.stdint; 15 16 public import redis; 17 import std.socket; 18 public class Redis 19 { 20 import std.socket : TcpSocket, InternetAddress; 21 22 private: 23 TcpSocket[string] conns; 24 string addr; 25 string password; 26 public: 27 28 mixin keyCommands; 29 mixin stringCommands; 30 mixin hashCommands; 31 mixin listCommands; 32 mixin setCommands; 33 mixin sortedsetCommands; 34 mixin serverCommands; 35 36 /** 37 * Create a new connection to the Redis server 38 */ 39 this(string host = "127.0.0.1", ushort port = 6379,string password = "") 40 { 41 auto conn = new TcpSocket(new InternetAddress(host, port)); 42 addr = host~":"~to!string(port); 43 conns[addr] = conn; 44 writeln(password); 45 writeln("AUTH "~password); 46 this.password = password; 47 if(password.length > 0){ 48 conn.send(toMultiBulk("AUTH", password)); 49 Response[] r = receiveResponses(conn, 1); 50 } 51 } 52 53 override string toString() 54 { 55 return "redis"; 56 } 57 58 auto opDispatch(string name,T...)(T args) 59 { 60 //try{ 61 auto result = send(name,args); 62 //} catch (exception e){ 63 //return e.msg; 64 //} 65 return result; 66 } 67 68 /** 69 * Call Redis using any type T that can be converted to a string 70 * 71 * Examples: 72 * 73 * --- 74 * send("SET name Adil") 75 * send("SADD", "myset", 1) 76 * send("SADD", "myset", 1.2) 77 * send("SADD", "myset", true) 78 * send("SADD", "myset", "Batman") 79 * send("SREM", "myset", ["$3", "$4"]) 80 * send("SADD", "myset", object) //provided 'object' implements toString() 81 * send("GET", "*") == send("GET *") 82 * send("ZADD", "my_unique_json", 1, json.toString()); 83 * send("EVAL", "return redis.call('set','lua','LUA')", 0); 84 * --- 85 */ 86 R send(R = Response, T...)(string key, T args) 87 { 88 //Implement a write queue here. 89 // All encoded responses are put into a write queue and flushed 90 // For a send request, flush the queue and listen to a response 91 // For async calls, just flush the queue 92 // This automatically gives us PubSub 93 94 SENDS: 95 debug(redis) { writeln(__FUNCTION__,"\t",__LINE__,escape(toMultiBulk(key, args)));} 96 conn.send(toMultiBulk(key, args)); 97 Response[] r = receiveResponses(conn, 1); 98 if(r.length && r[0].isMoved) 99 { 100 addr = ((split(r[0].toString," "))[2]); 101 goto SENDS; 102 } 103 return cast(R)(r[0]); 104 } 105 106 R send(R = Response)(string cmd) 107 { 108 SEND: 109 debug(redis) { writeln(escape(toMultiBulk(cmd)));} 110 conn.send(toMultiBulk(cmd)); 111 Response[] r = receiveResponses(conn, 1); 112 if(r.length && r[0].isMoved) 113 { 114 addr = ((split(r[0].toString," "))[2]); 115 goto SEND; 116 } 117 return cast(R)(r[0]); 118 } 119 120 /** 121 * Send a string that is already encoded in the Redis protocol 122 */ 123 R sendRaw(R = Response)(string cmd) 124 { 125 SEND: 126 127 debug(redis) { writeln(escape(cmd));} 128 129 conn.send(cmd); 130 Response[] r = receiveResponses(conn, 1); 131 if(r.length && r[0].isMoved) 132 { 133 addr = ((split(r[0].toString," "))[2]); 134 goto SEND; 135 } 136 return cast(R)(r[0]); 137 } 138 139 /** 140 * Send a series of commands as a pipeline 141 * 142 * Examples: 143 * 144 * --- 145 * pipeline(["SADD shopping_cart Shirt", "SADD shopping_cart Pant", "SADD shopping_cart Boots"]) 146 * --- 147 */ 148 import std.traits : isSomeChar; 149 Response[] pipeline(C)(C[][] commands) if (isSomeChar!C) 150 { 151 import std.array : appender; 152 153 auto app = appender!(C[])(); 154 foreach(c; commands) { 155 app ~= encode(c); 156 } 157 158 conn.send(app.data); 159 return receiveResponses(conn, commands.length); 160 } 161 162 /** 163 * Execute commands in a MULTI/EXEC block. 164 * 165 * @param all - (Default: false) - By default, only the results of a transaction are returned. If set to "true", the results of each queuing step is also returned. 166 * 167 * Examples: 168 * 169 * --- 170 * transaction(["SADD shopping_cart Shirt", "INCR shopping_cart_ctr"]) 171 * --- 172 */ 173 Response[] transaction(string[] commands, bool all = false) 174 { 175 auto cmd = ["MULTI"]; 176 cmd ~= commands; 177 cmd ~= "EXEC"; 178 auto rez = pipeline(cmd); 179 180 if(all) { 181 return rez; 182 } 183 184 auto resp = rez[$ - 1]; 185 if(resp.isError()) { 186 throw new RedisResponseException(resp.value); 187 } 188 189 return resp.values; 190 } 191 192 /** 193 * Simplified call to EVAL 194 * 195 * Examples: 196 * 197 * --- 198 * Response r = eval("return redis.call('set','lua','LUA_AGAIN')"); 199 * r.value == "LUA_AGAIN"; 200 * 201 * Response r1 = redis.eval("return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", ["key1", "key2"], ["first", "second"]); 202 * writeln(r1); // [key1, key2, first, second] 203 * 204 * Response r1 = redis.eval("return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", [1, 2]); 205 * writeln(r1); // [1, 2] 206 * --- 207 */ 208 Response eval(K = string, A = string)(string lua_script, K[] keys = [], A[] args = []) 209 { 210 conn.send(toMultiBulk("EVAL", lua_script, keys.length, keys, args)); 211 Response[] r = receiveResponses(conn, 1); 212 return (r[0]); 213 } 214 215 Response evalSha(K = string, A = string)(string sha1, K[] keys = [], A[] args = []) 216 { 217 conn.send(toMultiBulk("EVALSHA", sha1, keys.length, keys, args)); 218 Response[] r = receiveResponses(conn, 1); 219 return (r[0]); 220 } 221 222 TcpSocket conn() 223 { 224 if(addr !in conns){ 225 auto arr = split(addr,":"); 226 conns[addr] = new TcpSocket(new InternetAddress(arr[0], arr[1].to!ushort)); 227 if(password.length > 0){ 228 conns[addr].send(toMultiBulk("AUTH", password)); 229 Response[] r = receiveResponses(conns[addr], 1); 230 } 231 } 232 233 int32_t error; 234 conns[addr].getOption(SocketOptionLevel.SOCKET, SocketOption.ERROR, error); 235 if (error != 0) 236 { 237 conns[addr].close(); 238 auto arr = split(addr,":"); 239 conns[addr] = new TcpSocket(new InternetAddress(arr[0], arr[1].to!ushort)); 240 if(password.length > 0){ 241 conns[addr].send(toMultiBulk("AUTH", password)); 242 Response[] r = receiveResponses(conns[addr], 1); 243 } 244 throw new ConnectionException("Error while sending request, error " ~ to!string(error)); 245 246 } 247 return conns[addr]; 248 } 249 250 } 251 252 253 254 unittest 255 { 256 auto redis = new Redis(); 257 //debug(redis) { writeln("\n\n\nredis commands test.....");} 258 //assert(redis.ping() == "PONG"); 259 redis.flushall(); 260 assert(redis.set("xxkey","10") == true); 261 assert(redis.get("xxkey") == "10"); 262 assert(redis.exists("xxkey") == true); 263 //assert(redis.dump("xxkey") == "\u0000\xC0\n\b\u0000ײ\xBB\xFA\xA7\xB7\xE9\x83"); 264 assert(redis.del("xxkey") == 1); 265 assert(redis.set("xxkey","10") == true); 266 assert(redis.expire("xxkey",20) == 1); 267 assert(redis.expire("yykey",20) == 0); 268 assert(redis.expireat("xxkey",1798736461) == 1); 269 assert(redis.expireat("yykey",1798736461) == 0); 270 redis.set("ssoxx","1"); 271 redis.set("bboxx","2"); 272 redis.set("ssmxx","3"); 273 assert(redis.keys("*m*") == ["ssmxx"]); 274 redis.set("abc","test",10); 275 assert(redis.ttl("abc") == 10); 276 redis.set("incrkey","10"); 277 assert(redis.incr("incrkey") == 11); 278 279 assert(redis.hset("website","google","google.com") == 1); 280 assert(redis.hset("website","baidu","baidu.com") == 1); 281 assert(redis.hset("website","putao","putao.com") == 1); 282 assert(redis.hset("website","google","google.com") == 0); 283 assert(redis.hlen("website") == 3); 284 assert(redis.hget("website","google") == "google.com"); 285 assert(redis.hdel("website","google") == 1); 286 assert(redis.hlen("website") == 2); 287 assert(redis.hdel("website","baidu","putao") == 2); 288 assert(redis.hlen("website") == 0); 289 290 assert(redis.lpush("language","c") == 1); 291 assert(redis.lpush("language","php") == 2); 292 assert(redis.llen("language") == 2); 293 assert(redis.lset("language",0,"d") == true); 294 assert(redis.lpop("language") == "d"); 295 assert(redis.llen("language") == 1); 296 assert(redis.lrange("language",0,1) == ["c"]); 297 298 assert(redis.sadd("bbs","baidu.com","google.com") == 2); 299 assert(redis.sadd("bbs","douban.com") == 1); 300 assert(redis.scard("bbs") == 3); 301 assert(redis.sadd("net","douban.com") == 1); 302 assert(redis.spop("net") == "douban.com"); 303 304 debug(redis){writeln("redis commands test end\n\n\n");} 305 306 auto response = redis.send("LASTSAVE"); 307 assert(response.type == ResponseType.Integer); 308 309 assert(redis.send!(bool)("SET", "name", "adil baig")); 310 311 redis.send("SET emptystring ''"); 312 response = redis.send("GET emptystring"); 313 assert(response.value == ""); 314 315 response = redis.send("GET name"); 316 assert(response.type == ResponseType.Bulk); 317 assert(response.value == "adil baig"); 318 319 /* START Test casting byte[] */ 320 assert(cast(byte[])response == "adil baig"); //Test casting to byte[] 321 assert(cast(byte[])response == [97, 100, 105, 108, 32, 98, 97, 105, 103]); 322 323 redis.send("SET mykey 10"); 324 response = redis.send("INCR mykey"); 325 assert(response.type == ResponseType.Integer); 326 assert(response.intval == 11); 327 auto bytes = (cast(ubyte[])response); 328 assert(bytes.length == response.intval.sizeof); 329 assert(bytes[0] == 11); 330 /* END Test casting byte[] */ 331 332 assert(redis.send!(string)("GET name") == "adil baig"); 333 334 response = redis.send("GET nonexistentkey"); 335 assert(response.type == ResponseType.Nil); 336 assert(cast(ubyte[])response == []); 337 338 redis.send("DEL myset"); 339 redis.send("SADD", "myset", 1.2); 340 redis.send("SADD", "myset", 1); 341 redis.send("SADD", "myset", true); 342 redis.send("SADD", "myset", "adil"); 343 redis.send("SADD", "myset", 350001939); 344 redis.send("SADD", ["myset","$4"]); 345 auto r = redis.send("SMEMBERS myset"); 346 assert(r.type == ResponseType.MultiBulk); 347 assert(r.values.length == 6); 348 349 //Check pipeline 350 redis.send("DEL ctr"); 351 auto responses = redis.pipeline(["SET ctr 1", "INCR ctr", "INCR ctr", "INCR ctr", "INCR ctr"]); 352 353 assert(responses.length == 5); 354 assert(responses[0].type == ResponseType.Status); 355 assert(responses[1].intval == 2); 356 assert(responses[2].intval == 3); 357 assert(responses[3].intval == 4); 358 assert(responses[4].intval == 5); 359 360 redis.send("DEL buddies"); 361 auto buddiesQ = ["SADD buddies Batman", "SADD buddies Spiderman", "SADD buddies Hulk", "SMEMBERS buddies"]; 362 Response[] buddies = redis.pipeline(buddiesQ); 363 assert(buddies.length == buddiesQ.length); 364 assert(buddies[0].type == ResponseType.Integer); 365 assert(buddies[1].type == ResponseType.Integer); 366 assert(buddies[2].type == ResponseType.Integer); 367 assert(buddies[3].type == ResponseType.MultiBulk); 368 assert(buddies[3].values.length == 3); 369 370 //Check transaction 371 redis.send("DEL ctr"); 372 responses = redis.transaction(["SET ctr 1", "INCR ctr", "INCR ctr"], true); 373 assert(responses.length == 5); 374 assert(responses[0].type == ResponseType.Status); 375 assert(responses[1].type == ResponseType.Status); 376 assert(responses[2].type == ResponseType.Status); 377 assert(responses[3].type == ResponseType.Status); 378 assert(responses[4].type == ResponseType.MultiBulk); 379 assert(responses[4].values[0].type == ResponseType.Status); 380 assert(responses[4].values[1].intval == 2); 381 assert(responses[4].values[2].intval == 3); 382 383 redis.send("DEL ctr"); 384 responses = redis.transaction(["SET ctr 1", "INCR ctr", "INCR ctr"]); 385 assert(responses.length == 3); 386 assert(responses[0].type == ResponseType.Status); 387 assert(responses[1].intval == 2); 388 assert(responses[2].intval == 3); 389 390 response = redis.send("EVAL", "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", 2, "key1", "key2", "first", "second"); 391 assert(response.values.length == 4); 392 assert(response.values[0].value == "key1"); 393 assert(response.values[1].value == "key2"); 394 assert(response.values[2].value == "first"); 395 assert(response.values[3].value == "second"); 396 397 //Same as above, but simpler 398 response = redis.eval("return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", ["key1", "key2"], ["first", "second"]); 399 assert(response.values.length == 4); 400 assert(response.values[0].value == "key1"); 401 assert(response.values[1].value == "key2"); 402 assert(response.values[2].value == "first"); 403 assert(response.values[3].value == "second"); 404 405 response = redis.eval("return redis.call('set','lua','LUA_AGAIN')"); 406 assert(cast(string)redis.send("GET lua") == "LUA_AGAIN"); 407 408 // A BLPOP times out to a Nil multibulk 409 response = redis.send("BLPOP nonExistentList 1"); 410 assert(response.isNil()); 411 }