Commit ed9b544e authored by antirez's avatar antirez
Browse files

first commit

parents
-module(client).
-behavior(gen_server).
-export([start/1, start/2, connect/1, connect/2, asend/2, send/3, send/2,
disconnect/1, ssend/3, str/1, format/1, sformat/1, ssend/2,
get_all_results/1]).
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3]).
-include("erldis.hrl").
-define(EOL, "\r\n").
%% Helpers
str(X) when is_list(X) ->
X;
str(X) when is_atom(X) ->
atom_to_list(X);
str(X) when is_binary(X) ->
binary_to_list(X);
str(X) when is_integer(X) ->
integer_to_list(X);
str(X) when is_float(X) ->
float_to_list(X).
format([], Result) ->
string:join(lists:reverse(Result), ?EOL);
format([Line|Rest], Result) ->
JoinedLine = string:join([str(X) || X <- Line], " "),
format(Rest, [JoinedLine|Result]).
format(Lines) ->
format(Lines, []).
sformat(Line) ->
format([Line], []).
get_parser(Cmd)
when Cmd =:= set orelse Cmd =:= setnx orelse Cmd =:= del
orelse Cmd =:= exists orelse Cmd =:= rename orelse Cmd =:= renamenx
orelse Cmd =:= rpush orelse Cmd =:= lpush orelse Cmd =:= ltrim
orelse Cmd =:= lset orelse Cmd =:= sadd orelse Cmd =:= srem
orelse Cmd =:= sismember orelse Cmd =:= select orelse Cmd =:= move
orelse Cmd =:= save orelse Cmd =:= bgsave orelse Cmd =:= flushdb
orelse Cmd =:= flushall ->
fun proto:parse/2;
get_parser(Cmd) when Cmd =:= lrem ->
fun proto:parse_special/2;
get_parser(Cmd)
when Cmd =:= incr orelse Cmd =:= incrby orelse Cmd =:= decr
orelse Cmd =:= decrby orelse Cmd =:= llen orelse Cmd =:= scard ->
fun proto:parse_int/2;
get_parser(Cmd) when Cmd =:= type ->
fun proto:parse_types/2;
get_parser(Cmd) when Cmd =:= randomkey ->
fun proto:parse_string/2;
get_parser(Cmd)
when Cmd =:= get orelse Cmd =:= lindex orelse Cmd =:= lpop
orelse Cmd =:= rpop ->
fun proto:single_stateful_parser/2;
get_parser(Cmd)
when Cmd =:= keys orelse Cmd =:= lrange orelse Cmd =:= sinter
orelse Cmd =:= smembers orelse Cmd =:= sort ->
fun proto:stateful_parser/2.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Exported API
start(Host) ->
connect(Host).
start(Host, Port) ->
connect(Host, Port).
connect(Host) ->
connect(Host, 6379).
connect(Host, Port) ->
gen_server:start_link(?MODULE, [Host, Port], []).
ssend(Client, Cmd) -> ssend(Client, Cmd, []).
ssend(Client, Cmd, Args) ->
gen_server:cast(Client, {send, sformat([Cmd|Args]), get_parser(Cmd)}).
send(Client, Cmd) -> send(Client, Cmd, []).
send(Client, Cmd, Args) ->
gen_server:cast(Client, {send,
string:join([str(Cmd), format(Args)], " "), get_parser(Cmd)}).
asend(Client, Cmd) ->
gen_server:cast(Client, {asend, Cmd}).
disconnect(Client) ->
gen_server:call(Client, disconnect).
get_all_results(Client) ->
gen_server:call(Client, get_all_results).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% gen_server callbacks
init([Host, Port]) ->
process_flag(trap_exit, true),
ConnectOptions = [list, {active, once}, {packet, line}, {nodelay, true}],
case gen_tcp:connect(Host, Port, ConnectOptions) of
{error, Why} ->
{error, {socket_error, Why}};
{ok, Socket} ->
{ok, #redis{socket=Socket, parsers=queue:new()}}
end.
handle_call({send, Cmd, Parser}, From, State=#redis{parsers=Parsers}) ->
gen_tcp:send(State#redis.socket, [Cmd|?EOL]),
{noreply, State#redis{reply_caller=fun(V) -> gen_server:reply(From, lists:nth(1, V)) end,
parsers=queue:in(Parser, Parsers), remaining=1}};
handle_call(disconnect, _From, State) ->
{stop, normal, ok, State};
handle_call(get_all_results, From, State) ->
case queue:is_empty(State#redis.parsers) of
true ->
% answers came earlier than we could start listening...
% Very unlikely but totally possible.
{reply, lists:reverse(State#redis.results), State#redis{results=[]}};
false ->
% We are here earlier than results came, so just make
% ourselves wait until stuff is ready.
{noreply, State#redis{reply_caller=fun(V) -> gen_server:reply(From, V) end}}
end;
handle_call(_, _From, State) -> {noreply, State}.
handle_cast({asend, Cmd}, State) ->
gen_tcp:send(State#redis.socket, [Cmd|?EOL]),
{noreply, State};
handle_cast({send, Cmd, Parser}, State=#redis{parsers=Parsers, remaining=Remaining}) ->
% how we should do here: if remaining is already != 0 then we'll
% let handle_info take care of keeping track how many remaining things
% there are. If instead it's 0 we are the first call so let's just
% do it.
gen_tcp:send(State#redis.socket, [Cmd|?EOL]),
NewParsers = queue:in(Parser, Parsers),
case Remaining of
0 ->
{noreply, State#redis{remaining=1, parsers=NewParsers}};
_ ->
{noreply, State#redis{parsers=NewParsers}}
end;
handle_cast(_Msg, State) -> {noreply, State}.
trim2({ok, S}) ->
string:substr(S, 1, length(S)-2);
trim2(S) ->
trim2({ok, S}).
% This is useful to know if there are more messages still coming.
get_remaining(ParsersQueue) ->
case queue:is_empty(ParsersQueue) of
true -> 0;
false -> 1
end.
% This function helps with pipelining by creating a pubsub system with
% the caller. The caller could submit multiple requests and not listen
% until later when all or some of them have been answered, at that
% point 2 conditions can be true:
% 1) We still need to process more things in this response chain
% 2) We are finished.
%
% And these 2 are together with the following 2:
% 1) We called get_all_results before the end of the responses.
% 2) We called get_all_results after the end of the responses.
%
% If there's stuff missing in the chain we just push results, this also
% happens when there's nothing more to process BUT we haven't requested
% results yet.
% In case we have requested results: if requests are not yet ready we
% just push them, otherwise we finally answer all of them.
save_or_reply(Result, State=#redis{results=Results, reply_caller=ReplyCaller, parsers=Parsers}) ->
case get_remaining(Parsers) of
1 ->
State#redis{results=[Result|Results], remaining=1, pstate=empty, buffer=[]};
0 ->
% We don't reverse results here because if all the requests
% come in and then we submit another one, if we reverse
% they will be scrambled in the results field of the record.
% instead if we wait just before we reply they will be
% in the right order.
FullResults = [Result|Results],
NewState = case ReplyCaller of
undefined ->
State#redis{results=FullResults};
_ ->
ReplyCaller(lists:reverse(FullResults)),
State#redis{results=[]}
end,
NewState#redis{remaining=0, pstate=empty,
reply_caller=undefined, buffer=[],
parsers=Parsers}
end.
handle_info({tcp, Socket, Data}, State) ->
{{value, Parser}, NewParsers} = queue:out(State#redis.parsers),
Trimmed = trim2(Data),
NewState = case {State#redis.remaining-1, Parser(State#redis.pstate, Trimmed)} of
% This line contained an error code. Next line will hold
% The error message that we will parse.
{0, error} ->
% reinsert the parser in the front, next step is still gonna be needed
State#redis{remaining=1, pstate=error,
parsers=queue:in_r(Parser, NewParsers)};
% The stateful parser just started and tells us the number
% of results that we will have to parse for those calls
% where more than one result is expected. The next
% line will start with the first item to read.
{0, {hold, Remaining}} ->
% Reset the remaining value to the number of results
% that we need to parse.
% and reinsert the parser in the front, next step is still gonna be needed
State#redis{remaining=Remaining, pstate=read,
parsers=queue:in_r(Parser, NewParsers)};
% We either had only one thing to read or we are at the
% end of the stuff that we need to read. either way
% just pack up the buffer and send.
{0, {read, NBytes}} ->
inet:setopts(Socket, [{packet, 0}]), % go into raw mode to read bytes
CurrentValue = trim2(gen_tcp:recv(Socket, NBytes+2)), % also consume the \r\n
inet:setopts(Socket, [{packet, line}]), % go back to line mode
OldBuffer = State#redis.buffer,
case OldBuffer of
[] ->
save_or_reply(CurrentValue, State#redis{parsers=NewParsers});
_ ->
save_or_reply(lists:reverse([CurrentValue|OldBuffer]), State#redis{parsers=NewParsers})
end;
% The stateful parser tells us to read some bytes
{N, {read, NBytes}} ->
inet:setopts(Socket, [{packet, 0}]), % go into raw mode to read bytes
CurrentValue = trim2(gen_tcp:recv(Socket, NBytes+2)), % also consume the \r\n
inet:setopts(Socket, [{packet, line}]), % go back to line mode
OldBuffer = State#redis.buffer,
State#redis{remaining=N, buffer=[CurrentValue|OldBuffer],
pstate=read, parsers=queue:in_r(Parser, NewParsers)};
% Simple return values contained in a single line
{0, Value} ->
save_or_reply(Value, State#redis{parsers=NewParsers})
end,
inet:setopts(Socket, [{active, once}]),
{noreply, NewState};
handle_info(_Info, State) -> {noreply, State}.
terminate(_Reason, State) ->
case State#redis.socket of
undefined ->
pass;
Socket ->
gen_tcp:close(Socket)
end,
ok.
code_change(_OldVsn, State, _Extra) -> {ok, State}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-module(erldis).
-compile(export_all).
-define(EOL, "\r\n").
%% helpers
flatten({error, Message}) ->
{error, Message};
flatten(List) when is_list(List)->
lists:flatten(List).
%% exposed API
connect(Host) ->
client:connect(Host).
quit(Client) ->
client:asend(Client, "QUIT"),
client:disconnect(Client).
%% Commands operating on string values
internal_set_like(Client, Command, Key, Value) ->
client:send(Client, Command, [[Key, length(Value)],
[Value]]).
get_all_results(Client) -> client:get_all_results(Client).
set(Client, Key, Value) -> internal_set_like(Client, set, Key, Value).
setnx(Client, Key, Value) -> internal_set_like(Client, setnx, Key, Value).
incr(Client, Key) -> client:ssend(Client, incr, [Key]).
incrby(Client, Key, By) -> client:ssend(Client, incrby, [Key, By]).
decr(Client, Key) -> client:ssend(Client, decr, [Key]).
decrby(Client, Key, By) -> client:ssend(Client, decrby, [Key, By]).
get(Client, Key) -> client:ssend(Client, get, [Key]).
%% Commands operating on every value
exists(Client, Key) -> client:ssend(Client, exists, [Key]).
del(Client, Key) -> client:ssend(Client, del, [Key]).
type(Client, Key) -> client:ssend(Client, type, [Key]).
keys(Client, Pattern) -> client:ssend(Client, keys, [Pattern]).
randomkey(Client, Key) -> client:ssend(Client, randomkey, [Key]).
rename(Client, OldKey, NewKey) -> client:ssend(Client, rename, [OldKey, NewKey]).
renamenx(Client, OldKey, NewKey) -> client:ssend(Client, renamenx, [OldKey, NewKey]).
%% Commands operating on both lists and sets
sort(Client, Key) -> client:ssend(Client, sort, [Key]).
sort(Client, Key, Extra) -> client:ssend(Client, sort, [Key, Extra]).
%% Commands operating on lists
rpush(Client, Key, Value) -> internal_set_like(Client, rpush, Key, Value).
lpush(Client, Key, Value) -> internal_set_like(Client, lpush, Key, Value).
llen(Client, Key) -> client:ssend(Client, llen, [Key]).
lrange(Client, Key, Start, End) -> client:ssend(Client, lrange, [Key, Start, End]).
ltrim(Client, Key, Start, End) -> client:ssend(Client, ltrim, [Key, Start, End]).
lindex(Client, Key, Index) -> client:ssend(Client, lindex, [Key, Index]).
lpop(Client, Key) -> client:ssend(Client, lpop, [Key]).
rpop(Client, Key) -> client:ssend(Client, rpop, [Key]).
lrem(Client, Key, Number, Value) ->
client:send(Client, lrem, [[Key, Number, length(Value)],
[Value]]).
lset(Client, Key, Index, Value) ->
client:send(Client, lset, [[Key, Index, length(Value)],
[Value]]).
%% Commands operating on sets
sadd(Client, Key, Value) -> internal_set_like(Client, sadd, Key, Value).
srem(Client, Key, Value) -> internal_set_like(Client, srem, Key, Value).
scard(Client, Key) -> client:ssend(Client, scard, [Key]).
sismember(Client, Key, Value) -> internal_set_like(Client, sismember, Key, Value).
sintersect(Client, Keys) -> client:ssend(Client, sinter, Keys).
smembers(Client, Key) -> client:ssend(Client, smembers, [Key]).
%% Multiple DB commands
flushdb(Client) -> client:ssend(Client, flushdb).
flushall(Client) -> client:ssend(Client, flushall).
select(Client, Index) -> client:ssend(Client, select, [Index]).
move(Client, Key, DBIndex) -> client:ssend(Client, move, [Key, DBIndex]).
save(Client) -> client:ssend(Client, save).
bgsave(Client) -> client:ssend(Client, bgsave).
lastsave(Client) -> client:ssend(Client, lastsave).
shutdown(Client) -> client:asend(Client, shutdown).
-module(proto).
-export([parse/2, parse_int/2, parse_types/2,
parse_string/2, stateful_parser/2,
single_stateful_parser/2, parse_special/2]).
parse(empty, "+OK") ->
ok;
parse(empty, "+PONG") ->
pong;
parse(empty, "0") ->
false;
parse(empty, "1") ->
true;
parse(empty, "-1") ->
{error, no_such_key};
parse(empty, "-2") ->
{error, wrong_type};
parse(empty, "-3") ->
{error, same_db};
parse(empty, "-4") ->
{error, argument_out_of_range};
parse(empty, "-" ++ Message) ->
{error, Message}.
parse_special(empty, "-1") ->
parse(empty, "-1");
parse_special(empty, "-2") ->
parse(empty, "-2");
parse_special(empty, N) ->
list_to_integer(N).
parse_int(empty, "-ERR " ++ Message) ->
{error, Message};
parse_int(empty, Value) ->
list_to_integer(Value).
parse_string(empty, Message) ->
Message.
parse_types(empty, "none") -> none;
parse_types(empty, "string") -> string;
parse_types(empty, "list") -> list;
parse_types(empty, "set") -> set.
% I'm used when redis returns multiple results
stateful_parser(empty, "nil") ->
nil;
stateful_parser(error, "-ERR " ++ Error) ->
{error, Error};
stateful_parser(empty, "-" ++ _ErrorLength) ->
error;
stateful_parser(empty, NumberOfElements) ->
{hold, list_to_integer(NumberOfElements)};
stateful_parser(read, ElementSize) ->
{read, list_to_integer(ElementSize)}.
% I'm used when redis returns just one result
single_stateful_parser(empty, "nil") ->
nil;
single_stateful_parser(error, "-ERR " ++ Error) ->
{error, Error};
single_stateful_parser(empty, "-" ++ _ErrorLength) ->
error;
single_stateful_parser(empty, ElementSize) ->
{read, list_to_integer(ElementSize)}.
## -*- makefile -*-
ERL := erl
ERLC := $(ERL)c
INCLUDE_DIRS := ../include $(wildcard ../deps/*/include)
EBIN_DIRS := $(wildcard ../deps/*/ebin)
ERLC_FLAGS := -W $(INCLUDE_DIRS:../%=-I ../%) $(EBIN_DIRS:%=-pa %)
ifndef no_debug_info
ERLC_FLAGS += +debug_info
endif
ifdef debug
ERLC_FLAGS += -Ddebug
endif
ifdef test
ERLC_FLAGS += -DTEST
endif
EBIN_DIR := ../ebin
DOC_DIR := ../doc
EMULATOR := beam
ERL_TEMPLATE := $(wildcard *.et)
ERL_SOURCES := $(wildcard *.erl)
ERL_HEADERS := $(wildcard *.hrl) $(wildcard ../include/*.hrl)
ERL_OBJECTS := $(ERL_SOURCES:%.erl=$(EBIN_DIR)/%.beam)
ERL_TEMPLATES := $(ERL_TEMPLATE:%.et=$(EBIN_DIR)/%.beam)
ERL_OBJECTS_LOCAL := $(ERL_SOURCES:%.erl=./%.$(EMULATOR))
APP_FILES := $(wildcard *.app)
EBIN_FILES = $(ERL_OBJECTS) $(APP_FILES:%.app=../ebin/%.app) $(ERL_TEMPLATES)
MODULES = $(ERL_SOURCES:%.erl=%)
../ebin/%.app: %.app
cp $< $@
$(EBIN_DIR)/%.$(EMULATOR): %.erl
$(ERLC) $(ERLC_FLAGS) -o $(EBIN_DIR) $<
$(EBIN_DIR)/%.$(EMULATOR): %.et
$(ERL) -noshell -pa ../../elib/erltl/ebin/ -eval "erltl:compile(atom_to_list('$<'), [{outdir, \"../ebin\"}, report_errors, report_warnings, nowarn_unused_vars])." -s init stop
./%.$(EMULATOR): %.erl
$(ERLC) $(ERLC_FLAGS) -o . $<
$(DOC_DIR)/%.html: %.erl
$(ERL) -noshell -run edoc file $< -run init stop
mv *.html $(DOC_DIR)
include ../support/include.mk
all: $(EBIN_FILES)
clean:
rm -rf $(EBIN_FILES) erl_crash.dump
test: $(MODULES)
./$(MODULES):
@echo "Running tests for $@"
erl -pa ../ebin -run $@ test -run init stop -noshell
-module(erldis_tests).
-include_lib("eunit/include/eunit.hrl").
-include("erldis.hrl").
quit_test() ->
{ok, Client} = erldis:connect("localhost"),
ok = erldis:quit(Client),
false = is_process_alive(Client).
utils_test() ->
?assertEqual(client:str(1), "1"),
?assertEqual(client:str(atom), "atom"),
?assertEqual(client:format([[1, 2, 3]]), "1 2 3"),
?assertEqual(client:format([[1,2,3], [4,5,6]]), "1 2 3\r\n4 5 6").
pipeline_test() ->
{ok, Client} = erldis:connect("localhost"),
erldis:flushall(Client),
erldis:get(Client, "pippo"),
erldis:set(Client, "hello", "kitty!"),
erldis:setnx(Client, "foo", "bar"),
erldis:setnx(Client, "foo", "bar"),
[ok, nil, ok, true, false] = erldis:get_all_results(Client),
erldis:exists(Client, "hello"),
erldis:exists(Client, "foo"),
erldis:get(Client, "foo"),
erldis:del(Client, "hello"),
erldis:del(Client, "foo"),
erldis:exists(Client, "hello"),
erldis:exists(Client, "foo"),
[true, true, "bar", true, true, false, false] = erldis:get_all_results(Client),
erldis:set(Client, "pippo", "pluto"),
erldis:sadd(Client, "pippo", "paperino"),
% foo doesn't exist, the result will be nil
erldis:lrange(Client, "foo", 1, 2),
erldis:lrange(Client, "pippo", 1, 2),
[ok, {error, wrong_type}, nil,
{error, "Operation against a key holding the wrong kind of value"}
] = erldis:get_all_results(Client),
erldis:del(Client, "pippo"),
[true] = erldis:get_all_results(Client),
erldis:rpush(Client, "a_list", "1"),
erldis:rpush(Client, "a_list", "2"),
erldis:rpush(Client, "a_list", "3"),
erldis:rpush(Client, "a_list", "1"),
erldis:lrem(Client, "a_list", 1, "1"),
erldis:lrange(Client, "a_list", 0, 2),
[ok, ok, ok, ok, 1, ["2", "3", "1"]] = erldis:get_all_results(Client),
erldis:sort(Client, "a_list"),
erldis:sort(Client, "a_list", "DESC"),
erldis:lrange(Client, "a_list", 0, 2),
erldis:sort(Client, "a_list", "LIMIT 0 2 ASC"),
[["1", "2", "3"], ["3", "2", "1"], ["2", "3", "1"],
["1", "2"]] = erldis:get_all_results(Client),
ok = erldis:quit(Client).
% inline_tests(Client) ->
% [?_assertMatch(ok, erldis:set(Client, "hello", "kitty!")),
% ?_assertMatch(false, erldis:setnx(Client, "hello", "kitty!")),
% ?_assertMatch(true, erldis:exists(Client, "hello")),
% ?_assertMatch(true, erldis:del(Client, "hello")),
% ?_assertMatch(false, erldis:exists(Client, "hello")),
%
% ?_assertMatch(true, erldis:setnx(Client, "hello", "kitty!")),
% ?_assertMatch(true, erldis:exists(Client, "hello")),
% ?_assertMatch("kitty!", erldis:get(Client, "hello")),
% ?_assertMatch(true, erldis:del(Client, "hello")),
%
%
% ?_assertMatch(1, erldis:incr(Client, "pippo"))
% ,?_assertMatch(2, erldis:incr(Client, "pippo"))
% ,?_assertMatch(1, erldis:decr(Client, "pippo"))
% ,?_assertMatch(0, erldis:decr(Client, "pippo"))
% ,?_assertMatch(-1, erldis:decr(Client, "pippo"))
%
% ,?_assertMatch(6, erldis:incrby(Client, "pippo", 7))
% ,?_assertMatch(2, erldis:decrby(Client, "pippo", 4))
% ,?_assertMatch(-2, erldis:decrby(Client, "pippo", 4))
% ,?_assertMatch(true, erldis:del(Client, "pippo"))
% ].
-module(proto_tests).
-include_lib("eunit/include/eunit.hrl").
parse_test() ->
ok = proto:parse(empty, "+OK"),
pong = proto:parse(empty, "+PONG"),
false = proto:parse(empty, "0"),
true = proto:parse(empty, "1"),
{error, no_such_key} = proto:parse(empty, "-1").
<?php
/*******************************************************************************
* Redis PHP Bindings - http://code.google.com/p/redis/
*
* Copyright 2009 Ludovico Magnocavallo
* Released under the same license as Redis.
*
* Version: 0.1
*
* $Revision: 139 $
* $Date: 2009-03-15 22:59:40 +0100 (Dom, 15 Mar 2009) $
*
******************************************************************************/
class Redis {
var $server;
var $port;
var $_sock;
function Redis($host, $port=6379) {
$this->host = $host;
$this->port = $port;
}
function connect() {
if ($this->_sock)
return;
if ($sock = fsockopen($this->host, $this->port, $errno, $errstr)) {
$this->_sock = $sock;
return;
}
$msg = "Cannot open socket to {$this->host}:{$this->port}";
if ($errno || $errmsg)
$msg .= "," . ($errno ? " error $errno" : "") . ($errmsg ? " $errmsg" : "");
trigger_error("$msg.", E_USER_ERROR);
}
function disconnect() {
if ($this->_sock)
@fclose($this->_sock);
$this->_sock = null;
}
function &ping() {
$this->connect();
$this->_write("PING\r\n");
return $this->_simple_response();
}
function &do_echo($s) {
$this->connect();
$this->_write("ECHO " . strlen($s) . "\r\n$s\r\n");
return $this->_get_value();
}
function &set($name, $value, $preserve=false) {
$this->connect();
$this->_write(
($preserve ? 'SETNX' : 'SET') .
" $name " . strlen($value) . "\r\n$value\r\n"
);
return $preserve ? $this->_numeric_response() : $this->_simple_response();
}
function &get($name) {
$this->connect();
$this->_write("GET $name\r\n");
return $this->_get_value();
}
function &incr($name, $amount=1) {
$this->connect();
if ($amount == 1)
$this->_write("INCR $name\r\n");
else
$this->_write("INCRBY $name $amount\r\n");
return $this->_numeric_response();
}
function &decr($name, $amount=1) {
$this->connect();
if ($amount == 1)
$this->_write("DECR $name\r\n");
else
$this->_write("DECRBY $name $amount\r\n");
return $this->_numeric_response();
}
function &exists($name) {
$this->connect();
$this->_write("EXISTS $name\r\n");
return $this->_numeric_response();
}
function &delete($name) {
$this->connect();
$this->_write("DEL $name\r\n");
return $this->_numeric_response();
}
function &keys($pattern) {
$this->connect();
$this->_write("KEYS $pattern\r\n");
return explode(' ', $this->_get_value());
}
function &randomkey() {
$this->connect();
$this->_write("RANDOMKEY\r\n");
$s =& trim($this->_read());
$this->_check_for_error($s);
return $s;
}
function &rename($src, $dst, $preserve=False) {
$this->connect();
if ($preserve) {
$this->_write("RENAMENX $src $dst\r\n");
return $this->_numeric_response();
}
$this->_write("RENAME $src $dst\r\n");
return trim($this->_simple_response());
}
function &push($name, $value, $tail=true) {
// default is to append the element to the list
$this->connect();
$this->_write(
($tail ? 'RPUSH' : 'LPUSH') .
" $name " . strlen($value) . "\r\n$value\r\n"
);
return $this->_simple_response();
}
function &ltrim($name, $start, $end) {
$this->connect();
$this->_write("LTRIM $name $start $end\r\n");
return $this->_simple_response();
}
function &lindex($name, $index) {
$this->connect();
$this->_write("LINDEX $name $index\r\n");
return $this->_get_value();
}
function &pop($name, $tail=true) {
$this->connect();
$this->_write(
($tail ? 'RPOP' : 'LPOP') .
" $name\r\n"
);
return $this->_get_value();
}
function &llen($name) {
$this->connect();
$this->_write("LLEN $name\r\n");
return $this->_numeric_response();
}
function &lrange($name, $start, $end) {
$this->connect();
$this->_write("LRANGE $name $start $end\r\n");
return $this->_get_multi();
}
function &sort($name, $query=false) {
$this->connect();
if ($query === false) {
$this->_write("SORT $name\r\n");
} else {
$this->_write("SORT $name $query\r\n");
}
return $this->_get_multi();
}
function &lset($name, $value, $index) {
$this->connect();
$this->_write("LSET $name $index " . strlen($value) . "\r\n$value\r\n");
return $this->_simple_response();
}
function &sadd($name, $value) {
$this->connect();
$this->_write("SADD $name " . strlen($value) . "\r\n$value\r\n");
return $this->_numeric_response();
}
function &srem($name, $value) {
$this->connect();
$this->_write("SREM $name " . strlen($value) . "\r\n$value\r\n");
return $this->_numeric_response();
}
function &sismember($name, $value) {
$this->connect();
$this->_write("SISMEMBER $name " . strlen($value) . "\r\n$value\r\n");
return $this->_numeric_response();
}
function &sinter($sets) {
$this->connect();
$this->_write('SINTER ' . implode(' ', $sets) . "\r\n");
return $this->_get_multi();
}
function &smembers($name) {
$this->connect();
$this->_write("SMEMBERS $name\r\n");
return $this->_get_multi();
}
function &scard($name) {
$this->connect();
$this->_write("SCARD $name\r\n");
return $this->_numeric_response();
}
function &select_db($name) {
$this->connect();
$this->_write("SELECT $name\r\n");
return $this->_simple_response();
}
function &move($name, $db) {
$this->connect();
$this->_write("MOVE $name $db\r\n");
return $this->_numeric_response();
}
function &save($background=false) {
$this->connect();
$this->_write(($background ? "BGSAVE\r\n" : "SAVE\r\n"));
return $this->_simple_response();
}
function &lastsave() {
$this->connect();
$this->_write("LASTSAVE\r\n");
return $this->_numeric_response();
}
function &_write($s) {
while ($s) {
$i = fwrite($this->_sock, $s);
if ($i == 0)
break;
$s = substr($s, $i);
}
}
function &_read($len=1024) {
if ($s = fgets($this->_sock))
return $s;
$this->disconnect();
trigger_error("Cannot read from socket.", E_USER_ERROR);
}
function _check_for_error(&$s) {
if (!$s || $s[0] != '-')
return;
if (substr($s, 0, 4) == '-ERR')
trigger_error("Redis error: " . trim(substr($s, 4)), E_USER_ERROR);
trigger_error("Redis error: " . substr(trim($this->_read()), 5), E_USER_ERROR);
}
function &_simple_response() {
$s =& trim($this->_read());
if ($s[0] == '+')
return substr($s, 1);
if ($err =& $this->_check_for_error($s))
return $err;
trigger_error("Cannot parse first line '$s' for a simple response", E_USER_ERROR);
}
function &_numeric_response($allow_negative=True) {
$s =& trim($this->_read());
$i = (int)$s;
if ($i . '' == $s) {
if (!$allow_negative && $i < 0)
$this->_check_for_error($s);
return $i;
}
if ($s == 'nil')
return null;
trigger_error("Cannot parse '$s' as numeric response.");
}
function &_get_value() {
$s =& trim($this->_read());
if ($s == 'nil')
return '';
else if ($s[0] == '-')
$this->_check_for_error($s);
$i = (int)$s;
if ($i . '' != $s)
trigger_error("Cannot parse '$s' as data length.");
$buffer = '';
while ($i > 0) {
$s = $this->_read();
$l = strlen($s);
$i -= $l;
if ($l > $i) // ending crlf
$s = rtrim($s);
$buffer .= $s;
}
if ($i == 0) // let's restore the trailing crlf
$buffer .= $this->_read();
return $buffer;
}
function &_get_multi() {
$results = array();
$num =& $this->_numeric_response(false);
if ($num === false)
return $results;
while ($num) {
$results[] =& $this->_get_value();
$num -= 1;
}
return $results;
}
}
?>
<?php
// poor man's tests
require_once('redis.php');
$r =& new Redis('localhost');
$r->connect();
echo $r->ping() . "\n";
echo $r->do_echo('ECHO test') . "\n";
echo "SET aaa " . $r->set('aaa', 'bbb') . "\n";
echo "SETNX aaa " . $r->set('aaa', 'ccc', true) . "\n";
echo "GET aaa " . $r->get('aaa') . "\n";
echo "INCR aaa " . $r->incr('aaa') . "\n";
echo "GET aaa " . $r->get('aaa') . "\n";
echo "INCRBY aaa 3 " . $r->incr('aaa', 2) . "\n";
echo "GET aaa " . $r->get('aaa') . "\n";
echo "DECR aaa " . $r->decr('aaa') . "\n";
echo "GET aaa " . $r->get('aaa') . "\n";
echo "DECRBY aaa 2 " . $r->decr('aaa', 2) . "\n";
echo "GET aaa " . $r->get('aaa') . "\n";
echo "EXISTS aaa " . $r->exists('aaa') . "\n";
echo "EXISTS fsfjslfjkls " . $r->exists('fsfjslfjkls') . "\n";
echo "DELETE aaa " . $r->delete('aaa') . "\n";
echo "EXISTS aaa " . $r->exists('aaa') . "\n";
echo 'SET a1 a2 a3' . $r->set('a1', 'a') . $r->set('a2', 'b') . $r->set('a3', 'c') . "\n";
echo 'KEYS a* ' . print_r($r->keys('a*'), true) . "\n";
echo 'RANDOMKEY ' . $r->randomkey('a*') . "\n";
echo 'RENAME a1 a0 ' . $r->rename('a1', 'a0') . "\n";
echo 'RENAMENX a0 a2 ' . $r->rename('a0', 'a2', true) . "\n";
echo 'RENAMENX a0 a1 ' . $r->rename('a0', 'a1', true) . "\n";
echo 'LPUSH a0 aaa ' . $r->push('a0', 'aaa') . "\n";
echo 'LPUSH a0 bbb ' . $r->push('a0', 'bbb') . "\n";
echo 'RPUSH a0 ccc ' . $r->push('a0', 'ccc', false) . "\n";
echo 'LLEN a0 ' . $r->llen('a0') . "\n";
echo 'LRANGE sdkjhfskdjfh 0 100 ' . print_r($r->lrange('sdkjhfskdjfh', 0, 100), true) . "\n";
echo 'LRANGE a0 0 0 ' . print_r($r->lrange('sdkjhfskdjfh', 0, 0), true) . "\n";
echo 'LRANGE a0 0 100 ' . print_r($r->lrange('a0', 0, 100), true) . "\n";
echo 'LTRIM a0 0 1 ' . $r->ltrim('a0', 0, 1) . "\n";
echo 'LRANGE a0 0 100 ' . print_r($r->lrange('a0', 0, 100), true) . "\n";
echo 'LINDEX a0 0 ' . $r->lindex('a0', 0) . "\n";
echo 'LPUSH a0 bbb ' . $r->push('a0', 'bbb') . "\n";
echo 'LRANGE a0 0 100 ' . print_r($r->lrange('a0', 0, 100), true) . "\n";
echo 'RPOP a0 ' . $r->pop('a0') . "\n";
echo 'LPOP a0 ' . $r->pop('a0', false) . "\n";
echo 'LSET a0 ccc 0 ' . $r->lset('a0', 'ccc', 0) . "\n";
echo 'LRANGE a0 0 100 ' . print_r($r->lrange('a0', 0, 100), true) . "\n";
echo 'SADD s0 aaa ' . $r->sadd('s0', 'aaa') . "\n";
echo 'SADD s0 aaa ' . $r->sadd('s0', 'aaa') . "\n";
echo 'SADD s0 bbb ' . $r->sadd('s0', 'bbb') . "\n";
echo 'SREM s0 bbb ' . $r->srem('s0', 'bbb') . "\n";
echo 'SISMEMBER s0 aaa ' . $r->sismember('s0', 'aaa') . "\n";
echo 'SISMEMBER s0 bbb ' . $r->sismember('s0', 'bbb') . "\n";
echo 'SADD s0 bbb ' . $r->sadd('s0', 'bbb') . "\n";
echo 'SADD s1 bbb ' . $r->sadd('s1', 'bbb') . "\n";
echo 'SADD s1 aaa ' . $r->sadd('s1', 'aaa') . "\n";
echo 'SINTER s0 s1 ' . print_r($r->sinter(array('s0', 's1')), true) . "\n";
echo 'SREM s0 bbb ' . $r->srem('s0', 'bbb') . "\n";
echo 'SINTER s0 s1 ' . print_r($r->sinter(array('s0', 's1')), true) . "\n";
echo 'SMEMBERS s1 ' . print_r($r->smembers('s1'), true) . "\n";
echo 'SELECT 1 ' . $r->select_db(1) . "\n";
echo 'SMEMBERS s1 ' . print_r($r->smembers('s1'), true) . "\n";
echo 'SELECT 0 ' . $r->select_db(0) . "\n";
echo 'SMEMBERS s1 ' . print_r($r->smembers('s1'), true) . "\n";
echo 'MOVE s1 1 ' . $r->move('s1', 1) . "\n";
echo 'SMEMBERS s1 ' . print_r($r->smembers('s1'), true) . "\n";
echo 'SELECT 1 ' . $r->select_db(1) . "\n";
echo 'SMEMBERS s1 ' . print_r($r->smembers('s1'), true) . "\n";
echo 'SELECT 0 ' . $r->select_db(0) . "\n";
echo 'SAVE ' . $r->save() . "\n";
echo 'BGSAVE ' . $r->save(true) . "\n";
echo 'LASTSAVE ' . $r->lastsave() . "\n";
?>
\ No newline at end of file
This diff is collapsed.
Copyright (c) 2009 Ezra Zygmuntowicz
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
# redis-rb
A ruby client library for the redis key value storage system.
## Information about redis
Redis is a key value store with some interesting features:
1. It's fast.
2. Keys are strings but values can have types of "NONE", "STRING", "LIST", or "SET". List's can be atomically push'd, pop'd, lpush'd, lpop'd and indexed. This allows you to store things like lists of comments under one key while retaining the ability to append comments without reading and putting back the whole list.
See [redis on code.google.com](http://code.google.com/p/redis/wiki/README) for more information.
## Dependencies
1. redis -
rake redis:install
2. dtach -
rake dtach:install
3. svn - git is the new black, but we need it for the google codes.
## Setup
Use the tasks mentioned above (in Dependencies) to get your machine setup.
## Examples
Check the examples/ directory. *Note* you need to have redis-server running first.
\ No newline at end of file
== redis
A ruby client library for the redis key value storage system:
http://code.google.com/p/redis/wiki/README
redis is a key value store with some interesting features:
1. fast
2. keys are strings but values can have types of "NONE","STRING","LIST","SET"
list's can be atomicaly push'd, pop'd and lpush'd, lpop'd and indexed so you
can store things like lists of comments under one key and still be able to
append comments without reading and putting back the whole list.
require 'rubygems'
require 'rake/gempackagetask'
require 'rubygems/specification'
require 'date'
require 'spec/rake/spectask'
require 'tasks/redis.tasks'
GEM = 'redis'
GEM_VERSION = '0.0.2'
AUTHORS = ['Ezra Zygmuntowicz', 'Taylor Weibley']
EMAIL = "ez@engineyard.com"
HOMEPAGE = "http://github.com/ezmobius/redis-rb"
SUMMARY = "Ruby client library for redis key value storage server"
spec = Gem::Specification.new do |s|
s.name = GEM
s.version = GEM_VERSION
s.platform = Gem::Platform::RUBY
s.has_rdoc = true
s.extra_rdoc_files = ["LICENSE"]
s.summary = SUMMARY
s.description = s.summary
s.authors = AUTHORS
s.email = EMAIL
s.homepage = HOMEPAGE
# Uncomment this to add a dependency
# s.add_dependency "foo"
s.require_path = 'lib'
s.autorequire = GEM
s.files = %w(LICENSE README.markdown Rakefile) + Dir.glob("{lib,spec}/**/*")
end
task :default => :spec
desc "Run specs"
Spec::Rake::SpecTask.new do |t|
t.spec_files = FileList['spec/**/*_spec.rb']
t.spec_opts = %w(-fs --color)
end
Rake::GemPackageTask.new(spec) do |pkg|
pkg.gem_spec = spec
end
desc "install the gem locally"
task :install => [:package] do
sh %{sudo gem install pkg/#{GEM}-#{GEM_VERSION}}
end
desc "create a gemspec file"
task :make_spec do
File.open("#{GEM}.gemspec", "w") do |file|
file.puts spec.to_ruby
end
end
\ No newline at end of file
require 'benchmark'
$:.push File.join(File.dirname(__FILE__), 'lib')
require 'redis'
times = 20000
@r = Redis.new
@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}"]} }
end
@r.keys('*').each do |k|
@r.delete k
end
\ No newline at end of file
require 'fileutils'
class RedisCluster
def initialize(opts={})
opts = {:port => 6379, :host => 'localhost', :basedir => "#{Dir.pwd}/rdsrv" }.merge(opts)
FileUtils.mkdir_p opts[:basedir]
opts[:size].times do |i|
port = opts[:port] + i
FileUtils.mkdir_p "#{opts[:basedir]}/#{port}"
File.open("#{opts[:basedir]}/#{port}.conf", 'w'){|f| f.write(make_config(port, "#{opts[:basedir]}/#{port}", "#{opts[:basedir]}/#{port}.log"))}
system(%Q{#{File.join(File.expand_path(File.dirname(__FILE__)), "../redis/redis-server #{opts[:basedir]}/#{port}.conf &" )}})
end
end
def make_config(port=6379, data=port, logfile='stdout', loglevel='debug')
config = %Q{
timeout 300
save 900 1
save 300 10
save 60 10000
dir #{data}
loglevel #{loglevel}
logfile #{logfile}
databases 16
port #{port}
}
end
end
RedisCluster.new :size => 4
\ No newline at end of file
require 'rubygems'
require 'redis'
r = Redis.new
r.delete('foo')
puts
p'set foo to "bar"'
r['foo'] = 'bar'
puts
p 'value of foo'
p r['foo']
require 'rubygems'
require 'redis'
r = Redis.new
puts
p 'incr'
r.delete 'counter'
p r.incr('counter')
p r.incr('counter')
p r.incr('counter')
puts
p 'decr'
p r.decr('counter')
p r.decr('counter')
p r.decr('counter')
require 'rubygems'
require 'redis'
r = Redis.new
r.delete 'logs'
puts
p "pushing log messages into a LIST"
r.push_tail 'logs', 'some log message'
r.push_tail 'logs', 'another log message'
r.push_tail 'logs', 'yet another log message'
r.push_tail 'logs', 'also another log message'
puts
p 'contents of logs LIST'
p r.list_range('logs', 0, -1)
puts
p 'Trim logs LIST to last 2 elements(easy circular buffer)'
r.list_trim('logs', -2, -1)
p r.list_range('logs', 0, -1)
require 'rubygems'
require 'redis'
r = Redis.new
r.delete 'foo-tags'
r.delete 'bar-tags'
puts
p "create a set of tags on foo-tags"
r.set_add 'foo-tags', 'one'
r.set_add 'foo-tags', 'two'
r.set_add 'foo-tags', 'three'
puts
p "create a set of tags on bar-tags"
r.set_add 'bar-tags', 'three'
r.set_add 'bar-tags', 'four'
r.set_add 'bar-tags', 'five'
puts
p 'foo-tags'
p r.set_members('foo-tags')
puts
p 'bar-tags'
p r.set_members('bar-tags')
puts
p 'intersection of foo-tags and bar-tags'
p r.set_intersect('foo-tags', 'bar-tags')
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment