├── .gitignore ├── Makefile ├── README.md ├── TODO ├── include ├── redis.hrl ├── redis_internal.hrl └── redis_log.hrl ├── src ├── Makefile ├── overview.edoc ├── redis.app.src ├── redis.erl ├── redis_client.erl ├── redis_conn_sup.erl └── redis_proto.erl ├── test ├── redis-benchmark ├── redis.coverspec ├── redis_SUITE.erl ├── redis_pubsub_SUITE.erl └── test.spec └── vsn.mk /.gitignore: -------------------------------------------------------------------------------- 1 | ebin 2 | logs 3 | tags 4 | src/*.beam 5 | src/tags 6 | test/redis-insert 7 | 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ## -*- makefile -*- 2 | SHELL := /bin/bash 3 | include vsn.mk 4 | .PHONY: all test edoc plt dialyzer tags clean dist install uninstall 5 | 6 | PLT=".dialyzer_plt" 7 | ERL_LIB := $(shell erl -noshell -eval 'io:format("~s",[code:lib_dir()]),erlang:halt()' \ 8 | 2> /dev/null) 9 | APP_FULLNAME := $(APP_NAME)-$(APP_VSN) 10 | 11 | ifdef LOG 12 | MAKE_ARGS = 13 | else 14 | MAKE_ARGS ='NOLOG=true' 15 | endif 16 | 17 | all: compile 18 | 19 | compile: 20 | (mkdir -p ./ebin) 21 | (cd src;$(MAKE) $(MAKE_ARGS)) 22 | 23 | test_compile: 24 | (mkdir -p ./ebin) 25 | (cd src;$(MAKE) TEST=true $(MAKE_ARGS)) 26 | 27 | test: clean unit_test comm_test 28 | @#(make clean) 29 | 30 | unit_test: test_compile 31 | (erl -noinput -pa ./ebin -eval "eunit:test(\"./ebin\", []), init:stop()") 32 | 33 | comm_test: test_compile 34 | (mkdir -p ./test/log) 35 | @echo "pwd is `pwd`" 36 | (erl -s ct_run script_start -logdir `pwd`/test/log -include `pwd`/include -pa `pwd`/ebin -pa `pwd`/test-dir . -spec ./test/test.spec -s erlang halt) 37 | 38 | edoc: 39 | (mkdir -p ./edoc) 40 | (cd src; $(MAKE) edoc) 41 | 42 | plt : 43 | (./scripts/gen_plt.sh -a sasl -a compiler) 44 | 45 | dialyzer: clean 46 | (cd src;$(MAKE) DEBUG=true) 47 | (dialyzer --plt $(PLT) -Werror_handling -Wrace_conditions -r .) 48 | 49 | tags : 50 | (ctags -R .) 51 | 52 | clean: 53 | (rm -rf ./ebin/*; rm -rf ./edoc/*) 54 | (rm -rf ./test/log) 55 | (rm -rf ./test/*.beam) 56 | (cd src;$(MAKE) clean) 57 | 58 | dist: clean 59 | (cd .. \ 60 | && tar czf $(APP_FULLNAME).tar.gz \ 61 | $(APP_NAME)/src $(APP_NAME)/include \ 62 | $(APP_NAME)/Makefile $(APP_NAME)/vsn.mk) 63 | (mv ../$(APP_FULLNAME)-$(APP_VSN).tar.gz .) 64 | 65 | install: all 66 | ifeq ($(ERL_LIB), ) 67 | @echo "please install Erlang/OTP" 68 | else 69 | @echo "install..." 70 | (mkdir -p $(ERL_LIB)/$(APP_FULLNAME) && \ 71 | cp -rf ./ebin ./include ./src $(ERL_LIB)/$(APP_FULLNAME)) 72 | endif 73 | 74 | uninstall: 75 | @echo "uninstall the lib..." 76 | (rm -rf $(ERL_LIB)/$(APP_FULLNAME)) 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README 2 | erl-redis is redis client library for erlang. 3 | you can get much more information about redis at http://github.com/antirez/redis. 4 | 5 | ## Install 6 | At first, you must install and start the redis server. 7 | 8 | Then let us do some tests, you must start three redis-server bind 6379 in 9 | localhost for test usage. after above, we can do the test: 10 | $ make test 11 | 12 | if some test cases not passed, you need to do some toubleshooting: update your redis-server 13 | to new version or disable some redis commands. 14 | 15 | if all the test case passed, now you can install the redis client: 16 | 17 | $ make 18 | $ make install 19 | 20 | the make install statement will install the redis library into the 21 | $ERL_TOP/lib/ directory (kernel, stdlib also in that directory). 22 | now you can use the redis client in your application. 23 | (NOTE: we recommend you first run make test when you want to use erl-redis client, because 24 | different redis server versions have some trivial differences in protocol.) 25 | 26 | ## Usage 27 | 28 | the redis client includes four source files: `redis.erl`, `redis_client.erl`, `redis_conn_sup.erl`, 29 | `redis_proto.erl`. In this version, we do not support the multiple redis servers with some 30 | hashing algorithms, because redis is not used as an cache (just like memcached), it support 31 | the persistent storage, so about the cluster we need to think so much cases. So we just implement 32 | a simple redis client accrodding to the redis protocol. we will go along with the redis-cluster. 33 | 34 | the erl-redis now support all the commands in redis-1.3.5, all the commands now implemented with 35 | the new mbulk protocol. we have complete the follow commands: 36 | 37 | * generic commands 38 | * string commands 39 | * list commands 40 | * set commands 41 | * sorted set commands 42 | * hash commands 43 | * transaction(MULT/DISCARD/EXEC) commands 44 | * sort commands 45 | * pubsub commands 46 | * persistence commands 47 | 48 | all the API is in the src/redis.erl. 49 | 50 | ### usage scenarios 51 | e.g. 1 (no registered name) 52 | {ok, Pid} = redis_client:start(Host, Port, ""), 53 | Redis = redics_client:handler(Pid), 54 | Redis:set("k1", "v1"), 55 | Redis:get("k1"), 56 | redis_client:stop(Redis). 57 | 58 | e.g. 2 (with registered name) 59 | Name = redis_client:name(Host, Port), 60 | {ok, Pid} = redis_client:start(Host, Port, "passwd", Name), 61 | Redis = redics_client:handler(Name), 62 | Redis:set("k1", "v1"), 63 | Redis:get("k1"), 64 | redis_client:stop(Redis). 65 | 66 | e.g. 3 (use the redis in OTP) 67 | % in main supervisor: 68 | {redis_conn_sup, {redis_conn_sup, start_link, []}, 69 | permanent, 1000, supervisor, [redis_client]} 70 | 71 | % start a redis client: 72 | Name = redis_client:name(Host, Port), 73 | {ok, _} = redis_conn_sup:connect(Host, Port, Pass, Name), 74 | Redis = redis_client:handler(Name), 75 | Redis:set("k1", "v1"), 76 | Redis:get("k1"). 77 | 78 | e.g. 4 (use in OTP with connection pool) 79 | % in main supervisor: 80 | {redis_conn_sup, {redis_conn_sup, start_link, []}, 81 | permanent, 1000, supervisor, [redis_client]} 82 | 83 | % start client pool 84 | [begin 85 | Name = redis_client:name(Host, Port, I), 86 | {ok, _} = redis_conn_sup:connect(Host, Port, Pass, Name) 87 | end || I <- lists:seq(1, 5)], 88 | 89 | % random select a client 90 | Selected = redis_client:existing_name(Host, Port, random:uniform(5)), 91 | Redis = redis_client:handler(Selected), 92 | Redis:set("k1", "v1"), 93 | Redis:get("k1"). 94 | 95 | e.g.5 (use in OTP by the auxiliary functions) 96 | % in main supervisor: 97 | [redis_conn_sup:sup_spec()] 98 | 99 | % start the client pool in the main supervisor init functions 100 | {ok, Pid} = superviosr:start_link .... 101 | redis_conn_sup:sup_start_client(Host, Port, Pass, Pool), 102 | {ok, Pid}. 103 | 104 | % in the code, random select one client 105 | Redis = redis_conn_sup:sup_rand_client(Host, Port, Pass, Pool), 106 | % or 107 | % Redis = redis_conn_sup:sup_rand_client(), 108 | Redis:set("k1", "v1"), 109 | Redis:get("k1"). 110 | 111 | ## Benchmark 112 | in my laptop, 113 | CPU: Intel(R) Core(TM)2 Duo CPU T5870) 114 | OS: ubuntu 10.04 115 | redis_version:2.1.1 116 | arch_bits:32 117 | run the test/redis-benchmark, the benchmark result is: 118 | 119 | ./redis-benchmark -s localhost -n 10000 -c 10 -o 10 120 | usage: 121 | redis-benchmark [options] 122 | -s Server - the redis server 123 | -p Port - the redis server port (default 6379) 124 | -c Clients - the concurrent request client number (default 5) 125 | -n Count - the total requests count (default 10000) 126 | -o Pool - the connection pool size (default 5) 127 | 128 | ===== "PING" ====== 129 | 10000 requests completed in 0.50 s 130 | 20130 requests per second 131 | ===== "SET" ====== 132 | 10000 requests completed in 0.67 s 133 | 14907 requests per second 134 | ===== "GET" ====== 135 | 10000 requests completed in 0.68 s 136 | 14669 requests per second 137 | ===== "INCR" ====== 138 | 10000 requests completed in 0.65 s 139 | 15444 requests per second 140 | ===== "LPUSH" ====== 141 | 10000 requests completed in 0.50 s 142 | 20158 requests per second 143 | ===== "LPOP" ====== 144 | 10000 requests completed in 0.64 s 145 | 15726 requests per second 146 | 147 | ## Version 148 | the erl-redis version is synchronous with redis server version. 149 | Subtracting one from the Redis server major version will get the erl-redis version. 150 | e.g. if you want to request the Redis 1.2.6, you need the erl-redis with version 0.2.6 151 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | test connection, server commands 2 | -------------------------------------------------------------------------------- /include/redis.hrl: -------------------------------------------------------------------------------- 1 | %%%---------------------------------------------------------------------- 2 | %%% 3 | %%% @copyright erl-redis 2010 4 | %%% 5 | %%% @author litaocheng@gmail.com 6 | %%% @doc redis header file included by the client user 7 | %%% 8 | %%%---------------------------------------------------------------------- 9 | -ifndef(REDIS_HRL). 10 | -define(REDIS_HRL, ok). 11 | 12 | %% sort options argument 13 | %% more explanation see redis wiki 14 | -record(redis_sort, { 15 | by_pat = <<>> :: str(), 16 | get_pat = [] :: [str()], % get pattern list 17 | limit = null, % the limit info {Start, Count} 18 | asc = true, 19 | alpha = false, 20 | store = <<>> :: str() 21 | }). 22 | 23 | -type redis_sort() :: #redis_sort{}. 24 | 25 | -endif. % REDIS_HRL 26 | -------------------------------------------------------------------------------- /include/redis_internal.hrl: -------------------------------------------------------------------------------- 1 | %%%---------------------------------------------------------------------- 2 | %%% 3 | %%% @copyright erl-redis 2010 4 | %%% 5 | %%% @author litaocheng@gmail.com 6 | %%% @doc redis internal header file 7 | %%% 8 | %%%---------------------------------------------------------------------- 9 | -ifndef(REDIS_INTERNAL_HRL). 10 | -define(REDIS_INTERNAL_HRL, ok). 11 | 12 | -ifdef(TEST). 13 | -include_lib("eunit/include/eunit.hrl"). 14 | -endif. 15 | 16 | -include("redis_log.hrl"). 17 | -include("redis.hrl"). 18 | 19 | %% syntax similar with '?:' in c 20 | -ifndef(IF). 21 | -define(IF(C, T, F), (case (C) of true -> (T); false -> (F) end)). 22 | -endif. 23 | 24 | %% assert(1 =:= 1) 25 | -define(ASSERT(EXP), 26 | case (EXP) of 27 | true -> 28 | ok; 29 | false -> 30 | error(??EXP) 31 | end). 32 | 33 | 34 | -define(NONE, none). 35 | 36 | %% some convert macros 37 | -define(B2S(B), binary_to_list(B)). 38 | -define(S2B(S), list_to_binary(S)). 39 | 40 | -define(N2S(N), integer_to_list(N)). 41 | -define(S2N(S), list_to_integer(S)). 42 | 43 | -define(F2S(N), float_to_list(N)). 44 | -define(S2F(S), list_to_float(S)). 45 | 46 | -define(IOLIST2B(IO), iolist_to_binary(IO)). 47 | 48 | %% the separtor 49 | %-define(SEP, $\s). 50 | -define(SEP, <<"\s">>). 51 | 52 | %% the CRLF(carr 53 | %-define(CRLF, "\r\n"). 54 | -define(CRLF, <<"\r\n">>). 55 | %-define(CRLF_BIN, <<"\r\n">>). 56 | 57 | -define(FUN_NULL, '$fnull'). 58 | -define(CALL_FUN(V, Fun), 59 | case Fun of 60 | ?FUN_NULL -> 61 | V; 62 | _ -> 63 | Fun(V) 64 | end). 65 | 66 | %% the redis supervisor name 67 | -define(CONN_SUP, redis_conn_sup). 68 | -define(CONN_TIMEOUT, 5000). 69 | -define(COMMAND_TIMEOUT, 10000). 70 | -define(CONN_POOL_DEF, 5). 71 | -define(CONN_POOL_MIN, 1). 72 | -define(CONN_POOL_MAX, 64). 73 | 74 | %% 75 | %% about types and records 76 | %% 77 | 78 | %% type defines 79 | -type key() :: binary() | [byte()]. 80 | -type str() :: binary() | [byte()]. 81 | -type val() :: null() | str(). 82 | -type null() :: 'null'. 83 | -type command() :: binary(). 84 | -type uint() :: non_neg_integer(). 85 | -type int() :: integer(). 86 | 87 | -type length() :: non_neg_integer(). 88 | -type passwd() :: [byte()] | binary(). 89 | -type second() :: non_neg_integer(). 90 | -type timestamp() :: non_neg_integer(). 91 | -type status_code() :: atom() | binary(). 92 | -type error() :: {'error', any()}. 93 | -type value() :: null() | str() | status_code() | list() | error(). 94 | -type value_type() :: 'none' | 'string' | 'list' | 'set' | 'zset' | 'hash'. 95 | -type score() :: '-inf' | '+inf' | integer() | float() | 96 | {open, integer() | float()} | {closed, integer() | float()}. 97 | -type aggregate() :: 'sum' | 'min' | 'max'. 98 | -type count() :: non_neg_integer(). 99 | 100 | -type pattern() :: str(). 101 | -type channel() :: str(). 102 | -type psubscribe_fun() :: fun((pattern(), count()) -> any()). 103 | -type pmessage_fun() :: fun((pattern(), channel(), str()) -> any()). 104 | -type punsubscribe_fun() :: fun((pattern(), count()) -> any()). 105 | -type subscribe_fun() :: fun((channel(), count()) -> any()). 106 | -type message_fun() :: fun((channel(), str()) -> any()). 107 | -type unsubscribe_fun() :: fun((channel(), count()) -> any()). 108 | 109 | -type client() :: pid() | atom(). 110 | 111 | -type inet_host() :: atom() | string() | binary(). 112 | -type inet_port() :: 0..65535. 113 | -type inet_server() :: {inet_host(), inet_port()}. 114 | 115 | -type redis_handler() :: {redis, _, _}. 116 | 117 | -type trans_handler() :: atom(). 118 | 119 | 120 | -endif. % REDIS_INTERNAL_HRL 121 | -------------------------------------------------------------------------------- /include/redis_log.hrl: -------------------------------------------------------------------------------- 1 | %%%---------------------------------------------------------------------- 2 | %%% 3 | %%% @copyright erl-redis 2010 4 | %%% 5 | %%% @author litaocheng@gmail.com 6 | %%% @doc the log header 7 | %%% 8 | %%%---------------------------------------------------------------------- 9 | -ifndef(REDIS_LOG_HRL). 10 | -define(REDIS_LOG_HRL, ok). 11 | 12 | -define(ML_FMT(F), " (~p:~p) "++(F)). 13 | -define(ML_DATA(D), [?MODULE, ?LINE]++(D)). 14 | 15 | -ifdef(NOLOG). 16 | -define(DEBUG(F), ok). 17 | -define(DEBUG2(F, D), ok). 18 | 19 | -define(INFO(F), ok). 20 | -define(INFO2(F, D), ok). 21 | 22 | -define(WARN(F), 23 | error_logger:warning_msg(?ML_FMT("[*W*]"++F), ?ML_DATA([]))). 24 | -define(WARN2(F, D), 25 | error_logger:warning_msg(?ML_FMT("[*W*]"++F), ?ML_DATA(D))). 26 | 27 | -define(ERROR(F), 28 | error_logger:error_msg(?ML_FMT("[**E**]"++F), ?ML_DATA([]))). 29 | -define(ERROR2(F, D), 30 | error_logger:error_msg(?ML_FMT("[**E**]"++F), ?ML_DATA(D))). 31 | 32 | -else. 33 | 34 | -ifdef(EUNIT). 35 | 36 | -define(EUNITFMT(T, F, D), (?debugFmt((T) ++ "(~p) " ++ (F), [self()] ++ (D)))). 37 | 38 | -define(DEBUG(F), ?EUNITFMT("[D]", F, [])). 39 | -define(DEBUG2(F, D), ?EUNITFMT("[D]", F, D)). 40 | 41 | -define(INFO(F), ?EUNITFMT("[I]", F, [])). 42 | -define(INFO2(F, D), ?EUNITFMT("[I]", F, D)). 43 | 44 | -define(WARN(F), ?EUNITFMT("[*W*]", F, [])). 45 | -define(WARN2(F, D), ?EUNITFMT("[*W*]", F, D)). 46 | 47 | -define(ERROR(F), ?EUNITFMT("[**E**]", F, [])). 48 | -define(ERROR2(F, D), ?EUNITFMT("[**E**]", F, D)). 49 | 50 | -else. 51 | -define(DEBUG(F), 52 | error_logger:info_msg(?ML_FMT("[D]"++F), ?ML_DATA([]))). 53 | -define(DEBUG2(F, D), 54 | error_logger:info_msg(?ML_FMT("[D]"++F), ?ML_DATA(D))). 55 | 56 | -define(INFO(F), 57 | error_logger:info_msg(?ML_FMT("[I]"++F), ?ML_DATA([]))). 58 | -define(INFO2(F, D), 59 | error_logger:info_msg(?ML_FMT("[I]"++F), ?ML_DATA(D))). 60 | 61 | -define(WARN(F), 62 | error_logger:warning_msg(?ML_FMT("[*W*]"++F), ?ML_DATA([]))). 63 | -define(WARN2(F, D), 64 | error_logger:warning_msg(?ML_FMT("[*W*]"++F), ?ML_DATA(D))). 65 | 66 | -define(ERROR(F), 67 | error_logger:error_msg(?ML_FMT("[**E**]"++F), ?ML_DATA([]))). 68 | -define(ERROR2(F, D), 69 | error_logger:error_msg(?ML_FMT("[**E**]"++F), ?ML_DATA(D))). 70 | 71 | -endif. %EUNIT 72 | 73 | -endif. %NOLOG 74 | 75 | -endif. % REDIS_LOG_HRL 76 | -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | ## -*- makefile -*- 2 | ## Erlang 3 | #SHELL=/bin/bash 4 | include ../vsn.mk 5 | .PHONY: all edoc dialyzer clean 6 | DOC_OPTS={dir,\"../edoc\"} 7 | 8 | ERL := erl 9 | ERLC := $(ERL)c 10 | 11 | INCLUDE_DIRS :=../include ./ 12 | INCLUDE_FILES := $(wildcard ../include/*.hrl) $(wildcard ./*.hrl) 13 | 14 | EBIN_DIRS := $(wildcard ../libs/*/ebin) 15 | ERLC_FLAGS := +warnings_as_errors -W $(addprefix -I , $(INCLUDE_DIRS)) $(addprefix -pa , $(EBIN_DIRS)) 16 | 17 | ifdef TEST 18 | ERLC_FLAGS += +debug_info -DTEST 19 | endif 20 | 21 | ifdef NOLOG 22 | ERLC_FLAGS += -DNOLOG 23 | endif 24 | 25 | ifdef HIPE 26 | ERLC_FLAGS += +native +"{hipe, [o3]}" 27 | endif 28 | 29 | # in deploy environment, the ctl file set the log dir to /var/log/$APP_NAME 30 | # set the etc dir to /etc/$APP_NAME 31 | ifdef DEPLOY 32 | LOGDIR =/var/log/$(APP_NAME) 33 | CONFDIR =/etc/$(APP_NAME) 34 | else 35 | LOGDIR =./log 36 | CONFDIR =./etc 37 | endif 38 | 39 | BASE_DIR := .. 40 | EBIN_DIR := ../ebin 41 | DOC_DIR = ../edoc 42 | LIBS_DIR = ../libs 43 | EMULATOR := beam 44 | 45 | ERL_SOURCES := $(wildcard *.erl) 46 | ERL_MODULES := $(ERL_SOURCES:%.erl=%) 47 | ERL_MODULES_IN_APP := $(ERL_SOURCES:%.erl=`%`,) 48 | ERL_HEADERS := $(wildcard *.hrl) $(wildcard ../include/*.hrl) 49 | ERL_OBJECTS := $(ERL_SOURCES:%.erl=$(EBIN_DIR)/%.$(EMULATOR)) 50 | ERL_OBJECTS_LOCAL := $(ERL_SOURCES:%.erl=./%.$(EMULATOR)) 51 | APP_TPL_FILES = $(wildcard *.app.src) 52 | APP_FILES = $(APP_TPL_FILES:%.src=%) 53 | 54 | ALL_FILES = $(ERL_OBJECTS) $(addprefix $(EBIN_DIR)/, $(APP_FILES)) 55 | 56 | $(EBIN_DIR)/%.app: %.app.src 57 | @echo "creating .app file... " 58 | @sed -e 's:@APP_NAME@:$(APP_NAME):g' \ 59 | -e 's:@APP_VSN@:$(APP_VSN):g' \ 60 | -e 's:@MODULES@:$(ERL_MODULES_IN_APP):g' -e "s:\`:\':g" -e 's/,]/]/g' $< > $@ 61 | 62 | $(BASE_DIR)/%ctl: %ctl.tpl 63 | @sed -e 's:@LOGDIR@:$(LOGDIR):g' -e 's:@CONFDIR@:$(CONFDIR):g' $< > $@ 64 | (chmod 775 $@) 65 | 66 | $(EBIN_DIR)/%.$(EMULATOR): %.erl $(INCLUDE_FILES) 67 | $(ERLC) $(ERLC_FLAGS) -o $(EBIN_DIR) $< 68 | 69 | all: $(ALL_FILES) 70 | 71 | edoc: 72 | $(ERL) -noshell -eval "edoc:application($(APP_NAME), \".\", [${DOC_OPTS}]),init:stop()" 73 | 74 | clean: 75 | (rm -rf ./*.beam; rm -rf $(EBIN_DIR)/*.beam; rm -f *.app; rm -f *ctl) 76 | -------------------------------------------------------------------------------- /src/overview.edoc: -------------------------------------------------------------------------------- 1 | ** this is the overview.doc file for the application 'erl-redis' ** 2 | 3 | @author litaocheng 4 | @copyright 2010 5 | @version @VERSION@ 6 | @title Welcome to the `erl-redis' application! 7 | @doc `erl-redis' is an erlang client for redis. 8 | -------------------------------------------------------------------------------- /src/redis.app.src: -------------------------------------------------------------------------------- 1 | {application, @APP_NAME@, 2 | [{description, "the redis erlang client"}, 3 | {vsn, "@APP_VSN@"}, 4 | {modules, [@MODULES@]}, 5 | {registered, []}, 6 | {applications, [kernel, stdlib]}, 7 | {mod, {redis_app, []}}, 8 | {env, []} 9 | ] 10 | }. 11 | -------------------------------------------------------------------------------- /src/redis.erl: -------------------------------------------------------------------------------- 1 | %%%---------------------------------------------------------------------- 2 | %%% 3 | %%% @copyright erl-redis 2010 4 | %%% 5 | %%% @author litaocheng@gmail.com 6 | %%% @doc the interface for redis, all the commands are same with 7 | %%% http://redis.io/commands, except for all the function are 8 | %%% lowercase of the redis commands 9 | %%% 10 | %%% this module is parameterized module: 11 | %%% Client - the context related client process) 12 | %%% Pipeline - if in pipeline model 13 | %%% @end 14 | %%% 15 | %%%---------------------------------------------------------------------- 16 | -module(redis, [Client, Pipeline]). 17 | -author('litaocheng@gmail.com'). 18 | -vsn('0.1'). 19 | -include("redis_internal.hrl"). 20 | 21 | -export([i/0, version/0, db/0]). 22 | 23 | %% keys 24 | -export([del/1, exists/1, expire/2, expireat/2, keys/1, move/2, 25 | object/2, persist/1, randomkey/0, rename/2, renamenx/2, 26 | sort/2, ttl/1, type/1, eval/4]). 27 | 28 | %% strings 29 | -export([append/2, decr/1, decrby/2, get/1, getbit/2, getrange/3, getset/2, 30 | incr/1, incrby/2, mget/1, mset/1, msetnx/1, set/2, 31 | setbit/3, setex/3, setnx/2, setrange/3, strlen/1]). 32 | 33 | %% hashes 34 | -export([hdel/2, hexists/2, hget/2, hgetall/1, hincrby/3, 35 | hkeys/1, hlen/1, hmget/2, hmset/2, hset/3, hsetnx/3, hvals/1]). 36 | 37 | %% lists 38 | -export([blpop/2, brpop/2, brpoplpush/3, lindex/2, 39 | linsert/4, llen/1, lpop/1, lpush/2, lpushx/2, 40 | lrange/3, lrem/3, lset/3, ltrim/3, rpop/1, rpoplpush/2, 41 | rpush/2, rpushx/2]). 42 | 43 | %% sets 44 | -export([sadd/2, scard/1, sdiff/2, sdiffstore/3, sinter/1, 45 | sinterstore/2, sismember/2, smembers/1, smove/3, 46 | spop/1, srandmember/1, srem/2, sunion/1, sunionstore/2]). 47 | 48 | %% sorted sets 49 | -export([zadd/2, zcard/1, zcount/3, zincrby/3, 50 | zinterstore/2, zinterstore/3, zinterstore/4, 51 | zrange/3, zrange/4, zrangebyscore/3, zrangebyscore/4, zrank/2, zrem/2, 52 | zremrangebyrank/3, zremrangebyscore/3, 53 | zrevrange/3, zrevrange/4, zrevrangebyscore/3, zrevrangebyscore/4, 54 | zrevrank/2, zscore/2, 55 | zunionstore/2, zunionstore/3, zunionstore/4]). 56 | 57 | %% pub/sub 58 | -export([psubscribe/3, subscribe/3, 59 | publish/2, 60 | punsubscribe/1, punsubscribe/2, 61 | unsubscribe/1, unsubscribe/2]). 62 | 63 | %% transactions 64 | -export([multi/0, exec/0, discard/0, 65 | watch/1, unwatch/0]). 66 | 67 | %% pipeline 68 | -export([pipeline/1]). 69 | 70 | %% connection 71 | -export([auth/1, echo/1, ping/0, quit/0, select/1]). 72 | 73 | %% server 74 | -export([bgrewriteaof/0, bgsave/0, config_get/1, config_set/2, config_resetstat/0, 75 | dbsize/0, debug_object/1, debug_segfault/0, 76 | flushall/0, flushdb/0, info/0, lastsave/0, save/0, shutdown/0, slave_of/2, 77 | slave_off/0]). 78 | 79 | %% other unknown commands 80 | -export([command/1]). 81 | 82 | -compile(inline). 83 | -compile({inline_size, 30}). 84 | -import(redis_proto, [mbulk/1, mbulk/2, mbulk/3, mbulk/4, mbulk/5, mbulk_list/1]). 85 | 86 | -define(MULTI_REPLY(Ret), 87 | case BadServers of 88 | [] -> 89 | Ret; 90 | [_|_] -> 91 | {error, Ret, BadServers} 92 | end). 93 | -define(MULTI_REPLY_OK, 94 | case BadServers of 95 | [] -> 96 | ok; 97 | [_|_] -> 98 | {error, BadServers} 99 | end). 100 | 101 | %% @doc show stats info in the stdout 102 | -spec i() -> 'ok'. 103 | i() -> 104 | ok. 105 | 106 | %% @doc return the redis version info 107 | -spec version() -> string(). 108 | version() -> 109 | get_app_vsn(). 110 | 111 | %% @doc return the currently selected db 112 | -spec db() -> uint(). 113 | db() -> 114 | redis_manager:get_selected_db(Client). 115 | 116 | %%------- 117 | %% keys 118 | %%------- 119 | 120 | %% @doc delete keys 121 | %% return the number of keys that were removed 122 | -spec del(Keys :: [key()] | key()) -> integer(). 123 | del([H|_] = Key) when is_list(H); is_binary(H) -> 124 | call(mbulk_list([<<"DEL">> | Key])); 125 | del(Key) -> 126 | call(mbulk(<<"DEL">>, Key)). 127 | 128 | %% @doc test if the key exists 129 | -spec exists(key()) -> boolean(). 130 | exists(Key) -> 131 | call(mbulk(<<"EXISTS">>, Key), fun int_may_bool/1). 132 | 133 | %% @doc set a timeout on the specified key, after the Time the Key will 134 | %% automatically deleted by server. 135 | -spec expire(key(), second()) -> boolean(). 136 | expire(Key, Time) -> 137 | call(mbulk(<<"EXPIRE">>, Key, ?N2S(Time)), fun int_may_bool/1). 138 | 139 | %% @doc set a unix timestamp on the specified key, the Key will 140 | %% automatically deleted by server at the TimeStamp in the future. 141 | -spec expireat(Key :: key(), TimeStamp :: timestamp()) -> boolean(). 142 | expireat(Key, TimeStamp) -> 143 | call(mbulk(<<"EXPIREAT">>, Key, ?N2S(TimeStamp)), fun int_may_bool/1). 144 | 145 | %% @doc return the keys list matching a given pattern 146 | -spec keys(Pattern :: str()) -> [key()]. 147 | keys(Pattern) -> 148 | call(mbulk(<<"KEYS">>, Pattern), fun may_null_to_list/1). 149 | 150 | %% @doc move a key to anthor db 151 | -spec move(key(), uint()) -> boolean(). 152 | move(Key, DBIndex) -> 153 | call(mbulk(<<"MOVE">>, Key, ?N2S(DBIndex)), fun int_may_bool/1). 154 | 155 | %% @doc Inspect the internals of Redis objects 156 | %% SubCmd: REFCOUNT, ENCODING, IDLETIME 157 | -spec object(command(), list()) -> integer() | str(). 158 | object(SubCmd, Args) -> 159 | call(mbulk_list([<<"OBJECT">>, SubCmd | Args])). 160 | 161 | %% @doc Remove the expiration from a key 162 | -spec persist(key()) -> boolean(). 163 | persist(Key) -> 164 | call(mbulk("PERSIST", Key), fun int_may_bool/1). 165 | 166 | %% @doc return a random key from the keyspace 167 | -spec randomkey() -> key() | null(). 168 | randomkey() -> 169 | call(mbulk(<<"RANDOMKEY">>)). 170 | 171 | %% @doc atmoically rename a key 172 | -spec rename(OldKey :: key(), NewKey :: key()) -> 'ok' | error(). 173 | rename(OldKey, NewKey) -> 174 | call(mbulk(<<"RENAME">>, OldKey, NewKey)). 175 | 176 | %% @doc rename a key, only if the new key does not exist 177 | -spec renamenx(Key :: key(), NewKey :: key()) -> boolean(). 178 | renamenx(Key, NewKey) -> 179 | call(mbulk(<<"RENAMENX">>, Key, NewKey), fun int_may_bool/1). 180 | 181 | %% @doc sort the list in a list, set or sorted set 182 | -spec sort(Key :: key(), SortOpt :: redis_sort()) -> list(). 183 | sort(Key, SortOpt) -> 184 | #redis_sort{ 185 | by_pat = By, 186 | get_pat = Get, 187 | limit = Limit, 188 | asc = Asc, 189 | alpha = Alpha, 190 | store = Store 191 | } = SortOpt, 192 | 193 | ByPart = ?IF(is_empty_str(By), [], [<<"BY">>, By]), 194 | LimitPart = 195 | case Limit of 196 | {Start, End} -> 197 | [<<"LIMIT">>, ?N2S(Start), ?N2S(End)]; 198 | _ -> 199 | [] 200 | end, 201 | {FieldCount, GetPart} = 202 | case Get of 203 | [] -> 204 | {1, []}; 205 | _ -> 206 | {length(Get), lists:append([[<<"GET">>, P] || P <- Get])} 207 | end, 208 | AscPart = ?IF(is_empty_str(Asc), [], [<<"DESC">>]), 209 | AlphaPart = ?IF(is_empty_str(Alpha), [], [<<"ALPHA">>]), 210 | StorePart = ?IF(is_empty_str(Store), [], [<<"STORE">>, Store]), 211 | call( 212 | mbulk_list([<<"SORT">>, Key | 213 | lists:append([ByPart, LimitPart, GetPart, AscPart, AlphaPart, StorePart]) 214 | ]), fun(R) -> list_to_n_tuple(R, FieldCount) end). 215 | 216 | %% @doc get the time to live for a key 217 | -spec ttl(Key :: key()) -> integer(). 218 | ttl(Key) -> 219 | call(mbulk(<<"TTL">>, Key)). 220 | 221 | %% @doc determine the type of the value stored at key 222 | -spec type(Key :: key()) -> value_type(). 223 | type(Key) -> 224 | call(mbulk(<<"TYPE">>, Key)). 225 | 226 | %% @doc excute a lua script at server side 227 | -spec eval(Script :: str(), NumKeys :: count(), Keys :: [key()], Args :: [str()]) -> value(). 228 | eval(Script, NumKeys, Keys, Args) -> 229 | call(mbulk_list([<<"EVAL">>, Script, ?N2S(NumKeys) | Keys ++ Args])). 230 | 231 | 232 | %%------------ 233 | %% strings 234 | %%------------ 235 | 236 | %% @doc append a value to the string stored at key, 237 | %% return the new string length 238 | -spec append(key(), val()) -> integer(). 239 | append(Key, Val) -> 240 | call(mbulk(<<"APPEND">>, Key, Val)). 241 | 242 | %% @doc decrease the integer value of key by one 243 | -spec decr(key()) -> integer(). 244 | decr(Key) -> 245 | call(mbulk(<<"DECR">>, Key)). 246 | 247 | %% @doc decrease the integer value of key by given number 248 | -spec decrby(key(), integer()) -> integer(). 249 | decrby(Key, N) -> 250 | call(mbulk(<<"DECRBY">>, Key, ?N2S(N))). 251 | 252 | %% @doc get the value of a key 253 | -spec get(key()) -> val(). 254 | get(Key) -> 255 | call(mbulk(<<"GET">>, Key)). 256 | 257 | %% @doc return the bit value at offset in the string value 258 | %% stored at key 259 | -spec getbit(key(), uint()) -> integer(). 260 | getbit(Key, Offset) -> 261 | call(mbulk(<<"GETBIT">>, Key, Offset)). 262 | 263 | %% @doc return a substring of the string stored at a key 264 | -spec getrange(key(), int(), int()) -> str(). 265 | getrange(Key, Start, End) -> 266 | call(mbulk(<<"GETRANGE">>, Key, Start, End)). 267 | 268 | %% @doc atomaticly set the value and return the old value 269 | -spec getset(key(), str()) -> val(). 270 | getset(Key, Val) -> 271 | call(mbulk(<<"GETSET">>, Key, Val)). 272 | 273 | %% @doc increase the integer value of a key by one 274 | -spec incr(key()) -> int(). 275 | incr(Key) -> 276 | call(mbulk(<<"INCR">>, Key)). 277 | 278 | %% @doc increase the integer value of key by integer 279 | -spec incrby(key(), int()) -> int(). 280 | incrby(Key, N) -> 281 | call(mbulk(<<"INCRBY">>, Key, ?N2S(N))). 282 | 283 | %% @doc get the values of all the given keys 284 | -spec mget([key()]) -> [val()]. 285 | mget(Keys) -> 286 | call(mbulk_list([<<"MGET">> | Keys])). 287 | 288 | %% @doc set multiple keys to respective values 289 | -spec mset([{key(), str()}]) -> 'ok'. 290 | mset(KeyVals) -> 291 | L = [<<"MSET">> | lists:append([[K, V] || {K, V} <- KeyVals])], 292 | call(mbulk_list(L)). 293 | 294 | %% @doc set multiple keys to respective values, only if 295 | %% none of the keys exist 296 | -spec msetnx(KeyVals :: [{key(), str()}]) -> boolean(). 297 | msetnx(KeyVals) -> 298 | L = [<<"MSETNX">> | lists:append([[K, V] || {K, V} <- KeyVals])], 299 | call(mbulk_list(L), fun int_may_bool/1). 300 | 301 | %% @doc set the string value of the key 302 | -spec set(key(), str()) -> 'ok'. 303 | set(Key, Val) -> 304 | call(mbulk(<<"SET">>, Key, Val)). 305 | 306 | %% @doc set or clears the bit at offset in the string value 307 | %% stored at key 308 | -spec setbit(key(), uint(), 0 | 1) -> uint(). 309 | setbit(Key, Offset, Val) -> 310 | call(mbulk(<<"SETBIT">>, Key, ?N2S(Offset), Val)). 311 | 312 | %% @doc set the string value of the key with expired time 313 | -spec setex(key(), str(), second()) -> status_code(). 314 | setex(Key, Val, Expire) -> 315 | call(mbulk(<<"SETEX">>, Key, ?N2S(Expire), Val)). 316 | 317 | %% @doc set the string value of the key, only if the key 318 | %% does not exist 319 | -spec setnx(key(), str()) -> boolean(). 320 | setnx(Key, Val) -> 321 | call(mbulk(<<"SETNX">>, Key, Val), fun int_may_bool/1). 322 | 323 | %% @doc overwrite part of the string at key 324 | %% return the new string length 325 | -spec setrange(key(), uint(), str()) -> uint(). 326 | setrange(Key, Start, Val) -> 327 | call(mbulk(<<"SETRANGE">>, Key, ?N2S(Start), Val)). 328 | 329 | %% @doc get the length of the value stored in a key 330 | -spec strlen(key()) -> uint(). 331 | strlen(Key) -> 332 | call(mbulk(<<"STRLEN">>, Key)). 333 | 334 | %%------------ 335 | %% hashes 336 | %%------------ 337 | 338 | %% @doc delete one or more hash fields 339 | -spec hdel(key(), [key()]) -> uint(). 340 | hdel(Key, [H|_] = Field) when is_list(H); is_binary(H) -> 341 | call(mbulk_list([<<"HDEL">>, Key | Field])); 342 | hdel(Key, Field) -> 343 | call(mbulk(<<"HDEL">>, Key, Field)). 344 | 345 | %% @doc determine if a hash field exists 346 | -spec hexists(key(), key()) -> boolean(). 347 | hexists(Key, Field) -> 348 | call(mbulk(<<"HEXISTS">>, Key, Field), fun int_may_bool/1). 349 | 350 | %% @doc get the value of the a hash field 351 | -spec hget(key(), key()) -> val(). 352 | hget(Key, Field) -> 353 | call(mbulk(<<"HGET">>, Key, Field)). 354 | 355 | %% @doc get all the fields and values in a hash 356 | -spec hgetall(key()) -> [{key(), str()}]. 357 | hgetall(Key) -> 358 | call(mbulk(<<"HGETALL">>, Key), fun list_to_kv_tuple/1). 359 | 360 | %% @doc increment the integer value of a hash filed by 361 | %% given numer 362 | -spec hincrby(key(), key(), int()) -> int(). 363 | hincrby(Key, Field, N) -> 364 | call(mbulk(<<"HINCRBY">>, Key, Field, ?N2S(N))). 365 | 366 | %% @doc get all the fields in hash 367 | -spec hkeys(key()) -> [val()]. 368 | hkeys(Key) -> 369 | call(mbulk(<<"HKEYS">>, Key)). 370 | 371 | %% @doc get the number of fields in hash 372 | -spec hlen(key()) -> integer(). 373 | hlen(Key) -> 374 | call(mbulk(<<"HLEN">>, Key)). 375 | 376 | %% @doc get the values of all specified fields 377 | -spec hmget(key(), [key()]) -> [val()]. 378 | hmget(Key, Fields) -> 379 | call(mbulk_list([<<"HMGET">>, Key | Fields])). 380 | 381 | %% @doc Set multiple hash fields to multiple values 382 | -spec hmset(key(), [{key(), str()}]) -> 'ok'. 383 | hmset(Key, FieldVals) -> 384 | L = [<<"HMSET">>, Key | lists:append([[F, V] || {F, V} <- FieldVals])], 385 | call(mbulk_list(L)). 386 | 387 | %% @doc set the string value of a hash filed with 388 | -spec hset(key(), key(), str()) -> boolean(). 389 | hset(Key, Field, Val) -> 390 | call(mbulk(<<"HSET">>, Key, Field, Val), fun int_may_bool/1). 391 | 392 | %% @doc Set the value of a hash field, only if the field 393 | %% does not exist 394 | -spec hsetnx(key(), key(), str()) -> boolean(). 395 | hsetnx(Key, Field, Val) -> 396 | call(mbulk(<<"HSETNX">>, Key, Field, Val), fun int_may_bool/1). 397 | 398 | %% @doc return all the values in hash 399 | -spec hvals(key()) -> [val()]. 400 | hvals(Key) -> 401 | call(mbulk(<<"HVALS">>, Key)). 402 | 403 | %%------------------------ 404 | %% lists 405 | %%------------------------ 406 | 407 | %% @doc Remove and get the first element in a list, or block 408 | %% until one is available (timeout unit is second) 409 | -spec blpop([key()], timeout()) -> null() | [{str(), val()}]. 410 | blpop(Keys, Timeout) -> 411 | call(mbulk_list([<<"BLPOP">> | Keys ++ [timeout_val(Timeout)]]), 412 | fun list_to_kv_tuple/1). 413 | 414 | %% @doc Remove and get the last element in a list, 415 | %% or block until one is available 416 | -spec brpop([key()], timeout()) -> null() | [{str(), val()}]. 417 | brpop(Keys, Timeout) -> 418 | call(mbulk_list([<<"BRPOP">> | Keys ++ [timeout_val(Timeout)]]), 419 | fun list_to_kv_tuple/1). 420 | 421 | %% @doc Pop a value from a list, push it to another list 422 | %% and return it; or block until one is available 423 | -spec brpoplpush(key(), key(), timeout()) -> val(). 424 | brpoplpush(Src, Dst, Timeout) -> 425 | call(mbulk(<<"BRPOPLPUSH">>, Src, Dst, timeout_val(Timeout))). 426 | 427 | %% @doc Get an element from a list by its index 428 | -spec lindex(key(), uint()) -> val(). 429 | lindex(Key, Idx) -> 430 | call(mbulk(<<"LINDEX">>, Key, ?N2S(Idx))). 431 | 432 | %% @doc Insert an element before or after another 433 | %% element in a list 434 | -spec linsert(key(), 'before' | 'after', key(), val()) -> int(). 435 | linsert(Key, Pos, Pivot, Val) when Pos =:= 'before'; Pos =:= 'after' -> 436 | PosStr = 437 | case Pos of 438 | before -> <<"BEFORE">>; 439 | _ -> <<"AFTER">> 440 | end, 441 | call(mbulk(<<"LINSERT">>, Key, PosStr, Pivot, Val)). 442 | 443 | %% @doc get the length of the list 444 | -spec llen(key()) -> uint(). 445 | llen(Key) -> 446 | call(mbulk(<<"LLEN">>, Key)). 447 | 448 | %% @doc remove and get the first element of the list 449 | -spec lpop(key()) -> val(). 450 | lpop(Key) -> 451 | call(mbulk(<<"LPOP">>, Key)). 452 | 453 | %% @doc add multiple values to the head of the list 454 | -spec lpush(key(), str() | [str()]) -> uint(). 455 | lpush(Key, [H|_] = Val) when is_list(H); is_binary(H) -> 456 | call(mbulk_list([<<"LPUSH">>, Key | Val])); 457 | lpush(Key, Val) -> 458 | call(mbulk(<<"LPUSH">>, Key, Val)). 459 | 460 | %% @doc add value to the head of the list, only if the list exist 461 | -spec lpushx(key(), str()) -> uint(). 462 | lpushx(Key, Val) -> 463 | call(mbulk(<<"LPUSHX">>, Key, Val)). 464 | 465 | %% @doc Get a range of elements from a list 466 | -spec lrange(key(), int(), int()) -> [val()]. 467 | lrange(Key, Start, End) -> 468 | call(mbulk(<<"LRANGE">>, Key, ?N2S(Start), ?N2S(End)), fun may_null_to_list/1). 469 | 470 | %% @doc Remove elements from a list 471 | %% N > 0, from head to tail 472 | %% N < 0, from tail to head 473 | %% N = 0, remove all elements equal to Val 474 | -spec lrem(key(), int(), str()) -> uint(). 475 | lrem(Key, N, Val) -> 476 | call(mbulk(<<"LREM">>, Key, ?N2S(N), Val)). 477 | 478 | %% @doc Set the value of an element in a list by its index 479 | -spec lset(key(), int(), str()) -> 'ok'. 480 | lset(Key, Idx, Val) -> 481 | call(mbulk(<<"LSET">>, Key, ?N2S(Idx), Val)). 482 | 483 | %% @doc Trim a list to the specified range 484 | -spec ltrim(key(), int(), int()) -> 'ok'. 485 | ltrim(Key, Start, End) -> 486 | call(mbulk(<<"LTRIM">>, Key, ?N2S(Start), ?N2S(End))). 487 | 488 | %% @doc Remove and get the last element in a list 489 | -spec rpop(key()) -> val(). 490 | rpop(Key) -> 491 | call(mbulk(<<"RPOP">>, Key)). 492 | 493 | %% @doc Remove the last element in a list, append it to 494 | %% another list and return it 495 | -spec rpoplpush(key(), key()) -> val(). 496 | rpoplpush(Src, Dst) -> 497 | call(mbulk(<<"RPOPLPUSH">>, Src, Dst)). 498 | 499 | %% @doc Append one or multiple values to a list 500 | -spec rpush(key(), str() | [str()]) -> uint(). 501 | rpush(Key, [H|_] = Val) when is_list(H); is_binary(H) -> 502 | call(mbulk_list([<<"RPUSH">>, Key | Val])); 503 | rpush(Key, Val) -> 504 | call(mbulk(<<"RPUSH">>, Key, Val)). 505 | 506 | %% @doc add value to the tail of the list, only if the list exist 507 | -spec rpushx(key(), str()) -> uint(). 508 | rpushx(Key, Val) -> 509 | call(mbulk(<<"RPUSHX">>, Key, Val)). 510 | 511 | %%-------------------------------------- 512 | %% sets (in set all members is distinct) 513 | %%-------------------------------------- 514 | 515 | %% @doc Add one or more members to a set 516 | -spec sadd(key(), str()) -> uint(). 517 | sadd(Key, [H|_] = Mem) when is_list(H); is_binary(H) -> 518 | call(mbulk_list([<<"SADD">>, Key | Mem])); 519 | sadd(Key, Mem) -> 520 | call(mbulk(<<"SADD">>, Key, Mem)). 521 | 522 | %% @doc return the number of elements in set 523 | -spec scard(key()) -> uint(). 524 | scard(Key) -> 525 | call(mbulk(<<"SCARD">>, Key)). 526 | 527 | %% @doc Subtract multiple sets 528 | -spec sdiff(key(), [key()]) -> [val()]. 529 | sdiff(First, Keys) -> 530 | call(mbulk_list([<<"SDIFF">>, First | Keys]), fun may_null_to_list/1). 531 | 532 | %% @doc Subtract multiple sets and store the resulting 533 | %% set in a key 534 | -spec sdiffstore(key(), key(), [key()]) -> uint(). 535 | sdiffstore(Dst, First, Keys) -> 536 | call(mbulk_list([<<"SDIFFSTORE">>, Dst, First | Keys])). 537 | 538 | %% @doc return the intersection between the sets 539 | -spec sinter([key()]) -> [val()]. 540 | sinter(Keys) -> 541 | call(mbulk_list([<<"SINTER">> | Keys]), fun may_null_to_list/1). 542 | 543 | %% @doc Intersect multiple sets and store the 544 | %% resulting set in a key 545 | -spec sinterstore(key(), [key()]) -> uint(). 546 | sinterstore(Dst, Keys) -> 547 | call(mbulk_list([<<"SINTERSTORE">>, Dst | Keys])). 548 | 549 | %% @doc Determine if a given value is a member of a set 550 | -spec sismember(key(), str()) -> boolean(). 551 | sismember(Key, Mem) -> 552 | call(mbulk(<<"SISMEMBER">>, Key, Mem), fun int_may_bool/1). 553 | 554 | %% @doc get all the members in the set 555 | -spec smembers(key()) -> [val()]. 556 | smembers(Key) -> 557 | call(mbulk(<<"SMEMBERS">>, Key), fun may_null_to_list/1). 558 | 559 | %% @doc Move a member from one set to another 560 | -spec smove(key(), key(), str()) -> boolean(). 561 | smove(Src, Dst, Mem) -> 562 | call(mbulk(<<"SMOVE">>, Src, Dst, Mem), fun int_may_bool/1). 563 | 564 | %% @doc Remove and return a random member from a set 565 | -spec spop(key()) -> val(). 566 | spop(Key) -> 567 | call(mbulk(<<"SPOP">>, Key)). 568 | 569 | %% @doc Get a random member from a set 570 | -spec srandmember(key()) -> val(). 571 | srandmember(Key) -> 572 | call(mbulk(<<"SRANDMEMBER">>, Key)). 573 | 574 | %% @doc Remove one or more members from a set 575 | -spec srem(key(), key() | [key()]) -> uint(). 576 | srem(Key, [H|_] = Mem) when is_list(H); is_binary(H) -> 577 | call(mbulk_list([<<"SREM">>, Key | Mem])); 578 | srem(Key, Mem) -> 579 | call(mbulk(<<"SREM">>, Key, Mem)). 580 | 581 | %% @doc Add multiple sets 582 | -spec sunion([key()]) -> [val()]. 583 | sunion(Keys) -> 584 | call(mbulk_list([<<"SUNION">> | Keys])). 585 | 586 | %% @doc Add multiple sets and store the resulting set in a key 587 | -spec sunionstore(key(), [key()]) -> uint(). 588 | sunionstore(Dst, Keys) -> 589 | call(mbulk_list([<<"SUNIONSTORE">>, Dst | Keys])). 590 | 591 | %%-------------------- 592 | %% sorted sets 593 | %%-------------------- 594 | 595 | %% @doc Add one or more members to a sorted set, or 596 | %% update its score if it already exists 597 | -spec zadd(key(), [{str(), score()}]) -> uint(). 598 | zadd(Key, Mem) -> 599 | L = lists:append([[score_to_str(Score), Val] || {Val, Score} <- Mem]), 600 | call(mbulk_list([<<"ZADD">>, Key | L])). 601 | 602 | %% @doc return the number of elements in sorted set 603 | -spec zcard(key()) -> uint(). 604 | zcard(Key) -> 605 | call(mbulk(<<"ZCARD">>, Key)). 606 | 607 | %% @doc Count the members in a sorted set with 608 | %% scores within the given values 609 | -spec zcount(key(), score(), score()) -> uint(). 610 | zcount(Key, Min, Max) -> 611 | MinStr = score_to_str(Min), 612 | MaxStr = score_to_str(Max), 613 | call(mbulk(<<"ZCOUNT">>, Key, MinStr, MaxStr)). 614 | 615 | %% @doc Increment the score of a member in a sorted set 616 | -spec zincrby(key(), int(), key()) -> score(). 617 | zincrby(Key, N, Mem) -> 618 | call(mbulk(<<"ZINCRBY">>, Key, score_to_str(N), Mem), fun str_to_score/1). 619 | 620 | %% @doc Intersect multiple sorted sets and store 621 | %% the resulting sorted set in a new key 622 | zinterstore(Dst, Keys) -> 623 | zinterstore(Dst, Keys, [], sum). 624 | zinterstore(Dst, Keys, Weights) -> 625 | ?ASSERT(length(Keys) =:= length(Weights)), 626 | zinterstore(Dst, Keys, Weights, sum). 627 | zinterstore(Dst, Keys, Weights, Aggregate) -> 628 | do_zset_store(<<"ZINTERSTORE">>, Dst, Keys, Weights, Aggregate). 629 | 630 | %% @doc Return a range of members in a sorted set, by index 631 | -spec zrange(key(), int(), int(), boolean()) -> 632 | [val()] | [{str(), score()}]. 633 | zrange(Key, Start, End) -> 634 | zrange(Key, Start, End, false). 635 | zrange(Key, Start, End, WithScore) -> 636 | do_zrange(<<"ZRANGE">>, Key, Start, End, WithScore). 637 | 638 | %% @doc Return a range of members in a sorted set, by score 639 | -spec zrangebyscore(key(), score(), score(), boolean()) -> 640 | [val()] | [{str(), score()}]. 641 | zrangebyscore(Key, Min, Max) -> 642 | zrangebyscore(Key, Min, Max, false). 643 | zrangebyscore(Key, Min, Max, WithScore) -> 644 | do_zrange_by_score(<<"ZRANGEBYSCORE">>, Key, Min, Max, WithScore). 645 | 646 | %% @doc Determine the index of a member in a sorted set 647 | -spec zrank(key(), key()) -> int() | null(). 648 | zrank(Key, Mem) -> 649 | call(mbulk(<<"ZRANK">>, Key, Mem)). 650 | 651 | %% @doc Remove one or more members from a sorted set 652 | -spec zrem(key(), key() | [key()]) -> uint(). 653 | zrem(Key, [H|_] = Mem) when is_list(H); is_binary(H) -> 654 | call(mbulk_list([<<"ZREM">>, Key | Mem])); 655 | zrem(Key, Mem) -> 656 | call(mbulk(<<"ZREM">>, Key, Mem)). 657 | 658 | %% @doc Remove all members in a sorted set within the given indexes 659 | -spec zremrangebyrank(key(), uint(), uint()) -> uint(). 660 | zremrangebyrank(Key, Start, End) -> 661 | call(mbulk(<<"ZREMRANGEBYRANK">>, Key, ?N2S(Start), ?N2S(End))). 662 | 663 | %% @doc Remove all members in a sorted set within the given scores 664 | -spec zremrangebyscore(key(), score(), score()) -> uint(). 665 | zremrangebyscore(Key, Min, Max) -> 666 | call(mbulk(<<"ZREMRANGEBYSCORE">>, Key, 667 | score_to_str(Min), score_to_str(Max))). 668 | 669 | %% @doc Return a range of members in a sorted set, by index, 670 | %% with scores ordered from high to low 671 | -spec zrevrange(key(), int(), int(), boolean()) -> 672 | [value()] | [{key(), score()}]. 673 | zrevrange(Key, Start, End) -> 674 | zrevrange(Key, Start, End, false). 675 | zrevrange(Key, Start, End, WithScore) -> 676 | do_zrange(<<"ZREVRANGE">>, Key, Start, End, WithScore). 677 | 678 | %% @doc Return a range of members in a sorted set, by score, 679 | %% with scores ordered from high to low 680 | -spec zrevrangebyscore(key(), score(), score(), boolean()) -> 681 | [val()] | [{key(), score()}]. 682 | zrevrangebyscore(Key, Max, Min) -> 683 | zrevrangebyscore(Key, Max, Min, false). 684 | zrevrangebyscore(Key, Max, Min, WithScore) -> 685 | do_zrange_by_score(<<"ZREVRANGEBYSCORE">>, Key, Max, Min, WithScore). 686 | 687 | %% @doc Determine the index of a member in a sorted set, 688 | %% with scores ordered from high to low 689 | -spec zrevrank(key(), key()) -> int() | null(). 690 | zrevrank(Key, Mem) -> 691 | call(mbulk(<<"ZREVRANK">>, Key, Mem)). 692 | 693 | %% @doc Get the score associated with the given member 694 | %% in a sorted set 695 | -spec zscore(key(), key()) -> score() | null(). 696 | zscore(Key, Mem) -> 697 | call(mbulk(<<"ZSCORE">>, Key, Mem), fun str_to_score/1). 698 | 699 | %% @doc Add multiple sorted sets and store 700 | %% the resulting sorted set in a new key 701 | -spec zunionstore(key(), [key()]) -> uint(). 702 | zunionstore(Dst, Keys) -> 703 | zunionstore(Dst, Keys, [], sum). 704 | zunionstore(Dst, Keys, Weights) -> 705 | ?ASSERT(length(Keys) =:= length(Weights)), 706 | zunionstore(Dst, Keys, Weights, sum). 707 | zunionstore(Dst, Keys, Weights, Aggregate) -> 708 | do_zset_store(<<"ZUNIONSTORE">>, Dst, Keys, Weights, Aggregate). 709 | 710 | %%---------------- 711 | %% pub/sub 712 | %%---------------- 713 | 714 | %% @doc Listen for messages published to channels 715 | %% matching the given patterns 716 | %% CbSub: called when received subscribe reply 717 | %% CbMsg: called when received msg 718 | %% (can't pipeline) 719 | -spec psubscribe(pattern() | [pattern()], 720 | psubscribe_fun(), pmessage_fun()) -> 'ok'. 721 | psubscribe(Pattern, CbSub, CbMsg) 722 | when is_function(CbSub, 2), 723 | is_function(CbMsg, 3) -> 724 | L = may_single_to_list(Pattern), 725 | redis_client:psubscribe(Client, L, CbSub, CbMsg). 726 | 727 | %% @doc Listen for messages published to the given channels 728 | %% (can't pipeline) 729 | -spec subscribe(channel() | [channel()], 730 | subscribe_fun(), message_fun()) -> 'ok'. 731 | subscribe(Channel, CbSub, CbMsg) 732 | when is_function(CbSub, 2), 733 | is_function(CbMsg, 2) -> 734 | L = may_single_to_list(Channel), 735 | redis_client:subscribe(Client, L, CbSub, CbMsg). 736 | 737 | %% @doc publish a message to channel 738 | -spec publish(channel(), str()) -> uint(). 739 | publish(Channel, Msg) -> 740 | call(mbulk(<<"PUBLISH">>, Channel, Msg)). 741 | 742 | %% @doc unsubscribe to all channels 743 | %% CbUnSub: called when received unsubscribe reply 744 | %% (can't pipeline) 745 | -spec punsubscribe(punsubscribe_fun()) -> 'ok'. 746 | punsubscribe(CbUnSub) -> 747 | punsubscribe([], CbUnSub). 748 | -spec punsubscribe(str() | [str()], punsubscribe_fun()) -> 'ok'. 749 | punsubscribe(Pattern, CbUnSub) 750 | when is_function(CbUnSub, 2) -> 751 | L = may_single_to_list(Pattern), 752 | redis_client:punsubscribe(Client, L, CbUnSub). 753 | 754 | %% @doc unsubscribe to all channels 755 | %% (can't pipeline) 756 | -spec unsubscribe(unsubscribe_fun()) -> 'ok'. 757 | unsubscribe(CbUnSub) -> 758 | unsubscribe([], CbUnSub). 759 | -spec unsubscribe(channel() | [channel()], unsubscribe_fun()) -> 'ok'. 760 | unsubscribe(Channel, CbUnSub) 761 | when is_function(CbUnSub, 2) -> 762 | L = may_single_to_list(Channel), 763 | redis_client:unsubscribe(Client, L, CbUnSub). 764 | 765 | %%--------------------------- 766 | %% transaction commands 767 | %%--------------------------- 768 | 769 | %% @doc Mark the start of a transaction block 770 | -spec multi() -> 'ok'. 771 | multi() -> 772 | call(mbulk(<<"MULTI">>)). 773 | 774 | %% @doc Execute all commands issued after MULTI 775 | -spec exec() -> any(). 776 | exec() -> 777 | call(mbulk(<<"EXEC">>)). 778 | 779 | %% @doc Discard all commands issued after MULTI 780 | -spec discard() -> 'ok'. 781 | discard() -> 782 | call(mbulk(<<"DISCARD">>)). 783 | 784 | %% @doc watch some keys, before the EXEC command in transaction, if the watched keys 785 | %% were changed, the transaction failed, otherwise the transaction success 786 | -spec watch(key() | [key()]) -> 'ok'. 787 | watch(Key) -> 788 | L = may_single_to_list(Key), 789 | call(mbulk_list([<<"WATCH">> | L])). 790 | 791 | %% @doc Forget about all watched keys 792 | -spec unwatch() -> 'ok'. 793 | unwatch() -> 794 | call(mbulk(<<"UNWATCH">>)). 795 | 796 | %%------------ 797 | %% pipeline 798 | %%------------ 799 | 800 | %% @doc pipeline commands 801 | %% e.g. 802 | %% F = 803 | %% fun() -> 804 | %% exists("key1"), 805 | %% set("key1", "val1"), 806 | %% get("key1") 807 | %% end, 808 | %% pipeline(F). 809 | pipeline(Fun) when is_function(Fun, 0) -> 810 | case Pipeline of 811 | true -> 812 | Fun(), 813 | Cmds = get_pipeline_cmd(), 814 | FunList = get_pipeline_fun(), 815 | ResultList = redis_client:multi_command(Client, Cmds), 816 | clear_pipeline_ctx(), 817 | {Result, _} = 818 | lists:mapfoldl( 819 | fun(R, [F|T]) -> 820 | case F of 821 | ?NONE -> 822 | {R, T}; 823 | _ -> 824 | {F(R), T} 825 | end 826 | end, FunList, ResultList), 827 | Result; 828 | false -> 829 | throw(badmodel) 830 | end. 831 | 832 | %%---------------------- 833 | %% connections 834 | %%---------------------- 835 | 836 | %% @doc simple password authentication 837 | -spec auth(iolist()) -> 'ok' | error(). 838 | auth(Pass) -> 839 | call(mbulk(<<"AUTH">>, Pass)). 840 | 841 | %% @doc Echo the given string 842 | -spec echo(str()) -> str(). 843 | echo(Msg) -> 844 | call(mbulk(<<"ECHO">>, Msg)). 845 | 846 | %% @doc ping the server 847 | -spec ping() -> status_code(). 848 | ping() -> 849 | call(mbulk(<<"PING">>)). 850 | 851 | %% @doc close the connection 852 | %% (can't pipeline) 853 | -spec quit() -> 'ok'. 854 | quit() -> 855 | redis_client:quit(Client). 856 | 857 | %% @doc Change the selected database for the current connection 858 | %% (can't pipeline) 859 | -spec select(uint()) -> status_code(). 860 | select(Index) -> 861 | R = call(mbulk(<<"SELECT">>, ?N2S(Index))), 862 | case R of 863 | ok -> 864 | redis_client:set_selected_db(Client, Index); 865 | Other -> 866 | Other 867 | end. 868 | 869 | %%--------- 870 | %% server 871 | %%--------- 872 | 873 | %% @doc Asynchronously rewrite the append-only file 874 | -spec bgrewriteaof() -> 'ok'. 875 | bgrewriteaof() -> 876 | call(mbulk(<<"BGREWRITEAOF">>)). 877 | 878 | %% @doc Asynchronously save the dataset to disk 879 | -spec bgsave() -> status_code(). 880 | bgsave() -> 881 | call(mbulk(<<"BGSAVE">>)). 882 | 883 | %% @doc Get the value of a configuration parameter 884 | -spec config_get(pattern()) -> [{str(), str()}]. 885 | config_get(Pattern) -> 886 | call(mbulk(<<"CONFIG">>, <<"GET">>, Pattern), fun list_to_kv_tuple/1). 887 | 888 | %% @doc Set a configuration parameter to the given value 889 | -spec config_set(str(), str()) -> status_code(). 890 | config_set(Par, L) when Par =:= "save"; Par =:= <<"save">> -> 891 | Str = to_save_str(L), 892 | call(mbulk(<<"CONFIG">>, <<"SET">>, <<"save">>, Str)); 893 | config_set(Par, Val) when is_list(Par) -> 894 | ValStr = 895 | if 896 | is_integer(Val) -> 897 | ?N2S(Val); 898 | is_list(Val) -> 899 | Val 900 | end, 901 | call(mbulk(<<"CONFIG">>, <<"SET">>, Par, ValStr)). 902 | 903 | %% @doc Reset the stats returned by INFO 904 | -spec config_resetstat() -> 'ok'. 905 | config_resetstat() -> 906 | call(mbulk(<<"CONFIG">>, <<"RESETSTAT">>)). 907 | 908 | %% @doc Return the number of keys in the selected database 909 | -spec dbsize() -> count(). 910 | dbsize() -> 911 | call(mbulk(<<"DBSIZE">>)). 912 | 913 | %% @doc Get debugging information about a key 914 | -spec debug_object(key()) -> val(). 915 | debug_object(Key) -> 916 | call(mbulk(<<"DEBUG">>, <<"OBJECT">>, Key)). 917 | 918 | %% @doc Make the server crash 919 | -spec debug_segfault() -> no_return(). 920 | debug_segfault() -> 921 | call(mbulk(<<"DEBUG">>, <<"SEGFAULT">>)). 922 | 923 | %% @doc Remove all keys from all databases 924 | -spec flushall() -> status_code(). 925 | flushall() -> 926 | call(mbulk(<<"FLUSHALL">>)). 927 | 928 | %% @doc Remove all keys from the current database 929 | -spec flushdb() -> status_code(). 930 | flushdb() -> 931 | call(mbulk(<<"FLUSHDB">>)). 932 | 933 | %% @doc return the info about the server 934 | -spec info() -> [{str(), val()}]. 935 | info() -> 936 | call(mbulk(<<"INFO">>), fun list_to_kv_tuple/1). 937 | 938 | %% @doc Get the UNIX time stamp of the last successful save to disk 939 | -spec lastsave() -> timestamp(). 940 | lastsave() -> 941 | call(mbulk(<<"LASTSAVE">>)). 942 | 943 | %% @doc synchronously save the DB on disk 944 | -spec save() -> 'ok' | error(). 945 | save() -> 946 | call(mbulk(<<"SAVE">>)). 947 | 948 | %% @doc synchronously save the DB on disk, then shutdown the server 949 | %% (can't pipeline) 950 | -spec shutdown() -> status_code(). 951 | shutdown() -> 952 | redis_client:shutdown(Client). 953 | 954 | %% @doc Make the server a slave of another instance, 955 | %% or promote it as master 956 | -spec slave_of(iolist(), integer()) -> status_code(). 957 | slave_of(Host, Port) when 958 | (is_list(Host) orelse is_binary(Host)), 959 | is_integer(Port) -> 960 | call(mbulk(<<"SLAVEOF">>, Host, ?N2S(Port))). 961 | 962 | %% @doc make the redis from slave into a master instance 963 | -spec slave_off() -> 'ok'. 964 | slave_off() -> 965 | call(mbulk(<<"SLAVEOF">>, <<"no">>, <<"one">>)). 966 | 967 | %% @doc other unknown commands 968 | command(Args) -> 969 | call(mbulk_list(Args)). 970 | 971 | %%------------------------------------------------------------------------------ 972 | %% 973 | %% internal API 974 | %% 975 | %%------------------------------------------------------------------------------ 976 | 977 | %% get sorted set in range, by index 978 | do_zrange(Cmd, Key, Start, End, false) -> 979 | call(mbulk(Cmd, Key, ?N2S(Start), ?N2S(End))); 980 | do_zrange(Cmd, Key, Start, End, true) -> 981 | call(mbulk_list([Cmd, Key, ?N2S(Start), ?N2S(End), <<"WITHSCORES">>]), 982 | fun(R) -> list_to_kv_tuple(R, fun(S) -> str_to_score(S) end) end). 983 | 984 | %% get sorted set in range, by score 985 | do_zrange_by_score(Cmd, Key, Min, Max, false) -> 986 | call(mbulk(Cmd, Key, score_to_str(Min), score_to_str(Max)), 987 | fun may_null_to_list/1); 988 | do_zrange_by_score(Cmd, Key, Min, Max, true) -> 989 | call(mbulk_list([Cmd, Key, 990 | score_to_str(Min), score_to_str(Max), <<"WITHSCORES">>]), 991 | fun(R) -> list_to_kv_tuple(R, fun(S) -> str_to_score(S) end) end). 992 | 993 | do_zset_store(Cmd, Dst, Keys, Weights, Aggregate) when 994 | Aggregate =:= sum; 995 | Aggregate =:= min; 996 | Aggregate =:= max -> 997 | L = Keys 998 | ++ ?IF(Weights =:= [], [], [<<"WEIGHTS">> | Weights]) 999 | ++ [<<"AGGREGATE">>, aggregate_to_str(Aggregate)], 1000 | call(mbulk_list([Cmd, Dst, length(Keys) | L])). 1001 | 1002 | %% call the command 1003 | call(Cmd) -> 1004 | call(Cmd, ?NONE). 1005 | call(Cmd, Fun) -> 1006 | case Pipeline of 1007 | false -> 1008 | % normal model 1009 | R = redis_client:command(Client, Cmd), 1010 | ?IF(Fun =/= ?NONE, Fun(R), R); 1011 | true -> 1012 | % pipeline model 1013 | add_pipeline_cmd(Cmd, Fun) 1014 | end. 1015 | 1016 | %% get the application vsn 1017 | %% return 'undefined' | string(). 1018 | get_app_vsn() -> 1019 | case application:get_application() of 1020 | undefined -> 1021 | undefined; 1022 | _ -> 1023 | {ok, App} = application:get_application(), 1024 | {ok, Vsn} = application:get_key(App, vsn), 1025 | Vsn 1026 | end. 1027 | 1028 | %%-------------------- 1029 | %% pipeline internal 1030 | %%-------------------- 1031 | -define(PIPELINE_CMD_KEY, '$pipeline_cmd_key'). 1032 | -define(PIPELINE_FUN_KEY, '$pipeline_fun_key'). 1033 | 1034 | add_pipeline_cmd(Cmd, Fun) -> 1035 | proc_list_add(?PIPELINE_CMD_KEY, Cmd), 1036 | proc_list_add(?PIPELINE_FUN_KEY, Fun). 1037 | 1038 | get_pipeline_cmd() -> 1039 | proc_list_get(?PIPELINE_CMD_KEY). 1040 | 1041 | get_pipeline_fun() -> 1042 | proc_list_get(?PIPELINE_FUN_KEY). 1043 | 1044 | clear_pipeline_ctx() -> 1045 | erlang:erase(?PIPELINE_CMD_KEY), 1046 | erlang:erase(?PIPELINE_FUN_KEY), 1047 | ok. 1048 | 1049 | proc_list_add(Key, Val) -> 1050 | case erlang:get(Key) of 1051 | undefined -> 1052 | erlang:put(Key, [Val]); 1053 | L -> 1054 | erlang:put(Key, [Val | L]) 1055 | end. 1056 | 1057 | proc_list_get(Key) -> 1058 | case erlang:get(Key) of 1059 | undefined -> 1060 | []; 1061 | L -> 1062 | lists:reverse(L) 1063 | end. 1064 | 1065 | %%------------------ 1066 | %% data convert 1067 | %%------------------ 1068 | 1069 | %% the timeout value 1070 | timeout_val(infinity) -> 1071 | "0"; 1072 | timeout_val(T) when is_integer(T), T >= 0 -> 1073 | ?N2S(T). 1074 | 1075 | %% convert list to n elments tuple list 1076 | list_to_n_tuple([], _N) -> 1077 | []; 1078 | list_to_n_tuple(L, 1) -> 1079 | L; 1080 | list_to_n_tuple(L, N) when N > 1 -> 1081 | {1, [], AccL} = 1082 | lists:foldl( 1083 | fun 1084 | (E, {I, AccPart, AccL}) when I < N -> 1085 | {I+1, [E | AccPart], AccL}; 1086 | (E, {I, AccPart, AccL}) when I =:= N -> 1087 | {1, [], [list_to_tuple(lists:reverse([E | AccPart])) | AccL]} 1088 | end, 1089 | {1, [], []}, L), 1090 | lists:reverse(AccL). 1091 | 1092 | 1093 | aggregate_to_str(sum) -> <<"SUM">>; 1094 | aggregate_to_str(min) -> <<"MIN">>; 1095 | aggregate_to_str(max) -> <<"MAX">>. 1096 | 1097 | %% convert [{200, 2}, {300, 4}] to 1098 | %% "200 2 300 4" 1099 | to_save_str(L) -> 1100 | L2 = 1101 | [lists:concat([Time, " ", Change]) 1102 | || {Time, Change} <- L, is_integer(Time), is_integer(Change)], 1103 | string:join(L2, " "). 1104 | 1105 | %% if the value is empty string 1106 | is_empty_str("") -> true; 1107 | is_empty_str(<<>>) -> true; 1108 | is_empty_str(_) -> false. 1109 | 1110 | %% convert to list 1111 | may_single_to_list([]) -> []; 1112 | may_single_to_list([H|_] = V) when is_list(H); is_binary(H) -> V; 1113 | may_single_to_list(V) -> [V]. 1114 | 1115 | %% convert to boolean if the value is possible, otherwise return the value self 1116 | int_may_bool(0) -> false; 1117 | int_may_bool(1) -> true; 1118 | int_may_bool(V) -> V. 1119 | 1120 | %% convert list like [f1, v1, f2, v2] to the key-value tuple 1121 | %% [{f1, v1}, {f2, v2}]. 1122 | list_to_kv_tuple(null) -> 1123 | []; 1124 | list_to_kv_tuple(L) -> 1125 | list_to_kv_tuple(L, null). 1126 | 1127 | list_to_kv_tuple([], _Fun) -> 1128 | []; 1129 | list_to_kv_tuple(L, Fun) -> 1130 | {odd, null, AccL} = 1131 | lists:foldl( 1132 | fun 1133 | (E, {odd, null, AccL}) -> 1134 | {even, E, AccL}; 1135 | (E, {even, EFirst, AccL}) -> 1136 | {odd, null, [{EFirst, do_fun(Fun, E)} | AccL]} 1137 | end, 1138 | {odd, null, []}, L), 1139 | lists:reverse(AccL). 1140 | 1141 | %% call the function on the element 1142 | do_fun(null, E) -> 1143 | E; 1144 | do_fun(Fun, E) -> 1145 | Fun(E). 1146 | 1147 | %% convert reply to list 1148 | may_null_to_list(null) -> []; 1149 | may_null_to_list(L) when is_list(L) -> L. 1150 | 1151 | %% score convert string for zset 1152 | score_to_str('-inf') -> 1153 | <<"-inf">>; 1154 | score_to_str('+inf') -> 1155 | <<"+inf">>; 1156 | score_to_str({open, S}) -> 1157 | [$( | score_to_str(S)]; 1158 | score_to_str({closed, S}) -> 1159 | score_to_str(S); 1160 | score_to_str(S) when is_integer(S) -> 1161 | ?N2S(S); 1162 | score_to_str(S) when is_float(S) -> 1163 | ?F2S(S). 1164 | 1165 | %% string => score 1166 | str_to_score(null) -> 1167 | null; 1168 | str_to_score(B) when is_binary(B) -> 1169 | str_to_score(binary_to_list(B)); 1170 | str_to_score(S) when is_list(S) -> 1171 | case catch list_to_integer(S) of 1172 | {'EXIT', _} -> 1173 | list_to_float(S); 1174 | N -> 1175 | N 1176 | end. 1177 | 1178 | -------------------------------------------------------------------------------- /src/redis_client.erl: -------------------------------------------------------------------------------- 1 | %%%---------------------------------------------------------------------- 2 | %%% 3 | %%% @copyright erl-redis 2010 4 | %%% 5 | %%% @author litaocheng 6 | %%% @doc the redis client 7 | %%% @end 8 | %%% 9 | %%%---------------------------------------------------------------------- 10 | -module(redis_client). 11 | -author('litaocheng@gmail.com'). 12 | -vsn('0.1'). 13 | -behaviour(gen_server). 14 | -include("redis_internal.hrl"). 15 | 16 | -export([start/3, start/4, start_link/3, start_link/4]). 17 | -export([stop/1]). 18 | -export([handler/1, pipeline/1]). 19 | 20 | -export([get_server/1, get_sock/1]). 21 | -export([set_selected_db/2, get_selected_db/1]). 22 | -export([command/2, multi_command/2]). 23 | -export([psubscribe/4, punsubscribe/3, subscribe/4, unsubscribe/3]). 24 | -export([quit/1, shutdown/1]). 25 | 26 | %% some untility functions 27 | -export([msg_send_cb/2]). 28 | -export([name/2, existing_name/2, name/3, existing_name/3]). 29 | 30 | %% gen_server callbacks 31 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, 32 | terminate/2, code_change/3]). 33 | 34 | -compile({inline, [command/2]}). 35 | 36 | %% pub/sub info(used by channel pub/sub and pattern pub/sub) 37 | -record(pubsub, { 38 | id, % id: {type, pattern() | channel()} 39 | cb_sub, % the callback fun for subscribe 40 | cb_unsub, % the callback fun for unsubscribe 41 | cb_msg % the callback fun for received message 42 | }). 43 | 44 | -define(PUBSUB_CHANNEL, 'channel'). 45 | -define(PUBSUB_PATTERN, 'pattern'). 46 | 47 | -record(state, { 48 | server, % the host and port 49 | sock = null, % the socket 50 | dbindex = 0, % the selected db index 51 | ctx = normal, % the client context 52 | pubsub_tid % the pubsub table 53 | }). 54 | 55 | -define(TCP_OPTS, [inet, binary, {active, once}, 56 | {packet, line}, 57 | {nodelay, false}, 58 | {recbuf, 16#1000}, 59 | {sndbuf, 16#10000}, 60 | {send_timeout, 5000}, {send_timeout_close, true}]). 61 | 62 | -spec start(inet_host(), inet_port(), passwd()) -> 63 | {'ok', any()} | 'ignore' | {'error', any()}. 64 | start(Host, Port, Passwd) -> 65 | ?DEBUG2("start redis_client ~p:~p", [Host, Port]), 66 | gen_server:start(?MODULE, {{Host, Port}, Passwd}, []). 67 | 68 | -spec start(inet_host(), inet_port(), passwd(), atom()) -> 69 | {'ok', any()} | 'ignore' | {'error', any()}. 70 | start(Host, Port, Passwd, Name) -> 71 | ?DEBUG2("start redis_client ~p", [Name]), 72 | gen_server:start({local, Name}, ?MODULE, {{Host, Port}, Passwd}, []). 73 | 74 | -spec start_link(inet_host(), inet_port(), passwd()) -> 75 | {'ok', any()} | 'ignore' | {'error', any()}. 76 | start_link(Host, Port, Passwd) -> 77 | ?DEBUG2("start_link redis_client ~p:~p", [Host, Port]), 78 | gen_server:start_link(?MODULE, {{Host, Port}, Passwd}, []). 79 | 80 | -spec start_link(inet_host(), inet_port(), passwd(), atom()) -> 81 | {'ok', any()} | 'ignore' | {'error', any()}. 82 | start_link(Host, Port, Passwd, Name) -> 83 | ?DEBUG2("start_link redis_client ~p", [Name]), 84 | gen_server:start_link({local, Name}, ?MODULE, {{Host, Port}, Passwd}, []). 85 | 86 | -spec stop(client() | tuple()) -> 'ok'. 87 | stop({redis, Client, _}) -> 88 | call(Client, stop); 89 | stop(Client) -> 90 | call(Client, stop). 91 | 92 | %% @doc get the redis normal handler 93 | -spec handler(client()) -> redis_handler(). 94 | handler(Client) -> 95 | redis:new(Client, false). 96 | 97 | %% @doc get the redis pipeline handler 98 | -spec pipeline(client() | redis_handler()) -> redis_handler(). 99 | pipeline({redis, Client, _}) -> 100 | redis:new(Client, true); 101 | pipeline(Client) -> 102 | redis:new(Client, true). 103 | 104 | %% @doc get the server info 105 | -spec get_server(Client :: pid()) -> inet_server(). 106 | get_server(Client) -> 107 | call(Client, get_server). 108 | 109 | %% @doc return the socket 110 | -spec get_sock(Client :: pid()) -> {'ok', port()}. 111 | get_sock(Client) -> 112 | {ok, call(Client, get_sock)}. 113 | 114 | %% @doc set the select db 115 | -spec set_selected_db(client(), uint()) -> 116 | 'ok' | {'error', any()}. 117 | set_selected_db(Client, Index) -> 118 | call(Client, {set, dbindex, Index}). 119 | 120 | %% @doc get the selected db 121 | -spec get_selected_db(atom()) -> uint(). 122 | get_selected_db(Client) -> 123 | call(Client, {get, dbindex}). 124 | 125 | %% @doc send the command to redis server 126 | -spec command(client(), iolist()) -> any(). 127 | command(Client, Data) -> 128 | call(Client, {command, {Data, ?COMMAND_TIMEOUT}}). 129 | 130 | %% @doc send multiple commands to redis server 131 | -spec multi_command(client(), [iolist()]) -> any(). 132 | multi_command(Client, List) -> 133 | Len = length(List), 134 | call(Client, {command, {List, Len, ?COMMAND_TIMEOUT}}). 135 | 136 | %% @doc subscribe the the patterns, called by redis:psubscribe 137 | psubscribe(Client, Patterns, CbSub, CbMsg) -> 138 | call(Client, {subscribe, ?PUBSUB_PATTERN, Patterns, CbSub, CbMsg}). 139 | 140 | %% @doc unsubscribe from the patterns, called by redis:punsubscribe 141 | punsubscribe(Client, Patterns, Callback) -> 142 | call(Client, {unsubscribe, ?PUBSUB_PATTERN, Patterns, Callback}). 143 | 144 | %% NOTE: the subscribe and unsubscribe functions are asynchronous, 145 | %% it will return 'ok' immediately. you must do the working in 146 | %% callback functions 147 | subscribe(Client, Channels, CbSub, CbMsg) -> 148 | ?DEBUG2("subscribe to channels:~p", [Channels]), 149 | call(Client, {subscribe, ?PUBSUB_CHANNEL, Channels, CbSub, CbMsg}). 150 | 151 | unsubscribe(Client, Channels, Callback) -> 152 | ?DEBUG2("unsubscribe to channels:~p", [Channels]), 153 | call(Client, {unsubscribe, ?PUBSUB_CHANNEL, Channels, Callback}). 154 | 155 | -spec quit(client()) -> 156 | 'ok' | {'error', any()}. 157 | quit(Client) -> 158 | call(Client, {shutdown, <<"QUIT">>}). 159 | 160 | -spec shutdown(client()) -> 161 | 'ok' | {'error', any()}. 162 | shutdown(Client) -> 163 | call(Client, {shutdown, <<"SHUTDOWN">>}). 164 | 165 | %% @doc generate one callback functions for pubsub, which send the 166 | %% message to some process 167 | -spec msg_send_cb(pid() | atom(), atom()) -> 168 | fun(). 169 | msg_send_cb(Dest, subscribe) -> 170 | fun(Channel, N) -> 171 | catch Dest ! {subscirbe, Channel, N} 172 | end; 173 | msg_send_cb(Dest, unsubscribe) -> 174 | fun(Channel, N) -> 175 | catch Dest ! {unsubscribe, Channel, N} 176 | end; 177 | msg_send_cb(Dest, message) -> 178 | fun(Channel, Msg) -> 179 | catch Dest ! {message, Channel, Msg} 180 | end. 181 | 182 | %% @doc generate process registered name 183 | name(Host, Port) -> 184 | to_name(Host, Port, true). 185 | existing_name(Host, Port) -> 186 | to_name(Host, Port, false). 187 | 188 | %% @doc generate process registered name 189 | name(Host, Port, UserData) -> 190 | to_name(Host, Port, UserData, true). 191 | existing_name(Host, Port, UserData) -> 192 | to_name(Host, Port, UserData, false). 193 | 194 | %% 195 | %% gen_server callbacks 196 | %% 197 | init({Server = {Host, Port}, Passwd}) -> 198 | process_flag(trap_exit, true), 199 | case gen_tcp:connect(Host, Port, ?TCP_OPTS, ?CONN_TIMEOUT) of 200 | {ok, Sock} -> 201 | case do_auth(Sock, Passwd) of 202 | ok -> 203 | Tid = do_create_table(), 204 | {ok, #state{server = Server, sock = Sock, pubsub_tid = Tid}}; 205 | {tcp_error, Reason} -> 206 | {stop, Reason}; 207 | _ -> 208 | ?ERROR2("auth failed", []), 209 | {stop, auth_failed} 210 | end; 211 | {error, Reason} -> 212 | {stop, Reason} 213 | end. 214 | 215 | handle_call({command, {Data, Timeout}}, _From, 216 | State = #state{sock = Sock, server = _Server, ctx = normal}) -> 217 | ?DEBUG2("command:~n~p~n\t=> ~p", [Data, _Server]), 218 | Reply = do_send_recv(Data, Sock, Timeout), 219 | {reply, Reply, State}; 220 | handle_call({command, {Data, Len, Timeout}}, _From, 221 | State = #state{sock = Sock, server = _Server, ctx = normal}) -> 222 | ?DEBUG2("multi_command(~p):~p=> ~p", [Len, Data, _Server]), 223 | Reply = do_send_multi_recv(Data, Len, Sock, Timeout), 224 | {reply, Reply, State}; 225 | handle_call({command, _}, _From, State) -> 226 | {reply, badmodel, State}; 227 | 228 | %% abouts pub/sub 229 | handle_call({subscribe, Type, L, CbSub, CbMsg}, _From, 230 | State = #state{sock = Sock, pubsub_tid = Tid}) -> 231 | Cmd = 232 | case Type of 233 | ?PUBSUB_CHANNEL -> <<"SUBSCRIBE">>; 234 | ?PUBSUB_PATTERN -> <<"PSUBSCRIBE">> 235 | end, 236 | Data = redis_proto:mbulk_list([Cmd | L]), 237 | do_send(Sock, Data), 238 | ok = do_add_pubsub(Type, L, CbSub, CbMsg, Tid), 239 | {reply, ok, State#state{ctx = pubsub}}; 240 | handle_call({unsubscribe, _, _Callback}, _From, State = #state{ctx = normal}) -> 241 | {reply, {error, in_normal_mode}, State}; 242 | handle_call({unsubscribe, Type, L, Callback}, _From, 243 | State = #state{sock = Sock, pubsub_tid = Tid}) -> 244 | Cmd = 245 | case Type of 246 | ?PUBSUB_CHANNEL -> <<"UNSUBSCRIBE">>; 247 | ?PUBSUB_PATTERN -> <<"PUNSUBSCRIBE">> 248 | end, 249 | Data = redis_proto:mbulk_list([Cmd | L]), 250 | do_send(Sock, Data), 251 | ok = do_update_pubsub_unsub(Type, L, Callback, Tid), 252 | {reply, ok, State}; 253 | 254 | handle_call({shutdown, Data}, _From, State = #state{sock = Sock}) -> 255 | case catch do_send_recv(Sock, Data) of 256 | {error, tcp_closed} -> 257 | {stop, normal, ok, State}; 258 | _ -> 259 | {stop, normal, ok, State} 260 | end; 261 | handle_call(get_server, _From, State = #state{server = Server}) -> 262 | {reply, Server, State}; 263 | handle_call(get_sock, _From, State = #state{sock = Sock}) -> 264 | {reply, Sock, State}; 265 | handle_call({set, dbindex, Index}, _From, State) -> 266 | {reply, ok, State#state{dbindex = Index}}; 267 | handle_call({get, dbindex}, _From, State = #state{dbindex = Index}) -> 268 | {reply, Index, State}; 269 | handle_call(stop, _From, State) -> 270 | {stop, normal, ok, State}; 271 | handle_call(_Msg, _From, State) -> 272 | {noreply, State}. 273 | 274 | handle_cast(_Msg, State) -> 275 | {noreply, State}. 276 | 277 | handle_info({error, timeout}, State) -> % send timeout 278 | ?ERROR2("send timeout, the socket closed, process will restart", []), 279 | {stop, {error, timeout}, State}; 280 | handle_info({tcp_closed, Sock}, State = #state{sock = Sock}) -> 281 | ?ERROR2("socket closed by remote peer", []), 282 | {stop, tcp_closed, State}; 283 | handle_info({tcp, Sock, Packet}, State = #state{sock = Sock, ctx = pubsub}) -> 284 | State2 = do_handle_pubsub(Sock, Packet, State), 285 | {noreply, State2}; 286 | handle_info(_Info, State) -> 287 | {noreply, State}. 288 | 289 | terminate(_Reason, _State) -> 290 | ?DEBUG2("terminate reason:~p", [_Reason]), 291 | ok. 292 | 293 | code_change(_Old, State, _Extra) -> 294 | {ok, State}. 295 | 296 | %%----------------------------------------------------------------------------- 297 | %% 298 | %% internal API 299 | %% 300 | %%----------------------------------------------------------------------------- 301 | 302 | call(Client, Req) -> 303 | gen_server:call(Client, Req, infinity). 304 | 305 | %% create pubsub table 306 | do_create_table() -> 307 | ets:new(dummy, [set, private, {keypos, #pubsub.id}, 308 | {read_concurrency, true}]). 309 | 310 | to_name(Host, Port, First) -> 311 | L = lists:concat(["redis_client_", Host, "_", Port]), 312 | to_atom(L, First). 313 | to_name(Host, Port, UserData, First) when is_list(UserData); 314 | is_atom(UserData); 315 | is_integer(UserData) -> 316 | L = lists:concat(["redis_client_", Host, "_", Port, "_", UserData]), 317 | to_atom(L, First). 318 | 319 | to_atom(String, true) -> 320 | list_to_atom(String); 321 | to_atom(String, false) -> 322 | list_to_existing_atom(String). 323 | 324 | %% do sync send and recv 325 | do_send_recv(Data, Sock) -> 326 | do_send(Sock, Data), 327 | do_recv(Sock, null, ?COMMAND_TIMEOUT). 328 | 329 | do_send_recv(Data, Sock, Timeout) -> 330 | do_send(Sock, Data), 331 | do_recv(Sock, null, Timeout). 332 | 333 | do_send_multi_recv(Data, Len, Sock, Timeout) -> 334 | do_send(Sock, Data), 335 | do_send_multi_recv1(Len, Sock, Timeout, []). 336 | do_send_multi_recv1(0, _Sock, _Timeout, Acc) -> 337 | lists:reverse(Acc); 338 | do_send_multi_recv1(N, Sock, Timeout, Acc) -> 339 | Reply = do_recv(Sock, null, Timeout), 340 | do_send_multi_recv1(N - 1, Sock, Timeout, [Reply | Acc]). 341 | 342 | do_send(Sock, Data) -> 343 | case gen_tcp:send(Sock, Data) of 344 | ok -> % receive response 345 | ok; 346 | {error, Reason} -> 347 | ?ERROR2("send message error:~p", [Reason]), 348 | exit({error, Reason}) 349 | end. 350 | 351 | do_recv(Sock, PState, Timeout) -> 352 | receive 353 | {tcp, Sock, Packet} -> 354 | %?DEBUG2("receive packet :~p", [Packet]), 355 | do_handle_packet(Sock, Packet, PState, Timeout); 356 | {tcp_closed, _Socket} -> 357 | ?ERROR2("socket closed by remote peer", []), 358 | exit({error, tcp_closed}); 359 | {tcp_error, _Socket, Reason} -> 360 | ?ERROR2("recv message error:~p", [Reason]), 361 | exit({error, {tcp_error, Reason}}) 362 | after 363 | Timeout -> 364 | ?ERROR2("recv message timeout", []), 365 | exit({error, recv_timeout}) 366 | end. 367 | 368 | do_handle_packet(Sock, Packet, PState, Timeout) -> 369 | case redis_proto:parse_reply(Packet) of 370 | {mbulk_more, MB} when PState =:= null -> % multi bulk replies 371 | %?DEBUG2("need recv mbulk :~p", [MB]), 372 | recv_bulks(Sock, MB, Timeout); 373 | {bulk_more, N} -> % bulk reply 374 | %?DEBUG2("need recv bulk data len:~p", [N]), 375 | recv_bulk_data(Sock, N, Timeout); 376 | Val -> % integer, status or error 377 | %?DEBUG2("parse value is ~p", [Val]), 378 | inet:setopts(Sock, [{active, once}]), 379 | Val 380 | end. 381 | 382 | %% recv the bulk data 383 | recv_bulk_data(Sock, N, Timeout) -> 384 | ok = inet:setopts(Sock, [{packet, raw}]), 385 | <> = recv_n(Sock, N+2, Timeout), % include \r\n 386 | ok = inet:setopts(Sock, [{packet, line}, {active, once}]), 387 | Val. 388 | 389 | %% recv the multiple bulk replies 390 | recv_bulks(Sock, M, Timeout) -> 391 | ok = inet:setopts(Sock, [{active, once}]), 392 | recv_bulks1(Sock, M, [], Timeout). 393 | 394 | recv_bulks1(_Sock, 0, Acc, _Timeout) -> 395 | lists:reverse(Acc); 396 | recv_bulks1(Sock, N, Acc, Timeout) -> 397 | Bulk = do_recv(Sock, mbulk, Timeout), 398 | recv_bulks1(Sock, N-1, [Bulk | Acc], Timeout). 399 | 400 | recv_n(Sock, N, Timeout) -> 401 | case gen_tcp:recv(Sock, N, Timeout) of 402 | {ok, Data} -> 403 | Data; 404 | {error, timeout} -> 405 | ?ERROR2("recv message timeout", []), 406 | throw({recv, timeout}); 407 | {error, R} -> 408 | ?ERROR2("recv message error:~p", [R]), 409 | exit({tcp_error, R}) 410 | end. 411 | 412 | %% do the auth 413 | do_auth(Sock, Passwd) -> 414 | ?DEBUG2("do auth passwd :~p", [Passwd]), 415 | case Passwd of 416 | "" -> 417 | ok; 418 | _ -> 419 | do_send_recv([<<"AUTH ">>, Passwd, ?CRLF], Sock) 420 | end. 421 | 422 | %%------------ 423 | %% pub/sub 424 | %%------------ 425 | 426 | %% add the pubsub entry 427 | do_add_pubsub(Type, L, CbSub, CbMsg, Table) -> 428 | lists:foreach( 429 | fun(E) -> 430 | B = ?IF(is_binary(E), E, ?S2B(E)), 431 | PubSub = #pubsub{ 432 | id = {Type, B}, 433 | cb_sub = CbSub, 434 | cb_msg = CbMsg 435 | }, 436 | ets:insert(Table, PubSub) 437 | end, L), 438 | ok. 439 | 440 | %% delete the pubsub entry 441 | do_del_pubsub(Type, Data, Table) -> 442 | Id = {Type, Data}, 443 | [PubSub] = ets:lookup(Table, Id), 444 | ets:delete(Table, Id), 445 | PubSub. 446 | 447 | %% update the cb_unsub callback in pubsub 448 | do_update_pubsub_unsub(Type, [], Cb, Table) -> 449 | % update all the elements which #pubsub.cb_unsub is undefined 450 | ets:foldl( 451 | fun 452 | (#pubsub{id = {T, _}, cb_unsub = undefined} = PubSub, Acc) when Type =:= T -> 453 | PubSub2 = PubSub#pubsub{cb_unsub = Cb}, 454 | true = ets:insert(Table, PubSub2), 455 | Acc; 456 | (#pubsub{}, Acc) -> 457 | Acc 458 | end, ?NONE, Table), 459 | ok; 460 | do_update_pubsub_unsub(Type, L, Cb, Table) -> 461 | lists:foreach( 462 | fun(E) -> 463 | B = ?IF(is_binary(E), E, ?S2B(E)), 464 | Id = {Type, B}, 465 | PubSub = 466 | case ets:lookup(Table, Id) of 467 | [] -> 468 | #pubsub{ 469 | id = Id, 470 | cb_unsub = Cb 471 | }; 472 | [Exist] -> 473 | Exist#pubsub{cb_unsub = Cb} 474 | end, 475 | ets:insert(Table, PubSub) 476 | end, L). 477 | 478 | %% get the pubsub entry 479 | do_get_pubsub(Type, Data, Table) -> 480 | [PubSub] = ets:lookup(Table, {Type, Data}), 481 | PubSub. 482 | 483 | %% handle the pubsub tcp data 484 | do_handle_pubsub(Sock, Packet, State = #state{pubsub_tid = Table}) -> 485 | List = do_handle_packet(Sock, Packet, null, ?COMMAND_TIMEOUT), 486 | ?DEBUG2("***:~p", [List]), 487 | case List of 488 | [<<"subscribe">>, Channel, N] -> 489 | do_handle_subscribe(?PUBSUB_CHANNEL, Channel, N, Table), 490 | State; 491 | [<<"psubscribe">>, Pattern, N] -> 492 | do_handle_subscribe(?PUBSUB_PATTERN, Pattern, N, Table), 493 | State; 494 | [<<"unsubscribe">>, Channel, N] -> 495 | do_handle_unsubscribe(?PUBSUB_CHANNEL, Channel, N, State); 496 | [<<"punsubscribe">>, Pattern, N] -> 497 | do_handle_unsubscribe(?PUBSUB_PATTERN, Pattern, N, State); 498 | [<<"message">>, Channel, Msg] -> 499 | #pubsub{cb_msg = Fun} = do_get_pubsub(?PUBSUB_CHANNEL, Channel, Table), 500 | try 501 | Fun(Channel, Msg) 502 | catch 503 | _T:_R -> 504 | ?ERROR2("message callback ~p:~p", [_T, _R]) 505 | end, 506 | State; 507 | [<<"pmessage">>, Pattern, Channel, Msg] -> 508 | #pubsub{cb_msg= Fun} = do_get_pubsub(?PUBSUB_PATTERN, Pattern, Table), 509 | try 510 | ?DEBUG2("call pmessage pattern:~p channel:~p msg:~p", 511 | [Pattern, Channel, Msg]), 512 | Fun(Pattern, Channel, Msg) 513 | catch 514 | _T:_R -> 515 | ?ERROR2("pmessage callback ~p:~p", [_T, _R]) 516 | end, 517 | State 518 | end. 519 | 520 | %% handle the subscribe 521 | do_handle_subscribe(Type, Data, N, Table) -> 522 | #pubsub{cb_sub = Fun} = do_get_pubsub(Type, Data, Table), 523 | try 524 | ?DEBUG2("call subscribe Type:~p data:~p n:~p", 525 | [Type, Data, N]), 526 | Fun(Data, N) 527 | catch 528 | _T:_R -> 529 | ?ERROR2("subscribe callback ~p:~p", [_T, _R]) 530 | end. 531 | 532 | %% handle the unsubscribe 533 | do_handle_unsubscribe(_Type, _Data, 0, #state{pubsub_tid = Table} = State) -> 534 | ets:delete_all_objects(Table), 535 | State#state{ctx = normal}; 536 | do_handle_unsubscribe(Type, Data, N, #state{pubsub_tid = Table} = State) -> 537 | #pubsub{cb_unsub = Fun} = do_del_pubsub(Type, Data, Table), 538 | try 539 | Fun(Data, N) 540 | catch 541 | _T:_R -> 542 | ?ERROR2("unsubscribe callback ~p:~p", [_T, _R]) 543 | end, 544 | State. 545 | 546 | %%--------------- 547 | %% Eunit test 548 | %%--------------- 549 | -ifdef(TEST). 550 | 551 | to_name_test() -> 552 | ?assertEqual('redis_client_localhost_233', to_name(localhost, 233, true)), 553 | ?assertEqual('redis_client_localhost_233', to_name(localhost, 233, false)), 554 | ?assertEqual('redis_client_localhost_233', to_name('localhost', 233, true)), 555 | ?assertEqual('redis_client_localhost_233', to_name("localhost", 233, true)), 556 | 557 | ?assertEqual('redis_client_localhost_233_1', to_name(localhost, 233, 1, true)), 558 | ?assertEqual('redis_client_localhost_233_1', to_name(localhost, 233, 1, false)), 559 | ?assertEqual('redis_client_localhost_233_1', to_name('localhost', 233, "1", true)), 560 | ?assertEqual('redis_client_localhost_233_1', to_name("localhost", 233, '1', true)), 561 | ?assertError(function_clause, to_name("localhost", 233, <<1>>, true)), 562 | 563 | ok. 564 | 565 | -endif. 566 | -------------------------------------------------------------------------------- /src/redis_conn_sup.erl: -------------------------------------------------------------------------------- 1 | %%%---------------------------------------------------------------------- 2 | %%% 3 | %%% @copyright erl-redis 2010 4 | %%% 5 | %%% @author litao cheng 6 | %%% @doc redis client connection supervisor 7 | %%% @end 8 | %%% 9 | %%%---------------------------------------------------------------------- 10 | -module(redis_conn_sup). 11 | -author('litaocheng@gmail.com'). 12 | -vsn('0.1'). 13 | -include("redis_internal.hrl"). 14 | 15 | -behaviour(supervisor). 16 | 17 | -export([start_link/0]). 18 | -export([connect/3, connect/4]). 19 | 20 | %% some auxiliary functions 21 | -export([sup_spec/0 22 | , sup_start_client/3, sup_start_client/4 23 | , sup_rand_client/0, sup_rand_client/2, sup_rand_client/3]). 24 | 25 | %% supervisor callback 26 | -export([init/1]). 27 | 28 | %% @doc the application start callback 29 | -spec start_link() -> 30 | {'ok', pid()} | 'ignore' | {'error', any()}. 31 | start_link() -> 32 | ?INFO2("start the supervisor", []), 33 | supervisor:start_link({local, ?CONN_SUP}, ?MODULE, []). 34 | 35 | %% @doc start an new redis_client process connect the specify redis server 36 | connect(Host, Port, Passwd) -> 37 | supervisor:start_child(?CONN_SUP, [Host, Port, Passwd]). 38 | 39 | %% @doc start an new redis_client process connect the specify redis server 40 | connect(Host, Port, Passwd, Name) -> 41 | supervisor:start_child(?CONN_SUP, [Host, Port, Passwd, Name]). 42 | 43 | %% @doc return the specification as the child of other supervisor 44 | sup_spec() -> 45 | {redis_client_sup, {redis_conn_sup, start_link, []}, 46 | permanent, 1000, supervisor, [redis_client]}. 47 | 48 | 49 | %% @doc start client to Host:Port with a connection pool, in the ?CONN_SUP 50 | sup_start_client(Host, Port, Pass) -> 51 | sup_start_client(Host, Port, Pass, ?CONN_POOL_DEF). 52 | 53 | sup_start_client(Host, Port, Pass, Pool) when is_integer(Pool) -> 54 | [begin 55 | Name = redis_client:name(Host, Port, I), 56 | {ok, _} = connect(Host, Port, Pass, Name) 57 | end || I <- lists:seq(1, Pool)]. 58 | 59 | %% @doc random select one connection form the ?CONN_SUP 60 | sup_rand_client() -> 61 | Children = supervisor:which_children(?CONN_SUP), 62 | Len = length(Children), 63 | {_Id, Child, worker, _Modules} = lists:nth(random:uniform(Len), Children), 64 | redis_client:handler(Child). 65 | 66 | %% @doc random select one connection with Host:Port from the connection pool 67 | %% return the parameterized redis module 68 | sup_rand_client(Host, Port) -> 69 | sup_rand_client(Host, Port, ?CONN_POOL_DEF). 70 | 71 | sup_rand_client(Host, Port, Pool) -> 72 | Selected = redis_client:existing_name(Host, Port, random:uniform(Pool)), 73 | redis_client:handler(Selected). 74 | 75 | %% @doc the connection supervisor callback 76 | init([]) -> 77 | ?DEBUG2("init supervisor", []), 78 | Stragegy = {simple_one_for_one, 100000, 60}, 79 | Client = {undefined, {redis_client, start_link, []}, 80 | permanent, 1000, worker, [redis_client]}, 81 | 82 | {ok, {Stragegy, [Client]}}. 83 | 84 | %% 85 | %% internal API 86 | %% 87 | -------------------------------------------------------------------------------- /src/redis_proto.erl: -------------------------------------------------------------------------------- 1 | %%%---------------------------------------------------------------------- 2 | %%% 3 | %%% @copyright erl-redis 2010 4 | %%% 5 | %%% @author litaocheng@gmail.com 6 | %%% @doc the redis protocol module 7 | %%% @end 8 | %%% 9 | %%%---------------------------------------------------------------------- 10 | -module(redis_proto). 11 | -author('ltiaocheng@gmail.com'). 12 | -vsn('0.1'). 13 | -include("redis_internal.hrl"). 14 | 15 | -export([mbulk/1, mbulk/2, mbulk/3, mbulk/4, mbulk/5, mbulk_list/1]). 16 | 17 | -export([parse_reply/1]). 18 | -export([tokens/2]). 19 | 20 | -compile([opt_bin_info]). 21 | -compile({inline, 22 | [mbulk/1, mbulk/2, mbulk/3, mbulk/4, mbulk_list/1, parse_reply/1]}). 23 | 24 | %% @doc generate the mbulk command 25 | -spec mbulk(iolist()) -> iolist(). 26 | mbulk(Type) -> 27 | [<<"*1">>, ?CRLF, mbulk0(Type)]. 28 | 29 | -spec mbulk(iolist(), iolist()) -> 30 | iolist(). 31 | mbulk(Type, Arg) -> 32 | [<<"*2">>, ?CRLF, mbulk0(Type), mbulk0(Arg)]. 33 | 34 | -spec mbulk(iolist(), iolist(), iolist()) -> 35 | iolist(). 36 | mbulk(Type, Arg1, Arg2) -> 37 | [<<"*3">>, ?CRLF, mbulk0(Type), mbulk0(Arg1), mbulk0(Arg2)]. 38 | 39 | -spec mbulk(iolist(), iolist(), iolist(), iolist()) -> 40 | iolist(). 41 | mbulk(Type, Arg1, Arg2, Arg3) -> 42 | [<<"*4">>, ?CRLF, mbulk0(Type), mbulk0(Arg1), mbulk0(Arg2), mbulk0(Arg3)]. 43 | 44 | -spec mbulk(iolist(), iolist(), iolist(), iolist(), iolist()) -> 45 | iolist(). 46 | mbulk(Type, Arg1, Arg2, Arg3, Arg4) -> 47 | [<<"*5">>, ?CRLF, mbulk0(Type), mbulk0(Arg1), mbulk0(Arg2), mbulk0(Arg3), mbulk0(Arg4)]. 48 | 49 | -spec mbulk_list(L :: [iolist()]) -> 50 | iolist(). 51 | mbulk_list(L) -> 52 | N = length(L), 53 | Lines = [mbulk0(E) || E <- L], 54 | [<<"*">>, ?N2S(N), ?CRLF, Lines]. 55 | 56 | %% @doc parse the reply 57 | -spec parse_reply(Bin :: binary()) -> 58 | any(). 59 | parse_reply(<<"+", Rest/binary>>) -> 60 | parse_status_reply(Rest); 61 | parse_reply(<<"-", Rest/binary>>) -> 62 | parse_error_reply(Rest); 63 | parse_reply(<<":", Rest/binary>>) -> 64 | b2n(Rest); 65 | 66 | parse_reply(<<"$-1\r\n">>) -> 67 | null; 68 | parse_reply(<<"$0\r\n">>) -> 69 | {bulk_more, 0}; 70 | parse_reply(<<"$", Rest/binary>>) -> 71 | N = b2n(Rest), 72 | {bulk_more, N}; 73 | parse_reply(<<"*-1\r\n">>) -> 74 | null; 75 | parse_reply(<<"*0\r\n">>) -> 76 | null; 77 | parse_reply(<<"*", Rest/binary>>) -> 78 | N = b2n(Rest), 79 | {mbulk_more, N}. 80 | 81 | %% @doc return a list of tokens in binary, separated by the character Sep 82 | -spec tokens(S :: binary(), Sep :: char()) -> [binary()]. 83 | tokens(S, Sep) when is_integer(Sep) -> 84 | ?DEBUG2("string is ~p sep is ~p", [S, Sep]), 85 | tokens1(S, Sep, []). 86 | 87 | %% 88 | %%------------------------------------------------------------------------------ 89 | %% 90 | %% internal API 91 | %% 92 | %%------------------------------------------------------------------------------ 93 | 94 | %% generte mbulk command line 95 | mbulk0(N) when is_integer(N) -> 96 | mbulk0(?N2S(N)); 97 | mbulk0(B) when is_binary(B) -> 98 | N = byte_size(B), 99 | ["$", ?N2S(N), ?CRLF, B, ?CRLF]; 100 | mbulk0(L) when is_list(L) -> 101 | N = iolist_size(L), 102 | ["$", ?N2S(N), ?CRLF, L, ?CRLF]. 103 | 104 | %% tokens 105 | tokens1(<>, C, Toks) -> 106 | tokens1(Rest, C, Toks); 107 | tokens1(<>, Sep, Toks) -> 108 | tokens2(Rest, Sep, Toks, <>); 109 | tokens1(<<>>, _Sep, Toks) -> 110 | lists:reverse(Toks). 111 | 112 | tokens2(<>, C, Toks, Bin) -> 113 | tokens1(Rest, C, [Bin | Toks]); 114 | tokens2(<>, Sep, Toks, Bin) -> 115 | tokens2(Rest, Sep, Toks, <>); 116 | tokens2(<<>>, _Sep, Toks, Bin) -> 117 | lists:reverse([Bin | Toks]). 118 | 119 | %% parse status reply 120 | parse_status_reply(<<"OK\r\n">>) -> 121 | ok; 122 | parse_status_reply(<<"QUEUED\r\n">>) -> 123 | queued; 124 | parse_status_reply(<<"PONG\r\n">>) -> 125 | pong; 126 | parse_status_reply(<<"none\r\n">>) -> 127 | none; 128 | parse_status_reply(<<"string\r\n">>) -> 129 | string; 130 | parse_status_reply(<<"list\r\n">>) -> 131 | list; 132 | parse_status_reply(<<"set\r\n">>) -> 133 | set; 134 | parse_status_reply(Status) -> 135 | Len = byte_size(Status) - 2, 136 | <> = Status, 137 | Val. 138 | 139 | %% parse error reply 140 | parse_error_reply(Bin) when is_binary(Bin) -> 141 | L = byte_size(Bin) - 2, 142 | <> = Bin, 143 | {error, Msg}. 144 | 145 | %% binary to integer 146 | b2n(<<"0\r\n">>) -> 147 | 0; 148 | b2n(<<"1\r\n">>) -> 149 | 1; 150 | b2n(<<$-, Rest/binary>>) -> 151 | -b2n(Rest); 152 | b2n(Bin) -> 153 | b2n(Bin, 0). 154 | 155 | b2n(<>, N) when C >= $0, C =< $9 -> 156 | b2n(Rest, N * 10 + (C - $0)); 157 | b2n(<<"\r\n">>, N) -> 158 | N. 159 | 160 | -ifdef(TEST). 161 | 162 | b2n_test_() -> 163 | [ 164 | ?_assertEqual(233, b2n(<<"233\r\n">>)), 165 | ?_assertEqual(0, b2n(<<"0\r\n">>)), 166 | ?_assertEqual(123, b2n(<<"123\r\n">>)), 167 | ?_assertEqual(12432, b2n(<<"12432\r\n">>)) 168 | ]. 169 | 170 | parse_test() -> 171 | ?assertEqual(ok, parse_reply(<<"+OK\r\n">>)), 172 | ?assertEqual(queued, parse_reply(<<"+QUEUED\r\n">>)), 173 | ?assertEqual(pong, parse_reply(<<"+PONG\r\n">>)), 174 | ?assertEqual(<<"OTHER STATUS">>, parse_reply(<<"+OTHER STATUS\r\n">>)), 175 | 176 | ?assertEqual({error, <<"FORMAT">>}, parse_reply(<<"-FORMAT\r\n">>)), 177 | ?assertEqual({error, <<"UNKNOWN">>}, parse_reply(<<"-UNKNOWN\r\n">>)), 178 | 179 | ?assertEqual(0, parse_reply(<<":0\r\n">>)), 180 | ?assertEqual(1, parse_reply(<<":1\r\n">>)), 181 | ?assertEqual(231, parse_reply(<<":231\r\n">>)), 182 | ?assertEqual(987234, parse_reply(<<":987234\r\n">>)), 183 | ?assertEqual(-3, parse_reply(<<":-3\r\n">>)), 184 | 185 | ?assertEqual(null, parse_reply(<<"$-1\r\n">>)), 186 | ?assertEqual({bulk_more, 0}, parse_reply(<<"$0\r\n">>)), 187 | ?assertEqual({bulk_more, 1}, parse_reply(<<"$1\r\n">>)), 188 | ?assertEqual({bulk_more, 10}, parse_reply(<<"$10\r\n">>)), 189 | 190 | ?assertEqual(null, parse_reply(<<"*-1\r\n">>)), 191 | ?assertEqual(null, parse_reply(<<"*0\r\n">>)), 192 | ?assertEqual({mbulk_more, 2}, parse_reply(<<"*2\r\n">>)), 193 | 194 | ?assertEqual({mbulk_more, 1}, parse_reply(<<"*1\r\n">>)), 195 | 196 | ok. 197 | 198 | -endif. 199 | -------------------------------------------------------------------------------- /test/redis-benchmark: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %% -*- erlang -*- 3 | %%! -smp enable +A 16 +K true 4 | -mode(compile). 5 | -compile(inline). 6 | -import(redis_proto, [line/1, line/2, line/3, line/4, line_list/1, 7 | bulk/3, bulk/4, mbulk/1]). 8 | 9 | -define(HOST, localhost). 10 | -define(PORT, 6379). 11 | 12 | -record(config, { 13 | host = ?HOST, 14 | port = ?PORT, 15 | clients = 5, 16 | requests = 10000, 17 | pool = 5 18 | }). 19 | 20 | usage() -> 21 | io:format("usage:\n"), 22 | io:format(" redis-benchmark [options]\n"), 23 | io:format(" -s Server - the redis server\n"), 24 | io:format(" -p Port - the redis server port (default 6379) \n"), 25 | io:format(" -c Clients - the concurrent request client number (default 5) \n"), 26 | io:format(" -n Count - the total requests count (default 10000) \n"), 27 | io:format(" -o Pool - the connection pool size (default 5) \n"), 28 | io:format("\n\n"). 29 | 30 | main([]) -> 31 | usage(), 32 | halt(1); 33 | main(Args) -> 34 | usage(), 35 | Config = parse_args(Args), 36 | %io:format("args is ~p~n config is ~p~n", [Args, Config]), 37 | crypto:start(), 38 | true = code:add_path(filename:join([root_dir(), "ebin"])), 39 | true = erlang:system_info(smp_support), 40 | 41 | Host = Config#config.host, 42 | Port = Config#config.port, 43 | [begin 44 | Name = redis_client:name(Host, Port, I), 45 | {ok, _} = redis_client:start(Host, Port, "", Name) 46 | end || I <- lists:seq(1, Config#config.pool)], 47 | 48 | Requests = Config#config.requests, 49 | Clients = Config#config.clients, 50 | FRedis = 51 | fun() -> 52 | rand_redis(Host, Port, Config#config.pool) 53 | end, 54 | 55 | [begin 56 | do_bench(F, "normal::" ++ Title, Requests, Clients, FRedis, fun start_client_normal/4), 57 | do_bench(F, "pipeline::" ++ Title, Requests, Clients, FRedis, fun start_client_pipeline/4), 58 | io:format("\n") 59 | end || {F, Title} <- action_list()]. 60 | 61 | 62 | rand_redis(Host, Port, N) -> 63 | Selected = redis_client:existing_name(Host, Port, random:uniform(N)), 64 | %io:format("selected redis is ~p~n", [Selected]), 65 | redis_client:handler(Selected). 66 | 67 | do_bench(F, Title, N, Conc, FRedis, ClientFun) -> 68 | NP = N div Conc, 69 | Parent = self(), 70 | 71 | % spawn the clients 72 | Pids = [ClientFun(FRedis(), Parent, F, NP) || _ <- lists:duplicate(Conc, 0)], 73 | T1 = now(), 74 | % start the clients 75 | [Pid ! {Parent, start} || Pid <- Pids], 76 | Counts = 77 | [receive 78 | {ack, P, CN} -> 79 | CN 80 | end || P <- Pids], 81 | T2 = now(), 82 | 83 | T = timer:now_diff(T2, T1), 84 | N2 = lists:sum(Counts), 85 | Second = T/1000000, 86 | io:format("===== ~p ======\n", [Title]), 87 | io:format("~p requests completed in ~.2f s\n", [N2, Second]), 88 | io:format("~p requests per second\n", [round(N2/Second)]), 89 | ok. 90 | 91 | %%----------- 92 | %% clients 93 | %%----------- 94 | 95 | start_client_normal(Redis, Parent, F, NP) -> 96 | spawn( 97 | fun() -> 98 | receive 99 | {Parent, start} -> 100 | ok 101 | end, 102 | do_times(Redis, F, NP), 103 | Parent ! {ack, self(), NP} 104 | end). 105 | 106 | do_times(_Redis, _F, 0) -> 107 | ok; 108 | do_times(Redis, F, N) -> 109 | case is_function(F, 1) of 110 | true -> 111 | F(Redis); 112 | false -> 113 | F(Redis, integer_to_list(N)) 114 | end, 115 | do_times(Redis, F, N-1). 116 | 117 | start_client_pipeline(Redis, Parent, F, NP) -> 118 | spawn( 119 | fun() -> 120 | receive 121 | {Parent, start} -> 122 | ok 123 | end, 124 | FunPipeline = 125 | fun() -> 126 | [begin 127 | case is_function(F, 1) of 128 | true -> 129 | F(Redis); 130 | false -> 131 | F(Redis, integer_to_list(N)) 132 | end 133 | end || N <- lists:seq(1, NP)] 134 | end, 135 | Pipeline = redis_client:pipeline(Redis), 136 | Pipeline:pipeline(FunPipeline), 137 | Parent ! {ack, self(), NP} 138 | end). 139 | 140 | 141 | -define(F(Expr), 142 | (fun(Redis) -> Expr end)). 143 | 144 | action_list() -> 145 | [{?F(Redis:ping()), "PING"}, 146 | {fun(Redis, N) -> Redis:set("foo_rand000000000000", N) end, "SET"}, 147 | {?F(Redis:get("foo_rand000000000000")), "GET"}, 148 | {?F(Redis:incr("counter_rand000000000000")), "INCR"}, 149 | {?F(Redis:lpush("mylist", <<"bar">>)), "LPUSH"}, 150 | {?F(Redis:lpop("mylist")), "LPOP"} 151 | ]. 152 | 153 | parse_args(Args) -> 154 | parse_args(Args, #config{}). 155 | 156 | parse_args(["-s", Server | T], Config) -> 157 | parse_args(T, Config#config{host = Server}); 158 | parse_args(["-p", Port | T], Config) -> 159 | parse_args(T, Config#config{port = list_to_integer(Port)}); 160 | parse_args(["-c", Clients | T], Config) -> 161 | parse_args(T, Config#config{clients = list_to_integer(Clients)}); 162 | parse_args(["-n", N | T], Config) -> 163 | parse_args(T, Config#config{requests = list_to_integer(N)}); 164 | parse_args(["-o", N | T], Config) -> 165 | parse_args(T, Config#config{pool = list_to_integer(N)}); 166 | parse_args([_ | T], Config) -> 167 | parse_args(T, Config); 168 | parse_args([], Config) -> 169 | Config. 170 | 171 | root_dir() -> 172 | {ok, Cwd} = file:get_cwd(), 173 | Path = escript:script_name(), 174 | filename:dirname(filename:dirname(filename:join([Cwd, Path]))). 175 | -------------------------------------------------------------------------------- /test/redis.coverspec: -------------------------------------------------------------------------------- 1 | {level, details}. 2 | {incl_dirs, ["ebin"]}. 3 | -------------------------------------------------------------------------------- /test/redis_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(redis_SUITE). 2 | %% Note: This directive should only be used in test suites. 3 | -compile(export_all). 4 | 5 | -include_lib("common_test/include/ct.hrl"). 6 | -include("redis_internal.hrl"). 7 | -define(PF(F), 8 | fun() -> 9 | V = F, 10 | %?INFO2("~n~80..-s\ncall\t:\t~s~nresult\t:\t~p~n~80..=s~n~n", ["-", ??F, V, "="]), 11 | ct:log(default, "~n~80..-s\ncall\t:\t~s~nresult\t:\t~p~n~80..=s~n~n", ["-", ??F, V, "="]), 12 | V 13 | end()). 14 | -define(N2B(N), list_to_binary(integer_to_list(N))). 15 | 16 | suite() -> [ 17 | {timetrap,{minutes,2}} 18 | ]. 19 | 20 | init_per_suite(Config) -> 21 | crypto:start(), 22 | code:add_path("../ebin"), 23 | {ok, Pid} = redis_client:start(localhost, 6379, ""), 24 | Redis = redis_client:handler(Pid), 25 | ok = Redis:flushall(), 26 | io:format("Redis is ~p~n", [Redis]), 27 | [{redis_client, Redis} | Config]. 28 | 29 | end_per_suite(Config) -> 30 | Redis = ?config(redis_client, Config), 31 | redis_client:stop(Redis), 32 | crypto:stop(), 33 | ok. 34 | 35 | init_per_testcase(Name, Config) -> 36 | io:format("..init ~p~n~p~n", [Name, Config]), 37 | Config. 38 | 39 | end_per_testcase(Name, Config) -> 40 | io:format("...end ~p~n~p~n", [Name, Config]), 41 | ok. 42 | 43 | all() -> 44 | [ 45 | test_string, 46 | test_key, 47 | test_hash, 48 | test_list, 49 | test_set, 50 | test_zset, 51 | test_pipeline, 52 | test_sort, 53 | %test_trans, 54 | %test_persistence, 55 | test_dummy 56 | ]. 57 | 58 | %%------------------------------------------------------------------------- 59 | %% Test cases starts here. 60 | %%------------------------------------------------------------------------- 61 | test_dummy(_Config) -> 62 | ok. 63 | 64 | %% test keys commands 65 | test_string(Config) -> 66 | Redis = ?config(redis_client, Config), 67 | 68 | ?PF(2 = Redis:append("k1", "10")), 69 | 9 = Redis:decr("k1"), 70 | 7 = Redis:decrby("k1", 2), 71 | <<"7">> = Redis:get("k1"), 72 | 8 = Redis:incr("k1"), 73 | 10 = Redis:incrby("k1", 2), 74 | 75 | ok = Redis:set("k1", "hello world"), 76 | <<"world">> = Redis:getrange("k1", 6, -1), 77 | <<"hello world">> = Redis:getrange("k1", 0, -1), 78 | <<"d">> = Redis:getrange("k1", -1, -1), 79 | <<"d">> = Redis:getrange("k1", 10, -1), 80 | 11 = Redis:setrange("k1", 6, "home"), 81 | 13 = Redis:setrange("k1", 6, "home!!!"), 82 | <<"hello home!!!">> = Redis:get("k1"), 83 | ok = Redis:set("k1", "hello world"), 84 | 85 | <<"hello world">> = Redis:getset("k1", "v1"), 86 | <<"v1">> = Redis:get(<<"k1">>), 87 | <<"v1">> = Redis:get(["k", $1]), 88 | 89 | ok = Redis:mset([{"k1", "v1"}, {"k2", "v2"}]), 90 | [<<"v1">>, <<"v2">>] = Redis:mget(["k1", "k2"]), 91 | false = Redis:msetnx([{"k1", "v1"}, {"k3", "v3"}]), 92 | 93 | false = Redis:setnx("k1", "v1"), 94 | ?PF(2 = Redis:strlen("k1")), 95 | Redis:setex("k11", "v11", 10), 96 | 97 | 0 = Redis:setbit("key2", 1, 1), 98 | 1 = Redis:getbit("key2", 1), 99 | 100 | ok = ?PF(Redis:flushdb()), 101 | ok. 102 | 103 | %% test keys 104 | test_key(Config) -> 105 | Redis = ?config(redis_client, Config), 106 | 0 = Redis:del("key_not_exist"), 107 | ok = Redis:set("k1", "v1"), 108 | 1 = Redis:del("k1"), 109 | ok = Redis:set("k1", "v1"), 110 | 1 = Redis:del(["k1", "key_not_exist"]), 111 | 112 | false = Redis:exists("k1"), 113 | ok = Redis:set("k1", "v1"), 114 | true = Redis:exists("k1"), 115 | 116 | true = Redis:expire("k1", 5), 117 | true = Redis:expireat("k1", 132341234), 118 | false = Redis:persist("k1"), 119 | 120 | ok = Redis:set("k1", "v1"), 121 | [] = Redis:keys("k1_not_exist_*"), 122 | [<<"k1">>] = Redis:keys("*"), 123 | [<<"k1">>] = Redis:keys("k*"), 124 | 125 | <<"k1">> = Redis:randomkey(), 126 | ok = Redis:rename("k1", "k2"), 127 | true = Redis:renamenx("k2", "k3"), 128 | ok = Redis:set("k4", "v4"), 129 | false = Redis:renamenx("k3", "k4"), 130 | 131 | string = Redis:type("k3"), 132 | Redis:ttl("k3"), 133 | 134 | ok = ?PF(Redis:flushdb()), 135 | ok. 136 | 137 | 138 | test_hash(Config) -> 139 | Redis = ?config(redis_client, Config), 140 | 141 | 0 = Redis:hdel("k1", "f1"), 142 | false = Redis:hexists("k1", "f1"), 143 | true = Redis:hset("k1", "f1", "v1"), 144 | true = Redis:hexists("k1", "f1"), 145 | <<"v1">> = Redis:hget("k1", "f1"), 146 | 1 = Redis:hdel("k1", "f1"), 147 | 0 = Redis:hdel("k1", ["f1", "f2"]), 148 | 149 | 1 = Redis:hincrby("k1", "f1", 1), 150 | -1 = Redis:hincrby("k1", "f1", -2), 151 | 1 = Redis:hincrby("k1", "f1", 2), 152 | 153 | <<"1">> = Redis:hget("k1", <<"f1">>), 154 | true = Redis:hset("k1", "f2", "v2"), 155 | [{<<"f1">>, <<"1">>}, {<<"f2">>, <<"v2">>}] = 156 | Redis:hgetall("k1"), 157 | [<<"f1">>, <<"f2">>] = Redis:hkeys("k1"), 158 | [<<"1">>, <<"v2">>] = Redis:hvals("k1"), 159 | 2 = Redis:hlen("k1"), 160 | 0 = Redis:hlen("knotexists"), 161 | 162 | [<<"1">>, <<"v2">>] = Redis:hmget("k1", [<<"f1">>, "f2"]), 163 | ok = Redis:hmset("k1", [{"f3", "v3"}, {"f4", "v4"}]), 164 | [<<"v4">>] = Redis:hmget("k1", ["f4"]), 165 | 166 | true = Redis:hsetnx("k1", "f5", "v5"), 167 | false = Redis:hsetnx("k1", "f1", "v1"), 168 | 169 | 2 = Redis:hdel("k1", ["f1", "f2"]), 170 | 171 | ok = Redis:flushdb(), 172 | ok. 173 | 174 | test_list(Config) -> 175 | Redis = ?config(redis_client, Config), 176 | 177 | null = Redis:lindex("k1", 1), 178 | 0 = Redis:linsert("k1", before, "e1", "e2"), 179 | 0 = Redis:llen("k1"), 180 | [] = Redis:lrange("k1", 0, -1), 181 | 1 = Redis:lpush("k1", "e1"), 182 | 2 = Redis:linsert("k1", before, "e1", "e0"), 183 | [<<"e0">>, <<"e1">>] = Redis:lrange("k1", 0, -1), 184 | <<"e0">> = Redis:lpop("k1"), 185 | null = Redis:lpop("knotexist"), 186 | 2 = Redis:linsert("k1", 'after', "e1", "e2"), 187 | [<<"e1">>, <<"e2">>] = Redis:lrange("k1", 0, -1), 188 | [<<"e2">>] = Redis:lrange("k1", -1, -1), 189 | 2 = Redis:llen("k1"), 190 | 191 | 3 = Redis:lpush("k1", "e0"), 192 | 4 = Redis:linsert("k1", 'after', "e2", "e1"), 193 | [<<"e0">>, <<"e1">>, <<"e2">>, <<"e1">>] = Redis:lrange("k1", 0, -1), 194 | 195 | 1 = Redis:lrem("k1", 0, "e2"), 196 | 0 = Redis:lrem("k1", 0, "enotexist"), 197 | 1 = Redis:lrem("k1", -1, "e0"), 198 | [<<"e1">>, <<"e1">>] = Redis:lrange("k1", 0, -1), 199 | 3 = Redis:rpush("k1", "e2"), 200 | <<"e2">> = Redis:rpop("k1"), 201 | 3 = Redis:rpush("k1", ["e0"]), 202 | 1 = Redis:lrem("k1", 1, "e0"), 203 | 4 = Redis:lpush("k1", ["e-1", "e-2"]), 204 | ok = Redis:ltrim("k1", 2, -1), 205 | 206 | [<<"e1">>, <<"e1">>] = Redis:lrange("k1", 0, -1), 207 | 208 | 2 = Redis:lrem("k1", 2, "e1"), 209 | [] = Redis:lrange("k1", 0, -1), 210 | 211 | 0 = Redis:lpushx("k2", "e2"), 212 | % k1 not exists 213 | 0 = Redis:lpushx("k1", "e0"), 214 | 0 = Redis:rpushx("k2", "e2"), 215 | 216 | 3 = Redis:rpush("k1", ["e0", "e1", "e2"]), 217 | [<<"e0">>, <<"e1">>, <<"e2">>] = Redis:lrange("k1", 0, -1), 218 | ok = Redis:lset("k1", 0, "enew0"), 219 | ?PF({error, _} = Redis:lset("k1", 3, "enew2")), 220 | 221 | ok = Redis:ltrim("k1", 0, -1), 222 | 3 = Redis:llen("k1"), 223 | ok = Redis:ltrim("k1", 0, 1), 224 | 2 = Redis:llen("k1"), 225 | 226 | <<"e1">> = Redis:rpoplpush("k1", "dst"), 227 | 1 = Redis:llen("k1"), 228 | 1 = Redis:llen("dst"), 229 | 2 = Redis:rpushx("dst", "e2"), 230 | 231 | [{<<"k1">>, <<"enew0">>}] = Redis:blpop(["k1", "k2"], 1), 232 | [{<<"dst">>, <<"e2">>}] = Redis:brpop(["dst"], 1), 233 | [] = Redis:blpop(["knotexist"], 1), 234 | null = Redis:brpoplpush("knotexist", "k333", 1), 235 | ok = Redis:ltrim("k1", -1, 0), 236 | 0 = Redis:llen("k1"), 237 | 2 = Redis:lpush("k1", ["e1", "e2"]), 238 | <<"e1">> = Redis:brpoplpush("k1", "dst", 1), 239 | 240 | ok = Redis:flushdb(), 241 | ok. 242 | 243 | test_set(Config) -> 244 | Redis = ?config(redis_client, Config), 245 | 246 | 0 = Redis:scard("k1"), 247 | 1 = Redis:sadd("k1", "e1"), 248 | 1 = Redis:scard("k1"), 249 | 250 | <<"e1">> = Redis:spop("k1"), 251 | 1 = Redis:sadd("k1", ["e1"]), 252 | true = Redis:sismember("k1", "e1"), 253 | false = Redis:sismember("k1", "enotexist"), 254 | <<"e1">> = Redis:spop("k1"), 255 | 256 | [] = Redis:smembers("k1"), 257 | 2 = Redis:sadd("k1", ["e0", "e1"]), 258 | 0 = Redis:sadd("k1", ["e0", "e1"]), 259 | 0 = Redis:srem("k1", "enotexist"), 260 | 1 = Redis:srem("k1", ["e0"]), 261 | [<<"e1">>] = Redis:smembers("k1"), 262 | <<"e1">> = Redis:srandmember("k1"), 263 | 264 | %% about diff 265 | 3 = Redis:sadd("k2", ["e0", "s0", "s1"]), 266 | 2 = Redis:sadd("k3", ["e0", "e1"]), 267 | [<<"e1">>] = Redis:sdiff("k1", ["k2"]), 268 | [] = Redis:sdiff("k1", ["k2", "k3"]), 269 | 270 | 1 = Redis:sdiffstore("d1", "k1", ["k2"]), 271 | 272 | 0 = Redis:sdiffstore("d1", "k1", ["k2", "k3"]), 273 | false = Redis:exists("d1"), 274 | Redis:flushdb(), 275 | 276 | %% about inter 277 | 1 = Redis:sadd("k1", ["e1"]), 278 | 3 = Redis:sadd("k2", ["e0", "s0", "s1"]), 279 | 2 = Redis:sadd("k3", ["e0", "e1"]), 280 | 281 | [<<"e0">>] = Redis:sinter(["k2", "k3"]), 282 | [] = Redis:sinter(["k1", "k2"]), 283 | 284 | 1 = Redis:sinterstore("d1", ["k2", "k3"]), 285 | 0 = Redis:sinterstore("d1", ["k1", "k2"]), 286 | Redis:flushdb(), 287 | 288 | %% about union 289 | 1 = Redis:sadd("k1", ["e1"]), 290 | 2 = Redis:sadd("k3", ["e0", "e1"]), 291 | 292 | [<<"e0">>, <<"e1">>] = Redis:sunion(["k1", "k3"]), 293 | 294 | 2 = Redis:sunionstore("d1", ["k1", "k3"]), 295 | Redis:flushdb(), 296 | 297 | %% about move 298 | false = Redis:smove("k1", "k2", "e1"), 299 | 1 = Redis:sadd("k1", ["e1"]), 300 | true = Redis:smove("k1", "k2", "e1"), 301 | 1 = Redis:scard("k2"), 302 | 303 | Redis:flushdb(), 304 | 305 | ok. 306 | 307 | test_zset(Config) -> 308 | Redis = ?config(redis_client, Config), 309 | 310 | 2 = Redis:zadd("k1", [{"v1", 1}, {"v2", 2}]), 311 | 2 = Redis:zcard("k1"), 312 | 1 = Redis:zadd("k1", [{"v3", 3.0}]), 313 | 314 | % zcount 315 | 3 = Redis:zcount("k1", 1, 3), 316 | 3 = Redis:zcount("k1", '-inf', '+inf'), 317 | 2 = Redis:zcount("k1", 1.0, 2.0), 318 | 319 | 0 = Redis:zcount("k1", {open, 1}, {open, 1}), 320 | 0 = Redis:zcount("k1", {open, 1}, {open, 2}), 321 | 1 = Redis:zcount("k1", {closed, 1}, {open, 2}), 322 | 2 = Redis:zcount("k1", {closed, 1}, {closed, 2}), 323 | 3 = Redis:zcount("k1", {closed, 1}, {closed, 3.0}), 324 | 325 | % zincrby 326 | 4 = Redis:zincrby("k1", 1.0, "v3"), 327 | 5.1 = Redis:zincrby("k1", 1.1, "v3"), 328 | 329 | % zrange 330 | [<<"v1">>, <<"v2">>, <<"v3">>] = Redis:zrange("k1", 0, -1), 331 | [{<<"v1">>, 1}, 332 | {<<"v2">>, 2}, 333 | {<<"v3">>, 5.1}] = Redis:zrange("k1", 0, 2, true), 334 | 335 | % zrangebyscore 336 | [<<"v1">>, <<"v2">>] 337 | = Redis:zrangebyscore("k1", 1, 3), 338 | [<<"v1">>, <<"v2">>, <<"v3">>] 339 | = Redis:zrangebyscore("k1", '-inf', '+inf'), 340 | [<<"v1">>, <<"v2">>] 341 | = Redis:zrangebyscore("k1", 1.0, 2.0), 342 | 343 | [] = Redis:zrangebyscore("k1", {open, 1}, {open, 1}), 344 | [] = Redis:zrangebyscore("k1", {open, 1}, {open, 2}), 345 | [{<<"v1">>, 1}] 346 | = Redis:zrangebyscore("k1", {closed, 1}, {open, 2}, true), 347 | [<<"v1">>, <<"v2">>] 348 | = Redis:zrangebyscore("k1", {closed, 1}, {closed, 2}), 349 | [<<"v1">>, <<"v2">>, <<"v3">>] 350 | = Redis:zrangebyscore("k1", {closed, 1}, {closed, 6.0}), 351 | 352 | % zrank 353 | null = Redis:zrank("k1", "vnotexist"), 354 | 0 = Redis:zrank("k1", "v1"), 355 | 1 = Redis:zrank("k1", "v2"), 356 | 2 = Redis:zrank("k1", "v3"), 357 | 358 | % rev (score ordered from high to low) 359 | [<<"v3">>, <<"v2">>] = Redis:zrevrange("k1", 0, 1), 360 | [{<<"v3">>, 5.1}] = Redis:zrevrange("k1", 0, 0, true), 361 | [<<"v2">>, <<"v1">>] 362 | = Redis:zrevrangebyscore("k1", 2, 1), 363 | 0 = Redis:zrevrank("k1", "v3"), 364 | 2 = Redis:zrevrank("k1", "v1"), 365 | 366 | % zrem 367 | 0 = Redis:zrem("k1", "vnotexist"), 368 | 2 = Redis:zrem("k1", ["v1", "v2"]), 369 | [<<"v3">>] = Redis:zrange("k1", 0, -1), 370 | 2 = Redis:zadd("k1", [{"v1", 1}, {"v2", 2}]), 371 | 372 | 1 = Redis:zremrangebyrank("k1", 0, 0), 373 | 2 = Redis:zremrangebyrank("k1", 0, -1), 374 | 0 = Redis:zcard("k1"), 375 | 2 = Redis:zadd("k1", [{"v1", 1}, {"v2", 2}]), 376 | 377 | 0 = Redis:zremrangebyscore("k1", 0, 0.9), 378 | 0 = Redis:zremrangebyscore("k1", 2.1, 2.9), 379 | 1 = Redis:zremrangebyscore("k1", 0, 1), 380 | 1 = Redis:zremrangebyscore("k1", 0, {closed, 2.0}), 381 | 0 = Redis:zcard("k1"), 382 | 383 | % zscore 384 | null = Redis:zscore("k1", "v1"), 385 | 2 = Redis:zadd("k1", [{"v1", 1}, {"v2", 2}]), 386 | 1 = Redis:zscore("k1", "v1"), 387 | 2 = Redis:zscore("k1", "v2"), 388 | 389 | % zinter 390 | Redis:flushdb(), 391 | 2 = Redis:zadd("k1", [{"v1", 1}, {"v2", 2}]), 392 | 2 = Redis:zadd("k2", [{"v1", 10}, {"v20", 20}]), 393 | 1 = Redis:zinterstore("d1", ["k1", "k2"]), 394 | [<<"v1">>] = Redis:zrange("d1", 0, -1), 395 | 1 = Redis:zinterstore("d1", ["k1", "k2"], [3, 4]), 396 | [{<<"v1">>, 43}] = Redis:zrange("d1", 0, -1, true), 397 | 1 = Redis:zinterstore("d1", ["k1", "k2"], [3, 4], max), 398 | [{<<"v1">>, 40}] = Redis:zrange("d1", 0, -1, true), 399 | 400 | % zunion 401 | Redis:flushdb(), 402 | 2 = Redis:zadd("k1", [{"v1", 1}, {"v2", 2}]), 403 | 2 = Redis:zadd("k2", [{"v1", 10}, {"v20", 20}]), 404 | 3 = Redis:zunionstore("d1", ["k1", "k2"]), 405 | [{<<"v2">>,2}, {<<"v1">>, 11}, {<<"v20">>, 20}] 406 | = Redis:zrange("d1", 0, -1, true), 407 | 408 | 2 = Redis:zunionstore("d2", ["k1"]), 409 | [<<"v1">>, <<"v2">>] = Redis:zrange("d2", 0, -1), 410 | 3 = Redis:zunionstore("d2", ["k1", "k2"], [2, 3]), 411 | [{<<"v2">>, 4}, {<<"v1">>, 32}, {<<"v20">>, 60}] 412 | = Redis:zrange("d2", 0, -1, true), 413 | 414 | 3 = Redis:zunionstore("d2", ["k1", "k2"], [2, 3], min), 415 | [{<<"v1">>, 2}, {<<"v2">>, 4}, {<<"v20">>, 60}] 416 | = Redis:zrange("d2", 0, -1, true), 417 | 418 | Redis:flushdb(), 419 | ok. 420 | 421 | test_pipeline(Config) -> 422 | Redis = ?config(redis_client, Config), 423 | Pipeline = redis_client:pipeline(Redis), 424 | N = 5, 425 | L = lists:seq(1, N), 426 | Fun = 427 | fun() -> 428 | [Pipeline:set(K, K*2) || K <- L], 429 | [Pipeline:get(K) || K <- L] 430 | end, 431 | R = Pipeline:pipeline(Fun), 432 | RExpect = lists:duplicate(N, ok) ++ [?N2B(K * 2) || K <- L], 433 | R = RExpect, 434 | ok. 435 | 436 | test_sort(Config) -> 437 | Redis = ?config(redis_client, Config), 438 | SortOpt = #redis_sort{}, 439 | ?PF(Redis:sort("mylist", SortOpt)), 440 | 441 | Redis:ltrim("top_uid", 0, -1), 442 | Redis:lpush("top_uid", "2"), 443 | Redis:rpush("top_uid", "5"), 444 | Redis:lpush("top_uid", "8"), 445 | Redis:rpush("top_uid", "10"), 446 | 447 | Redis:set("age_2", "20"), 448 | Redis:set("age_5", "50"), 449 | Redis:set("age_8", "80"), 450 | Redis:set("age_10", "10"), 451 | 452 | Redis:set("lastlogin_1", "2010-03-20"), 453 | Redis:set("lastlogin_2", "2010-02-20"), 454 | Redis:set("lastlogin_5", "2009-08-21"), 455 | Redis:set("lastlogin_8", "2008-05-22"), 456 | Redis:set("lastlogin_10", "2008-04-11"), 457 | 458 | ?PF(Redis:sort("top_uid", #redis_sort{})), 459 | 460 | ?PF(Redis:sort("top_uid", #redis_sort{ 461 | asc = false, 462 | limit = {0, 2} 463 | })), 464 | 465 | ?PF(Redis:sort("top_uid", #redis_sort{ 466 | asc = false, 467 | alpha = true 468 | })), 469 | 470 | ?PF(Redis:sort("top_uid", #redis_sort{ 471 | by_pat = <<"age_*">> 472 | })), 473 | 474 | ?PF(Redis:sort("top_uid", #redis_sort{ 475 | alpha = true, 476 | get_pat = [<<"lastlogin_*">>] 477 | })), 478 | 479 | ?PF(Redis:sort("top_uid", #redis_sort{ 480 | by_pat = <<"age_*">>, 481 | get_pat = ["#", "age_*", <<"lastlogin_*">>] 482 | })), 483 | 484 | ?PF(Redis:sort("top_uid", #redis_sort{ 485 | by_pat = <<"lastlogin_*">>, 486 | get_pat = ["#", "age_*", <<"lastlogin_*">>] 487 | })), 488 | 489 | ok. 490 | 491 | test_trans(Config) -> 492 | Redis = ?config(redis_client, Config), 493 | ok = ?PF(Redis:trans_begin()), 494 | queued = Redis:hash_get("myhash", "f2"), 495 | queued = Redis:hash_set("myhash", "f2", "v22"), 496 | [<<"v2">>, 0] = ?PF(Redis:trans_commit()), 497 | 498 | ok = Redis:set("k1", "v1"), 499 | ok = ?PF(Redis:trans_begin()), 500 | ?PF(Redis:get("k1")), 501 | queued = Redis:set("k1", "v11"), 502 | queued = Redis:hash_get("myhash", "f2"), 503 | queued = Redis:hash_set("myhash", "f2", "v22"), 504 | ok = ?PF(Redis:trans_abort()), 505 | <<"v1">> = Redis:get("k1"), 506 | 507 | % include watch 508 | ok = Redis:watch(["kw1", "kw2"]), 509 | Redis:trans_begin(), 510 | Redis:trans_commit(), 511 | 512 | ok = Redis:unwatch(), 513 | Redis:trans_begin(), 514 | Redis:trans_abort(), 515 | ok. 516 | 517 | test_persistence(Config) -> 518 | Redis = ?config(redis_client, Config), 519 | ?PF(Redis:save()), 520 | ?PF(Redis:bg_save()), 521 | ?PF(Redis:lastsave_time()), 522 | ?PF(Redis:info()), 523 | ok = ?PF(Redis:slave_off()), 524 | ok = ?PF(Redis:slave_of("localhost", 16379)), 525 | ?PF(Redis:bg_rewrite_aof()), 526 | [{_, _} | _] = ?PF(Redis:config_get("*")), 527 | ok = ?PF(Redis:config_set("save", [{3600, 100}, {60, 10000}])), 528 | ok = ?PF(Redis:config_set("maxmemory", "2000000000")), 529 | ok. 530 | 531 | bool(true) -> ok; 532 | bool(false) -> ok. 533 | 534 | non_neg_int(N) when is_integer(N), N >= 0 -> ok. 535 | 536 | int(N) when is_integer(N) -> ok. 537 | atom(A) when is_atom(A) -> ok. 538 | list(L) when is_list(L) -> ok. 539 | 540 | now_sec() -> 541 | {A, B, C} = now(), 542 | A * 1000000 + B + C div 1000000. 543 | -------------------------------------------------------------------------------- /test/redis_pubsub_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(redis_pubsub_SUITE). 2 | %% Note: This directive should only be used in test suites. 3 | -compile(export_all). 4 | 5 | -include_lib("common_test/include/ct.hrl"). 6 | -include("redis_internal.hrl"). 7 | -define(P(F, D), 8 | ct:log(default, F, D)). 9 | 10 | suite() -> [ 11 | {timetrap,{minutes,2}} 12 | ]. 13 | 14 | init_per_suite(Config) -> 15 | crypto:start(), 16 | code:add_path("../ebin"), 17 | [RedisSub, RedisPub, RedisPSub, RedisPPub] = 18 | [begin 19 | {ok, Pid} = redis_client:start(localhost, 6379, "") , 20 | redis_client:handler(Pid) 21 | end || _ <- lists:seq(1, 4)], 22 | ok = RedisSub:flushall(), 23 | 24 | [{redis_sub, RedisSub}, 25 | {redis_pub, RedisPub}, 26 | {redis_psub, RedisPSub}, 27 | {redis_ppub, RedisPPub} | Config]. 28 | 29 | end_per_suite(Config) -> 30 | RedisSub = ?config(redis_sub, Config), 31 | redis_client:stop(RedisSub), 32 | RedisPub = ?config(redis_pub, Config), 33 | redis_client:stop(RedisPub), 34 | crypto:stop(), 35 | ok. 36 | 37 | init_per_testcase(Name, Config) -> 38 | io:format("..init ~p~n~p~n", [Name, Config]), 39 | Config. 40 | 41 | end_per_testcase(Name, Config) -> 42 | io:format("...end ~p~n~p~n", [Name, Config]), 43 | ok. 44 | 45 | all() -> 46 | [ 47 | test_channel, 48 | test_pattern 49 | ]. 50 | 51 | %%------------------------------------------------------------------------- 52 | %% Test cases sthreets here. 53 | %%------------------------------------------------------------------------- 54 | 55 | %% test channel pub/sub 56 | test_channel(Config) -> 57 | RedisSub = ?config(redis_sub, Config), 58 | RedisPub = ?config(redis_pub, Config), 59 | 60 | ok = RedisSub:subscribe([<<"one">>, <<"two">>], 61 | fun cb_channel_sub/2, 62 | fun cb_msg/2), 63 | 64 | badmodel = (catch RedisSub:get("k1")), 65 | 66 | ok = RedisSub:subscribe([<<"three">>], 67 | fun cb_channel_sub/2, 68 | fun cb_msg/2), 69 | 70 | ok = RedisSub:unsubscribe([<<"one">>], 71 | fun cb_channel_unsub/2), 72 | ok = RedisSub:unsubscribe(fun cb_channel_unsub/2), 73 | 74 | 0 = RedisPub:publish(<<"one">>, "no_sub"), 75 | RedisSub:subscribe([<<"one">>], 76 | fun cb_channel_sub/2, 77 | fun cb_msg/2), 78 | erlang:put('one-msg', true), 79 | 1 = RedisPub:publish(<<"one">>, "erase"), 80 | 1 = RedisPub:publish(<<"one">>, "get"), 81 | ok = RedisSub:unsubscribe(fun cb_channel_unsub/2), 82 | ok. 83 | 84 | %% subscribe callbacks 85 | cb_channel_sub(<<"one">>, 1) -> 86 | ?P("sub callback with channel one", []), 87 | ok; 88 | cb_channel_sub(<<"two">>, 2) -> 89 | ?P("sub callback with channel two", []), 90 | ok; 91 | cb_channel_sub(<<"three">>, 3) -> 92 | ?P("sub callback with channel three", []), 93 | ok. 94 | 95 | cb_channel_unsub(<<"one">>, _) -> 96 | ok; 97 | cb_channel_unsub(<<"two">>, _) -> 98 | ok; 99 | cb_channel_unsub(<<"three">>, _) -> 100 | ok. 101 | 102 | cb_msg(<<"one">>, <<"erase">>) -> 103 | erlang:erase('one-msg'), 104 | ok; 105 | cb_msg(<<"one">>, <<"get">>) -> 106 | undefined = erlang:get('one-msg'), 107 | ok; 108 | cb_msg(<<"two">>, <<"two-msg">>) -> 109 | ?P("msg callback with channel two", []), 110 | ok; 111 | cb_msg(<<"three">>, <<"three-msg">>) -> 112 | ?P("msg callback with channel three", []), 113 | ok. 114 | 115 | %%----------------- 116 | %% pattern pub/sub 117 | %%----------------- 118 | test_pattern(Config) -> 119 | RedisSub = ?config(redis_psub, Config), 120 | RedisPub = ?config(redis_ppub, Config), 121 | 122 | RedisSub:psubscribe("news.*", 123 | fun(<<"news.*">>, 1) -> 124 | "subscribe pattern ok" 125 | end, 126 | fun cb_pmessage1/3), 127 | RedisSub:psubscribe("news.china.*", 128 | fun(<<"news.china.*">>, 2) -> 129 | "subscribe pattern ok" 130 | end, 131 | fun cb_pmessage2/3), 132 | RedisSub:subscribe("news.china.edu", 133 | fun(<<"news.china.edu">>, 3) -> 134 | "subscribe channel ok" 135 | end, 136 | fun(<<"news.china.edu">>, <<"news 1">>) -> 137 | ok 138 | end), 139 | badmodel = (catch RedisSub:publish("news", "news")), 140 | 3 = RedisPub:publish("news.china.edu", "news 1"), 141 | 2 = RedisPub:publish("news.china.food", "news 2"), 142 | 1 = RedisPub:publish("news.china", "news 3"), 143 | 0 = RedisPub:publish("other_topic", "news 4"), 144 | 145 | % unsubscribe 146 | RedisSub:punsubscribe("news.china.*", 147 | fun(<<"news.china.*">>, 2) -> 148 | "unsub news.china.*" 149 | end), 150 | RedisSub:punsubscribe(fun cb_punsub/2), 151 | ok. 152 | 153 | cb_pmessage1(<<"news.*">>, <<"news.china.edu">>, <<"news 1">>) -> 154 | ok; 155 | cb_pmessage1(<<"news.*">>, <<"news.china.food">>, <<"news 2">>) -> 156 | ok; 157 | cb_pmessage1(<<"news.*">>, <<"news.china">>, <<"news 3">>) -> 158 | ok. 159 | 160 | cb_pmessage2(<<"news.china.*">>, <<"news.china.edu">>, <<"news 1">>) -> 161 | ok; 162 | cb_pmessage2(<<"news.china.*">>, <<"news.china.food">>, <<"news 2">>) -> 163 | ok. 164 | 165 | cb_punsub(<<"news.*">>, _) -> 166 | ok; 167 | cb_punsub(<<"news.china.edu">>, _) -> 168 | ok. 169 | -------------------------------------------------------------------------------- /test/test.spec: -------------------------------------------------------------------------------- 1 | {suites, ".", [redis_SUITE, redis_pubsub_SUITE]}. 2 | -------------------------------------------------------------------------------- /vsn.mk: -------------------------------------------------------------------------------- 1 | APP_NAME=redis 2 | APP_VSN=1.1.4 3 | --------------------------------------------------------------------------------