From 57172ffb316b5e681c4f515927894f5129739237 Mon Sep 17 00:00:00 2001 From: antirez Date: Sat, 9 May 2009 09:25:59 +0200 Subject: [PATCH] CPP client added thanks to Brian Hammond --- TODO | 1 + benchmark.c | 11 +- client-libraries/cpp/Makefile | 44 ++ client-libraries/cpp/README.rst | 16 + client-libraries/cpp/TODO | 9 + client-libraries/cpp/anet.c | 270 +++++++ client-libraries/cpp/anet.h | 49 ++ client-libraries/cpp/fmacros.h | 7 + client-libraries/cpp/redisclient.cpp | 898 +++++++++++++++++++++++ client-libraries/cpp/redisclient.h | 479 ++++++++++++ client-libraries/cpp/test_client.cpp | 629 ++++++++++++++++ client-libraries/ruby/.gitignore | 1 + client-libraries/ruby/Rakefile | 9 +- client-libraries/ruby/bench.rb | 35 +- client-libraries/ruby/lib/pipeline.rb | 31 + client-libraries/ruby/lib/redis.rb | 73 +- client-libraries/ruby/lib/server.rb | 60 +- client-libraries/ruby/redis-rb.gemspec | 10 +- client-libraries/ruby/spec/redis_spec.rb | 77 +- client-libraries/update-cpp-client.sh | 12 + doc/ExpireCommand.html | 58 ++ utils/redis-sha1.rb | 4 +- 22 files changed, 2736 insertions(+), 47 deletions(-) create mode 100644 client-libraries/cpp/Makefile create mode 100644 client-libraries/cpp/README.rst create mode 100644 client-libraries/cpp/TODO create mode 100644 client-libraries/cpp/anet.c create mode 100644 client-libraries/cpp/anet.h create mode 100644 client-libraries/cpp/fmacros.h create mode 100644 client-libraries/cpp/redisclient.cpp create mode 100644 client-libraries/cpp/redisclient.h create mode 100644 client-libraries/cpp/test_client.cpp create mode 100644 client-libraries/ruby/lib/pipeline.rb create mode 100755 client-libraries/update-cpp-client.sh create mode 100644 doc/ExpireCommand.html diff --git a/TODO b/TODO index a39003f8..81d8c77a 100644 --- a/TODO +++ b/TODO @@ -1,5 +1,6 @@ BEFORE REDIS 1.0.0-rc1 +- Contrib dir with RHL for Centos and other contributions like init scripts - Update the FAQ with max number of keys in a DB and the overcommit thing - Add number of keys for every DB in INFO - maxmemory support in config file. diff --git a/benchmark.c b/benchmark.c index b5c6084d..bdf324bb 100644 --- a/benchmark.c +++ b/benchmark.c @@ -62,6 +62,7 @@ static struct config { int donerequests; int keysize; int datasize; + int randomkeys; aeEventLoop *el; char *hostip; int hostport; @@ -341,6 +342,8 @@ void parseOptions(int argc, char **argv) { i++; if (config.datasize < 1) config.datasize=1; if (config.datasize > 1024*1024) config.datasize = 1024*1024; + } else if (!strcmp(argv[i],"-r")) { + config.randomkeys = 1; } else if (!strcmp(argv[i],"-q")) { config.quiet = 1; } else if (!strcmp(argv[i],"-l")) { @@ -354,6 +357,7 @@ void parseOptions(int argc, char **argv) { printf(" -n Total number of requests (default 10000)\n"); printf(" -d Data size of SET/GET value in bytes (default 2)\n"); printf(" -k 1=keep alive 0=reconnect (default 1)\n"); + printf(" -r Use random keys for SET/GET/INCR\n"); printf(" -q Quiet. Just show query/sec values\n"); printf(" -l Loop. Run the tests forever\n"); exit(1); @@ -374,6 +378,7 @@ int main(int argc, char **argv) { config.keepalive = 1; config.donerequests = 0; config.datasize = 3; + config.randomkeys = 0; config.quiet = 0; config.loop = 0; config.latency = NULL; @@ -402,7 +407,7 @@ int main(int argc, char **argv) { prepareForBenchmark(); c = createClient(); if (!c) exit(1); - c->obuf = sdscatprintf(c->obuf,"SET foo %d\r\n",config.datasize); + c->obuf = sdscatprintf(c->obuf,"SET foo_rand000000000000 %d\r\n",config.datasize); { char *data = zmalloc(config.datasize+2); memset(data,'x',config.datasize); @@ -418,7 +423,7 @@ int main(int argc, char **argv) { prepareForBenchmark(); c = createClient(); if (!c) exit(1); - c->obuf = sdscat(c->obuf,"GET foo\r\n"); + c->obuf = sdscat(c->obuf,"GET foo_rand000000000000\r\n"); c->replytype = REPLY_BULK; c->readlen = -1; createMissingClients(c); @@ -428,7 +433,7 @@ int main(int argc, char **argv) { prepareForBenchmark(); c = createClient(); if (!c) exit(1); - c->obuf = sdscat(c->obuf,"INCR counter\r\n"); + c->obuf = sdscat(c->obuf,"INCR counter_rand000000000000\r\n"); c->replytype = REPLY_INT; createMissingClients(c); aeMain(config.el); diff --git a/client-libraries/cpp/Makefile b/client-libraries/cpp/Makefile new file mode 100644 index 00000000..c4e0416f --- /dev/null +++ b/client-libraries/cpp/Makefile @@ -0,0 +1,44 @@ +# Redis C++ Client Library Makefile + +#CFLAGS?= -pedantic -O2 -Wall -W -DNDEBUG +CFLAGS?= -pedantic -O0 -W -DDEBUG -g +CC = g++ + +CLIENTOBJS = anet.o redisclient.o +LIBNAME = libredisclient.a + +TESTAPP = test_client +TESTAPPOBJS = test_client.o +TESTAPPLIBS = $(LIBNAME) -lstdc++ + +all: $(LIBNAME) $(TESTAPP) + +$(LIBNAME): $(CLIENTOBJS) + ar rcs $(LIBNAME) $(CLIENTOBJS) + +.c.o: + $(CC) -c $(CFLAGS) $< + +.cpp.o: + $(CC) -c $(CFLAGS) $< + +$(TESTAPP): $(LIBNAME) $(TESTAPPOBJS) + $(CC) -o $(TESTAPP) $(TESTAPPOBJS) $(TESTAPPLIBS) + +test: $(TESTAPP) + @./test_client + +check: test + +clean: + rm -rf $(LIBNAME) *.o $(TESTAPP) + +dep: + $(CC) -MM *.c *.cpp + +log: + git log '--pretty=format:%ad %s' --date=short > Changelog + +anet.o: anet.c fmacros.h anet.h +redisclient.o: redisclient.cpp redisclient.h anet.h + diff --git a/client-libraries/cpp/README.rst b/client-libraries/cpp/README.rst new file mode 100644 index 00000000..36603068 --- /dev/null +++ b/client-libraries/cpp/README.rst @@ -0,0 +1,16 @@ +redis-cpp-client +================ + +* A C++ client for the Redis_ key-value database (which is hosted at github_). +* This client has no external dependencies other than g++ (no Boost for instance). +* It uses anet from antirez_ (redis' author), which is bundled. +* This client is licensed under the same license as redis. +* Tested on Linux and Mac OS X. + +* This is a work in progress. I will update this README when the client is "done". + If I had to put a version number on it right now, I'd call it version 0.85 + +.. _Redis: http://code.google.com/p/redis/ +.. _github: http://github.com/antirez/redis/tree/master +.. _antirez: https://github.com/antirez + diff --git a/client-libraries/cpp/TODO b/client-libraries/cpp/TODO new file mode 100644 index 00000000..55967cf4 --- /dev/null +++ b/client-libraries/cpp/TODO @@ -0,0 +1,9 @@ ++ finish command implementations += finish unit tests + Only a few left, to test the SORT command's edge cases (e.g. BY pattern) ++ determine if we should not use bool return values and instead throw redis_error. (latter). ++ maybe more fine-grained exceptions (not just redis_error but operation_not_permitted_error, etc.) +- benchmarking +- consistent hashing? +- make all string literals constants so they can be easily changed (minor) +- add conveniences that store a std::set in its entirety (same for std::list, std::vector) diff --git a/client-libraries/cpp/anet.c b/client-libraries/cpp/anet.c new file mode 100644 index 00000000..893f42c8 --- /dev/null +++ b/client-libraries/cpp/anet.c @@ -0,0 +1,270 @@ +/* anet.c -- Basic TCP socket stuff made a bit less boring + * + * Copyright (c) 2006-2009, Salvatore Sanfilippo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include "fmacros.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "anet.h" + +static void anetSetError(char *err, const char *fmt, ...) +{ + va_list ap; + + if (!err) return; + va_start(ap, fmt); + vsnprintf(err, ANET_ERR_LEN, fmt, ap); + va_end(ap); +} + +int anetNonBlock(char *err, int fd) +{ + int flags; + + /* Set the socket nonblocking. + * Note that fcntl(2) for F_GETFL and F_SETFL can't be + * interrupted by a signal. */ + if ((flags = fcntl(fd, F_GETFL)) == -1) { + anetSetError(err, "fcntl(F_GETFL): %s\n", strerror(errno)); + return ANET_ERR; + } + if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) { + anetSetError(err, "fcntl(F_SETFL,O_NONBLOCK): %s\n", strerror(errno)); + return ANET_ERR; + } + return ANET_OK; +} + +int anetTcpNoDelay(char *err, int fd) +{ + int yes = 1; + if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &yes, sizeof(yes)) == -1) + { + anetSetError(err, "setsockopt TCP_NODELAY: %s\n", strerror(errno)); + return ANET_ERR; + } + return ANET_OK; +} + +int anetSetSendBuffer(char *err, int fd, int buffsize) +{ + if (setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &buffsize, sizeof(buffsize)) == -1) + { + anetSetError(err, "setsockopt SO_SNDBUF: %s\n", strerror(errno)); + return ANET_ERR; + } + return ANET_OK; +} + +int anetTcpKeepAlive(char *err, int fd) +{ + int yes = 1; + if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &yes, sizeof(yes)) == -1) { + anetSetError(err, "setsockopt SO_KEEPALIVE: %s\n", strerror(errno)); + return ANET_ERR; + } + return ANET_OK; +} + +int anetResolve(char *err, char *host, char *ipbuf) +{ + struct sockaddr_in sa; + + sa.sin_family = AF_INET; + if (inet_aton(host, &sa.sin_addr) == 0) { + struct hostent *he; + + he = gethostbyname(host); + if (he == NULL) { + anetSetError(err, "can't resolve: %s\n", host); + return ANET_ERR; + } + memcpy(&sa.sin_addr, he->h_addr, sizeof(struct in_addr)); + } + strcpy(ipbuf,inet_ntoa(sa.sin_addr)); + return ANET_OK; +} + +#define ANET_CONNECT_NONE 0 +#define ANET_CONNECT_NONBLOCK 1 +static int anetTcpGenericConnect(char *err, char *addr, int port, int flags) +{ + int s, on = 1; + struct sockaddr_in sa; + + if ((s = socket(AF_INET, SOCK_STREAM, 0)) == -1) { + anetSetError(err, "creating socket: %s\n", strerror(errno)); + return ANET_ERR; + } + /* Make sure connection-intensive things like the redis benckmark + * will be able to close/open sockets a zillion of times */ + setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); + + sa.sin_family = AF_INET; + sa.sin_port = htons(port); + if (inet_aton(addr, &sa.sin_addr) == 0) { + struct hostent *he; + + he = gethostbyname(addr); + if (he == NULL) { + anetSetError(err, "can't resolve: %s\n", addr); + close(s); + return ANET_ERR; + } + memcpy(&sa.sin_addr, he->h_addr, sizeof(struct in_addr)); + } + if (flags & ANET_CONNECT_NONBLOCK) { + if (anetNonBlock(err,s) != ANET_OK) + return ANET_ERR; + } + if (connect(s, (struct sockaddr*)&sa, sizeof(sa)) == -1) { + if (errno == EINPROGRESS && + flags & ANET_CONNECT_NONBLOCK) + return s; + + anetSetError(err, "connect: %s\n", strerror(errno)); + close(s); + return ANET_ERR; + } + return s; +} + +int anetTcpConnect(char *err, char *addr, int port) +{ + return anetTcpGenericConnect(err,addr,port,ANET_CONNECT_NONE); +} + +int anetTcpNonBlockConnect(char *err, char *addr, int port) +{ + return anetTcpGenericConnect(err,addr,port,ANET_CONNECT_NONBLOCK); +} + +/* Like read(2) but make sure 'count' is read before to return + * (unless error or EOF condition is encountered) */ +int anetRead(int fd, char *buf, int count) +{ + int nread, totlen = 0; + while(totlen != count) { + nread = read(fd,buf,count-totlen); + if (nread == 0) return totlen; + if (nread == -1) return -1; + totlen += nread; + buf += nread; + } + return totlen; +} + +/* Like write(2) but make sure 'count' is read before to return + * (unless error is encountered) */ +int anetWrite(int fd, char *buf, int count) +{ + int nwritten, totlen = 0; + while(totlen != count) { + nwritten = write(fd,buf,count-totlen); + if (nwritten == 0) return totlen; + if (nwritten == -1) return -1; + totlen += nwritten; + buf += nwritten; + } + return totlen; +} + +int anetTcpServer(char *err, int port, char *bindaddr) +{ + int s, on = 1; + struct sockaddr_in sa; + + if ((s = socket(AF_INET, SOCK_STREAM, 0)) == -1) { + anetSetError(err, "socket: %s\n", strerror(errno)); + return ANET_ERR; + } + if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1) { + anetSetError(err, "setsockopt SO_REUSEADDR: %s\n", strerror(errno)); + close(s); + return ANET_ERR; + } + memset(&sa,0,sizeof(sa)); + sa.sin_family = AF_INET; + sa.sin_port = htons(port); + sa.sin_addr.s_addr = htonl(INADDR_ANY); + if (bindaddr) { + if (inet_aton(bindaddr, &sa.sin_addr) == 0) { + anetSetError(err, "Invalid bind address\n"); + close(s); + return ANET_ERR; + } + } + if (bind(s, (struct sockaddr*)&sa, sizeof(sa)) == -1) { + anetSetError(err, "bind: %s\n", strerror(errno)); + close(s); + return ANET_ERR; + } + if (listen(s, 32) == -1) { + anetSetError(err, "listen: %s\n", strerror(errno)); + close(s); + return ANET_ERR; + } + return s; +} + +int anetAccept(char *err, int serversock, char *ip, int *port) +{ + int fd; + struct sockaddr_in sa; + unsigned int saLen; + + while(1) { + saLen = sizeof(sa); + fd = accept(serversock, (struct sockaddr*)&sa, &saLen); + if (fd == -1) { + if (errno == EINTR) + continue; + else { + anetSetError(err, "accept: %s\n", strerror(errno)); + return ANET_ERR; + } + } + break; + } + if (ip) strcpy(ip,inet_ntoa(sa.sin_addr)); + if (port) *port = ntohs(sa.sin_port); + return fd; +} diff --git a/client-libraries/cpp/anet.h b/client-libraries/cpp/anet.h new file mode 100644 index 00000000..b1e9a567 --- /dev/null +++ b/client-libraries/cpp/anet.h @@ -0,0 +1,49 @@ +/* anet.c -- Basic TCP socket stuff made a bit less boring + * + * Copyright (c) 2006-2009, Salvatore Sanfilippo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef ANET_H +#define ANET_H + +#define ANET_OK 0 +#define ANET_ERR -1 +#define ANET_ERR_LEN 256 + +int anetTcpConnect(char *err, char *addr, int port); +int anetTcpNonBlockConnect(char *err, char *addr, int port); +int anetRead(int fd, char *buf, int count); +int anetResolve(char *err, char *host, char *ipbuf); +int anetTcpServer(char *err, int port, char *bindaddr); +int anetAccept(char *err, int serversock, char *ip, int *port); +int anetWrite(int fd, char *buf, int count); +int anetNonBlock(char *err, int fd); +int anetTcpNoDelay(char *err, int fd); +int anetTcpKeepAlive(char *err, int fd); + +#endif diff --git a/client-libraries/cpp/fmacros.h b/client-libraries/cpp/fmacros.h new file mode 100644 index 00000000..7c9b91ae --- /dev/null +++ b/client-libraries/cpp/fmacros.h @@ -0,0 +1,7 @@ +#ifndef _REDIS_FMACRO_H +#define _REDIS_FMACRO_H + +#define _BSD_SOURCE +#define _XOPEN_SOURCE + +#endif diff --git a/client-libraries/cpp/redisclient.cpp b/client-libraries/cpp/redisclient.cpp new file mode 100644 index 00000000..75e6e878 --- /dev/null +++ b/client-libraries/cpp/redisclient.cpp @@ -0,0 +1,898 @@ +/* redisclient.cpp -- a C++ client library for redis. + * + * Copyright (c) 2009, Brian Hammond + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include "redisclient.h" +#include "anet.h" + +#include + +#ifndef NDEBUG +#include +#include +#include +#endif + +#include +#include +#include + +#include +#include + +using namespace std; + +namespace +{ + const string whitespace(" \f\n\r\t\v"); + const string CRLF("\r\n"); + + // Modifies in-place. + + inline string & rtrim(string & str, const string & ws = whitespace) + { + string::size_type pos = str.find_last_not_of(ws); + str.erase(pos + 1); + return str; + } + + vector::size_type split(const string & str, char delim, vector & elems) + { + stringstream ss(str); + string item; + vector::size_type n = 0; + while (getline(ss, item, delim)) + { + elems.push_back(item); + ++n; + } + return n; + } + + inline void split_lines(const string & str, vector & elems) + { + split(str, '\n', elems); + for (vector::iterator it = elems.begin(); it != elems.end(); ++it) + rtrim(*it); + } + +#ifndef NDEBUG + + void output_proto_debug(const string & data, bool is_received = true) + { + string escaped_data(data); + size_t pos; + while ((pos = escaped_data.find("\n")) != string::npos) + escaped_data.replace(pos, 1, "\\n"); + while ((pos = escaped_data.find("\r")) != string::npos) + escaped_data.replace(pos, 1, "\\r"); + + cerr + << time(NULL) << ": " + << (is_received ? "RECV '" : "SEND '") + << escaped_data + << "'" + << endl; + } + +#endif + + class makecmd + { + public: + explicit makecmd(const string & initial, bool finalize = false) + { + buffer_ << initial; + if (!finalize) + buffer_ << " "; + } + + template + makecmd & operator<<(T const & datum) + { + buffer_ << datum; + return *this; + } + + template + makecmd & operator<<(const vector & data) + { + size_t n = data.size(); + for (size_t i = 0; i < n; ++i) + { + buffer_ << data[i]; + if (i < n - 1) + buffer_ << " "; + } + return *this; + } + + operator std::string () + { + buffer_ << CRLF; + return buffer_.str(); + } + + private: + ostringstream buffer_; + }; + + // Reads N bytes from given blocking socket. + + string read_n(int socket, ssize_t n) + { + char * buffer = new char[n + 1]; + buffer[n] = '\0'; + + char * bp = buffer; + ssize_t bytes_read = 0; + + while (bytes_read != n) + { + ssize_t bytes_received = 0; + do bytes_received = recv(socket, bp, n - (bp - buffer), 0); + while (bytes_received < 0 && errno == EINTR); + + if (bytes_received == 0) + throw redis::connection_error("connection was closed"); + + bytes_read += bytes_received; + bp += bytes_received; + } + + string str(buffer); + delete [] buffer; + return str; + } + + // Reads a single line of character data from the given blocking socket. + // Returns the line that was read, not including EOL delimiter(s). Both LF + // ('\n') and CRLF ("\r\n") delimiters are supported. If there was an I/O + // error reading from the socket, connection_error is raised. If max_size + // bytes are read before finding an EOL delimiter, a blank string is + // returned. + + string read_line(int socket, ssize_t max_size = 2048) + { + assert(socket > 0); + assert(max_size > 0); + + ostringstream oss; + + enum { buffer_size = 64 }; + char buffer[buffer_size]; + memset(buffer, 0, buffer_size); + + ssize_t total_bytes_read = 0; + bool found_delimiter = false; + + while (total_bytes_read < max_size && !found_delimiter) + { + // Peek at what's available. + + ssize_t bytes_received = 0; + do bytes_received = recv(socket, buffer, buffer_size, MSG_PEEK); + while (bytes_received < 0 && errno == EINTR); + + if (bytes_received == 0) + throw redis::connection_error("connection was closed"); + + // Some data is available; Length might be < buffer_size. + // Look for newline in whatever was read though. + + char * eol = static_cast(memchr(buffer, '\n', bytes_received)); + + // If found, write data from the buffer to the output string. + // Else, write the entire buffer and continue reading more data. + + ssize_t to_read = bytes_received; + + if (eol) + { + to_read = eol - buffer + 1; + oss.write(buffer, to_read); + found_delimiter = true; + } + else + oss.write(buffer, bytes_received); + + // Now read from the socket to remove the peeked data from the socket's + // read buffer. This will not block since we've peeked already and know + // there's data waiting. It might fail if we were interrupted however. + + do bytes_received = recv(socket, buffer, to_read, 0); + while (bytes_received < 0 && errno == EINTR); + } + + // Construct final line string. Remove trailing CRLF-based whitespace. + + string line = oss.str(); + return rtrim(line, CRLF); + } + + unsigned long unsigned_number_from_string(const string & data) + { + errno = 0; + + unsigned long value = strtoul(data.c_str(), NULL, 10); + + if (value == ULONG_MAX && errno == ERANGE) + throw redis::value_error("invalid number; out of range of long"); + + if (value == 0 && errno == EINVAL) + throw redis::value_error("invalid number; unrecognized format"); + + return value; + } + + redis::client::int_type number_from_string(const string & data) + { + errno = 0; + + redis::client::int_type value = strtol(data.c_str(), NULL, 10); + + if ((value == LONG_MAX || value == LONG_MIN) && errno == ERANGE) + throw redis::value_error("invalid number; out of range of long"); + + if (value == 0 && errno == EINVAL) + throw redis::value_error("invalid number; unrecognized format"); + + return value; + } + + const string status_reply_ok("OK"); + const string prefix_status_reply_error("-ERR "); + const char prefix_status_reply_value = '+'; + const char prefix_single_bulk_reply = '$'; + const char prefix_multi_bulk_reply = '*'; + const char prefix_int_reply = ':'; + + const string server_info_key_version = "redis_version"; + const string server_info_key_bgsave_in_progress = "bgsave_in_progress"; + const string server_info_key_connected_clients = "connected_clients"; + const string server_info_key_connected_slaves = "connected_slaves"; + const string server_info_key_used_memory = "used_memory"; + const string server_info_key_changes_since_last_save = "changes_since_last_save"; + const string server_info_key_last_save_time = "last_save_time"; + const string server_info_key_total_connections_received = "total_connections_received"; + const string server_info_key_total_commands_processed = "total_commands_processed"; + const string server_info_key_uptime_in_seconds = "uptime_in_seconds"; + const string server_info_key_uptime_in_days = "uptime_in_days"; +} + +namespace redis +{ + redis_error::redis_error(const string & err) : err_(err) + { + } + + redis_error::operator std::string () + { + return err_; + } + + redis_error::operator const std::string () const + { + return err_; + } + + connection_error::connection_error(const string & err) : redis_error(err) + { + } + + protocol_error::protocol_error(const string & err) : redis_error(err) + { + } + + key_error::key_error(const string & err) : redis_error(err) + { + } + + value_error::value_error(const string & err) : redis_error(err) + { + } + + client::string_type client::missing_value("**nonexistent-key**"); + + client::client(const string_type & host, unsigned int port) + { + char err[ANET_ERR_LEN]; + socket_ = anetTcpConnect(err, const_cast(host.c_str()), port); + if (socket_ == ANET_ERR) + throw connection_error(err); + anetTcpNoDelay(NULL, socket_); + } + + client::~client() + { + if (socket_ != ANET_ERR) + close(socket_); + } + + void client::auth(const client::string_type & pass) + { + send_(makecmd("AUTH") << pass); + recv_ok_reply_(); + } + + void client::set(const client::string_type & key, + const client::string_type & value) + { + send_(makecmd("SET") << key << ' ' << value.size() << CRLF << value); + recv_ok_reply_(); + } + + client::string_type client::get(const client::string_type & key) + { + send_(makecmd("GET") << key); + return recv_bulk_reply_(); + } + + client::string_type client::getset(const client::string_type & key, + const client::string_type & value) + { + send_(makecmd("GETSET") << key << ' ' << value.size() << CRLF << value); + return recv_bulk_reply_(); + } + + void client::mget(const client::string_vector & keys, string_vector & out) + { + send_(makecmd("MGET") << keys); + recv_multi_bulk_reply_(out); + } + + bool client::setnx(const client::string_type & key, + const client::string_type & value) + { + send_(makecmd("SETNX") << key << ' ' << value.size() << CRLF << value); + return recv_int_reply_() == 1; + } + + client::int_type client::incr(const client::string_type & key) + { + send_(makecmd("INCR") << key); + return recv_int_reply_(); + } + + client::int_type client::incrby(const client::string_type & key, + client::int_type by) + { + send_(makecmd("INCRBY") << key << ' ' << by); + return recv_int_reply_(); + } + + client::int_type client::decr(const client::string_type & key) + { + send_(makecmd("DECR") << key); + return recv_int_reply_(); + } + + client::int_type client::decrby(const client::string_type & key, + client::int_type by) + { + send_(makecmd("DECRBY") << key << ' ' << by); + return recv_int_reply_(); + } + + bool client::exists(const client::string_type & key) + { + send_(makecmd("EXISTS") << key); + return recv_int_reply_() == 1; + } + + void client::del(const client::string_type & key) + { + send_(makecmd("DEL") << key); + recv_int_ok_reply_(); + } + + client::datatype client::type(const client::string_type & key) + { + send_(makecmd("TYPE") << key); + string response = recv_single_line_reply_(); + + if (response == "none") return datatype_none; + if (response == "string") return datatype_string; + if (response == "list") return datatype_list; + if (response == "set") return datatype_set; + + return datatype_none; + } + + client::int_type client::keys(const client::string_type & pattern, + client::string_vector & out) + { + send_(makecmd("KEYS") << pattern); + string resp = recv_bulk_reply_(); + return split(resp, ' ', out); + } + + client::string_type client::randomkey() + { + send_(makecmd("RANDOMKEY", true)); + return recv_single_line_reply_(); + } + + void client::rename(const client::string_type & old_name, + const client::string_type & new_name) + { + send_(makecmd("RENAME") << old_name << ' ' << new_name); + recv_ok_reply_(); + } + + bool client::renamenx(const client::string_type & old_name, + const client::string_type & new_name) + { + send_(makecmd("RENAMENX") << old_name << ' ' << new_name); + return recv_int_reply_() == 1; + } + + client::int_type client::dbsize() + { + send_(makecmd("DBSIZE")); + return recv_int_reply_(); + } + + void client::expire(const string_type & key, unsigned int secs) + { + send_(makecmd("EXPIRE") << key << ' ' << secs); + recv_int_ok_reply_(); + } + + void client::rpush(const client::string_type & key, + const client::string_type & value) + { + send_(makecmd("RPUSH") << key << ' ' << value.length() << CRLF << value); + recv_ok_reply_(); + } + + void client::lpush(const client::string_type & key, + const client::string_type & value) + { + send_(makecmd("LPUSH") << key << ' ' << value.length() << CRLF << value); + recv_ok_reply_(); + } + + client::int_type client::llen(const client::string_type & key) + { + send_(makecmd("LLEN") << key); + return recv_int_reply_(); + } + + client::int_type client::lrange(const client::string_type & key, + client::int_type start, + client::int_type end, + client::string_vector & out) + { + send_(makecmd("LRANGE") << key << ' ' << start << ' ' << end); + return recv_multi_bulk_reply_(out); + } + + void client::ltrim(const client::string_type & key, + client::int_type start, + client::int_type end) + { + send_(makecmd("LTRIM") << key << ' ' << start << ' ' << end); + recv_ok_reply_(); + } + + client::string_type client::lindex(const client::string_type & key, + client::int_type index) + { + send_(makecmd("LINDEX") << key << ' ' << index); + return recv_bulk_reply_(); + } + + void client::lset(const client::string_type & key, + client::int_type index, + const client::string_type & value) + { + send_(makecmd("LSET") << key << ' ' << index << ' ' << value.length() << CRLF << value); + recv_ok_reply_(); + } + + client::int_type client::lrem(const client::string_type & key, + client::int_type count, + const client::string_type & value) + { + send_(makecmd("LREM") << key << ' ' << count << ' ' << value.length() << CRLF << value); + return recv_int_reply_(); + } + + client::string_type client::lpop(const client::string_type & key) + { + send_(makecmd("LPOP") << key); + return recv_bulk_reply_(); + } + + client::string_type client::rpop(const client::string_type & key) + { + send_(makecmd("RPOP") << key); + return recv_bulk_reply_(); + } + + void client::sadd(const client::string_type & key, + const client::string_type & value) + { + send_(makecmd("SADD") << key << ' ' << value.length() << CRLF << value); + recv_int_ok_reply_(); + } + + void client::srem(const client::string_type & key, + const client::string_type & value) + { + send_(makecmd("SREM") << key << ' ' << value.length() << CRLF << value); + recv_int_ok_reply_(); + } + + void client::smove(const client::string_type & srckey, + const client::string_type & dstkey, + const client::string_type & value) + { + send_(makecmd("SMOVE") << srckey << ' ' << dstkey << ' ' << value.length() << CRLF << value); + recv_int_ok_reply_(); + } + + client::int_type client::scard(const client::string_type & key) + { + send_(makecmd("SCARD") << key); + return recv_int_reply_(); + } + + bool client::sismember(const client::string_type & key, + const client::string_type & value) + { + send_(makecmd("SISMEMBER") << key << ' ' << value.length() << CRLF << value); + return recv_int_reply_() == 1; + } + + client::int_type client::sinter(const client::string_vector & keys, client::string_set & out) + { + send_(makecmd("SINTER") << keys); + return recv_multi_bulk_reply_(out); + } + + void client::sinterstore(const client::string_type & dstkey, + const client::string_vector & keys) + { + send_(makecmd("SINTERSTORE") << dstkey << ' ' << keys); + recv_ok_reply_(); + } + + client::int_type client::sunion(const client::string_vector & keys, + client::string_set & out) + { + send_(makecmd("SUNION") << keys); + return recv_multi_bulk_reply_(out); + } + + void client::sunionstore(const client::string_type & dstkey, + const client::string_vector & keys) + { + send_(makecmd("SUNIONSTORE") << dstkey << ' ' << keys); + recv_ok_reply_(); + } + + client::int_type client::smembers(const client::string_type & key, + client::string_set & out) + { + send_(makecmd("SMEMBERS") << key); + return recv_multi_bulk_reply_(out); + } + + void client::select(client::int_type dbindex) + { + send_(makecmd("SELECT") << dbindex); + recv_ok_reply_(); + } + + void client::move(const client::string_type & key, + client::int_type dbindex) + { + send_(makecmd("MOVE") << key << ' ' << dbindex); + recv_int_ok_reply_(); + } + + void client::flushdb() + { + send_(makecmd("FLUSHDB", true)); + recv_ok_reply_(); + } + + void client::flushall() + { + send_(makecmd("FLUSHALL", true)); + recv_ok_reply_(); + } + + client::int_type client::sort(const client::string_type & key, + client::string_vector & out, + client::sort_order order, + bool lexicographically) + { + send_(makecmd("SORT") << key + << (order == sort_order_ascending ? " ASC" : " DESC") + << (lexicographically ? " ALPHA" : "")); + + return recv_multi_bulk_reply_(out); + } + + client::int_type client::sort(const client::string_type & key, + client::string_vector & out, + client::int_type limit_start, + client::int_type limit_end, + client::sort_order order, + bool lexicographically) + { + send_(makecmd("SORT") << key + << " LIMIT " << limit_start << ' ' << limit_end + << (order == sort_order_ascending ? " ASC" : " DESC") + << (lexicographically ? " ALPHA" : "")); + + return recv_multi_bulk_reply_(out); + } + + client::int_type client::sort(const client::string_type & key, + client::string_vector & out, + const client::string_type & by_pattern, + client::int_type limit_start, + client::int_type limit_end, + const client::string_type & get_pattern, + client::sort_order order, + bool lexicographically) + { + send_(makecmd("SORT") << key + << " BY " << by_pattern + << " LIMIT " << limit_start << ' ' << limit_end + << " GET " << get_pattern + << (order == sort_order_ascending ? " ASC" : " DESC") + << (lexicographically ? " ALPHA" : "")); + + return recv_multi_bulk_reply_(out); + } + + void client::save() + { + send_(makecmd("SAVE", true)); + recv_ok_reply_(); + e.g. } + + void client::bgsave() + { + send_(makecmd("BGSAVE", true)); + recv_ok_reply_(); + } + + time_t client::lastsave() + { + send_(makecmd("LASTSAVE", true)); + return recv_int_reply_(); + } + + void client::shutdown() + { + send_(makecmd("SHUTDOWN", true)); + + // we expected to get a connection_error as redis closes the connection on shutdown command. + + try + { + recv_ok_reply_(); + } + catch (connection_error & e) + { + } + } + + void client::info(server_info & out) + { + send_(makecmd("INFO", true)); + string response = recv_bulk_reply_(); + + if (response.empty()) + throw protocol_error("empty"); + + string_vector lines; + split_lines(response, lines); + if (lines.empty()) + throw protocol_error("empty line for info"); + + for (string_vector::const_iterator it = lines.begin(); + it != lines.end(); ++it) + { + const string & line = *it; + string_vector line_parts; + split(line, ':', line_parts); + if (line_parts.size() != 2) + throw protocol_error("unexpected line format for info"); + + const string & key = line_parts[0]; + const string & val = line_parts[1]; + + if (key == server_info_key_version) + out.version = val; + else if (key == server_info_key_bgsave_in_progress) + out.bgsave_in_progress = unsigned_number_from_string(val) == 1; + else if (key == server_info_key_connected_clients) + out.connected_clients = unsigned_number_from_string(val); + else if (key == server_info_key_connected_slaves) + out.connected_slaves = unsigned_number_from_string(val); + else if (key == server_info_key_used_memory) + out.used_memory = unsigned_number_from_string(val); + else if (key == server_info_key_changes_since_last_save) + out.changes_since_last_save = unsigned_number_from_string(val); + else if (key == server_info_key_last_save_time) + out.last_save_time = unsigned_number_from_string(val); + else if (key == server_info_key_total_connections_received) + out.total_connections_received = unsigned_number_from_string(val); + else if (key == server_info_key_total_commands_processed) + out.total_commands_processed = unsigned_number_from_string(val); + else if (key == server_info_key_uptime_in_seconds) + out.uptime_in_seconds = unsigned_number_from_string(val); + else if (key == server_info_key_uptime_in_days) + out.uptime_in_days = unsigned_number_from_string(val); + else + throw protocol_error(string("unexpected info key '") + key + "'"); + } + } + + // + // Private methods + // + + void client::send_(const string & msg) + { +#ifndef NDEBUG + output_proto_debug(msg, false); +#endif + + if (anetWrite(socket_, const_cast(msg.data()), msg.size()) == -1) + throw connection_error(strerror(errno)); + } + + string client::recv_single_line_reply_() + { + string line = read_line(socket_); + +#ifndef NDEBUG + output_proto_debug(line); +#endif + + if (line.empty()) + throw protocol_error("empty single line reply"); + + if (line.find(prefix_status_reply_error) == 0) + { + string error_msg = line.substr(prefix_status_reply_error.length()); + if (error_msg.empty()) + error_msg = "unknown error"; + throw protocol_error(error_msg); + } + + if (line[0] != prefix_status_reply_value) + throw protocol_error("unexpected prefix for status reply"); + + return line.substr(1); + } + + void client::recv_ok_reply_() + { + if (recv_single_line_reply_() != status_reply_ok) + throw protocol_error("expected OK response"); + } + + client::int_type client::recv_bulk_reply_(char prefix) + { + string line = read_line(socket_); + +#ifndef NDEBUG + output_proto_debug(line); +#endif + + if (line[0] != prefix) + throw protocol_error("unexpected prefix for bulk reply"); + + return number_from_string(line.substr(1)); + } + + string client::recv_bulk_reply_() + { + int_type length = recv_bulk_reply_(prefix_single_bulk_reply); + + if (length == -1) + return client::missing_value; + + int_type real_length = length + 2; // CRLF + + string data = read_n(socket_, real_length); + +#ifndef NDEBUG + output_proto_debug(data.substr(0, data.length()-2)); +#endif + + if (data.empty()) + throw protocol_error("invalid bulk reply data; empty"); + + if (data.length() != static_cast(real_length)) + throw protocol_error("invalid bulk reply data; data of unexpected length"); + + data.erase(data.size() - 2); + + return data; + } + + client::int_type client::recv_multi_bulk_reply_(string_vector & out) + { + int_type length = recv_bulk_reply_(prefix_multi_bulk_reply); + + if (length == -1) + throw key_error("no such key"); + + for (int_type i = 0; i < length; ++i) + out.push_back(recv_bulk_reply_()); + + return length; + } + + client::int_type client::recv_multi_bulk_reply_(string_set & out) + { + int_type length = recv_bulk_reply_(prefix_multi_bulk_reply); + + if (length == -1) + throw key_error("no such key"); + + for (int_type i = 0; i < length; ++i) + out.insert(recv_bulk_reply_()); + + return length; + } + + client::int_type client::recv_int_reply_() + { + string line = read_line(socket_); + +#ifndef NDEBUG + output_proto_debug(line); +#endif + + if (line.empty()) + throw protocol_error("invalid integer reply; empty"); + + if (line[0] != prefix_int_reply) + throw protocol_error("unexpected prefix for integer reply"); + + return number_from_string(line.substr(1)); + } + + void client::recv_int_ok_reply_() + { + if (recv_int_reply_() != 1) + throw protocol_error("expecting int reply of 1"); + } +} diff --git a/client-libraries/cpp/redisclient.h b/client-libraries/cpp/redisclient.h new file mode 100644 index 00000000..b1fbb582 --- /dev/null +++ b/client-libraries/cpp/redisclient.h @@ -0,0 +1,479 @@ +/* redisclient.h -- a C++ client library for redis. + * + * Copyright (c) 2009, Brian Hammond + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef REDISCLIENT_H +#define REDISCLIENT_H + +#include +#include +#include +#include +#include + +namespace redis +{ + struct server_info + { + std::string version; + bool bgsave_in_progress; + unsigned long connected_clients; + unsigned long connected_slaves; + unsigned long used_memory; + unsigned long changes_since_last_save; + unsigned long last_save_time; + unsigned long total_connections_received; + unsigned long total_commands_processed; + unsigned long uptime_in_seconds; + unsigned long uptime_in_days; + }; + + // Generic error that is thrown when communicating with the redis server. + + class redis_error + { + public: + redis_error(const std::string & err); + operator std::string (); + operator const std::string () const; + private: + std::string err_; + }; + + // Some socket-level I/O or general connection error. + + class connection_error : public redis_error + { + public: + connection_error(const std::string & err); + }; + + // Redis gave us a reply we were not expecting. + // Possibly an internal error (here or in redis, probably here). + + class protocol_error : public redis_error + { + public: + protocol_error(const std::string & err); + }; + + // A key that you expected to exist does not in fact exist. + + class key_error : public redis_error + { + public: + key_error(const std::string & err); + }; + + // A value of an expected type or other semantics was found to be invalid. + + class value_error : public redis_error + { + public: + value_error(const std::string & err); + }; + + // You should construct a 'client' object per connection to a redis-server. + // + // Please read the online redis command reference: + // http://code.google.com/p/redis/wiki/CommandReference + // + // No provisions for customizing the allocator on the string/bulk value type + // (std::string) are provided. If needed, you can always change the + // string_type typedef in your local version. + + class client + { + public: + typedef std::string string_type; + typedef std::vector string_vector; + typedef std::set string_set; + + typedef long int_type; + + explicit client(const string_type & host = "localhost", + unsigned int port = 6379); + + ~client(); + + // + // Connection handling + // + + void auth(const string_type & pass); + + // + // Commands operating on string values + // + // Note that empty string values do not denote nonexistent keys but well, + // empty values! If a nonexistent key is queried, the value returned will + // be missing_value, including when string_vector objects are returned. + // + + static string_type missing_value; + + // set a key to a string value + + void set(const string_type & key, const string_type & value); + + // return the string value of the key + + string_type get(const string_type & key); + + // set a key to a string returning the old value of the key + + string_type getset(const string_type & key, const string_type & value); + + // multi-get, return the strings values of the keys + + void mget(const string_vector & keys, string_vector & out); + + // set a key to a string value if the key does not exist. returns true if + // the key was set, else false. This does not throw since you are ok with + // this failing if the dst key already exists. + + bool setnx(const string_type & key, const string_type & value); + + // increment the integer value of key + // returns new value + + int_type incr(const string_type & key); + + // increment the integer value of key by integer + // returns new value + + int_type incrby(const string_type & key, int_type by); + + // decrement the integer value of key + // returns new value + + int_type decr(const string_type & key); + + // decrement the integer value of key by integer + // returns new value + + int_type decrby(const string_type & key, int_type by); + + // test if a key exists + + bool exists(const string_type & key); + + // delete a key + // throws if doesn't exist + + void del(const string_type & key); + + enum datatype + { + datatype_none, // key doesn't exist + datatype_string, + datatype_list, + datatype_set + }; + + // return the type of the value stored at key + + datatype type(const string_type & key); + + // + // Commands operating on the key space + // + + // find all the keys matching a given pattern + // returns numbers of keys appended to 'out' + + int_type keys(const string_type & pattern, string_vector & out); + + // return a random key from the key space + // returns empty string if db is empty + + string_type randomkey(); + + // rename the old key in the new one, destroying the new key if + // it already exists + + void rename(const string_type & old_name, const string_type & new_name); + + // rename the old key in the new one, if the new key does not already + // exist. This does not throw since you are ok with this failing if the + // new_name key already exists. + + bool renamenx(const string_type & old_name, const string_type & new_name); + + // return the number of keys in the current db + + int_type dbsize(); + + // set a time to live in seconds on a key. + // fails if there's already a timeout on the key. + + // NB: there's currently no generic way to remove a timeout on a key + + void expire(const string_type & key, unsigned int secs); + + // + // Commands operating on lists + // + + // Append an element to the tail of the list value at key + + void rpush(const string_type & key, const string_type & value); + + // Append an element to the head of the list value at key + + void lpush(const string_type & key, const string_type & value); + + // Return the length of the list value at key + // Returns 0 if the list does not exist; see 'exists' + + int_type llen(const string_type & key); + + // Fetch a range of elements from the list at key + // end can be negative for reverse offsets + // Returns number of elements appended to 'out' + + int_type lrange(const string_type & key, + int_type start, + int_type end, + string_vector & out); + + // Fetches the entire list at key. + + int_type get_list(const string_type & key, string_vector & out) + { + return lrange(key, 0, -1, out); + } + + // Trim the list at key to the specified range of elements + + void ltrim(const string_type & key, int_type start, int_type end); + + // Return the element at index position from the list at key + + string_type lindex(const string_type & key, int_type); + + // set a new value as the element at index position of the list at key + + void lset(const string_type & key, + int_type index, + const string_type &); + + // If count is zero all the elements are removed. If count is negative + // elements are removed from tail to head, instead to go from head to tail + // that is the normal behaviour. So for example LREM with count -2 and + // hello as value to remove against the list (a,b,c,hello,x,hello,hello) + // will lave the list (a,b,c,hello,x). Returns the number of removed + // elements if the operation succeeded. + // + // Note: this will not throw if the number of elements removed != count + // since you might want to remove at most count elements by don't care if + // < count elements are removed. See lrem_exact(). + + int_type lrem(const string_type & key, + int_type count, + const string_type & value); + + // An extension of 'lrem' that wants to remove exactly 'count' elements. + // Throws value_error if 'count' elements are not found & removed from the + // list at 'key'. + + void lrem_exact(const string_type & key, + int_type count, + const string_type & value) + { + if (lrem(key, count, value) != count) + throw value_error("failed to remove exactly N elements from list"); + } + + // Return and remove (atomically) the first element of the list at key + + string_type lpop(const string_type & key); + + // Return and remove (atomically) the last element of the list at key + + string_type rpop(const string_type & key); + + // + // Commands operating on sets + // + + // Add the specified member to the set value at key + // returns true if added, or false if already a member of the set. + + void sadd(const string_type & key, const string_type & value); + + // Remove the specified member from the set value at key + // returns true if removed or false if value is not a member of the set. + + void srem(const string_type & key, const string_type & value); + + // Move the specified member from one set to another atomically + // returns true if element was moved, else false (e.g. not found) + + void smove(const string_type & srckey, + const string_type & dstkey, + const string_type & value); + + // Return the number of elements (the cardinality) of the set at key + + int_type scard(const string_type & key); + + // Test if the specified value is a member of the set at key + // Returns false if key doesn't exist or value is not a member of the set at key + + bool sismember(const string_type & key, const string_type & value); + + // Return the intersection between the sets stored at key1, key2, ..., keyN + + int_type sinter(const string_vector & keys, string_set & out); + + // Compute the intersection between the sets stored at key1, key2, ..., + // keyN, and store the resulting set at dstkey + + void sinterstore(const string_type & dstkey, const string_vector & keys); + + // Return the union between the sets stored at key1, key2, ..., keyN + + int_type sunion(const string_vector & keys, string_set & out); + + // Compute the union between the sets stored at key1, key2, ..., keyN, + // and store the resulting set at dstkey + + void sunionstore(const string_type & dstkey, const string_vector & keys); + + // Return all the members of the set value at key + + int_type smembers(const string_type & key, string_set & out); + + // + // Multiple databases handling commands + // + + // Select the DB having the specified index + + void select(int_type dbindex); + + // Move the key from the currently selected DB to the DB having as index + // dbindex. Throws if key was already in the db at dbindex or not found in + // currently selected db. + + void move(const string_type & key, int_type dbindex); + + // Remove all the keys of the currently selected DB + + void flushdb(); + + // Remove all the keys from all the databases + + void flushall(); + + // + // Sorting + // Just go read http://code.google.com/p/redis/wiki/SortCommand + // + + enum sort_order + { + sort_order_ascending, + sort_order_descending + }; + + int_type sort(const string_type & key, + string_vector & out, + sort_order order = sort_order_ascending, + bool lexicographically = false); + + int_type sort(const string_type & key, + string_vector & out, + int_type limit_start, + int_type limit_end, + sort_order order = sort_order_ascending, + bool lexicographically = false); + + int_type sort(const string_type & key, + string_vector & out, + const string_type & by_pattern, + int_type limit_start, + int_type limit_end, + const string_type & get_pattern, + sort_order order = sort_order_ascending, + bool lexicographically = false); + + // + // Persistence control commands + // + + // Synchronously save the DB on disk + + void save(); + + // Asynchronously save the DB on disk + + void bgsave(); + + // Return the UNIX time stamp of the last successfully saving of the + // dataset on disk + + time_t lastsave(); + + // Synchronously save the DB on disk, then shutdown the server. This + // object's connection to the server will be lost on success. Otherwise, + // redis_error is raised. Thus, on success, you should delete or otherwise + // no longer use the object. + + void shutdown(); + + // + // Remote server control commands + // + + // Provide information and statistics about the server + + void info(server_info & out); + + private: + client(const client &); + client & operator=(const client &); + + void send_(const std::string &); + void recv_ok_reply_(); + void recv_int_ok_reply_(); + std::string recv_single_line_reply_(); + int_type recv_bulk_reply_(char prefix); + std::string recv_bulk_reply_(); + int_type recv_multi_bulk_reply_(string_vector & out); + int_type recv_multi_bulk_reply_(string_set & out); + int_type recv_int_reply_(); + + private: + int socket_; + }; +} + +#endif diff --git a/client-libraries/cpp/test_client.cpp b/client-libraries/cpp/test_client.cpp new file mode 100644 index 00000000..c0891cb0 --- /dev/null +++ b/client-libraries/cpp/test_client.cpp @@ -0,0 +1,629 @@ +#include "redisclient.h" + +#include + +using namespace std; + +#define ASSERT_EQUAL(x,y) assert_equal(x, y, __LINE__) +#define ASSERT_NOT_EQUAL(x,y) assert_not_equal(x, y, __LINE__) +#define ASSERT_GT(x,y) assert_gt(x, y, __LINE__) + +template +void assert_equal(const T & actual, const T & expected, int lineno) +{ +#ifndef NDEBUG + cerr << "assert_equal('" << expected << "', '" << actual << "')" << endl; +#endif + + if (expected != actual) + { + cerr << "expected '" << expected << "' got '" << actual << "'" << endl + << "failing test called from line " << lineno << endl; + + exit(1); + } + +#ifndef NDEBUG + cerr << "... OK" << endl; +#endif +} + +template +void assert_not_equal(const T & a, const T & b, int lineno) +{ + if (a == b) + { + cerr << "expected inequality" << endl + << "failing test called from line " << lineno << endl; + + exit(1); + } +} + +template +void assert_gt(const T & a, const T & b, int lineno) +{ +#ifndef NDEBUG + cerr << "assert_gt('" << a << "', '" << b << "')" << endl; +#endif + + if (a <= b) + { + cerr << "expected '" << a << "' > '" << b << "'" << endl + << "failing test called from line " << lineno << endl; + + exit(1); + } + +#ifndef NDEBUG + cerr << "... OK" << endl; +#endif +} + +void test(const string & name) +{ +#ifndef NDEBUG + cerr << "------------------------------" << endl + << "starting test: " << name << endl; +#endif +} + +int main(int argc, char ** argv) +{ + try + { + redis::client c; + + // Test on high number databases + + c.select(14); + c.flushdb(); + + c.select(15); + c.flushdb(); + + string foo("foo"), bar("bar"), baz("baz"), buz("buz"), goo("goo"); + + test("auth"); + { + // TODO ... needs a conf for redis-server + } + + test("info"); + { + // doesn't throw? then, has valid numbers and known info-keys. + redis::server_info info; + c.info(info); + } + + test("set, get"); + { + c.set(foo, bar); + ASSERT_EQUAL(c.get(foo), bar); + } + + test("getset"); + { + ASSERT_EQUAL(c.getset(foo, baz), bar); + ASSERT_EQUAL(c.get(foo), baz); + } + + test("mget"); + { + string x_val("hello"), y_val("world"); + c.set("x", x_val); + c.set("y", y_val); + redis::client::string_vector keys; + keys.push_back("x"); + keys.push_back("y"); + redis::client::string_vector vals; + c.mget(keys, vals); + ASSERT_EQUAL(vals.size(), size_t(2)); + ASSERT_EQUAL(vals[0], x_val); + ASSERT_EQUAL(vals[1], y_val); + } + + test("setnx"); + { + ASSERT_EQUAL(c.setnx(foo, bar), false); + ASSERT_EQUAL(c.setnx(buz, baz), true); + ASSERT_EQUAL(c.get(buz), baz); + } + + test("incr"); + { + ASSERT_EQUAL(c.incr("goo"), 1L);test("nonexistent (0) -> 1"); + ASSERT_EQUAL(c.incr("goo"), 2L);test("1->2"); + } + + test("decr"); + { + ASSERT_EQUAL(c.decr("goo"), 1L);test("2->1"); + ASSERT_EQUAL(c.decr("goo"), 0L);test("1->0"); + } + + test("incrby"); + { + ASSERT_EQUAL(c.incrby("goo", 3), 3L);test("0->3"); + ASSERT_EQUAL(c.incrby("goo", 2), 5L);test("3->5"); + } + + test("exists"); + { + ASSERT_EQUAL(c.exists("goo"), true); + } + + test("del"); + { + c.del("goo"); + ASSERT_EQUAL(c.exists("goo"), false); + } + + test("type (basic)"); + { + ASSERT_EQUAL(c.type(goo), redis::client::datatype_none);test("we deleted it"); + c.set(goo, "redis"); + ASSERT_EQUAL(c.type(goo), redis::client::datatype_string); + } + + test("keys"); + { + redis::client::string_vector keys; + ASSERT_EQUAL(c.keys("*oo", keys), 2L); + ASSERT_EQUAL(keys.size(), 2UL); + ASSERT_EQUAL(keys[0], foo); + ASSERT_EQUAL(keys[1], goo); + } + + test("randomkey"); + { + ASSERT_GT(c.randomkey().size(), 0UL); + } + + test("rename"); + { + ASSERT_EQUAL(c.exists("foo"), true); + ASSERT_EQUAL(c.exists("doo"), false); + c.rename("foo", "doo"); + ASSERT_EQUAL(c.exists("foo"), false); + ASSERT_EQUAL(c.exists("doo"), true); + } + + test("renamenx"); + { + ASSERT_EQUAL(c.exists("doo"), true); + ASSERT_EQUAL(c.exists("foo"), false); + ASSERT_EQUAL(c.renamenx("doo", "foo"), true); + ASSERT_EQUAL(c.exists("doo"), false); + ASSERT_EQUAL(c.exists("foo"), true); + ASSERT_EQUAL(c.renamenx("goo", "foo"), false); + ASSERT_EQUAL(c.exists("foo"), true); + ASSERT_EQUAL(c.exists("goo"), true); + } + + test("dbsize"); + { + ASSERT_GT(c.dbsize(), 0L); + } + + test("expire"); + { + c.expire("goo", 1); +#ifndef NDEBUG + cerr << "please wait a few seconds.." << endl; +#endif + sleep(2); + ASSERT_EQUAL(c.exists("goo"), false); + } + + test("rpush"); + { + ASSERT_EQUAL(c.exists("list1"), false); + c.rpush("list1", "val1"); + ASSERT_EQUAL(c.llen("list1"), 1L); + ASSERT_EQUAL(c.type("list1"), redis::client::datatype_list); + c.rpush("list1", "val2"); + ASSERT_EQUAL(c.llen("list1"), 2L); + ASSERT_EQUAL(c.lindex("list1", 0), string("val1")); + ASSERT_EQUAL(c.lindex("list1", 1), string("val2")); + } + + test("lpush"); + { + c.del("list1"); + ASSERT_EQUAL(c.exists("list1"), false); + c.lpush("list1", "val1"); + ASSERT_EQUAL(c.type("list1"), redis::client::datatype_list); + ASSERT_EQUAL(c.llen("list1"), 1L); + c.lpush("list1", "val2"); + ASSERT_EQUAL(c.llen("list1"), 2L); + ASSERT_EQUAL(c.lindex("list1", 0), string("val2")); + ASSERT_EQUAL(c.lindex("list1", 1), string("val1")); + } + + test("llen"); + { + c.del("list1"); + ASSERT_EQUAL(c.exists("list1"), false); + ASSERT_EQUAL(c.llen("list1"), 0L); + c.lpush("list1", "x"); + ASSERT_EQUAL(c.llen("list1"), 1L); + c.lpush("list1", "y"); + ASSERT_EQUAL(c.llen("list1"), 2L); + } + + test("lrange"); + { + ASSERT_EQUAL(c.exists("list1"), true); + ASSERT_EQUAL(c.llen("list1"), 2L); + redis::client::string_vector vals; + ASSERT_EQUAL(c.lrange("list1", 0, -1, vals), 2L); + ASSERT_EQUAL(vals.size(), 2UL); + ASSERT_EQUAL(vals[0], string("y")); + ASSERT_EQUAL(vals[1], string("x")); + } + + test("lrange with subset of full list"); + { + ASSERT_EQUAL(c.exists("list1"), true); + ASSERT_EQUAL(c.llen("list1"), 2L); + redis::client::string_vector vals; + ASSERT_EQUAL(c.lrange("list1", 0, 1, vals), 2L); // inclusive, so entire list + ASSERT_EQUAL(vals.size(), 2UL); + ASSERT_EQUAL(vals[0], string("y")); + ASSERT_EQUAL(vals[1], string("x")); + + redis::client::string_vector vals2; + ASSERT_EQUAL(c.lrange("list1", 0, 0, vals2), 1L); // inclusive, so first item + ASSERT_EQUAL(vals2.size(), 1UL); + ASSERT_EQUAL(vals2[0], string("y")); + + redis::client::string_vector vals3; + ASSERT_EQUAL(c.lrange("list1", -1, -1, vals3), 1L); // inclusive, so first item + ASSERT_EQUAL(vals3.size(), 1UL); + ASSERT_EQUAL(vals3[0], string("x")); + } + + test("get_list"); + { + ASSERT_EQUAL(c.exists("list1"), true); + ASSERT_EQUAL(c.llen("list1"), 2L); + redis::client::string_vector vals; + ASSERT_EQUAL(c.get_list("list1", vals), 2L); + ASSERT_EQUAL(vals.size(), 2UL); + ASSERT_EQUAL(vals[0], string("y")); + ASSERT_EQUAL(vals[1], string("x")); + } + + test("ltrim"); + { + ASSERT_EQUAL(c.exists("list1"), true); + ASSERT_EQUAL(c.llen("list1"), 2L); + c.ltrim("list1", 0, 0); + ASSERT_EQUAL(c.exists("list1"), true); + ASSERT_EQUAL(c.llen("list1"), 1L); + redis::client::string_vector vals; + ASSERT_EQUAL(c.get_list("list1", vals), 1L); + ASSERT_EQUAL(vals[0], string("y")); + } + + test("lindex"); + { + ASSERT_EQUAL(c.lindex("list1", 0), string("y")); + c.rpush("list1", "x"); + ASSERT_EQUAL(c.llen("list1"), 2L); + ASSERT_EQUAL(c.lindex("list1", -1), string("x")); + ASSERT_EQUAL(c.lindex("list1", 1), string("x")); + } + + test("lset"); + { + c.lset("list1", 1, "z"); + ASSERT_EQUAL(c.lindex("list1", 1), string("z")); + ASSERT_EQUAL(c.llen("list1"), 2L); + } + + test("lrem"); + { + c.lrem("list1", 1, "z"); + ASSERT_EQUAL(c.llen("list1"), 1L); + ASSERT_EQUAL(c.lindex("list1", 0), string("y")); + + // list1 = [ y ] + ASSERT_EQUAL(c.lrem("list1", 0, "q"), 0L); + + c.rpush("list1", "z"); + c.rpush("list1", "z"); + c.rpush("list1", "z"); + c.rpush("list1", "a"); + // list1 = [ y, z, z, z, a ] + ASSERT_EQUAL(c.lrem("list1", 2, "z"), 2L); + // list1 = [ y, z, a ] + ASSERT_EQUAL(c.llen("list1"), 3L); + ASSERT_EQUAL(c.lindex("list1", 0), string("y")); + ASSERT_EQUAL(c.lindex("list1", 1), string("z")); + ASSERT_EQUAL(c.lindex("list1", 2), string("a")); + + c.rpush("list1", "z"); + // list1 = [ y, z, a, z ] + ASSERT_EQUAL(c.lrem("list1", -1, "z"), 1L); // <0 => rm R to L + // list1 = [ y, z, a ] + ASSERT_EQUAL(c.llen("list1"), 3L); + ASSERT_EQUAL(c.lindex("list1", 0), string("y")); + ASSERT_EQUAL(c.lindex("list1", 1), string("z")); + ASSERT_EQUAL(c.lindex("list1", 2), string("a")); + + // list1 = [ y, z, a ] + // try to remove 5 'a's but there's only 1 ... no problem. + ASSERT_EQUAL(c.lrem("list1", 5, "a"), 1L); + // list1 = [ y, z ] + ASSERT_EQUAL(c.llen("list1"), 2L); + ASSERT_EQUAL(c.lindex("list1", 0), string("y")); + ASSERT_EQUAL(c.lindex("list1", 1), string("z")); + } + + test("lrem_exact"); + { + // list1 = [ y, z ] + + // try to remove 5 'z's but there's only 1 ... now it's a problem. + + bool threw = false; + + try + { + c.lrem_exact("list1", 5, "z"); + } + catch (redis::value_error & e) + { + threw = true; + } + + ASSERT_EQUAL(threw, true); + + // This DOES remove the one 'z' though + // list1 = [ y ] + + ASSERT_EQUAL(c.llen("list1"), 1L); + ASSERT_EQUAL(c.lindex("list1", 0), string("y")); + } + + test("lpop"); + { + ASSERT_EQUAL(c.lpop("list1"), string("y")); + // list1 = [] + ASSERT_EQUAL(c.lpop("list1"), redis::client::missing_value); + } + + test("rpop"); + { + c.rpush("list1", "hello"); + c.rpush("list1", "world"); + ASSERT_EQUAL(c.rpop("list1"), string("world")); + ASSERT_EQUAL(c.rpop("list1"), string("hello")); + ASSERT_EQUAL(c.lpop("list1"), redis::client::missing_value); + } + + test("sadd"); + { + c.sadd("set1", "sval1"); + ASSERT_EQUAL(c.exists("set1"), true); + ASSERT_EQUAL(c.type("set1"), redis::client::datatype_set); + ASSERT_EQUAL(c.sismember("set1", "sval1"), true); + } + + test("srem"); + { + c.srem("set1", "sval1"); + ASSERT_EQUAL(c.exists("set1"), true); + ASSERT_EQUAL(c.type("set1"), redis::client::datatype_set); + ASSERT_EQUAL(c.sismember("set1", "sval1"), false); + } + + test("smove"); + { + c.sadd("set1", "hi"); + // set1 = { hi } + ASSERT_EQUAL(c.exists("set2"), false); + c.smove("set1", "set2", "hi"); + ASSERT_EQUAL(c.sismember("set1", "hi"), false); + ASSERT_EQUAL(c.sismember("set2", "hi"), true); + } + + test("scard"); + { + ASSERT_EQUAL(c.scard("set1"), 0L); + ASSERT_EQUAL(c.scard("set2"), 1L); + } + + test("sismember"); + { + // see above + } + + test("smembers"); + { + c.sadd("set2", "bye"); + redis::client::string_set members; + ASSERT_EQUAL(c.smembers("set2", members), 2L); + ASSERT_EQUAL(members.size(), 2UL); + ASSERT_NOT_EQUAL(members.find("hi"), members.end()); + ASSERT_NOT_EQUAL(members.find("bye"), members.end()); + } + + test("sinter"); + { + c.sadd("set3", "bye"); + c.sadd("set3", "bye2"); + redis::client::string_vector keys; + keys.push_back("set2"); + keys.push_back("set3"); + redis::client::string_set intersection; + ASSERT_EQUAL(c.sinter(keys, intersection), 1L); + ASSERT_EQUAL(intersection.size(), 1UL); + ASSERT_NOT_EQUAL(intersection.find("bye"), intersection.end()); + } + + test("sinterstore"); + { + c.sadd("seta", "1"); + c.sadd("seta", "2"); + c.sadd("seta", "3"); + + c.sadd("setb", "2"); + c.sadd("setb", "3"); + c.sadd("setb", "4"); + + redis::client::string_vector keys; + keys.push_back("seta"); + keys.push_back("setb"); + + c.sinterstore("setc", keys); + + redis::client::string_set members; + ASSERT_EQUAL(c.smembers("setc", members), 2L); + ASSERT_EQUAL(members.size(), 2UL); + ASSERT_NOT_EQUAL(members.find("2"), members.end()); + ASSERT_NOT_EQUAL(members.find("3"), members.end()); + } + + test("sunion"); + { + c.sadd("setd", "1"); + c.sadd("sete", "2"); + redis::client::string_vector keys; + keys.push_back("setd"); + keys.push_back("sete"); + redis::client::string_set a_union; + ASSERT_EQUAL(c.sunion(keys, a_union), 2L); + ASSERT_EQUAL(a_union.size(), 2UL); + ASSERT_NOT_EQUAL(a_union.find("1"), a_union.end()); + ASSERT_NOT_EQUAL(a_union.find("2"), a_union.end()); + } + + test("sunionstore"); + { + c.sadd("setf", "1"); + c.sadd("setg", "2"); + + redis::client::string_vector keys; + keys.push_back("setf"); + keys.push_back("setg"); + + c.sunionstore("seth", keys); + + redis::client::string_set members; + ASSERT_EQUAL(c.smembers("seth", members), 2L); + ASSERT_EQUAL(members.size(), 2UL); + ASSERT_NOT_EQUAL(members.find("1"), members.end()); + ASSERT_NOT_EQUAL(members.find("2"), members.end()); + } + + test("move"); + { + c.select(14); + ASSERT_EQUAL(c.exists("ttt"), false); + c.select(15); + c.set("ttt", "uuu"); + c.move("ttt", 14); + c.select(14); + ASSERT_EQUAL(c.exists("ttt"), true); + c.select(15); + ASSERT_EQUAL(c.exists("ttt"), false); + } + + test("move should fail since key exists already"); + { + c.select(14); + c.set("ttt", "xxx"); + c.select(15); + c.set("ttt", "uuu"); + + bool threw = false; + + try + { + c.move("ttt", 14); + } + catch (redis::protocol_error & e) + { + threw = true; + } + + ASSERT_EQUAL(threw, true); + + c.select(14); + ASSERT_EQUAL(c.exists("ttt"), true); + c.select(15); + ASSERT_EQUAL(c.exists("ttt"), true); + } + + test("sort ascending"); + { + c.sadd("sort1", "3"); + c.sadd("sort1", "2"); + c.sadd("sort1", "1"); + + redis::client::string_vector sorted; + ASSERT_EQUAL(c.sort("sort1", sorted), 3L); + ASSERT_EQUAL(sorted.size(), 3UL); + ASSERT_EQUAL(sorted[0], string("1")); + ASSERT_EQUAL(sorted[1], string("2")); + ASSERT_EQUAL(sorted[2], string("3")); + } + + test("sort descending"); + { + redis::client::string_vector sorted; + ASSERT_EQUAL(c.sort("sort1", sorted, redis::client::sort_order_descending), 3L); + ASSERT_EQUAL(sorted.size(), 3UL); + ASSERT_EQUAL(sorted[0], string("3")); + ASSERT_EQUAL(sorted[1], string("2")); + ASSERT_EQUAL(sorted[2], string("1")); + } + + test("sort with limit"); + { + // TODO + } + + test("sort lexicographically"); + { + // TODO + } + + test("sort with pattern and weights"); + { + // TODO + } + + test("save"); + { + c.save(); + } + + test("bgsave"); + { + c.bgsave(); + } + + test("lastsave"); + { + ASSERT_GT(c.lastsave(), 0L); + } + + test("shutdown"); + { +// You can test this if you really want to ... +// c.shutdown(); + } + } + catch (redis::redis_error & e) + { + cerr << "got exception: " << string(e) << endl << "FAIL" << endl; + return 1; + } + + cout << endl << "testing completed successfully" << endl; + return 0; +} diff --git a/client-libraries/ruby/.gitignore b/client-libraries/ruby/.gitignore index b33f14f7..10d0977b 100644 --- a/client-libraries/ruby/.gitignore +++ b/client-libraries/ruby/.gitignore @@ -1,3 +1,4 @@ nohup.out redis/* rdsrv +pkg/* \ No newline at end of file diff --git a/client-libraries/ruby/Rakefile b/client-libraries/ruby/Rakefile index 74b7d15b..bdc9f373 100644 --- a/client-libraries/ruby/Rakefile +++ b/client-libraries/ruby/Rakefile @@ -7,10 +7,11 @@ require 'tasks/redis.tasks' GEM = 'redis' -GEM_VERSION = '0.0.3' -AUTHORS = ['Ezra Zygmuntowicz', 'Taylor Weibley'] -EMAIL = "ez@engineyard.com" -HOMEPAGE = "http://github.com/ezmobius/redis-rb" +GEM_NAME = 'redis' +GEM_VERSION = '0.0.3.3' +AUTHORS = ['Ezra Zygmuntowicz', 'Taylor Weibley', 'Matthew Clark'] +EMAIL = "matt.clark@punchstock.com" +HOMEPAGE = "http://github.com/winescout/redis-rb" SUMMARY = "Ruby client library for redis key value storage server" spec = Gem::Specification.new do |s| diff --git a/client-libraries/ruby/bench.rb b/client-libraries/ruby/bench.rb index 88b04e72..82898d0c 100644 --- a/client-libraries/ruby/bench.rb +++ b/client-libraries/ruby/bench.rb @@ -4,12 +4,41 @@ require 'redis' times = 20000 -@r = Redis.new +@r = Redis.new#(:debug => true) @r['foo'] = "The first line we sent to the server is some text" + Benchmark.bmbm do |x| - x.report("set") { 20000.times {|i| @r["foo#{i}"] = "The first line we sent to the server is some text"; @r["foo#{i}"]} } + x.report("set") do + 20000.times do |i| + @r["set#{i}"] = "The first line we sent to the server is some text"; @r["foo#{i}"] + end + end + + x.report("set (pipelined)") do + @r.pipelined do |pipeline| + 20000.times do |i| + pipeline["set_pipelined#{i}"] = "The first line we sent to the server is some text"; @r["foo#{i}"] + end + end + end + + x.report("push+trim") do + 20000.times do |i| + @r.push_head "push_trim#{i}", i + @r.list_trim "push_trim#{i}", 0, 30 + end + end + + x.report("push+trim (pipelined)") do + @r.pipelined do |pipeline| + 20000.times do |i| + pipeline.push_head "push_trim_pipelined#{i}", i + pipeline.list_trim "push_trim_pipelined#{i}", 0, 30 + end + end + end end @r.keys('*').each do |k| @r.delete k -end \ No newline at end of file +end \ No newline at end of file diff --git a/client-libraries/ruby/lib/pipeline.rb b/client-libraries/ruby/lib/pipeline.rb new file mode 100644 index 00000000..deaedd15 --- /dev/null +++ b/client-libraries/ruby/lib/pipeline.rb @@ -0,0 +1,31 @@ +require "redis" + +class Redis + class Pipeline < Redis + BUFFER_SIZE = 50_000 + + def initialize(redis) + @redis = redis + @commands = [] + end + + def get_response + end + + def write(data) + @commands << data + write_and_read if @commands.size >= BUFFER_SIZE + end + + def finish + write_and_read + end + + def write_and_read + @redis.write @commands.join + @redis.read_socket + @commands.clear + end + + end +end \ No newline at end of file diff --git a/client-libraries/ruby/lib/redis.rb b/client-libraries/ruby/lib/redis.rb index 96b8244e..b27918bd 100644 --- a/client-libraries/ruby/lib/redis.rb +++ b/client-libraries/ruby/lib/redis.rb @@ -1,6 +1,7 @@ require 'socket' require 'set' require File.join(File.dirname(__FILE__),'server') +require File.join(File.dirname(__FILE__),'pipeline') class RedisError < StandardError @@ -22,7 +23,13 @@ class Redis @opts = {:host => 'localhost', :port => '6379', :db => 0}.merge(opts) $debug = @opts[:debug] @db = @opts[:db] - @server = Server.new(@opts[:host], @opts[:port]) + @server = Server.new(@opts[:host], @opts[:port], (@opts[:timeout]||10)) + end + + def pipelined + pipeline = Pipeline.new(self) + yield pipeline + pipeline.finish end def to_s @@ -39,9 +46,10 @@ class Redis def with_socket_management(server, &block) begin - block.call(server.socket) + socket = server.socket + block.call(socket) #Timeout or server down - rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNREFUSED => e + rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNREFUSED, Timeout::Error => e server.close puts "Client (#{server.inspect}) disconnected from server: #{e.inspect}\n" if $debug retry @@ -116,7 +124,7 @@ class Redis def bulk_reply begin - x = read.chomp + x = read puts "bulk_reply read value is #{x.inspect}" if $debug return x rescue => e @@ -260,16 +268,6 @@ class Redis get_response == OK end - def list_length(key) - write "LLEN #{key}\r\n" - case i = get_response - when -2 - raise RedisError, "key: #{key} does not hold a list value" - else - i - end - end - def list_range(key, start, ending) write "LRANGE #{key} #{start} #{ending}\r\n" get_response @@ -357,6 +355,31 @@ class Redis write "SINTERSTORE #{destkey} #{keys.join(' ')}\r\n" get_response end + + def set_union(*keys) + write "SUNION #{keys.join(' ')}\r\n" + Set.new(get_response) + end + + def set_union_store(destkey, *keys) + write "SUNIONSTORE #{destkey} #{keys.join(' ')}\r\n" + get_response + end + + def set_diff(*keys) + write "SDIFF #{keys.join(' ')}\r\n" + Set.new(get_response) + end + + def set_diff_store(destkey, *keys) + write "SDIFFSTORE #{destkey} #{keys.join(' ')}\r\n" + get_response + end + + def set_move(srckey, destkey, member) + write "SMOVE #{srckey} #{destkey} #{member.to_s.size}\r\n#{member}\r\n" + get_response == 1 + end def sort(key, opts={}) cmd = "SORT #{key}" @@ -466,6 +489,28 @@ class Redis buff[0..-3] end + def read_socket + begin + socket = @server.socket + while res = socket.read(8096) + break if res.size != 8096 + end + #Timeout or server down + rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNREFUSED => e + server.close + puts "Client (#{server.inspect}) disconnected from server: #{e.inspect}\n" if $debug + retry + rescue Timeout::Error => e + #BTM - Ignore this error so we don't go into an endless loop + puts "Client (#{server.inspect}) Timeout\n" if $debug + #Server down + rescue NoMethodError => e + puts "Client (#{server.inspect}) tryin server that is down: #{e.inspect}\n Dying!" if $debug + raise Errno::ECONNREFUSED + #exit + end + end + def read_proto with_socket_management(@server) do |socket| if res = socket.gets diff --git a/client-libraries/ruby/lib/server.rb b/client-libraries/ruby/lib/server.rb index 789ef152..c5ac808c 100644 --- a/client-libraries/ruby/lib/server.rb +++ b/client-libraries/ruby/lib/server.rb @@ -1,3 +1,24 @@ +begin + # Timeout code is courtesy of Ruby memcache-client + # http://github.com/mperham/memcache-client/tree + # Try to use the SystemTimer gem instead of Ruby's timeout library + # when running on something that looks like Ruby 1.8.x. See: + # http://ph7spot.com/articles/system_timer + # We don't want to bother trying to load SystemTimer on jruby and + # ruby 1.9+. + if defined?(JRUBY_VERSION) || (RUBY_VERSION >= '1.9') + require 'timeout' + RedisTimer = Timeout + else + require 'system_timer' + RedisTimer = SystemTimer + end +rescue LoadError => e + puts "[redis-rb] Could not load SystemTimer gem, falling back to Ruby's slower/unsafe timeout library: #{e.message}" + require 'timeout' + RedisTimer = Timeout +end + ## # This class represents a redis server instance. @@ -38,7 +59,7 @@ class Server # Create a new Redis::Server object for the redis instance # listening on the given host and port. - def initialize(host, port = DEFAULT_PORT) + def initialize(host, port = DEFAULT_PORT, timeout = 10) raise ArgumentError, "No host specified" if host.nil? or host.empty? raise ArgumentError, "No port specified" if port.nil? or port.to_i.zero? @@ -48,7 +69,7 @@ class Server @sock = nil @retry = nil @status = 'NOT CONNECTED' - @timeout = 1 + @timeout = timeout end ## @@ -83,23 +104,34 @@ class Server puts "Unable to open socket: #{err.class.name}, #{err.message}" if $debug mark_dead err end - - return @sock + @sock end def connect_to(host, port, timeout=nil) - addrs = Socket.getaddrinfo(host, nil) - addr = addrs.detect { |ad| ad[0] == 'AF_INET' } - sock = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0) + socket = TCPSocket.new(host, port, 0) if timeout - secs = Integer(timeout) - usecs = Integer((timeout - secs) * 1_000_000) - optval = [secs, usecs].pack("l_2") - sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval - sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval + socket.instance_eval <<-EOR + alias :blocking_gets :gets + def gets(*args) + RedisTimer.timeout(#{timeout}) do + self.blocking_gets(*args) + end + end + alias :blocking_read :read + def read(*args) + RedisTimer.timeout(#{timeout}) do + self.blocking_read(*args) + end + end + alias :blocking_write :write + def write(*args) + RedisTimer.timeout(#{timeout}) do + self.blocking_write(*args) + end + end + EOR end - sock.connect(Socket.pack_sockaddr_in(port, addr[3])) - sock + socket end ## diff --git a/client-libraries/ruby/redis-rb.gemspec b/client-libraries/ruby/redis-rb.gemspec index 1b79e8fc..5e284b30 100644 --- a/client-libraries/ruby/redis-rb.gemspec +++ b/client-libraries/ruby/redis-rb.gemspec @@ -1,17 +1,17 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |s| - s.name = %q{redis-rb} - s.version = "0.0.3" + s.name = %q{redis} + s.version = "0.0.3.4" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= - s.authors = ["Ezra Zygmuntowicz", "Taylor Weibley"] - s.autorequire = %q{redis} + s.authors = ["Ezra Zygmuntowicz", "Taylor Weibley", "Matthew Clark"] + #s.autorequire = %q{redis} s.date = %q{2009-03-31} s.description = %q{Ruby client library for redis key value storage server} s.email = %q{ez@engineyard.com} s.extra_rdoc_files = ["LICENSE"] - s.files = ["LICENSE", "README.markdown", "Rakefile", "lib/redis.rb", "lib/dist_redis.rb", "lib/hash_ring.rb", "lib/server.rb", "lib/better_timeout.rb", "spec/redis_spec.rb", "spec/spec_helper.rb"] + s.files = ["LICENSE", "README.markdown", "Rakefile", "lib/redis.rb", "lib/dist_redis.rb", "lib/hash_ring.rb", "lib/pipeline.rb", "lib/server.rb", "spec/redis_spec.rb", "spec/spec_helper.rb"] s.has_rdoc = true s.homepage = %q{http://github.com/winescout/redis-rb} s.require_paths = ["lib"] diff --git a/client-libraries/ruby/spec/redis_spec.rb b/client-libraries/ruby/spec/redis_spec.rb index 2cf02f18..71a63259 100644 --- a/client-libraries/ruby/spec/redis_spec.rb +++ b/client-libraries/ruby/spec/redis_spec.rb @@ -39,6 +39,19 @@ describe "redis" do @r['foo'].should == 'nik' end + it "should properly handle trailing newline characters" do + @r['foo'] = "bar\n" + @r['foo'].should == "bar\n" + end + + it "should store and retrieve all possible characters at the beginning and the end of a string" do + (0..255).each do |char_idx| + string = "#{char_idx.chr}---#{char_idx.chr}" + @r['foo'] = string + @r['foo'].should == string + end + end + it "should be able to SET a key with an expiry" do @r.set('foo', 'bar', 1) @r['foo'].should == 'bar' @@ -271,11 +284,60 @@ describe "redis" do @r.set_add "set", 'key1' @r.set_add "set", 'key2' @r.set_add "set2", 'key2' - @r.set_inter_store('newone', 'set', 'set2') + @r.set_inter_store('newone', 'set', 'set2').should == 'OK' @r.set_members('newone').should == Set.new(['key2']) @r.delete('set') end + # + it "should be able to do set union" do + @r.set_add "set", 'key1' + @r.set_add "set", 'key2' + @r.set_add "set2", 'key2' + @r.set_add "set2", 'key3' + @r.set_union('set', 'set2').should == Set.new(['key1','key2','key3']) + @r.delete('set') + end + # + it "should be able to do set union and store the results in a key" do + @r.set_add "set", 'key1' + @r.set_add "set", 'key2' + @r.set_add "set2", 'key2' + @r.set_add "set2", 'key3' + @r.set_union_store('newone', 'set', 'set2').should == 'OK' + @r.set_members('newone').should == Set.new(['key1','key2','key3']) + @r.delete('set') + end + + # these don't seem to be implemented in redis head? + # it "should be able to do set difference" do + # @r.set_add "set", 'key1' + # @r.set_add "set", 'key2' + # @r.set_add "set2", 'key2' + # @r.set_add "set2", 'key3' + # @r.set_diff('set', 'set2').should == Set.new(['key1','key3']) + # @r.delete('set') + # end + # # + # it "should be able to do set difference and store the results in a key" do + # @r.set_add "set", 'key1' + # @r.set_add "set", 'key2' + # @r.set_add "set2", 'key2' + # @r.set_add "set2", 'key3' + # count = @r.set_diff_store('newone', 'set', 'set2') + # count.should == 3 + # @r.set_members('newone').should == Set.new(['key1','key3']) + # @r.delete('set') + # end # + it "should be able move elements from one set to another" do + @r.set_add 'set1', 'a' + @r.set_add 'set1', 'b' + @r.set_add 'set2', 'x' + @r.set_move('set1', 'set2', 'a').should == true + @r.set_member?('set2', 'a').should == true + @r.delete('set1') + end + # it "should be able to do crazy SORT queries" do @r['dog_1'] = 'louie' @r.push_tail 'dogs', 1 @@ -334,4 +396,15 @@ describe "redis" do end end -end \ No newline at end of file + it "should be able to pipeline writes" do + @r.pipelined do |pipeline| + pipeline.push_head "list", "hello" + pipeline.push_head "list", 42 + end + + @r.type?('list').should == "list" + @r.list_length('list').should == 2 + @r.pop_head('list').should == '42' + @r.delete('list') + end +end diff --git a/client-libraries/update-cpp-client.sh b/client-libraries/update-cpp-client.sh new file mode 100755 index 00000000..87236f6a --- /dev/null +++ b/client-libraries/update-cpp-client.sh @@ -0,0 +1,12 @@ +#!/bin/sh +rm -rf temp +mkdir temp +cd temp +git clone git://github.com/fictorial/redis-cpp-client.git +cd redis-cpp-client +rm -rf .git +cd .. +cd .. +rm -rf cpp +mv temp/redis-cpp-client cpp +rm -rf temp diff --git a/doc/ExpireCommand.html b/doc/ExpireCommand.html new file mode 100644 index 00000000..3af6cd41 --- /dev/null +++ b/doc/ExpireCommand.html @@ -0,0 +1,58 @@ + + + + + + + +
+ + + +
+ + +

ExpireCommand

+ +
+ +
+ +
+

Expire _key_ _seconds_

+Time complexity: O(1)
Set a timeout on the specified key. After the timeout the key will beautomatically delete by the server. A key with an associated timeout issaid to be volatile in Redis terminology.
+
Voltile keys are stored on disk like the other keys, the timeout is persistenttoo like all the other aspects of the dataset. Saving a dataset containingthe dataset and stopping the server does not stop the flow of time as Redisregisters on disk when the key will no longer be available as Unix time, andnot the remaining seconds.
+

How the expire is removed from a key

When the key is set to a new value using the SET command, the INCR commandor any other command that modify the value stored at key the timeout isremoved from the key and the key becomes non volatile.
+

Restrictions with write operations against volatile keys

Write operations like LPUSH, LSET and every other command that has theeffect of modifying the value stored at a volatile key have a special semantic:basically a volatile key is destroyed when it is target of a write operation.See for example the following usage pattern:
+
+% ./redis-cli lpush mylist foobar /Users/antirez/hack/redis
+OK
+% ./redis-cli lpush mylist hello  /Users/antirez/hack/redis
+OK
+% ./redis-cli expire mylist 10000 /Users/antirez/hack/redis
+1
+% ./redis-cli lpush mylist newelement
+OK
+% ./redis-cli lrange mylist 0 -1  /Users/antirez/hack/redis
+1. newelement
+
What happened here is that lpush against the key with a timeout set deletedthe key before to perform the operation. There is so a simple rule, writeoperations against volatile keys will destroy the key before to perform theoperation. Why Redis uses this behavior? In order to retain an importantproperty: a server that receives a given number of commands in the samesequence will end with the same dataset in memory. Without the delete-on-writesemantic what happens is that the state of the server depends on the timeof the commands to. This is not a desirable property in a distributed databasethat supports replication.
+

Setting the timeout again on already volatile keys

Trying to call EXPIRE against a key that already has an associated timeoutwill not change the timeout of the key, but will just return 0. If insteadthe key does not have a timeout associated the timeout will be set and EXPIREwill return 1.
+

Return value

Integer reply, specifically:

+1: the timeout was set.
+0: the timeout was not set since the key already has an associated timeout, or the key does not exist.
+

See also

+
  • []
+
+ +
+
+ + + diff --git a/utils/redis-sha1.rb b/utils/redis-sha1.rb index af9514b2..6f9e045b 100644 --- a/utils/redis-sha1.rb +++ b/utils/redis-sha1.rb @@ -29,7 +29,7 @@ def redisSha1(opts={}) sha1 end -host = (ARGV[0] or "127.0.0.1") -port = (ARGV[1] or "6379") +host = ARGV[0] || "127.0.0.1" +port = ARGV[1] || "6379" puts "Performing SHA1 of Redis server #{host} #{port}" p "Dataset SHA1: #{redisSha1(:host => host, :port => port.to_i)}" -- 2.45.2