├── db ├── readme └── test │ └── README ├── include ├── edis_bench.hrl └── edis.hrl ├── .gitignore ├── leveldb.config ├── pd.config ├── joachimpd.config ├── hanoidb.config ├── src ├── edis.app.src ├── edis_node_sup.erl ├── edis_client_mgr.erl ├── edis_pubsub_sup.erl ├── edis_gb_trees.erl ├── edis_listener_sup.erl ├── edis_sup.erl ├── edis_db_sup.erl ├── edis_backend.erl ├── edis.erl ├── backends │ ├── edis_pd_backend.erl │ ├── edis_eleveldb_backend.erl │ └── edis_hanoidb_backend.erl ├── edis_client_sup.erl ├── edis_config.erl ├── edis_db_monitor.erl ├── edis_pubsub.erl ├── edis_node_monitor.erl ├── edis_lists.erl ├── edis_listener.erl ├── edis_util.erl ├── edis_client.erl └── edis_vclock.erl ├── test ├── test.config ├── test-hanoidb.config ├── connection_SUITE.erl ├── benchmarks │ ├── pubsub_bench.erl │ ├── hashes_bench.erl │ ├── erldis_hashes_bench.erl │ ├── strings_bench.erl │ ├── erldis_strings_bench.erl │ ├── sets_bench.erl │ ├── erldis_lists_bench.erl │ ├── erldis_sets_bench.erl │ ├── lists_bench.erl │ └── erldis_keys_bench.erl ├── keys_SUITE.erl └── edis_bench.erl ├── Emakefile ├── rebar.config ├── priv ├── edis.init.d ├── edis.config └── script_builder ├── Makefile ├── README.md └── LICENSE /db/readme: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /db/test/README: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /include/edis_bench.hrl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbd/edis/HEAD/include/edis_bench.hrl -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc 2 | logs/ct/index.html 3 | db 4 | bin 5 | ebin 6 | deps 7 | erl_crash.dump 8 | logs/ 9 | log/ 10 | *~ 11 | .hosts.erlang -------------------------------------------------------------------------------- /leveldb.config: -------------------------------------------------------------------------------- 1 | [{edis, [{listener_port_range, {6380, 6380}}, {dir, "db"}]}, 2 | {lager, [ 3 | {handlers, 4 | [ 5 | {lager_console_backend, [debug,true]} 6 | ]} 7 | ]}, 8 | {sasl, [{errlog_type, all}, 9 | {sasl_error_logger, {file, "logs/leveldb.edis.sasl.log"}}]}]. -------------------------------------------------------------------------------- /pd.config: -------------------------------------------------------------------------------- 1 | [{edis, [{listener_port_range, {6384, 6385}}, {backend, {edis_pd_backend, []}}]}, 2 | {lager, [ 3 | {handlers, 4 | [ 5 | {lager_console_backend, [debug,true]} 6 | ]} 7 | ]}, 8 | {sasl, [{errlog_type, all}, 9 | {sasl_error_logger, {file, "logs/pd.edis.sasl.log"}}]}]. 10 | -------------------------------------------------------------------------------- /joachimpd.config: -------------------------------------------------------------------------------- 1 | [{edis, [{listener_port_range, {6386, 6387}}, {backend, {edis_pd_backend, []}}]}, 2 | {lager, [ 3 | {handlers, 4 | [ 5 | {lager_console_backend, [debug,true]} 6 | ]} 7 | ]}, 8 | {sasl, [{errlog_type, all}, 9 | {sasl_error_logger, {file, "logs/pd.edis.sasl.log"}}]}]. 10 | -------------------------------------------------------------------------------- /hanoidb.config: -------------------------------------------------------------------------------- 1 | [{edis, [{listener_port_range, {6381, 6381}}, {backend, {edis_hanoidb_backend, [ 2 | {sync, {seconds, 10}}, 3 | {compress, none}, 4 | {block_size, 32768}] }}]}, 5 | {lager, [ 6 | {handlers, 7 | [ 8 | {lager_console_backend, [debug,true]} 9 | ]} 10 | ]}, 11 | {sasl, [{errlog_type, all}, 12 | {sasl_error_logger, {file, "logs/hanoidb.edis.sasl.log"}}]}]. 13 | -------------------------------------------------------------------------------- /src/edis.app.src: -------------------------------------------------------------------------------- 1 | {application, edis, 2 | [ 3 | {description, "An Erlang implementation of the Redis KV Store"}, 4 | {vsn, "0.2.0"}, 5 | {registered, []}, 6 | {applications, [ 7 | kernel, 8 | stdlib, 9 | crypto 10 | ]}, 11 | {mod, {edis, []}}, 12 | {env, [{listener_port_range, {6379,6379}}, 13 | {client_timeout, 32000}, 14 | {databases, 16}, 15 | {requirepass, undefined}]} 16 | ]}. 17 | -------------------------------------------------------------------------------- /test/test.config: -------------------------------------------------------------------------------- 1 | %% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- 2 | %% ex: ts=4 sw=4 ft=erlang et 3 | 4 | [{edis, [{listener_port_range, {16380, 16380}}, 5 | {dir, "db/test"}, 6 | {backend, {edis_pd_backend, []}}]}, 7 | {lager, [ 8 | {handlers, 9 | [ 10 | {lager_console_backend, [debug,true]} 11 | ]} 12 | ]}, 13 | {sasl, [{errlog_type, all}, 14 | {sasl_error_logger, {file, "logs/test-host.sasl.log"}}]}]. 15 | -------------------------------------------------------------------------------- /test/test-hanoidb.config: -------------------------------------------------------------------------------- 1 | %% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- 2 | %% ex: ts=4 sw=4 ft=erlang et 3 | 4 | [{edis, [{listener_port_range, {16380, 16380}}, 5 | {dir, "db/test"}, 6 | {backend, {edis_hanoidb_backend, [ 7 | {sync, {seconds, 10}}, 8 | {compress, none}, 9 | {block_size, 32768}] }}]}, 10 | {lager, [ 11 | {handlers, 12 | [ 13 | {lager_console_backend, [debug,true]} 14 | ]} 15 | ]}, 16 | {sasl, [{errlog_type, all}, 17 | {sasl_error_logger, {file, "logs/test-host.sasl.log"}}]}]. 18 | -------------------------------------------------------------------------------- /src/edis_node_sup.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Joachim Nilsson 3 | %%% @copyright (C) 2011 InakaLabs SRL 4 | %%% @doc Edis Node supervisor 5 | %%% @end 6 | %%%------------------------------------------------------------------- 7 | -module(edis_node_sup). 8 | -author('Joachim Nilsson '). 9 | 10 | -include("edis.hrl"). 11 | 12 | -behaviour(supervisor). 13 | 14 | -export([start_link/0, reload/0, init/1]). 15 | 16 | %% ==================================================================== 17 | %% External functions 18 | %% ==================================================================== 19 | %% @doc Starts the supervisor process 20 | -spec start_link() -> ignore | {error, term()} | {ok, pid()}. 21 | start_link() -> 22 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 23 | 24 | %% @doc Reloads configuration. Restarts the managers 25 | -spec reload() -> ok. 26 | reload() -> 27 | true = exit(erlang:whereis(?MODULE), kill), 28 | ok. 29 | 30 | %% ==================================================================== 31 | %% Server functions 32 | %% ==================================================================== 33 | %% @hidden 34 | -spec init([]) -> {ok, {{one_for_one, 5, 10}, [supervisor:child_spec()]}}. 35 | init([]) -> 36 | lager:info("Node supervisor initialized~n", []), 37 | Mgr = {edis_node_monitor, {edis_node_monitor, start_link, []}, permanent, brutal_kill, worker, [edis_node_monitor]}, 38 | {ok, {{one_for_one, 5, 1}, [Mgr]}}. -------------------------------------------------------------------------------- /Emakefile: -------------------------------------------------------------------------------- 1 | {"src/*", [{parse_transform, lager_transform}, warn_unused_vars, warn_export_all, warn_shadow_vars, warn_unused_import, warn_unused_function, warn_bif_clash, warn_unused_record, warn_deprecated_function, warn_obsolete_guard, strict_validation, report, warn_export_vars, warn_exported_vars, warn_missing_spec, warn_untyped_record, debug_info, {outdir, "ebin"}, {i, "include"}, {i, "deps/lager/include"}]}. 2 | {"src/*/*", [{parse_transform, lager_transform}, warn_unused_vars, warn_export_all, warn_shadow_vars, warn_unused_import, warn_unused_function, warn_bif_clash, warn_unused_record, warn_deprecated_function, warn_obsolete_guard, strict_validation, report, warn_export_vars, warn_exported_vars, warn_missing_spec, warn_untyped_record, debug_info, {outdir, "ebin"}, {i, "include"}, {i, "deps/lager/include"}]}. 3 | {"test/*", [{parse_transform, lager_transform}, warn_unused_vars, warn_shadow_vars, warn_unused_import, warn_unused_function, warn_bif_clash, warn_unused_record, warn_deprecated_function, warn_obsolete_guard, strict_validation, report, warn_export_vars, warn_exported_vars, warn_missing_spec, warn_untyped_record, debug_info, {outdir, "ebin"}, {i, "include"}, {i, "deps/lager/include"}]}. 4 | {"test/*/*", [{parse_transform, lager_transform}, arn_unused_vars, warn_export_all, warn_shadow_vars, warn_unused_import, warn_unused_function, warn_bif_clash, warn_unused_record, warn_deprecated_function, warn_obsolete_guard, strict_validation, report, warn_export_vars, warn_exported_vars, warn_missing_spec, warn_untyped_record, debug_info, {outdir, "ebin"}, {i, "include"}, {i, "deps/lager/include"}]}. -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {deps, [{lager, "2.0.*", {git, "git://github.com/basho/lager.git", "master"}}, 2 | {eper, "0.*", {git, "git://github.com/massemanet/eper.git", "master"}}, 3 | {erldis, "\.*", {git, "git://github.com/japerk/erldis.git", "master"}}, 4 | {eleveldb, "\.*", {git, "git://github.com/basho/eleveldb.git", "master"}}, 5 | {hanoidb, "\.*", {git, "git://github.com/basho-labs/hanoidb.git", "master"}}, 6 | {pmod_transform, ".*", {git,"git://github.com/erlang/pmod_transform.git", "master"}}]}. 7 | {require_otp_vsn, "R1[456]"}. 8 | {erl_first_files, ["src/edis_backend.erl", "test/edis_bench.erl"]}. 9 | {erl_opts, [{parse_transform, lager_transform}, 10 | {src_dirs, ["src", "src/backends", "tests", "test/benchmarks"]}, 11 | %{i, "deps/lager/include"}, 12 | warn_unused_vars, 13 | warn_export_all, 14 | warn_shadow_vars, 15 | warn_unused_import, 16 | warn_unused_function, 17 | warn_bif_clash, 18 | warn_unused_record, 19 | warn_deprecated_function, 20 | warn_obsolete_guard, 21 | strict_validation, 22 | warn_export_vars, 23 | warn_exported_vars, 24 | warn_missing_spec, 25 | warn_untyped_record, debug_info]}. 26 | {xref_checks, [undefined_function_calls]}. 27 | {ct_extra_params," -dir ./test/ebin -logdir logs/ct"}. 28 | {edoc_opts, [{report_missing_types, true}, {source_path, ["src"]}, {report_missing_types, true}, {todo, true}, {packages, false}, {subpackages, false}]}. 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/edis_client_mgr.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc Edis client manager 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(edis_client_mgr). 9 | -author('Fernando Benavides '). 10 | -author('Chad DePue '). 11 | 12 | -include("edis.hrl"). 13 | 14 | -behaviour(supervisor). 15 | 16 | -export([start_link/1, start_client/0, init/1]). 17 | 18 | %% ==================================================================== 19 | %% External functions 20 | %% ==================================================================== 21 | %% @doc Starts the supervisor process 22 | -spec start_link(atom()) -> ignore | {error, term()} | {ok, pid()}. 23 | start_link(Name) -> 24 | supervisor:start_link({local, Name}, ?MODULE, []). 25 | 26 | %% @doc Starts a new client process 27 | -spec start_client() -> {ok, pid() | undefined} | {error, term()}. 28 | start_client() -> 29 | supervisor:start_child(?MODULE, []). 30 | 31 | %% ==================================================================== 32 | %% Server functions 33 | %% ==================================================================== 34 | %% @hidden 35 | -spec init([]) -> {ok, {{simple_one_for_one, 100, 1}, [supervisor:child_spec()]}}. 36 | init([]) -> 37 | {ok, {{simple_one_for_one, 100, 1}, 38 | [{edis_client, {edis_client, start_link, []}, 39 | temporary, brutal_kill, worker, [edis_client]}]}}. -------------------------------------------------------------------------------- /src/edis_pubsub_sup.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc Edis Pub/Sub supervisor 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(edis_pubsub_sup). 9 | -author('Fernando Benavides '). 10 | -author('Chad DePue '). 11 | 12 | -include("edis.hrl"). 13 | 14 | -behaviour(supervisor). 15 | 16 | -export([start_link/0, reload/0, init/1]). 17 | 18 | %% ==================================================================== 19 | %% External functions 20 | %% ==================================================================== 21 | %% @doc Starts the supervisor process 22 | -spec start_link() -> ignore | {error, term()} | {ok, pid()}. 23 | start_link() -> 24 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 25 | 26 | %% @doc Reloads configuration. Restarts the managers 27 | -spec reload() -> ok. 28 | reload() -> 29 | true = exit(erlang:whereis(?MODULE), kill), 30 | ok. 31 | 32 | %% ==================================================================== 33 | %% Server functions 34 | %% ==================================================================== 35 | %% @hidden 36 | -spec init([]) -> {ok, {{one_for_one, 5, 10}, [supervisor:child_spec()]}}. 37 | init([]) -> 38 | lager:info("Pub/Sub supervisor initialized~n", []), 39 | Mgr = {edis_pubsub, {edis_pubsub, start_link, []}, permanent, brutal_kill, worker, [edis_pubsub]}, 40 | {ok, {{one_for_one, 5, 1}, [Mgr]}}. -------------------------------------------------------------------------------- /src/edis_gb_trees.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc Extension for gb_trees with a couple of extra functions 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(edis_gb_trees). 9 | -extends(gb_trees). 10 | -author('Fernando Benavides '). 11 | -author('Chad DePue '). 12 | 13 | -compile({parse_transform,pmod_pt}). 14 | 15 | -export([rev_iterator/1, previous/1]). 16 | 17 | -type edis_gb_tree() :: gb_tree(). 18 | 19 | %% @doc Returns a reverse iterator. It's just like the one returned by {@link iterator/1} but it 20 | %% traverses the tree in the exact opposite direction 21 | -spec rev_iterator(edis_gb_tree()) -> [gb_trees:gb_tree_node()]. 22 | rev_iterator({_, T}) -> 23 | rev_iterator_1(T). 24 | 25 | rev_iterator_1(T) -> 26 | rev_iterator(T, []). 27 | 28 | %% The rev_iterator structure is really just a list corresponding to 29 | %% the call stack of an post-order traversal. This is quite fast. 30 | 31 | rev_iterator({_, _, _, nil} = T, As) -> 32 | [T | As]; 33 | rev_iterator({_, _, _, R} = T, As) -> 34 | rev_iterator(R, [T | As]); 35 | rev_iterator(nil, As) -> 36 | As. 37 | 38 | %% @doc Like {@link next/1} for reverse iterators 39 | -spec previous([gb_trees:gb_tree_node()]) -> 'none' | {term(), term(), [gb_trees:gb_tree_node()]}. 40 | previous([{X, V, T, _} | As]) -> 41 | {X, V, rev_iterator(T, As)}; 42 | previous([]) -> 43 | none. 44 | -------------------------------------------------------------------------------- /priv/edis.init.d: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | PREFIX=/usr/local 3 | 4 | if [[ $EUID -ne 0 ]]; then 5 | echo "This script must be run as sudo or root" 1>&2 6 | exit 1 7 | fi 8 | 9 | function get_port { 10 | PORT=`epmd -names | grep edis | cut -f 5 -d' '` 11 | } 12 | 13 | function stop { 14 | get_port 15 | if [ -z $PORT ]; then 16 | echo "edis is not running." 17 | else 18 | echo -n "Stopping edis..." 19 | PID=`ps aux | grep beam | grep "/edis " | awk '{print $2}'` 20 | kill $PID 21 | sleep 1 22 | echo ".. Done (it was running on $PID)." 23 | fi 24 | } 25 | 26 | function start { 27 | get_port 28 | if [ -z $PORT ]; then 29 | echo "Starting edis..." 30 | ulimit -n 99999 31 | export HOME=/tmp 32 | if [ -f /etc/edis/edis.config ]; then 33 | $PREFIX/bin/edis /etc/edis/edis.config ; 34 | echo $PREFIX 35 | else 36 | $PREFIX/bin/edis 37 | fi 38 | sleep 1 39 | get_port 40 | echo "Done. PORT=$PORT" 41 | else 42 | echo "edis is already running, PORT=$PORT" 43 | fi 44 | } 45 | 46 | function restart { 47 | echo "Restarting edis..." 48 | stop 49 | start 50 | } 51 | 52 | function status { 53 | get_port 54 | if [ -z $PORT ]; then 55 | echo "edis is not running." 56 | exit 1 57 | else 58 | echo "edis is running, PORT=$PORT" 59 | fi 60 | } 61 | 62 | case "$1" in 63 | start) 64 | start 65 | ;; 66 | stop) 67 | stop 68 | ;; 69 | restart) 70 | restart 71 | ;; 72 | status) 73 | status 74 | ;; 75 | *) 76 | echo "Usage: $0 {start|stop|restart|status}" 77 | esac 78 | -------------------------------------------------------------------------------- /src/edis_listener_sup.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc Edis TCP listener supervisor 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(edis_listener_sup). 9 | -author('Fernando Benavides '). 10 | -author('Chad DePue '). 11 | 12 | -include("edis.hrl"). 13 | 14 | -behaviour(supervisor). 15 | 16 | -export([start_link/0, init/1, reload/0]). 17 | 18 | %% ==================================================================== 19 | %% External functions 20 | %% ==================================================================== 21 | %% @doc Starts the supervisor process 22 | -spec start_link() -> ignore | {error, term()} | {ok, pid()}. 23 | start_link() -> 24 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 25 | 26 | %% @doc Reloads configuration. Restarts the listeners 27 | -spec reload() -> ok. 28 | reload() -> 29 | true = exit(erlang:whereis(?MODULE), kill), 30 | ok. 31 | 32 | %% ==================================================================== 33 | %% Server functions 34 | %% ==================================================================== 35 | %% @hidden 36 | -spec init([]) -> {ok, {{one_for_one, 5, 10}, [supervisor:child_spec()]}}. 37 | init([]) -> 38 | lager:info("Listener supervisor initialized~n", []), 39 | {MinPort, MaxPort} = edis_config:get(listener_port_range), 40 | Listeners = 41 | [{list_to_atom("edis-listener-" ++ integer_to_list(I)), 42 | {edis_listener, start_link, [I]}, permanent, brutal_kill, worker, [edis_listener]} 43 | || I <- lists:seq(MinPort, MaxPort)], 44 | {ok, {{one_for_one, 5, 10}, Listeners}}. -------------------------------------------------------------------------------- /include/edis.hrl: -------------------------------------------------------------------------------- 1 | 2 | -define(POS_INFINITY,1.7976931348623157e308). 3 | -define(NEG_INFINITY,-1.7976931348623157e308). 4 | 5 | -type edis_sort_field() :: self | binary() | {binary(), binary()}. 6 | -record(edis_sort_options, {by = self :: edis_sort_field(), 7 | limit = undefined :: undefined | {integer(), integer()}, 8 | get = [] :: [edis_sort_field()], 9 | direction = asc :: asc | desc, 10 | type = default :: alpha | default, 11 | store_in :: undefined | binary()}). 12 | 13 | -record(edis_message, {channel :: binary(), 14 | message :: binary()}). 15 | 16 | -record(edis_command, {timestamp = edis_util:timestamp() 17 | :: float(), 18 | db = 0 :: non_neg_integer(), 19 | cmd :: binary(), 20 | args = [] :: [term()], 21 | group :: keys | strings | hashes | lists | sets | zsets | pub_sub | transactions | connection | server, 22 | result_type :: edis:result_type(), 23 | timeout :: undefined | infinity | pos_integer(), 24 | expire :: undefined | never | pos_integer()}). 25 | 26 | -record(edis_item, {key :: binary(), 27 | type :: edis_db:item_type(), 28 | encoding :: edis_db:item_encoding(), 29 | value :: term(), 30 | vclock = edis_vclock:fresh() :: edis_vclock:vclock(), %type defined in edis_vclock.erl 31 | timestamp = edis_vclock:timestamp() :: integer(), 32 | expire = infinity :: infinity | pos_integer()}). -------------------------------------------------------------------------------- /priv/edis.config: -------------------------------------------------------------------------------- 1 | [{edis, [% Accept connections on the specified ports, default for Redis is 6379. 2 | {listener_port_range, {6379,6379}}, 3 | % The working directory. 4 | % The DB (or DBs) will be written inside this directory, if the backend needs it 5 | % Note that you must specify a directory here, not a file name. 6 | {dir, "/etc/edis/db"}, 7 | % If the server couldn't send a message in this interval, it closes the connection to the client 8 | {client_timeout, 35000}, 9 | % Set the number of databases. The default database is DB 0, you can select 10 | % a different one on a per-connection basis using SELECT where 11 | % dbid is a number between 0 and 'databases'-1 12 | {databases, 16}, 13 | % Require clients to issue AUTH before processing any other 14 | % commands. This might be useful in environments in which you do not trust 15 | % others with access to the host running redis-server. 16 | % 17 | % This should be undefined for backward compatibility and because most 18 | % people do not need auth (e.g. they run their own servers). 19 | % 20 | % Warning: since edis is pretty fast an outside user can try up to 21 | % 150k passwords per second against a good box. This means that you should 22 | % use a very strong password otherwise it will be very easy to break. 23 | {requirepass, undefined}, 24 | % edis_backend implementation to use as the backend for edis 25 | {backend, {edis_eleveldb_backend, [{create_if_missing, true}]}}]}, 26 | {lager, [ 27 | {handlers, 28 | [ 29 | {lager_console_backend, [debug,true]} 30 | ]} 31 | ]}, 32 | {sasl, [{errlog_type, all}, 33 | {sasl_error_logger, {file, "/var/log/edis.sasl.log"}}]}]. -------------------------------------------------------------------------------- /src/edis_sup.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc Edis main supervisor 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(edis_sup). 9 | -author('Fernando Benavides '). 10 | -author('Chad DePue '). 11 | 12 | -behaviour(supervisor). 13 | 14 | -export([start_link/0, init/1]). 15 | 16 | %%------------------------------------------------------------------- 17 | %% PUBLIC API 18 | %%------------------------------------------------------------------- 19 | %% @doc Starts a new supervisor 20 | -spec start_link() -> {ok, pid()}. 21 | start_link() -> 22 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 23 | 24 | %%------------------------------------------------------------------- 25 | %% SUPERVISOR API 26 | %%------------------------------------------------------------------- 27 | %% @hidden 28 | -spec init([]) -> {ok, {{one_for_one, 5, 10}, [supervisor:child_spec()]}}. 29 | init([]) -> 30 | ListenerSup = {edis_listener_sup, {edis_listener_sup, start_link, []}, 31 | permanent, 1000, supervisor, [edis_listener_sup]}, 32 | ClientSup = {edis_client_sup, {edis_client_sup, start_link, []}, 33 | permanent, 1000, supervisor, [edis_client_sup]}, 34 | DbSup = {edis_db_sup, {edis_db_sup, start_link, []}, 35 | permanent, 1000, supervisor, [edis_db_sup]}, 36 | PubSubSup = {edis_pubsub_sup, {edis_pubsub_sup, start_link, []}, 37 | permanent, 1000, supervisor, [edis_pubsub_sup]}, 38 | NodeSup = {edis_node_sup, {edis_node_sup, start_link, []}, 39 | permanent, 1000, supervisor, [edis_node_sup]}, 40 | {ok, {{one_for_one, 5, 10}, [NodeSup, PubSubSup, DbSup, ClientSup, ListenerSup]}}. -------------------------------------------------------------------------------- /src/edis_db_sup.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc Edis DataBase supervisor 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(edis_db_sup). 9 | -author('Fernando Benavides '). 10 | -author('Chad DePue '). 11 | 12 | -include("edis.hrl"). 13 | 14 | -behaviour(supervisor). 15 | 16 | -export([start_link/0, reload/0, init/1]). 17 | 18 | %% ==================================================================== 19 | %% External functions 20 | %% ==================================================================== 21 | %% @doc Starts the supervisor process 22 | -spec start_link() -> ignore | {error, term()} | {ok, pid()}. 23 | start_link() -> 24 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 25 | 26 | %% @doc Reloads configuration. Restarts the dbs 27 | -spec reload() -> ok. 28 | reload() -> 29 | true = exit(erlang:whereis(?MODULE), kill), 30 | ok. 31 | 32 | %% ==================================================================== 33 | %% Server functions 34 | %% ==================================================================== 35 | %% @hidden 36 | -spec init([]) -> {ok, {{one_for_one, 5, 10}, [supervisor:child_spec()]}}. 37 | init([]) -> 38 | Databases = edis_config:get(databases), 39 | lager:info("Client supervisor initialized (~p databases)~n", [Databases]), 40 | Monitor = {edis_db_monitor, {edis_db_monitor, start_link, []}, 41 | permanent, brutal_kill, worker, [edis_db_monitor]}, 42 | Children = 43 | [{edis_db:process(I), {edis_db, start_link, [I]}, 44 | permanent, brutal_kill, supervisor, [edis_db]} 45 | || I <- lists:seq(0, Databases - 1)], 46 | {ok, {{one_for_one, length(Children), 1}, [Monitor | Children]}}. -------------------------------------------------------------------------------- /priv/script_builder: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | 3 | main(_Args) -> 4 | %% Make sure file:consult can parse the .app file 5 | case file:consult("ebin/edis.app") of 6 | {ok, _} -> 7 | ok; 8 | {error, Reason} -> 9 | io:format("Invalid syntax in ebin/edis.app: ~p\n", [Reason]), 10 | halt(1) 11 | end, 12 | 13 | %% Add ebin paths to our path 14 | true = code:add_path("ebin"), 15 | ok = code:add_paths(filelib:wildcard("deps/*/ebin")), 16 | 17 | %% Read the contents of the files in ebin(s) 18 | Files = lists:flatmap(fun(Dir) -> load_files(Dir) end, ["ebin"|filelib:wildcard("deps/*/ebin")]), 19 | 20 | case zip:create("mem", Files, [memory]) of 21 | {ok, {"mem", ZipBin}} -> 22 | %% Archive was successfully created. Prefix that with header and write to "edis" file 23 | Script = <<"#!/usr/bin/env escript\n%%! +Bc +K true -name edis -smp enable -detached\n", ZipBin/binary>>, 24 | case file:write_file("bin/edis", Script) of 25 | ok -> ok; 26 | {error, WriteError} -> 27 | io:format("Failed to write bin/edis: ~p\n", [WriteError]), 28 | halt(1) 29 | end; 30 | {error, ZipError} -> 31 | io:format("Failed to construct bin/edis archive: ~p\n", [ZipError]), 32 | halt(1) 33 | end, 34 | 35 | %% Finally, update executable perms for our script 36 | case os:type() of 37 | {unix,_} -> 38 | [] = os:cmd("chmod u+x bin/edis"), 39 | ok; 40 | _ -> 41 | ok 42 | end, 43 | 44 | %% Place the eleveldb.so file near our script 45 | {ok, _Bytes} = file:copy("deps/eleveldb/priv/eleveldb.so", "bin/eleveldb.so"), 46 | 47 | %% Add a helpful message 48 | io:format("Congratulations! You now have a self-contained script called \"edis\" in\n" 49 | "your bin directory.\n"). 50 | 51 | load_files(Dir) -> 52 | [read_file(Filename, Dir) || Filename <- filelib:wildcard("*", Dir)]. 53 | 54 | read_file(Filename, Dir) -> 55 | {ok, Bin} = file:read_file(filename:join(Dir, Filename)), 56 | {Filename, Bin}. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # edis Makefile 2 | # Copyright (C) 2011 Electronic Inaka, LLC 3 | # edis is licensed by Electronic Inaka, LLC under the Apache 2.0 license 4 | 5 | ERL := erl -pa deps/*/ebin -pa ebin -pa src -boot start_sasl +Bc +K true -smp enable -s crypto -s inets -s ssl -s lager ${ERL_ARGS} 6 | PREFIX= /usr/local 7 | INSTALL_BIN= $(PREFIX)/bin 8 | INSTALL= cp -p 9 | 10 | all: erl 11 | mkdir -p bin 12 | ./priv/script_builder 13 | 14 | erl: 15 | rebar get-deps compile 16 | 17 | clean: 18 | rm -rf bin 19 | rebar clean 20 | 21 | build_plt: erl 22 | dialyzer --verbose --build_plt --apps kernel stdlib sasl erts ssl tools os_mon runtime_tools crypto inets \ 23 | xmerl webtool snmp public_key mnesia eunit syntax_tools compiler --output_plt ~/.edis_plt -pa deps/*/ebin ebin 24 | 25 | analyze: erl 26 | dialyzer --verbose -pa deps/*/ebin --plt ~/.edis_plt -Wunmatched_returns -Werror_handling -Wbehaviours ebin 27 | 28 | xref: erl 29 | rebar skip_deps=true xref 30 | 31 | run: erl 32 | if [ ! -f ~/.hosts.erlang ] ; then echo "file ~/.hosts.erlang does not exist, run 'touch ~/.hosts.erlang' to fix this problem" ; exit 1 ; fi 33 | ${ERL} -s edis 34 | 35 | test: erl 36 | #${ERL} -config test/test.config -noshell -sname edis_test_server -s edis & 37 | mkdir -p ./test/ebin 38 | mkdir -p ./logs/ct 39 | # To run all tests, remove: suites=keys 40 | rebar skip_deps=true ct suites=keys -v; \ 41 | #kill `ps aux | grep beam | grep edis_[t]est_server | awk '{print $$2}'` 42 | 43 | test-hanoidb: erl 44 | ${ERL} -config test/test-hanoidb.config -noshell -sname edis_test_server -s edis -run elog debug & 45 | mkdir -p ./test/ebin 46 | erlc -o ./test/ebin -pa deps/lager/ebin +debug_info +'{parse_transform, lager_transform}' ./test/*_SUITE.erl 47 | rebar skip_deps=true ct ; \ 48 | kill `ps aux | grep beam | grep edis_[t]est_server | awk '{print $$2}'` 49 | 50 | shell: erl 51 | ${ERL} 52 | 53 | doc: erl 54 | rebar skip_deps=true doc 55 | 56 | install: 57 | mkdir -p $(INSTALL_BIN) 58 | $(INSTALL) bin/* $(INSTALL_BIN) 59 | 60 | service: install 61 | mkdir -p /etc/edis/db/ 62 | if [ ! -f /etc/edis/edis.config ] ; then cp priv/edis.config /etc/edis/ ; fi 63 | cp priv/edis.init.d /etc/init.d/edis 64 | 65 | -------------------------------------------------------------------------------- /src/edis_backend.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc DB backend behaviour for edis. 6 | %%%
    7 | %%%
  • 8 | %%%
    init(string(), non_neg_integer(), eleveldb:open_options()) -> {ok, ref()} | {error, term()}
    9 | %%% Opens and/or initializes the backend
    10 | %%%
  • 11 | %%%
    write(ref(), edis_backend:write_actions()) -> ok | {error, term()}
    12 | %%% Atomically apply a set of actions
    13 | %%%
  • 14 | %%%
    put(ref(), binary(), #edis_item{}) -> ok | {error, term()}
    15 | %%% Puts an item in the database
    16 | %%%
  • 17 | %%%
    delete(ref(), binary()) -> ok | {error, term()}
    18 | %%% Deletes an item
    19 | %%%
  • 20 | %%%
    is_empty(ref()) -> boolean()
    21 | %%% Determines if the database is empty
    22 | %%%
  • 23 | %%%
    destroy(ref()) -> ok | {error, term()}
    24 | %%% Deletes database
    25 | %%%
  • 26 | %%%
    status(ref()) -> {ok, binary()} | error
    27 | %%% Determines the databse status
    28 | %%%
  • 29 | %%%
    get(ref(), binary()) -> #edis_item{} | not_found | {error, term()}
    30 | %%% Gets an item from the database
    31 | %%%
  • 32 | %%%
33 | %%% @end 34 | %%%------------------------------------------------------------------- 35 | -module(edis_backend). 36 | -author('Fernando Benavides '). 37 | -author('Chad DePue '). 38 | 39 | -include("edis.hrl"). 40 | 41 | -type write_actions() :: [{put, Key::binary(), Value::#edis_item{}} | 42 | {delete, Key::binary()} | 43 | clear]. 44 | -type fold_fun() :: fun((Item::#edis_item{}, any()) -> any()). 45 | -export_type([write_actions/0, fold_fun/0]). 46 | 47 | -export([behaviour_info/1]). 48 | 49 | %% @hidden 50 | -spec behaviour_info(callbacks|term()) -> undefined | [{atom(), non_neg_integer()}]. 51 | behaviour_info(callbacks) -> [{init, 3}, {write, 2}, {put, 3}, {delete, 2}, {is_empty, 1}, 52 | {destroy, 1}, {status, 1}, {get, 2}]; 53 | behaviour_info(_) -> undefined. -------------------------------------------------------------------------------- /test/connection_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% @hidden 2 | -module(connection_SUITE). 3 | -compile(export_all). 4 | 5 | all() -> 6 | [echo,ping,select,auth,quit]. 7 | 8 | init_per_testcase(_TestCase, Config) -> 9 | {ok,Client} = connect_erldis(10), 10 | NewConfig = lists:keystore(client,1,Config,{client,Client}), 11 | NewConfig. 12 | 13 | connect_erldis(0) -> {error,{socket_error,econnrefused}}; 14 | connect_erldis(Times) -> 15 | timer:sleep(2000), 16 | case erldis:connect(localhost,16380) of 17 | {ok,Client} -> {ok,Client}; 18 | _ -> connect_erldis(Times - 1) 19 | end. 20 | 21 | echo(Config) -> 22 | {client,Client} = lists:keyfind(client, 1, Config), 23 | Msg = <<"echo this message">>, 24 | Msg = erldis_client:sr_scall(Client, [<<"echo">>,Msg]). 25 | 26 | ping(Config) -> 27 | {client,Client} = lists:keyfind(client, 1, Config), 28 | pong = erldis_client:sr_scall(Client, <<"ping">>). 29 | 30 | select(Config) -> 31 | {client,Client} = lists:keyfind(client, 1, Config), 32 | ok = erldis_client:sr_scall(Client, [<<"select">>, <<"15">>]), 33 | ok = erldis_client:sr_scall(Client, [<<"select">>, <<"a">>]), 34 | 35 | {error,<<"ERR invalid DB index">>} = erldis_client:sr_scall(Client, [<<"select">>, <<"16">>]), 36 | {error,<<"ERR wrong number of arguments for 'SELECT' command">>} = erldis_client:sr_scall(Client, [<<"select">>, <<"4">>, <<"3">>]), 37 | {error,<<"ERR invalid DB index">>} = erldis_client:sr_scall(Client, [<<"select">>, <<"-1">>]). 38 | 39 | auth(Config) -> 40 | {client,Client} = lists:keyfind(client, 1, Config), 41 | Pwd = <<"hhedls56329">>, 42 | 43 | [ok] = erldis_client:scall(Client, [<<"config">>,<<"set">>,<<"requirepass">>, Pwd]), 44 | {ok,NewClient} = connect_erldis(10), 45 | {error,<<"ERR operation not permitted">>} = erldis_client:sr_scall(NewClient, <<"ping">>), 46 | 47 | [{error,<<"ERR invalid password">>}] = erldis_client:scall(NewClient, [<<"auth">>, <<"bad_pass">>]), 48 | {error,<<"ERR operation not permitted">>} = erldis_client:sr_scall(NewClient, <<"ping">>), 49 | 50 | [ok] = erldis_client:scall(NewClient, [<<"auth">>, Pwd]), 51 | pong = erldis_client:sr_scall(NewClient, <<"ping">>), 52 | pong = erldis_client:sr_scall(NewClient, <<"ping">>), 53 | 54 | [ok] = erldis_client:scall(NewClient, [<<"config">>,<<"set">>,<<"requirepass">>]). 55 | 56 | quit(Config) -> 57 | {client,Client} = lists:keyfind(client, 1, Config), 58 | shutdown = erldis_client:stop(Client). 59 | -------------------------------------------------------------------------------- /src/edis.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc Redis (http://redis.io), but in Erlang 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(edis). 9 | -author('Fernando Benavides '). 10 | -author('Chad DePue '). 11 | -vsn('0.1'). 12 | 13 | -include("edis.hrl"). 14 | 15 | -type result_type() :: ok | string | bulk | number | float | zrange | multi_bulk | boolean | multi_result | sort. 16 | -type command() :: #edis_command{}. 17 | -export_type([result_type/0, command/0]). 18 | 19 | -behaviour(application). 20 | 21 | -export([start/0, stop/0]). 22 | -export([start/2, stop/1]). 23 | -export([main/1]). 24 | 25 | %%------------------------------------------------------------------- 26 | %% ESCRIPT API 27 | %%------------------------------------------------------------------- 28 | %% @doc Entry point for escript 29 | -spec main([string()]) -> ok. 30 | main(Args) -> 31 | case Args of 32 | [] -> ok; 33 | [ConfigFile] -> edis_util:load_config(ConfigFile) 34 | end, 35 | crypto:start(), 36 | ok = application:start(lager), 37 | ok = start(), 38 | Pid = erlang:whereis(edis_sup), 39 | Ref = erlang:monitor(process, Pid), 40 | receive 41 | {'DOWN',Ref,process,Pid,shutdown} -> halt(0); 42 | {'DOWN',Ref,process,Pid,Reason} -> error_logger:error_msg("System down: ~p~n", [Reason]), halt(1); 43 | Error -> error_logger:error_msg("Unexpected message: ~p~n", [Error]), halt(-1) 44 | end. 45 | 46 | %%------------------------------------------------------------------- 47 | %% ADMIN API 48 | %%------------------------------------------------------------------- 49 | %% @doc Starts the application 50 | -spec start() -> ok | {error, {already_started, ?MODULE}}. 51 | start() -> application:start(?MODULE). 52 | 53 | %% @doc Stops the application 54 | -spec stop() -> ok. 55 | stop() -> application:stop(?MODULE). 56 | 57 | %%------------------------------------------------------------------- 58 | %% BEHAVIOUR CALLBACKS 59 | %%------------------------------------------------------------------- 60 | %% @private 61 | -spec start(any(), any()) -> {ok, pid()}. 62 | start(_StartType, _StartArgs) -> 63 | edis_sup:start_link(). 64 | 65 | %% @private 66 | -spec stop(any()) -> ok. 67 | stop(_State) -> ok. 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | An *Erlang* version of [Redis](http://redis.io), with the goal of similar algorithmic performance but support for multiple master nodes and larger-than-RAM datasets. For [More info](http://inakanetworks.com/assets/pdf//Edis_Implementing_Redis_In_Erlang.pdf), see this PDF of a Talk at Erlang Factory 2012. 2 | 3 | ## Contact Us 4 | For **questions** or **general comments** regarding the use of this library, please use our public 5 | [hipchat room](http://inaka.net/hipchat). 6 | 7 | If you find any **bugs** or have a **problem** while using this library, please [open an issue](https://github.com/inaka/edis/issues/new) in this repo (or a pull request :)). 8 | 9 | And you can check all of our open-source projects at [inaka.github.io](http://inaka.github.io) 10 | 11 | ## Usage 12 | Just run `$ make run` and open connections with your favourite redis client. 13 | 14 | ## Differences with Redis 15 | ### Different Behaviour 16 | * _SAVE_, _BGSAVE_ and _LASTSAVE_ are database dependent. The original Redis saves all databases at once, edis saves just the one you _SELECT_'ed. 17 | * _INFO_ provides much less information and no statistics (so, _CONFIG RESETSTAT_ does nothing at all) 18 | * _MULTI_ doesn't support: 19 | - cross-db commands (i.e. _FLUSHALL_, _SELECT_, _MOVE_) 20 | - non-db commands (i.e _AUTH_, _CONFIG *_, _SHUTDOWN_, _MONITOR_) 21 | - pub/sub commands (i.e. _PUBLISH_, _SUBSCRIBE_, _UNSUBSCRIBE_, _PSUBSCRIBE_, _PUNSUBSCRIBE_) 22 | * _(P)UNSUBSCRIBE_ commands are not allowed outside _PUBSUB_ mode 23 | * _PUBLISH_ response is not precise: it's the amount of all clients subscribed to any channel and/or pattern, not just those that will handle the message. On the other hand it runs in _O(1)_ because it's asynchronous, it just dispatches the message. 24 | 25 | ### Missing Features 26 | * Dynamic node configuration (i.e. the _SLAVEOF_ command is not implemented) 27 | * Encoding optimization (i.e. all objects are encoded as binary representations of erlang terms, so for instance "123" will never be stored as an int) 28 | * _OBJECT REFCOUNT_ allways returns 1 for existing keys and (nil) otherwise 29 | 30 | ### Unsupported Commands 31 | _SYNC_, _SLOWLOG_, _SLAVEOF_, _DEBUG *_ 32 | 33 | ### License 34 | edis is licensed by Electronic Inaka, LLC under the Apache 2.0 license; see the LICENSE file in this repository. 35 | 36 | ### TODO 37 | 38 | * Backends 39 | * HanoiDB 40 | * Make use of the efficient range searchs with start/end for searching ranges 41 | * Make use of time-based key expiry 42 | * Finish the TODO items 43 | 44 | -------------------------------------------------------------------------------- /src/backends/edis_pd_backend.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc Process Dictionary based backend. DON'T DO THIS AT HOME, KIDS 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(edis_pd_backend). 9 | -author('Fernando Benavides '). 10 | -author('Chad DePue '). 11 | 12 | -behaviour(edis_backend). 13 | 14 | -include("edis.hrl"). 15 | 16 | -record(ref, {}). 17 | -opaque ref() :: #ref{}. 18 | -export_type([ref/0]). 19 | 20 | -export([init/3, write/2, put/3, delete/2, fold/3, is_empty/1, destroy/1, status/1, get/2]). 21 | 22 | %% ==================================================================== 23 | %% Behaviour functions 24 | %% ==================================================================== 25 | -spec init(string(), non_neg_integer(), [any()]) -> {ok, ref()} | {error, term()}. 26 | init(_Dir, _Index, _Options) -> 27 | lager:warning("USING PD BACKEND!! This should not be a production environment!~n", []), 28 | {ok, #ref{}}. 29 | 30 | -spec write(ref(), edis_backend:write_actions()) -> ok | {error, term()}. 31 | write(#ref{}, Actions) -> 32 | lists:foreach( 33 | fun({put, Key, Item}) -> erlang:put(Key, Item); 34 | ({delete, Key}) -> erlang:erase(Key); 35 | (clear) -> erlang:erase() 36 | end, Actions). 37 | 38 | -spec put(ref(), binary(), #edis_item{}) -> ok | {error, term()}. 39 | put(#ref{}, Key, Item) -> 40 | _ = erlang:put(Key, Item), ok. 41 | 42 | -spec delete(ref(), binary()) -> ok | {error, term()}. 43 | delete(#ref{}, Key) -> 44 | _ = erlang:erase(Key), ok. 45 | 46 | -spec fold(ref(), edis_backend:fold_fun(), term()) -> term(). 47 | fold(#ref{}, Fun, InitValue) -> 48 | lists:foldl( 49 | fun({_Key, Item}, Acc) -> 50 | Fun(Item, Acc) 51 | end, InitValue, [{K, V} || {K, V} <- erlang:get(), not is_atom(K)]). 52 | 53 | -spec is_empty(ref()) -> boolean(). 54 | is_empty(#ref{}) -> [] =:= erlang:get(). 55 | 56 | -spec destroy(ref()) -> ok | {error, term()}. 57 | destroy(#ref{}) -> 58 | lists:foreach(fun({K,V}) when is_atom(K) -> erlang:put(K, V); 59 | (_) -> ok 60 | end, erlang:erase()). 61 | 62 | -spec status(ref()) -> {ok, binary()} | error. 63 | status(#ref{}) -> {ok, iolist_to_binary(io_lib:format("~p~n", [erlang:process_info(self(), dictionary)]))}. 64 | 65 | -spec get(ref(), binary()) -> #edis_item{} | not_found | {error, term()}. 66 | get(#ref{}, Key) -> 67 | case erlang:get(Key) of 68 | undefined -> not_found; 69 | Item -> Item 70 | end. -------------------------------------------------------------------------------- /src/edis_client_sup.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc Edis client supervisor 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(edis_client_sup). 9 | -author('Fernando Benavides '). 10 | -author('Chad DePue '). 11 | 12 | -include("edis.hrl"). 13 | -define(MANAGERS, 1). %%NOTE: Pump it up to reduce message_queue_lens on massive client initialization 14 | 15 | -behaviour(supervisor). 16 | 17 | -export([start_link/0, start_client/0, init/1, count_clients/0, reload/0]). 18 | 19 | %% ==================================================================== 20 | %% External functions 21 | %% ==================================================================== 22 | %% @doc Starts the supervisor process 23 | -spec start_link() -> ignore | {error, term()} | {ok, pid()}. 24 | start_link() -> 25 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 26 | 27 | %% @doc Starts a new client process 28 | -spec start_client() -> {ok, pid() | undefined} | {error, term()}. 29 | start_client() -> 30 | _ = random:seed(erlang:now()), 31 | Manager = 32 | list_to_atom("edis-client-mgr-" ++ integer_to_list(random:uniform(?MANAGERS))), 33 | supervisor:start_child(Manager, []). 34 | 35 | %% @doc Reloads configuration. Restarts the managers 36 | -spec reload() -> ok. 37 | reload() -> 38 | true = exit(erlang:whereis(?MODULE), kill), 39 | ok. 40 | 41 | %% @doc Returns the count of reigstered clients under the supervision of this process 42 | -spec count_clients() -> non_neg_integer(). 43 | count_clients() -> 44 | lists:sum( 45 | lists:map( 46 | fun(I) -> 47 | proplists:get_value( 48 | active, 49 | supervisor:count_children( 50 | list_to_atom("edis-client-mgr-" ++ integer_to_list(I))), 51 | 0) 52 | end, lists:seq(1, ?MANAGERS))). 53 | 54 | %% ==================================================================== 55 | %% Server functions 56 | %% ==================================================================== 57 | %% @hidden 58 | -spec init([]) -> {ok, {{one_for_one, 5, 10}, [supervisor:child_spec()]}}. 59 | init([]) -> 60 | lager:info("Client supervisor initialized (~p managers)~n", [?MANAGERS]), 61 | Managers = 62 | [{list_to_atom("edis-client-mgr-" ++ integer_to_list(I)), 63 | {edis_client_mgr, start_link, [list_to_atom("edis-client-mgr-" ++ integer_to_list(I))]}, 64 | permanent, brutal_kill, supervisor, [edis_client_mgr]} 65 | || I <- lists:seq(1, ?MANAGERS)], 66 | {ok, {{one_for_one, length(Managers), 1}, Managers}}. -------------------------------------------------------------------------------- /src/backends/edis_eleveldb_backend.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc leveldb based backend 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(edis_eleveldb_backend). 9 | -author('Fernando Benavides '). 10 | -author('Chad DePue '). 11 | 12 | -behaviour(edis_backend). 13 | 14 | -include("edis.hrl"). 15 | 16 | -record(ref, {db :: eleveldb:db_ref(), 17 | file :: string()}). 18 | -opaque ref() :: #ref{}. 19 | -export_type([ref/0]). 20 | 21 | -export([init/3, write/2, put/3, delete/2, fold/3, is_empty/1, destroy/1, status/1, get/2]). 22 | 23 | %% ==================================================================== 24 | %% Behaviour functions 25 | %% ==================================================================== 26 | -spec init(string(), non_neg_integer(), eleveldb:open_options()) -> {ok, ref()} | {error, term()}. 27 | init(Dir, Index, Options) -> 28 | File = Dir ++ "/edis-" ++ integer_to_list(Index), 29 | case eleveldb:open(File, Options) of 30 | {ok, Db} -> {ok, #ref{db = Db, file = File}}; 31 | Error -> Error 32 | end. 33 | 34 | -spec write(ref(), edis_backend:write_actions()) -> ok | {error, term()}. 35 | write(#ref{db = Db}, Actions) -> 36 | ParseAction = fun({put, Key, Item}) -> {put, Key, erlang:term_to_binary(Item)}; 37 | (Action) -> Action 38 | end, 39 | eleveldb:write(Db, lists:map(ParseAction, Actions), []). 40 | 41 | -spec put(ref(), binary(), #edis_item{}) -> ok | {error, term()}. 42 | put(#ref{db = Db}, Key, Item) -> 43 | eleveldb:put(Db, Key, erlang:term_to_binary(Item), []). 44 | 45 | -spec delete(ref(), binary()) -> ok | {error, term()}. 46 | delete(#ref{db = Db}, Key) -> 47 | eleveldb:delete(Db, Key, []). 48 | 49 | -spec fold(ref(), edis_backend:fold_fun(), term()) -> term(). 50 | fold(#ref{db = Db}, Fun, InitValue) -> 51 | eleveldb:fold( 52 | Db, 53 | fun({_Key, Bin}, Acc) -> 54 | Fun(erlang:binary_to_term(Bin), Acc) 55 | end, InitValue, [{fill_cache, false}]). 56 | 57 | -spec is_empty(ref()) -> boolean(). 58 | is_empty(#ref{db = Db}) -> eleveldb:is_empty(Db). 59 | 60 | -spec destroy(ref()) -> ok | {error, term()}. 61 | destroy(#ref{file = File}) -> eleveldb:destroy(File, []). 62 | 63 | -spec status(ref()) -> {ok, binary()} | error. 64 | status(#ref{db = Db}) -> eleveldb:status(Db, <<"leveldb.stats">>). 65 | 66 | -spec get(ref(), binary()) -> #edis_item{} | not_found | {error, term()}. 67 | get(#ref{db = Db}, Key) -> 68 | case eleveldb:get(Db, Key, []) of 69 | {ok, Bin} -> erlang:binary_to_term(Bin); 70 | NotFoundOrError -> NotFoundOrError 71 | end. -------------------------------------------------------------------------------- /src/edis_config.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc edis Configuration utilities 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(edis_config). 9 | -author('Fernando Benavides '). 10 | -author('Chad DePue '). 11 | 12 | -export([get/1, set/2]). 13 | 14 | -include("edis.hrl"). 15 | 16 | -type config_option() :: listener_port_range | client_timeout | databases | requirepass | dir | backend. 17 | 18 | %% @doc sets configuration params 19 | -spec set(config_option(), term()) -> ok. 20 | set(listener_port_range, {P1, P2}) when is_integer(P1), is_integer(P2), P1 =< P2 -> 21 | ok = application:set_env(edis, listener_port_range, {P1, P2}), 22 | edis_listener_sup:reload(); 23 | set(listener_port_range, Param) -> 24 | lager:alert("Invalid range: ~p~n", [Param]), 25 | throw(invalid_param); 26 | set(client_timeout, Timeout) when is_integer(Timeout), Timeout >= 0 -> 27 | ok = application:set_env(edis, client_timeout, Timeout); 28 | set(client_timeout, Param) -> 29 | lager:alert("Invalid timeout: ~p~n", [Param]), 30 | throw(invalid_param); 31 | set(databases, Dbs) when is_integer(Dbs), Dbs > 0 -> 32 | ok = application:set_env(edis, databases, Dbs), 33 | edis_db_sup:reload(); 34 | set(databases, Param) -> 35 | lager:alert("Invalid number: ~p~n", [Param]), 36 | throw(invalid_param); 37 | set(requirepass, Pass) -> 38 | ok = application:set_env(edis, requirepass, Pass); 39 | set(dir, Dir) when is_binary(Dir) -> 40 | ok = application:set_env(edis, dir, binary_to_list(Dir)), 41 | edis_db_sup:reload(); 42 | set(dir, Param) -> 43 | lager:alert("Invalid dir: ~p~n", [Param]), 44 | throw(invalid_param); 45 | set(Param, Value) -> 46 | lager:alert("Unsupported param: ~p: ~p~n", [Param, Value]), 47 | throw(unsupported_param). 48 | 49 | %% @doc gets configuration params 50 | -spec get(binary() | config_option()) -> term(). 51 | get(listener_port_range) -> 52 | get(listener_port_range, {6379,6379}); 53 | get(client_timeout) -> 54 | get(client_timeout, 35000); 55 | get(databases) -> 56 | get(databases, 16); 57 | get(requirepass) -> 58 | get(requirepass, undefined); 59 | get(dir) -> 60 | get(dir, "./db/"); 61 | get(backend) -> 62 | get(backend, {edis_eleveldb_backend, [{create_if_missing, true}]}); 63 | get(Pattern) -> 64 | [{K, V} || 65 | {K, V} <- application:get_all_env(edis), 66 | re:run(atom_to_binary(K, utf8), Pattern) =/= nomatch]. 67 | 68 | get(Field, Default) -> 69 | case application:get_env(edis, Field) of 70 | {ok, Value} -> 71 | lager:debug("~p := ~p~n", [Field, Value]), 72 | Value; 73 | _ -> 74 | lager:debug("~p := ~p~n", [Field, Default]), 75 | Default 76 | end. 77 | -------------------------------------------------------------------------------- /src/backends/edis_hanoidb_backend.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc leveldb based backend 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(edis_hanoidb_backend). 9 | -author('Greg Burd '). 10 | 11 | -behaviour(edis_backend). 12 | 13 | -include("edis.hrl"). 14 | -include_lib("hanoidb/include/hanoidb.hrl"). 15 | 16 | -record(ref, {db :: hanoidb:db_ref(), 17 | file :: string()}). 18 | -opaque ref() :: #ref{}. 19 | -export_type([ref/0]). 20 | 21 | -export([init/3, write/2, put/3, delete/2, fold/3, is_empty/1, destroy/1, status/1, get/2]). 22 | 23 | %% ==================================================================== 24 | %% Behaviour functions 25 | %% ==================================================================== 26 | -spec init(string(), non_neg_integer(), hanoidb:open_options()) -> {ok, ref()} | {error, term()}. 27 | init(Dir, Index, Options) -> 28 | File = Dir ++ "/edis-" ++ integer_to_list(Index), 29 | case hanoidb:open(File, Options) of 30 | {ok, Db} -> {ok, #ref{db = Db, file = File}}; 31 | Error -> Error 32 | end. 33 | 34 | -spec write(ref(), edis_backend:write_actions()) -> ok | {error, term()}. 35 | write(#ref{db = Db}, Actions) -> 36 | ParseAction = fun({put, Key, Item}) -> 37 | {put, sext:encode(Key), erlang:term_to_binary(Item)}; 38 | ({delete, Key}) -> 39 | {delete, sext:encode(Key)}; 40 | (Action) -> Action 41 | end, 42 | hanoidb:transact(Db, lists:map(ParseAction, Actions)). 43 | 44 | -spec put(ref(), binary(), #edis_item{}) -> ok | {error, term()}. 45 | put(#ref{db = Db}, Key, Item) -> 46 | hanoidb:put(Db, sext:encode(Key), erlang:term_to_binary(Item)). 47 | 48 | -spec delete(ref(), binary()) -> ok | {error, term()}. 49 | delete(#ref{db = Db}, Key) -> 50 | hanoidb:delete(Db, sext:encode(Key)). 51 | 52 | -spec fold(ref(), edis_backend:fold_fun(), term()) -> term(). 53 | fold(#ref{db = Db}, Fun, InitValue) -> 54 | hanoidb:range( 55 | Db, 56 | fun({_Key, Bin}, Acc) -> 57 | Fun(erlang:binary_to_term(Bin), Acc) 58 | end, 59 | InitValue). 60 | 61 | -define(MAX_OBJECT_KEY, <<16,0,0,0,4>>). %% TODO: really? 62 | 63 | -spec is_empty(ref()) -> boolean(). 64 | is_empty(#ref{db = Db}) -> 65 | FoldFun = fun(_K, _V, _Acc) -> throw(ok) end, 66 | try 67 | Range = #key_range{ from_key = sext:decode(<<>>), 68 | from_inclusive = true, 69 | to_key = ?MAX_OBJECT_KEY, 70 | to_inclusive = false 71 | }, 72 | [] =:= hanoi:fold_range(Db, FoldFun, [], Range) 73 | catch 74 | _:ok -> 75 | false 76 | end. 77 | 78 | -spec destroy(ref()) -> ok | {error, term()}. 79 | destroy(#ref{db = Db}) -> 80 | hanoidb:destroy(Db). 81 | 82 | -spec status(ref()) -> {ok, binary()} | error. 83 | status(#ref{db = _Db}) -> 84 | throw(not_implemented). %% TODO: not yet implemented 85 | 86 | -spec get(ref(), binary()) -> #edis_item{} | not_found | {error, term()}. 87 | get(#ref{db = Db}, Key) -> 88 | case hanoidb:get(Db, sext:encode(Key)) of 89 | {ok, Bin} -> 90 | erlang:binary_to_term(Bin); 91 | NotFoundOrError -> 92 | NotFoundOrError 93 | end. 94 | -------------------------------------------------------------------------------- /src/edis_db_monitor.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc edis Database Monitor 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(edis_db_monitor). 9 | -author('Fernando Benavides '). 10 | -author('Chad DePue '). 11 | 12 | -include("edis.hrl"). 13 | 14 | -behaviour(gen_event). 15 | 16 | -export([start_link/0, add_sup_handler/0, delete_handler/0, notify/1]). 17 | -export([init/1, handle_event/2, handle_call/2, handle_info/2, terminate/2, code_change/3]). 18 | 19 | -record(state, {client :: pid()}). 20 | -type state() :: #state{}. 21 | 22 | %% ==================================================================== 23 | %% External functions 24 | %% ==================================================================== 25 | %% @doc Starts the event manager 26 | -spec start_link() -> {ok, pid()} | {error, term()}. 27 | start_link() -> 28 | gen_event:start_link({local, ?MODULE}). 29 | 30 | %% @doc Subscribes client. 31 | %% From this point on, all db operations will be notified to the client procedure using 32 | %% Erlang messages in the form of #edis_command{}. 33 | %% If the event handler later is deleted, the event manager sends a message {gen_event_EXIT,Handler,Reason} to the calling process. Reason is one of the following: 34 | %% 35 | %% normal, if the event handler has been removed due to a call to delete_handler/3, or remove_handler has been returned by a callback function (see below). 36 | %% shutdown, if the event handler has been removed because the event manager is terminating. 37 | %% {swapped,NewHandler,Pid}, if the process Pid has replaced the event handler with another event handler NewHandler using a call to swap_handler/3 or swap_sup_handler/3. 38 | %% a term, if the event handler is removed due to an error. Which term depends on the error. 39 | %% @end 40 | -spec add_sup_handler() -> ok. 41 | add_sup_handler() -> 42 | Self = self(), 43 | gen_event:add_sup_handler(?MODULE, {?MODULE, Self}, Self). 44 | 45 | %% @doc Unsubscribes client. 46 | -spec delete_handler() -> ok. 47 | delete_handler() -> 48 | gen_event:delete_handler(?MODULE, {?MODULE, self()}, normal). 49 | 50 | %% @doc Notifies an event. 51 | -spec notify(#edis_command{}) -> ok. 52 | notify(Command) -> 53 | gen_event:notify(?MODULE, Command). 54 | 55 | %% ==================================================================== 56 | %% Server functions 57 | %% ==================================================================== 58 | %% @hidden 59 | -spec init(pid()) -> {ok, state()}. 60 | init(Client) -> {ok, #state{client = Client}}. 61 | 62 | %% @hidden 63 | -spec handle_event(term(), state()) -> {ok, state()}. 64 | handle_event(Event, State = #state{client = Client}) -> 65 | Client ! Event, 66 | {ok, State}. 67 | 68 | %% @hidden 69 | -spec handle_call(term(), state()) -> {ok, ok, state()}. 70 | handle_call(_Request, State) -> {ok, ok, State}. 71 | %% @hidden 72 | -spec handle_info(term(), state()) -> {ok, state()}. 73 | handle_info(Info, State) -> 74 | lager:warning("Unexpected Info:~n\t~p~n", [Info]), 75 | {ok, State}. 76 | 77 | %% @hidden 78 | -spec terminate(term(), state()) -> ok. 79 | terminate(_Reason, _State) -> ok. 80 | 81 | %% @hidden 82 | -spec code_change(term(), state(), term()) -> {ok, state()}. 83 | code_change(_OldVsn, State, _Extra) -> {ok, State}. 84 | -------------------------------------------------------------------------------- /src/edis_pubsub.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc edis PubSub Monitor 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(edis_pubsub). 9 | -author('Fernando Benavides '). 10 | -author('Chad DePue '). 11 | 12 | -include("edis.hrl"). 13 | 14 | -behaviour(gen_event). 15 | 16 | -export([start_link/0, add_sup_handler/0, delete_handler/0, count_handlers/0, notify/1]). 17 | -export([init/1, handle_event/2, handle_call/2, handle_info/2, terminate/2, code_change/3]). 18 | 19 | -record(state, {client :: pid(), 20 | type :: pattern | channel, 21 | value :: binary() | re:mp()}). 22 | -type state() :: #state{}. 23 | 24 | %% ==================================================================== 25 | %% External functions 26 | %% ==================================================================== 27 | %% @doc Starts the event manager 28 | -spec start_link() -> {ok, pid()} | {error, term()}. 29 | start_link() -> 30 | gen_event:start_link({local, ?MODULE}). 31 | 32 | %% @doc Subscribes client. 33 | %% From this point on, all db operations will be notified to the client procedure using 34 | %% Erlang messages in the form of #edis_message{}. 35 | %% If the event handler later is deleted, the event manager sends a message {gen_event_EXIT,Handler,Reason} to the calling process. Reason is one of the following: 36 | %% 37 | %% normal, if the event handler has been removed due to a call to delete_handler/3, or remove_handler has been returned by a callback function (see below). 38 | %% shutdown, if the event handler has been removed because the event manager is terminating. 39 | %% {swapped,NewHandler,Pid}, if the process Pid has replaced the event handler with another event handler NewHandler using a call to swap_handler/3 or swap_sup_handler/3. 40 | %% a term, if the event handler is removed due to an error. Which term depends on the error. 41 | %% @end 42 | -spec add_sup_handler() -> ok. 43 | add_sup_handler() -> 44 | Self = self(), 45 | gen_event:add_sup_handler(?MODULE, {?MODULE, Self}, Self). 46 | 47 | %% @doc Unsubscribes client. 48 | -spec delete_handler() -> ok. 49 | delete_handler() -> 50 | true = unlink(erlang:whereis(?MODULE)), 51 | gen_event:delete_handler(?MODULE, {?MODULE, self()}, normal). 52 | 53 | %% @doc Returns the number of currently active handlers 54 | -spec count_handlers() -> non_neg_integer(). 55 | count_handlers() -> 56 | length(gen_event:which_handlers(?MODULE)). 57 | 58 | %% @doc Notifies an event. 59 | -spec notify(#edis_message{}) -> ok. 60 | notify(Command) -> 61 | gen_event:notify(?MODULE, Command). 62 | 63 | %% ==================================================================== 64 | %% Server functions 65 | %% ==================================================================== 66 | %% @hidden 67 | -spec init(pid()) -> {ok, state()}. 68 | init(Client) -> {ok, #state{client = Client}}. 69 | 70 | %% @hidden 71 | -spec handle_event(term(), state()) -> {ok, state()}. 72 | handle_event(Event, State = #state{client = Client}) -> 73 | Client ! Event, 74 | {ok, State}. 75 | 76 | %% @hidden 77 | -spec handle_call(term(), state()) -> {ok, ok, state()}. 78 | handle_call(_Request, State) -> {ok, ok, State}. 79 | %% @hidden 80 | -spec handle_info(term(), state()) -> {ok, state()}. 81 | handle_info(Info, State) -> 82 | lager:warning("Unexpected Info:~n\t~p~n", [Info]), 83 | {ok, State}. 84 | 85 | %% @hidden 86 | -spec terminate(term(), state()) -> ok. 87 | terminate(_Reason, _State) -> ok. 88 | 89 | %% @hidden 90 | -spec code_change(term(), state(), term()) -> {ok, state()}. 91 | code_change(_OldVsn, State, _Extra) -> {ok, State}. -------------------------------------------------------------------------------- /test/benchmarks/pubsub_bench.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc Benchmarks for pubsub commands 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(pubsub_bench). 9 | -author('Fernando Benavides '). 10 | -author('Chad DePue '). 11 | 12 | -behaviour(edis_bench). 13 | 14 | -include("edis.hrl"). 15 | 16 | -export([all/0, 17 | init/1, init_per_testcase/2, init_per_round/3, 18 | quit/1, quit_per_testcase/2, quit_per_round/3]). 19 | -export([psubscribe/1, publish/1, punsubscribe/1, subscribe/1, unsubscribe/1]). 20 | 21 | %% ==================================================================== 22 | %% External functions 23 | %% ==================================================================== 24 | -spec all() -> [atom()]. 25 | all() -> [Fun || {Fun, _} <- ?MODULE:module_info(exports) -- edis_bench:behaviour_info(callbacks), 26 | Fun =/= module_info]. 27 | 28 | -spec init([]) -> ok. 29 | init(_Extra) -> ok. 30 | 31 | -spec quit([]) -> ok. 32 | quit(_Extra) -> ok. 33 | 34 | -spec init_per_testcase(atom(), []) -> ok. 35 | init_per_testcase(_Function, _Extra) -> ok. 36 | 37 | -spec quit_per_testcase(atom(), []) -> ok. 38 | quit_per_testcase(_Function, _Extra) -> ok. 39 | 40 | -spec init_per_round(atom(), [binary()], []) -> ok. 41 | init_per_round(publish, Keys, _Extra) -> 42 | lists:foreach( 43 | fun(Key) -> 44 | P = proc_lib:spawn( 45 | fun() -> 46 | ok = edis_pubsub:add_sup_handler(), 47 | receive 48 | stop -> edis_pubsub:delete_handler() 49 | end 50 | end), 51 | Name = binary_to_atom(<<"pubsub-bench-", Key/binary>>, utf8), 52 | case erlang:whereis(Name) of 53 | undefined -> true; 54 | _ -> erlang:unregister(Name) 55 | end, 56 | erlang:register(Name, P) 57 | end, Keys), 58 | wait_for_handlers(length(Keys)); 59 | init_per_round(_Fun, _Keys, _Extra) -> ok. 60 | 61 | -spec quit_per_round(atom(), [binary()], []) -> ok. 62 | quit_per_round(publish, Keys, _Extra) -> 63 | lists:foreach( 64 | fun(Key) -> 65 | binary_to_atom(<<"pubsub-bench-", Key/binary>>, utf8) ! stop 66 | end, Keys), 67 | wait_for_handlers(0); 68 | quit_per_round(_Fun, _Keys, _Extra) -> ok. 69 | 70 | -spec psubscribe([binary()]) -> {[term()], gb_set()}. 71 | psubscribe(Patterns) -> 72 | lists:foldl( 73 | fun(Pattern, {_Result, AccPatternSet}) -> 74 | NextPatternSet = gb_sets:add_element(Pattern, AccPatternSet), 75 | {[<<"psubscribe">>, Pattern, gb_sets:size(NextPatternSet)], NextPatternSet} 76 | end, {nothing, gb_sets:empty()}, Patterns). 77 | 78 | -spec punsubscribe([binary()]) -> {[term()], gb_set()}. 79 | punsubscribe(Patterns) -> 80 | InitialState = psubscribe(lists:map(fun edis_util:integer_to_binary/1, lists:seq(1, 10000))), 81 | lists:foldl( 82 | fun(Pattern, {_Result, AccPatternSet}) -> 83 | NextPatternSet = gb_sets:del_element(Pattern, AccPatternSet), 84 | {[<<"psubscribe">>, Pattern, gb_sets:size(NextPatternSet)], NextPatternSet} 85 | end, InitialState, Patterns). 86 | 87 | -spec publish([binary()]) -> non_neg_integer(). 88 | publish([Key|_]) -> 89 | edis_pubsub:notify(#edis_message{channel = Key, message = Key}), 90 | edis_pubsub:count_handlers(). 91 | 92 | -spec subscribe([binary()]) -> {[term()], gb_set()}. 93 | subscribe(Channels) -> 94 | lists:foldl( 95 | fun(Channel, {_Result, AccChannelSet}) -> 96 | NextChannelSet = gb_sets:add_element(Channel, AccChannelSet), 97 | {[<<"subscribe">>, Channel, gb_sets:size(NextChannelSet)], NextChannelSet} 98 | end, {nothing, gb_sets:empty()}, Channels). 99 | 100 | -spec unsubscribe([binary()]) -> {[term()], gb_set()}. 101 | unsubscribe(Channels) -> 102 | InitialState = subscribe(lists:map(fun edis_util:integer_to_binary/1, lists:seq(1, 10000))), 103 | lists:foldl( 104 | fun(Channel, {_Result, AccChannelSet}) -> 105 | NextChannelSet = gb_sets:del_element(Channel, AccChannelSet), 106 | {[<<"subscribe">>, Channel, gb_sets:size(NextChannelSet)], NextChannelSet} 107 | end, InitialState, Channels). 108 | 109 | wait_for_handlers(N) -> 110 | case edis_pubsub:count_handlers() of 111 | N -> ok; 112 | _ -> timer:sleep(100), wait_for_handlers(N) 113 | end. -------------------------------------------------------------------------------- /src/edis_node_monitor.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% File : edis_node_monitor.erl 3 | %%% Author : Joachim Nilsson 4 | %%% Description : A node monitor for multi-node support 5 | %%% 6 | %%% Created : 22 March 2013 7 | %%%------------------------------------------------------------------- 8 | -module(edis_node_monitor). 9 | 10 | -behaviour(gen_server). 11 | 12 | %% API 13 | -export([start_link/0]). 14 | 15 | %% gen_server callbacks 16 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, 17 | terminate/2, code_change/3]). 18 | 19 | -record(state, {}). 20 | -define(SERVER, ?MODULE). 21 | 22 | %%==================================================================== 23 | %% API 24 | %%==================================================================== 25 | %%-------------------------------------------------------------------- 26 | %% Function: start_link() -> {ok,Pid} | ignore | {error,Error} 27 | %% Description: Starts the server 28 | %%-------------------------------------------------------------------- 29 | start_link() -> 30 | gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). 31 | 32 | 33 | %%==================================================================== 34 | %% gen_server callbacks 35 | %%==================================================================== 36 | 37 | %%-------------------------------------------------------------------- 38 | %% Function: init(Args) -> {ok, State} | 39 | %% {ok, State, Timeout} | 40 | %% ignore | 41 | %% {stop, Reason} 42 | %% Description: Initiates the server 43 | %%-------------------------------------------------------------------- 44 | init([]) -> 45 | ok = net_kernel:monitor_nodes(true, [nodedown_reason]), 46 | net_adm:world(), 47 | {ok, #state{}}. 48 | 49 | %%-------------------------------------------------------------------- 50 | %% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | 51 | %% {reply, Reply, State, Timeout} | 52 | %% {noreply, State} | 53 | %% {noreply, State, Timeout} | 54 | %% {stop, Reason, Reply, State} | 55 | %% {stop, Reason, State} 56 | %% Description: Handling call messages 57 | %%-------------------------------------------------------------------- 58 | handle_call(_Request, _From, State) -> 59 | Reply = ok, 60 | {reply, Reply, State}. 61 | 62 | %%-------------------------------------------------------------------- 63 | %% Function: handle_cast(Msg, State) -> {noreply, State} | 64 | %% {noreply, State, Timeout} | 65 | %% {stop, Reason, State} 66 | %% Description: Handling cast messages 67 | %%-------------------------------------------------------------------- 68 | handle_cast(_Msg, State) -> 69 | 70 | 71 | 72 | 73 | {noreply, State}. 74 | 75 | %%-------------------------------------------------------------------- 76 | %% Function: handle_info(Info, State) -> {noreply, State} | 77 | %% {noreply, State, Timeout} | 78 | %% {stop, Reason, State} 79 | %% Description: Handling all non call/cast messages 80 | %%-------------------------------------------------------------------- 81 | handle_info({Event, Node, Data}, State) -> 82 | 83 | 84 | %% INFO ABOUT NODES SHOULD COME IN HERE. CHECK OUT FORMAT (monitor_nodes?) AND CATCH 85 | %% Is this the format that monitor_nodes will deliver a message? {Event, Node, Data} 86 | 87 | case Event of 88 | nodeup -> 89 | lager:notice("~n~n\t\t~p is UP! (~p)~n~n", [Node, Data]); 90 | nodedown -> 91 | lager:notice("~n~n\t\t~p is DOWN! (~p).", [Node, Data]) 92 | end, 93 | 94 | 95 | {noreply, State}. 96 | 97 | %%-------------------------------------------------------------------- 98 | %% Function: terminate(Reason, State) -> void() 99 | %% Description: This function is called by a gen_server when it is about to 100 | %% terminate. It should be the opposite of Module:init/1 and do any necessary 101 | %% cleaning up. When it returns, the gen_server terminates with Reason. 102 | %% The return value is ignored. 103 | %%-------------------------------------------------------------------- 104 | terminate(_Reason, _State) -> 105 | ok. 106 | 107 | %%-------------------------------------------------------------------- 108 | %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} 109 | %% Description: Convert process state when code is changed 110 | %%-------------------------------------------------------------------- 111 | code_change(_OldVsn, State, _Extra) -> 112 | {ok, State}. 113 | 114 | %%-------------------------------------------------------------------- 115 | %%% Internal functions 116 | %%-------------------------------------------------------------------- -------------------------------------------------------------------------------- /src/edis_lists.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc New lists implementation with O(1) for lists:length/1 and some other 6 | %%% behaviour changes 7 | %%% @end 8 | %%%------------------------------------------------------------------- 9 | -module(edis_lists). 10 | -author('Fernando Benavides '). 11 | -author('Chad DePue '). 12 | 13 | -record(edis_list, {size = 0 :: non_neg_integer(), 14 | list = [] :: list()}). 15 | -opaque edis_list(T) :: #edis_list{list :: [T]}. 16 | -export_type([edis_list/1]). 17 | 18 | -export([length/1, from_list/1, to_list/1, nth/2, nthtail/2, reverse/1, splitwith/2, insert/4, 19 | append/2, push/2, pop/1, sublist/3, filter/2, remove/3, replace_head/2, split/2, empty/0]). 20 | 21 | %% @doc returns an empty edis_list 22 | -spec empty() -> edis_list(_). 23 | empty() -> #edis_list{}. 24 | 25 | %% @doc returns the edis_list size 26 | -spec length(edis_list(_)) -> non_neg_integer(). 27 | length(#edis_list{size = S}) -> S. 28 | 29 | %% @doc returns an edis_list with the same elements of the list 30 | -spec from_list([T]) -> edis_list(T). 31 | from_list(L) -> #edis_list{size = erlang:length(L), list = L}. 32 | 33 | %% @doc returns a list with the same elements of the edis_list 34 | -spec to_list(edis_list(T)) -> [T]. 35 | to_list(#edis_list{list = L}) -> L. 36 | 37 | %% @doc returns the Nth element of the list L 38 | -spec nth(pos_integer(), edis_list(T)) -> T | undefined. 39 | nth(N, #edis_list{size = S}) when S < N -> undefined; 40 | nth(N, #edis_list{list = L}) -> lists:nth(N, L). 41 | 42 | %% @doc returns the Nth tail of the list L 43 | -spec nthtail(non_neg_integer(), edis_list(T)) -> edis_list(T). 44 | nthtail(N, #edis_list{size = S}) when S < N -> []; 45 | nthtail(N, #edis_list{list = L}) -> lists:nthtail(N, L). 46 | 47 | %% @doc reverses all elements in the list L. 48 | -spec reverse(edis_list(T)) -> edis_list(T). 49 | reverse(EL = #edis_list{list = L}) -> EL#edis_list{list = lists:reverse(L)}. 50 | 51 | %% @doc partition edis_list into two edis_lists according to Pred 52 | -spec splitwith(fun((T) -> boolean()), edis_list(T)) -> {edis_list(T), edis_list(T)}. 53 | splitwith(_Pred, #edis_list{size = 0}) -> {#edis_list{}, #edis_list{}}; 54 | splitwith(Pred, #edis_list{size = S, list = L}) -> 55 | {L0, L1} = lists:splitwith(Pred, L), 56 | EL0 = from_list(L0), 57 | {EL0, #edis_list{size = S - EL0#edis_list.size, list = L1}}. 58 | 59 | %% @doc insert a value before or after the pivot 60 | -spec insert(T, before|'after', T, edis_list(T)) -> edis_list(T). 61 | insert(Value, Position, Pivot, EL = #edis_list{size = S, list = L}) -> 62 | case {lists:splitwith(fun(Val) -> Val =/= Pivot end, L), Position} of 63 | {{_, []}, _} -> %% Pivot was not found 64 | EL; 65 | {{Before, After}, before} -> 66 | #edis_list{size = S + 1, list = lists:append(Before, [Value|After])}; 67 | {{Before, [Pivot|After]}, 'after'} -> 68 | #edis_list{size = S + 1, list = lists:append(Before, [Pivot, Value|After])} 69 | end. 70 | 71 | %% @doc appends lists X and Y 72 | -spec append(edis_list(T0), edis_list(T1)) -> edis_list(T0|T1). 73 | append(#edis_list{size = S0, list = L0}, #edis_list{size = S1, list = L1}) -> 74 | #edis_list{size = S0+S1, list = lists:append(L0,L1)}. 75 | 76 | %% @doc inserts X to the left of the list 77 | -spec push(T0, edis_list(T)) -> edis_list(T|T0). 78 | push(X, #edis_list{size = S, list = L}) -> #edis_list{size = S + 1, list = [X|L]}. 79 | 80 | %% @doc pops the leftmost element of the list 81 | %% @throws empty 82 | -spec pop(edis_list(T)) -> {T, edis_list(T)}. 83 | pop(#edis_list{size = 0}) -> throw(empty); 84 | pop(#edis_list{size = S, list = [X|L]}) -> {X, #edis_list{size = S-1, list = L}}. 85 | 86 | %% @doc Returns the sub-list starting at Start of length Length. 87 | -spec sublist(edis_list(T), pos_integer(), non_neg_integer()) -> edis_list(T). 88 | sublist(#edis_list{size = S}, Start, _Len) when Start > S -> #edis_list{}; 89 | sublist(#edis_list{size = S, list = L}, Start, Len) -> 90 | #edis_list{size = erlang:max(S - Start + 1, Len), list = lists:sublist(L, Start, Len)}. 91 | 92 | %% @doc Returns an edis_list of all elements Elem in the received edis_list for which Pred(Elem) returns true. 93 | -spec filter(fun((T) -> boolean()), edis_list(T)) -> edis_list(T). 94 | filter(_Pred, #edis_list{size = 0}) -> #edis_list{}; 95 | filter(Pred, #edis_list{list = L}) -> from_list(lists:filter(Pred, L)). 96 | 97 | %% @doc Removes the first N elements which compares equal to the value 98 | -spec remove(T, non_neg_integer(), edis_list(T)) -> edis_list(T). 99 | remove(_, 0, EL) -> EL; 100 | remove(_, _, #edis_list{size = 0}) -> empty(); 101 | remove(X, Count, EL) -> remove(X, Count, EL#edis_list.list, EL#edis_list.size, []). 102 | 103 | remove(_X, 0, End, Size, RevStart) -> #edis_list{list = lists:reverse(RevStart) ++ End, size = Size}; 104 | remove(_X, _C, [], Size, RevStart) -> #edis_list{list = lists:reverse(RevStart), size = Size}; 105 | remove(X, C, [X|Rest], Size, RevStart) -> remove(X, C-1, Rest, Size-1, RevStart); 106 | remove(X, C, [Y|Rest], Size, RevStart) -> remove(X, C, Rest, Size, [Y|RevStart]). 107 | 108 | %% @doc replaces head element 109 | -spec replace_head(T0, edis_list(T)) -> edis_list(T0|T). 110 | replace_head(H, EL = #edis_list{list = [_|Rest]}) -> EL#edis_list{list = [H|Rest]}. 111 | 112 | %% @doc Splits the received list into two lists. The first list contains the first N elements and the other list the rest of the elements (the Nth tail) 113 | -spec split(non_neg_integer(), edis_list(T)) -> {edis_list(T), edis_list(T)}. 114 | split(N, EL = #edis_list{size = S}) when N > S -> {EL, #edis_list{}}; 115 | split(N, #edis_list{size = S, list = L}) -> 116 | {L0, L1} = lists:split(N, L), 117 | {#edis_list{size = N, list = L0}, #edis_list{size = S - N, list = L1}}. 118 | -------------------------------------------------------------------------------- /test/benchmarks/hashes_bench.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc Benchmarks for hashes commands 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(hashes_bench). 9 | -author('Fernando Benavides '). 10 | -author('Chad DePue '). 11 | 12 | -behaviour(edis_bench). 13 | 14 | -define(KEY, <<"test-hash">>). 15 | 16 | -include("edis.hrl"). 17 | 18 | -export([all/0, 19 | init/1, init_per_testcase/2, init_per_round/3, 20 | quit/1, quit_per_testcase/2, quit_per_round/3]). 21 | -export([hdel/1, hexists/1, hget/1, hgetall/1, hincrby/1, hkeys/1, hlen/1, hmget/1, hmset/1, 22 | hset/1, hsetnx/1, hvals/1]). 23 | 24 | %% ==================================================================== 25 | %% External functions 26 | %% ==================================================================== 27 | -spec all() -> [atom()]. 28 | all() -> [Fun || {Fun, _} <- ?MODULE:module_info(exports) -- edis_bench:behaviour_info(callbacks), 29 | Fun =/= module_info]. 30 | 31 | -spec init([]) -> ok. 32 | init(_Extra) -> ok. 33 | 34 | -spec quit([]) -> ok. 35 | quit(_Extra) -> ok. 36 | 37 | -spec init_per_testcase(atom(), []) -> ok. 38 | init_per_testcase(_Function, _Extra) -> ok. 39 | 40 | -spec quit_per_testcase(atom(), []) -> ok. 41 | quit_per_testcase(_Function, _Extra) -> ok. 42 | 43 | -spec init_per_round(atom(), [binary()], []) -> ok. 44 | init_per_round(incrby, Keys, _Extra) -> 45 | _ = edis_db:run( 46 | edis_db:process(0), 47 | #edis_command{cmd = <<"HSET">>, 48 | args = [?KEY, ?KEY, edis_util:integer_to_binary(length(Keys))], 49 | result_type = boolean, group = hashes}), 50 | ok; 51 | init_per_round(Fun, Keys, _Extra) when Fun =:= hgetall; 52 | Fun =:= hkeys; 53 | Fun =:= hvals; 54 | Fun =:= hlen -> 55 | _ = 56 | edis_db:run( 57 | edis_db:process(0), 58 | #edis_command{cmd = <<"HMSET">>, args = [?KEY, [{Key, <<"x">>} || Key <- Keys]], 59 | group = hashes, result_type = ok}), 60 | ok; 61 | init_per_round(Fun, _Keys, _Extra) when Fun =:= hmget; 62 | Fun =:= hmset -> 63 | _ = 64 | edis_db:run( 65 | edis_db:process(0), 66 | #edis_command{cmd = <<"HMSET">>, 67 | args = [?KEY, 68 | [{edis_util:integer_to_binary(Key), <<"x">>} || Key <- lists:seq(1, 5000)]], 69 | group = hashes, result_type = ok}), 70 | ok; 71 | init_per_round(_Fun, Keys, _Extra) -> 72 | _ = 73 | edis_db:run( 74 | edis_db:process(0), 75 | #edis_command{cmd = <<"HMSET">>, args = [?KEY, [{Key, <<"x">>} || Key <- Keys] ++ 76 | [{<>, <<"y">>} || Key <- Keys]], 77 | group = hashes, result_type = ok}), 78 | ok. 79 | 80 | -spec quit_per_round(atom(), [binary()], []) -> ok. 81 | quit_per_round(_, _Keys, _Extra) -> 82 | _ = edis_db:run( 83 | edis_db:process(0), 84 | #edis_command{cmd = <<"DEL">>, args = [?KEY], group = keys, result_type = number} 85 | ), 86 | ok. 87 | 88 | -spec hdel([binary()]) -> pos_integer(). 89 | hdel(Keys) -> 90 | edis_db:run( 91 | edis_db:process(0), 92 | #edis_command{cmd = <<"HDEL">>, args = [?KEY | Keys], group = hashes, result_type = number}). 93 | 94 | -spec hexists([binary(),...]) -> boolean(). 95 | hexists([Key|_]) -> 96 | edis_db:run( 97 | edis_db:process(0), 98 | #edis_command{cmd = <<"HEXISTS">>, args = [?KEY, Key], result_type = boolean, group = hashes}). 99 | 100 | -spec hget([binary()]) -> binary(). 101 | hget([Key|_]) -> 102 | edis_db:run( 103 | edis_db:process(0), 104 | #edis_command{cmd = <<"HGET">>, args = [?KEY, Key], result_type = bulk, group = hashes}). 105 | 106 | -spec hgetall([binary()]) -> binary(). 107 | hgetall(_Keys) -> 108 | edis_db:run( 109 | edis_db:process(0), 110 | #edis_command{cmd = <<"HGETALL">>, args = [?KEY], result_type = multi_bulk, group = hashes}). 111 | 112 | -spec hincrby([binary()]) -> integer(). 113 | hincrby(Keys) -> 114 | edis_db:run( 115 | edis_db:process(0), 116 | #edis_command{cmd = <<"HINCRBY">>, args = [?KEY, ?KEY, length(Keys)], 117 | group = hashes, result_type = number}). 118 | 119 | -spec hkeys([binary()]) -> binary(). 120 | hkeys(_Keys) -> 121 | edis_db:run( 122 | edis_db:process(0), 123 | #edis_command{cmd = <<"HKEYS">>, args = [?KEY], result_type = multi_bulk, group = hashes}). 124 | 125 | -spec hlen([binary()]) -> binary(). 126 | hlen(_Keys) -> 127 | edis_db:run( 128 | edis_db:process(0), 129 | #edis_command{cmd = <<"HLEN">>, args = [?KEY], result_type = number, group = hashes}). 130 | 131 | -spec hmget([binary()]) -> binary(). 132 | hmget(Keys) -> 133 | edis_db:run( 134 | edis_db:process(0), 135 | #edis_command{cmd = <<"HMGET">>, args = [?KEY | Keys], result_type = multi_bulk, group = hashes}). 136 | 137 | -spec hmset([binary()]) -> binary(). 138 | hmset(Keys) -> 139 | edis_db:run( 140 | edis_db:process(0), 141 | #edis_command{cmd = <<"HMSET">>, args = [?KEY, [{Key, <<"y">>} || Key <- Keys]], 142 | result_type = ok, group = hashes}). 143 | 144 | -spec hset([binary()]) -> binary(). 145 | hset([Key|_]) -> 146 | edis_db:run( 147 | edis_db:process(0), 148 | #edis_command{cmd = <<"HSET">>, args = [?KEY, Key, Key], result_type = boolean, group = hashes}). 149 | 150 | -spec hsetnx([binary()]) -> binary(). 151 | hsetnx([Key|_]) -> 152 | edis_db:run( 153 | edis_db:process(0), 154 | #edis_command{cmd = <<"HSETNX">>, args = [?KEY, 155 | case random:uniform(2) of 156 | 1 -> Key; 157 | 2 -> <> 158 | end, Key], result_type = boolean, group = hashes}). 159 | 160 | -spec hvals([binary()]) -> binary(). 161 | hvals(_Keys) -> 162 | edis_db:run( 163 | edis_db:process(0), 164 | #edis_command{cmd = <<"HVALS">>, args = [?KEY], result_type = multi_bulk, group = hashes}). -------------------------------------------------------------------------------- /src/edis_listener.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc Listener process for clients 6 | %%% @reference See this article for more information, and liberally borrowed from Fernando Benavides' code - fernando@inakanetworks.com 7 | %%% @end 8 | %%%------------------------------------------------------------------- 9 | -module(edis_listener). 10 | -author('Chad DePue '). 11 | -author('Fernando Benavides '). 12 | 13 | -include("edis.hrl"). 14 | 15 | -behaviour(gen_server). 16 | 17 | %% ------------------------------------------------------------------- 18 | %% Exported functions 19 | %% ------------------------------------------------------------------- 20 | -export([start_link/1]). 21 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). 22 | 23 | -define(TCP_OPTIONS,[binary, 24 | {packet, line}, 25 | {keepalive, true}, 26 | {active, false}, 27 | {reuseaddr, true}, 28 | {nodelay, true}, %% We want to be informed even when packages are small 29 | {backlog, 128000}, %% We don't care if we have logs of pending connections, we'll process them anyway 30 | {send_timeout, edis_config:get(client_timeout)}, %% If we couldn't send a message in this interval, something is definitively wrong... 31 | {send_timeout_close, true} %%... and therefore the connection should be closed 32 | ]). 33 | 34 | -record(state, {listener :: port(), % Listening socket 35 | acceptor :: term() % Asynchronous acceptor's internal reference 36 | }). 37 | 38 | %% ==================================================================== 39 | %% External functions 40 | %% ==================================================================== 41 | 42 | %% @doc Starts a new client listener 43 | -spec start_link(pos_integer()) -> {ok, pid()}. 44 | start_link(Port) -> 45 | gen_server:start_link( 46 | {local, list_to_atom("edis-client-listener-" ++ integer_to_list(Port))}, ?MODULE, Port, []). 47 | 48 | %% ==================================================================== 49 | %% Callback functions 50 | %% ==================================================================== 51 | %% @hidden 52 | -spec init(pos_integer()) -> {ok, #state{}} | {stop, term()}. 53 | init(Port) -> 54 | case gen_tcp:listen(Port, ?TCP_OPTIONS) of 55 | {ok, Socket} -> 56 | {ok, Ref} = prim_inet:async_accept(Socket, -1), 57 | lager:info("Client listener initialized (listening on port ~p)~n", [Port]), 58 | {ok, #state{listener = Socket, 59 | acceptor = Ref}}; 60 | {error, Reason} -> 61 | lager:alert("Client listener couldn't listen to port ~p: ~p~n", [Port, Reason]), 62 | {stop, Reason} 63 | end. 64 | 65 | %% @hidden 66 | -spec handle_call(X, reference(), #state{}) -> {stop, {unknown_request, X}, {unknown_request, X}, #state{}}. 67 | handle_call(Request, _From, State) -> 68 | {stop, {unknown_request, Request}, {unknown_request, Request}, State}. 69 | 70 | %% @hidden 71 | -spec handle_cast(any(), #state{}) -> {noreply, #state{}, hibernate}. 72 | handle_cast(_Msg, State) -> {noreply, State, hibernate}. 73 | 74 | %% @hidden 75 | -spec handle_info(any(), #state{}) -> {noreply, #state{}, hibernate}. 76 | handle_info({inet_async, ListSock, Ref, {ok, CliSocket}}, 77 | #state{listener = ListSock, acceptor = Ref} = State) -> 78 | try 79 | PeerPort = 80 | case inet:peername(CliSocket) of 81 | {ok, {_Ip, Port}} -> Port; 82 | PeerErr -> PeerErr 83 | end, 84 | case set_sockopt(ListSock, CliSocket) of 85 | ok -> 86 | void; 87 | {error, Reason} -> 88 | exit({set_sockopt, Reason}) 89 | end, 90 | 91 | %% New client connected - spawn a new process using the simple_one_for_one supervisor. 92 | lager:debug("Client ~p starting...~n", [PeerPort]), 93 | {ok, Pid} = edis_client_sup:start_client(), 94 | 95 | ok = gen_tcp:controlling_process(CliSocket, Pid), 96 | 97 | %% Instruct the new FSM that it owns the socket. 98 | ok = edis_client:set_socket(Pid, CliSocket), 99 | 100 | %% Signal the network driver that we are ready to accept another connection 101 | NewRef = 102 | case prim_inet:async_accept(ListSock, -1) of 103 | {ok, NR} -> 104 | NR; 105 | {error, Err} -> 106 | lager:alert("Couldn't accept: ~p~n", [inet:format_error(Err)]), 107 | exit({async_accept, inet:format_error(Err)}) 108 | end, 109 | 110 | {noreply, State#state{acceptor = NewRef}} 111 | catch 112 | exit:Error -> 113 | lager:error("Error in async accept: ~p.\n", [Error]), 114 | {stop, Error, State} 115 | end; 116 | handle_info({inet_async, ListSock, Ref, Error}, 117 | #state{listener = ListSock, acceptor = Ref} = State) -> 118 | lager:alert("Error in socket acceptor: ~p.\n", [Error]), 119 | {stop, Error, State}; 120 | handle_info(_Info, State) -> 121 | {noreply, State, hibernate}. 122 | 123 | %% @hidden 124 | -spec terminate(any(), #state{}) -> any(). 125 | terminate(Reason, _State) -> 126 | lager:info("Listener terminated: ~p~n", [Reason]), 127 | ok. 128 | 129 | %% @hidden 130 | -spec code_change(any(), any(), any()) -> {ok, any()}. 131 | code_change(_OldVsn, State, _Extra) -> {ok, State}. 132 | 133 | %% ==================================================================== 134 | %% Internal functions 135 | %% ==================================================================== 136 | %% @doc Taken from prim_inet. We are merely copying some socket options from the 137 | %% listening socket to the new client socket. 138 | -spec set_sockopt(port(), port()) -> ok | {error, Reason :: term()}. 139 | set_sockopt(ListSock, CliSocket) -> 140 | true = inet_db:register_socket(CliSocket, inet_tcp), 141 | case prim_inet:getopts(ListSock, [active, nodelay, keepalive, delay_send, priority, tos]) of 142 | {ok, Opts} -> 143 | case prim_inet:setopts(CliSocket, Opts) of 144 | ok -> 145 | ok; 146 | Error -> 147 | gen_tcp:close(CliSocket), 148 | Error 149 | end; 150 | Error -> 151 | gen_tcp:close(CliSocket), 152 | Error 153 | end. -------------------------------------------------------------------------------- /test/benchmarks/erldis_hashes_bench.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc Benchmarks for hashes commands using erldis 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(erldis_hashes_bench). 9 | -author('Fernando Benavides '). 10 | -author('Chad DePue '). 11 | 12 | -behaviour(edis_bench). 13 | 14 | -define(KEY, <<"test-hash">>). 15 | 16 | -include("edis.hrl"). 17 | -include("edis_bench.hrl"). 18 | 19 | -export([bench/1, bench/2, bench/4, bench_all/0, bench_all/1, bench_all/3]). 20 | -export([all/0, 21 | init/1, init_per_testcase/2, init_per_round/3, 22 | quit/1, quit_per_testcase/2, quit_per_round/3]). 23 | -export([hdel/2, hexists/2, hget/2, hgetall/2, hincrby/2, hkeys/2, hlen/2, hmget/2, hmset/2, 24 | hset/2, hsetnx/2, hvals/2]). 25 | 26 | %% ==================================================================== 27 | %% External functions 28 | %% ==================================================================== 29 | -spec bench_all() -> [{atom(), float()}]. 30 | bench_all() -> 31 | lists:map(fun(F) -> 32 | io:format("Benchmarking ~p...~n", [F]), 33 | Bench = bench(F), 34 | io:format("~n~n\t~p: ~p~n", [F, Bench]), 35 | {F, Bench} 36 | end, all()). 37 | 38 | -spec bench_all([edis_bench:option()]) -> [{atom(), float()}]. 39 | bench_all(Options) -> 40 | lists:map(fun(F) -> 41 | io:format("Benchmarking ~p...~n", [F]), 42 | Bench = bench(F, Options), 43 | io:format("~n~n\t~p: ~p~n", [F, Bench]), 44 | {F, Bench} 45 | end, all()). 46 | 47 | -spec bench_all(pos_integer(), pos_integer(), [edis_bench:option()]) -> [{atom(), float()}]. 48 | bench_all(P1, P2, Options) -> 49 | lists:map(fun(F) -> 50 | io:format("Benchmarking ~p...~n", [F]), 51 | Bench = bench(F, P1, P2, Options), 52 | io:format("~n~n\t~p: ~p~n", [F, Bench]), 53 | {F, Bench} 54 | end, all()). 55 | 56 | -spec bench(atom()) -> ok. 57 | bench(Function) -> bench(Function, []). 58 | 59 | -spec bench(atom(), [edis_bench:option()]) -> ok. 60 | bench(Function, Options) -> bench(Function, 6380, 6379, Options). 61 | 62 | -spec bench(atom(), pos_integer(), pos_integer(), [edis_bench:option()]) -> float(). 63 | bench(Function, P1, P2, Options) -> 64 | edis_bench:bench({?MODULE, Function, [P1]}, {?MODULE, Function, [P2]}, 65 | Options ++ 66 | [{outliers,100}, {symbols, #symbols{down_down = $x, 67 | up_up = $x, 68 | up_down = $x, 69 | down_up = $x, 70 | down_none = $e, 71 | up_none = $e, 72 | none_down = $r, 73 | none_up = $r}}]). 74 | 75 | -spec all() -> [atom()]. 76 | all() -> [Fun || {Fun, _} <- ?MODULE:module_info(exports) -- edis_bench:behaviour_info(callbacks), 77 | Fun =/= module_info, Fun =/= module_info, Fun =/= bench_all, Fun =/= bench]. 78 | 79 | -spec init([pos_integer()]) -> ok. 80 | init([Port]) -> 81 | case erldis:connect(localhost,Port) of 82 | {ok, Client} -> 83 | Name = process(Port), 84 | case erlang:whereis(Name) of 85 | undefined -> true; 86 | _ -> erlang:unregister(Name) 87 | end, 88 | erlang:register(Name, Client), 89 | ok; 90 | Error -> throw(Error) 91 | end. 92 | 93 | -spec quit([pos_integer()]) -> ok. 94 | quit([Port]) -> 95 | Name = process(Port), 96 | case erlang:whereis(Name) of 97 | undefined -> ok; 98 | Client -> erldis_client:stop(Client) 99 | end, 100 | ok. 101 | 102 | -spec init_per_testcase(atom(), [pos_integer()]) -> ok. 103 | init_per_testcase(_Function, _Extra) -> ok. 104 | 105 | -spec quit_per_testcase(atom(), [pos_integer()]) -> ok. 106 | quit_per_testcase(_Function, _Extra) -> ok. 107 | 108 | -spec init_per_round(atom(), [binary()], [pos_integer()]) -> ok. 109 | init_per_round(incrby, Keys, [Port]) -> 110 | _ = erldis:hset(process(Port), ?KEY, ?KEY, edis_util:integer_to_binary(length(Keys))), 111 | ok; 112 | init_per_round(Fun, Keys, [Port]) when Fun =:= hgetall; 113 | Fun =:= hkeys; 114 | Fun =:= hvals; 115 | Fun =:= hlen -> 116 | erldis:hmset(process(Port), ?KEY, [{Key, <<"x">>} || Key <- Keys]); 117 | init_per_round(Fun, _Keys, [Port]) when Fun =:= hmget; Fun =:= hmset -> 118 | erldis:hmset(process(Port), 119 | ?KEY, [{edis_util:integer_to_binary(Key), <<"x">>} || Key <- lists:seq(1, 5000)]); 120 | init_per_round(_Fun, Keys, [Port]) -> 121 | erldis:hmset(process(Port), 122 | ?KEY, [{Key, <<"x">>} || Key <- Keys] ++ 123 | [{<>, <<"y">>} || Key <- Keys]). 124 | 125 | -spec quit_per_round(atom(), [binary()], [pos_integer()]) -> ok. 126 | quit_per_round(_, _Keys, [Port]) -> 127 | _ = erldis:del(process(Port), ?KEY), 128 | ok. 129 | 130 | 131 | -spec hdel([binary()], pos_integer()) -> pos_integer(). 132 | hdel(Keys, Port) -> erldis:hdel(process(Port), ?KEY, Keys). 133 | 134 | -spec hexists([binary(),...], pos_integer()) -> boolean(). 135 | hexists([Key|_], Port) -> erldis:hexists(process(Port), ?KEY, Key). 136 | 137 | -spec hget([binary()], pos_integer()) -> binary(). 138 | hget([Key|_], Port) -> erldis:hget(process(Port), ?KEY, Key). 139 | 140 | -spec hgetall([binary()], pos_integer()) -> binary(). 141 | hgetall(_Keys, Port) -> erldis:hgetall(process(Port), ?KEY). 142 | 143 | -spec hincrby([binary()], pos_integer()) -> integer(). 144 | hincrby(Keys, Port) -> erldis:hincrby(process(Port), ?KEY, ?KEY, length(Keys)). 145 | 146 | -spec hkeys([binary()], pos_integer()) -> binary(). 147 | hkeys(_Keys, Port) -> erldis:hkeys(process(Port), ?KEY). 148 | 149 | -spec hlen([binary()], pos_integer()) -> binary(). 150 | hlen(_Keys, Port) -> erldis:hlen(process(Port), ?KEY). 151 | 152 | -spec hmget([binary()], pos_integer()) -> binary(). 153 | hmget(Keys, Port) -> erldis:hmget(process(Port), ?KEY, Keys). 154 | 155 | -spec hmset([binary()], pos_integer()) -> binary(). 156 | hmset(Keys, Port) -> erldis:hmset(process(Port), ?KEY, [{Key, <<"y">>} || Key <- Keys]). 157 | 158 | -spec hset([binary()], pos_integer()) -> binary(). 159 | hset([Key|_], Port) -> erldis:hset(process(Port), ?KEY, Key, Key). 160 | 161 | -spec hsetnx([binary()], pos_integer()) -> binary(). 162 | hsetnx([Key|_], Port) -> erldis:hsetnx(process(Port), ?KEY, case random:uniform(2) of 163 | 1 -> Key; 164 | 2 -> <> 165 | end, Key). 166 | 167 | -spec hvals([binary()], pos_integer()) -> binary(). 168 | hvals(_Keys, Port) -> erldis:hvals(process(Port), ?KEY). 169 | 170 | process(Port) -> list_to_atom("erldis-tester-" ++ integer_to_list(Port)). -------------------------------------------------------------------------------- /test/benchmarks/strings_bench.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc Benchmarks for string commands 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(strings_bench). 9 | -author('Fernando Benavides '). 10 | -author('Chad DePue '). 11 | 12 | -behaviour(edis_bench). 13 | 14 | -include("edis.hrl"). 15 | 16 | -define(KEY, <<"test-string">>). 17 | 18 | -export([all/0, 19 | init/1, init_per_testcase/2, init_per_round/3, 20 | quit/1, quit_per_testcase/2, quit_per_round/3]). 21 | -export([append/1, decr/1, decrby/1, get/1, getbit/1, getrange/1, getset/1, incr/1, incrby/1, 22 | mget/1, mset/1, msetnx/1, set/1, setex/1, setnx/1, setbit/1, setrange/1, strlen/1]). 23 | 24 | %% ==================================================================== 25 | %% External functions 26 | %% ==================================================================== 27 | -spec all() -> [atom()]. 28 | all() -> [Fun || {Fun, _} <- ?MODULE:module_info(exports) -- edis_bench:behaviour_info(callbacks), 29 | Fun =/= module_info]. 30 | 31 | -spec init([]) -> ok. 32 | init(_Extra) -> ok. 33 | 34 | -spec quit([]) -> ok. 35 | quit(_Extra) -> ok. 36 | 37 | -spec init_per_testcase(atom(), []) -> ok. 38 | init_per_testcase(_Function, _Extra) -> ok. 39 | 40 | -spec quit_per_testcase(atom(), []) -> ok. 41 | quit_per_testcase(_Function, _Extra) -> ok. 42 | 43 | -spec init_per_round(atom(), [binary()], []) -> ok. 44 | init_per_round(Fun, Keys, _Extra) when Fun =:= decr; 45 | Fun =:= decrby; 46 | Fun =:= incr; 47 | Fun =:= incrby -> 48 | edis_db:run( 49 | edis_db:process(0), 50 | #edis_command{cmd = <<"SET">>, 51 | args = [?KEY, edis_util:integer_to_binary(length(Keys))], 52 | result_type = ok, group = strings}); 53 | init_per_round(mget, Keys, _Extra) -> 54 | edis_db:run( 55 | edis_db:process(0), 56 | #edis_command{cmd = <<"MSET">>, args = [{Key, <<"X">>} || Key <- Keys], 57 | result_type = ok, group = strings}); 58 | init_per_round(Fun, Keys, _Extra) when Fun =:= mset; 59 | Fun =:= msetnx -> 60 | _ = edis_db:run( 61 | edis_db:process(0), 62 | #edis_command{cmd = <<"DEL">>, args = Keys, result_type = ok, group = keys}), 63 | ok; 64 | init_per_round(Fun, _Keys, _Extra) when Fun =:= set; 65 | Fun =:= setex; 66 | Fun =:= setnx-> 67 | _ = edis_db:run( 68 | edis_db:process(0), 69 | #edis_command{cmd = <<"DEL">>, args = [?KEY], result_type = ok, group = keys}), 70 | ok; 71 | init_per_round(_Fun, Keys, _Extra) -> 72 | edis_db:run( 73 | edis_db:process(0), 74 | #edis_command{cmd = <<"SET">>, 75 | args = [?KEY, list_to_binary(lists:duplicate(length(Keys), $X))], 76 | result_type = ok, group = strings}). 77 | 78 | -spec quit_per_round(atom(), [binary()], []) -> ok. 79 | quit_per_round(_, _Keys, _Extra) -> ok. 80 | 81 | -spec append([binary()]) -> pos_integer(). 82 | append([Key|_]) -> 83 | edis_db:run( 84 | edis_db:process(0), 85 | #edis_command{cmd = <<"APPEND">>, args = [?KEY, Key], 86 | group = strings, result_type = number}). 87 | 88 | -spec decr([binary()]) -> integer(). 89 | decr(_) -> 90 | edis_db:run( 91 | edis_db:process(0), 92 | #edis_command{cmd = <<"DECR">>, args = [?KEY], 93 | group = strings, result_type = number}). 94 | 95 | -spec decrby([binary()]) -> integer(). 96 | decrby(Keys) -> 97 | edis_db:run( 98 | edis_db:process(0), 99 | #edis_command{cmd = <<"DECRBY">>, args = [?KEY, length(Keys)], 100 | group = strings, result_type = number}). 101 | 102 | -spec get([binary()]) -> binary(). 103 | get(_) -> 104 | edis_db:run( 105 | edis_db:process(0), 106 | #edis_command{cmd = <<"GET">>, args = [?KEY], 107 | group = strings, result_type = bulk}). 108 | 109 | -spec getbit([binary()]) -> 1 | 0. 110 | getbit(Keys) -> 111 | edis_db:run( 112 | edis_db:process(0), 113 | #edis_command{cmd = <<"GETBIT">>, args = [?KEY, 114 | random:uniform(length(Keys)) - 1], 115 | group = strings, result_type = number}). 116 | 117 | -spec getrange([binary()]) -> binary(). 118 | getrange(_) -> 119 | edis_db:run( 120 | edis_db:process(0), 121 | #edis_command{cmd = <<"GETRANGE">>, args = [?KEY, 1, -2], 122 | group = strings, result_type = number}). 123 | 124 | -spec getset([binary()]) -> binary(). 125 | getset([Key|_]) -> 126 | edis_db:run( 127 | edis_db:process(0), 128 | #edis_command{cmd = <<"GETSET">>, args = [?KEY, Key], 129 | group = strings, result_type = bulk}). 130 | 131 | -spec incr([binary()]) -> integer(). 132 | incr(_) -> 133 | edis_db:run( 134 | edis_db:process(0), 135 | #edis_command{cmd = <<"INCR">>, args = [?KEY], 136 | group = strings, result_type = number}). 137 | 138 | -spec incrby([binary()]) -> integer(). 139 | incrby(Keys) -> 140 | edis_db:run( 141 | edis_db:process(0), 142 | #edis_command{cmd = <<"INCRBY">>, args = [?KEY, length(Keys)], 143 | group = strings, result_type = number}). 144 | 145 | -spec mget([binary()]) -> [binary()]. 146 | mget(Keys) -> 147 | edis_db:run( 148 | edis_db:process(0), 149 | #edis_command{cmd = <<"MGET">>, args = Keys, group = strings, result_type = multi_bulk}). 150 | 151 | -spec mset([binary()]) -> ok. 152 | mset(Keys) -> 153 | edis_db:run( 154 | edis_db:process(0), 155 | #edis_command{cmd = <<"MSET">>, args = [{Key, <<"X">>} || Key <- Keys], 156 | result_type = ok, group = strings}). 157 | 158 | -spec msetnx([binary()]) -> ok. 159 | msetnx(Keys) -> 160 | edis_db:run( 161 | edis_db:process(0), 162 | #edis_command{cmd = <<"MSET">>, args = [{Key, <<"X">>} || Key <- Keys], 163 | result_type = ok, group = strings}). 164 | 165 | -spec set([binary()]) -> ok. 166 | set(Keys) -> 167 | edis_db:run( 168 | edis_db:process(0), 169 | #edis_command{cmd = <<"SET">>, 170 | args = [?KEY, unicode:characters_to_binary(lists:duplicate(length(Keys), $X))], 171 | result_type = ok, group = strings}). 172 | 173 | -spec setex([binary()]) -> ok. 174 | setex(Keys) -> 175 | edis_db:run( 176 | edis_db:process(0), 177 | #edis_command{cmd = <<"SETEX">>, 178 | args = [?KEY, length(Keys), unicode:characters_to_binary(lists:duplicate(length(Keys), $X))], 179 | result_type = ok, group = strings}). 180 | 181 | -spec setnx([binary()]) -> ok. 182 | setnx(Keys) -> 183 | edis_db:run( 184 | edis_db:process(0), 185 | #edis_command{cmd = <<"SETNX">>, 186 | args = [?KEY, unicode:characters_to_binary(lists:duplicate(length(Keys), $X))], 187 | result_type = boolean, group = strings}). 188 | 189 | -spec setbit([binary()]) -> 1 | 0. 190 | setbit(Keys) -> 191 | edis_db:run( 192 | edis_db:process(0), 193 | #edis_command{cmd = <<"SETBIT">>, args = [?KEY, random:uniform(length(Keys)) - 1, 194 | random:uniform(2) - 1], 195 | group = strings, result_type = number}). 196 | 197 | -spec setrange([binary()]) -> pos_integer(). 198 | setrange(Keys) -> 199 | edis_db:run( 200 | edis_db:process(0), 201 | #edis_command{cmd = <<"SETRANGE">>, args = [?KEY, random:uniform(length(Keys)) - 1, <<"xxx">>], 202 | group = strings, result_type = number}). 203 | 204 | -spec strlen([binary()]) -> pos_integer(). 205 | strlen(_Keys) -> 206 | edis_db:run( 207 | edis_db:process(0), 208 | #edis_command{cmd = <<"STRLEN">>, args = [?KEY], group = strings, result_type = number}). -------------------------------------------------------------------------------- /test/keys_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% Author: Joachim Nilsson joachim@inakanetworks.com 2 | 3 | %% SCOPE: 4 | %% OBJECT subtypes "refcount" and "encoding" are not tested, only "idletime" 5 | %% SORT has not been covered, very complicated 6 | 7 | 8 | -module(keys_SUITE). 9 | -compile(export_all). 10 | -include_lib("common_test/include/ct.hrl"). 11 | 12 | all() -> 13 | [exists_del, object_idletime, expire_ttl, expireat, persist, rename, renamenx, type, move, pattern, randomkey]. 14 | 15 | init_per_suite(Config) -> 16 | {ok,Client} = connect_erldis(10), 17 | NewConfig = lists:keystore(client,1,Config,{client,Client}), 18 | NewConfig. 19 | 20 | init_per_testcase(_TestCase,Config) -> 21 | {client,Client} = lists:keyfind(client, 1, Config), 22 | erldis_client:sr_scall(Client,[<<"flushdb">>]), 23 | ok = erldis_client:sr_scall(Client,[<<"set">>,<<"string">>,<<"I'm not a list">>]), 24 | Config. 25 | 26 | end_per_suite(_Config) -> 27 | ok. 28 | 29 | connect_erldis(0) -> {error,{socket_error,econnrefused}}; 30 | connect_erldis(Times) -> 31 | timer:sleep(2000), 32 | case erldis:connect(localhost,16380) of 33 | {ok,Client} -> {ok,Client}; 34 | _ -> connect_erldis(Times - 1) 35 | end. 36 | 37 | generate_random(Amount, Client) -> generate_random(Amount,[], Client). 38 | generate_random(0, Acc, _Client) -> Acc; 39 | generate_random(Amount, Acc, Client) -> 40 | NewAcc = [erldis_client:sr_scall(Client,[<<"randomkey">>]) | Acc], 41 | generate_random(Amount-1, NewAcc, Client). 42 | 43 | 44 | 45 | 46 | 47 | %%%%%%%%%%% 48 | %% TESTS %% 49 | %%%%%%%%%%% 50 | 51 | exists_del(Config)-> 52 | {client,Client} = lists:keyfind(client, 1, Config), 53 | 54 | ok = erldis_client:sr_scall(Client,[<<"set">>,<<"key1">>,<<"hello">>]), 55 | ok = erldis_client:sr_scall(Client,[<<"set">>,<<"key2">>,<<"world">>]), 56 | true = erldis_client:sr_scall(Client,[<<"exists">>,<<"key1">>]), 57 | 2 = erldis_client:sr_scall(Client,[<<"del">>,<<"key1">>, <<"key2">>]), 58 | false = erldis_client:sr_scall(Client,[<<"exists">>,<<"key1">>]). 59 | 60 | 61 | object_idletime(Config)-> 62 | {client,Client} = lists:keyfind(client, 1, Config), 63 | 64 | ok = erldis_client:sr_scall(Client,[<<"set">>,<<"key1">>,<<"hello">>]), 65 | timer:sleep(2000), 66 | 2 = erldis_client:sr_scall(Client,[<<"object">>,<<"idletime">>,<<"key1">>]). 67 | 68 | 69 | expire_ttl(Config)-> 70 | {client,Client} = lists:keyfind(client, 1, Config), 71 | 72 | ok = erldis_client:sr_scall(Client,[<<"set">>,<<"key1">>,<<"hello">>]), 73 | true = erldis_client:sr_scall(Client,[<<"del">>,<<"key1">>,<<"hello">>]), 74 | -1 = erldis_client:sr_scall(Client,[<<"ttl">>,<<"key1">>]), 75 | ok = erldis_client:sr_scall(Client,[<<"set">>,<<"key1">>,<<"hello">>]), 76 | -1 = erldis_client:sr_scall(Client,[<<"ttl">>,<<"key1">>]), 77 | true = erldis_client:sr_scall(Client,[<<"expire">>,<<"key1">>,<<"3">>]), 78 | timer:sleep(1000), 79 | 2 = erldis_client:sr_scall(Client,[<<"ttl">>,<<"key1">>]), 80 | timer:sleep(2000), 81 | false = erldis_client:sr_scall(Client,[<<"ttl">>,<<"key1">>]). 82 | 83 | 84 | expireat(Config)-> 85 | {client,Client} = lists:keyfind(client, 1, Config), 86 | 87 | %% Retrieving Unix timestamp 88 | Epoch = calendar:datetime_to_gregorian_seconds({{1970,1,1},{0,0,0}}), 89 | Timestamp = calendar:datetime_to_gregorian_seconds(calendar:universal_time())-Epoch, 90 | WaitThree = Timestamp + 3, 91 | 92 | ok = erldis_client:sr_scall(Client,[<<"set">>,<<"key1">>,<<"hello">>]), 93 | true = erldis_client:sr_scall(Client,[<<"expireat">>,<<"key1">>,WaitThree]), 94 | timer:sleep(1000), 95 | 2 = erldis_client:sr_scall(Client,[<<"ttl">>,<<"key1">>]), 96 | timer:sleep(2000), 97 | false = erldis_client:sr_scall(Client,[<<"ttl">>,<<"key1">>]). 98 | 99 | 100 | 101 | persist(Config)-> 102 | {client,Client} = lists:keyfind(client, 1, Config), 103 | 104 | ok = erldis_client:sr_scall(Client,[<<"set">>,<<"key1">>,<<"hello">>]), 105 | true = erldis_client:sr_scall(Client,[<<"expire">>,<<"key1">>,<<"3">>]), 106 | timer:sleep(1000), 107 | 2 = erldis_client:sr_scall(Client,[<<"ttl">>,<<"key1">>]), 108 | true = erldis_client:sr_scall(Client,[<<"persist">>,<<"key1">>]), 109 | timer:sleep(2500), 110 | true = erldis_client:sr_scall(Client,[<<"exists">>,<<"key1">>]). 111 | 112 | rename(Config)-> 113 | {client,Client} = lists:keyfind(client, 1, Config), 114 | 115 | ok = erldis_client:sr_scall(Client,[<<"set">>,<<"key1">>,<<"hello">>]), 116 | OldKey = erldis_client:sr_scall(Client,[<<"get">>,<<"key1">>]), 117 | ok = erldis_client:sr_scall(Client,[<<"rename">>,<<"key1">>,<<"key2">>]), 118 | OldKey = erldis_client:sr_scall(Client,[<<"get">>,<<"key2">>]). 119 | 120 | renamenx(Config)-> 121 | {client,Client} = lists:keyfind(client, 1, Config), 122 | 123 | ok = erldis_client:sr_scall(Client,[<<"set">>,<<"key1">>,<<"hello">>]), 124 | OldKey = erldis_client:sr_scall(Client,[<<"get">>,<<"key1">>]), 125 | ok = erldis_client:sr_scall(Client,[<<"set">>,<<"key2">>,<<"world">>]), 126 | false = erldis_client:sr_scall(Client,[<<"renamenx">>,<<"key1">>,<<"key2">>]), 127 | 128 | erldis_client:sr_scall(Client,[<<"del">>,<<"key2">>]), 129 | true = erldis_client:sr_scall(Client,[<<"renamenx">>,<<"key1">>,<<"key2">>]), 130 | OldKey = erldis_client:sr_scall(Client,[<<"get">>,<<"key2">>]). 131 | 132 | type(Config)-> 133 | {client,Client} = lists:keyfind(client, 1, Config), 134 | 135 | ok = erldis_client:sr_scall(Client,[<<"set">>,<<"key1">>,<<"hello">>]), 136 | true = erldis_client:sr_scall(Client,[<<"lpush">>,<<"key2">>,<<"hello">>]), 137 | true = erldis_client:sr_scall(Client,[<<"sadd">>,<<"key3">>,<<"hello">>]), 138 | true = erldis_client:sr_scall(Client,[<<"zadd">>,<<"key4">>,<<"1">>, <<"uno">>]), 139 | true = erldis_client:sr_scall(Client,[<<"hset">>,<<"key5">>,<<"age">>, <<"5">>]), 140 | 141 | <<"string">> = erldis_client:sr_scall(Client,[<<"type">>,<<"key1">>]), 142 | erldis_client:sr_scall(Client,[<<"del">>,<<"key1">>]), 143 | <<"none">> = erldis_client:sr_scall(Client,[<<"type">>,<<"key1">>]), 144 | <<"list">> = erldis_client:sr_scall(Client,[<<"type">>,<<"key2">>]), 145 | <<"set">> = erldis_client:sr_scall(Client,[<<"type">>,<<"key3">>]), 146 | <<"zset">> = erldis_client:sr_scall(Client,[<<"type">>,<<"key4">>]), 147 | <<"hash">> = erldis_client:sr_scall(Client,[<<"type">>,<<"key5">>]). 148 | 149 | move(Config)-> 150 | {client,Client} = lists:keyfind(client, 1, Config), 151 | 152 | ok = erldis_client:sr_scall(Client,[<<"select">>,<<"2">>]), 153 | ok = erldis_client:sr_scall(Client,[<<"flushdb">>]), 154 | ok = erldis_client:sr_scall(Client,[<<"select">>,<<"1">>]), 155 | ok = erldis_client:sr_scall(Client,[<<"flushdb">>]), 156 | ok = erldis_client:sr_scall(Client,[<<"set">>,<<"key">>,<<"hello">>]), 157 | OldKey = erldis_client:sr_scall(Client,[<<"get">>,<<"key">>]), 158 | true = erldis_client:sr_scall(Client,[<<"move">>,<<"key">>,<<"2">>]), 159 | false = erldis_client:sr_scall(Client,[<<"exists">>,<<"key">>]), 160 | erldis_client:sr_scall(Client,[<<"select">>,<<"2">>]), 161 | true = erldis_client:sr_scall(Client,[<<"exists">>,<<"key">>]), 162 | OldKey = erldis_client:sr_scall(Client,[<<"get">>,<<"key">>]). 163 | 164 | 165 | 166 | pattern(Config)-> 167 | {client,Client} = lists:keyfind(client, 1, Config), 168 | 169 | ok = erldis_client:sr_scall(Client,[<<"flushdb">>]), 170 | ok = erldis_client:sr_scall(Client,[<<"set">>,<<"foo">>,<<"bar">>]), 171 | ok = erldis_client:sr_scall(Client,[<<"set">>,<<"food">>,<<"bar">>]), 172 | Keys = erldis_client:scall(Client,[<<"keys">>,<<"*">>]), 173 | [] = (Keys -- [<<"foo">>, <<"food">>]). 174 | 175 | 176 | randomkey(Config)-> 177 | {client,Client} = lists:keyfind(client, 1, Config), 178 | 179 | ok = erldis_client:sr_scall(Client,[<<"flushdb">>]), 180 | ok = erldis_client:sr_scall(Client,[<<"set">>,<<"foo">>,<<"bar">>]), 181 | ok = erldis_client:sr_scall(Client,[<<"set">>,<<"food">>,<<"bar">>]), 182 | ok = erldis_client:sr_scall(Client,[<<"set">>,<<"fool">>,<<"bar">>]), 183 | ok = erldis_client:sr_scall(Client,[<<"set">>,<<"foot">>,<<"bar">>]), 184 | 185 | %% If 200 keys are chosen randomly, there is a high probability that every key has been chosen atleast once 186 | %% Will prove that every key in the db can be chosen by the randomkey function 187 | %% Will prove that the randomkey function does not always choose one specific key 188 | %% Will NOT prove that the randomkey function follows some other regular expression of a non-random nature 189 | 190 | Randoms = generate_random(200,Client), 191 | 192 | true = lists:member(<<"foo">>, Randoms), 193 | true = lists:member(<<"food">>, Randoms), 194 | true = lists:member(<<"fool">>, Randoms), 195 | true = lists:member(<<"foot">>, Randoms). -------------------------------------------------------------------------------- /test/benchmarks/erldis_strings_bench.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc Benchmarks for string commands using erldis 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(erldis_strings_bench). 9 | -author('Fernando Benavides '). 10 | -author('Chad DePue '). 11 | 12 | -behaviour(edis_bench). 13 | 14 | -define(KEY, <<"test-string">>). 15 | 16 | -include("edis.hrl"). 17 | -include("edis_bench.hrl"). 18 | 19 | -export([bench/1, bench/2, bench/4, bench_all/0, bench_all/1, bench_all/3]). 20 | -export([all/0, 21 | init/1, init_per_testcase/2, init_per_round/3, 22 | quit/1, quit_per_testcase/2, quit_per_round/3]). 23 | -export([append/2, decr/2, decrby/2, get/2, getbit/2, getrange/2, getset/2, incr/2, incrby/2, 24 | mget/2, mset/2, msetnx/2, set/2, setex/2, setnx/2, setbit/2, setrange/2, strlen/2]). 25 | 26 | %% ==================================================================== 27 | %% External functions 28 | %% ==================================================================== 29 | -spec bench_all() -> [{atom(), float()}]. 30 | bench_all() -> 31 | lists:map(fun(F) -> 32 | io:format("Benchmarking ~p...~n", [F]), 33 | Bench = bench(F), 34 | io:format("~n~n\t~p: ~p~n", [F, Bench]), 35 | {F, Bench} 36 | end, all()). 37 | 38 | -spec bench_all([edis_bench:option()]) -> [{atom(), float()}]. 39 | bench_all(Options) -> 40 | lists:map(fun(F) -> 41 | io:format("Benchmarking ~p...~n", [F]), 42 | Bench = bench(F, Options), 43 | io:format("~n~n\t~p: ~p~n", [F, Bench]), 44 | {F, Bench} 45 | end, all()). 46 | 47 | -spec bench_all(pos_integer(), pos_integer(), [edis_bench:option()]) -> [{atom(), float()}]. 48 | bench_all(P1, P2, Options) -> 49 | lists:map(fun(F) -> 50 | io:format("Benchmarking ~p...~n", [F]), 51 | Bench = bench(F, P1, P2, Options), 52 | io:format("~n~n\t~p: ~p~n", [F, Bench]), 53 | {F, Bench} 54 | end, all()). 55 | 56 | -spec bench(atom()) -> float(). 57 | bench(Function) -> bench(Function, []). 58 | 59 | -spec bench(atom(), [edis_bench:option()]) -> float(). 60 | bench(Function, Options) -> bench(Function, 6380, 6379, Options). 61 | 62 | -spec bench(atom(), pos_integer(), pos_integer(), [edis_bench:option()]) -> float(). 63 | bench(Function, P1, P2, Options) -> 64 | edis_bench:bench({?MODULE, Function, [P1]}, {?MODULE, Function, [P2]}, 65 | Options ++ 66 | [{step, 10}, {start, 50}, 67 | {outliers,100}, {symbols, #symbols{down_down = $x, 68 | up_up = $x, 69 | up_down = $x, 70 | down_up = $x, 71 | down_none = $e, 72 | up_none = $e, 73 | none_down = $r, 74 | none_up = $r}}]). 75 | 76 | -spec all() -> [atom()]. 77 | all() -> [Fun || {Fun, _} <- ?MODULE:module_info(exports) -- edis_bench:behaviour_info(callbacks), 78 | Fun =/= module_info, Fun =/= bench_all, Fun =/= bench, Fun =/= msetnx]. 79 | 80 | -spec init([pos_integer()]) -> ok. 81 | init([Port]) -> 82 | case erldis:connect(localhost,Port) of 83 | {ok, Client} -> 84 | Name = process(Port), 85 | case erlang:whereis(Name) of 86 | undefined -> true; 87 | _ -> erlang:unregister(Name) 88 | end, 89 | erlang:register(Name, Client), 90 | ok; 91 | Error -> throw(Error) 92 | end. 93 | 94 | -spec quit([pos_integer()]) -> ok. 95 | quit([Port]) -> 96 | Name = process(Port), 97 | case erlang:whereis(Name) of 98 | undefined -> ok; 99 | Client -> erldis_client:stop(Client) 100 | end, 101 | ok. 102 | 103 | -spec init_per_testcase(atom(), [pos_integer()]) -> ok. 104 | init_per_testcase(_Function, _Extra) -> ok. 105 | 106 | -spec quit_per_testcase(atom(), [pos_integer()]) -> ok. 107 | quit_per_testcase(_Function, _Extra) -> ok. 108 | 109 | -spec init_per_round(atom(), [binary()], [pos_integer()]) -> ok. 110 | init_per_round(Fun, [Key|_], [Port]) when Fun =:= decr; 111 | Fun =:= decrby; 112 | Fun =:= incr; 113 | Fun =:= incrby -> 114 | erldis:set(process(Port), ?KEY, Key); 115 | init_per_round(mget, Keys, [Port]) -> 116 | erldis:mset(process(Port), [{Key, <<"X">>} || Key <- Keys]); 117 | init_per_round(Fun, Keys, [Port]) when Fun =:= mset; 118 | Fun =:= msetnx -> 119 | _ = erldis:delkeys(process(Port), Keys), 120 | ok; 121 | init_per_round(Fun, _Keys, [Port]) when Fun =:= set; 122 | Fun =:= setex; 123 | Fun =:= setnx-> 124 | _ = erldis:del(process(Port), ?KEY), 125 | ok; 126 | init_per_round(_Fun, Keys, [Port]) -> 127 | erldis:set(process(Port), ?KEY, list_to_binary(lists:duplicate(length(Keys), $X))). 128 | 129 | -spec quit_per_round(atom(), [binary()], [pos_integer()]) -> ok. 130 | quit_per_round(_, _Keys, [Port]) -> 131 | erldis:flushdb(process(Port)). 132 | 133 | -spec append([binary()], pos_integer()) -> pos_integer(). 134 | append([Key|_], Port) -> 135 | erldis:append(process(Port), ?KEY, Key). 136 | 137 | -spec decr([binary()], pos_integer()) -> integer(). 138 | decr(_, Port) -> 139 | erldis:decr(process(Port), ?KEY). 140 | 141 | -spec decrby([binary()], pos_integer()) -> integer(). 142 | decrby([Key|_], Port) -> 143 | erldis:decrby(process(Port), ?KEY, edis_util:binary_to_integer(Key)). 144 | 145 | -spec get([binary()], pos_integer()) -> binary(). 146 | get(_, Port) -> 147 | erldis:get(process(Port), ?KEY). 148 | 149 | -spec getbit([binary()], pos_integer()) -> 1 | 0. 150 | getbit([Key|_], Port) -> 151 | erldis:getbit(process(Port), ?KEY, random:uniform(edis_util:binary_to_integer(Key)) - 1). 152 | 153 | -spec getrange([binary()], pos_integer()) -> binary(). 154 | getrange(_, Port) -> 155 | erldis:getrange(process(Port), ?KEY, 1, -2). 156 | 157 | -spec getset([binary()], pos_integer()) -> binary(). 158 | getset([Key|_], Port) -> 159 | erldis:getset(process(Port), ?KEY, Key). 160 | 161 | -spec incr([binary()], pos_integer()) -> integer(). 162 | incr(_, Port) -> 163 | erldis:incr(process(Port), ?KEY). 164 | 165 | -spec incrby([binary()], pos_integer()) -> integer(). 166 | incrby([Key|_], Port) -> 167 | erldis:incrby(process(Port), ?KEY, edis_util:binary_to_integer(Key)). 168 | 169 | -spec mget([binary()], pos_integer()) -> [binary()]. 170 | mget(Keys, Port) -> 171 | erldis:mget(process(Port), Keys). 172 | 173 | -spec mset([binary()], pos_integer()) -> ok. 174 | mset(Keys, Port) -> 175 | erldis:mset(process(Port), [{Key, <<"X">>} || Key <- Keys]). 176 | 177 | -spec msetnx([binary()], pos_integer()) -> ok. 178 | msetnx(Keys, Port) -> 179 | erldis:msetnx(process(Port), [{Key, <<"X">>} || Key <- Keys]). 180 | 181 | -spec set([binary()], pos_integer()) -> ok. 182 | set([Key|_], Port) -> 183 | erldis:set(process(Port), ?KEY, 184 | unicode:characters_to_binary(lists:duplicate(edis_util:binary_to_integer(Key), $X))). 185 | 186 | -spec setex([binary()], pos_integer()) -> ok. 187 | setex([Key|_], Port) -> 188 | erldis:setex(process(Port), ?KEY, edis_util:binary_to_integer(Key), 189 | unicode:characters_to_binary(lists:duplicate(edis_util:binary_to_integer(Key), $X))). 190 | 191 | -spec setnx([binary()], pos_integer()) -> ok. 192 | setnx([Key|_], Port) -> 193 | erldis:setnx(process(Port), ?KEY, 194 | unicode:characters_to_binary(lists:duplicate(edis_util:binary_to_integer(Key), $X))). 195 | 196 | -spec setbit([binary()], pos_integer()) -> 1 | 0. 197 | setbit([Key|_], Port) -> 198 | erldis:setbit(process(Port), ?KEY, 199 | random:uniform(edis_util:binary_to_integer(Key)) - 1, random:uniform(2) - 1). 200 | 201 | -spec setrange([binary()], pos_integer()) -> pos_integer(). 202 | setrange([Key|_], Port) -> 203 | erldis:setrange(process(Port), ?KEY, random:uniform(edis_util:binary_to_integer(Key)) - 1, <<"xxx">>). 204 | 205 | -spec strlen([binary()], pos_integer()) -> pos_integer(). 206 | strlen(_Keys, Port) -> 207 | erldis:strlen(process(Port), ?KEY). 208 | 209 | process(Port) -> list_to_atom("erldis-tester-" ++ integer_to_list(Port)). -------------------------------------------------------------------------------- /test/benchmarks/sets_bench.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc Benchmarks for sets commands 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(sets_bench). 9 | -author('Fernando Benavides '). 10 | -author('Chad DePue '). 11 | 12 | -behaviour(edis_bench). 13 | 14 | -define(KEY, <<"test-set">>). 15 | -define(KEY2, <<"test-set2">>). 16 | 17 | -include("edis.hrl"). 18 | 19 | -export([all/0, 20 | init/1, init_per_testcase/2, init_per_round/3, 21 | quit/1, quit_per_testcase/2, quit_per_round/3]). 22 | -export([sadd/1, scard/1, sdiff/1, sdiffstore/1, sinter_min/1, sinter_n/1, sinter_m/1, 23 | sinterstore_min/1, sinterstore_n/1, sinterstore_m/1, sismember/1, smembers/1, 24 | smove/1, spop/1, srandmember/1, srem/1, sunion/1, sunionstore/1]). 25 | 26 | %% ==================================================================== 27 | %% External functions 28 | %% ==================================================================== 29 | -spec all() -> [atom()]. 30 | all() -> [Fun || {Fun, _} <- ?MODULE:module_info(exports) -- edis_bench:behaviour_info(callbacks), 31 | Fun =/= module_info]. 32 | 33 | -spec init([]) -> ok. 34 | init(_Extra) -> ok. 35 | 36 | -spec quit([]) -> ok. 37 | quit(_Extra) -> ok. 38 | 39 | -spec init_per_testcase(atom(), []) -> ok. 40 | init_per_testcase(_Function, _Extra) -> ok. 41 | 42 | -spec quit_per_testcase(atom(), []) -> ok. 43 | quit_per_testcase(_Function, _Extra) -> ok. 44 | 45 | -spec init_per_round(atom(), [binary()], []) -> ok. 46 | init_per_round(Fun, Keys, _Extra) when Fun =:= scard; 47 | Fun =:= sismember; 48 | Fun =:= smembers; 49 | Fun =:= smove; 50 | Fun =:= spop; 51 | Fun =:= srandmember -> 52 | sadd(Keys), 53 | ok; 54 | init_per_round(Fun, Keys, _Extra) when Fun =:= sinter_min; 55 | Fun =:= sinterstore_min -> 56 | edis_db:run( 57 | edis_db:process(0), 58 | #edis_command{cmd = <<"SADD">>, args = [?KEY2, <<"1">>], 59 | group = sets, result_type = number}), 60 | sadd(Keys), 61 | ok; 62 | init_per_round(Fun, Keys, _Extra) when Fun =:= sinter_n; 63 | Fun =:= sinterstore_n -> 64 | edis_db:run( 65 | edis_db:process(0), 66 | #edis_command{cmd = <<"SADD">>, args = [?KEY2 | Keys], 67 | group = sets, result_type = number}), 68 | sadd(Keys), 69 | ok; 70 | init_per_round(Fun, Keys, _Extra) when Fun =:= sinter_m; 71 | Fun =:= sinterstore_m -> 72 | lists:foreach(fun(Key) -> 73 | edis_db:run( 74 | edis_db:process(0), 75 | #edis_command{cmd = <<"SADD">>, args = [Key, ?KEY, ?KEY2, Key], 76 | group = sets, result_type = number}) 77 | end, Keys); 78 | init_per_round(Fun, Keys, _Extra) when Fun =:= sdiff; 79 | Fun =:= sdiffstore; 80 | Fun =:= sunion; 81 | Fun =:= sunionstore -> 82 | edis_db:run( 83 | edis_db:process(0), 84 | #edis_command{cmd = <<"SADD">>, 85 | args = [?KEY2 | lists:map(fun edis_util:integer_to_binary/1, lists:seq(1, 100))], 86 | group = sets, result_type = number}), 87 | sadd(Keys), 88 | ok; 89 | init_per_round(Fun, _Keys, _Extra) when Fun =:= srem -> 90 | edis_db:run( 91 | edis_db:process(0), 92 | #edis_command{cmd = <<"SADD">>, 93 | args = [?KEY | lists:map(fun edis_util:integer_to_binary/1, lists:seq(1, 10000))], 94 | group = sets, result_type = number}), 95 | ok; 96 | init_per_round(_Fun, _Keys, _Extra) -> 97 | _ = edis_db:run( 98 | edis_db:process(0), 99 | #edis_command{cmd = <<"DEL">>, args = [?KEY], group = keys, result_type = number}), 100 | ok. 101 | 102 | -spec quit_per_round(atom(), [binary()], []) -> ok. 103 | quit_per_round(_, Keys, _Extra) -> 104 | _ = edis_db:run( 105 | edis_db:process(0), 106 | #edis_command{cmd = <<"DEL">>, args = [?KEY, ?KEY2 | Keys], group = keys, result_type = number} 107 | ), 108 | ok. 109 | 110 | -spec sadd([binary()]) -> pos_integer(). 111 | sadd(Keys) -> 112 | catch edis_db:run( 113 | edis_db:process(0), 114 | #edis_command{cmd = <<"SADD">>, args = [?KEY | Keys], 115 | group = sets, result_type = number}). 116 | 117 | -spec scard([binary()]) -> pos_integer(). 118 | scard(_Keys) -> 119 | catch edis_db:run( 120 | edis_db:process(0), 121 | #edis_command{cmd = <<"SCARD">>, args = [?KEY], group = sets, result_type = number}). 122 | 123 | -spec sdiff([binary()]) -> [binary()]. 124 | sdiff(_Keys) -> 125 | catch edis_db:run( 126 | edis_db:process(0), 127 | #edis_command{cmd = <<"SDIFF">>, args = [?KEY, ?KEY2], group = sets, result_type = multi_bulk}). 128 | 129 | -spec sdiffstore([binary()]) -> [binary()]. 130 | sdiffstore(_Keys) -> 131 | catch edis_db:run( 132 | edis_db:process(0), 133 | #edis_command{cmd = <<"SDIFFSTORE">>, args = [?KEY, ?KEY, ?KEY2], 134 | group = sets, result_type = multi_bulk}). 135 | 136 | -spec sinter_min([binary()]) -> [binary()]. 137 | sinter_min(_Keys) -> 138 | catch edis_db:run( 139 | edis_db:process(0), 140 | #edis_command{cmd = <<"SINTER">>, args = [?KEY, ?KEY2], group = sets, result_type = multi_bulk}). 141 | 142 | -spec sinter_n([binary()]) -> [binary()]. 143 | sinter_n(_Keys) -> 144 | catch edis_db:run( 145 | edis_db:process(0), 146 | #edis_command{cmd = <<"SINTER">>, args = [?KEY, ?KEY2], group = sets, result_type = multi_bulk}). 147 | 148 | -spec sinter_m([binary()]) -> [binary()]. 149 | sinter_m(Keys) -> 150 | catch edis_db:run( 151 | edis_db:process(0), 152 | #edis_command{cmd = <<"SINTER">>, args = Keys, group = sets, result_type = multi_bulk}). 153 | 154 | -spec sinterstore_min([binary()]) -> [binary()]. 155 | sinterstore_min(_Keys) -> 156 | catch edis_db:run( 157 | edis_db:process(0), 158 | #edis_command{cmd = <<"SINTERSTORE">>, args = [?KEY, ?KEY, ?KEY2], 159 | group = sets, result_type = multi_bulk}). 160 | 161 | -spec sinterstore_n([binary()]) -> [binary()]. 162 | sinterstore_n(_Keys) -> 163 | catch edis_db:run( 164 | edis_db:process(0), 165 | #edis_command{cmd = <<"SINTERSTORE">>, args = [?KEY, ?KEY, ?KEY2], 166 | group = sets, result_type = multi_bulk}). 167 | 168 | -spec sinterstore_m([binary()]) -> [binary()]. 169 | sinterstore_m(Keys) -> 170 | catch edis_db:run( 171 | edis_db:process(0), 172 | #edis_command{cmd = <<"SINTERSTORE">>, args = [?KEY | Keys], 173 | group = sets, result_type = multi_bulk}). 174 | 175 | -spec sismember([binary()]) -> true. 176 | sismember([Key|_]) -> 177 | catch edis_db:run( 178 | edis_db:process(0), 179 | #edis_command{cmd = <<"SISMEMBER">>, args = [?KEY, Key], group = sets, result_type = boolean}). 180 | 181 | -spec smembers([binary()]) -> [binary()]. 182 | smembers(_) -> 183 | catch edis_db:run( 184 | edis_db:process(0), 185 | #edis_command{cmd = <<"SMEMBERS">>, args = [?KEY], group = sets, result_type = multi_bulk}). 186 | 187 | -spec smove([binary()]) -> boolean(). 188 | smove([Key|_]) -> 189 | catch edis_db:run( 190 | edis_db:process(0), 191 | #edis_command{cmd = <<"SMOVE">>, args = [?KEY, ?KEY2, Key], group = sets, result_type = boolean}). 192 | 193 | -spec spop([binary()]) -> binary(). 194 | spop(_Keys) -> 195 | catch edis_db:run( 196 | edis_db:process(0), 197 | #edis_command{cmd = <<"SPOP">>, args = [?KEY], group = sets, result_type = bulk}). 198 | 199 | -spec srandmember([binary()]) -> binary(). 200 | srandmember(_Keys) -> 201 | catch edis_db:run( 202 | edis_db:process(0), 203 | #edis_command{cmd = <<"SRANDMEMBER">>, args = [?KEY], group = sets, result_type = bulk}). 204 | 205 | -spec srem([binary()]) -> number(). 206 | srem(Keys) -> 207 | catch edis_db:run( 208 | edis_db:process(0), 209 | #edis_command{cmd = <<"SREM">>, args = [?KEY | Keys], group = sets, result_type = number}). 210 | 211 | -spec sunion([binary()]) -> [binary()]. 212 | sunion(_Keys) -> 213 | catch edis_db:run( 214 | edis_db:process(0), 215 | #edis_command{cmd = <<"SUNION">>, args = [?KEY, ?KEY2], group = sets, result_type = multi_bulk}). 216 | 217 | -spec sunionstore([binary()]) -> [binary()]. 218 | sunionstore(_Keys) -> 219 | catch edis_db:run( 220 | edis_db:process(0), 221 | #edis_command{cmd = <<"SUNIONSTORE">>, args = [?KEY, ?KEY, ?KEY2], 222 | group = sets, result_type = multi_bulk}). 223 | -------------------------------------------------------------------------------- /src/edis_util.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc edis utilities 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(edis_util). 9 | -author('Fernando Benavides '). 10 | -author('Chad DePue '). 11 | 12 | -export([timestamp/0, now/0, upper/1, lower/1, binary_to_integer/1, binary_to_integer/2, 13 | integer_to_binary/1, binary_to_float/1, binary_to_float/2, reverse_tuple_to_list/1, 14 | make_pairs/1, glob_to_re/1,random_binary/0, join/2, load_config/1, 15 | multiply/2, sum/2, min/2, max/2]). 16 | 17 | -include("edis.hrl"). 18 | 19 | -define(EPOCH, 62167219200). 20 | 21 | %% @doc Current timestamp 22 | -spec timestamp() -> float(). 23 | timestamp() -> 24 | ?MODULE:now() + element(3, erlang:now()) / 1000000. 25 | 26 | %% @doc UTC in *NIX seconds 27 | -spec now() -> pos_integer(). 28 | now() -> 29 | calendar:datetime_to_gregorian_seconds(calendar:universal_time()) - ?EPOCH. 30 | 31 | %% @doc converts all characters in the specified binary to uppercase. 32 | -spec upper(binary()) -> binary(). 33 | upper(Bin) -> 34 | upper(Bin, <<>>). 35 | 36 | %% @private 37 | upper(<<>>, Acc) -> 38 | Acc; 39 | upper(<>, Acc) when $a =< C, C =< $z -> 40 | upper(Rest, <>); 41 | upper(<<195, C, Rest/binary>>, Acc) when 160 =< C, C =< 182 -> %% A-0 with tildes plus enye 42 | upper(Rest, <>); 43 | upper(<<195, C, Rest/binary>>, Acc) when 184 =< C, C =< 190 -> %% U and Y with tilde plus greeks 44 | upper(Rest, <>); 45 | upper(<>, Acc) -> 46 | upper(Rest, <>). 47 | 48 | %% @doc converts all characters in the specified binary to lowercase 49 | -spec lower(binary()) -> binary(). 50 | lower(Bin) -> 51 | lower(Bin, <<>>). 52 | 53 | lower(<<>>, Acc) -> 54 | Acc; 55 | lower(<>, Acc) when $A =< C, C =< $Z -> 56 | lower(Rest, <>); 57 | lower(<<195, C, Rest/binary>>, Acc) when 128 =< C, C =< 150 -> %% A-0 with tildes plus enye 58 | lower(Rest, <>); 59 | lower(<<195, C, Rest/binary>>, Acc) when 152 =< C, C =< 158 -> %% U and Y with tilde plus greeks 60 | lower(Rest, <>); 61 | lower(<>, Acc) -> 62 | lower(Rest, <>). 63 | 64 | %% @doc returns an integer whose binary representation is Bin 65 | -spec binary_to_integer(binary()) -> integer(). 66 | binary_to_integer(Bin) -> 67 | try list_to_integer(binary_to_list(Bin)) 68 | catch 69 | _:badarg -> 70 | throw(not_integer) 71 | end. 72 | 73 | %% @doc returns a float whose binary representation is Bin 74 | -spec binary_to_float(binary()) -> float(). 75 | binary_to_float(Bin) -> 76 | case lower(Bin) of 77 | <<"inf">> -> ?POS_INFINITY; 78 | <<"infinity">> -> ?POS_INFINITY; 79 | <<"+infinity">> -> ?POS_INFINITY; 80 | <<"+inf">> -> ?POS_INFINITY; 81 | <<"-inf">> -> ?NEG_INFINITY; 82 | <<"-infinity">> -> ?NEG_INFINITY; 83 | _ -> 84 | try list_to_float(binary_to_list(Bin)) 85 | catch 86 | _:badarg -> 87 | try 1.0 * list_to_integer(binary_to_list(Bin)) 88 | catch 89 | _:badarg -> 90 | throw(not_float) 91 | end 92 | end 93 | end. 94 | 95 | %% @doc returns an integer whose binary representation is Bin. 96 | %% If Bin is not a integer, Default is returned 97 | -spec binary_to_integer(binary(), integer()) -> integer(). 98 | binary_to_integer(Bin, Default) -> 99 | try list_to_integer(binary_to_list(Bin)) 100 | catch 101 | _:badarg -> 102 | try erlang:trunc(list_to_float(binary_to_list(Bin))) 103 | catch 104 | _:badarg -> 105 | lager:warning("Using ~p because we received '~s'. This behaviour was copied from redis-server~n", [Default, Bin]), 106 | Default 107 | end 108 | end. 109 | 110 | %% @doc returns a float whose binary representation is Bin. 111 | %% If Bin is not a float, Default is returned 112 | -spec binary_to_float(binary(), float() | undefined) -> float() | undefined. 113 | binary_to_float(Bin, Default) -> 114 | try list_to_float(binary_to_list(Bin)) 115 | catch 116 | _:badarg -> 117 | try 1.0 * list_to_integer(binary_to_list(Bin)) 118 | catch 119 | _:badarg -> 120 | Default 121 | end 122 | end. 123 | 124 | %% @doc returns a binary whose integer representation is Int 125 | -spec integer_to_binary(binary()) -> integer(). 126 | integer_to_binary(Int) -> 127 | list_to_binary(integer_to_list(Int)). 128 | 129 | -spec reverse_tuple_to_list({any(),any()}) -> [any()]. 130 | reverse_tuple_to_list({F,S}) -> 131 | [S,F]. 132 | 133 | %% @doc returns a list of binary tuples. The first tuple contains the first pair of elements in the received list, 134 | %% the second tuple contains the second pair and so on. 135 | %% If the received list is odd, the last element will be ignored 136 | -spec make_pairs([any()]) -> [{any(), any()}]. 137 | make_pairs(KVs) -> 138 | make_pairs(KVs, []). 139 | 140 | make_pairs([], Acc) -> lists:reverse(Acc); 141 | make_pairs([_], Acc) -> lists:reverse(Acc); 142 | make_pairs([K, V | Rest], Acc) -> 143 | make_pairs(Rest, [{K,V} | Acc]). 144 | 145 | %% @doc converts the GLOB into reg exp 146 | -spec glob_to_re(binary()) -> binary(). 147 | glob_to_re(Pattern) -> 148 | binary:replace( 149 | binary:replace( 150 | binary:replace( 151 | binary:replace(Pattern, <<"*">>, <<".*">>, [global]), 152 | <<"?">>, <<".">>, [global]), 153 | <<"(">>, <<"\\(">>, [global]), 154 | <<")">>, <<"\\)">>, [global]). 155 | 156 | %% @doc returns a random binary 157 | -spec random_binary() -> binary(). 158 | random_binary() -> 159 | Now = {_, _, Micro} = erlang:now(), 160 | Nowish = calendar:now_to_universal_time(Now), 161 | Nowsecs = calendar:datetime_to_gregorian_seconds(Nowish), 162 | Then = calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}), 163 | Prefix = io_lib:format("~14.16.0b", [(Nowsecs - Then) * 1000000 + Micro]), 164 | list_to_binary(Prefix ++ integer_to_list(Micro) ++ base64:encode(crypto:rand_bytes(9))). 165 | 166 | %% @doc joins the list of binaries with Sep 167 | -spec join([binary()], binary()) -> binary(). 168 | join([], _) -> <<>>; 169 | join([Bin], _) -> Bin; 170 | join([Bin|Bins], Sep) -> join(Bins, Sep, Bin). 171 | 172 | join([], _, Acc) -> Acc; 173 | join([Bin|Bins], Sep, Acc) -> join(Bins, Sep, <>). 174 | 175 | -spec multiply(float(),float()) -> float(). 176 | multiply(0.0,_) -> 0.0; 177 | multiply(_,0.0) -> 0.0; 178 | multiply(?POS_INFINITY,V) when V > 0.0 -> ?POS_INFINITY; 179 | multiply(?POS_INFINITY,V) when V < 0.0 -> ?NEG_INFINITY; 180 | multiply(?NEG_INFINITY,V) when V > 0.0 -> ?NEG_INFINITY; 181 | multiply(?NEG_INFINITY,V) when V < 0.0 -> ?POS_INFINITY; 182 | multiply(V,?POS_INFINITY) when V > 0.0 -> ?POS_INFINITY; 183 | multiply(V,?POS_INFINITY) when V < 0.0 -> ?NEG_INFINITY; 184 | multiply(V,?NEG_INFINITY) when V > 0.0 -> ?NEG_INFINITY; 185 | multiply(V,?NEG_INFINITY) when V < 0.0 -> ?POS_INFINITY; 186 | multiply(V1,V2) -> V1*V2. 187 | 188 | -spec sum(float(),float()) -> float(). 189 | sum(?POS_INFINITY,?NEG_INFINITY) -> 0.0; 190 | sum(?NEG_INFINITY,?POS_INFINITY) -> 0.0; 191 | sum(?POS_INFINITY,_) -> ?POS_INFINITY; 192 | sum(?NEG_INFINITY,_) -> ?NEG_INFINITY; 193 | sum(_,?POS_INFINITY) -> ?POS_INFINITY; 194 | sum(_,?NEG_INFINITY) -> ?NEG_INFINITY; 195 | sum(V1,V2) when V1/V2 < 0.0 -> V1+V2; 196 | sum(V1,V2) when V1 > 0.0, ?POS_INFINITY-V1 < V2 -> ?POS_INFINITY; 197 | sum(V1,V2) when V1 < 0.0, ?NEG_INFINITY-V1 > V2 -> ?NEG_INFINITY; 198 | sum(V1,V2) -> V1+V2. 199 | 200 | -spec min(float(),float()) -> float(). 201 | min(?POS_INFINITY,V) -> V; 202 | min(V,?POS_INFINITY) -> V; 203 | min(?NEG_INFINITY,_) -> ?NEG_INFINITY; 204 | min(_,?NEG_INFINITY) -> ?NEG_INFINITY; 205 | min(V1,V2) -> erlang:min(V1,V2). 206 | 207 | -spec max(float(),float()) -> float(). 208 | max(?POS_INFINITY,_) -> ?POS_INFINITY; 209 | max(_,?POS_INFINITY) -> ?POS_INFINITY; 210 | max(?NEG_INFINITY,V) -> V; 211 | max(V,?NEG_INFINITY) -> V; 212 | max(V1,V2) -> erlang:max(V1,V2). 213 | 214 | %% @doc Loads an Erlang config file and sets the corresponding application environment variables 215 | -spec load_config(string()) -> ok. 216 | load_config(File) -> 217 | case file:consult(File) of 218 | {error, Reason} -> 219 | throw(Reason); 220 | {ok, [Configs]} -> 221 | lists:foreach(fun load_app_config/1, Configs) 222 | end. 223 | 224 | %% @doc Reads an erlang config file and sets the corresponding application environments 225 | load_app_config({App, Envs}) -> 226 | case application:load(App) of 227 | ok -> ok; 228 | {error, {already_loaded, App}} -> 229 | ok = 230 | case application:stop(App) of 231 | ok -> ok; 232 | {error, {not_started, App}} -> ok 233 | end, 234 | ok = application:unload(App), 235 | ok = application:load(App) 236 | end, 237 | lists:foreach(fun({Key, Value}) -> 238 | ok = application:set_env(App, Key, Value) 239 | end, Envs). -------------------------------------------------------------------------------- /test/benchmarks/erldis_lists_bench.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc Benchmarks for lists commands using erldis 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(erldis_lists_bench). 9 | -author('Fernando Benavides '). 10 | -author('Chad DePue '). 11 | 12 | -behaviour(edis_bench). 13 | 14 | -define(KEY, <<"test-list">>). 15 | 16 | -include("edis.hrl"). 17 | -include("edis_bench.hrl"). 18 | 19 | -export([bench/1, bench/2, bench/4, bench_all/0, bench_all/1, bench_all/3]). 20 | -export([all/0, 21 | init/1, init_per_testcase/2, init_per_round/3, 22 | quit/1, quit_per_testcase/2, quit_per_round/3]). 23 | -export([blpop/2, blpop_nothing/2, brpop/2, brpop_nothing/2, brpoplpush/2, lindex/2, linsert/2, 24 | llen/2, lpop/2, lpush/2, lpushx/2, lrange_s/2, lrange_n/2, lrem_x/2, lrem_y/2, lrem_0/2, 25 | lset/2, ltrim/2, rpop/2, rpoplpush/2, rpoplpush_self/2, rpush/2, rpushx/2]). 26 | 27 | %% ==================================================================== 28 | %% External functions 29 | %% ==================================================================== 30 | -spec bench_all() -> [{atom(), float()}]. 31 | bench_all() -> 32 | lists:map(fun(F) -> 33 | io:format("Benchmarking ~p...~n", [F]), 34 | Bench = bench(F), 35 | io:format("~n~n\t~p: ~p~n", [F, Bench]), 36 | {F, Bench} 37 | end, all()). 38 | 39 | -spec bench_all([edis_bench:option()]) -> [{atom(), float()}]. 40 | bench_all(Options) -> 41 | lists:map(fun(F) -> 42 | io:format("Benchmarking ~p...~n", [F]), 43 | Bench = bench(F, Options), 44 | io:format("~n~n\t~p: ~p~n", [F, Bench]), 45 | {F, Bench} 46 | end, all()). 47 | 48 | -spec bench_all(pos_integer(), pos_integer(), [edis_bench:option()]) -> [{atom(), float()}]. 49 | bench_all(P1, P2, Options) -> 50 | lists:map(fun(F) -> 51 | io:format("Benchmarking ~p...~n", [F]), 52 | Bench = bench(F, P1, P2, Options), 53 | io:format("~n~n\t~p: ~p~n", [F, Bench]), 54 | {F, Bench} 55 | end, all()). 56 | 57 | -spec bench(atom()) -> float(). 58 | bench(Function) -> bench(Function, []). 59 | 60 | -spec bench(atom(), [edis_bench:option()]) -> float(). 61 | bench(Function, Options) -> bench(Function, 6380, 6379, Options). 62 | 63 | -spec bench(atom(), pos_integer(), pos_integer(), [edis_bench:option()]) -> float(). 64 | bench(Function, P1, P2, Options) -> 65 | edis_bench:bench({?MODULE, Function, [P1]}, {?MODULE, Function, [P2]}, 66 | Options ++ 67 | [{step, 10}, {start, 50}, 68 | {outliers,100}, {symbols, #symbols{down_down = $x, 69 | up_up = $x, 70 | up_down = $x, 71 | down_up = $x, 72 | down_none = $e, 73 | up_none = $e, 74 | none_down = $r, 75 | none_up = $r}}]). 76 | 77 | -spec all() -> [atom()]. 78 | all() -> [Fun || {Fun, _} <- ?MODULE:module_info(exports) -- edis_bench:behaviour_info(callbacks), 79 | Fun =/= module_info, Fun =/= bench_all, Fun =/= bench, Fun =/= blpop_nothing, 80 | Fun =/= brpop_nothing]. 81 | 82 | -spec init([pos_integer()]) -> ok. 83 | init([Port]) -> 84 | case erldis:connect(localhost,Port) of 85 | {ok, Client} -> 86 | Name = process(Port), 87 | case erlang:whereis(Name) of 88 | undefined -> true; 89 | _ -> erlang:unregister(Name) 90 | end, 91 | erlang:register(Name, Client), 92 | ok; 93 | Error -> throw(Error) 94 | end. 95 | 96 | -spec quit([pos_integer()]) -> ok. 97 | quit([Port]) -> 98 | Name = process(Port), 99 | case erlang:whereis(Name) of 100 | undefined -> ok; 101 | Client -> erldis_client:stop(Client) 102 | end, 103 | ok. 104 | 105 | -spec init_per_testcase(atom(), [pos_integer()]) -> ok. 106 | init_per_testcase(_Function, _Extra) -> ok. 107 | 108 | -spec quit_per_testcase(atom(), [pos_integer()]) -> ok. 109 | quit_per_testcase(_Function, _Extra) -> ok. 110 | 111 | -spec init_per_round(atom(), [binary()], [pos_integer()]) -> ok. 112 | init_per_round(Fun, Keys, [Port]) when Fun =:= blpop_nothing; 113 | Fun =:= brpop_nothing -> 114 | _ = erldis:del(process(Port), [?KEY | Keys]), 115 | ok; 116 | init_per_round(Fun, Keys, [Port]) when Fun =:= lindex; 117 | Fun =:= lrange_s; 118 | Fun =:= lrange_n; 119 | Fun =:= lrem; 120 | Fun =:= ltrim -> 121 | _ = 122 | erldis:lpush(process(Port), ?KEY, [<<"x">> || _ <- lists:seq(1, erlang:max(5000, length(Keys)))]), 123 | ok; 124 | init_per_round(lset, Keys, [Port]) -> 125 | _ = erldis:lpush(process(Port), ?KEY, [<<"y">> || _ <- Keys]), 126 | ok; 127 | init_per_round(_Fun, Keys, [Port]) -> 128 | _ = erldis:lpush(process(Port), ?KEY, Keys), 129 | ok. 130 | 131 | -spec quit_per_round(atom(), [binary()], []) -> ok. 132 | quit_per_round(_, _Keys, [Port]) -> 133 | _ = erldis:del(process(Port), ?KEY), 134 | ok. 135 | 136 | -spec blpop_nothing([binary()], pos_integer()) -> timeout. 137 | blpop_nothing(Keys, Port) -> 138 | erldis:blpop(process(Port), Keys ++ [1]). 139 | 140 | -spec blpop([binary()], pos_integer()) -> [binary()]. 141 | blpop(_Keys, Port) -> 142 | erldis:blpop(process(Port), [?KEY, 1000]). 143 | 144 | -spec brpop_nothing([binary()], pos_integer()) -> timeout. 145 | brpop_nothing(Keys, Port) -> 146 | erldis:brpop(process(Port), Keys ++ [1]). 147 | 148 | -spec brpop([binary()], pos_integer()) -> undefined. 149 | brpop(_Keys, Port) -> 150 | erldis:brpop(process(Port), [?KEY, 1000]). 151 | 152 | -spec brpoplpush([binary()], pos_integer()) -> undefined. 153 | brpoplpush(_Keys, Port) -> 154 | erldis:brpoplpush(process(Port), [?KEY, <<(?KEY)/binary, "-2">>, 1000], infinity). 155 | 156 | -spec lindex([binary()], pos_integer()) -> binary(). 157 | lindex(Keys, Port) -> 158 | erldis:lindex(process(Port), ?KEY, length(Keys)). 159 | 160 | -spec linsert([binary()], pos_integer()) -> binary(). 161 | linsert([Key|_], Port) -> 162 | erldis:linsert(process(Port), ?KEY, <<"BEFORE">>, Key, <<"x">>). 163 | 164 | -spec llen([binary()], pos_integer()) -> binary(). 165 | llen(_Keys, Port) -> 166 | erldis:llen(process(Port), ?KEY). 167 | 168 | -spec lpop([binary()], pos_integer()) -> binary(). 169 | lpop(_Keys, Port) -> 170 | erldis:lpop(process(Port), ?KEY). 171 | 172 | -spec lpush([binary()], pos_integer()) -> integer(). 173 | lpush(_Keys, Port) -> 174 | erldis:lpush(process(Port), ?KEY, ?KEY). 175 | 176 | -spec lpushx([binary()], pos_integer()) -> integer(). 177 | lpushx(_Keys, Port) -> 178 | erldis:lpushx(process(Port), ?KEY, ?KEY). 179 | 180 | -spec lrange_s([binary()], pos_integer()) -> [binary()]. 181 | lrange_s([Key|_], Port) -> 182 | erldis:lrange(process(Port), ?KEY, edis_util:binary_to_integer(Key), edis_util:binary_to_integer(Key)). 183 | 184 | -spec lrange_n([binary()], pos_integer()) -> [binary()]. 185 | lrange_n([Key|_], Port) -> 186 | erldis:lrange(process(Port), ?KEY, 0, edis_util:binary_to_integer(Key)). 187 | 188 | -spec lrem_x([binary()], pos_integer()) -> integer(). 189 | lrem_x([Key|_], Port) -> 190 | erldis:lrem(process(Port), ?KEY, edis_util:binary_to_integer(Key), <<"x">>). 191 | 192 | -spec lrem_y([binary()], pos_integer()) -> integer(). 193 | lrem_y([Key|_], Port) -> 194 | erldis:lrem(process(Port), ?KEY, edis_util:binary_to_integer(Key), <<"y">>). 195 | 196 | -spec lrem_0([binary()], pos_integer()) -> integer(). 197 | lrem_0(_Keys, Port) -> 198 | erldis:lrem(process(Port), ?KEY, 0, <<"x">>). 199 | 200 | -spec lset([binary()], pos_integer()) -> ok. 201 | lset([Key|_], Port) -> 202 | erldis:lset(process(Port), ?KEY, erlang:trunc(edis_util:binary_to_integer(Key) / 2), <<"x">>). 203 | 204 | -spec ltrim([binary()], pos_integer()) -> ok. 205 | ltrim([Key|_], Port) -> 206 | erldis:ltrim(process(Port), ?KEY, edis_util:binary_to_integer(Key), -1). 207 | 208 | -spec rpop([binary()], pos_integer()) -> binary(). 209 | rpop(_Keys, Port) -> 210 | erldis:rpop(process(Port), ?KEY). 211 | 212 | -spec rpush([binary()], pos_integer()) -> integer(). 213 | rpush(_Keys, Port) -> 214 | erldis:rpush(process(Port), ?KEY, ?KEY). 215 | 216 | -spec rpushx([binary()], pos_integer()) -> integer(). 217 | rpushx(_Keys, Port) -> 218 | erldis:rpushx(process(Port), ?KEY, ?KEY). 219 | 220 | -spec rpoplpush([binary()], pos_integer()) -> binary(). 221 | rpoplpush(_Keys, Port) -> 222 | erldis:rpoplpush(process(Port), ?KEY, <<(?KEY)/binary, "-2">>). 223 | 224 | -spec rpoplpush_self([binary()], pos_integer()) -> binary(). 225 | rpoplpush_self(_Keys, Port) -> 226 | erldis:rpoplpush(process(Port), ?KEY, ?KEY). 227 | 228 | process(Port) -> list_to_atom("erldis-tester-" ++ integer_to_list(Port)). -------------------------------------------------------------------------------- /test/benchmarks/erldis_sets_bench.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc Benchmarks for sets commands using erldis 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(erldis_sets_bench). 9 | -author('Fernando Benavides '). 10 | -author('Chad DePue '). 11 | 12 | -behaviour(edis_bench). 13 | 14 | -define(KEY, <<"test-set">>). 15 | -define(KEY2, <<"test-set2">>). 16 | 17 | -include("edis.hrl"). 18 | -include("edis_bench.hrl"). 19 | 20 | -export([bench/1, bench/2, bench/4, bench_all/0, bench_all/1, bench_all/3]). 21 | -export([all/0, 22 | init/1, init_per_testcase/2, init_per_round/3, 23 | quit/1, quit_per_testcase/2, quit_per_round/3]). 24 | -export([sadd/2, scard/2, sdiff/2, sdiffstore/2, sinter_min/2, sinter_n/2, sinter_m/2, 25 | sinterstore_min/2, sinterstore_n/2, sinterstore_m/2, sismember/2, smembers/2, 26 | smove/2, spop/2, srandmember/2, srem/2, sunion/2, sunionstore/2]). 27 | 28 | %% ==================================================================== 29 | %% External functions 30 | %% ==================================================================== 31 | -spec bench_all() -> [{atom(), float()}]. 32 | bench_all() -> 33 | lists:map(fun(F) -> 34 | io:format("Benchmarking ~p...~n", [F]), 35 | Bench = bench(F), 36 | io:format("~n~n\t~p: ~p~n", [F, Bench]), 37 | {F, Bench} 38 | end, all()). 39 | 40 | -spec bench_all([edis_bench:option()]) -> [{atom(), float()}]. 41 | bench_all(Options) -> 42 | lists:map(fun(F) -> 43 | io:format("Benchmarking ~p...~n", [F]), 44 | Bench = bench(F, Options), 45 | io:format("~n~n\t~p: ~p~n", [F, Bench]), 46 | {F, Bench} 47 | end, all()). 48 | 49 | -spec bench_all(pos_integer(), pos_integer(), [edis_bench:option()]) -> [{atom(), float()}]. 50 | bench_all(P1, P2, Options) -> 51 | lists:map(fun(F) -> 52 | io:format("Benchmarking ~p...~n", [F]), 53 | Bench = bench(F, P1, P2, Options), 54 | io:format("~n~n\t~p: ~p~n", [F, Bench]), 55 | {F, Bench} 56 | end, all()). 57 | 58 | -spec bench(atom()) -> float(). 59 | bench(Function) -> bench(Function, []). 60 | 61 | -spec bench(atom(), [edis_bench:option()]) -> float(). 62 | bench(Function, Options) -> bench(Function, 6380, 6379, Options). 63 | 64 | -spec bench(atom(), pos_integer(), pos_integer(), [edis_bench:option()]) -> float(). 65 | bench(Function, P1, P2, Options) -> 66 | edis_bench:bench({?MODULE, Function, [P1]}, {?MODULE, Function, [P2]}, 67 | Options ++ 68 | [{step, 10}, {start, 50}, 69 | {outliers,100}, {symbols, #symbols{down_down = $x, 70 | up_up = $x, 71 | up_down = $x, 72 | down_up = $x, 73 | down_none = $e, 74 | up_none = $e, 75 | none_down = $r, 76 | none_up = $r}}]). 77 | 78 | -spec all() -> [atom()]. 79 | all() -> [Fun || {Fun, _} <- ?MODULE:module_info(exports) -- edis_bench:behaviour_info(callbacks), 80 | Fun =/= module_info, Fun =/= bench_all, Fun =/= bench, Fun =/= sadd]. 81 | 82 | -spec init([pos_integer()]) -> ok. 83 | init([Port]) -> 84 | case erldis:connect(localhost,Port) of 85 | {ok, Client} -> 86 | Name = process(Port), 87 | case erlang:whereis(Name) of 88 | undefined -> true; 89 | _ -> erlang:unregister(Name) 90 | end, 91 | erlang:register(Name, Client), 92 | ok; 93 | Error -> throw(Error) 94 | end. 95 | 96 | -spec quit([pos_integer()]) -> ok. 97 | quit([Port]) -> 98 | Name = process(Port), 99 | case erlang:whereis(Name) of 100 | undefined -> ok; 101 | Client -> erldis_client:stop(Client) 102 | end, 103 | ok. 104 | 105 | -spec init_per_testcase(atom(), [pos_integer()]) -> ok. 106 | init_per_testcase(_Function, _Extra) -> ok. 107 | 108 | -spec quit_per_testcase(atom(), [pos_integer()]) -> ok. 109 | quit_per_testcase(_Function, _Extra) -> ok. 110 | 111 | -spec init_per_round(atom(), [binary()], [pos_integer()]) -> ok. 112 | init_per_round(Fun, Keys, [Port]) when Fun =:= scard; 113 | Fun =:= sismember; 114 | Fun =:= smembers; 115 | Fun =:= smove; 116 | Fun =:= spop; 117 | Fun =:= srandmember -> 118 | sadd(Keys, Port), 119 | ok; 120 | init_per_round(Fun, Keys, [Port]) when Fun =:= sinter_min; 121 | Fun =:= sinterstore_min -> 122 | erldis:sadd(process(Port), ?KEY2, <<"1">>), 123 | sadd(Keys, Port), 124 | ok; 125 | init_per_round(Fun, Keys, [Port]) when Fun =:= sinter_n; 126 | Fun =:= sinterstore_n -> 127 | erldis:sadd(process(Port), ?KEY2, Keys), 128 | sadd(Keys, Port), 129 | ok; 130 | init_per_round(Fun, Keys, [Port]) when Fun =:= sinter_m; 131 | Fun =:= sinterstore_m -> 132 | lists:foreach( 133 | fun(Key) -> 134 | erldis:sadd(process(Port), Key, [?KEY, ?KEY2, Key]) 135 | end, Keys); 136 | init_per_round(Fun, Keys, [Port]) when Fun =:= sdiff; 137 | Fun =:= sdiffstore; 138 | Fun =:= sunion; 139 | Fun =:= sunionstore -> 140 | erldis:sadd(process(Port), ?KEY2, 141 | lists:map( 142 | fun edis_util:integer_to_binary/1, lists:seq(1, 100))), 143 | sadd(Keys, Port), 144 | ok; 145 | init_per_round(Fun, _Keys, [Port]) when Fun =:= srem -> 146 | erldis:sadd( 147 | process(Port), ?KEY, 148 | lists:map(fun edis_util:integer_to_binary/1, lists:seq(1, 10000))), 149 | ok; 150 | init_per_round(_Fun, _Keys, [Port]) -> 151 | _ = erldis:del(process(Port), ?KEY), 152 | ok. 153 | 154 | -spec quit_per_round(atom(), [binary()], [pos_integer()]) -> ok. 155 | quit_per_round(_, Keys, [Port]) -> 156 | _ = erldis:delkeys(process(Port), [?KEY, ?KEY2 | Keys]), 157 | ok. 158 | 159 | -spec sadd([binary()], pos_integer()) ->pos_integer(). 160 | sadd(Keys, Port) -> 161 | erldis:sadd(process(Port), ?KEY, Keys). 162 | 163 | -spec scard([binary()], pos_integer()) ->pos_integer(). 164 | scard(_Keys, Port) -> 165 | erldis:scard(process(Port), ?KEY). 166 | 167 | -spec sdiff([binary()], pos_integer()) ->[binary()]. 168 | sdiff(_Keys, Port) -> 169 | erldis:sdiff(process(Port), [?KEY, ?KEY2]). 170 | 171 | -spec sdiffstore([binary()], pos_integer()) ->[binary()]. 172 | sdiffstore(_Keys, Port) -> 173 | erldis:sdiffstore(process(Port), ?KEY, [?KEY, ?KEY2]). 174 | 175 | -spec sinter_min([binary()], pos_integer()) ->[binary()]. 176 | sinter_min(_Keys, Port) -> 177 | erldis:sinter(process(Port), [?KEY, ?KEY2]). 178 | 179 | -spec sinter_n([binary()], pos_integer()) ->[binary()]. 180 | sinter_n(_Keys, Port) -> 181 | erldis:sinter(process(Port), [?KEY, ?KEY2]). 182 | 183 | -spec sinter_m([binary()], pos_integer()) ->[binary()]. 184 | sinter_m(Keys, Port) -> 185 | erldis:sinter(process(Port), Keys). 186 | 187 | -spec sinterstore_min([binary()], pos_integer()) ->[binary()]. 188 | sinterstore_min(_Keys, Port) -> 189 | erldis:sinterstore(process(Port), ?KEY, [?KEY, ?KEY2]). 190 | 191 | -spec sinterstore_n([binary()], pos_integer()) ->[binary()]. 192 | sinterstore_n(_Keys, Port) -> 193 | erldis:sinterstore(process(Port), ?KEY, [?KEY, ?KEY2]). 194 | 195 | -spec sinterstore_m([binary()], pos_integer()) ->[binary()]. 196 | sinterstore_m(Keys, Port) -> 197 | erldis:sinterstore(process(Port), ?KEY, Keys). 198 | 199 | -spec sismember([binary()], pos_integer()) ->true. 200 | sismember([Key|_], Port) -> 201 | erldis:sismember(process(Port), ?KEY, Key). 202 | 203 | -spec smembers([binary()], pos_integer()) ->[binary()]. 204 | smembers(_, Port) -> 205 | erldis:smembers(process(Port), ?KEY). 206 | 207 | -spec smove([binary()], pos_integer()) ->boolean(). 208 | smove([Key|_], Port) -> 209 | erldis:smove(process(Port), ?KEY, ?KEY2, Key). 210 | 211 | -spec spop([binary()], pos_integer()) ->binary(). 212 | spop(_Keys, Port) -> 213 | erldis:spop(process(Port), ?KEY). 214 | 215 | -spec srandmember([binary()], pos_integer()) ->binary(). 216 | srandmember(_Keys, Port) -> 217 | erldis:srandmember(process(Port), ?KEY). 218 | 219 | -spec srem([binary()], pos_integer()) ->number(). 220 | srem(Keys, Port) -> 221 | erldis:srem(process(Port), ?KEY, Keys). 222 | 223 | -spec sunion([binary()], pos_integer()) ->[binary()]. 224 | sunion(_Keys, Port) -> 225 | erldis:sunion(process(Port), [?KEY, ?KEY2]). 226 | 227 | -spec sunionstore([binary()], pos_integer()) ->[binary()]. 228 | sunionstore(_Keys, Port) -> 229 | erldis:sunionstore(process(Port), ?KEY, [?KEY, ?KEY2]). 230 | 231 | process(Port) -> list_to_atom("erldis-tester-" ++ integer_to_list(Port)). -------------------------------------------------------------------------------- /test/benchmarks/lists_bench.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc Benchmarks for lists commands 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(lists_bench). 9 | -author('Fernando Benavides '). 10 | -author('Chad DePue '). 11 | 12 | -behaviour(edis_bench). 13 | 14 | -define(KEY, <<"test-list">>). 15 | 16 | -include("edis.hrl"). 17 | 18 | -export([all/0, 19 | init/1, init_per_testcase/2, init_per_round/3, 20 | quit/1, quit_per_testcase/2, quit_per_round/3]). 21 | -export([blpop/1, blpop_nothing/1, brpop/1, brpop_nothing/1, brpoplpush/1, lindex/1, linsert/1, 22 | llen/1, lpop/1, lpush/1, lpushx/1, lrange_s/1, lrange_n/1, lrem_x/1, lrem_y/1, lrem_0/1, 23 | lset/1, ltrim/1, rpop/1, rpoplpush/1, rpoplpush_self/1, rpush/1, rpushx/1]). 24 | 25 | %% ==================================================================== 26 | %% External functions 27 | %% ==================================================================== 28 | -spec all() -> [atom()]. 29 | all() -> [Fun || {Fun, _} <- ?MODULE:module_info(exports) -- edis_bench:behaviour_info(callbacks), 30 | Fun =/= module_info]. 31 | 32 | -spec init([]) -> ok. 33 | init(_Extra) -> ok. 34 | 35 | -spec quit([]) -> ok. 36 | quit(_Extra) -> ok. 37 | 38 | -spec init_per_testcase(atom(), []) -> ok. 39 | init_per_testcase(_Function, _Extra) -> ok. 40 | 41 | -spec quit_per_testcase(atom(), []) -> ok. 42 | quit_per_testcase(_Function, _Extra) -> ok. 43 | 44 | -spec init_per_round(atom(), [binary()], []) -> ok. 45 | init_per_round(Fun, Keys, _Extra) when Fun =:= blpop_nothing; 46 | Fun =:= brpop_nothing -> 47 | _ = edis_db:run( 48 | edis_db:process(0), 49 | #edis_command{cmd = <<"DEL">>, args = [?KEY | Keys], group = keys, result_type = number}), 50 | ok; 51 | init_per_round(Fun, Keys, _Extra) when Fun =:= lindex; 52 | Fun =:= lrange_s; 53 | Fun =:= lrange_n; 54 | Fun =:= lrem; 55 | Fun =:= ltrim -> 56 | _ = 57 | edis_db:run( 58 | edis_db:process(0), 59 | #edis_command{cmd = <<"LPUSH">>, 60 | args = [?KEY | [<<"x">> || _ <- lists:seq(1, erlang:max(5000, length(Keys)))]], 61 | group = hashes, result_type = ok}), 62 | ok; 63 | init_per_round(lset, Keys, _Extra) -> 64 | _ = 65 | edis_db:run( 66 | edis_db:process(0), 67 | #edis_command{cmd = <<"LPUSH">>, args = [?KEY | [<<"y">> || _ <- Keys]], 68 | group = lists, result_type = number}), 69 | ok; 70 | init_per_round(_Fun, Keys, _Extra) -> 71 | _ = 72 | edis_db:run( 73 | edis_db:process(0), 74 | #edis_command{cmd = <<"LPUSH">>, args = [?KEY | Keys], group = lists, result_type = number}), 75 | ok. 76 | 77 | -spec quit_per_round(atom(), [binary()], []) -> ok. 78 | quit_per_round(_, _Keys, _Extra) -> 79 | _ = edis_db:run( 80 | edis_db:process(0), 81 | #edis_command{cmd = <<"DEL">>, args = [?KEY], group = keys, result_type = number} 82 | ), 83 | ok. 84 | 85 | -spec blpop_nothing([binary()]) -> timeout. 86 | blpop_nothing(Keys) -> 87 | catch edis_db:run( 88 | edis_db:process(0), 89 | #edis_command{cmd = <<"BLPOP">>, args = Keys, 90 | timeout = 10, expire = edis_util:now(), 91 | group = lists, result_type = multi_bulk}, 10). 92 | 93 | -spec blpop([binary()]) -> undefined. 94 | blpop(_Keys) -> 95 | edis_db:run( 96 | edis_db:process(0), 97 | #edis_command{cmd = <<"BLPOP">>, args = [?KEY], 98 | timeout = 1000, expire = edis_util:now() + 1, 99 | group = lists, result_type = multi_bulk}, 1000). 100 | 101 | -spec brpop_nothing([binary()]) -> timeout. 102 | brpop_nothing(Keys) -> 103 | catch edis_db:run( 104 | edis_db:process(0), 105 | #edis_command{cmd = <<"BRPOP">>, args = Keys, 106 | timeout = 10, expire = edis_util:now(), 107 | group = lists, result_type = multi_bulk}, 10). 108 | 109 | -spec brpop([binary()]) -> undefined. 110 | brpop(_Keys) -> 111 | edis_db:run( 112 | edis_db:process(0), 113 | #edis_command{cmd = <<"BRPOP">>, args = [?KEY], 114 | timeout = 1000, expire = edis_util:now() + 1, 115 | group = lists, result_type = multi_bulk}, 1000). 116 | 117 | -spec brpoplpush([binary()]) -> undefined. 118 | brpoplpush(_Keys) -> 119 | edis_db:run( 120 | edis_db:process(0), 121 | #edis_command{cmd = <<"BRPOPLPUSH">>, args = [?KEY, <<(?KEY)/binary, "-2">>], 122 | timeout = 1000, expire = edis_util:now() + 1, 123 | group = lists, result_type = bulk}, 1000). 124 | 125 | -spec lindex([binary()]) -> binary(). 126 | lindex(Keys) -> 127 | edis_db:run( 128 | edis_db:process(0), 129 | #edis_command{cmd = <<"LINDEX">>, args = [?KEY, length(Keys)], 130 | group = lists, result_type = bulk}). 131 | 132 | -spec linsert([binary()]) -> binary(). 133 | linsert([Key|_]) -> 134 | edis_db:run( 135 | edis_db:process(0), 136 | #edis_command{cmd = <<"LINSERT">>, args = [?KEY, before, Key, <<"x">>], 137 | group = lists, result_type = bulk}). 138 | 139 | -spec llen([binary()]) -> binary(). 140 | llen(_) -> 141 | edis_db:run( 142 | edis_db:process(0), 143 | #edis_command{cmd = <<"LLEN">>, args = [?KEY], group = lists, result_type = number}). 144 | 145 | -spec lpop([binary()]) -> binary(). 146 | lpop(_Keys) -> 147 | edis_db:run( 148 | edis_db:process(0), 149 | #edis_command{cmd = <<"LPOP">>, args = [?KEY], 150 | group = lists, result_type = bulk}). 151 | 152 | -spec lpush([binary()]) -> integer(). 153 | lpush(_Keys) -> 154 | edis_db:run( 155 | edis_db:process(0), 156 | #edis_command{cmd = <<"LPUSH">>, args = [?KEY, ?KEY], 157 | group = lists, result_type = number}). 158 | 159 | -spec lpushx([binary()]) -> integer(). 160 | lpushx(_Keys) -> 161 | edis_db:run( 162 | edis_db:process(0), 163 | #edis_command{cmd = <<"LPUSHX">>, args = [?KEY, ?KEY], 164 | group = lists, result_type = number}). 165 | 166 | -spec lrange_s([binary()]) -> [binary()]. 167 | lrange_s([Key|_]) -> 168 | edis_db:run( 169 | edis_db:process(0), 170 | #edis_command{cmd = <<"LRANGE">>, 171 | args = [?KEY, edis_util:binary_to_integer(Key), edis_util:binary_to_integer(Key)], 172 | group = lists, result_type = multi_bulk}). 173 | 174 | -spec lrange_n([binary()]) -> [binary()]. 175 | lrange_n([Key|_]) -> 176 | edis_db:run( 177 | edis_db:process(0), 178 | #edis_command{cmd = <<"LRANGE">>, 179 | args = [?KEY, 0, edis_util:binary_to_integer(Key)], 180 | group = lists, result_type = multi_bulk}). 181 | 182 | -spec lrem_x([binary()]) -> integer(). 183 | lrem_x([Key|_]) -> 184 | edis_db:run( 185 | edis_db:process(0), 186 | #edis_command{cmd = <<"LREM">>, args = [?KEY, edis_util:binary_to_integer(Key), <<"x">>], 187 | group = lists, result_type = number}). 188 | 189 | -spec lrem_y([binary()]) -> integer(). 190 | lrem_y([Key|_]) -> 191 | edis_db:run( 192 | edis_db:process(0), 193 | #edis_command{cmd = <<"LREM">>, args = [?KEY, edis_util:binary_to_integer(Key), <<"y">>], 194 | group = lists, result_type = number}). 195 | 196 | -spec lrem_0([binary()]) -> integer(). 197 | lrem_0(_Keys) -> 198 | edis_db:run( 199 | edis_db:process(0), 200 | #edis_command{cmd = <<"LREM">>, args = [?KEY, 0, <<"x">>], 201 | group = lists, result_type = number}). 202 | 203 | -spec lset([binary()]) -> ok. 204 | lset([Key|_]) -> 205 | edis_db:run( 206 | edis_db:process(0), 207 | #edis_command{cmd = <<"LSET">>, 208 | args = [?KEY, erlang:trunc(edis_util:binary_to_integer(Key) / 2), <<"x">>], 209 | group = lists, result_type = ok}). 210 | 211 | -spec ltrim([binary()]) -> ok. 212 | ltrim([Key|_]) -> 213 | edis_db:run( 214 | edis_db:process(0), 215 | #edis_command{cmd = <<"LTRIM">>, args = [?KEY, edis_util:binary_to_integer(Key), -1], 216 | group = lists, result_type = ok}). 217 | 218 | -spec rpop([binary()]) -> binary(). 219 | rpop(_Keys) -> 220 | edis_db:run( 221 | edis_db:process(0), 222 | #edis_command{cmd = <<"RPOP">>, args = [?KEY], 223 | group = lists, result_type = bulk}). 224 | 225 | -spec rpush([binary()]) -> integer(). 226 | rpush(_Keys) -> 227 | edis_db:run( 228 | edis_db:process(0), 229 | #edis_command{cmd = <<"RPUSH">>, args = [?KEY, ?KEY], 230 | group = lists, result_type = number}). 231 | 232 | -spec rpushx([binary()]) -> integer(). 233 | rpushx(_Keys) -> 234 | edis_db:run( 235 | edis_db:process(0), 236 | #edis_command{cmd = <<"RPUSHX">>, args = [?KEY, ?KEY], 237 | group = lists, result_type = number}). 238 | 239 | -spec rpoplpush([binary()]) -> binary(). 240 | rpoplpush(_Keys) -> 241 | edis_db:run( 242 | edis_db:process(0), 243 | #edis_command{cmd = <<"RPOPLPUSH">>, args = [?KEY, <<(?KEY)/binary, "-2">>], 244 | group = lists, result_type = bulk}). 245 | 246 | -spec rpoplpush_self([binary()]) -> binary(). 247 | rpoplpush_self(_Keys) -> 248 | edis_db:run( 249 | edis_db:process(0), 250 | #edis_command{cmd = <<"RPOPLPUSH">>, args = [?KEY, ?KEY], group = lists, result_type = bulk}). 251 | -------------------------------------------------------------------------------- /test/benchmarks/erldis_keys_bench.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc Benchmarks for keys commands using erldis 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(erldis_keys_bench). 9 | -author('Fernando Benavides '). 10 | -author('Chad DePue '). 11 | 12 | -behaviour(edis_bench). 13 | 14 | -define(KEY, <<"test-keys">>). 15 | 16 | -include("edis.hrl"). 17 | -include("edis_bench.hrl"). 18 | 19 | -export([bench/1, bench/2, bench/4, bench_all/0, bench_all/1, bench_all/3]). 20 | -export([all/0, 21 | init/1, init_per_testcase/2, init_per_round/3, 22 | quit/1, quit_per_testcase/2, quit_per_round/3]). 23 | -export([del/2, exists/2, expire/2, expireat/2, keys/2, move/2, object_refcount/2, ttl/2, type/2, 24 | object_encoding/2, object_idletime/2, persist/2, randomkey/2, rename/2, renamenx/2, 25 | sort_list_n/2, sort_list_m/2, sort_set_n/2, sort_set_m/2, sort_zset_n/2, sort_zset_m/2]). 26 | 27 | %% ==================================================================== 28 | %% External functions 29 | %% ==================================================================== 30 | -spec bench_all() -> [{atom(), float()}]. 31 | bench_all() -> 32 | lists:map(fun(F) -> 33 | io:format("Benchmarking ~p...~n", [F]), 34 | Bench = bench(F), 35 | io:format("~n~n\t~p: ~p~n", [F, Bench]), 36 | {F, Bench} 37 | end, all()). 38 | 39 | -spec bench_all([edis_bench:option()]) -> [{atom(), float()}]. 40 | bench_all(Options) -> 41 | lists:map(fun(F) -> 42 | io:format("Benchmarking ~p...~n", [F]), 43 | Bench = bench(F, Options), 44 | io:format("~n~n\t~p: ~p~n", [F, Bench]), 45 | {F, Bench} 46 | end, all()). 47 | 48 | -spec bench_all(pos_integer(), pos_integer(), [edis_bench:option()]) -> [{atom(), float()}]. 49 | bench_all(P1, P2, Options) -> 50 | lists:map(fun(F) -> 51 | io:format("Benchmarking ~p...~n", [F]), 52 | Bench = bench(F, P1, P2, Options), 53 | io:format("~n~n\t~p: ~p~n", [F, Bench]), 54 | {F, Bench} 55 | end, all()). 56 | 57 | -spec bench(atom()) -> float(). 58 | bench(Function) -> bench(Function, []). 59 | 60 | -spec bench(atom(), [edis_bench:option()]) -> float(). 61 | bench(Function, Options) -> bench(Function, 6380, 6379, Options). 62 | 63 | -spec bench(atom(), pos_integer(), pos_integer(), [edis_bench:option()]) -> float(). 64 | bench(Function, P1, P2, Options) -> 65 | edis_bench:bench({?MODULE, Function, [P1]}, {?MODULE, Function, [P2]}, 66 | Options ++ 67 | [{step, 10}, {start, 50}, 68 | {outliers,100}, {symbols, #symbols{down_down = $x, 69 | up_up = $x, 70 | up_down = $x, 71 | down_up = $x, 72 | down_none = $e, 73 | up_none = $e, 74 | none_down = $r, 75 | none_up = $r}}]). 76 | 77 | -spec all() -> [atom()]. 78 | all() -> [Fun || {Fun, _} <- ?MODULE:module_info(exports) -- edis_bench:behaviour_info(callbacks), 79 | Fun =/= module_info, Fun =/= bench_all, Fun =/= bench]. 80 | 81 | -spec init([pos_integer()]) -> ok. 82 | init([Port]) -> 83 | case erldis:connect(localhost,Port) of 84 | {ok, Client} -> 85 | Name = process(Port), 86 | case erlang:whereis(Name) of 87 | undefined -> true; 88 | _ -> erlang:unregister(Name) 89 | end, 90 | erlang:register(Name, Client), 91 | ok; 92 | Error -> throw(Error) 93 | end. 94 | 95 | -spec quit([pos_integer()]) -> ok. 96 | quit([Port]) -> 97 | Name = process(Port), 98 | case erlang:whereis(Name) of 99 | undefined -> ok; 100 | Client -> erldis_client:stop(Client) 101 | end, 102 | ok. 103 | 104 | -spec init_per_testcase(atom(), [pos_integer()]) -> ok. 105 | init_per_testcase(_Function, _Extra) -> ok. 106 | 107 | -spec quit_per_testcase(atom(), [pos_integer()]) -> ok. 108 | quit_per_testcase(_Function, _Extra) -> ok. 109 | 110 | -spec init_per_round(atom(), [binary()], [pos_integer()]) -> ok. 111 | init_per_round(move, Keys, [Port]) -> 112 | ok = erldis:select(process(Port), 1), 113 | _ = del(Keys, Port), 114 | ok = erldis:select(process(Port), 0), 115 | ok = erldis:flushdb(process(Port)), 116 | erldis:mset(process(Port), [{Key, Key} || Key <- Keys]); 117 | init_per_round(keys, Keys, [Port]) -> 118 | erldis:mset(process(Port), [{Key, Key} || Key <- Keys]); 119 | init_per_round(sort_list_n, Keys, [Port]) -> 120 | _ = erldis:del(process(Port), ?KEY), 121 | _ = erldis:lpush(process(Port), ?KEY, Keys), 122 | ok; 123 | init_per_round(sort_list_m, _Keys, [Port]) -> 124 | _ = erldis:del(process(Port), ?KEY), 125 | _ = erldis:lpush(process(Port), ?KEY, [edis_util:integer_to_binary(I) || I <- lists:seq(1, 1000)]), 126 | ok; 127 | init_per_round(sort_set_n, Keys, [Port]) -> 128 | _ = erldis:del(process(Port), ?KEY), 129 | _ = erldis:sadd(process(Port), ?KEY, Keys), 130 | ok; 131 | init_per_round(sort_set_m, _Keys, [Port]) -> 132 | _ = erldis:del(process(Port), ?KEY), 133 | _ = erldis:sadd(process(Port), ?KEY, [edis_util:integer_to_binary(I) || I <- lists:seq(1, 1000)]), 134 | ok; 135 | init_per_round(sort_zset_n, Keys, [Port]) -> 136 | _ = erldis:del(process(Port), ?KEY), 137 | _ = erldis:zadd(process(Port), ?KEY, [{1.0, Key} || Key <- Keys]), 138 | ok; 139 | init_per_round(sort_zset_m, _Keys, [Port]) -> 140 | _ = erldis:del(process(Port), ?KEY), 141 | _ = erldis:zadd(process(Port), ?KEY, [{1.0 * I, edis_util:integer_to_binary(I)} || I <- lists:seq(1, 1000)]), 142 | ok; 143 | init_per_round(_Fun, Keys, [Port]) -> 144 | erldis:mset(process(Port), [{Key, iolist_to_binary(Keys)} || Key <- Keys]). 145 | 146 | -spec quit_per_round(atom(), [binary()], []) -> ok. 147 | quit_per_round(exists, Keys, [Port]) -> del(Keys, Port), ok; 148 | quit_per_round(expire, Keys, [Port]) -> del(Keys, Port), ok; 149 | quit_per_round(expireat, Keys, [Port]) -> del(Keys, Port), ok; 150 | quit_per_round(move, Keys, [Port]) -> del(Keys, Port), ok; 151 | quit_per_round(object_refcount, Keys, [Port]) -> del(Keys, Port), ok; 152 | quit_per_round(object_encoding, Keys, [Port]) -> del(Keys, Port), ok; 153 | quit_per_round(object_idletime, Keys, [Port]) -> del(Keys, Port), ok; 154 | quit_per_round(persist, Keys, [Port]) -> del(Keys, Port), ok; 155 | quit_per_round(rename, Keys, [Port]) -> del([?KEY|Keys], Port), ok; 156 | quit_per_round(renamenx, Keys, [Port]) -> del([?KEY|Keys], Port), ok; 157 | quit_per_round(ttl, Keys, [Port]) -> del(Keys, Port), ok; 158 | quit_per_round(type, Keys, [Port]) -> del(Keys, Port), ok; 159 | quit_per_round(_, _Keys, _Extra) -> ok. 160 | 161 | -spec del([binary()], pos_integer()) -> pos_integer(). 162 | del(Keys, Port) -> 163 | erldis:delkeys(process(Port), Keys). 164 | 165 | -spec exists([binary(),...], pos_integer()) -> boolean(). 166 | exists([Key|_], Port) -> 167 | erldis:exists(process(Port), Key). 168 | 169 | -spec expire([binary(),...], pos_integer()) -> boolean(). 170 | expire([Key|_], Port) -> 171 | erldis:expire(process(Port), Key, edis_util:binary_to_integer(Key, 0)). 172 | 173 | -spec expireat([binary(),...], pos_integer()) -> boolean(). 174 | expireat([Key|_], Port) -> 175 | erldis:expireat(process(Port), Key, edis_util:binary_to_integer(Key, 0)). 176 | 177 | -spec keys([binary()], pos_integer()) -> pos_integer(). 178 | keys(_Keys, Port) -> 179 | erldis:keys(process(Port), <<"*">>). 180 | 181 | -spec move([binary(),...], pos_integer()) -> boolean(). 182 | move([Key|_], Port) -> 183 | erldis:move(process(Port), Key, 1). 184 | 185 | -spec object_refcount([binary(),...], pos_integer()) -> integer(). 186 | object_refcount([Key|_], Port) -> 187 | erldis:object_refcount(process(Port), Key). 188 | 189 | -spec object_encoding([binary(),...], pos_integer()) -> binary(). 190 | object_encoding([Key|_], Port) -> 191 | erldis:object_encoding(process(Port), Key). 192 | 193 | -spec object_idletime([binary(),...], pos_integer()) -> integer(). 194 | object_idletime([Key|_], Port) -> 195 | erldis:object_idletime(process(Port), Key). 196 | 197 | -spec persist([binary(),...], pos_integer()) -> boolean(). 198 | persist([Key|_], Port) -> 199 | erldis:persist(process(Port), Key). 200 | 201 | -spec randomkey([binary()], pos_integer()) -> binary(). 202 | randomkey(_Keys, Port) -> 203 | erldis:randomkey(process(Port)). 204 | 205 | -spec rename([binary(),...], pos_integer()) -> ok. 206 | rename([Key|_], Port) -> 207 | erldis:rename(process(Port), Key, ?KEY). 208 | 209 | -spec renamenx([binary(),...], pos_integer()) -> boolean(). 210 | renamenx([Key|_], Port) -> 211 | erldis:renamenx(process(Port), Key, ?KEY). 212 | 213 | -spec ttl([binary(),...], pos_integer()) -> number(). 214 | ttl([Key|_], Port) -> 215 | erldis:ttl(process(Port), Key). 216 | 217 | -spec type([binary(),...], pos_integer()) -> binary(). 218 | type([Key|_], Port) -> 219 | erldis:type(process(Port), Key). 220 | 221 | -spec sort_list_n([binary(),...], pos_integer()) -> [binary()]. 222 | sort_list_n(Keys, Port) -> sort_n(Keys, Port). 223 | 224 | -spec sort_set_n([binary(),...], pos_integer()) -> [binary()]. 225 | sort_set_n(Keys, Port) -> sort_n(Keys, Port). 226 | 227 | -spec sort_zset_n([binary(),...], pos_integer()) -> [binary()]. 228 | sort_zset_n(Keys, Port) -> sort_n(Keys, Port). 229 | 230 | -spec sort_n([binary(),...], pos_integer()) -> [binary()]. 231 | sort_n(_Keys, Port) -> 232 | erldis:sort(process(Port), ?KEY). 233 | 234 | -spec sort_list_m([binary(),...], pos_integer()) -> [binary()]. 235 | sort_list_m(Keys, Port) -> sort_m(Keys, Port). 236 | 237 | -spec sort_set_m([binary(),...], pos_integer()) -> [binary()]. 238 | sort_set_m(Keys, Port) -> sort_m(Keys, Port). 239 | 240 | -spec sort_zset_m([binary(),...], pos_integer()) -> [binary()]. 241 | sort_zset_m(Keys, Port) -> sort_m(Keys, Port). 242 | 243 | -spec sort_m([binary(),...], pos_integer()) -> [binary()]. 244 | sort_m(Keys, Port) -> 245 | erldis:sort(process(Port), ?KEY, <<"LIMIT 0 ", (edis_util:integer_to_binary(length(Keys)))/binary>>). 246 | 247 | process(Port) -> list_to_atom("erldis-tester-" ++ integer_to_list(Port)). -------------------------------------------------------------------------------- /test/edis_bench.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc Benchmarker. Given a module to test with, it helps users determine 6 | %%% the order of functions 7 | %%% @end 8 | %%%------------------------------------------------------------------- 9 | -module(edis_bench). 10 | -author('Fernando Benavides '). 11 | -author('Chad DePue '). 12 | 13 | -include("edis.hrl"). 14 | -include("edis_bench.hrl"). 15 | 16 | -type symbols() :: #symbols{}. 17 | -export_type([symbols/0]). 18 | 19 | -type option() :: {start, pos_integer} | {step, pos_integer()} | {rounds, pos_integer()} | 20 | {extra_args, [term()]} | {outliers, pos_integer()} | {columns, pos_integer()} | 21 | {first_col, pos_integer()} | {rows, pos_integer()} | debug | {k, number()} | {x, number()} | 22 | {symbols, symbols()}. 23 | -export_type([option/0]). 24 | 25 | -export([bench/4, bench/3, bench/2, behaviour_info/1]). 26 | 27 | -export([zero/1, constant/1, linear/1, quadratic/1, logarithmic/1, xlogarithmic/1, exponential/1]). 28 | 29 | %% ==================================================================== 30 | %% External functions 31 | %% ==================================================================== 32 | %% @hidden 33 | -spec behaviour_info(callbacks|term()) -> [{atom(), non_neg_integer()}]. 34 | behaviour_info(callbacks) -> 35 | [{all, 0}, 36 | {init, 1}, {init_per_testcase, 2}, {init_per_round, 3}, 37 | {quit, 1}, {quit_per_testcase, 2}, {quit_per_round, 3}]. 38 | 39 | %% @doc Runs all the benchmarking functions on Module against {@link zero/0} function. 40 | %% The list is obtained calling Module:all(). 41 | -spec bench(atom(), [option()]) -> ok. 42 | bench(Module, Options) -> 43 | ok = try Module:init(proplists:get_value(extra_args, Options, [])) catch _:undef -> ok end, 44 | try 45 | lists:foreach( 46 | fun(Function) -> 47 | io:format("~n~p:~p ...~n", [Module, Function]), 48 | bench(Module, Function, zero, Options) 49 | end, Module:all()) 50 | after 51 | try Module:quit(proplists:get_value(extra_args, Options, [])) catch _:undef -> ok end 52 | end. 53 | 54 | %% @doc Compares the different runs of Module:Function to a given function. 55 | %% Returns the standard deviation of the distances between them (outliers excluded). 56 | %% The higher the value the more different functions are. 57 | -spec bench(atom(), atom(), atom() | fun((pos_integer()) -> number()), [option()]) -> ok. 58 | bench(Module, Function, MathFunction, Options) when is_atom(MathFunction) -> 59 | bench(Module, Function, fun(X) -> ?MODULE:MathFunction(X) end, Options); 60 | bench(Module, Function, MathFunction, Options) -> 61 | RawResults = run(Module, Function, Options), 62 | graph( 63 | [{K, V, proplists:get_value(x, Options, 0) + 64 | (proplists:get_value(k, Options, 1) * MathFunction(K))} || {K,V} <- RawResults], 65 | Options). 66 | 67 | %% @doc Compares the different runs of Module1:Function1 with Module2:Function2 68 | %% Returns the standard deviation of the distances between them (outliers excluded). 69 | %% The higher the value the more different functions are. 70 | -spec bench({atom(), atom(), [term()]}, {atom(), atom(), [term()]}, [option()]) -> float(). 71 | bench({Module1, Function1, ExtraArgs1}, {Module2, Function2, ExtraArgs2}, Options) -> 72 | RawResults1 = run(Module1, Function1, [{extra_args, ExtraArgs1}|Options]), 73 | RawResults2 = run(Module2, Function2, [{extra_args, ExtraArgs2}|Options]), 74 | RawResults = lists:zipwith(fun({K,V1}, {K,V2}) -> {K, V1, V2} end, RawResults1, RawResults2), 75 | graph(RawResults, Options), 76 | Diffs = [(V1-V2)/V2 || {_K,V1,V2} <- remove_outliers(RawResults, Options), V1 /= 0, V2 /= 0], 77 | case proplists:get_bool(debug, Options) of 78 | true -> lager:info("Diffs: ~p~n", [Diffs]); 79 | false -> ok 80 | end, 81 | lists:sum(Diffs) / erlang:length(Diffs). 82 | 83 | %% ==================================================================== 84 | %% Math functions 85 | %% ==================================================================== 86 | %% @doc O(1) comparer 87 | -spec zero(pos_integer()) -> pos_integer(). 88 | zero(_) -> 0. 89 | 90 | %% @doc O(1) comparer 91 | -spec constant(pos_integer()) -> pos_integer(). 92 | constant(_) -> 1. 93 | 94 | %% @doc O(n) comparer 95 | -spec linear(pos_integer()) -> pos_integer(). 96 | linear(N) -> N. 97 | 98 | %% @doc O(n^2) comparer 99 | -spec quadratic(pos_integer()) -> pos_integer(). 100 | quadratic(N) -> N * N. 101 | 102 | %% @doc O(log(n)) comparer 103 | -spec logarithmic(pos_integer()) -> float(). 104 | logarithmic(N) -> math:log(N) + 1. 105 | 106 | %% @doc O(n*log(n)) comparer 107 | -spec xlogarithmic(pos_integer()) -> float(). 108 | xlogarithmic(N) -> N * math:log(N) + 1. 109 | 110 | %% @doc O(e^n) comparer 111 | -spec exponential(pos_integer()) -> float(). 112 | exponential(N) -> math:pow(2.718281828459045, N). 113 | 114 | %% ==================================================================== 115 | %% Internal functions 116 | %% ==================================================================== 117 | %% @doc Runs the benchmarking function Module:Function using options. 118 | -spec run(atom(), atom(), [option()]) -> [{pos_integer(), error | pos_integer()}]. 119 | run(Module, Function, Options) -> 120 | ok = try Module:init(proplists:get_value(extra_args, Options, [])) catch _:undef -> ok end, 121 | try do_run(Module, Function, Options) 122 | after 123 | try Module:quit(proplists:get_value(extra_args, Options, [])) catch _:undef -> ok end 124 | end. 125 | 126 | do_run(Module, Function, Options) -> 127 | ok = try Module:init_per_testcase(Function, proplists:get_value(extra_args, Options, [])) catch _:undef -> ok end, 128 | Start = proplists:get_value(start, Options, 1), 129 | try lists:map(fun(N) -> do_run(Module, Function, N, Options) end, 130 | lists:seq(Start, 131 | Start + proplists:get_value(rounds, Options, 250) * 132 | proplists:get_value(step, Options, 1), 133 | proplists:get_value(step, Options, 1))) 134 | after 135 | try Module:quit_per_testcase(Function, proplists:get_value(extra_args, Options, [])) catch _:undef -> ok end 136 | end. 137 | 138 | do_run(Module, Function, N, Options) -> 139 | Items = lists:reverse(lists:map(fun edis_util:integer_to_binary/1, lists:seq(1, N))), 140 | ok = try Module:init_per_round(Function, Items, proplists:get_value(extra_args, Options, [])) catch _:undef -> ok end, 141 | try timer:tc(Module, Function, [Items | proplists:get_value(extra_args, Options, [])]) of 142 | {Time, Result} -> 143 | case proplists:get_bool(debug, Options) of 144 | true -> lager:info("~p: ~p~n\t~p~n", [N, Time/1000, Result]); 145 | false -> ok 146 | end, 147 | {N, (Time+1)/1000} 148 | catch 149 | _:Error -> 150 | lager:error("Error on ~p:~p (N: ~p):~n\t~p~n", [Module, Function, N, Error]), 151 | {N, error} 152 | after 153 | try Module:quit_per_round(Function, Items, proplists:get_value(extra_args, Options, [])) catch _:undef -> ok end 154 | end. 155 | 156 | graph(Results, Options) -> 157 | RawData = lists:sublist(Results, 158 | proplists:get_value(first_col, Options, 1), 159 | erlang:min(proplists:get_value(columns, Options, 250), 160 | proplists:get_value(rounds, Options, 250))), 161 | case proplists:get_bool(debug, Options) of 162 | true -> lager:info("RawData:~n\t~p~n", [RawData]); 163 | false -> ok 164 | end, 165 | Data = remove_outliers(RawData, Options), 166 | Top = lists:max([erlang:max(V, M) || {_, V, M} <- Data]), 167 | Bottom = erlang:trunc(lists:min([erlang:min(V, M) || {_, V, M} <- Data, V > 0, M > 0]) / 2), 168 | Step = 169 | case {Top, Bottom} of 170 | {error, _} -> throw(everything_is_an_error); 171 | {_, error} -> throw(everything_is_an_error); 172 | _ -> 173 | (Top - Bottom) / proplists:get_value(rows, Options, 70) 174 | end, 175 | graph(Top, Bottom, Step, proplists:get_value(symbols, Options, #symbols{}), Data). 176 | 177 | remove_outliers(RawData, Options) -> 178 | SortedBy2 = lists:keysort(2, [{K, V, M} || {K, V, M} <- RawData, V =/= error, M =/= error]), 179 | SortedBy3 = lists:keysort(3, [{K, V, M} || {K, V, M} <- RawData, V =/= error, M =/= error]), 180 | Outliers = 181 | [{K, error, M} || {K, error, M} <- RawData] ++ [{K, V, error} || {K, V, error} <- RawData] ++ 182 | lists:sublist(lists:reverse(SortedBy2), 1, erlang:trunc(proplists:get_value(outliers, Options, 20) / 2) + 1) ++ 183 | lists:sublist(lists:reverse(SortedBy3), 1, erlang:trunc(proplists:get_value(outliers, Options, 20) / 2) + 1), 184 | [case lists:member({K,V,M}, Outliers) of 185 | true -> {K, 0, M}; 186 | false -> {K, V, M} 187 | end || {K,V,M} <- RawData]. 188 | 189 | graph(Top, Bottom, _Step, _Symbols, Data) when Top =< Bottom -> 190 | io:format(" ~s~n", [lists:duplicate(length(Data), $-)]), 191 | io:format(" ~s~n", [lists:map(fun({K, _, _}) -> integer_to_list(K rem 10) end, Data)]); 192 | graph(Top, Bottom, Step, Symbols, Data) -> 193 | io:format("~7.2.0f~s~n", 194 | [Top * 1.0, 195 | lists:map( 196 | fun({_, V, M}) when Top >= V, V > Top - Step, 197 | Top >= M, M > Top - Step -> 198 | case {Top - V, Top - M} of 199 | {Pos, Mos} when Pos < Step/2, Mos < Step/2 -> Symbols#symbols.up_up; 200 | {Pos, Mos} when Pos < Step/2, Mos >= Step/2 -> Symbols#symbols.up_down; 201 | {Pos, Mos} when Pos >= Step/2, Mos < Step/2 -> Symbols#symbols.down_up; 202 | {Pos, Mos} when Pos >= Step/2, Mos >= Step/2 -> Symbols#symbols.down_down 203 | end; 204 | ({_, V, _M}) when Top >= V, V > Top - Step -> 205 | case Top - V of 206 | Pos when Pos < Step/2 -> Symbols#symbols.up_none; 207 | Pos when Pos >= Step/2 -> Symbols#symbols.down_none 208 | end; 209 | ({_, _V, M}) when Top >= M, M > Top - Step -> 210 | case Top - M of 211 | Pos when Pos < Step/2 -> Symbols#symbols.none_up; 212 | Pos when Pos >= Step/2 -> Symbols#symbols.none_down 213 | end; 214 | (_) -> Symbols#symbols.none_none 215 | end, Data)]), 216 | graph(Top-Step, Bottom, Step, Symbols, Data). -------------------------------------------------------------------------------- /src/edis_client.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Fernando Benavides 3 | %%% @author Chad DePue 4 | %%% @copyright (C) 2011 InakaLabs SRL 5 | %%% @doc edis client FSM 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(edis_client). 9 | -author('Fernando Benavides '). 10 | -author('Chad DePue '). 11 | 12 | -behaviour(gen_fsm). 13 | 14 | -export([start_link/0, set_socket/2]). 15 | -export([init/1, handle_event/3, handle_sync_event/4, handle_info/3, terminate/3, code_change/4]). 16 | -export([socket/2, command_start/2, arg_size/2, command_name/2, argument/2]). 17 | -export([disconnect/1]). 18 | 19 | -include("edis.hrl"). 20 | 21 | -define(FSM_TIMEOUT, 60000). 22 | 23 | -record(state, {socket :: undefined | port(), 24 | peerport :: undefined | pos_integer(), 25 | missing_args = 0 :: non_neg_integer(), 26 | next_arg_size :: undefined | integer(), 27 | command_name :: undefined | binary(), 28 | args = [] :: [binary()], 29 | buffer = <<>> :: binary(), 30 | command_runner :: undefined | pid() 31 | }). 32 | -opaque state() :: #state{}. 33 | 34 | %% ==================================================================== 35 | %% External functions 36 | %% ==================================================================== 37 | %% -- General --------------------------------------------------------- 38 | %% @doc Start the client 39 | -spec start_link() -> {ok, pid()}. 40 | start_link() -> 41 | gen_fsm:start_link(?MODULE, [], []). 42 | 43 | %% @doc Associates the client with the socket 44 | -spec set_socket(pid(), port()) -> ok. 45 | set_socket(Client, Socket) -> 46 | gen_fsm:send_event(Client, {socket_ready, Socket}). 47 | 48 | %% @doc Stop the client 49 | -spec disconnect(pid()) -> ok. 50 | disconnect(Client) -> 51 | gen_fsm:send_event(Client, disconnect). 52 | 53 | %% ==================================================================== 54 | %% Server functions 55 | %% ==================================================================== 56 | %% @hidden 57 | -spec init([]) -> {ok, socket, state(), ?FSM_TIMEOUT}. 58 | init([]) -> 59 | {ok, socket, #state{}, ?FSM_TIMEOUT}. 60 | 61 | %% ASYNC EVENTS ------------------------------------------------------- 62 | %% @hidden 63 | -spec socket({socket_ready, port()} | timeout | term(), state()) -> {next_state, command_start, state(), hibernate} | {stop, timeout | {unexpected_event, term()}, state()}. 64 | socket({socket_ready, Socket}, State) -> 65 | % Now we own the socket 66 | PeerPort = 67 | case inet:peername(Socket) of 68 | {ok, {_Ip, Port}} -> Port; 69 | Error -> Error 70 | end, 71 | lager:info("New Client: ~p ~n", [PeerPort]), 72 | ok = inet:setopts(Socket, [{active, once}, {packet, line}, binary]), 73 | _ = erlang:process_flag(trap_exit, true), %% We want to know even if it stops normally 74 | {ok, CmdRunner} = edis_command_runner:start_link(Socket), 75 | {next_state, command_start, State#state{socket = Socket, 76 | peerport = PeerPort, 77 | command_runner = CmdRunner}, hibernate}; 78 | socket(timeout, State) -> 79 | lager:alert("Timeout~n", []), 80 | {stop, timeout, State}; 81 | socket(Other, State) -> 82 | lager:alert("Unexpected message: ~p\n", [Other]), 83 | {stop, {unexpected_event, Other}, State}. 84 | 85 | %% @hidden 86 | -spec command_start(term(), state()) -> {next_state, command_start, state(), hibernate} | {next_state, arg_size | argument, state()} | {stop, {unexpected_event, term()}, state()}. 87 | command_start({data, <<"\r\n">>}, State) -> 88 | {next_state, command_start, State}; 89 | command_start({data, <<"\n">>}, State) -> 90 | {next_state, command_start, State}; 91 | command_start({data, <<"*", N/binary>>}, State) -> %% Unified Request Protocol 92 | {NArgs, "\r\n"} = string:to_integer(binary_to_list(N)), 93 | lager:debug("URP command - ~p args~n",[NArgs]), 94 | {next_state, arg_size, State#state{missing_args = NArgs, command_name = undefined, args = []}}; 95 | command_start({data, OldCmd}, State) -> 96 | [Command|Args] = binary:split(OldCmd, [<<" ">>, <<"\r\n">>], [global,trim]), 97 | lager:debug("Old protocol command ~p (~p args)~n",[Command, length(Args)]), 98 | case edis_command_runner:last_arg(edis_util:upper(Command)) of 99 | inlined -> 100 | ok = edis_command_runner:run(State#state.command_runner, edis_util:upper(Command), Args), 101 | {next_state, command_start, State, hibernate}; 102 | safe -> 103 | case lists:reverse(Args) of 104 | [LastArg | FirstArgs] -> 105 | case string:to_integer(binary_to_list(LastArg)) of 106 | {error, no_integer} -> 107 | ok = edis_command_runner:err(State#state.command_runner, 108 | io_lib:format( 109 | "lenght of last param expected for '~s'. ~s received instead", 110 | [Command, LastArg])), 111 | {next_state, command_start, State, hibernate}; 112 | {ArgSize, _Rest} -> 113 | {next_state, argument, State#state{command_name = Command, 114 | args = lists:reverse(FirstArgs), 115 | missing_args = 1, 116 | buffer = <<>>, 117 | next_arg_size = ArgSize}} 118 | end; 119 | [] -> 120 | ok = edis_command_runner:run(State#state.command_runner, edis_util:upper(Command), []), 121 | {next_state, command_start, State, hibernate} 122 | end 123 | end; 124 | command_start(Event, State) -> 125 | lager:alert("Unexpected Event: ~p~n", [Event]), 126 | {stop, {unexpected_event, Event}, State}. 127 | 128 | %% @hidden 129 | -spec arg_size(term(), state()) -> {next_state, command_start, state(), hibernate} | {next_state, command_name | argument, state()} | {stop, {unexpected_event, term()}, state()}. 130 | arg_size({data, <<"$", N/binary>>}, State) -> 131 | case string:to_integer(binary_to_list(N)) of 132 | {error, no_integer} -> 133 | ok = edis_command_runner:err(State#state.command_runner, 134 | io_lib:format( 135 | "lenght of next arg expected. ~s received instead", [N])), 136 | {next_state, command_start, State, hibernate}; 137 | {ArgSize, _Rest} -> 138 | lager:debug("Arg Size: ~p ~n", [ArgSize]), 139 | {next_state, case State#state.command_name of 140 | undefined -> command_name; 141 | _Command -> argument 142 | end, State#state{next_arg_size = ArgSize, buffer = <<>>}} 143 | end; 144 | arg_size(Event, State) -> 145 | lager:alert("Unexpected Event: ~p~n", [Event]), 146 | {stop, {unexpected_event, Event}, State}. 147 | 148 | %% @hidden 149 | -spec command_name(term(), state()) -> {next_state, command_start, state(), hibernate} | {next_state, arg_size, state()} | {stop, {unexpected_event, term()}, state()}. 150 | command_name({data, Data}, State = #state{next_arg_size = Size, missing_args = 1}) -> 151 | <> = Data, 152 | lager:debug("Command: ~p ~n", [Command]), 153 | ok = edis_command_runner:run(State#state.command_runner, edis_util:upper(Command), []), 154 | {next_state, command_start, State, hibernate}; 155 | command_name({data, Data}, State = #state{next_arg_size = Size, missing_args = MissingArgs}) -> 156 | <> = Data, 157 | lager:debug("Command: ~p ~n", [Command]), 158 | {next_state, arg_size, State#state{command_name = Command, missing_args = MissingArgs - 1}}; 159 | command_name(Event, State) -> 160 | lager:alert("Unexpected Event: ~p~n", [Event]), 161 | {stop, {unexpected_event, Event}, State}. 162 | 163 | %% @hidden 164 | -spec argument(term(), state()) -> {next_state, command_start, state(), hibernate} | {next_state, argument | arg_size, state()} | {stop, {unexpected_event, term()}, state()}. 165 | argument({data, Data}, State = #state{buffer = Buffer, 166 | next_arg_size = Size}) -> 167 | case <> of 168 | <> -> 169 | case State#state.missing_args of 170 | 1 -> 171 | ok = edis_command_runner:run(State#state.command_runner, 172 | edis_util:upper(State#state.command_name), 173 | lists:reverse([Argument|State#state.args])), 174 | {next_state, command_start, State, hibernate}; 175 | MissingArgs -> 176 | {next_state, arg_size, State#state{missing_args = MissingArgs - 1, 177 | args = [Argument | State#state.args]}} 178 | end; 179 | NewBuffer -> %% Not the whole argument yet, just an \r\n in the middle of it 180 | {next_state, argument, State#state{buffer = NewBuffer}} 181 | end; 182 | argument(Event, State) -> 183 | lager:alert("Unexpected Event: ~p~n", [Event]), 184 | {stop, {unexpected_event, Event}, State}. 185 | 186 | %% OTHER EVENTS ------------------------------------------------------- 187 | %% @hidden 188 | -spec handle_event(X, atom(), state()) -> {stop, {atom(), unexpected_event, X}, state()}. 189 | handle_event(Event, StateName, StateData) -> 190 | {stop, {StateName, unexpected_event, Event}, StateData}. 191 | 192 | %% @hidden 193 | -spec handle_sync_event(X, reference(), atom(), state()) -> {stop, {atom(), unexpected_event, X}, state()}. 194 | handle_sync_event(Event, _From, StateName, StateData) -> 195 | {stop, {StateName, unexpected_event, Event}, StateData}. 196 | 197 | %% @hidden 198 | -spec handle_info(term(), atom(), state()) -> term(). 199 | handle_info({'EXIT', CmdRunner, Reason}, _StateName, State = #state{command_runner = CmdRunner}) -> 200 | lager:debug("Command runner stopped: ~p~n", [Reason]), 201 | {stop, Reason, State}; 202 | handle_info({tcp, Socket, Bin}, StateName, #state{socket = Socket, 203 | peerport = PeerPort} = StateData) -> 204 | % Flow control: enable forwarding of next TCP message 205 | ok = inet:setopts(Socket, [{active, false}]), 206 | lager:debug("~p >> ~s", [PeerPort, Bin]), 207 | Result = ?MODULE:StateName({data, Bin}, StateData), 208 | ok = inet:setopts(Socket, [{active, once}]), 209 | Result; 210 | handle_info({tcp_closed, Socket}, _StateName, #state{socket = Socket, 211 | peerport = PeerPort} = StateData) -> 212 | lager:debug("Disconnected ~p.~n", [PeerPort]), 213 | {stop, normal, StateData}; 214 | handle_info(_Info, StateName, StateData) -> 215 | {next_state, StateName, StateData}. 216 | 217 | %% @hidden 218 | -spec terminate(term(), atom(), state()) -> ok. 219 | terminate(normal, _StateName, #state{socket = Socket, command_runner = CmdRunner}) -> 220 | edis_command_runner:stop(CmdRunner), 221 | (catch gen_tcp:close(Socket)), 222 | ok; 223 | terminate(Reason, _StateName, #state{socket = Socket, command_runner = CmdRunner}) -> 224 | debug:warn("Terminating client: ~p~n", [Reason]), 225 | edis_command_runner:stop(CmdRunner), 226 | (catch gen_tcp:close(Socket)), 227 | ok. 228 | 229 | %% @hidden 230 | -spec code_change(term(), atom(), state(), any()) -> {ok, atom(), state()}. 231 | code_change(_OldVsn, StateName, StateData, _Extra) -> 232 | {ok, StateName, StateData}. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2015 Erlang Solutions Ltd. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/edis_vclock.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% riak_core: Core Riak Application 4 | %% 5 | %% Copyright (c) 2007-2010 Basho Technologies, Inc. All Rights Reserved. 6 | %% 7 | %% This file is provided to you under the Apache License, 8 | %% Version 2.0 (the "License"); you may not use this file 9 | %% except in compliance with the License. You may obtain 10 | %% a copy of the License at 11 | %% 12 | %% http://www.apache.org/licenses/LICENSE-2.0 13 | %% 14 | %% Unless required by applicable law or agreed to in writing, 15 | %% software distributed under the License is distributed on an 16 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 | %% KIND, either express or implied. See the License for the 18 | %% specific language governing permissions and limitations 19 | %% under the License. 20 | %% 21 | %% ------------------------------------------------------------------- 22 | 23 | %% @doc A simple Erlang implementation of vector clocks as inspired by Lamport logical clocks. 24 | %% 25 | %% @reference Leslie Lamport (1978). "Time, clocks, and the ordering of events 26 | %% in a distributed system". Communications of the ACM 21 (7): 558-565. 27 | %% 28 | %% @reference Friedemann Mattern (1988). "Virtual Time and Global States of 29 | %% Distributed Systems". Workshop on Parallel and Distributed Algorithms: 30 | %% pp. 215-226 31 | 32 | -module(edis_vclock). 33 | 34 | -export([fresh/0,descends/2,merge/1,get_counter/2,get_timestamp/2, 35 | increment/2,increment/3,all_nodes/1,equal/2,prune/3,timestamp/0]). 36 | 37 | -ifdef(TEST). 38 | -include_lib("eunit/include/eunit.hrl"). 39 | -endif. 40 | 41 | -export_type([vclock/0, timestamp/0, vclock_node/0]). 42 | 43 | -opaque vclock() :: [vc_entry()]. 44 | % The timestamp is present but not used, in case a client wishes to inspect it. 45 | -type vc_entry() :: {vclock_node(), {counter(), timestamp()}}. 46 | 47 | % Nodes can have any term() as a name, but they must differ from each other. 48 | -type vclock_node() :: term(). 49 | -type counter() :: integer(). 50 | -type timestamp() :: integer(). 51 | 52 | % @doc Create a brand new vclock. 53 | -spec fresh() -> vclock(). 54 | fresh() -> 55 | []. 56 | 57 | % @doc Return true if Va is a direct descendant of Vb, else false -- remember, a vclock is its own descendant! 58 | -spec descends(Va :: vclock()|[], Vb :: vclock()|[]) -> boolean(). 59 | descends(_, []) -> 60 | % all vclocks descend from the empty vclock 61 | true; 62 | descends(Va, Vb) -> 63 | [{NodeB, {CtrB, _T}}|RestB] = Vb, 64 | case lists:keyfind(NodeB, 1, Va) of 65 | false -> 66 | false; 67 | {_, {CtrA, _TSA}} -> 68 | (CtrA >= CtrB) andalso descends(Va,RestB) 69 | end. 70 | 71 | % @doc Combine all VClocks in the input list into their least possible 72 | % common descendant. 73 | -spec merge(VClocks :: [vclock()]) -> vclock() | []. 74 | merge([]) -> []; 75 | merge([SingleVclock]) -> SingleVclock; 76 | merge([First|Rest]) -> merge(Rest, lists:keysort(1, First)). 77 | 78 | merge([], NClock) -> NClock; 79 | merge([AClock|VClocks],NClock) -> 80 | merge(VClocks, merge(lists:keysort(1, AClock), NClock, [])). 81 | 82 | merge([], [], AccClock) -> lists:reverse(AccClock); 83 | merge([], Left, AccClock) -> lists:reverse(AccClock, Left); 84 | merge(Left, [], AccClock) -> lists:reverse(AccClock, Left); 85 | merge(V=[{Node1,{Ctr1,TS1}=CT1}=NCT1|VClock], 86 | N=[{Node2,{Ctr2,TS2}=CT2}=NCT2|NClock], AccClock) -> 87 | if Node1 < Node2 -> 88 | merge(VClock, N, [NCT1|AccClock]); 89 | Node1 > Node2 -> 90 | merge(V, NClock, [NCT2|AccClock]); 91 | true -> 92 | ({_Ctr,_TS} = CT) = if Ctr1 > Ctr2 -> CT1; 93 | Ctr1 < Ctr2 -> CT2; 94 | true -> {Ctr1, erlang:max(TS1,TS2)} 95 | end, 96 | merge(VClock, NClock, [{Node1,CT}|AccClock]) 97 | end. 98 | 99 | % @doc Get the counter value in VClock set from Node. 100 | -spec get_counter(Node :: vclock_node(), VClock :: vclock()) -> counter(). 101 | get_counter(Node, VClock) -> 102 | case lists:keyfind(Node, 1, VClock) of 103 | {_, {Ctr, _TS}} -> Ctr; 104 | false -> 0 105 | end. 106 | 107 | % @doc Get the timestamp value in a VClock set from Node. 108 | -spec get_timestamp(Node :: vclock_node(), VClock :: vclock()) -> timestamp() | undefined. 109 | get_timestamp(Node, VClock) -> 110 | case lists:keyfind(Node, 1, VClock) of 111 | {_, {_Ctr, TS}} -> TS; 112 | false -> undefined 113 | end. 114 | 115 | % @doc Increment VClock at Node. 116 | -spec increment(Node :: vclock_node(), VClock :: vclock()) -> vclock(). 117 | increment(Node, VClock) -> 118 | increment(Node, timestamp(), VClock). 119 | 120 | % @doc Increment VClock at Node. 121 | -spec increment(Node :: vclock_node(), IncTs :: timestamp(), 122 | VClock :: vclock()) -> vclock(). 123 | increment(Node, IncTs, VClock) -> 124 | {{_Ctr, _TS}=C1,NewV} = case lists:keytake(Node, 1, VClock) of 125 | false -> 126 | {{1, IncTs}, VClock}; 127 | {value, {_N, {C, _T}}, ModV} -> 128 | {{C + 1, IncTs}, ModV} 129 | end, 130 | [{Node,C1}|NewV]. 131 | 132 | 133 | % @doc Return the list of all nodes that have ever incremented VClock. 134 | -spec all_nodes(VClock :: vclock()) -> [vclock_node()]. 135 | all_nodes(VClock) -> 136 | [X || {X,{_,_}} <- VClock]. 137 | 138 | -define(DAYS_FROM_GREGORIAN_BASE_TO_EPOCH, (1970*365+478)). 139 | -define(SECONDS_FROM_GREGORIAN_BASE_TO_EPOCH, 140 | (?DAYS_FROM_GREGORIAN_BASE_TO_EPOCH * 24*60*60) 141 | %% == calendar:datetime_to_gregorian_seconds({{1970,1,1},{0,0,0}}) 142 | ). 143 | 144 | % @doc Return a timestamp for a vector clock 145 | -spec timestamp() -> timestamp(). 146 | timestamp() -> 147 | %% Same as calendar:datetime_to_gregorian_seconds(erlang:universaltime()), 148 | %% but significantly faster. 149 | {MegaSeconds, Seconds, _} = os:timestamp(), 150 | ?SECONDS_FROM_GREGORIAN_BASE_TO_EPOCH + MegaSeconds*1000000 + Seconds. 151 | 152 | % @doc Compares two VClocks for equality. 153 | -spec equal(VClockA :: vclock(), VClockB :: vclock()) -> boolean(). 154 | equal(VA,VB) -> 155 | lists:sort(VA) =:= lists:sort(VB). 156 | 157 | % @doc Possibly shrink the size of a vclock, depending on current age and size. 158 | -spec prune(V::vclock(), Now::integer(), BucketProps::term()) -> vclock(). 159 | prune(V,Now,BucketProps) -> 160 | %% This sort need to be deterministic, to avoid spurious merge conflicts later. 161 | %% We achieve this by using the node ID as secondary key. 162 | SortV = lists:sort(fun({N1,{_,T1}},{N2,{_,T2}}) -> {T1,N1} < {T2,N2} end, V), 163 | prune_vclock1(SortV,Now,BucketProps). 164 | % @private 165 | prune_vclock1(V,Now,BProps) -> 166 | case length(V) =< get_property(small_vclock, BProps) of 167 | true -> V; 168 | false -> 169 | {_,{_,HeadTime}} = hd(V), 170 | case (Now - HeadTime) < get_property(young_vclock,BProps) of 171 | true -> V; 172 | false -> prune_vclock1(V,Now,BProps,HeadTime) 173 | end 174 | end. 175 | % @private 176 | prune_vclock1(V,Now,BProps,HeadTime) -> 177 | % has a precondition that V is longer than small and older than young 178 | case (length(V) > get_property(big_vclock,BProps)) orelse 179 | ((Now - HeadTime) > get_property(old_vclock,BProps)) of 180 | true -> prune_vclock1(tl(V),Now,BProps); 181 | false -> V 182 | end. 183 | 184 | get_property(Key, PairList) -> 185 | case lists:keyfind(Key, 1, PairList) of 186 | {_Key, Value} -> 187 | Value; 188 | false -> 189 | undefined 190 | end. 191 | 192 | %% =================================================================== 193 | %% EUnit tests 194 | %% =================================================================== 195 | -ifdef(TEST). 196 | 197 | % doc Serves as both a trivial test and some example code. 198 | example_test() -> 199 | A = vclock:fresh(), 200 | B = vclock:fresh(), 201 | A1 = vclock:increment(a, A), 202 | B1 = vclock:increment(b, B), 203 | true = vclock:descends(A1,A), 204 | true = vclock:descends(B1,B), 205 | false = vclock:descends(A1,B1), 206 | A2 = vclock:increment(a, A1), 207 | C = vclock:merge([A2, B1]), 208 | C1 = vclock:increment(c, C), 209 | true = vclock:descends(C1, A2), 210 | true = vclock:descends(C1, B1), 211 | false = vclock:descends(B1, C1), 212 | false = vclock:descends(B1, A1), 213 | ok. 214 | 215 | prune_small_test() -> 216 | % vclock with less entries than small_vclock will be untouched 217 | Now = riak_core_util:moment(), 218 | OldTime = Now - 32000000, 219 | SmallVC = [{<<"1">>, {1, OldTime}}, 220 | {<<"2">>, {2, OldTime}}, 221 | {<<"3">>, {3, OldTime}}], 222 | Props = [{small_vclock,4}], 223 | ?assertEqual(lists:sort(SmallVC), lists:sort(prune(SmallVC, Now, Props))). 224 | 225 | prune_young_test() -> 226 | % vclock with all entries younger than young_vclock will be untouched 227 | Now = riak_core_util:moment(), 228 | NewTime = Now - 1, 229 | VC = [{<<"1">>, {1, NewTime}}, 230 | {<<"2">>, {2, NewTime}}, 231 | {<<"3">>, {3, NewTime}}], 232 | Props = [{small_vclock,1},{young_vclock,1000}], 233 | ?assertEqual(lists:sort(VC), lists:sort(prune(VC, Now, Props))). 234 | 235 | prune_big_test() -> 236 | % vclock not preserved by small or young will be pruned down to 237 | % no larger than big_vclock entries 238 | Now = riak_core_util:moment(), 239 | NewTime = Now - 1000, 240 | VC = [{<<"1">>, {1, NewTime}}, 241 | {<<"2">>, {2, NewTime}}, 242 | {<<"3">>, {3, NewTime}}], 243 | Props = [{small_vclock,1},{young_vclock,1}, 244 | {big_vclock,2},{old_vclock,100000}], 245 | ?assert(length(prune(VC, Now, Props)) =:= 2). 246 | 247 | prune_old_test() -> 248 | % vclock not preserved by small or young will be pruned down to 249 | % no larger than big_vclock and no entries more than old_vclock ago 250 | Now = riak_core_util:moment(), 251 | NewTime = Now - 1000, 252 | OldTime = Now - 100000, 253 | VC = [{<<"1">>, {1, NewTime}}, 254 | {<<"2">>, {2, OldTime}}, 255 | {<<"3">>, {3, OldTime}}], 256 | Props = [{small_vclock,1},{young_vclock,1}, 257 | {big_vclock,2},{old_vclock,10000}], 258 | ?assert(length(prune(VC, Now, Props)) =:= 1). 259 | 260 | prune_order_test() -> 261 | % vclock with two nodes of the same timestamp will be pruned down 262 | % to the same node 263 | Now = riak_core_util:moment(), 264 | OldTime = Now - 100000, 265 | VC1 = [{<<"1">>, {1, OldTime}}, 266 | {<<"2">>, {2, OldTime}}], 267 | VC2 = lists:reverse(VC1), 268 | Props = [{small_vclock,1},{young_vclock,1}, 269 | {big_vclock,2},{old_vclock,10000}], 270 | ?assertEqual(prune(VC1, Now, Props), prune(VC2, Now, Props)). 271 | 272 | accessor_test() -> 273 | VC = [{<<"1">>, {1, 1}}, 274 | {<<"2">>, {2, 2}}], 275 | ?assertEqual(1, get_counter(<<"1">>, VC)), 276 | ?assertEqual(1, get_timestamp(<<"1">>, VC)), 277 | ?assertEqual(2, get_counter(<<"2">>, VC)), 278 | ?assertEqual(2, get_timestamp(<<"2">>, VC)), 279 | ?assertEqual(0, get_counter(<<"3">>, VC)), 280 | ?assertEqual(undefined, get_timestamp(<<"3">>, VC)), 281 | ?assertEqual([<<"1">>, <<"2">>], all_nodes(VC)). 282 | 283 | merge_test() -> 284 | VC1 = [{<<"1">>, {1, 1}}, 285 | {<<"2">>, {2, 2}}, 286 | {<<"4">>, {4, 4}}], 287 | VC2 = [{<<"3">>, {3, 3}}, 288 | {<<"4">>, {3, 3}}], 289 | ?assertEqual([], merge(vclock:fresh())), 290 | ?assertEqual([{<<"1">>,{1,1}},{<<"2">>,{2,2}},{<<"3">>,{3,3}},{<<"4">>,{4,4}}], 291 | merge([VC1, VC2])). 292 | 293 | merge_less_left_test() -> 294 | VC1 = [{<<"5">>, {5, 5}}], 295 | VC2 = [{<<"6">>, {6, 6}}, {<<"7">>, {7, 7}}], 296 | ?assertEqual([{<<"5">>, {5, 5}},{<<"6">>, {6, 6}}, {<<"7">>, {7, 7}}], 297 | vclock:merge([VC1, VC2])). 298 | 299 | merge_less_right_test() -> 300 | VC1 = [{<<"6">>, {6, 6}}, {<<"7">>, {7, 7}}], 301 | VC2 = [{<<"5">>, {5, 5}}], 302 | ?assertEqual([{<<"5">>, {5, 5}},{<<"6">>, {6, 6}}, {<<"7">>, {7, 7}}], 303 | vclock:merge([VC1, VC2])). 304 | 305 | merge_same_id_test() -> 306 | VC1 = [{<<"1">>, {1, 2}},{<<"2">>,{1,4}}], 307 | VC2 = [{<<"1">>, {1, 3}},{<<"3">>,{1,5}}], 308 | ?assertEqual([{<<"1">>, {1, 3}},{<<"2">>,{1,4}},{<<"3">>,{1,5}}], 309 | vclock:merge([VC1, VC2])). 310 | 311 | -endif. --------------------------------------------------------------------------------