├── rebar ├── .gitignore ├── rebar.config ├── src ├── er.app.src ├── er_app.erl ├── eru.lfe ├── erp.lfe ├── er.lfe ├── er_server.lfe ├── er_sup.erl ├── er_pool_switcher.erl ├── er_redis.erl └── er_pool.erl ├── include ├── utils-macro.lfe ├── utils-defkey.lfe ├── utils.lfe ├── redis-return-types.lfe └── redis-cmds.lfe ├── test ├── er_concurrency_tests.erl └── er_tests.erl └── README.md /rebar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattsta/er-outdated/HEAD/rebar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .hg/ 2 | .hgignore 3 | *.beam 4 | tags 5 | deps/ 6 | .eunit/ 7 | ebin/ 8 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {cover_enabled, true}. 2 | {erl_opts, [debug_info]}. 3 | 4 | {deps, [ 5 | {lfe, "0.6.2", 6 | {git, "git://github.com/rvirding/lfe.git", {tag, "v0.6.2"}}} 7 | ]}. 8 | -------------------------------------------------------------------------------- /src/er.app.src: -------------------------------------------------------------------------------- 1 | {application, er, 2 | [ 3 | {description, "Erlang Redis Library"}, 4 | % version numbers are: {redis.version}-er-{er.version} 5 | {vsn, "2.4-er-1.5.3"}, 6 | {modules, []}, 7 | {registered, [er_sup]}, 8 | {applications, [ 9 | kernel, 10 | stdlib 11 | ]}, 12 | {mod, {er_app, []}}, 13 | {env, []} 14 | ]}. 15 | -------------------------------------------------------------------------------- /src/er_app.erl: -------------------------------------------------------------------------------- 1 | -module(er_app). 2 | 3 | -behaviour(application). 4 | 5 | %% Application callbacks 6 | -export([start/0, start/2, stop/1]). 7 | 8 | %% =================================================================== 9 | %% Application callbacks 10 | %% =================================================================== 11 | 12 | start() -> 13 | er_sup:start_link(). 14 | 15 | start(_StartType, _StartArgs) -> 16 | start(). 17 | 18 | stop(_State) -> 19 | ok. 20 | -------------------------------------------------------------------------------- /src/eru.lfe: -------------------------------------------------------------------------------- 1 | (defmodule eru 2 | (export all)) 3 | 4 | (eval-when-compile 5 | (include-file "include/utils.lfe")) 6 | 7 | (include-file "include/utils.lfe") 8 | (include-file "include/utils-macro.lfe") 9 | 10 | (defun dump_all (server) 11 | (dump server #b("*"))) 12 | 13 | (defun dump (server pattern) 14 | (: lists map 15 | (lambda (k) (tuple k (value server k))) 16 | (: er keys server pattern))) 17 | ; (lc ((<- k (: er keys server pattern))) (tuple k (value server k)))) 18 | 19 | (defun value (server key) 20 | (value server key (: er type server key))) 21 | 22 | (defun value 23 | ((server key 'string) (: er get server key)) 24 | ((server key 'list) (: er lrange server key 0 'inf)) 25 | ((server key 'set) (: er smembers server key)) 26 | ((server key 'zset) (: er zrevrange server key 0 (: er zcard server key))) 27 | ((server key 'hash) (: er hgetall_k server key))) 28 | 29 | (make-key-generator-of-max-args 32) 30 | 31 | (defun hcopy (server from to) 32 | (: er hmset server to (: er hgetall server from))) 33 | -------------------------------------------------------------------------------- /include/utils-macro.lfe: -------------------------------------------------------------------------------- 1 | ; Here we create all redis-cmd-* macros 2 | ; redis-cmd-* macros are created by macro redis-cmd 3 | 4 | (defmacro redis-cmd (small-type return-decoder) 5 | `(defsyntax ,(mk-a 'redis-cmd small-type) 6 | ([command-name] (redis-cmd-mk command-name () ,return-decoder)) 7 | ([command-name command-args] (redis-cmd-mk command-name command-args ,return-decoder)) 8 | ([fun-name command-name command-args] (redis-cmd-mk fun-name command-name command-args ,return-decoder)))) 9 | 10 | (redis-cmd -n redis-return-nil) 11 | (redis-cmd -s redis-return-status) 12 | (redis-cmd -i redis-return-integer) 13 | (redis-cmd -l redis-return-single-line) 14 | (redis-cmd -b redis-return-bulk) 15 | (redis-cmd -m redis-return-multibulk) 16 | (redis-cmd -m-pl redis-return-multibulk-pl) 17 | (redis-cmd -m-kl redis-return-multibulk-kl) 18 | (redis-cmd -strip redis-return-strip-ok) 19 | (redis-cmd -o redis-return-special) 20 | (redis-cmd -i-tf redis-return-integer-true-false) 21 | 22 | ; Here we make all er_key/{1..N} functions 23 | (defmacro make-key-generator-of-max-args (len) 24 | (let* ((arg-names (: lists map (fun xn 1) (: lists seq 1 len))) 25 | (fns (: lists map (fun mk-key-fun 1) arg-names))) 26 | `(progn ,@fns))) 27 | 28 | (defmacro return-type (name redis-cmds) 29 | `(defun ,(mk-a-return-type name) () ',redis-cmds)) 30 | -------------------------------------------------------------------------------- /src/erp.lfe: -------------------------------------------------------------------------------- 1 | (defmodule (erp client) 2 | (export all)) 3 | 4 | (eval-when-compile 5 | (include-file "include/utils.lfe")) 6 | 7 | (include-file "include/utils-macro.lfe") 8 | 9 | (defmacro redis-cmd-mk 10 | ((command-name command-args wrapper-fun-name) 11 | (let* ((cmd (b command-name))) 12 | `(defun ,command-name (,@command-args) 13 | (,wrapper-fun-name (: er_redis q client (list ,cmd ,@command-args)))))) 14 | ((fun-name command-name command-args wrapper-fun-name) 15 | (let* ((cmd (b command-name))) 16 | `(defun ,fun-name (,@command-args) 17 | (,wrapper-fun-name (: er_redis q client (list ,cmd ,@command-args))))))) 18 | 19 | (include-file "include/redis-return-types.lfe") 20 | (include-file "include/redis-cmds.lfe") 21 | 22 | (defun er_next () 23 | (redis-return-strip-ok (: gen_server call client 'next 'infinity))) 24 | 25 | (defun er_transaction ((txn-fun) (when (is_function txn-fun 1)) 26 | (let ((safe-redis-exec 27 | (lambda (client-for-txn) 28 | (funcall txn-fun client-for-txn) 29 | (try 30 | (: er exec client-for-txn) 31 | (catch 32 | ; EXEC without MULTI means we had a discard command in the txn-fun 33 | ((tuple 'throw (tuple 'redis_error #b("ERR EXEC without MULTI")) o) 34 | 'discarded) 35 | ((tuple 'throw Throwed o) (throw Throwed))))))) 36 | (case (: er multi client) 37 | ((tuple use-cxn 'ok) (funcall safe-redis-exec use-cxn)) 38 | ('ok (funcall safe-redis-exec client)))))) 39 | -------------------------------------------------------------------------------- /src/er.lfe: -------------------------------------------------------------------------------- 1 | (defmodule er 2 | (export all)) 3 | 4 | (eval-when-compile 5 | (include-file "include/utils.lfe")) 6 | 7 | (include-file "include/utils-macro.lfe") 8 | 9 | (defmacro redis-cmd-mk 10 | ((command-name command-args wrapper-fun-name) 11 | (let* ((cmd (b command-name))) 12 | `(defun ,command-name (client ,@command-args) 13 | (,wrapper-fun-name (: er_redis q client (list ,cmd ,@command-args)))))) 14 | ((fun-name command-name command-args wrapper-fun-name) 15 | (let* ((cmd (b command-name))) 16 | `(defun ,fun-name (client ,@command-args) 17 | (,wrapper-fun-name (: er_redis q client (list ,cmd ,@command-args))))))) 18 | 19 | (include-file "include/redis-return-types.lfe") 20 | (include-file "include/redis-cmds.lfe") 21 | 22 | (defun er_next (client) 23 | (redis-return-strip-ok (: gen_server call client 'next 'infinity))) 24 | 25 | ; if client is an er_pool, you get a private PID so other processes using 26 | ; the pool won't disturb your sequential operations. 27 | ; if the client is an er_redis, you operate normally. 28 | ; er_pool returns a private pid on multi and cleans it up on exec 29 | (defun er_transaction ((client txn-fun) (when (is_function txn-fun 1)) 30 | (let ((safe-redis-exec 31 | (lambda (client-for-txn) 32 | (funcall txn-fun client-for-txn) 33 | (try 34 | (: er exec client-for-txn) 35 | (catch 36 | ; EXEC without MULTI means we had a discard command in the txn-fun 37 | ((tuple 'throw (tuple 'redis_error #b("ERR EXEC without MULTI")) o) 38 | 'discarded) 39 | ((tuple 'throw Throwed o) (throw Throwed))))))) 40 | (case (: er multi client) 41 | ((tuple use-cxn 'ok) (funcall safe-redis-exec use-cxn)) 42 | ('ok (funcall safe-redis-exec client)))))) 43 | -------------------------------------------------------------------------------- /src/er_server.lfe: -------------------------------------------------------------------------------- 1 | (defmodule er_server 2 | (export all)) 3 | 4 | (eval-when-compile 5 | (include-file "include/utils.lfe")) 6 | 7 | (include-file "include/utils-macro.lfe") 8 | 9 | (defmacro redis-cmd-mk 10 | ((command-name command-args wrapper-fun-name) 11 | (let* ((cmd (b command-name))) 12 | `(defun ,command-name (gen-server-name ,@command-args) 13 | (,wrapper-fun-name 14 | (: gen_server call gen-server-name 15 | (tuple 'cmd 16 | (list ,cmd ,@command-args))))))) 17 | ((fun-name command-name command-args wrapper-fun-name) 18 | (let* ((cmd (b command-name))) 19 | `(defun ,fun-name (gen-server-name ,@command-args) 20 | (,wrapper-fun-name 21 | (: gen_server call gen-server-name 22 | (tuple 'cmd 23 | (list ,cmd ,@command-args)))))))) 24 | 25 | (include-file "include/redis-return-types.lfe") 26 | (include-file "include/redis-cmds.lfe") 27 | 28 | (defrecord state 29 | (cxn 'nil)) 30 | 31 | (defun start_link 32 | ([gen-server-name ip port] 33 | (when (is_atom gen-server-name) (is_list ip) (is_integer port)) 34 | (: gen_server start_link 35 | (tuple 'local gen-server-name) 'er_server (tuple ip port) '()))) 36 | 37 | (defun init 38 | ([(tuple ip port)] 39 | (case (: er_redis connect ()) 40 | ((tuple 'ok connection) (tuple 'ok (make-state cxn connection)))))) 41 | 42 | (defun handle_call 43 | ([(tuple 'cmd cmd-list) from state] 44 | (let* ((cxn (state-cxn state))) 45 | (spawn (lambda () 46 | (: gen_server reply from 47 | (: er_redis q cxn cmd-list))))) 48 | (tuple 'noreply state))) 49 | 50 | (defun handle_cast (_request state) 51 | (tuple 'noreply state)) 52 | 53 | (defun terminate (_reason _state) 54 | 'ok) 55 | 56 | (defun handle_info (_request state) 57 | (tuple 'noreply state)) 58 | 59 | (defun code_change (_old-version state _extra) 60 | (tuple 'ok state)) 61 | 62 | -------------------------------------------------------------------------------- /src/er_sup.erl: -------------------------------------------------------------------------------- 1 | -module(er_sup). 2 | 3 | -behaviour(supervisor). 4 | 5 | %% API 6 | -export([start_link/0]). 7 | 8 | %% Supervisor callbacks 9 | -export([init/1]). 10 | 11 | %% Helpers 12 | -export([add_er_pool/3]). 13 | 14 | -export([er_named_pool_spec/3]). 15 | 16 | %% Helper macro for declaring children of supervisor 17 | -define(ER_CHILD(I, Args, Type), {I, {er_pool, start_link, Args}, 18 | permanent, 5000, Type, [er_pool]}). 19 | 20 | %% =================================================================== 21 | %% API functions 22 | %% =================================================================== 23 | 24 | start_link() -> 25 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 26 | 27 | add_er_pool(Name, Host, Port) when is_atom(Name), 28 | is_list(Host), is_integer(Port) -> 29 | ChildSpec = ?ER_CHILD(Name, [Name, Host, Port], worker), 30 | supervisor:start_child(?MODULE, ChildSpec). 31 | 32 | -spec er_named_pool_spec(atom(), string(), integer() | string()) -> tuple(). 33 | er_named_pool_spec(Name, IP, Port) when 34 | is_list(IP) andalso (is_integer(Port) orelse is_list(Port)) -> 35 | RedisIP = IP, 36 | RedisPort = case Port of 37 | IntPort when is_integer(IntPort) -> IntPort; 38 | ListPort when is_list(ListPort) -> 39 | try list_to_integer(ListPort) of 40 | IntPort -> IntPort 41 | catch 42 | badarg:_ -> throw("port numbers must be integers!") 43 | end 44 | end, 45 | {supname(Name), 46 | {er_pool, start_link, [Name, RedisIP, RedisPort]}, 47 | permanent, 5000, worker, [er_pool]}. 48 | 49 | supname(Name) -> 50 | list_to_atom(atom_to_list(Name) ++ "_sup"). 51 | 52 | %% =================================================================== 53 | %% Supervisor callbacks 54 | %% =================================================================== 55 | 56 | init([]) -> 57 | {ok, {{one_for_one, 5, 10}, []}}. 58 | -------------------------------------------------------------------------------- /test/er_concurrency_tests.erl: -------------------------------------------------------------------------------- 1 | -module(er_concurrency_tests). 2 | -include_lib("eunit/include/eunit.hrl"). 3 | -import(lists, [flatten/1]). 4 | 5 | -define(E(A, B), ?assertEqual(A, B)). 6 | -define(_E(A, B), ?_assertEqual(A, B)). 7 | -define(_M(A, B), ?_assertMatch(A, B)). 8 | % Easily enable/disable debug printing by swapping which ?d is used: 9 | %-define(d(A, B), ?debugTime(A, B)). 10 | -define(d(A, B), B). 11 | 12 | kv_pairs() -> 13 | AtoZ = lists:seq($A, $Z), 14 | % Generate keyA - keyZ 15 | Keys = [<<"key", KeyId>> || KeyId <- AtoZ], 16 | % Generate valA - valZ 17 | Vals = [<<"val", KeyId>> || KeyId <- AtoZ], 18 | lists:zip(Keys, Vals). 19 | 20 | redis_setup_clean() -> 21 | ConcurrencyMod = er_pool, % er_pool, inparallel = .4 to .6 seconds 22 | % er_pool, inorder = 16 seconds 23 | % er_redis, inparallel = 1.5 to 2 seconds 24 | % er_redis, inorder = 16 seconds 25 | Cxn = case ConcurrencyMod:start_link(er_pool, "127.0.0.1", 6991) of 26 | {ok, C} -> C; 27 | {error, {already_started, Pid}} -> Pid 28 | end, 29 | ok = er:flushall(Cxn), 30 | % Store (keyA, valA), (keyB, valB), ..., (keyZ, valZ) 31 | [er:set(Cxn, K, V) || {K, V} <- kv_pairs()], 32 | Cxn. 33 | 34 | redis_cleanup(Cxn) -> 35 | Cxn ! shutdown. 36 | 37 | er_concurrency_test_() -> 38 | {setup, 39 | fun redis_setup_clean/0, 40 | fun redis_cleanup/1, 41 | fun(C) -> 42 | [ 43 | % Changing both tests to 'inorder' causes test() to take ~16 seconds. 44 | {inparallel, 45 | % generate and run 26 concurrent gets and make sure results work 46 | [?d(V, ?_E(V, er:get(C, K))) || {K, V} <- kv_pairs()] 47 | }, 48 | {inparallel, 49 | % generate and run 26 * 200 = 5200 concurrent gets and make sure results work 50 | [[?d(V, ?_E(V, er:get(C, K))) || {K, V} <- kv_pairs()] 51 | || _ <- lists:seq(1, 200)] 52 | } 53 | ] 54 | end 55 | }. 56 | -------------------------------------------------------------------------------- /include/utils-defkey.lfe: -------------------------------------------------------------------------------- 1 | (eval-when-compile 2 | ; make-fns-from-args consolidates double-defkey for defkey recursive uses 3 | (defun make-functions-from-args (parts) 4 | `(progn 5 | (defkey ,(a (join-under (ll parts))) ,parts) 6 | (defkey ,(a (join-colon (ll parts))) ,parts))) 7 | ; (caps? $A) -> true 8 | ; (caps? $e) -> false 9 | (defun caps? 10 | ((x) (when (andalso (is_integer x) (>= x 65) (=< x 90))) 'true) 11 | ((_) 'false)) 12 | ; (extract-caps (a b C d E f)) -> (C E) 13 | (defun extract-caps 14 | ([()] '()) 15 | ([(x . xs)] (let* ((first-character (car (l x)))) 16 | (cond 17 | ((caps? first-character) (cons x (extract-caps xs))) 18 | (else (extract-caps xs)))))) 19 | ; (listize-parts (a b C d E f) (C E)) -> (#b("a") #b("b") C #b("d") E #b("f")) 20 | (defun listize-parts 21 | ([() _] '()) 22 | ([(x . xs) caps-parts] (cond 23 | ; if x in caps-parts, don't turn it into a list. 24 | ((: lists member x caps-parts) 25 | (cons x (listize-parts xs caps-parts))) 26 | ; else, turn x into a list and recurse. 27 | ; NB: we convert to binary because LFE 28 | ; currently has a problem converting 29 | ; to lists. It wants to execute them 30 | ; after they are constructed (it's not quoting). 31 | (else 32 | (cons (b x) 33 | (listize-parts xs caps-parts))))))) 34 | 35 | ; (defkey rambo (a b C d E f)) generates equivalent of: 36 | ; rambo(C, E) -> eru:er_key(<<"a">>, <<"b">>, C, <<"d">>, E, <<"f">>). 37 | ; i.e. anything starting with caps is an argument and everything else 38 | ; is presented to the key generator as-is. 39 | ; also works with one arg: (defkey (site N)) makes: site:N(N) and site_N(N) 40 | ; counter keys auto-generate counter accessors: 41 | ; (defkey (counter bob)) => counter:bob(), counter_bob(), bob:N(N), bob_N(N) 42 | ; normal usage of defining a function name and its components: 43 | ; (defkey zomboid (rabbit Hole)) => zomboid(Hole) = rabbit:[Hole] 44 | ; normal usage of auto-making function name based on component names: 45 | ; (defkey (rabbit Hole)) => rabbit(Hole) = rabbit:Hole 46 | (defmacro defkey 47 | ([('counter . (name))] `(progn 48 | ,(make-functions-from-args (list 'counter name)) 49 | (defkey (,name N)))) 50 | ([parts] (when (is_list parts)) (make-functions-from-args parts)) 51 | ([name parts] (let* ((variable-parts (extract-caps parts)) 52 | (adjusted-list-parts 53 | (listize-parts parts variable-parts))) 54 | `(defun ,name ,variable-parts 55 | (: eru er_key ,@adjusted-list-parts))))) 56 | 57 | ; (defkey-suite post (to tags last_update authors (comment Comment))) creates: 58 | ; counter:post(), counter_post() 59 | ; post:N:to(N), post_N_to(N) 60 | ; post:N:tags(N), post_N_tags(N) 61 | ; post:N:last_update(N), post_N_last_update(N) 62 | ; post:N:authors(N), post_N_authors(N) 63 | ; post:N:comment:Comment(N, Comment), post_N_comment_Comment(N, Comment) 64 | (defmacro defkey-suite (key subkeys) 65 | `(progn 66 | (defkey (counter ,key)) 67 | ,@(: lists map 68 | (match-lambda 69 | ([subkey] (when (is_atom subkey)) `(defkey (,key N ,subkey))) 70 | ([subkey] (when (is_list subkey)) `(defkey (,key N ,@subkey)))) 71 | subkeys))) 72 | -------------------------------------------------------------------------------- /src/er_pool_switcher.erl: -------------------------------------------------------------------------------- 1 | -module(er_pool_switcher). 2 | -behaviour(gen_server). 3 | 4 | %% gen_server callbacks 5 | -export([init/1, 6 | handle_call/3, handle_cast/2, 7 | handle_info/2, terminate/2, code_change/3]). 8 | 9 | %% api callbacks 10 | -export([start_link/2]). 11 | 12 | -record(state, {pools :: [{pid(), list()}] % Pid of er_pool and Args 13 | }). 14 | 15 | %%==================================================================== 16 | %% api callbacks 17 | %%==================================================================== 18 | start_link(GenServerName, PoolsWithArgs) 19 | when is_atom(GenServerName) andalso is_list(hd(PoolsWithArgs)) -> 20 | gen_server:start_link({local, GenServerName}, ?MODULE, PoolsWithArgs, []). 21 | 22 | %%==================================================================== 23 | %% gen_server callbacks 24 | %%==================================================================== 25 | 26 | init(PoolArgs) -> 27 | process_flag(trap_exit, true), 28 | PoolPids = [pool(Args) || Args <- PoolArgs], 29 | attach_liveness_check(PoolPids), 30 | {ok, #state{pools = lists:zip(PoolPids, PoolArgs)}}. 31 | 32 | % If current pid is a stale backup, try to bring live. 33 | % If we can't bring it live, cycle to the next backup. 34 | handle_call({cmd, Parts}, From, #state{pools = [{nil, Args}|T]} = State) -> 35 | case pool(Args) of 36 | nil -> NextPool = T ++ [{nil, Args}]; % switch to next backup 37 | P -> NextPool = [{P, Args} | T] % this connect worked. use it. 38 | end, 39 | handle_call({cmd, Parts}, From, State#state{pools = NextPool}); 40 | 41 | handle_call({cmd, Parts}, From, #state{pools = [{Pid, _}|_]} = State) -> 42 | spawn(fun() -> 43 | gen_server:reply(From, gen_server:call(Pid, {cmd, Parts}, infinity)) 44 | end), 45 | {noreply, State}. 46 | 47 | handle_cast(_Msg, State) -> 48 | {noreply, State}. 49 | 50 | % An er_pool died. If live, promote next-highest to live. 51 | handle_info({'EXIT', DeadPid, _Reason}, 52 | #state{pools = [{LivePid, Args} | T] = Pools} = State) -> 53 | case DeadPid of 54 | LivePid -> Connected = pool(Args), 55 | NewPools = T ++ [{Connected, Args}]; 56 | OtherPid -> {OtherPid, OtherArgs} = lists:keyfind(OtherPid, 1, Pools), 57 | Connected = pool(OtherArgs), 58 | NewPoolEntry = {Connected, OtherArgs}, 59 | NewPools = lists:keyreplace(OtherPid, 1, Pools, NewPoolEntry) 60 | end, 61 | attach_liveness_check(NewPools), 62 | {noreply, State#state{pools = NewPools}}; 63 | 64 | handle_info(Info, State) -> 65 | error_logger:error_msg("Other info: ~p with state ~p~n", [Info, State]), 66 | {noreply, State}. 67 | 68 | terminate(_Reason, #state{pools = Pools}) -> 69 | [exit(Pid, normal) || {Pid, _} <- Pools]. 70 | 71 | code_change(_OldVsn, State, _Extra) -> 72 | {ok, State}. 73 | 74 | %%-------------------------------------------------------------------- 75 | %%% Internal functions 76 | %%-------------------------------------------------------------------- 77 | pool(Args) -> 78 | try apply(er_pool, start_link_nameless, Args) of 79 | {ok, Pid} -> Pid 80 | catch 81 | throw:Error -> 82 | error_logger:error_msg("Connect failed: ~p ~p. Keeping stale.~n", 83 | [Error, Args]), 84 | nil 85 | end. 86 | 87 | attach_liveness_check([{ErPid, _Args} | _T]) when is_list(ErPid) -> 88 | attach_liveness_check(ErPid, 250); 89 | attach_liveness_check([ErPid | _T]) when is_pid(ErPid) -> 90 | attach_liveness_check(ErPid, 250). 91 | 92 | % Ping against redis every IntervalMS milliseconds. 93 | % If redis is down, the pool crashes. 94 | attach_liveness_check(ErPool, IntervalMS) when is_pid(ErPool) -> 95 | timer:apply_interval(IntervalMS, er, ping, [ErPool]). 96 | -------------------------------------------------------------------------------- /include/utils.lfe: -------------------------------------------------------------------------------- 1 | ; turn anything reasonable into an atom 2 | (defun a 3 | ((c) (when (is_list c)) (list_to_atom c)) 4 | ((c) (when (is_atom c)) c) 5 | ((c) (when (is_integer c)) (a (l c))) 6 | ((c) (when (is_binary c)) (a (l c)))) 7 | (defun la 8 | ((c) (when (andalso (is_list c) (is_list (car c)))) 9 | (: lists map (fun a 1) c))) ;(lc ((<- element c)) (a element)))) 10 | 11 | (defun mk-a (c d) 12 | (a (: lists flatten (cons (l c) (l d))))) 13 | 14 | ; turn anything reasonable into a list 15 | (defun l 16 | ((c) (when (is_list c)) c) 17 | ((c) (when (is_atom c)) (atom_to_list c)) 18 | ((c) (when (is_integer c)) (integer_to_list c)) 19 | ((c) (when (is_binary c)) (: unicode characters_to_list c))) 20 | (defun ll 21 | ((c) (when (is_list c)) 22 | (: lists map (fun l 1) c))) ;(lc ((<- element c)) (l element)))) 23 | 24 | ; turn anything reasonable into a binary 25 | (defun b 26 | ((c) (when (is_list c)) (: unicode characters_to_binary c)) 27 | ((c) (when (is_atom c)) (atom_to_binary c 'utf8)) 28 | ((c) (when (is_integer c)) (b (l c))) 29 | ((c) (when (is_binary c)) c)) 30 | (defun bl 31 | ((c) (when (is_list c)) 32 | (: lists map (fun b 1) c))) ;(lc ((<- element c)) (b element)))) 33 | 34 | (defun xn (n-times) 35 | (xn '"a" n-times)) 36 | ; example: (xn '"a" 3) => (('"a") ('"a" '"aa") ('"a" '"aa" '"aaa")) 37 | (defun xn (what n-times) 38 | ; map (1 2 3 ... n-times) to create: 39 | ; ((what) (what whatwhat) (what whatwhat whatwhatwhat) ...) 40 | (: lists map 41 | (lambda (n) 42 | (: lists flatten (: lists duplicate n what))) 43 | (: lists seq 1 n-times))) 44 | ; (lc ((<- n (: lists seq 1 n-times))) (: lists flatten (: lists duplicate n what))) 45 | 46 | (defun join 47 | ((c join-str) (when (andalso (is_list c) (is_list (car c)))) 48 | (b (: string join c join-str)))) 49 | 50 | ; example: (join-colon '('"hello" '"there" '"third")) => #b("hello:there:third") 51 | (defun join-colon 52 | ((c) (join c '":"))) 53 | 54 | ; example: (join-under '('"hello" '"there" '"third")) => #b("hello_there_third") 55 | (defun join-under 56 | ((c) (join c '"_"))) 57 | 58 | ; splice args into correct positions to make the er_key functions 59 | (defun mk-key-fun (args) 60 | `(defun er_key (,(la args) (join-colon (ll (list ,@(la args))))))) 61 | 62 | (defun mk-a-return-type (type) 63 | (mk-a 'return-type:: type)) 64 | 65 | ; This is supposed to take the return types in redis-return-types.lfe 66 | ; and automatically extract which commands have which return type. 67 | ; Can't figure out how to force LFE to let me funcall or execute 68 | ; a function based on an atom name. 69 | ;(defun build-cmd-type-dict () 70 | ; ; fold over reutrn-type::return-types to get all types 71 | ; (: lists foldl 72 | ; (lambda (type acc) 73 | ; (: dict merge (lambda (k v1 v2) v1) acc 74 | ; ; for each type, fold over each command to store its type 75 | ; (: lists foldl 76 | ; (lambda (cmd-name cmd-acc) 77 | ; (: dict store cmd-name type cmd-acc)) ; fun 78 | ; (: dict new) ; acc0 79 | ; ((mk-a-return-type type))) ; list <-- this line is the problem. 80 | ; ; ^^^ needs to call the dynamic function name at compile time. doesn't work yet 81 | ; (: dict new) ; acc0 82 | ; (return-type::return-types)))) ; list 83 | 84 | ;(defun to-tuple-list (cmd-type cmds) 85 | ; (lc ((<- cmd cmds)) (tuple cmd cmd-type))) 86 | 87 | ;(defun dict-from-type (cmd-type cmds) 88 | ; (: dict from_list (to-tuple-list cmd-type cmds))) 89 | 90 | ; This is a less-loopy example of above, but we still end up with an 91 | ; atom name that has to be executed against. No go. 92 | ;(defun build-cmd-type-dict () 93 | ; (let* ((dict1 (dict-from-type 'nil (return-type::nil))) 94 | ; (dict2 (dict-from-type 'status (return-type::status))) 95 | ; (dict3 (dict-from-type 'integer (return-type::integer))) 96 | ; (dict4 (dict-from-type 'single-line (return-type::single-line))) 97 | ; (dict5 (dict-from-type 'bulk (return-type::bulk))) 98 | ; (dict6 (dict-from-type 'multibulk (return-type::multibulk))) 99 | ; (dict7 (dict-from-type 'special (return-type::special))) 100 | ; (alldicts (list dict1 dict2 dict3 dict4 dict5 dict6 dict7)) 101 | ; (nofun (lambda (key val1 val2) val1))) 102 | ; (: lists foldl (lambda (e acc) (: dict merge nofun e acc)) (: dict new) alldicts))) 103 | 104 | ;(defun find-return-wrapper (cmd) 105 | ; (let* ((cmd-to-type (build-cmd-type-dict))) 106 | ; 'return-nil)) 107 | -------------------------------------------------------------------------------- /include/redis-return-types.lfe: -------------------------------------------------------------------------------- 1 | ;; include/redis-return-types.lfe 2 | ;; What is this file? Redis return type conversion functions. 3 | ;; What are these (return-type * ...) sexps? 4 | ;; - I don't remember exactly. It was going to be a way to auto-generate 5 | ;; something that didn't pan out. The list hasn't been kept in sync with 6 | ;; redis-cmds.lfe. They can probably all be deleted. 7 | ;; Under these return-type declarations are the actual return conversion functions. 8 | ;; The goal is to return as much actual information as possible so clients don't 9 | ;; have to needlessly unpack tuples that just say {ok, Value} or other nonsense. 10 | 11 | ;; create list of all return types available as (return-type::return-types) 12 | (return-type return-types 13 | (nil status integer single-line bulk multibulk special)) 14 | 15 | ;; create lists of functions with each return type 16 | ;; available as (return-type::nil), (return-type::status), etc 17 | (return-type nil 18 | (quit)) 19 | 20 | (return-type status 21 | (auth type rename select flushdb flushall set 22 | setex mset nset rpush lpush ltrim lset sinterstore 23 | sunionstore sdiffstore hmset save bgsave shutdown 24 | bgrewriteaof slaveof)) 25 | 26 | (return-type integer 27 | (exists del renamenx dbsize expire expireat ttl 28 | move setnx msetnx incr incrby decr decrby append 29 | llen lrem sadd srem smove scard sismember zadd 30 | zrem zincrby zcard zremrangebyrank zremrangebyscore 31 | zunionstore zinterstore hset hincrby hexists hdel hlen 32 | publish lastsave)) 33 | 34 | (return-type bulk 35 | (keys get getset substr lindex lpop rpop rpoplpush 36 | spop srandmember zrank zrevrank zscore hget info )) 37 | 38 | (return-type single-line 39 | (randomkey)) 40 | 41 | (return-type multibulk 42 | (mget lrange blpop brpop sinter sunion sdiff smembers 43 | zrange zrevrange zrangebyscore hkeys hvals hgetall 44 | sort multi exec discard)) 45 | 46 | (return-type special 47 | (subscribe unsubscribe psubscribe punsubscribe monitor)) 48 | 49 | 50 | ;; Functions for handling generic return types 51 | (defun redis-return-nil (x) x) 52 | 53 | (defun redis-return-status 54 | ([(tuple 'error bin)] (throw (tuple 'redis_error bin))) 55 | ([x] (when (is_binary x)) 56 | (list_to_atom (: string to_lower (binary_to_list x)))) 57 | ([#b("QUEUED")] 'queued) 58 | ([(x)] (when (is_binary x)) 59 | ; we trust redis to have a stable list of return atoms 60 | (list_to_atom (: string to_lower (binary_to_list x)))) 61 | ([(tuple pid status)] (when (is_pid pid)) (tuple pid (redis-return-status status)))) 62 | 63 | (defun redis-return-integer 64 | ([(#b("inf"))] 'inf) 65 | ([(#b("-inf"))] '-inf) 66 | ([(#b("nan"))] 'nan) 67 | ([x] (when (is_integer x)) x) 68 | ([(tuple 'ok x)] (when (is_integer x)) x) 69 | ([(tuple 'ok #b("inf"))] 'inf) 70 | ([(tuple 'ok #b("-inf"))] '-inf) 71 | ([(tuple 'ok #b("nan"))] 'nan) 72 | ([(tuple 'ok x)] (when (is_binary x)) (list_to_integer (binary_to_list x))) 73 | ([#b("QUEUED")] 'queued) 74 | ([(x)] (when (is_binary x)) (list_to_integer (binary_to_list x))) 75 | ([(tuple 'error bin)] (throw (tuple 'redis_error bin)))) 76 | 77 | (defun redis-return-single-line 78 | ([()] #b()) 79 | ([(tuple 'ok value)] value) 80 | ([#b("QUEUED")] 'queued) 81 | ([(x)] x)) 82 | 83 | (defun redis-return-bulk 84 | ([((tuple 'ok value) . xs)] (cons value (redis-return-bulk xs))) 85 | ([(tuple 'ok value)] value) 86 | ([#b("QUEUED")] 'queued) 87 | ([x] x)) 88 | 89 | (defun to-proplist 90 | ([()] '()) 91 | ([(a b . xs)] (cons (tuple (binary_to_atom a 'utf8) b) (to-proplist xs)))) 92 | 93 | (defun to-keylist 94 | ([()] '()) 95 | ([(a b . xs)] (cons (tuple a b) (to-keylist xs)))) 96 | 97 | (defun redis-return-multibulk-pl (x) 98 | (to-proplist (redis-return-multibulk x))) 99 | 100 | (defun redis-return-multibulk-kl (x) 101 | (to-keylist (redis-return-multibulk x))) 102 | 103 | (defun redis-return-multibulk 104 | ([(tuple 'ok 'nil)] 'nil) 105 | ([x] (when (is_atom x)) x) 106 | ([x] (when (is_list x)) (element 2 (: lists unzip x))) 107 | ([#b("QUEUED")] 'queued)) 108 | 109 | (defun redis-return-strip-ok 110 | ([()] ()) 111 | ([(tuple pid retval)] (when (is_pid pid)) (tuple pid (redis-return-strip-ok retval))) 112 | ([((tuple 'ok #b("message")) . xs)] (cons 'message (redis-return-strip-ok xs))) 113 | ([((tuple 'ok #b("subscribe")) . xs)] (cons 'subscribe (redis-return-strip-ok xs))) 114 | ([((tuple 'ok value) . xs)] (cons value (redis-return-strip-ok xs))) 115 | ([(x . xs)] (cons x (redis-return-strip-ok xs))) 116 | ([#b("QUEUED")] 'queued)) 117 | 118 | (defun redis-return-special 119 | ([potential-errors-or-ignorable-return-values] 120 | (when (is_list potential-errors-or-ignorable-return-values)) 121 | (: lists map 122 | (match-lambda 123 | ([(tuple 'error bin)] (throw (tuple 'redis_error bin))) 124 | ([x] x)) 125 | potential-errors-or-ignorable-return-values)) 126 | ([(tuple 'error bin)] (throw (tuple 'redis_error bin))) 127 | ([x] x)) 128 | 129 | ;; Functions for handling more specialized return types 130 | (defun redis-return-integer-true-false 131 | ([0] 'false) ; er_redis converts some things to ints 132 | ([(#b("0"))] 'false) ; and others it leaves in binaries 133 | ([1] 'true) 134 | ([(#b("1"))] 'true) 135 | ([#b("QUEUED")] 'queued)) 136 | -------------------------------------------------------------------------------- /src/er_redis.erl: -------------------------------------------------------------------------------- 1 | -module(er_redis). 2 | -behaviour(gen_server). 3 | 4 | %% gen_server callbacks 5 | -export([init/1, 6 | handle_call/3, handle_cast/2, 7 | handle_info/2, terminate/2, code_change/3]). 8 | 9 | %% api callbacks 10 | -export([connect/0, connect/2, q/2]). 11 | -export([start_link/0, start_link/2]). 12 | 13 | -record(state, {ip, port, socket}). 14 | 15 | %%==================================================================== 16 | %% api callbacks 17 | %%==================================================================== 18 | connect() -> start_link(). 19 | connect(IP, Port) -> start_link(IP, Port). 20 | 21 | start_link() -> 22 | start_link("127.0.0.1", 6379). 23 | 24 | start_link(IP, Port) -> 25 | gen_server:start_link(?MODULE, [IP, Port], []). 26 | 27 | q(Server, Parts) -> 28 | gen_server:call(Server, {cmd, Parts}, infinity). 29 | 30 | %%==================================================================== 31 | %% gen_server callbacks 32 | %%==================================================================== 33 | 34 | %%-------------------------------------------------------------------- 35 | %% Function: init(Args) -> {ok, State} | 36 | %% {ok, State, Timeout} | 37 | %% ignore | 38 | %% {stop, Reason} 39 | %% Description: Initiates the server 40 | %%-------------------------------------------------------------------- 41 | init([IP, Port]) when is_list(IP), is_integer(Port) -> 42 | initial_connect(#state{ip = IP, port = Port}). 43 | 44 | %%-------------------------------------------------------------------- 45 | %% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | 46 | %% {reply, Reply, State, Timeout} | 47 | %% {noreply, State} | 48 | %% {noreply, State, Timeout} | 49 | %% {stop, Reason, Reply, State} | 50 | %% {stop, Reason, State} 51 | %% Description: Handling call messages 52 | %%-------------------------------------------------------------------- 53 | handle_call(Call, From, #state{socket = undefined} = State) -> 54 | handle_call(Call, From, state_connect(State)); 55 | 56 | handle_call({cmd, Parts} = Cmd, From, #state{socket = Socket} = State) -> 57 | case gen_tcp:send(Socket, multibulk_cmd(Parts)) of 58 | ok -> case read_resp(Socket) of 59 | connection_closed -> handle_call(Cmd, From, state_connect(State)); 60 | Reply -> {reply, Reply, State} 61 | end; 62 | {error, closed} -> handle_call(Cmd, From, state_connect(State)); 63 | Error -> error_logger:error_msg("SEND ERROR: ~p~n", [Error]) 64 | end; 65 | handle_call(next, _From, #state{socket = Socket} = State) -> 66 | case read_resp(Socket) of 67 | connection_closed -> {reply, connection_closed, state_connect(State)}; 68 | Reply -> {reply, Reply, State} 69 | end; 70 | handle_call(shutdown, _From, State) -> 71 | {stop, normal, State}. 72 | 73 | %%-------------------------------------------------------------------- 74 | %% Function: handle_cast(Msg, State) -> {noreply, State} | 75 | %% {noreply, State, Timeout} | 76 | %% {stop, Reason, State} 77 | %% Description: Handling cast messages 78 | %%-------------------------------------------------------------------- 79 | handle_cast(_Msg, State) -> 80 | {noreply, State}. 81 | 82 | %%-------------------------------------------------------------------- 83 | %% Function: handle_info(Info, State) -> {noreply, State} | 84 | %% {noreply, State, Timeout} | 85 | %% {stop, Reason, State} 86 | %% Description: Handling all non call/cast messages 87 | %%-------------------------------------------------------------------- 88 | handle_info(shutdown, State) -> 89 | {stop, normal, State}; 90 | handle_info(Other, State) -> 91 | error_logger:error_msg("Other info of: ~p~n", [Other]), 92 | {noreply, State}. 93 | 94 | %%-------------------------------------------------------------------- 95 | %% Function: terminate(Reason, State) -> void() 96 | %% Description: This function is called by a gen_server when it is about to 97 | %% terminate. It should be the opposite of Module:init/1 and do any necessary 98 | %% cleaning up. When it returns, the gen_server terminates with Reason. 99 | %% The return value is ignored. 100 | %%-------------------------------------------------------------------- 101 | terminate(_Reason, #state{socket=Socket}) -> 102 | ok = gen_tcp:close(Socket). 103 | 104 | %%-------------------------------------------------------------------- 105 | %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} 106 | %% Description: Convert process state when code is changed 107 | %%-------------------------------------------------------------------- 108 | code_change(_OldVsn, State, _Extra) -> 109 | {ok, State}. 110 | 111 | %%-------------------------------------------------------------------- 112 | %%% Internal functions 113 | %%-------------------------------------------------------------------- 114 | initial_connect(State) -> 115 | {ok, state_connect(State)}. 116 | 117 | state_connect(#state{ip = IP, port = Port, socket = PrevSock} = State) -> 118 | (catch gen_tcp:close(PrevSock)), 119 | SocketOpts = [binary, {nodelay, true}, 120 | {packet, line}, {active, false}, 121 | {recbuf, 1024}], 122 | case gen_tcp:connect(IP, Port, SocketOpts) of 123 | {ok, Socket} -> State#state{socket = Socket}; 124 | {error, Error} -> exit(self(), {er_connect_failed, Error, IP, Port}) 125 | end. 126 | 127 | strip(B) when is_binary(B) -> 128 | S = size(B) - 2, % 2 = size(<<"\r\n">>) 129 | <> = B, 130 | B1. 131 | 132 | read_resp(Socket) -> 133 | inet:setopts(Socket, [{active, once}, {packet, line}]), 134 | receive 135 | {tcp, Socket, Line} -> 136 | case Line of 137 | <<"*", Rest/binary>> -> 138 | Count = list_to_integer(binary_to_list(strip(Rest))), 139 | read_multi_bulk(Socket, Count, []); 140 | <<"+", Rest/binary>> -> 141 | strip(Rest); 142 | <<"-", Rest/binary>> -> 143 | {error, strip(Rest)}; 144 | <<":", Size/binary>> -> 145 | list_to_integer(binary_to_list(strip(Size))); 146 | <<"$", Size/binary>> -> 147 | Size1 = list_to_integer(binary_to_list(strip(Size))), 148 | read_body(Socket, Size1); 149 | <<"\r\n">> -> 150 | read_resp(Socket); 151 | Uknown -> 152 | {unknown, Uknown} 153 | end; 154 | {tcp_closed, Socket} -> connection_closed 155 | end. 156 | 157 | read_body(_Socket, -1) -> 158 | {ok, nil}; 159 | read_body(_Socket, 0) -> 160 | {ok, <<>>}; 161 | read_body(Socket, Size) -> 162 | inet:setopts(Socket, [{packet, raw}]), 163 | gen_tcp:recv(Socket, Size). 164 | 165 | read_multi_bulk(_Data, 0, Acc) -> 166 | lists:reverse(Acc); 167 | read_multi_bulk(_Data, -1, _Acc) -> 168 | {ok, nil}; % Occurs during b[lr]pop. Maybe during brpoplpush too 169 | read_multi_bulk(Socket, Count, Acc) when Count > 0 -> 170 | Acc1 = [read_resp(Socket) | Acc], 171 | read_multi_bulk(Socket, Count-1, Acc1). 172 | 173 | -define(i2l(X), integer_to_list(X)). 174 | multibulk_cmd(Args) when is_list(Args) -> 175 | ConvertedArgs = lists:flatten([to_binary(B) || B <- Args]), 176 | TotalLength = length(ConvertedArgs), 177 | 178 | ArgCount = [<<"*">>, ?i2l(TotalLength), <<"\r\n">>], 179 | ArgBin = [[<<"$">>, ?i2l(iolist_size(A)), <<"\r\n">>, A, <<"\r\n">>] 180 | || A <- ConvertedArgs], 181 | 182 | [ArgCount, ArgBin]; 183 | multibulk_cmd(Args) when is_binary(Args) -> 184 | multibulk_cmd([Args]). 185 | 186 | to_binary(X) when is_binary(X) -> X; 187 | % Basic determination of a char list: "abc" 188 | to_binary(X) when is_list(X) andalso is_integer(hd(X)) -> list_to_binary(X); 189 | to_binary([]) -> <<"">>; 190 | % Basic determination of a list of stuff: [abc, def, <<"other">>, 12] 191 | to_binary(X) when is_list(X) -> [to_binary(A) || A <- X]; 192 | to_binary(X) when is_atom(X) -> atom_to_binary(X, utf8); 193 | to_binary(X) when is_integer(X) -> list_to_binary(integer_to_list(X)). 194 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | er: erlang redis 2 | ================ 3 | 4 | Status 5 | ------ 6 | er is production ready. 7 | er is feature complete with redis-2.2 as of antirez/redis@59aee5513d27fdf2d24499f35611093fa0fab3fb 8 | 9 | Code Guide 10 | ---------- 11 | `src/er.lfe` and `src/erp.lfe` are where redis commands get created. 12 | 13 | `er.lfe` creates a module where the first parameter of all commands is the 14 | redis connection (the conenction is either a `er_redis` PID or `er_pool` name): 15 | {ok, Client} = er_redis:connect(). 16 | er:set(Client, <<"chevron">>, <<"locked">>). 17 | 18 | `erp.lfe` creates a parameterized module where the redis connection is carried 19 | through all the commands: 20 | {ok, Client} = er_redis:connect(). 21 | RedisClient = erp:new(Client). 22 | RedisClient:set(<<"artist">>, <<"pallett">>). 23 | 24 | `er_pool.erl` gives you a centrally managed connection pool of redis clients. 25 | Create a named pool, then use regular `er` commands against it. If you use 26 | a command requiring exclusive use of a client (b[lr]pop, brpoplpush, subscribe, watch, etc), 27 | the client is taken out of the general pool and reserved for your individual 28 | use. When the client is done with exclusive operations, the client is returned 29 | to the general connection pool. 30 | 31 | ### Connect and use anonymous pid 32 | {ok, Client} = er_pool:start_link(). 33 | er:set(Client, italian, greyhound). 34 | <<"greyhound">> = er:get(Client, italian). 35 | 36 | ### Connect and use named pool (no need for pid tracking) 37 | er_pool:start_link(userdb). 38 | er:set(userdb, hash_value_1, erlang:md5(<<"hash this">>). 39 | er:lpush(userdb, updates, hash_value_1). 40 | 41 | ### Transaction support (multi/exec/discard) 42 | er_pool:start_link(txn_readme_test). 43 | TxnFun = fun(Cxn) -> 44 | er:setnx(Cxn, hello, there), 45 | er:incr(Cxn, "counter:uid") 46 | end, 47 | er:er_transaction(txn_readme_test, TxnFun). 48 | 49 | ### Blocking Pop 50 | Blocking pop blocks the current process until Timeout (600 seconds below) or until 51 | an item becomes available. Blocking operations return the atom nil on timeout. 52 | er_pool:start_link(workqueue_pool). 53 | er:blpop(workqueue_pool, blocked_key, 600). 54 | 55 | ### PubSub 56 | er_pool:start_link(announcements). 57 | % Subscribe to key `another`. Returns a pid and subscribe validation. 58 | {SubClientPid, [subscribe, <<"another">>, 1]} = er:subscribe(announcements, another). 59 | 60 | % To receive published messages, run a blocking receive. 61 | % er_next blocks your current process until something is published. 62 | % You can run er:er_next(SubClientPid) in a loop to consume all published messages. 63 | [message, <<"bob">>, PublishedMessage] = er:er_next(SubClientPid). 64 | 65 | % Clean up when you are done to avoid leaking processes and sockets: 66 | SubClientPid ! shutdown. 67 | 68 | For per-command usage examples, see `test/er_tests.erl`. 69 | 70 | `er_redis.erl` was mainly taken from http://github.com/bmizerany/redis-erl/blob/master/src/redis.erl then 71 | heavily modified to fit my purposes better. Only 20 to 30 lines of the original file survived. 72 | 73 | Since `er` and `erp` perform the same functions only with slightly different 74 | interfaces, most of their code is shared by directly including common files. 75 | 76 | See files `include/{utils,utils-macro,redis-cmds,redis-return-types}.lfe`: 77 | 78 | * `utils.lfe` - shared utility functions 79 | * `utils-macro.lfe` - includes the macro which generates return value macros 80 | * `redis-cmds.lfe` - all redis commands and their return type handlers 81 | * `redis-return-types.lfe` - an alternate format of specifying redis return types. 82 | Contains functions for converting redis return values to native types. 83 | 84 | eru 85 | --- 86 | Module `eru` has utility functions to help you through your redis adventures. 87 | 88 | `eru:dump(Server)` -- 89 | dump all keys and values from a server regardless of type. 90 | 91 | `eru:dump(Server, Key)` -- 92 | print the key and value from a server regardless of type. 93 | 94 | `eru:er_key/1, eru:er_key/2, ..., eru:er_key/32` -- 95 | create a formatted redis key. Accepts atoms, integers, lists, and binaries. 96 | example: 97 | `eru:er_key(this, <<"is">>, "a", gr, 8, key)` = `<<"this:is:a:gr:8:key">>` 98 | 99 | 100 | Building 101 | -------- 102 | Download LFE: 103 | ./rebar get-deps 104 | 105 | Build: 106 | ./rebar compile 107 | 108 | Testing 109 | ------- 110 | NB: Tests run against a redis on port 6991. 111 | Running tests will DELETE ALL DATA on your port 6991 redis. 112 | rebar eunit skip_deps=true suite=er -v 113 | rebar eunit skip_deps=true suite=er_concurrency_tests -v 114 | 115 | Next Steps 116 | ---------- 117 | In no specific order: 118 | 119 | * More comprehensive test cases for er/erp 120 | * More features 121 | * bidirectional mnesia sync? 122 | * redis as erlang term store vs. redis as generic store 123 | * generate modules for pre-defined common accessors 124 | 125 | When to use er 126 | -------------- 127 | Use `er` when you want to access redis from erlang. 128 | 129 | `er` converts redis return types to erlang-friendly terms. redis 1 and 0 130 | responses are converted to true/false atoms when appropriate. redis binary 131 | results are returned as erlang binaries. redis integer results 132 | are returned as erlang numbers. 133 | 134 | `er` aims to be the most semantically correct erlang redis client with a sane 135 | translation layer between how redis views the world and how erlang views 136 | the world. 137 | 138 | A note on testing 139 | ----------------- 140 | `er` is just an interface to redis. We're more interested in testing 141 | the border between redis and erlang than testing redis itself. We don't 142 | need the entire redis test suite imported yet. (Though, if you want to 143 | write a redis tcl test suite to eunit translator go right ahead.) 144 | 145 | Testing is the exploratory way `er` becomes more correct over time. Find 146 | an untested redis command, use it, see the result. Did it return the right 147 | value with the proper type? Numbers got returned as N and not <<"N">>? 148 | True/False got returned as true/false and not 1/0? 149 | 150 | If the return types are wrong, jump into `include/redis-cmds.lfe` and 151 | update the return type for the command you are testing. 152 | 153 | If the redis command has optional arguments you may need to create 154 | multiple functions with varying airity. 155 | See anything using `withscores` as an example in `include/redis-cmds.lfe`. 156 | 157 | You can implement the same redis command multiple times but with different 158 | return types by specifying a unique function name. See `hgetall_p` and 159 | `hgetall_k` in `include/redis-cmds.lfe` for an example. 160 | 161 | Testing new commands may require modifying the return type processing 162 | functions themselves in `include/redis-return-types.lfe`. Jump in and 163 | make things better. 164 | 165 | Issues during testing to keep in mind: 166 | 167 | * rebar does not rebuild based on lfe includes. Delete ebin/ to rebuild. 168 | * Does the return type make sense? 169 | * No always-integers returned in binaries 170 | * Redis errors throw exceptions instead of returning error tuples 171 | * Did your input get auto-converted to a binary and sent to redis? 172 | * Can you read the same value back from redis? 173 | * Everything-to-binary conversion functions are at the end of `src/er_redis.erl` 174 | * Test creation, read, update, read 175 | * Test edge cases and boundary conditions 176 | * Test error conditions 177 | * Test impossibles (see if you can crash redis) 178 | * e.g. inf + -inf, nan + inf, -inf + 3, etc. 179 | * Will concurrent testing or fuzzing help? Add test suites as necessary. 180 | 181 | Contributions 182 | ------------- 183 | Want to help? Patches welcome. 184 | 185 | * Poke around in `test/er_tests.erl` and add some missing tests. Ideas: 186 | * Figure out how to implement `sort` nicely. 187 | * Make `info` output pretty 188 | * Test `eru` 189 | * Test long-lived things 190 | * publish/subscribe 191 | * multi/exec/discard (what should the interface for those look like?) 192 | * monitor 193 | * Write something to parse `include/er-cmds.lfe` and output docs for return types. 194 | * Make a nicer interface to pubsub and blocking calls. 195 | * Spawn an owner process for them you send/receive erlang messages to/from? 196 | * named? unnamed? how long lived? auto-reconnect on errors/timeout? 197 | * Figure out why markdown isn't auto-linking my User/Project@Commit references like the docs say it should 198 | * Find a bug. Fix a bug. 199 | -------------------------------------------------------------------------------- /src/er_pool.erl: -------------------------------------------------------------------------------- 1 | -module(er_pool). 2 | -behaviour(gen_server). 3 | 4 | %% gen_server callbacks 5 | -export([init/1, 6 | handle_call/3, handle_cast/2, 7 | handle_info/2, terminate/2, code_change/3]). 8 | 9 | %% api callbacks 10 | -export([start_link/0, start_link/1, start_link/3, start_link/4]). 11 | -export([start_link_nameless/2, start_link_nameless/3, start_link_nameless/4]). 12 | 13 | -record(state, {ip :: string(), 14 | port :: pos_integer(), 15 | available :: [pid()], 16 | reserved :: [pid()], 17 | error_strategy :: {retry, pos_integer()} | % retry count 18 | {wait, pos_integer()} | % retry every-N ms 19 | crash 20 | }). 21 | 22 | %%==================================================================== 23 | %% api callbacks 24 | %%==================================================================== 25 | % With names 26 | start_link() -> 27 | start_link(?MODULE). 28 | 29 | start_link(GenServerName) when is_atom(GenServerName) -> 30 | start_link(GenServerName, "127.0.0.1", 6379). 31 | 32 | start_link(GenServerName, IP, Port) when is_atom(GenServerName) -> 33 | start_link(GenServerName, IP, Port, 25). 34 | 35 | start_link(GenServerName, IP, Port, SocketCount) when is_atom(GenServerName) -> 36 | start_link(GenServerName, IP, Port, SocketCount, crash). 37 | 38 | start_link(GenServerName, IP, Port, SocketCount, Strategy) 39 | when is_atom(GenServerName) -> 40 | gen_server:start_link({local, GenServerName}, ?MODULE, 41 | [IP, Port, SocketCount, Strategy], []). 42 | 43 | % Without names 44 | start_link_nameless(IP, Port) -> 45 | start_link_nameless(IP, Port, 25). 46 | 47 | start_link_nameless(IP, Port, SocketCount) -> 48 | start_link_nameless(IP, Port, SocketCount, crash). 49 | 50 | start_link_nameless(IP, Port, SocketCount, Strategy) -> 51 | gen_server:start_link(?MODULE, [IP, Port, SocketCount, Strategy], []). 52 | 53 | %%==================================================================== 54 | %% gen_server callbacks 55 | %%==================================================================== 56 | 57 | %%-------------------------------------------------------------------- 58 | %% Function: init(Args) -> {ok, State} | 59 | %% {ok, State, Timeout} | 60 | %% ignore | 61 | %% {stop, Reason} 62 | %% Description: Initiates the server 63 | %%-------------------------------------------------------------------- 64 | init([IP, Port, SocketCount, Strategy]) when is_list(IP), is_integer(Port) -> 65 | process_flag(trap_exit, true), 66 | PreState = #state{ip = IP, port = Port, error_strategy = Strategy}, 67 | try initial_connect(SocketCount, PreState) of 68 | State -> {ok, State} 69 | catch 70 | throw:Error -> {stop, Error} 71 | end. 72 | 73 | %%-------------------------------------------------------------------- 74 | %% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | 75 | %% {reply, Reply, State, Timeout} | 76 | %% {noreply, State} | 77 | %% {noreply, State, Timeout} | 78 | %% {stop, Reason, Reply, State} | 79 | %% {stop, Reason, State} 80 | %% Description: Handling call messages 81 | %%-------------------------------------------------------------------- 82 | % These commands persisit state on one connection. We can't add 83 | % their connection back to the general pool. 84 | handle_call({cmd, Parts}, From, 85 | #state{available = [H|T], reserved = R} = State) when 86 | hd(Parts) =:= <<"multi">> orelse 87 | hd(Parts) =:= <<"watch">> orelse 88 | hd(Parts) =:= <<"subscribe">> orelse 89 | hd(Parts) =:= <<"psubscribe">> orelse 90 | hd(Parts) =:= <<"monitor">> -> 91 | spawn(fun() -> 92 | gen_server:reply(From, {H, gen_server:call(H, {cmd, Parts}, infinity)}) 93 | end), 94 | Caller = self(), 95 | spawn(fun() -> Caller ! add_connection end), 96 | {noreply, State#state{available = T, reserved = [H | R]}}; 97 | 98 | % Blocking list ops *do* block, but don't need to return their er_redis pid 99 | % Transactional returns should self-clean-up 100 | handle_call({cmd, Parts}, From, 101 | #state{available = [H|T], reserved = R} = State) when 102 | hd(Parts) =:= <<"exec">> orelse 103 | hd(Parts) =:= <<"discard">> orelse 104 | hd(Parts) =:= <<"blpop">> orelse 105 | hd(Parts) =:= <<"brpoplpush">> orelse 106 | hd(Parts) =:= <<"brpop">> -> 107 | Caller = self(), 108 | spawn(fun() -> 109 | gen_server:reply(From, gen_server:call(H, {cmd, Parts}, infinity)), 110 | Caller ! {done_processing_reserved, H} 111 | end), 112 | spawn(fun() -> Caller ! add_connection end), 113 | {noreply, State#state{available = T, reserved = [H | R]}}; 114 | 115 | handle_call({cmd, Parts}, From, 116 | #state{available = [H|T]} = State) -> 117 | spawn(fun() -> 118 | gen_server:reply(From, gen_server:call(H, {cmd, Parts}, infinity)) 119 | end), 120 | {noreply, State#state{available = T ++ [H]}}. 121 | 122 | %%-------------------------------------------------------------------- 123 | %% Function: handle_cast(Msg, State) -> {noreply, State} | 124 | %% {noreply, State, Timeout} | 125 | %% {stop, Reason, State} 126 | %% Description: Handling cast messages 127 | %%-------------------------------------------------------------------- 128 | handle_cast(_Msg, State) -> 129 | {noreply, State}. 130 | 131 | %%-------------------------------------------------------------------- 132 | %% Function: handle_info(Info, State) -> {noreply, State} | 133 | %% {noreply, State, Timeout} | 134 | %% {stop, Reason, State} 135 | %% Description: Handling all non call/cast messages 136 | %%-------------------------------------------------------------------- 137 | 138 | % A blocking or reserved operation finished. Add the reserved server 139 | % back into the available pool. 140 | handle_info({done_processing_reserved, Pid}, 141 | #state{available=Available, reserved=Reserved} = State) -> 142 | RemovedOld = Reserved -- [Pid], 143 | NewAvail = [Pid | Available], 144 | {noreply, State#state{available=NewAvail, reserved=RemovedOld}}; 145 | 146 | % An er_redis died because of a connection error. Do something. 147 | % {wait, N} and {retry, N} are not perfect right now. 148 | handle_info(add_connection, #state{available=Available} = State) -> 149 | try connect(State) of 150 | Connected -> {noreply, State#state{available=[Connected | Available]}} 151 | catch 152 | throw:Error -> run_error_strategy(Error, State) 153 | end; 154 | 155 | % An er_redis died because of a connection error. Do something. 156 | % {wait, N} and {retry, N} are not perfect right now. 157 | handle_info({'EXIT', _Pid, {er_connect_failed, _, _, _}} = Error, State) -> 158 | run_error_strategy(Error, State); 159 | 160 | % An er_redis died because of some other error. Remove it from list of servers. 161 | handle_info({'EXIT', Pid, _Reason}, 162 | #state{available=Available, reserved=Reserved} = State) -> 163 | try connect(State) of 164 | Connected -> 165 | case lists:member(Pid, Available) of 166 | true -> RemovedOld = Available -- [Pid], 167 | NewAvail = [Connected | RemovedOld], 168 | {noreply, State#state{available = NewAvail}}; 169 | false -> RemovedOld = Reserved -- [Pid], 170 | NewAvail = [Connected | Available], 171 | {noreply, State#state{available = NewAvail, 172 | reserved = RemovedOld}} 173 | end 174 | catch 175 | throw:Error -> run_error_strategy(Error, State) 176 | end; 177 | 178 | handle_info(shutdown, State) -> 179 | {stop, normal, State}; 180 | 181 | handle_info(Info, State) -> 182 | error_logger:error_msg("Other info: ~p with state ~p~n", [Info, State]), 183 | {noreply, State}. 184 | 185 | %%-------------------------------------------------------------------- 186 | %% Function: terminate(Reason, State) -> void() 187 | %% Description: This function is called by a gen_server when it is about to 188 | %% terminate. It should be the opposite of Module:init/1 and do any necessary 189 | %% cleaning up. When it returns, the gen_server terminates with Reason. 190 | %% The return value is ignored. 191 | %%-------------------------------------------------------------------- 192 | terminate(_Reason, #state{available=Available, reserved=Reserved}) -> 193 | [exit(P, normal) || P <- Available], 194 | [exit(P, normal) || P <- Reserved]. 195 | 196 | %%-------------------------------------------------------------------- 197 | %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} 198 | %% Description: Convert process state when code is changed 199 | %%-------------------------------------------------------------------- 200 | code_change(_OldVsn, State, _Extra) -> 201 | {ok, State}. 202 | 203 | %%-------------------------------------------------------------------- 204 | %%% Internal functions 205 | %%-------------------------------------------------------------------- 206 | initial_connect(SockCount, State) -> 207 | ErServers = [connect(State) || _ <- lists:seq(1, SockCount)], 208 | State#state{available = ErServers, reserved = []}. 209 | 210 | connect(#state{ip = IP, port = Port}) -> 211 | case er_redis:connect(IP, Port) of 212 | {ok, Server} -> Server; 213 | Other -> throw({er_pool, connect, Other}) 214 | end. 215 | 216 | run_error_strategy(ErError, #state{error_strategy = Strategy} = State) -> 217 | case Strategy of 218 | {wait, N} -> timer:sleep(N), 219 | {noreply, State}; 220 | {retry, N} -> case N > 0 of 221 | true -> {noreply, State#state{error_strategy={retry, N-1}}}; 222 | false -> {stop, max_retries_reached, State} 223 | end; 224 | _ -> {stop, {er_error, ErError}, State} 225 | end. 226 | -------------------------------------------------------------------------------- /include/redis-cmds.lfe: -------------------------------------------------------------------------------- 1 | ;; include/redis-cmds.lfe 2 | ;; What is this? All redis commands and then some. 3 | ;; This file gets directly included by src/er.lfe and src/erp.lfe 4 | ;; Return macros come from include/utils-macro.lfe which must be included 5 | ;; before this file in src/er.lfe and src/erp.lfe. 6 | 7 | ;; cmd macros define the return value for native erlang conversion 8 | ;; redis-cmd-n = nil return 9 | ;; redis-cmd-s = status return 10 | ;; redis-cmd-i = int return 11 | ;; redis-cmd-l = line return 12 | ;; redis-cmd-b = bulk return 13 | ;; redis-cmd-m = multibulk return 14 | ;; redis-cmd-m-pl = return a property list [{key, <<"value">>}, ..] (auto-atomize keys) 15 | ;; redis-cmd-m-kl = return a key list [{<<"key">>, <<"value">>}, ...] 16 | ;; redis-cmd-strip = convert things like {ok, Value} to Value 17 | ;; redis-cmd-o = other/special return 18 | ;; redis-cmd-i-tf = int return where 0 = false and 1 = true 19 | 20 | ;; Connection handling ;; 21 | 22 | ; close the connection 23 | (redis-cmd-n quit) 24 | 25 | ; simple password authentication if enabled 26 | (redis-cmd-s auth) 27 | 28 | ;; Connection commands ;; 29 | 30 | ; ping the server 31 | (redis-cmd-s ping) 32 | 33 | ; echo the given string 34 | (redis-cmd-b echo (_msg_)) 35 | 36 | ;; Commands operating on all the kind of values ;; 37 | 38 | ; test if a key exists 39 | ; returns true on exists; false on not exists 40 | (redis-cmd-i-tf exists (_key_)) 41 | 42 | ; delete a key 43 | ; returns number of keys deleted: 0 to N 44 | (redis-cmd-i del (_keys_)) 45 | 46 | ; return the type of the value stored at key 47 | (redis-cmd-s type (_key_)) 48 | 49 | ; return all the keys matching a given pattern 50 | (redis-cmd-b keys (_pattern_)) 51 | 52 | ; return a random key from the key space 53 | (redis-cmd-l randomkey) 54 | 55 | ; rename the old key in the new one, destroing the newname key if it already exists 56 | ; > er:rename(C, bob2, bob3). 57 | ; ** exception throw: {redis_error,<<"ERR no such key">>} 58 | ; > er:rename(C, bob3, bob3). 59 | ; ** exception throw: {redis_error,<<"ERR source and destination objects are the same">>} 60 | (redis-cmd-s rename (_oldname_ _newname_)) 61 | 62 | ; rename the old key in the new one, if the newname key does not already exist 63 | (redis-cmd-i-tf renamenx (_oldname_ _newname_)) 64 | 65 | ; return the number of keys in the current db 66 | (redis-cmd-i dbsize) 67 | 68 | ; set a time to live in seconds on a key 69 | (redis-cmd-i-tf expire (_key_ _seconds-forward_)) 70 | (redis-cmd-i-tf expireat (_key_ _unixtime_)) 71 | 72 | ; remove a previously set expire 73 | (redis-cmd-i-tf persist (_key_)) 74 | 75 | ; get the time to live in seconds of a key 76 | (redis-cmd-i ttl (_key_)) 77 | 78 | ; Select the DB having the specified index 79 | (redis-cmd-s select (_index_)) 80 | 81 | ; Move the key from the currently selected DB to the DB having as index dbindex 82 | (redis-cmd-i move (_key_ _dbindex_)) 83 | 84 | ; Remove all the keys of the currently selected DB 85 | (redis-cmd-s flushdb) 86 | 87 | ; Remove all the keys from all the databases 88 | (redis-cmd-s flushall) 89 | 90 | 91 | ;; Commands operating on string values ;; 92 | 93 | ; set a key to a string value 94 | (redis-cmd-s set (_key_ _value_)) 95 | 96 | ; return the string value of the key 97 | (redis-cmd-b get (_key_)) 98 | 99 | ; set a key to a string returning the old value of the key 100 | (redis-cmd-b getset (_key_ _value_)) 101 | 102 | ; multi-get, return the strings values of the keys 103 | ; _key1_ _key2_ ... _keyN_ 104 | (redis-cmd-m mget (_keys_)) 105 | 106 | ; set a key to a string value if the key does not exist 107 | (redis-cmd-i-tf setnx (_key_ _value_)) 108 | 109 | ; Set+Expire combo command 110 | (redis-cmd-s setex (_key_ _time_ _value_)) 111 | 112 | ; set a multiple keys to multiple values in a single atomic operation 113 | ; _key1_ _value1_ _key2_ _value2_ ... _keyN_ _valueN_ 114 | (redis-cmd-s mset (_key-value-pairs_)) 115 | 116 | ; set a multiple keys to multiple values in a single atomic operation if none of the keys already exist 117 | ; _key1_ _value1_ _key2_ _value2_ ... _keyN_ _valueN_ 118 | (redis-cmd-i-tf msetnx (_key-value-pairs_)) 119 | 120 | ; increment the integer value of key 121 | (redis-cmd-i incr (_key_)) 122 | 123 | ; increment the integer value of key by integer 124 | (redis-cmd-i incrby (_key_ _integer_)) 125 | 126 | ; decrement the integer value of key 127 | (redis-cmd-i decr (_key_)) 128 | 129 | ; decrement the integer value of key by integer 130 | (redis-cmd-i decrby (_key_ _integer_)) 131 | 132 | ; append the specified string to the string stored at key 133 | (redis-cmd-i append (_key_ _value_)) 134 | 135 | ; return a substring out of a larger string 136 | (redis-cmd-b substr (_key_ _start_ _end_)) ; substr = getrange in 2.2+ 137 | (redis-cmd-b getrange (_key_ _start_ _end_)) ; getrange = substr for redis 2.2+ 138 | 139 | (redis-cmd-i setrange (_key_ _start_ _end_)) 140 | 141 | (redis-cmd-i getbit (_key_ _position_)) 142 | (redis-cmd-i setbit (_key_ _position_ _value_)) 143 | 144 | ; return the length of a string 145 | (redis-cmd-i strlen (_key_)) 146 | 147 | ;; Commands operating on lists ;; 148 | 149 | ; Append an element to the tail of the List value at key 150 | (redis-cmd-i rpush (_key_ _value_)) 151 | 152 | ; Append an element to the head of the List value at key 153 | (redis-cmd-i lpush (_key_ _value_)) 154 | 155 | ; Return the length of the List value at key 156 | (redis-cmd-i llen (_key_)) 157 | 158 | ; Return a range of elements from the List at key 159 | (redis-cmd-m lrange (_key_ _start_ _end_)) 160 | 161 | ; Trim the list at key to the specified range of elements 162 | (redis-cmd-s ltrim (_key_ _start_ _end_)) 163 | 164 | ; Return the element at index position from the List at key 165 | (redis-cmd-b lindex (_key_ _index_)) 166 | 167 | ; Push on the left if the list exists. 168 | ; Returns number of elements in list. Returns zero if list isn't created. 169 | (redis-cmd-i lpushx (_key_ _value_)) 170 | 171 | ; Push on the right if the list exists. 172 | ; Returns number of elements in list. Returns zero if list isn't created. 173 | (redis-cmd-i rpushx (_key_ _value_)) 174 | 175 | ; Insert before or after a value in a list 176 | ; Returns the number of elements in list 177 | (redis-cmd-i linsert (_key_ _before_or_after_ _existing_value_ _new_value_)) 178 | 179 | ; Set a new value as the element at index position of the List at key 180 | (redis-cmd-s lset (_key_ _index_ _value_)) 181 | 182 | ; Remove the first-N, last-N, or all the elements matching value from the List at key 183 | (redis-cmd-i lrem (_key_ _count_ _value_)) 184 | 185 | ; Return and remove (atomically) the first element of the List at key 186 | (redis-cmd-b lpop (_key_)) 187 | 188 | ; Return and remove (atomically) the last element of the List at key 189 | (redis-cmd-b rpop (_key_)) 190 | 191 | ; Blocking LPOP 192 | ; _key1_ _key2_ ... _keyN_ _timeout_ 193 | (redis-cmd-m blpop (_keys_ _timeout_)) 194 | 195 | ; Blocking RPOP 196 | ; _key1_ _key2_ ... _keyN_ _timeout_ 197 | (redis-cmd-m brpop (_keys_ _timeout_)) 198 | 199 | ; Return and remove (atomically) the last element of the source List stored at _srckey_ and push the same element to the destination List stored at _dstkey_ 200 | (redis-cmd-b rpoplpush (_srckey_ _dstkey_)) 201 | 202 | ; Blocking rpoplpush 203 | (redis-cmd-b brpoplpush (_srckey_ _dstkey_)) 204 | 205 | 206 | 207 | ;; Commands operating on sets ;; 208 | 209 | ; Add the specified member to the Set value at key 210 | (redis-cmd-i-tf sadd (_key_ _member_)) 211 | 212 | ; Remove the specified member from the Set value at key 213 | (redis-cmd-i-tf srem (_key_ _member_)) 214 | 215 | ; Remove and return (pop) a random element from the Set value at key 216 | (redis-cmd-b spop (_key_)) 217 | 218 | ; Move the specified member from one Set to another atomically 219 | (redis-cmd-i-tf smove (_srckey_ _dstkey_ _member_)) 220 | 221 | ; Return the number of elements (the cardinality) of the Set at key 222 | (redis-cmd-i scard (_key_)) 223 | 224 | ; Test if the specified value is a member of the Set at key 225 | (redis-cmd-i-tf sismember (_key_ _member_)) 226 | 227 | ; Return the intersection between the Sets stored at key1, key2, ..., keyN 228 | ; _key1_ _key2_ ... _keyN_ 229 | (redis-cmd-m sinter (_keys_)) 230 | 231 | ; Compute the intersection between the Sets stored at key1, key2, ..., keyN, and store the resulting Set at dstkey 232 | ; _dstkey_ _key1_ _key2_ ... _keyN_ 233 | (redis-cmd-s sinterstore (_dstkey_ _keys_)) 234 | 235 | ; Return the union between the Sets stored at key1, key2, ..., keyN 236 | ; _key1_ _key2_ ... _keyN_ 237 | (redis-cmd-m sunion (_keys_)) 238 | 239 | ; Compute the union between the Sets stored at key1, key2, ..., keyN, and store the resulting Set at dstkey 240 | ; _dstkey_ _key1_ _key2_ ... _keyN_ 241 | (redis-cmd-s sunionstore (_dstkey_ _keys_)) 242 | 243 | ; Return the difference between the Set stored at key1 and all the Sets key2, ..., keyN 244 | ; _key1_ _key2_ ... _keyN_ 245 | (redis-cmd-m sdiff (_keys_)) 246 | 247 | ; Compute the difference between the Set key1 and all the Sets key2, ..., keyN, and store the resulting Set at dstkey 248 | ; _dstkey_ _key1_ _key2_ ... _keyN_ 249 | (redis-cmd-s sdiffstore (_dstkey_ _keys_)) 250 | 251 | ; Return all the members of the Set value at key 252 | (redis-cmd-m smembers (_key_)) 253 | 254 | ; Return a random member of the Set value at key 255 | (redis-cmd-b srandmember (_key_)) 256 | 257 | 258 | ;; Commands operating on sorted sets (zsets, Redis version >= 1.1) ;; 259 | 260 | ; Add the specified member to the Sorted Set value at key or update the score if it already exist 261 | (redis-cmd-i-tf zadd (_key_ _score_ _member_)) 262 | 263 | ; Remove the specified member from the Sorted Set value at key 264 | (redis-cmd-i-tf zrem (_key_ _member_)) 265 | 266 | ; If the member already exists increment its score by _increment_, otherwise add the member setting _increment_ as score 267 | (redis-cmd-i zincrby (_key_ _increment_ _member_)) 268 | 269 | ; Return the rank (or index) or _member_ in the sorted set at _key_, with scores being ordered from low to high 270 | ; NB: Docs say this returns bulk, but we treat it as returning an integer 271 | (redis-cmd-i zrank (_key_ _member_)) 272 | 273 | ; Return the rank (or index) or _member_ in the sorted set at _key_, with scores being ordered from high to low 274 | ; NB: Docs say this returns bulk, but we treat it as returning an integer 275 | (redis-cmd-i zrevrank (_key_ _member_)) 276 | 277 | ; Return a range of elements from the sorted set at key 278 | (redis-cmd-m zrange (_key_ _start_ _end_)) 279 | 280 | ; Return a range of elements from the sorted set at key, exactly like ZRANGE, but the sorted set is ordered in traversed in reverse order, from the greatest to the smallest score 281 | (redis-cmd-m zrevrange (_key_ _start_ _end_)) 282 | 283 | ; Return all the elements with score >= min and score <= max (a range query) from the sorted set 284 | (redis-cmd-m zrangebyscore (_key_ _min_ _max_)) 285 | 286 | ; Count the number of elements of a sorted set with a score that lays within a given interval 287 | (redis-cmd-i zcount (_key_ _lower_score_ _upper_score_)) 288 | 289 | ; Return the cardinality (number of elements) of the sorted set at key 290 | (redis-cmd-i zcard (_key_)) 291 | 292 | ; Return the score associated with the specified element of the sorted set at key 293 | (redis-cmd-b zscore (_key_ _element_)) 294 | 295 | ; Remove all the elements with rank >= min and rank <= max from the sorted set 296 | (redis-cmd-i zremrangebyrank (_key_ _min_ _max_)) 297 | 298 | ; Remove all the elements with score >= min and score <= max from the sorted set 299 | (redis-cmd-i zremrangebyscore (_key_ _min_ _max_)) 300 | 301 | ;; ER-ONLY functions. 302 | (redis-cmd-m-kl zrange zrange (_key_ _start_ _end_ withscores)) 303 | (redis-cmd-m-kl zrevrange zrevrange (_key_ _start_ _end_ withscores)) 304 | (redis-cmd-m-kl zrangebyscore zrangebyscore (_key_ _min_ _max_ withscores)) 305 | ;; END ER_ONLY functions. 306 | 307 | ; Perform a union or intersection over a number of sorted sets with optional weight and aggregate` 308 | ; _dstkey_ _N_ _key1_ ... _keyN_ WEIGHTS _w1_ ... _wN_ AGGREGATE SUM|MIN|MAX 309 | (redis-cmd-i zunionstore (_dstkey_ _n_ _key-spec_)) 310 | (redis-cmd-i zinterstore (_dstkey_ _n_ _key-spec_)) 311 | 312 | ;; Commands operating on hashes ;; 313 | 314 | ; Set the hash field to the specified value. Creates the hash if needed. 315 | (redis-cmd-i-tf hset (_key_ _field_ _value_)) 316 | 317 | ; Retrieve the value of the specified hash field. 318 | (redis-cmd-b hget (_key_ _field_)) 319 | 320 | ; Set the hash fields to their respective values. 321 | ; _key1_ _field1_ ... _fieldN_ 322 | (redis-cmd-m hmget (_key_ _fields_)) 323 | 324 | ; Set the hash fields to their respective values. 325 | ; _key_ _field1_ _value1_ ... _fieldN_ _valueN_ 326 | (redis-cmd-s hmset (_key_ _field-value-pairs_)) 327 | 328 | ; Increment the integer value of the hash at _key_ on _field_ with _integer_. 329 | (redis-cmd-i hincrby (_key_ _field_ _integer_)) 330 | 331 | ; Test for existence of a specified field in a hash 332 | (redis-cmd-i-tf hexists (_key_ _field_)) 333 | 334 | ; Remove the specified field from a hash 335 | (redis-cmd-i-tf hdel (_key_ _field_)) 336 | 337 | ; Return the number of items in a hash. 338 | (redis-cmd-i hlen (_key_)) 339 | 340 | ; Return all the fields in a hash. 341 | (redis-cmd-m hkeys (_key_)) 342 | 343 | ; Return all the values in a hash. 344 | (redis-cmd-m hvals (_key_)) 345 | 346 | ; Return all the fields and associated values in a hash. 347 | (redis-cmd-m hgetall (_key_)) 348 | 349 | ;; ER-ONLY functions. 350 | ; Return all the fields of a hash as a proplist (e.g. [{atom, Binary}]) 351 | (redis-cmd-m-pl hgetall_p hgetall (_key_)) 352 | ; Return all the fields of a hash as a keylist (e.g. [{Binary, Binary}]) 353 | (redis-cmd-m-kl hgetall_k hgetall (_key_)) 354 | ;; END ER_ONLY functions. 355 | 356 | ;; Sorting ;; 357 | 358 | ; Sort a Set or a List accordingly to the specified parameters` 359 | ; _key_ BY _pattern_ LIMIT _start_ _end_ GET _pattern_ ASC|DESC ALPHA 360 | (redis-cmd-m sort (_key_ _sort-definition_)) 361 | 362 | ;; Transactions ;; 363 | 364 | ; Redis atomic transactions 365 | (redis-cmd-s multi) 366 | (redis-cmd-o exec) ; return the redis return values without translation (?) 367 | (redis-cmd-s discard) 368 | 369 | ;; Publish/Subscribe ;; 370 | 371 | ; Redis Public/Subscribe messaging paradigm implementation 372 | (redis-cmd-strip subscribe (_channels_)) 373 | (redis-cmd-o unsubscribe (_channels_)) 374 | (redis-cmd-o unsubscribe) 375 | (redis-cmd-strip psubscribe (_channel-patterns_)) 376 | (redis-cmd-o punsubscribe (_channel-patterns_)) 377 | (redis-cmd-o punsubscribe) 378 | (redis-cmd-i publish (_channel_ _msg_)) 379 | 380 | ;; Persistence control commands ;; 381 | 382 | ; Synchronously save the DB on disk 383 | (redis-cmd-s save) 384 | 385 | ; Asynchronously save the DB on disk 386 | (redis-cmd-s bgsave) 387 | 388 | ; Get and/or set configuration parameters 389 | (redis-cmd-s config (_getset_ _params_and_or_values_)) 390 | 391 | ; Return the UNIX time stamp of the last successfully saving of the dataset on disk 392 | (redis-cmd-i lastsave) 393 | 394 | ; Synchronously save the DB on disk, then shutdown the server 395 | (redis-cmd-s shutdown) 396 | 397 | ; Rewrite the append only file in background when it gets too big 398 | (redis-cmd-s bgrewriteaof) 399 | 400 | 401 | ;; Remote server control commands ;; 402 | 403 | ; Provide information and statistics about the server 404 | (redis-cmd-b info) 405 | 406 | ; Dump all the received requests in real time 407 | (redis-cmd-o monitor) 408 | 409 | ; Change the replication settings 410 | (redis-cmd-s slaveof) 411 | -------------------------------------------------------------------------------- /test/er_tests.erl: -------------------------------------------------------------------------------- 1 | -module(er_tests). 2 | -include_lib("eunit/include/eunit.hrl"). 3 | 4 | -define(E(A, B), ?assertEqual(A, B)). 5 | -define(_E(A, B), ?_assertEqual(A, B)). 6 | 7 | redis_setup_clean() -> 8 | TestModule = er_redis, 9 | {ok, Cxn} = TestModule:start_link("127.0.0.1", 6991), 10 | {ok, _PoolPid} = er_pool:start_link(testing_pool, "127.0.0.1", 6991), 11 | ok = er:flushall(Cxn), 12 | Cxn. 13 | 14 | redis_cleanup(Cxn) -> 15 | Cxn ! shutdown, 16 | testing_pool ! shutdown. 17 | 18 | er_basic_commands_test_() -> 19 | {setup, 20 | fun redis_setup_clean/0, 21 | fun redis_cleanup/1, 22 | fun(C) -> 23 | [ 24 | ?_E(false, er:exists(C, existing)), 25 | ?_E(ok, er:set(C, existing, ralph)), 26 | ?_E(true, er:exists(C, existing)), 27 | ?_E(<<"ralph">>, er:get(C, existing)), 28 | ?_E(1, er:del(C, existing)), 29 | ?_E(0, er:del(C, existing)), 30 | ?_E(false, er:exists(C, existing)), 31 | ?_E([], er:keys(C, "*")), 32 | ?_E(nil, er:randomkey(C)), 33 | ?_E(ok, er:set(C, <<>>, ralph)), 34 | ?_E(<<>>, er:randomkey(C)), 35 | ?_E(<<"ralph">>, er:get(C, <<>>)), 36 | ?_E(1, er:del(C, <<>>)), 37 | ?_assertException(throw, 38 | {redis_error, <<"ERR no such key">>}, 39 | er:rename(C, bob2, bob3)), 40 | ?_E(ok, er:set(C, bob3, bob3content)), 41 | ?_assertException(throw, 42 | {redis_error, 43 | <<"ERR source and destination objects are the same">>}, 44 | er:rename(C, bob3, bob3)), 45 | ?_E(ok, er:rename(C, bob3, bob2)), 46 | ?_E(true, er:renamenx(C, bob2, bob3)), 47 | ?_E(ok, er:set(C, bob4, bob4content)), 48 | ?_E(false, er:renamenx(C, bob3, bob4)), 49 | ?_E(2, er:dbsize(C)), 50 | ?_E(ok, er:set(C, expireme, expiremecontent)), 51 | ?_E(true, er:expire(C, expireme, 30)), 52 | ?_E(true, er:expire(C, expireme, 30)), % behavior changed in redis 2.2 53 | ?_E(false, er:expire(C, expireme_noexist, 30)), 54 | ?_assertMatch(TTL when TTL =:= 29 orelse TTL =:= 30, 55 | er:ttl(C, expireme)), 56 | ?_E(true, er:setnx(C, abc123, abc)), 57 | ?_E(false, er:setnx(C, abc123, abc)), 58 | ?_E(false, er:msetnx(C, [abc123, abc, abc234, def])), 59 | ?_E(true, er:msetnx(C, [abc234, def, abc567, hij])), 60 | % getset, 61 | % mget, 62 | % setex, 63 | % mset, 64 | % incr, 65 | % incrby, 66 | % decr, 67 | % decrby, 68 | % append, 69 | 70 | % bitkey: 01000000 = @ 71 | ?_E(0, er:setbit(C, bitkey, 1, 1)), 72 | ?_E(<<01000000:8>>, er:get(C, bitkey)), 73 | ?_E(1, er:getbit(C, bitkey, 1)), 74 | ?_E(0, er:getbit(C, bitkey, 0)), 75 | ?_E(0, er:getbit(C, bitkey, 12)), 76 | ?_E(0, er:getbit(C, bitkey, 32)), 77 | ?_E(0, er:getbit(C, bitkey, 64)), 78 | ?_E(0, er:getbit(C, bitkey, 999)), 79 | % test setting arbitrarily large index 80 | ?_E(0, er:setbit(C, bitkey, 1024968, 1)), 81 | ?_E(0, er:getbit(C, bitkey, 1024967)), 82 | ?_E(1, er:getbit(C, bitkey, 1024968)), 83 | ?_E(0, er:getbit(C, bitkey, 1024969)), 84 | % binarykey 85 | ?_E(0, er:setbit(C, binarykey, 0, 1)), 86 | % 10000000 = 128 87 | ?_E(<<128>>, er:get(C, binarykey)), 88 | ?_E(0, er:setbit(C, binarykey, 7, 1)), 89 | % 10000001 = 128 90 | ?_E(<<129>>, er:get(C, binarykey)), 91 | ?_E(0, er:setbit(C, binarykey, 64, 1)), 92 | % binarykey = (see next three lines) 93 | % [10000001] 94 | % [00000000][00000000][00000000][00000000][00000000][00000000][00000000] 95 | % [1000000] 96 | % = 129, 8x0, 8x0, 8x0, 8x0, 8x0, 8x0, 8x0, 128 97 | ?_E(<<129,0,0,0,0,0,0,0,128>>, er:get(C, binarykey)), 98 | % binaryint 99 | % 5 = 00000101 100 | % *NB:* Binary 5 is not the same as ASCII 5. 101 | ?_E(ok,er:set(C, binaryint, <<5>>)), 102 | ?_E(0, er:getbit(C, binaryint, 0)), 103 | ?_E(0, er:getbit(C, binaryint, 1)), 104 | ?_E(0, er:getbit(C, binaryint, 2)), 105 | ?_E(0, er:getbit(C, binaryint, 3)), 106 | ?_E(0, er:getbit(C, binaryint, 4)), 107 | ?_E(1, er:getbit(C, binaryint, 5)), 108 | ?_E(0, er:getbit(C, binaryint, 6)), 109 | ?_E(1, er:getbit(C, binaryint, 7)), 110 | % nothinghere. substr got renamed getrange in redis 2.2 111 | % getrange is now just an alias to substr in redis 112 | ?_E(nil, er:getrange(C, nothinghere, 1, 3)), 113 | ?_E(nil, er:substr(C, nothinghere, 1, 3)), 114 | % rangekey tests 115 | ?_E(ok, er:set(C, rangekey, "Hello")), 116 | ?_E(<<"ell">>, er:getrange(C, rangekey, 1, 3)), 117 | ?_E(<<"ell">>, er:substr(C, rangekey, 1, 3)), 118 | ?_E(5, er:setrange(C, rangekey, 1, "no")), 119 | ?_E(<<"nolo">>, er:getrange(C, rangekey, 1, 4)), 120 | % zero padding happens when adding to a nonexistent key 121 | ?_E(13, er:setrange(C, rangeemptykey, 3, "Empty Test")), 122 | ?_E(<<0,0,0,"Empty Test">>, er:get(C, rangeemptykey)), 123 | % reading past the length of a string 124 | ?_E(nil, er:getrange(C, rangekey, 64, 32)), 125 | ?_E(nil, er:substr(C, rangekey, 64, 32)) 126 | ] 127 | end 128 | }. 129 | 130 | er_lists_commands_test_() -> 131 | {setup, 132 | fun redis_setup_clean/0, 133 | fun redis_cleanup/1, 134 | fun(C) -> 135 | [ 136 | % [] 137 | ?_E(1, er:rpush(C, listA, aitem1)), 138 | % aitem1 139 | ?_E(2, er:rpush(C, listA, aitem2)), 140 | % aitem1, aitem2 141 | ?_E(3, er:rpush(C, listA, aitem3)), 142 | % aitem1, aitem2, aitem3 143 | ?_E(4, er:lpush(C, listA, aitem4)), 144 | % aitem4, aitem1, aitem2, aitem3 145 | ?_E(5, er:lpush(C, listA, aitem5)), 146 | % aitem5, aitem4, aitem1, aitem2, aitem3 147 | ?_E(6, er:lpush(C, listA, aitem6)), 148 | % aitem6, aitem5, aitem4, aitem1, aitem2, aitem3 149 | ?_E(6, er:llen(C, listA)), 150 | ?_E([<<"aitem6">>], er:lrange(C, listA, 0, 0)), 151 | ?_E([<<"aitem6">>, <<"aitem5">>], er:lrange(C, listA, 0, 1)), 152 | ?_E([], er:lrange(C, listA, 10, 20)), 153 | ?_E(7, er:lpushx(C, listA, aitem7)), 154 | % aitem7, aitem6, aitem5, aitem4, aitem1, aitem2, aitem3 155 | ?_E(8, er:rpushx(C, listA, aitem3nd)), 156 | % aitem7, aitem6, aitem5, aitem4, aitem1, aitem2, aitem3, aitem3nd 157 | ?_E(0, er:lpushx(C, noneList, aitemZ)), 158 | ?_E(0, er:rpushx(C, noneList, aitemZ)), 159 | ?_E(9, er:linsert(C, listA, before, aitem5, aitemNew)), 160 | % aitem7, aitem6, aitemNew, aitem5, aitem4, aitem1, aitem2, aitem3, aitem3nd 161 | ?_E(10, er:linsert(C, listA, 'after', aitem1, aitemNew2)), 162 | % aitem7, aitem6, aitemNew, aitem5, aitem4, aitem1, aitemNew2, aitem2, aitem3, aitem3nd 163 | ?_E(<<"aitem6">>, er:lindex(C, listA, 1)), 164 | ?_E(<<"aitem1">>, er:lindex(C, listA, 5)), 165 | % aitem7, aitem6, aitemNew, aitem5, aitem4, aitem1, aitemNew2, aitem2, aitem3, aitem3nd 166 | ?_E([<<"listA">>, <<"aitem7">>], er:blpop(C, listA, 3000)), 167 | % aitem6, aitemNew, aitem5, aitem4, aitem1, aitemNew2, aitem2, aitem3, aitem3nd 168 | ?_E([<<"listA">>, <<"aitem3nd">>], er:brpop(C, listA, 3000)), 169 | % aitem6, aitemNew, aitem5, aitem4, aitem1, aitemNew2, aitem2, aitem3 170 | ?_E(<<"aitem6">>, er:lpop(C, listA)), 171 | % aitemNew, aitem5, aitem4, aitem1, aitemNew2, aitem2, aitem3 172 | ?_E(<<"aitem3">>, er:rpop(C, listA)) 173 | % aitemNew, aitem5, aitem4, aitem1, aitemNew2 174 | 175 | % ltrim 176 | % lset 177 | % lrem 178 | % rpoplpush 179 | % brpoplpush 180 | ] 181 | end 182 | }. 183 | 184 | er_sets_commands_test_() -> 185 | {setup, 186 | fun redis_setup_clean/0, 187 | fun redis_cleanup/1, 188 | fun(C) -> 189 | [ 190 | ?_E(true, er:sadd(C, setA, amember1)), 191 | ?_E(false, er:sadd(C, setA, amember1)), 192 | ?_E(true, er:sadd(C, setA, amember2)), 193 | ?_E(true, er:sadd(C, setA, amember3)), 194 | ?_E(true, er:srem(C, setA, amember1)), 195 | ?_E(false, er:srem(C, setA, amember1)), 196 | ?_assertMatch(M when M =:= <<"amember2">> orelse M =:= <<"amember3">>, 197 | er:spop(C, setA)), 198 | ?_assertMatch(M when M =:= <<"amember2">> orelse M =:= <<"amember3">>, 199 | er:spop(C, setA)), 200 | ?_E(nil, er:spop(C, setA)), 201 | ?_E(0, er:scard(C, setA)), 202 | ?_E(true, er:sadd(C, setB, bmember1)), 203 | ?_E(true, er:sadd(C, setB, bmember2)), 204 | ?_E(true, er:sadd(C, setB, bmember3)), 205 | ?_E(3, er:scard(C, setB)), 206 | ?_E(false, er:smove(C, setB, setA, bmembernone)), 207 | ?_E(true, er:smove(C, setB, setA, bmember1)), 208 | ?_E(1, er:scard(C, setA)), 209 | ?_E(2, er:scard(C, setB)), 210 | ?_E(true, er:sismember(C, setB, bmember2)), 211 | ?_E(false, er:sismember(C, setB, bmember9)) 212 | % sinter 213 | % sinterstore 214 | % sunion 215 | % sunionstore 216 | % sdiff 217 | % sdiffstore 218 | % smembers 219 | % srandmember 220 | ] 221 | end 222 | }. 223 | 224 | er_sorted_sets_commands_test_() -> 225 | {setup, 226 | fun redis_setup_clean/0, 227 | fun redis_cleanup/1, 228 | fun(C) -> 229 | [ 230 | ?_E(true, er:zadd(C, zsetA, 10, amember1)), 231 | ?_E(false, er:zadd(C, zsetA, 10, amember1)), 232 | ?_E(true, er:zadd(C, zsetA, 10, amember2)), 233 | ?_E(true, er:zadd(C, zsetA, 10, amember3)), 234 | ?_E(true, er:zrem(C, zsetA, amember3)), 235 | ?_E(false, er:zrem(C, zsetA, amembernone)), 236 | ?_E(20, er:zincrby(C, zsetA, 10, amember1)), 237 | ?_E(-20, er:zincrby(C, zsetA, -40, amember1)), 238 | ?_E(0, er:zrank(C, zsetA, amember1)), 239 | ?_E(1, er:zrank(C, zsetA, amember2)), 240 | ?_E(1, er:zrevrank(C, zsetA, amember1)), 241 | ?_E(0, er:zrevrank(C, zsetA, amember2)), 242 | ?_E(inf, er:zincrby(C, zsetA, "inf", amember1)), 243 | ?_E(inf, er:zincrby(C, zsetA, "inf", amember1)), 244 | ?_E(1, er:zrank(C, zsetA, amember1)), % -20 + inf = top of list 245 | ?_E(0, er:zrank(C, zsetA, amember2)), 246 | ?_assertException(throw, 247 | {redis_error, <<"ERR resulting score is not a number (NaN)">>}, 248 | er:zincrby(C, zsetA, "-inf", amember1)), 249 | ?_assertException(throw, 250 | {redis_error, <<"ERR resulting score is not a number (NaN)">>}, 251 | er:zincrby(C, zsetA, "-inf", amember1)), 252 | ?_E(1, er:zrank(C, zsetA, amember1)), % at position inf, it moves up 253 | ?_E(0, er:zrank(C, zsetA, amember2)), 254 | ?_E([<<"amember2">>, <<"amember1">>], er:zrange(C, zsetA, 0, 3)), 255 | ?_E([{<<"amember2">>, <<"10">>}, 256 | {<<"amember1">>, <<"inf">>}], er:zrange(C, zsetA, 0,3,withscores)), 257 | ?_E([<<"amember1">>, <<"amember2">>], er:zrevrange(C, zsetA, 0, 3)), 258 | ?_E([{<<"amember1">>, <<"inf">>}, 259 | {<<"amember2">>, <<"10">>}], er:zrevrange(C,zsetA,0,3,withscores)) 260 | % zrangebyscore 261 | % zcard 262 | % zscore 263 | % zremrangebyrank 264 | % zremrangebyscore 265 | % zunionstore 266 | % zinterstore 267 | ] 268 | end 269 | }. 270 | 271 | er_hashes_commands_test_() -> 272 | {setup, 273 | fun redis_setup_clean/0, 274 | fun redis_cleanup/1, 275 | fun(C) -> 276 | [ 277 | ?_E(true, er:hset(C, hashA, fieldA, valueA)), 278 | ?_E(true, er:hset(C, hashA, fieldB, valueB)), 279 | ?_E(true, er:hset(C, hashA, fieldC, valueC)), 280 | ?_E(false, er:hset(C, hashA, fieldA, valueA1)), 281 | ?_E(<<"valueA1">>, er:hget(C, hashA, fieldA)), 282 | ?_E(nil, er:hget(C, hashB, fieldZ)), 283 | ?_E([nil, <<"valueA1">>, <<"valueC">>], 284 | er:hmget(C, hashA, [fieldNone, fieldA, fieldC])), 285 | ?_E([<<"valueA1">>, <<"valueC">>, nil], 286 | er:hmget(C, hashA, [fieldA, fieldC, fieldNone])), 287 | ?_E([nil, <<"valueA1">>, nil, <<"valueC">>, nil], 288 | er:hmget(C, hashA, [fieldZombie, fieldA, 289 | fieldDead, fieldC, 290 | fieldNone])), 291 | ?_E([<<"valueA1">>, <<"valueC">>], 292 | er:hmget(C, hashA, [fieldA, fieldC])), 293 | ?_E([nil], er:hmget(C, hashNone, [fieldNothing])), 294 | ?_E(ok, er:hmset(C, hashC, [fieldA, [], fieldB, valB])), 295 | ?_E(ok, er:hmset(C, hashC, [fieldA, valA, fieldB, valB])), 296 | ?_E(<<"valA">>, er:hget(C, hashC, fieldA)), 297 | ?_E(<<"valB">>, er:hget(C, hashC, fieldB)), 298 | ?_E(12, er:hincrby(C, hashD, fieldAddr, 12)), 299 | ?_E(72, er:hincrby(C, hashD, fieldAddr, 60)), 300 | ?_E(true, er:hexists(C, hashD, fieldAddr)), 301 | ?_E(false, er:hexists(C, hashD, fieldBddr)), 302 | ?_E(false, er:hexists(C, hashZ, fieldZ)), 303 | ?_E(true, er:hdel(C, hashD, fieldAddr)), 304 | ?_E(false, er:hdel(C, hashD, fieldAddr)), 305 | ?_E(3, er:hlen(C, hashA)), 306 | ?_E([<<"fieldA">>, <<"fieldB">>, <<"fieldC">>], 307 | er:hkeys(C, hashA)), 308 | ?_E([<<"valueA1">>, <<"valueB">>, <<"valueC">>], 309 | er:hvals(C, hashA)), 310 | ?_E([<<"fieldA">>, <<"valueA1">>, 311 | <<"fieldB">>, <<"valueB">>, 312 | <<"fieldC">>, <<"valueC">>], 313 | er:hgetall(C, hashA)), 314 | ?_E([{fieldA, <<"valueA1">>}, 315 | {fieldB, <<"valueB">>}, 316 | {fieldC, <<"valueC">>}], 317 | er:hgetall_p(C, hashA)), 318 | ?_E([{<<"fieldA">>, <<"valueA1">>}, 319 | {<<"fieldB">>, <<"valueB">>}, 320 | {<<"fieldC">>, <<"valueC">>}], 321 | er:hgetall_k(C, hashA)), 322 | ?_E(ok, er:hmset(C, hashHasEmpty, [fieldA, valA, fieldB, <<"">>])), 323 | ?_E([<<"fieldA">>, <<"valA">>, 324 | <<"fieldB">>, <<"">>], 325 | er:hgetall(C, hashHasEmpty)) 326 | ] 327 | end 328 | }. 329 | 330 | er_sorting_commands_test_() -> 331 | {setup, 332 | fun redis_setup_clean/0, 333 | fun redis_cleanup/1, 334 | fun(_C) -> 335 | [ 336 | % sort 337 | ] 338 | end 339 | }. 340 | 341 | er_transactions_commands_test_() -> 342 | {setup, 343 | fun redis_setup_clean/0, 344 | fun redis_cleanup/1, 345 | fun(IndividualRedis) -> 346 | TxnA = fun(C) -> 347 | queued = er:setnx(C, bob, four), 348 | queued = er:setnx(C, bob, three) 349 | end, 350 | TxnB = fun(C) -> 351 | er:incr(C, bob), 352 | er:setnx(C, bob, three) 353 | end, 354 | TxnC = fun(C) -> 355 | queued = er:incr(C, incrementer), 356 | er:discard(C) 357 | end, 358 | [ 359 | % 1 = success, 0 = failure for setnx 360 | ?_E([1, 0], er:er_transaction(testing_pool, TxnA)), 361 | ?_E([0, 0], er:er_transaction(IndividualRedis, TxnA)), 362 | % Errors during a MULTI are thrown. Since the entire multi wasn't run, 363 | % we don't care about the individual statuses from other returns. 364 | ?_assertException(throw, 365 | {redis_error,<<"ERR value is not an integer or out of range">>}, 366 | er:er_transaction(testing_pool, TxnB)), 367 | ?_assertException(throw, 368 | {redis_error,<<"ERR value is not an integer or out of range">>}, 369 | er:er_transaction(IndividualRedis, TxnB)), 370 | ?_E(discarded, er:er_transaction(testing_pool, TxnC)), 371 | ?_E(discarded, er:er_transaction(IndividualRedis, TxnC)) 372 | % watch -- test concurrently 373 | % unwatch -- test concurrently 374 | ] 375 | end 376 | }. 377 | 378 | er_pubsub_commands_test_() -> 379 | {setup, 380 | fun redis_setup_clean/0, 381 | fun redis_cleanup/1, 382 | fun(_C) -> 383 | [ 384 | % subscribe 385 | % unsubscribe 386 | % psubscribe 387 | % punsubscribe 388 | % publish 389 | ] 390 | end 391 | }. 392 | 393 | er_persistence_commands_test_() -> 394 | {setup, 395 | fun redis_setup_clean/0, 396 | fun redis_cleanup/1, 397 | fun(_C) -> 398 | [ 399 | % save 400 | % bgsave 401 | % lastsave 402 | % shutdown 403 | % bgrewriteaof 404 | ] 405 | end 406 | }. 407 | 408 | eru_test_() -> 409 | {setup, 410 | fun redis_setup_clean/0, 411 | fun redis_cleanup/1, 412 | fun(C) -> 413 | [ 414 | ?_E(ok, er:hmset(C, foo, [one, two, three, four, five, six])), 415 | ?_E([<<"one">>,<<"two">>,<<"three">>, 416 | <<"four">>,<<"five">>, <<"six">>], er:hgetall(C, foo)), 417 | ?_E(ok, eru:hcopy(C, foo, foo_copy)), 418 | ?_E([<<"one">>,<<"two">>,<<"three">>, 419 | <<"four">>,<<"five">>, <<"six">>], er:hgetall(C, foo_copy)) 420 | ] 421 | end 422 | }. 423 | 424 | er_server_control_commands_test_() -> 425 | {setup, 426 | fun redis_setup_clean/0, 427 | fun redis_cleanup/1, 428 | fun(C) -> 429 | [ 430 | ?_E(pong, er:ping(C)), 431 | ?_E(<<"hello there">>, er:echo(C, "hello there")) 432 | % info 433 | % monitor 434 | % slaveof 435 | % config 436 | ] 437 | end 438 | }. 439 | 440 | er_return_test_() -> 441 | {inparallel, 442 | [ 443 | ?_E(nil, er:'redis-return-nil'(nil)), 444 | ?_E(ok, er:'redis-return-status'([<<"ok">>])), 445 | ?_assertException(throw, {redis_error, <<"throwed">>}, 446 | er:'redis-return-status'({error, <<"throwed">>})), 447 | ?_E(53, er:'redis-return-integer'([<<"53">>])), 448 | ?_E(inf, er:'redis-return-integer'([<<"inf">>])), 449 | ?_E('-inf', er:'redis-return-integer'([<<"-inf">>])), 450 | ?_E(<<"ok">>, er:'redis-return-single-line'([<<"ok">>])), 451 | ?_E(ok, er:'redis-return-bulk'(ok)), 452 | ?_E(ok, er:'redis-return-multibulk'(ok)), 453 | ?_E(nil, er:'redis-return-multibulk'({ok, nil})), 454 | ?_E(ok, er:'redis-return-special'(ok)), 455 | ?_E(true, er:'redis-return-integer-true-false'([<<"1">>])), 456 | ?_E(false, er:'redis-return-integer-true-false'([<<"0">>])), 457 | ?_E([], er:'redis-return-multibulk-pl'([])), 458 | ?_E([], er:'redis-return-multibulk-kl'([])), 459 | ?_E([{hello, <<"ok">>}], 460 | er:'redis-return-multibulk-pl'([{ok, <<"hello">>}, {ok, <<"ok">>}])), 461 | ?_E([{hello, <<"ok">>}, {hello2, <<"ok2">>}], 462 | er:'redis-return-multibulk-pl'([{ok, <<"hello">>}, {ok, <<"ok">>}, 463 | {ok, <<"hello2">>}, {ok, <<"ok2">>}])), 464 | ?_E([{<<"hello">>, <<"ok">>}], 465 | er:'redis-return-multibulk-kl'([{ok, <<"hello">>}, {ok, <<"ok">>}])), 466 | ?_E([{<<"hello">>, <<"ok">>}, {<<"hello2">>, <<"ok2">>}], 467 | er:'redis-return-multibulk-kl'([{ok, <<"hello">>}, {ok, <<"ok">>}, 468 | {ok, <<"hello2">>}, {ok, <<"ok2">>}])) 469 | ] 470 | }. 471 | 472 | --------------------------------------------------------------------------------