├── .gitignore ├── .travis.yml ├── AUTHORS ├── CHANGELOG.md ├── Emakefile.src ├── LICENSE ├── Makefile ├── README.md ├── include ├── eredis.hrl └── eredis_sub.hrl ├── mix.exs ├── priv ├── basho_bench_eredis.config ├── basho_bench_eredis_pipeline.config └── basho_bench_erldis.config ├── rebar.config ├── rebar.lock ├── src ├── basho_bench_driver_eredis.erl ├── basho_bench_driver_erldis.erl ├── eredis.app.src ├── eredis.erl ├── eredis_client.erl ├── eredis_parser.erl ├── eredis_sub.erl └── eredis_sub_client.erl └── test ├── eredis_parser_tests.erl ├── eredis_sub_tests.erl └── eredis_tests.erl /.gitignore: -------------------------------------------------------------------------------- 1 | ebin/* 2 | .eunit 3 | *~ 4 | Emakefile 5 | .rebar 6 | .DS_Store 7 | doc 8 | _build 9 | erl_crash.dump 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | sudo: required 3 | notifications: 4 | email: false 5 | otp_release: 6 | - 21.0.1 7 | - 20.3 8 | - 19.3 9 | - 19.2 10 | - 19.1 11 | - 19.0 12 | - 18.3 13 | - 18.2.1 14 | - 18.1 15 | - 17.5 16 | - 17.4 17 | - 17.3 18 | - 17.1 19 | - 17.0 20 | - R16B03-1 21 | - R16B03 22 | - R16B02 23 | - R16B01 24 | services: 25 | - redis-server 26 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Knut Nesheim 2 | tomlion 3 | Metin Akat 4 | Ville Tuulos 5 | adzeitor 6 | Valentino Volonghi 7 | Dave Peticolas 8 | Ransom Richardson 9 | Michael Gregson 10 | Matthew Conway 11 | Aleksey Morarash 12 | Mikl Kurkov 13 | Seth Falcon -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v1.1.0 4 | 5 | * Merged a ton of of old and neglected pull requests. Thanks to 6 | patient contributors: 7 | * Emil Falk 8 | * Evgeny Khramtsov 9 | * Kevin Wilson 10 | * Luis Rascão 11 | * Аверьянов Илья (savonarola on github) 12 | * ololoru 13 | * Giacomo Olgeni 14 | 15 | * Removed rebar binary, made everything a bit more rebar3 & mix 16 | friendly. 17 | 18 | 19 | ## v1.0.8 20 | 21 | * Fixed include directive to work with rebar 2.5.1. Thanks to Feng Hao 22 | for the patch. 23 | 24 | ## v1.0.7 25 | 26 | * If an eredis_sub_client needs to reconnect to Redis, the controlling 27 | process is now notified with the message `{eredis_reconnect_attempt, 28 | Pid}`. If the reconnection attempt fails, the message is 29 | `{eredis_reconnect_failed, Pid, Reason}`. Thanks to Piotr Nosek for 30 | the patch. 31 | 32 | * No more deprecation warnings of the `queue` type on OTP 17. Thanks 33 | to Daniel Kempkens for the patch. 34 | 35 | * Various spec fixes. Thanks to Hernan Rivas Acosta and Anton Kalyaev. 36 | 37 | ## v1.0.6 38 | 39 | * If the connection to Redis is lost, requests in progress will 40 | receive `{error, tcp_closed}` instead of the `gen_server:call` 41 | timing out. Thanks to Seth Falcon for the patch. 42 | 43 | ## v1.0.5 44 | 45 | * Added support for not selecting any specific database. Thanks to 46 | Mikl Kurkov for the patch. 47 | 48 | ## v1.0.4 49 | 50 | * Added `eredis:q_noreply/2` which sends a fire-and-forget request to 51 | Redis. Thanks to Ransom Richardson for the patch. 52 | 53 | * Various type annotation improvements, typo fixes and robustness 54 | improvements. Thanks to Michael Gregson, Matthew Conway and Ransom 55 | Richardson. 56 | 57 | ## v1.0.3 58 | 59 | * Fixed bug in eredis_sub where when the connection to Redis was lost, 60 | the socket would not be set into {active, once} on reconnect. Thanks 61 | to georgeye for the patch. 62 | 63 | ## v1.0.2 64 | 65 | * Fixed bug in eredis_sub where the socket was incorrectly set to 66 | `{active, once}` twice. At large volumes of messages, this resulted 67 | in too many messages from the socket and we would be unable to keep 68 | up. Thanks to pmembrey for reporting. 69 | 70 | ## v1.0 71 | 72 | * Support added for pubsub thanks to Dave Peticolas 73 | (jdavisp3). Implemented in `eredis_sub` and `eredis_sub_client` is a 74 | subscriber that will forward messages from Redis to an Erlang 75 | process with flow control. The user can configure to either drop 76 | messages or crash the driver if a certain queue size inside the 77 | driver is reached. 78 | 79 | * Fixed error handling when eredis starts up and Redis is still 80 | loading the dataset into memory. 81 | 82 | ## v0.7.0 83 | 84 | * Support added for pipelining requests, which allows batching 85 | multiple requests in a single call to eredis. Thanks to Dave 86 | Peticolas (jdavisp3) for the implementation. 87 | 88 | ## v0.6.0 89 | 90 | * Support added for transactions, by Dave Peticolas (jdavisp3) who implemented 91 | parsing of nested multibulks. 92 | 93 | ## v0.5.0 94 | 95 | * Configurable reconnect sleep time, by Valentino Volonghi (dialtone) 96 | 97 | * Support for using eredis as a poolboy worker, by Valentino Volonghi 98 | (dialtone) 99 | -------------------------------------------------------------------------------- /Emakefile.src: -------------------------------------------------------------------------------- 1 | {['src/eredis*'], 2 | [{outdir, "ebin"}, 3 | {i, "include"}, 4 | {{EXTRA_OPTS}} 5 | debug_info, 6 | warn_unused_function, 7 | warn_bif_clash, 8 | warn_deprecated_function, 9 | warn_obsolete_guard, 10 | warn_shadow_vars, 11 | warn_export_vars, 12 | warn_unused_records, 13 | warn_unused_import 14 | ]}. 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2011 wooga GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | APP=eredis 2 | 3 | PRE17 := $(shell ERL_FLAGS="" erl -eval 'io:format("~s~n", [case re:run(erlang:system_info(otp_release), "^R") of nomatch -> ""; _ -> pre17 end]), halt().' -noshell) 4 | 5 | .PHONY: all compile clean Emakefile 6 | 7 | all: compile 8 | 9 | compile: ebin/$(APP).app Emakefile 10 | erl -noinput -eval 'up_to_date = make:all()' -s erlang halt 11 | 12 | clean: 13 | rm -f -- ebin/*.beam Emakefile ebin/$(APP).app 14 | 15 | ebin/$(APP).app: src/$(APP).app.src 16 | mkdir -p ebin 17 | cp -f -- $< $@ 18 | 19 | ifdef DEBUG 20 | EXTRA_OPTS:=debug_info, 21 | endif 22 | 23 | ifdef TEST 24 | EXTRA_OPTS:=$(EXTRA_OPTS) {d,'TEST', true}, 25 | endif 26 | 27 | ifndef PRE17 28 | EXTRA_OPTS:=$(EXTRA_OPTS) {d,namespaced_types}, 29 | endif 30 | 31 | Emakefile: Emakefile.src 32 | sed "s/{{EXTRA_OPTS}}/$(EXTRA_OPTS)/" $< > $@ 33 | 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eredis 2 | 3 | Non-blocking Redis client with focus on performance and robustness. 4 | 5 | Supported Redis features: 6 | 7 | * Any command, through eredis:q/2 8 | * Transactions 9 | * Pipelining 10 | * Authentication & multiple dbs 11 | * Pubsub 12 | 13 | ## Example 14 | 15 | If you have Redis running on localhost, with default settings, you may 16 | copy and paste the following into a shell to try out Eredis: 17 | 18 | git clone git://github.com/wooga/eredis.git 19 | cd eredis 20 | ./rebar compile 21 | erl -pa ebin/ 22 | {ok, C} = eredis:start_link(). 23 | {ok, <<"OK">>} = eredis:q(C, ["SET", "foo", "bar"]). 24 | {ok, <<"bar">>} = eredis:q(C, ["GET", "foo"]). 25 | 26 | To connect to a Redis instance listening on a Unix domain socket: 27 | 28 | {ok, C1} = eredis:start_link({local, "/var/run/redis.sock"}, 0). 29 | 30 | MSET and MGET: 31 | 32 | ```erlang 33 | KeyValuePairs = ["key1", "value1", "key2", "value2", "key3", "value3"]. 34 | {ok, <<"OK">>} = eredis:q(C, ["MSET" | KeyValuePairs]). 35 | {ok, Values} = eredis:q(C, ["MGET" | ["key1", "key2", "key3"]]). 36 | ``` 37 | 38 | HASH 39 | 40 | ```erlang 41 | HashObj = ["id", "objectId", "message", "message", "receiver", "receiver", "status", "read"]. 42 | eredis:q(C, ["HMSET", "key" | HashObj]). 43 | {ok, Values} = eredis:q(C, ["HGETALL", "key"]). 44 | ``` 45 | 46 | LIST 47 | 48 | ```erlang 49 | eredis:q(C, ["LPUSH", "keylist", "value"]). 50 | eredis:q(C, ["RPUSH", "keylist", "value"]). 51 | eredis:q(C, ["LRANGE", "keylist",0,-1]). 52 | ``` 53 | 54 | Transactions: 55 | 56 | ```erlang 57 | {ok, <<"OK">>} = eredis:q(C, ["MULTI"]). 58 | {ok, <<"QUEUED">>} = eredis:q(C, ["SET", "foo", "bar"]). 59 | {ok, <<"QUEUED">>} = eredis:q(C, ["SET", "bar", "baz"]). 60 | {ok, [<<"OK">>, <<"OK">>]} = eredis:q(C, ["EXEC"]). 61 | ``` 62 | 63 | Pipelining: 64 | 65 | ```erlang 66 | P1 = [["SET", a, "1"], 67 | ["LPUSH", b, "3"], 68 | ["LPUSH", b, "2"]]. 69 | [{ok, <<"OK">>}, {ok, <<"1">>}, {ok, <<"2">>}] = eredis:qp(C, P1). 70 | ``` 71 | 72 | Pubsub: 73 | 74 | ```erl 75 | 1> eredis_sub:sub_example(). 76 | received {subscribed,<<"foo">>,<0.34.0>} 77 | {<0.34.0>,<0.37.0>} 78 | 2> eredis_sub:pub_example(). 79 | received {message,<<"foo">>,<<"bar">>,<0.34.0>} 80 | ``` 81 | 82 | Pattern Subscribe: 83 | 84 | ```erl 85 | 1> eredis_sub:psub_example(). 86 | received {subscribed,<<"foo*">>,<0.33.0>} 87 | {<0.33.0>,<0.36.0>} 88 | 2> eredis_sub:ppub_example(). 89 | received {pmessage,<<"foo*">>,<<"foo123">>,<<"bar">>,<0.33.0>} 90 | ok 91 | 3> 92 | ``` 93 | 94 | EUnit tests: 95 | 96 | ```console 97 | ./rebar eunit 98 | ``` 99 | 100 | 101 | ## Commands 102 | 103 | Eredis has one main function to interact with redis, which is 104 | `eredis:q(Client::pid(), Command::iolist())`. The response will either 105 | be `{ok, Value::binary() | [binary()]}` or `{error, 106 | Message::binary()}`. The value is always the exact value returned by 107 | Redis, without any type conversion. If Redis returns a list of values, 108 | this list is returned in the exact same order without any type 109 | conversion. 110 | 111 | To send multiple requests to redis in a batch, aka. pipelining 112 | requests, you may use `eredis:qp(Client::pid(), 113 | [Command::iolist()])`. This function returns `{ok, [Value::binary()]}` 114 | where the values are the redis responses in the same order as the 115 | commands you provided. 116 | 117 | To start the client, use any of the `eredis:start_link/0,1,2,3,4,5,6,7` 118 | functions. They all include sensible defaults. `start_link/7` takes 119 | the following arguments: 120 | 121 | * Host, dns name or ip adress as string; or unix domain socket as {local, Path} (available in OTP 19+) 122 | * Port, integer, default is 6379 123 | * Database, integer or 0 for default database 124 | * Password, string or empty string([]) for no password 125 | * Reconnect sleep, integer of milliseconds to sleep between reconnect attempts 126 | * Connect timeout, timeout value in milliseconds to use in `gen_tcp:connect`, default is 5000 127 | * Socket options, proplist of options to be sent to `gen_tcp:connect`, default is `?SOCKET_OPTS` 128 | 129 | ## Reconnecting on Redis down / network failure / timeout / etc 130 | 131 | When Eredis for some reason looses the connection to Redis, Eredis 132 | will keep trying to reconnect until a connection is successfully 133 | established, which includes the `AUTH` and `SELECT` calls. The sleep 134 | time between attempts to reconnect can be set in the 135 | `eredis:start_link/5` call. 136 | 137 | As long as the connection is down, Eredis will respond to any request 138 | immediately with `{error, no_connection}` without actually trying to 139 | connect. This serves as a kind of circuit breaker and prevents a 140 | stampede of clients just waiting for a failed connection attempt or 141 | `gen_server:call` timeout. 142 | 143 | Note: If Eredis is starting up and cannot connect, it will fail 144 | immediately with `{connection_error, Reason}`. 145 | 146 | ## Pubsub 147 | 148 | Thanks to Dave Peticolas (jdavisp3), eredis supports 149 | pubsub. `eredis_sub` offers a separate client that will forward 150 | channel messages from Redis to an Erlang process in a "active-once" 151 | pattern similar to gen_tcp sockets. After every message sent, the 152 | controlling process must acknowledge receipt using 153 | `eredis_sub:ack_message/1`. 154 | 155 | If the controlling process does not process messages fast enough, 156 | eredis will queue the messages up to a certain queue size controlled 157 | by configuration. When the max size is reached, eredis will either 158 | drop messages or crash, also based on configuration. 159 | 160 | Subscriptions are managed using `eredis_sub:subscribe/2` and 161 | `eredis_sub:unsubscribe/2`. When Redis acknowledges the change in 162 | subscription, a message is sent to the controlling process for each 163 | channel. 164 | 165 | eredis also supports Pattern Subscribe using `eredis_sub:psubscribe/2` 166 | and `eredis_sub:unsubscribe/2`. As with normal subscriptions, a message 167 | is sent to the controlling process for each channel. 168 | 169 | As of v1.0.7 the controlling process will be notified in case of 170 | reconnection attempts or failures. See `test/eredis_sub_tests` for 171 | details. 172 | 173 | ## AUTH and SELECT 174 | 175 | Eredis also implements the AUTH and SELECT calls for you. When the 176 | client is started with something else than default values for password 177 | and database, it will issue the `AUTH` and `SELECT` commands 178 | appropriately, even when reconnecting after a timeout. 179 | 180 | 181 | ## Benchmarking 182 | 183 | Using basho_bench(https://github.com/basho/basho_bench/) you may 184 | benchmark Eredis on your own hardware using the provided config and 185 | driver. See `priv/basho_bench_driver_eredis.config` and 186 | `src/basho_bench_driver_eredis.erl`. 187 | 188 | ## Queueing 189 | 190 | Eredis uses the same queueing mechanism as Erldis. `eredis:q/2` uses 191 | `gen_server:call/2` to do a blocking call to the client 192 | gen_server. The client will immediately send the request to Redis, add 193 | the caller to the queue and reply with `noreply`. This frees the 194 | gen_server up to accept new requests and parse responses as they come 195 | on the socket. 196 | 197 | When data is received on the socket, we call `eredis_parser:parse/2` 198 | until it returns a value, we then use `gen_server:reply/2` to reply to 199 | the first process waiting in the queue. 200 | 201 | This queueing mechanism works because Redis guarantees that the 202 | response will be in the same order as the requests. 203 | 204 | ## Response parsing 205 | 206 | The response parser is the biggest difference between Eredis and other 207 | libraries like Erldis, redis-erl and redis_pool. The common approach 208 | is to either directly block or use active once to get the first part 209 | of the response, then repeatedly use `gen_tcp:recv/2` to get more data 210 | when needed. Profiling identified this as a bottleneck, in particular 211 | for `MGET` and `HMGET`. 212 | 213 | To be as fast as possible, Eredis takes a different approach. The 214 | socket is always set to active once, which will let us receive data 215 | fast without blocking the gen_server. The tradeoff is that we must 216 | parse partial responses, which makes the parser more complex. 217 | 218 | In order to make multibulk responses more efficient, the parser 219 | will parse all data available and continue where it left off when more 220 | data is available. 221 | 222 | ## Future improvements 223 | 224 | When the parser is accumulating data, a new binary is generated for 225 | every call to `parse/2`. This might create binaries that will be 226 | reference counted. This could be improved by replacing it with an 227 | iolist. 228 | 229 | When parsing bulk replies, the parser knows the size of the bulk. If the 230 | bulk is big and would come in many chunks, this could improved by 231 | having the client explicitly use `gen_tcp:recv/2` to fetch the entire 232 | bulk at once. 233 | 234 | ## Credits 235 | 236 | Although this project is almost a complete rewrite, many patterns are 237 | the same as you find in Erldis, most notably the queueing of requests. 238 | 239 | `create_multibulk/1` and `to_binary/1` were taken verbatim from Erldis. 240 | -------------------------------------------------------------------------------- /include/eredis.hrl: -------------------------------------------------------------------------------- 1 | %% Public types 2 | 3 | -type reconnect_sleep() :: no_reconnect | integer(). 4 | 5 | -type option() :: {host, string()} | {port, integer()} | {database, string()} | {password, string()} | {reconnect_sleep, reconnect_sleep()}. 6 | -type server_args() :: [option()]. 7 | 8 | -type return_value() :: undefined | binary() | [binary() | nonempty_list()]. 9 | 10 | -type pipeline() :: [iolist()]. 11 | 12 | -type channel() :: binary(). 13 | 14 | %% Continuation data is whatever data returned by any of the parse 15 | %% functions. This is used to continue where we left off the next time 16 | %% the user calls parse/2. 17 | -type continuation_data() :: any(). 18 | -type parser_state() :: status_continue | bulk_continue | multibulk_continue. 19 | 20 | %% Internal types 21 | -ifdef(namespaced_types). 22 | -type eredis_queue() :: queue:queue(). 23 | -else. 24 | -type eredis_queue() :: queue(). 25 | -endif. 26 | 27 | %% Internal parser state. Is returned from parse/2 and must be 28 | %% included on the next calls to parse/2. 29 | -record(pstate, { 30 | state = undefined :: parser_state() | undefined, 31 | continuation_data :: continuation_data() | undefined 32 | }). 33 | 34 | -define(NL, "\r\n"). 35 | 36 | -define(SOCKET_MODE, binary). 37 | -define(SOCKET_OPTS, [{active, once}, {packet, raw}, {reuseaddr, false}, 38 | {keepalive, false}, {send_timeout, ?SEND_TIMEOUT}]). 39 | 40 | -define(RECV_TIMEOUT, 5000). 41 | -define(SEND_TIMEOUT, 5000). 42 | -------------------------------------------------------------------------------- /include/eredis_sub.hrl: -------------------------------------------------------------------------------- 1 | %% State in eredis_sub_client 2 | -record(state, { 3 | host :: string() | undefined, 4 | port :: integer() | undefined, 5 | password :: binary() | undefined, 6 | reconnect_sleep :: integer() | undefined | no_reconnect, 7 | 8 | socket :: port() | undefined, 9 | parser_state :: #pstate{} | undefined, 10 | 11 | %% Channels we should subscribe to 12 | channels = [] :: [channel()], 13 | 14 | % The process we send pubsub and connection state messages to. 15 | controlling_process :: undefined | {reference(), pid()}, 16 | 17 | % This is the queue of messages to send to the controlling 18 | % process. 19 | msg_queue :: eredis_queue(), 20 | 21 | %% When the queue reaches this size, either drop all 22 | %% messages or exit. 23 | max_queue_size :: integer() | inifinity, 24 | queue_behaviour :: drop | exit, 25 | 26 | % The msg_state keeps track of whether we are waiting 27 | % for the controlling process to acknowledge the last 28 | % message. 29 | msg_state = need_ack :: ready | need_ack 30 | }). 31 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Eredis.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :eredis, 7 | version: "1.2.0", 8 | elixir: "~> 1.5.1", 9 | start_permanent: Mix.env == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [] 17 | end 18 | 19 | # Run "mix help deps" to learn about dependencies. 20 | defp deps do 21 | [] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /priv/basho_bench_eredis.config: -------------------------------------------------------------------------------- 1 | {mode, max}. 2 | %{mode, {rate, 5}}. 3 | 4 | {duration, 15}. 5 | 6 | {concurrent, 30}. 7 | 8 | {driver, basho_bench_driver_eredis}. 9 | 10 | {code_paths, ["../eredis/ebin"]}. 11 | 12 | {operations, [{get,1}, {put,4}]}. 13 | 14 | {key_generator, {uniform_int, 10000}}. 15 | 16 | {value_generator, {function, basho_bench_driver_eredis, value_gen, []}}. 17 | %{value_generator, {fixed_bin, 1}}. 18 | -------------------------------------------------------------------------------- /priv/basho_bench_eredis_pipeline.config: -------------------------------------------------------------------------------- 1 | {mode, max}. 2 | %{mode, {rate, 5}}. 3 | 4 | {duration, 15}. 5 | 6 | {concurrent, 30}. 7 | 8 | {driver, basho_bench_driver_eredis}. 9 | 10 | {code_paths, ["../eredis/ebin"]}. 11 | 12 | {operations, [{pipeline_get,100}, {pipeline_put,1}]}. 13 | 14 | {key_generator, {uniform_int, 10000}}. 15 | 16 | {value_generator, {function, basho_bench_driver_eredis, value_gen, []}}. 17 | %{value_generator, {fixed_bin, 1}}. 18 | -------------------------------------------------------------------------------- /priv/basho_bench_erldis.config: -------------------------------------------------------------------------------- 1 | {mode, max}. 2 | %{mode, {rate, 5}}. 3 | 4 | {duration, 15}. 5 | 6 | {concurrent, 10}. 7 | 8 | {driver, basho_bench_driver_erldis}. 9 | 10 | {code_paths, ["../eredis/ebin", 11 | "../erldis/ebin/"]}. 12 | 13 | {operations, [{get,1}, {put,4}]}. 14 | 15 | {key_generator, {uniform_int, 10000}}. 16 | 17 | {value_generator, {function, basho_bench_driver_erldis, value_gen, []}}. 18 | {value_generator, {fixed_bin, 64}}. 19 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [ 2 | {platform_define, "^[0-9]+", namespaced_types} 3 | ]}. 4 | {cover_enabled, true}. 5 | %% basho_bench_driver_erldis calls undefined functions, so disable xref_checks. 6 | %% This allows this project to be used as a dependency by other rebar projects 7 | %% that use xref. 8 | {xref_checks, []}. 9 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /src/basho_bench_driver_eredis.erl: -------------------------------------------------------------------------------- 1 | -module(basho_bench_driver_eredis). 2 | 3 | -export([new/1, 4 | run/4]). 5 | 6 | -export([value_gen/1]). 7 | 8 | new(_Id) -> 9 | case whereis(eredis_driver) of 10 | undefined -> 11 | case eredis:start_link() of 12 | {ok, Client} -> 13 | register(eredis_driver, Client), 14 | {ok, Client}; 15 | {error, Reason} -> 16 | {error, Reason} 17 | end; 18 | Pid -> 19 | {ok, Pid} 20 | end. 21 | 22 | run(get, KeyGen, _ValueGen, Client) -> 23 | Start = KeyGen(), 24 | %%case eredis:q(["MGET" | lists:seq(Start, Start + 500)]) of 25 | case catch(eredis:q(Client, ["GET", Start], 100)) of 26 | {ok, _Value} -> 27 | {ok, Client}; 28 | {error, Reason} -> 29 | {error, Reason, Client}; 30 | {'EXIT', {timeout, _}} -> 31 | {error, timeout, Client} 32 | end; 33 | 34 | run(pipeline_get, KeyGen, _ValueGen, Client) -> 35 | Seq = lists:seq(1, 5), 36 | P = [["GET", KeyGen()] || _ <- Seq], 37 | 38 | case catch(eredis:qp(Client, P, 500)) of 39 | {error, Reason} -> 40 | {error, Reason, Client}; 41 | {'EXIT', {timeout, _}} -> 42 | {error, timeout, Client}; 43 | Res -> 44 | case check_pipeline_get(Res, Seq) of 45 | ok -> 46 | {ok, Client}; 47 | {error, Reason} -> 48 | {error, Reason, Client} 49 | end 50 | end; 51 | 52 | run(put, KeyGen, ValueGen, Client) -> 53 | case catch(eredis:q(Client, ["SET", KeyGen(), ValueGen()], 100)) of 54 | {ok, <<"OK">>} -> 55 | {ok, Client}; 56 | {error, Reason} -> 57 | {error, Reason, Client}; 58 | {'EXIT', {timeout, _}} -> 59 | {error, timeout, Client} 60 | end; 61 | 62 | run(pipeline_put, KeyGen, ValueGen, Client) -> 63 | Seq = lists:seq(1, 5), 64 | P = [["SET", KeyGen(), ValueGen()] || _ <- Seq], 65 | R = [{ok, <<"OK">>} || _ <- Seq], 66 | 67 | case catch(eredis:qp(Client, P, 500)) of 68 | R -> 69 | {ok, Client}; 70 | {error, Reason} -> 71 | {error, Reason, Client}; 72 | {'EXIT', {timeout, _}} -> 73 | {error, timeout, Client} 74 | end. 75 | 76 | 77 | check_pipeline_get([], []) -> 78 | ok; 79 | check_pipeline_get([{ok, _}|Res], [_|Seq]) -> 80 | check_pipeline_get(Res, Seq); 81 | check_pipeline_get([{error, Reason}], _) -> 82 | {error, Reason}. 83 | 84 | 85 | value_gen(_Id) -> 86 | fun() -> 87 | %% %% Example data from http://json.org/example.html 88 | <<"{\"web-app\":{\"servlet\":[{\"servlet-name\":\"cofaxCDS\",\"servlet-class\":\"org.cofax.cds.CDSServlet\",\"init-param\":{\"configGlossary:installationAt\":\"Philadelphia,PA\",\"configGlossary:adminEmail\":\"ksm@pobox.com\",\"configGlossary:poweredBy\":\"Cofax\",\"configGlossary:poweredByIcon\":\"/images/cofax.gif\",\"configGlossary:staticPath\":\"/content/static\",\"templateProcessorClass\":\"org.cofax.WysiwygTemplate\",\"templateLoaderClass\":\"org.cofax.FilesTemplateLoader\",\"templatePath\":\"templates\",\"templateOverridePath\":\"\",\"defaultListTemplate\":\"listTemplate.htm\",\"defaultFileTemplate\":\"articleTemplate.htm\",\"useJSP\":false,\"jspListTemplate\":\"listTemplate.jsp\",\"jspFileTemplate\":\"articleTemplate.jsp\",\"cachePackageTagsTrack\":200,\"cachePackageTagsStore\":200,\"cachePackageTagsRefresh\":60,\"cacheTemplatesTrack\":100,\"cacheTemplatesStore\":50,\"cacheTemplatesRefresh\":15,\"cachePagesTrack\":200,\"cachePagesStore\":100,\"cachePagesRefresh\":10,\"cachePagesDirtyRead\":10,\"searchEngineListTemplate\":\"forSearchEnginesList.htm\",\"searchEngineFileTemplate\":\"forSearchEngines.htm\",\"searchEngineRobotsDb\":\"WEB-INF/robots.db\",\"useDataStore\":true,\"dataStoreClass\":\"org.cofax.SqlDataStore\",\"redirectionClass\":\"org.cofax.SqlRedirection\",\"dataStoreName\":\"cofax\",\"dataStoreDriver\":\"com.microsoft.jdbc.sqlserver.SQLServerDriver\",\"dataStoreUrl\":\"jdbc:microsoft:sqlserver://LOCALHOST:1433;DatabaseName=goon\",\"dataStoreUser\":\"sa\",\"dataStorePassword\":\"dataStoreTestQuery\",\"dataStoreTestQuery\":\"SETNOCOUNTON;selecttest='test';\",\"dataStoreLogFile\":\"/usr/local/tomcat/logs/datastore.log\",\"dataStoreInitConns\":10,\"dataStoreMaxConns\":100,\"dataStoreConnUsageLimit\":100,\"dataStoreLogLevel\":\"debug\",\"maxUrlLength\":500}},{\"servlet-name\":\"cofaxEmail\",\"servlet-class\":\"org.cofax.cds.EmailServlet\",\"init-param\":{\"mailHost\":\"mail1\",\"mailHostOverride\":\"mail2\"}},{\"servlet-name\":\"cofaxAdmin\",\"servlet-class\":\"org.cofax.cds.AdminServlet\"},{\"servlet-name\":\"fileServlet\",\"servlet-class\":\"org.cofax.cds.FileServlet\"},{\"servlet-name\":\"cofaxTools\",\"servlet-class\":\"org.cofax.cms.CofaxToolsServlet\",\"init-param\":{\"templatePath\":\"toolstemplates/\",\"log\":1,\"logLocation\":\"/usr/local/tomcat/logs/CofaxTools.log\",\"logMaxSize\":\"\",\"dataLog\":1,\"dataLogLocation\":\"/usr/local/tomcat/logs/dataLog.log\",\"dataLogMaxSize\":\"\",\"removePageCache\":\"/content/admin/remove?cache=pages&id=\",\"removeTemplateCache\":\"/content/admin/remove?cache=templates&id=\",\"fileTransferFolder\":\"/usr/local/tomcat/webapps/content/fileTransferFolder\",\"lookInContext\":1,\"adminGroupID\":4,\"betaServer\":true}}],\"servlet-mapping\":{\"cofaxCDS\":\"/\",\"cofaxEmail\":\"/cofaxutil/aemail/*\",\"cofaxAdmin\":\"/admin/*\",\"fileServlet\":\"/static/*\",\"cofaxTools\":\"/tools/*\"},\"taglib\":{\"taglib-uri\":\"cofax.tld\",\"taglib-location\":\"/WEB-INF/tlds/cofax.tld\"}}">> 89 | end. 90 | -------------------------------------------------------------------------------- /src/basho_bench_driver_erldis.erl: -------------------------------------------------------------------------------- 1 | -module(basho_bench_driver_erldis). 2 | 3 | -export([new/1, 4 | run/4]). 5 | 6 | new(_Id) -> 7 | case erldis_client:connect() of 8 | {ok, Pid} -> 9 | {ok, Pid}; 10 | {error, {already_started, Pid}} -> 11 | {ok, Pid} 12 | end. 13 | 14 | run(get, KeyGen, _ValueGen, Client) -> 15 | Start = KeyGen(), 16 | case erldis:mget(Client, lists:seq(Start, Start + 500)) of 17 | {error, Reason} -> 18 | {error, Reason, Client}; 19 | _Value -> 20 | {ok, Client} 21 | end; 22 | 23 | run(put, KeyGen, ValueGen, Client) -> 24 | case erldis:set(Client, integer_to_list(KeyGen()), ValueGen()) of 25 | {error, Reason} -> 26 | {error, Reason, Client}; 27 | _Value -> 28 | {ok, Client} 29 | end. 30 | -------------------------------------------------------------------------------- /src/eredis.app.src: -------------------------------------------------------------------------------- 1 | {application,eredis, 2 | [{description,"Erlang Redis Client"}, 3 | {vsn,"1.2.0"}, 4 | {modules,[eredis,eredis_client,eredis_parser,eredis_sub, 5 | eredis_sub_client]}, 6 | {registered,[]}, 7 | {applications,[kernel,stdlib]}, 8 | {maintainers,["Knut Nesheim"]}, 9 | {licenses,["MIT"]}]}. 10 | -------------------------------------------------------------------------------- /src/eredis.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Erlang Redis client 3 | %% 4 | %% Usage: 5 | %% {ok, Client} = eredis:start_link(). 6 | %% {ok, <<"OK">>} = eredis:q(Client, ["SET", "foo", "bar"]). 7 | %% {ok, <<"bar">>} = eredis:q(Client, ["GET", "foo"]). 8 | 9 | -module(eredis). 10 | -include("eredis.hrl"). 11 | 12 | %% Default timeout for calls to the client gen_server 13 | %% Specified in http://www.erlang.org/doc/man/gen_server.html#call-3 14 | -define(TIMEOUT, 5000). 15 | 16 | -export([start_link/0, start_link/1, start_link/2, start_link/3, start_link/4, 17 | start_link/5, start_link/6, start_link/7, stop/1, q/2, q/3, qp/2, qp/3, q_noreply/2, 18 | q_async/2, q_async/3]). 19 | 20 | %% Exported for testing 21 | -export([create_multibulk/1]). 22 | 23 | %% Type of gen_server process id 24 | -type client() :: pid() | 25 | atom() | 26 | {atom(),atom()} | 27 | {global,term()} | 28 | {via,atom(),term()}. 29 | 30 | %% 31 | %% PUBLIC API 32 | %% 33 | 34 | start_link() -> 35 | start_link("127.0.0.1", 6379, 0, ""). 36 | 37 | start_link(Host, Port) -> 38 | start_link(Host, Port, 0, ""). 39 | 40 | start_link(Host, Port, Database) -> 41 | start_link(Host, Port, Database, ""). 42 | 43 | start_link(Host, Port, Database, Password) -> 44 | start_link(Host, Port, Database, Password, 100). 45 | 46 | start_link(Host, Port, Database, Password, ReconnectSleep) -> 47 | start_link(Host, Port, Database, Password, ReconnectSleep, ?TIMEOUT). 48 | 49 | start_link(Host, Port, Database, Password, ReconnectSleep, ConnectTimeout) -> 50 | start_link(Host, Port, Database, Password, ReconnectSleep, ConnectTimeout, []). 51 | 52 | start_link(Host, Port, Database, Password, ReconnectSleep, ConnectTimeout, SocketOptions) 53 | when is_list(Host) orelse 54 | (is_tuple(Host) andalso tuple_size(Host) =:= 2 andalso element(1, Host) =:= local), 55 | is_integer(Port), 56 | is_integer(Database) orelse Database == undefined, 57 | is_list(Password), 58 | is_integer(ReconnectSleep) orelse ReconnectSleep =:= no_reconnect, 59 | is_integer(ConnectTimeout), 60 | is_list(SocketOptions) -> 61 | 62 | eredis_client:start_link(Host, Port, Database, Password, 63 | ReconnectSleep, ConnectTimeout, SocketOptions). 64 | 65 | %% @doc: Callback for starting from poolboy 66 | -spec start_link(server_args()) -> {ok, Pid::pid()} | {error, Reason::term()}. 67 | start_link(Args) -> 68 | Host = proplists:get_value(host, Args, "127.0.0.1"), 69 | Port = proplists:get_value(port, Args, 6379), 70 | Database = proplists:get_value(database, Args, 0), 71 | Password = proplists:get_value(password, Args, ""), 72 | ReconnectSleep = proplists:get_value(reconnect_sleep, Args, 100), 73 | ConnectTimeout = proplists:get_value(connect_timeout, Args, ?TIMEOUT), 74 | SocketOptions = proplists:get_value(socket_options, Args, []), 75 | 76 | start_link(Host, Port, Database, Password, ReconnectSleep, ConnectTimeout, SocketOptions). 77 | 78 | stop(Client) -> 79 | eredis_client:stop(Client). 80 | 81 | -spec q(Client::client(), Command::[any()]) -> 82 | {ok, return_value()} | {error, Reason::binary() | no_connection}. 83 | %% @doc: Executes the given command in the specified connection. The 84 | %% command must be a valid Redis command and may contain arbitrary 85 | %% data which will be converted to binaries. The returned values will 86 | %% always be binaries. 87 | q(Client, Command) -> 88 | call(Client, Command, ?TIMEOUT). 89 | 90 | q(Client, Command, Timeout) -> 91 | call(Client, Command, Timeout). 92 | 93 | 94 | -spec qp(Client::client(), Pipeline::pipeline()) -> 95 | [{ok, return_value()} | {error, Reason::binary()}] | 96 | {error, no_connection}. 97 | %% @doc: Executes the given pipeline (list of commands) in the 98 | %% specified connection. The commands must be valid Redis commands and 99 | %% may contain arbitrary data which will be converted to binaries. The 100 | %% values returned by each command in the pipeline are returned in a list. 101 | qp(Client, Pipeline) -> 102 | pipeline(Client, Pipeline, ?TIMEOUT). 103 | 104 | qp(Client, Pipeline, Timeout) -> 105 | pipeline(Client, Pipeline, Timeout). 106 | 107 | -spec q_noreply(Client::client(), Command::[any()]) -> ok. 108 | %% @doc Executes the command but does not wait for a response and ignores any errors. 109 | %% @see q/2 110 | q_noreply(Client, Command) -> 111 | cast(Client, Command). 112 | 113 | -spec q_async(Client::client(), Command::[any()]) -> ok. 114 | % @doc Executes the command, and sends a message to this process with the response (with either error or success). Message is of the form `{response, Reply}', where `Reply' is the reply expected from `q/2'. 115 | q_async(Client, Command) -> 116 | q_async(Client, Command, self()). 117 | 118 | -spec q_async(Client::client(), Command::[any()], Pid::pid()|atom()) -> ok. 119 | %% @doc Executes the command, and sends a message to `Pid' with the response (with either or success). 120 | %% @see 1_async/2 121 | q_async(Client, Command, Pid) when is_pid(Pid) -> 122 | Request = {request, create_multibulk(Command), Pid}, 123 | gen_server:cast(Client, Request). 124 | 125 | %% 126 | %% INTERNAL HELPERS 127 | %% 128 | 129 | call(Client, Command, Timeout) -> 130 | Request = {request, create_multibulk(Command)}, 131 | gen_server:call(Client, Request, Timeout). 132 | 133 | pipeline(_Client, [], _Timeout) -> 134 | []; 135 | pipeline(Client, Pipeline, Timeout) -> 136 | Request = {pipeline, [create_multibulk(Command) || Command <- Pipeline]}, 137 | gen_server:call(Client, Request, Timeout). 138 | 139 | cast(Client, Command) -> 140 | Request = {request, create_multibulk(Command)}, 141 | gen_server:cast(Client, Request). 142 | 143 | -spec create_multibulk(Args::[any()]) -> Command::iolist(). 144 | %% @doc: Creates a multibulk command with all the correct size headers 145 | create_multibulk(Args) -> 146 | ArgCount = [<<$*>>, integer_to_list(length(Args)), <>], 147 | ArgsBin = lists:map(fun to_bulk/1, lists:map(fun to_binary/1, Args)), 148 | 149 | [ArgCount, ArgsBin]. 150 | 151 | to_bulk(B) when is_binary(B) -> 152 | [<<$$>>, integer_to_list(iolist_size(B)), <>, B, <>]. 153 | 154 | %% @doc: Convert given value to binary. Fallbacks to 155 | %% term_to_binary/1. For floats, throws {cannot_store_floats, Float} 156 | %% as we do not want floats to be stored in Redis. Your future self 157 | %% will thank you for this. 158 | to_binary(X) when is_list(X) -> list_to_binary(X); 159 | to_binary(X) when is_atom(X) -> atom_to_binary(X, utf8); 160 | to_binary(X) when is_binary(X) -> X; 161 | to_binary(X) when is_integer(X) -> integer_to_binary(X); 162 | to_binary(X) when is_float(X) -> throw({cannot_store_floats, X}); 163 | to_binary(X) -> term_to_binary(X). 164 | -------------------------------------------------------------------------------- /src/eredis_client.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% eredis_client 3 | %% 4 | %% The client is implemented as a gen_server which keeps one socket 5 | %% open to a single Redis instance. Users call us using the API in 6 | %% eredis.erl. 7 | %% 8 | %% The client works like this: 9 | %% * When starting up, we connect to Redis with the given connection 10 | %% information, or fail. 11 | %% * Users calls us using gen_server:call, we send the request to Redis, 12 | %% add the calling process at the end of the queue and reply with 13 | %% noreply. We are then free to handle new requests and may reply to 14 | %% the user later. 15 | %% * We receive data on the socket, we parse the response and reply to 16 | %% the client at the front of the queue. If the parser does not have 17 | %% enough data to parse the complete response, we will wait for more 18 | %% data to arrive. 19 | %% * For pipeline commands, we include the number of responses we are 20 | %% waiting for in each element of the queue. Responses are queued until 21 | %% we have all the responses we need and then reply with all of them. 22 | %% 23 | -module(eredis_client). 24 | -behaviour(gen_server). 25 | -include("eredis.hrl"). 26 | 27 | %% API 28 | -export([start_link/7, stop/1, select_database/2]). 29 | 30 | -export([do_sync_command/2]). 31 | 32 | %% gen_server callbacks 33 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, 34 | terminate/2, code_change/3]). 35 | 36 | -record(state, { 37 | host :: string() | undefined, 38 | port :: integer() | undefined, 39 | password :: binary() | undefined, 40 | database :: binary() | undefined, 41 | reconnect_sleep :: reconnect_sleep() | undefined, 42 | connect_timeout :: integer() | undefined, 43 | socket_options :: list(), 44 | 45 | socket :: port() | undefined, 46 | parser_state :: #pstate{} | undefined, 47 | queue :: eredis_queue() | undefined 48 | }). 49 | 50 | %% 51 | %% API 52 | %% 53 | 54 | -spec start_link(Host::list(), 55 | Port::integer(), 56 | Database::integer() | undefined, 57 | Password::string(), 58 | ReconnectSleep::reconnect_sleep(), 59 | ConnectTimeout::integer() | undefined, 60 | SocketOptions::list()) -> 61 | {ok, Pid::pid()} | {error, Reason::term()}. 62 | start_link(Host, Port, Database, Password, ReconnectSleep, ConnectTimeout, SocketOptions) -> 63 | gen_server:start_link(?MODULE, [Host, Port, Database, Password, 64 | ReconnectSleep, ConnectTimeout, SocketOptions], []). 65 | 66 | 67 | stop(Pid) -> 68 | gen_server:call(Pid, stop). 69 | 70 | %%==================================================================== 71 | %% gen_server callbacks 72 | %%==================================================================== 73 | 74 | init([Host, Port, Database, Password, ReconnectSleep, ConnectTimeout, SocketOptions]) -> 75 | State = #state{host = Host, 76 | port = Port, 77 | database = read_database(Database), 78 | password = list_to_binary(Password), 79 | reconnect_sleep = ReconnectSleep, 80 | connect_timeout = ConnectTimeout, 81 | socket_options = SocketOptions, 82 | 83 | parser_state = eredis_parser:init(), 84 | queue = queue:new()}, 85 | 86 | case ReconnectSleep of 87 | no_reconnect -> 88 | case connect(State) of 89 | {ok, _NewState} = Res -> Res; 90 | {error, Reason} -> {stop, Reason} 91 | end; 92 | T when is_integer(T) -> 93 | self() ! initiate_connection, 94 | {ok, State} 95 | end. 96 | 97 | handle_call({request, Req}, From, State) -> 98 | do_request(Req, From, State); 99 | 100 | handle_call({pipeline, Pipeline}, From, State) -> 101 | do_pipeline(Pipeline, From, State); 102 | 103 | handle_call(stop, _From, State) -> 104 | {stop, normal, ok, State}; 105 | 106 | handle_call(_Request, _From, State) -> 107 | {reply, unknown_request, State}. 108 | 109 | 110 | handle_cast({request, Req}, State) -> 111 | case do_request(Req, undefined, State) of 112 | {reply, _Reply, State1} -> 113 | {noreply, State1}; 114 | {noreply, State1} -> 115 | {noreply, State1} 116 | end; 117 | 118 | handle_cast({request, Req, Pid}, State) -> 119 | case do_request(Req, Pid, State) of 120 | {reply, Reply, State1} -> 121 | safe_send(Pid, {response, Reply}), 122 | {noreply, State1}; 123 | {noreply, State1} -> 124 | {noreply, State1} 125 | end; 126 | 127 | handle_cast(_Msg, State) -> 128 | {noreply, State}. 129 | 130 | %% Receive data from socket, see handle_response/2. Match `Socket' to 131 | %% enforce sanity. 132 | handle_info({tcp, Socket, Bs}, #state{socket = Socket} = State) -> 133 | ok = inet:setopts(Socket, [{active, once}]), 134 | {noreply, handle_response(Bs, State)}; 135 | 136 | handle_info({tcp, Socket, _}, #state{socket = OurSocket} = State) 137 | when OurSocket =/= Socket -> 138 | %% Ignore tcp messages when the socket in message doesn't match 139 | %% our state. In order to test behavior around receiving 140 | %% tcp_closed message with clients waiting in queue, we send a 141 | %% fake tcp_close message. This allows us to ignore messages that 142 | %% arrive after that while we are reconnecting. 143 | {noreply, State}; 144 | 145 | handle_info({tcp_error, _Socket, _Reason}, State) -> 146 | %% This will be followed by a close 147 | {noreply, State}; 148 | 149 | %% Socket got closed, for example by Redis terminating idle 150 | %% clients. If desired, spawn of a new process which will try to reconnect and 151 | %% notify us when Redis is ready. In the meantime, we can respond with 152 | %% an error message to all our clients. 153 | handle_info({tcp_closed, _Socket}, State) -> 154 | maybe_reconnect(tcp_closed, State); 155 | 156 | %% Redis is ready to accept requests, the given Socket is a socket 157 | %% already connected and authenticated. 158 | handle_info({connection_ready, Socket}, #state{socket = undefined} = State) -> 159 | {noreply, State#state{socket = Socket}}; 160 | 161 | %% eredis can be used in Poolboy, but it requires to support a simple API 162 | %% that Poolboy uses to manage the connections. 163 | handle_info(stop, State) -> 164 | {stop, shutdown, State}; 165 | 166 | handle_info(initiate_connection, #state{socket = undefined} = State) -> 167 | case connect(State) of 168 | {ok, NewState} -> 169 | {noreply, NewState}; 170 | {error, Reason} -> 171 | maybe_reconnect(Reason, State) 172 | end; 173 | 174 | handle_info(_Info, State) -> 175 | {stop, {unhandled_message, _Info}, State}. 176 | 177 | terminate(_Reason, State) -> 178 | case State#state.socket of 179 | undefined -> ok; 180 | Socket -> gen_tcp:close(Socket) 181 | end, 182 | ok. 183 | 184 | code_change(_OldVsn, State, _Extra) -> 185 | {ok, State}. 186 | 187 | %%-------------------------------------------------------------------- 188 | %%% Internal functions 189 | %%-------------------------------------------------------------------- 190 | 191 | -spec do_request(Req::iolist(), From::pid(), #state{}) -> 192 | {noreply, #state{}} | {reply, Reply::any(), #state{}}. 193 | %% @doc: Sends the given request to redis. If we do not have a 194 | %% connection, returns error. 195 | do_request(_Req, _From, #state{socket = undefined} = State) -> 196 | {reply, {error, no_connection}, State}; 197 | 198 | do_request(Req, From, State) -> 199 | case gen_tcp:send(State#state.socket, Req) of 200 | ok -> 201 | NewQueue = queue:in({1, From}, State#state.queue), 202 | {noreply, State#state{queue = NewQueue}}; 203 | {error, Reason} -> 204 | {reply, {error, Reason}, State} 205 | end. 206 | 207 | -spec do_pipeline(Pipeline::pipeline(), From::pid(), #state{}) -> 208 | {noreply, #state{}} | {reply, Reply::any(), #state{}}. 209 | %% @doc: Sends the entire pipeline to redis. If we do not have a 210 | %% connection, returns error. 211 | do_pipeline(_Pipeline, _From, #state{socket = undefined} = State) -> 212 | {reply, {error, no_connection}, State}; 213 | 214 | do_pipeline(Pipeline, From, State) -> 215 | case gen_tcp:send(State#state.socket, Pipeline) of 216 | ok -> 217 | NewQueue = queue:in({length(Pipeline), From, []}, State#state.queue), 218 | {noreply, State#state{queue = NewQueue}}; 219 | {error, Reason} -> 220 | {reply, {error, Reason}, State} 221 | end. 222 | 223 | -spec handle_response(Data::binary(), State::#state{}) -> NewState::#state{}. 224 | %% @doc: Handle the response coming from Redis. This includes parsing 225 | %% and replying to the correct client, handling partial responses, 226 | %% handling too much data and handling continuations. 227 | handle_response(Data, #state{parser_state = ParserState, 228 | queue = Queue} = State) -> 229 | 230 | case eredis_parser:parse(ParserState, Data) of 231 | %% Got complete response, return value to client 232 | {ReturnCode, Value, NewParserState} -> 233 | NewQueue = reply({ReturnCode, Value}, Queue), 234 | State#state{parser_state = NewParserState, 235 | queue = NewQueue}; 236 | 237 | %% Got complete response, with extra data, reply to client and 238 | %% recurse over the extra data 239 | {ReturnCode, Value, Rest, NewParserState} -> 240 | NewQueue = reply({ReturnCode, Value}, Queue), 241 | handle_response(Rest, State#state{parser_state = NewParserState, 242 | queue = NewQueue}); 243 | 244 | %% Parser needs more data, the parser state now contains the 245 | %% continuation data and we will try calling parse again when 246 | %% we have more data 247 | {continue, NewParserState} -> 248 | State#state{parser_state = NewParserState} 249 | end. 250 | 251 | %% @doc: Sends a value to the first client in queue. Returns the new 252 | %% queue without this client. If we are still waiting for parts of a 253 | %% pipelined request, push the reply to the the head of the queue and 254 | %% wait for another reply from redis. 255 | reply(Value, Queue) -> 256 | case queue:out(Queue) of 257 | {{value, {1, From}}, NewQueue} -> 258 | safe_reply(From, Value), 259 | NewQueue; 260 | {{value, {1, From, Replies}}, NewQueue} -> 261 | safe_reply(From, lists:reverse([Value | Replies])), 262 | NewQueue; 263 | {{value, {N, From, Replies}}, NewQueue} when N > 1 -> 264 | queue:in_r({N - 1, From, [Value | Replies]}, NewQueue); 265 | {empty, Queue} -> 266 | %% Oops 267 | error_logger:info_msg("eredis: Nothing in queue, but got value from parser~n"), 268 | exit(empty_queue) 269 | end. 270 | 271 | %% @doc Send `Value' to each client in queue. Only useful for sending 272 | %% an error message. Any in-progress reply data is ignored. 273 | -spec reply_all(any(), eredis_queue()) -> ok. 274 | reply_all(Value, Queue) -> 275 | case queue:peek(Queue) of 276 | empty -> 277 | ok; 278 | {value, Item} -> 279 | safe_reply(receipient(Item), Value), 280 | reply_all(Value, queue:drop(Queue)) 281 | end. 282 | 283 | receipient({_, From}) -> 284 | From; 285 | receipient({_, From, _}) -> 286 | From. 287 | 288 | safe_reply(undefined, _Value) -> 289 | ok; 290 | safe_reply(Pid, Value) when is_pid(Pid) -> 291 | safe_send(Pid, {response, Value}); 292 | safe_reply(From, Value) -> 293 | gen_server:reply(From, Value). 294 | 295 | safe_send(Pid, Value) -> 296 | try erlang:send(Pid, Value) 297 | catch 298 | Err:Reason -> 299 | error_logger:info_msg("eredis: Failed to send message to ~p with reason ~p~n", [Pid, {Err, Reason}]) 300 | end. 301 | 302 | %% @doc: Helper for connecting to Redis, authenticating and selecting 303 | %% the correct database. These commands are synchronous and if Redis 304 | %% returns something we don't expect, we crash. Returns {ok, State} or 305 | %% {SomeError, Reason}. 306 | connect(State) -> 307 | {ok, {AFamily, Addr}} = get_addr(State#state.host), 308 | Port = case AFamily of 309 | local -> 0; 310 | _ -> State#state.port 311 | end, 312 | 313 | SocketOptions = lists:ukeymerge(1, lists:keysort(1, State#state.socket_options), lists:keysort(1, ?SOCKET_OPTS)), 314 | ConnectOptions = [AFamily | [?SOCKET_MODE | SocketOptions]], 315 | 316 | case gen_tcp:connect(Addr, Port, ConnectOptions, State#state.connect_timeout) of 317 | {ok, Socket} -> 318 | case authenticate(Socket, State#state.password) of 319 | ok -> 320 | case select_database(Socket, State#state.database) of 321 | ok -> 322 | {ok, State#state{socket = Socket}}; 323 | {error, Reason} -> 324 | {error, {select_error, Reason}} 325 | end; 326 | {error, Reason} -> 327 | {error, {authentication_error, Reason}} 328 | end; 329 | {error, Reason} -> 330 | {error, {connection_error, Reason}} 331 | end. 332 | 333 | get_addr({local, Path}) -> 334 | {ok, {local, {local, Path}}}; 335 | get_addr(Hostname) -> 336 | case inet:parse_address(Hostname) of 337 | {ok, {_,_,_,_} = Addr} -> {ok, {inet, Addr}}; 338 | {ok, {_,_,_,_,_,_,_,_} = Addr} -> {ok, {inet6, Addr}}; 339 | {error, einval} -> 340 | case inet:getaddr(Hostname, inet6) of 341 | {error, _} -> 342 | case inet:getaddr(Hostname, inet) of 343 | {ok, Addr}-> {ok, {inet, Addr}}; 344 | {error, _} = Res -> Res 345 | end; 346 | {ok, Addr} -> {ok, {inet6, Addr}} 347 | end 348 | end. 349 | 350 | select_database(_Socket, undefined) -> 351 | ok; 352 | select_database(_Socket, <<"0">>) -> 353 | ok; 354 | select_database(Socket, Database) -> 355 | do_sync_command(Socket, ["SELECT", " ", Database, "\r\n"]). 356 | 357 | authenticate(_Socket, <<>>) -> 358 | ok; 359 | authenticate(Socket, Password) -> 360 | do_sync_command(Socket, ["AUTH", " \"", Password, "\"\r\n"]). 361 | 362 | %% @doc: Executes the given command synchronously, expects Redis to 363 | %% return "+OK\r\n", otherwise it will fail. 364 | do_sync_command(Socket, Command) -> 365 | ok = inet:setopts(Socket, [{active, false}]), 366 | case gen_tcp:send(Socket, Command) of 367 | ok -> 368 | %% Hope there's nothing else coming down on the socket.. 369 | case gen_tcp:recv(Socket, 0, ?RECV_TIMEOUT) of 370 | {ok, <<"+OK\r\n">>} -> 371 | ok = inet:setopts(Socket, [{active, once}]), 372 | ok; 373 | Other -> 374 | {error, {unexpected_data, Other}} 375 | end; 376 | {error, Reason} -> 377 | {error, Reason} 378 | end. 379 | 380 | maybe_reconnect(Reason, #state{reconnect_sleep = no_reconnect, queue = Queue} = State) -> 381 | reply_all({error, Reason}, Queue), 382 | %% If we aren't going to reconnect, then there is nothing else for 383 | %% this process to do. 384 | {stop, normal, State#state{socket = undefined}}; 385 | maybe_reconnect(Reason, #state{queue = Queue} = State) -> 386 | error_logger:error_msg("eredis: Re-establishing connection to ~p:~p due to ~p", 387 | [State#state.host, State#state.port, Reason]), 388 | Self = self(), 389 | spawn_link(fun() -> reconnect_loop(Self, State) end), 390 | 391 | %% tell all of our clients what has happened. 392 | reply_all({error, Reason}, Queue), 393 | 394 | %% Throw away the socket and the queue, as we will never get a 395 | %% response to the requests sent on the old socket. The absence of 396 | %% a socket is used to signal we are "down" 397 | {noreply, State#state{socket = undefined, queue = queue:new()}}. 398 | 399 | %% @doc: Loop until a connection can be established, this includes 400 | %% successfully issuing the auth and select calls. When we have a 401 | %% connection, give the socket to the redis client. 402 | reconnect_loop(Client, #state{reconnect_sleep = ReconnectSleep} = State) -> 403 | case catch(connect(State)) of 404 | {ok, #state{socket = Socket}} -> 405 | Client ! {connection_ready, Socket}, 406 | gen_tcp:controlling_process(Socket, Client), 407 | Msgs = get_all_messages([]), 408 | [Client ! M || M <- Msgs]; 409 | {error, _Reason} -> 410 | timer:sleep(ReconnectSleep), 411 | reconnect_loop(Client, State); 412 | %% Something bad happened when connecting, like Redis might be 413 | %% loading the dataset and we got something other than 'OK' in 414 | %% auth or select 415 | _ -> 416 | timer:sleep(ReconnectSleep), 417 | reconnect_loop(Client, State) 418 | end. 419 | 420 | read_database(undefined) -> 421 | undefined; 422 | read_database(Database) when is_integer(Database) -> 423 | list_to_binary(integer_to_list(Database)). 424 | 425 | 426 | get_all_messages(Acc) -> 427 | receive 428 | M -> 429 | [M | Acc] 430 | after 0 -> 431 | lists:reverse(Acc) 432 | end. 433 | -------------------------------------------------------------------------------- /src/eredis_parser.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Parser of the Redis protocol, see http://redis.io/topics/protocol 3 | %% 4 | %% The idea behind this parser is that we accept any binary data 5 | %% available on the socket. If there is not enough data to parse a 6 | %% complete response, we ask the caller to call us later when there is 7 | %% more data. If there is too much data, we only parse the first 8 | %% response and let the caller call us again with the rest. 9 | %% 10 | %% This approach lets us write a "pure" parser that does not depend on 11 | %% manipulating the socket, which erldis and redis-erl is 12 | %% doing. Instead, we may ask the socket to send us data as fast as 13 | %% possible and parse it continously. The overhead of manipulating the 14 | %% socket when parsing multibulk responses is killing the performance 15 | %% of erldis. 16 | %% 17 | %% Future improvements: 18 | %% * When we return a bulk continuation, we also include the size of 19 | %% the bulk. The caller may use this to explicitly call 20 | %% gen_tcp:recv/2 with the desired size. 21 | 22 | -module(eredis_parser). 23 | -include("eredis.hrl"). 24 | -include_lib("eunit/include/eunit.hrl"). 25 | 26 | -export([init/0, parse/2]). 27 | 28 | %% Exported for testing 29 | -export([parse_bulk/1, parse_bulk/2, 30 | parse_multibulk/1, parse_multibulk/2, buffer_create/0, buffer_create/1]). 31 | 32 | %% 33 | %% API 34 | %% 35 | 36 | %% @doc: Initialize the parser 37 | init() -> 38 | #pstate{}. 39 | 40 | 41 | -spec parse(State::#pstate{}, Data::binary()) -> 42 | {ok, return_value(), NewState::#pstate{}} | 43 | {ok, return_value(), Rest::binary(), NewState::#pstate{}} | 44 | {error, ErrString::binary(), NewState::#pstate{}} | 45 | {error, ErrString::binary(), Rest::binary(), NewState::#pstate{}} | 46 | {continue, NewState::#pstate{}}. 47 | 48 | %% @doc: Parses the (possibly partial) response from Redis. Returns 49 | %% either {ok, Value, NewState}, {ok, Value, Rest, NewState} or 50 | %% {continue, NewState}. External entry point for parsing. 51 | %% 52 | %% In case {ok, Value, NewState} is returned, Value contains the value 53 | %% returned by Redis. NewState will be an empty parser state. 54 | %% 55 | %% In case {ok, Value, Rest, NewState} is returned, Value contains the 56 | %% most recent value returned by Redis, while Rest contains any extra 57 | %% data that was given, but was not part of the same response. In this 58 | %% case you should immeditely call parse again with Rest as the Data 59 | %% argument and NewState as the State argument. 60 | %% 61 | %% In case {continue, NewState} is returned, more data is needed 62 | %% before a complete value can be returned. As soon as you have more 63 | %% data, call parse again with NewState as the State argument and any 64 | %% new binary data as the Data argument. 65 | 66 | %% Parser in initial state, the data we receive will be the beginning 67 | %% of a response 68 | parse(#pstate{state = undefined} = State, NewData) -> 69 | %% Look at the first byte to get the type of reply 70 | case NewData of 71 | %% Status 72 | <<$+, Data/binary>> -> 73 | return_result(parse_simple(Data), State, status_continue); 74 | 75 | %% Error 76 | <<$-, Data/binary>> -> 77 | return_error(parse_simple(Data), State, status_continue); 78 | 79 | %% Integer reply 80 | <<$:, Data/binary>> -> 81 | return_result(parse_simple(Data), State, status_continue); 82 | 83 | %% Multibulk 84 | <<$*, _Rest/binary>> -> 85 | return_result(parse_multibulk(NewData), State, multibulk_continue); 86 | 87 | %% Bulk 88 | <<$$, _Rest/binary>> -> 89 | return_result(parse_bulk(NewData), State, bulk_continue); 90 | 91 | _ -> 92 | %% TODO: Handle the case where we start parsing a new 93 | %% response, but cannot make any sense of it 94 | {error, unknown_response} 95 | end; 96 | 97 | %% The following clauses all match on different continuation states 98 | 99 | parse(#pstate{state = bulk_continue, 100 | continuation_data = ContinuationData} = State, NewData) -> 101 | return_result(parse_bulk(ContinuationData, NewData), State, bulk_continue); 102 | 103 | parse(#pstate{state = multibulk_continue, 104 | continuation_data = ContinuationData} = State, NewData) -> 105 | return_result(parse_multibulk(ContinuationData, NewData), State, multibulk_continue); 106 | 107 | parse(#pstate{state = status_continue, 108 | continuation_data = ContinuationData} = State, NewData) -> 109 | return_result(parse_simple(ContinuationData, NewData), State, status_continue). 110 | 111 | %% 112 | %% MULTIBULK 113 | %% 114 | 115 | parse_multibulk(Data) when is_binary(Data) -> parse_multibulk(buffer_create(Data)); 116 | 117 | parse_multibulk(Buffer) -> 118 | case get_newline_pos(Buffer) of 119 | undefined -> 120 | {continue, {incomplete_size, Buffer}}; 121 | NewlinePos -> 122 | OffsetNewlinePos = NewlinePos - 1, 123 | <<$*, Size:OffsetNewlinePos/binary, ?NL, Bulk/binary>> = buffer_to_binary(Buffer), 124 | IntSize = list_to_integer(binary_to_list(Size)), 125 | 126 | do_parse_multibulk(IntSize, buffer_create(Bulk)) 127 | end. 128 | 129 | %% Size of multibulk was incomplete, try again 130 | parse_multibulk({incomplete_size, Buffer}, NewData0) -> 131 | NewBuffer = buffer_append(Buffer, NewData0), 132 | parse_multibulk(NewBuffer); 133 | 134 | %% Ran out of data inside do_parse_multibulk in parse_bulk, must 135 | %% continue traversing the bulks 136 | parse_multibulk({in_parsing_bulks, Count, Buffer, Acc}, 137 | NewData0) -> 138 | NewBuffer = buffer_append(Buffer, NewData0), 139 | 140 | %% Continue where we left off 141 | do_parse_multibulk(Count, NewBuffer, Acc). 142 | 143 | %% @doc: Parses the given number of bulks from Data. If Data does not 144 | %% contain enough bulks, {continue, ContinuationData} is returned with 145 | %% enough information to start parsing with the correct count and 146 | %% accumulated data. 147 | do_parse_multibulk(Count, Buffer) -> 148 | do_parse_multibulk(Count, Buffer, []). 149 | 150 | do_parse_multibulk(-1, Buffer, []) -> 151 | {ok, undefined, buffer_to_binary(Buffer)}; 152 | do_parse_multibulk(0, Buffer, Acc) -> 153 | {ok, lists:reverse(Acc), buffer_to_binary(Buffer)}; 154 | do_parse_multibulk(Count, Buffer, Acc) -> 155 | case buffer_size(Buffer) == 0 of 156 | true -> {continue, {in_parsing_bulks, Count, buffer_create(), Acc}}; 157 | false -> 158 | %% Try parsing the first bulk in Data, if it works, we get the 159 | %% extra data back that was not part of the bulk which we can 160 | %% recurse on. If the bulk does not contain enough data, we 161 | %% return with a continuation and enough data to pick up where we 162 | %% left off. In the continuation we will get more data 163 | %% automagically in Data, so parsing the bulk might work. 164 | case parse_bulk(Buffer) of 165 | {ok, Value, Rest} -> 166 | do_parse_multibulk(Count - 1, buffer_create(Rest), [Value | Acc]); 167 | {continue, _} -> 168 | {continue, {in_parsing_bulks, Count, Buffer, Acc}} 169 | end 170 | end. 171 | 172 | %% 173 | %% BULK 174 | %% 175 | 176 | parse_bulk(Data) when is_binary(Data) -> parse_bulk(buffer_create(Data)); 177 | 178 | parse_bulk(Buffer) -> 179 | case buffer_hd(Buffer) of 180 | [$*] -> parse_multibulk(Buffer); 181 | [$+] -> parse_simple(buffer_tl(Buffer)); 182 | [$-] -> parse_simple(buffer_tl(Buffer)); 183 | [$:] -> parse_simple(buffer_tl(Buffer)); 184 | [$$] -> do_parse_bulk(Buffer) 185 | end. 186 | 187 | %% Bulk, at beginning of response 188 | do_parse_bulk(Buffer) -> 189 | %% Find the position of the first terminator, everything up until 190 | %% this point contains the size specifier. If we cannot find it, 191 | %% we received a partial response and need more data 192 | case get_newline_pos(Buffer) of 193 | undefined -> 194 | {continue, {incomplete_size, Buffer}}; 195 | NewlinePos -> 196 | OffsetNewlinePos = NewlinePos - 1, % Take into account the first $ 197 | <<$$, Size:OffsetNewlinePos/binary, Bulk/binary>> = buffer_to_binary(Buffer), 198 | IntSize = list_to_integer(binary_to_list(Size)), 199 | 200 | if 201 | %% Nil response from redis 202 | IntSize =:= -1 -> 203 | <> = Bulk, 204 | {ok, undefined, Rest}; 205 | %% We have enough data for the entire bulk 206 | size(Bulk) - (size(<>) * 2) >= IntSize -> 207 | <> = Bulk, 208 | {ok, Value, Rest}; 209 | true -> 210 | %% Need more data, so we send the bulk without the 211 | %% size specifier to our future self 212 | {continue, {IntSize, buffer_create(Bulk)}} 213 | end 214 | end. 215 | 216 | %% Bulk, continuation from partial bulk size 217 | parse_bulk({incomplete_size, Buffer}, NewData0) -> 218 | NewBuffer = buffer_append(Buffer, NewData0), 219 | parse_bulk(NewBuffer); 220 | 221 | %% Bulk, continuation from partial bulk value 222 | parse_bulk({IntSize, Buffer0}, Data) -> 223 | Buffer = buffer_append(Buffer0, Data), 224 | 225 | case buffer_size(Buffer) - (size(<>) * 2) >= IntSize of 226 | true -> 227 | <> = buffer_to_binary(Buffer), 228 | {ok, Value, Rest}; 229 | false -> 230 | {continue, {IntSize, Buffer}} 231 | end. 232 | 233 | 234 | %% 235 | %% SIMPLE REPLIES 236 | %% 237 | %% Handles replies on the following format: 238 | %% TData\r\n 239 | %% Where T is a type byte, like '+', '-', ':'. Data is terminated by \r\n 240 | 241 | %% @doc: Parse simple replies. Data must not contain type 242 | %% identifier. Type must be handled by the caller. 243 | parse_simple(Data) when is_binary(Data) -> parse_simple(buffer_create(Data)); 244 | 245 | parse_simple(Buffer) -> 246 | case get_newline_pos(Buffer) of 247 | undefined -> 248 | {continue, {incomplete_simple, Buffer}}; 249 | NewlinePos -> 250 | <> = buffer_to_binary(Buffer), 251 | {ok, Value, Rest} 252 | end. 253 | 254 | parse_simple({incomplete_simple, Buffer}, NewData0) -> 255 | NewBuffer = buffer_append(Buffer, NewData0), 256 | parse_simple(NewBuffer). 257 | 258 | %% 259 | %% INTERNAL HELPERS 260 | %% 261 | get_newline_pos({B, _}) -> 262 | case re:run(B, ?NL) of 263 | {match, [{Pos, _}]} -> Pos; 264 | nomatch -> undefined 265 | end. 266 | 267 | buffer_create() -> 268 | {[], 0}. 269 | 270 | buffer_create(Data) -> 271 | {[Data], byte_size(Data)}. 272 | 273 | buffer_append({List, Size}, Binary) -> 274 | NewList = case List of 275 | [] -> [Binary]; 276 | [Head | Tail] -> [Head, Tail, Binary] 277 | end, 278 | {NewList, Size + byte_size(Binary)}. 279 | 280 | buffer_hd({[<> | _], _}) -> [Char]; 281 | buffer_hd({[], _}) -> []. 282 | 283 | buffer_tl({[<<_, RestBin/binary>> | Rest], Size}) -> {[RestBin | Rest], Size - 1}. 284 | 285 | buffer_to_binary({List, _}) -> iolist_to_binary(List). 286 | 287 | buffer_size({_, Size}) -> Size. 288 | 289 | %% @doc: Helper for handling the result of parsing. Will update the 290 | %% parser state with the continuation of given name if necessary. 291 | return_result({ok, Value, <<>>}, _State, _StateName) -> 292 | {ok, Value, init()}; 293 | return_result({ok, Value, Rest}, _State, _StateName) -> 294 | {ok, Value, Rest, init()}; 295 | return_result({continue, ContinuationData}, State, StateName) -> 296 | {continue, State#pstate{state = StateName, continuation_data = ContinuationData}}. 297 | 298 | %% @doc: Helper for returning an error. Uses return_result/3 and just transforms the {ok, ...} tuple into an error tuple 299 | return_error(Result, State, StateName) -> 300 | case return_result(Result, State, StateName) of 301 | {ok, Value, ParserState} -> 302 | {error, Value, ParserState}; 303 | {ok, Value, Rest, ParserState} -> 304 | {error, Value, Rest, ParserState}; 305 | Res -> 306 | Res 307 | end. 308 | -------------------------------------------------------------------------------- /src/eredis_sub.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Erlang PubSub Redis client 3 | %% 4 | -module(eredis_sub). 5 | -include("eredis.hrl"). 6 | 7 | %% Default timeout for calls to the client gen_server 8 | %% Specified in http://www.erlang.org/doc/man/gen_server.html#call-3 9 | -define(TIMEOUT, 5000). 10 | 11 | -export([start_link/0, start_link/1, start_link/3, start_link/6, stop/1, 12 | controlling_process/1, controlling_process/2, controlling_process/3, 13 | ack_message/1, subscribe/2, unsubscribe/2, channels/1]). 14 | 15 | -export([psubscribe/2,punsubscribe/2]). 16 | 17 | -export([receiver/1, sub_example/0, pub_example/0]). 18 | 19 | -export([psub_example/0,ppub_example/0]). 20 | 21 | %% 22 | %% PUBLIC API 23 | %% 24 | 25 | start_link() -> 26 | start_link([]). 27 | 28 | start_link(Host, Port, Password) -> 29 | start_link(Host, Port, Password, 100, infinity, drop). 30 | 31 | start_link(Host, Port, Password, ReconnectSleep, 32 | MaxQueueSize, QueueBehaviour) 33 | when is_list(Host) andalso 34 | is_integer(Port) andalso 35 | is_list(Password) andalso 36 | (is_integer(ReconnectSleep) orelse ReconnectSleep =:= no_reconnect) andalso 37 | (is_integer(MaxQueueSize) orelse MaxQueueSize =:= infinity) andalso 38 | (QueueBehaviour =:= drop orelse QueueBehaviour =:= exit) -> 39 | 40 | eredis_sub_client:start_link(Host, Port, Password, ReconnectSleep, 41 | MaxQueueSize, QueueBehaviour). 42 | 43 | 44 | %% @doc: Callback for starting from poolboy 45 | -spec start_link(server_args()) -> {ok, Pid::pid()} | {error, Reason::term()}. 46 | start_link(Args) -> 47 | Host = proplists:get_value(host, Args, "127.0.0.1"), 48 | Port = proplists:get_value(port, Args, 6379), 49 | Password = proplists:get_value(password, Args, ""), 50 | ReconnectSleep = proplists:get_value(reconnect_sleep, Args, 100), 51 | MaxQueueSize = proplists:get_value(max_queue_size, Args, infinity), 52 | QueueBehaviour = proplists:get_value(queue_behaviour, Args, drop), 53 | start_link(Host, Port, Password, ReconnectSleep, 54 | MaxQueueSize, QueueBehaviour). 55 | 56 | stop(Pid) -> 57 | eredis_sub_client:stop(Pid). 58 | 59 | 60 | -spec controlling_process(Client::pid()) -> ok. 61 | %% @doc: Make the calling process the controlling process. The 62 | %% controlling process received pubsub-related messages, of which 63 | %% there are three kinds. In each message, the pid refers to the 64 | %% eredis client process. 65 | %% 66 | %% {message, Channel::binary(), Message::binary(), pid()} 67 | %% This is sent for each pubsub message received by the client. 68 | %% 69 | %% {pmessage, Pattern::binary(), Channel::binary(), Message::binary(), pid()} 70 | %% This is sent for each pattern pubsub message received by the client. 71 | %% 72 | %% {dropped, NumMessages::integer(), pid()} 73 | %% If the queue reaches the max size as specified in start_link 74 | %% and the behaviour is to drop messages, this message is sent when 75 | %% the queue is flushed. 76 | %% 77 | %% {subscribed, Channel::binary(), pid()} 78 | %% When using eredis_sub:subscribe(pid()), this message will be 79 | %% sent for each channel Redis aknowledges the subscription. The 80 | %% opposite, 'unsubscribed' is sent when Redis aknowledges removal 81 | %% of a subscription. 82 | %% 83 | %% {eredis_disconnected, pid()} 84 | %% This is sent when the eredis client is disconnected from redis. 85 | %% 86 | %% {eredis_connected, pid()} 87 | %% This is sent when the eredis client reconnects to redis after 88 | %% an existing connection was disconnected. 89 | %% 90 | %% Any message of the form {message, _, _, _} must be acknowledged 91 | %% before any subsequent message of the same form is sent. This 92 | %% prevents the controlling process from being overrun with redis 93 | %% pubsub messages. See ack_message/1. 94 | controlling_process(Client) -> 95 | controlling_process(Client, self()). 96 | 97 | -spec controlling_process(Client::pid(), Pid::pid()) -> ok. 98 | %% @doc: Make the given process (pid) the controlling process. 99 | controlling_process(Client, Pid) -> 100 | controlling_process(Client, Pid, ?TIMEOUT). 101 | 102 | %% @doc: Make the given process (pid) the controlling process subscriber 103 | %% with the given Timeout. 104 | controlling_process(Client, Pid, Timeout) -> 105 | gen_server:call(Client, {controlling_process, Pid}, Timeout). 106 | 107 | 108 | -spec ack_message(Client::pid()) -> ok. 109 | %% @doc: acknowledge the receipt of a pubsub message. each pubsub 110 | %% message must be acknowledged before the next one is received 111 | ack_message(Client) -> 112 | gen_server:cast(Client, {ack_message, self()}). 113 | 114 | 115 | %% @doc: Subscribe to the given channels. Returns immediately. The 116 | %% result will be delivered to the controlling process as any other 117 | %% message. Delivers {subscribed, Channel::binary(), pid()} 118 | -spec subscribe(pid(), [channel()]) -> ok. 119 | subscribe(Client, Channels) -> 120 | gen_server:cast(Client, {subscribe, self(), Channels}). 121 | 122 | %% @doc: Pattern subscribe to the given channels. Returns immediately. The 123 | %% result will be delivered to the controlling process as any other 124 | %% message. Delivers {subscribed, Channel::binary(), pid()} 125 | -spec psubscribe(pid(), [channel()]) -> ok. 126 | psubscribe(Client, Channels) -> 127 | gen_server:cast(Client, {psubscribe, self(), Channels}). 128 | 129 | 130 | 131 | unsubscribe(Client, Channels) -> 132 | gen_server:cast(Client, {unsubscribe, self(), Channels}). 133 | 134 | punsubscribe(Client, Channels) -> 135 | gen_server:cast(Client, {punsubscribe, self(), Channels}). 136 | 137 | %% @doc: Returns the channels the given client is currently 138 | %% subscribing to. Note: this list is based on the channels at startup 139 | %% and any channel added during runtime. It might not immediately 140 | %% reflect the channels Redis thinks the client is subscribed to. 141 | channels(Client) -> 142 | gen_server:call(Client, get_channels). 143 | 144 | 145 | 146 | %% 147 | %% STUFF FOR TRYING OUT PUBSUB 148 | %% 149 | 150 | receiver(Sub) -> 151 | receive 152 | Msg -> 153 | io:format("received ~p~n", [Msg]), 154 | ack_message(Sub), 155 | ?MODULE:receiver(Sub) 156 | end. 157 | 158 | sub_example() -> 159 | {ok, Sub} = start_link(), 160 | Receiver = spawn_link(fun () -> 161 | controlling_process(Sub), 162 | subscribe(Sub, [<<"foo">>]), 163 | receiver(Sub) 164 | end), 165 | {Sub, Receiver}. 166 | 167 | psub_example() -> 168 | {ok, Sub} = start_link(), 169 | Receiver = spawn_link(fun () -> 170 | controlling_process(Sub), 171 | psubscribe(Sub, [<<"foo*">>]), 172 | receiver(Sub) 173 | end), 174 | {Sub, Receiver}. 175 | 176 | pub_example() -> 177 | {ok, P} = eredis:start_link(), 178 | eredis:q(P, ["PUBLISH", "foo", "bar"]), 179 | eredis_client:stop(P). 180 | 181 | ppub_example() -> 182 | {ok, P} = eredis:start_link(), 183 | eredis:q(P, ["PUBLISH", "foo123", "bar"]), 184 | eredis_client:stop(P). 185 | 186 | 187 | -------------------------------------------------------------------------------- /src/eredis_sub_client.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% eredis_pubsub_client 3 | %% 4 | %% This client implements a subscriber to a Redis pubsub channel. It 5 | %% is implemented in the same way as eredis_client, except channel 6 | %% messages are streamed to the controlling process. Messages are 7 | %% queued and delivered when the client acknowledges receipt. 8 | %% 9 | %% There is one consuming process per eredis_sub_client. 10 | -module(eredis_sub_client). 11 | -behaviour(gen_server). 12 | -include("eredis.hrl"). 13 | -include("eredis_sub.hrl"). 14 | 15 | 16 | %% API 17 | -export([start_link/6, stop/1]). 18 | 19 | %% gen_server callbacks 20 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, 21 | terminate/2, code_change/3]). 22 | 23 | %% 24 | %% API 25 | %% 26 | 27 | -spec start_link(Host::list(), 28 | Port::integer(), 29 | Password::string(), 30 | ReconnectSleep::reconnect_sleep(), 31 | MaxQueueSize::integer() | infinity, 32 | QueueBehaviour::drop | exit) -> 33 | {ok, Pid::pid()} | {error, Reason::term()}. 34 | start_link(Host, Port, Password, ReconnectSleep, MaxQueueSize, QueueBehaviour) -> 35 | Args = [Host, Port, Password, ReconnectSleep, MaxQueueSize, QueueBehaviour], 36 | gen_server:start_link(?MODULE, Args, []). 37 | 38 | 39 | stop(Pid) -> 40 | gen_server:call(Pid, stop). 41 | 42 | %%==================================================================== 43 | %% gen_server callbacks 44 | %%==================================================================== 45 | 46 | init([Host, Port, Password, ReconnectSleep, MaxQueueSize, QueueBehaviour]) -> 47 | State = #state{host = Host, 48 | port = Port, 49 | password = list_to_binary(Password), 50 | reconnect_sleep = ReconnectSleep, 51 | channels = [], 52 | parser_state = eredis_parser:init(), 53 | msg_queue = queue:new(), 54 | max_queue_size = MaxQueueSize, 55 | queue_behaviour = QueueBehaviour}, 56 | 57 | case connect(State) of 58 | {ok, NewState} -> 59 | ok = inet:setopts(NewState#state.socket, [{active, once}]), 60 | {ok, NewState}; 61 | {error, Reason} -> 62 | {stop, Reason} 63 | end. 64 | 65 | %% Set the controlling process. All messages on all channels are directed here. 66 | handle_call({controlling_process, Pid}, _From, State) -> 67 | case State#state.controlling_process of 68 | undefined -> 69 | ok; 70 | {OldRef, _OldPid} -> 71 | erlang:demonitor(OldRef) 72 | end, 73 | Ref = erlang:monitor(process, Pid), 74 | {reply, ok, State#state{controlling_process={Ref, Pid}, msg_state = ready}}; 75 | 76 | handle_call(get_channels, _From, State) -> 77 | {reply, {ok, State#state.channels}, State}; 78 | 79 | 80 | handle_call(stop, _From, State) -> 81 | {stop, normal, ok, State}; 82 | 83 | handle_call(_Request, _From, State) -> 84 | {reply, unknown_request, State}. 85 | 86 | 87 | %% Controlling process acks, but we have no connection. When the 88 | %% connection comes back up, we should be ready to forward a message 89 | %% again. 90 | handle_cast({ack_message, Pid}, 91 | #state{controlling_process={_, Pid}, socket = undefined} = State) -> 92 | {noreply, State#state{msg_state = ready}}; 93 | 94 | %% Controlling process acknowledges receipt of previous message. Send 95 | %% the next if there is any messages queued or ask for more on the 96 | %% socket. 97 | handle_cast({ack_message, Pid}, 98 | #state{controlling_process={_, Pid}} = State) -> 99 | NewState = case queue:out(State#state.msg_queue) of 100 | {empty, _Queue} -> 101 | State#state{msg_state = ready}; 102 | {{value, Msg}, Queue} -> 103 | send_to_controller(Msg, State), 104 | State#state{msg_queue = Queue, msg_state = need_ack} 105 | end, 106 | {noreply, NewState}; 107 | 108 | handle_cast({subscribe, Pid, Channels}, #state{controlling_process = {_, Pid}} = State) -> 109 | Command = eredis:create_multibulk(["SUBSCRIBE" | Channels]), 110 | ok = gen_tcp:send(State#state.socket, Command), 111 | NewChannels = add_channels(Channels, State#state.channels), 112 | {noreply, State#state{channels = NewChannels}}; 113 | 114 | 115 | handle_cast({psubscribe, Pid, Channels}, #state{controlling_process = {_, Pid}} = State) -> 116 | Command = eredis:create_multibulk(["PSUBSCRIBE" | Channels]), 117 | ok = gen_tcp:send(State#state.socket, Command), 118 | NewChannels = add_channels(Channels, State#state.channels), 119 | {noreply, State#state{channels = NewChannels}}; 120 | 121 | 122 | 123 | handle_cast({unsubscribe, Pid, Channels}, #state{controlling_process = {_, Pid}} = State) -> 124 | Command = eredis:create_multibulk(["UNSUBSCRIBE" | Channels]), 125 | ok = gen_tcp:send(State#state.socket, Command), 126 | NewChannels = remove_channels(Channels, State#state.channels), 127 | {noreply, State#state{channels = NewChannels}}; 128 | 129 | 130 | 131 | handle_cast({punsubscribe, Pid, Channels}, #state{controlling_process = {_, Pid}} = State) -> 132 | Command = eredis:create_multibulk(["PUNSUBSCRIBE" | Channels]), 133 | ok = gen_tcp:send(State#state.socket, Command), 134 | NewChannels = remove_channels(Channels, State#state.channels), 135 | {noreply, State#state{channels = NewChannels}}; 136 | 137 | 138 | 139 | handle_cast({ack_message, _}, State) -> 140 | {noreply, State}; 141 | 142 | handle_cast(_Msg, State) -> 143 | {noreply, State}. 144 | 145 | 146 | %% Receive data from socket, see handle_response/2 147 | handle_info({tcp, _Socket, Bs}, State) -> 148 | ok = inet:setopts(State#state.socket, [{active, once}]), 149 | 150 | NewState = handle_response(Bs, State), 151 | case queue:len(NewState#state.msg_queue) > NewState#state.max_queue_size of 152 | true -> 153 | case State#state.queue_behaviour of 154 | drop -> 155 | Msg = {dropped, queue:len(NewState#state.msg_queue)}, 156 | send_to_controller(Msg, NewState), 157 | {noreply, NewState#state{msg_queue = queue:new()}}; 158 | exit -> 159 | {stop, max_queue_size, State} 160 | end; 161 | false -> 162 | {noreply, NewState} 163 | end; 164 | 165 | handle_info({tcp_error, _Socket, _Reason}, State) -> 166 | %% This will be followed by a close 167 | {noreply, State}; 168 | 169 | %% Socket got closed, for example by Redis terminating idle 170 | %% clients. If desired, spawn of a new process which will try to reconnect and 171 | %% notify us when Redis is ready. In the meantime, we can respond with 172 | %% an error message to all our clients. 173 | handle_info({tcp_closed, _Socket}, #state{reconnect_sleep = no_reconnect} = State) -> 174 | %% If we aren't going to reconnect, then there is nothing else for this process to do. 175 | {stop, normal, State#state{socket = undefined}}; 176 | 177 | handle_info({tcp_closed, _Socket}, State) -> 178 | Self = self(), 179 | send_to_controller({eredis_disconnected, Self}, State), 180 | spawn(fun() -> reconnect_loop(Self, State) end), 181 | 182 | %% Throw away the socket. The absence of a socket is used to 183 | %% signal we are "down"; discard possibly patrially parsed data 184 | {noreply, State#state{socket = undefined, parser_state = eredis_parser:init()}}; 185 | 186 | %% Controller might want to be notified about every reconnect attempt 187 | handle_info(reconnect_attempt, State) -> 188 | send_to_controller({eredis_reconnect_attempt, self()}, State), 189 | {noreply, State}; 190 | 191 | %% Controller might want to be notified about every reconnect failure and reason 192 | handle_info({reconnect_failed, Reason}, State) -> 193 | send_to_controller({eredis_reconnect_failed, self(), 194 | {error, {connection_error, Reason}}}, State), 195 | {noreply, State}; 196 | 197 | %% Redis is ready to accept requests, the given Socket is a socket 198 | %% already connected and authenticated. 199 | handle_info({connection_ready, Socket}, #state{socket = undefined} = State) -> 200 | send_to_controller({eredis_connected, self()}, State), 201 | ok = inet:setopts(Socket, [{active, once}]), 202 | {noreply, State#state{socket = Socket}}; 203 | 204 | 205 | %% Our controlling process is down. 206 | handle_info({'DOWN', Ref, process, Pid, _Reason}, 207 | #state{controlling_process={Ref, Pid}} = State) -> 208 | {stop, shutdown, State#state{controlling_process=undefined, 209 | msg_state=ready, 210 | msg_queue=queue:new()}}; 211 | 212 | %% eredis can be used in Poolboy, but it requires to support a simple API 213 | %% that Poolboy uses to manage the connections. 214 | handle_info(stop, State) -> 215 | {stop, shutdown, State}; 216 | 217 | handle_info(_Info, State) -> 218 | {stop, {unhandled_message, _Info}, State}. 219 | 220 | terminate(_Reason, State) -> 221 | case State#state.socket of 222 | undefined -> ok; 223 | Socket -> gen_tcp:close(Socket) 224 | end, 225 | ok. 226 | 227 | code_change(_OldVsn, State, _Extra) -> 228 | {ok, State}. 229 | 230 | %%-------------------------------------------------------------------- 231 | %%% Internal functions 232 | %%-------------------------------------------------------------------- 233 | 234 | -spec remove_channels([binary()], [binary()]) -> [binary()]. 235 | remove_channels(Channels, OldChannels) -> 236 | lists:foldl(fun lists:delete/2, OldChannels, Channels). 237 | 238 | -spec add_channels([binary()], [binary()]) -> [binary()]. 239 | add_channels(Channels, OldChannels) -> 240 | lists:foldl(fun(C, Cs) -> 241 | case lists:member(C, Cs) of 242 | true -> 243 | Cs; 244 | false -> 245 | [C|Cs] 246 | end 247 | end, OldChannels, Channels). 248 | 249 | -spec handle_response(Data::binary(), State::#state{}) -> NewState::#state{}. 250 | %% @doc: Handle the response coming from Redis. This should only be 251 | %% channel messages that we should forward to the controlling process 252 | %% or queue if the previous message has not been acked. If there are 253 | %% more than a single response in the data we got, queue the responses 254 | %% and serve them up when the controlling process is ready 255 | handle_response(Data, #state{parser_state = ParserState} = State) -> 256 | case eredis_parser:parse(ParserState, Data) of 257 | {ReturnCode, Value, NewParserState} -> 258 | reply({ReturnCode, Value}, 259 | State#state{parser_state=NewParserState}); 260 | 261 | {ReturnCode, Value, Rest, NewParserState} -> 262 | NewState = reply({ReturnCode, Value}, 263 | State#state{parser_state=NewParserState}), 264 | handle_response(Rest, NewState); 265 | 266 | {continue, NewParserState} -> 267 | State#state{parser_state = NewParserState} 268 | end. 269 | 270 | %% @doc: Sends a reply to the controlling process if the process has 271 | %% acknowledged the previous process, otherwise the message is queued 272 | %% for later delivery. 273 | reply({ok, [<<"message">>, Channel, Message]}, State) -> 274 | queue_or_send({message, Channel, Message, self()}, State); 275 | 276 | reply({ok, [<<"pmessage">>, Pattern, Channel, Message]}, State) -> 277 | queue_or_send({pmessage, Pattern, Channel, Message, self()}, State); 278 | 279 | 280 | 281 | reply({ok, [<<"subscribe">>, Channel, _]}, State) -> 282 | queue_or_send({subscribed, Channel, self()}, State); 283 | 284 | reply({ok, [<<"psubscribe">>, Channel, _]}, State) -> 285 | queue_or_send({subscribed, Channel, self()}, State); 286 | 287 | 288 | reply({ok, [<<"unsubscribe">>, Channel, _]}, State) -> 289 | queue_or_send({unsubscribed, Channel, self()}, State); 290 | 291 | 292 | reply({ok, [<<"punsubscribe">>, Channel, _]}, State) -> 293 | queue_or_send({unsubscribed, Channel, self()}, State); 294 | reply({ReturnCode, Value}, State) -> 295 | throw({unexpected_response_from_redis, ReturnCode, Value, State}). 296 | 297 | 298 | queue_or_send(Msg, State) -> 299 | case State#state.msg_state of 300 | need_ack -> 301 | MsgQueue = queue:in(Msg, State#state.msg_queue), 302 | State#state{msg_queue = MsgQueue}; 303 | ready -> 304 | send_to_controller(Msg, State), 305 | State#state{msg_state = need_ack} 306 | end. 307 | 308 | 309 | %% @doc: Helper for connecting to Redis. These commands are 310 | %% synchronous and if Redis returns something we don't expect, we 311 | %% crash. Returns {ok, State} or {error, Reason}. 312 | connect(State) -> 313 | case gen_tcp:connect(State#state.host, State#state.port, [?SOCKET_MODE | ?SOCKET_OPTS]) of 314 | {ok, Socket} -> 315 | case authenticate(Socket, State#state.password) of 316 | ok -> 317 | {ok, State#state{socket = Socket}}; 318 | {error, Reason} -> 319 | {error, {authentication_error, Reason}} 320 | end; 321 | {error, Reason} -> 322 | {error, {connection_error, Reason}} 323 | end. 324 | 325 | 326 | authenticate(_Socket, <<>>) -> 327 | ok; 328 | authenticate(Socket, Password) -> 329 | eredis_client:do_sync_command(Socket, ["AUTH", " \"", Password, "\"\r\n"]). 330 | 331 | 332 | %% @doc: Loop until a connection can be established, this includes 333 | %% successfully issuing the auth and select calls. When we have a 334 | %% connection, give the socket to the redis client. 335 | reconnect_loop(Client, #state{reconnect_sleep=ReconnectSleep}=State) -> 336 | Client ! reconnect_attempt, 337 | case catch(connect(State)) of 338 | {ok, #state{socket = Socket}} -> 339 | gen_tcp:controlling_process(Socket, Client), 340 | Client ! {connection_ready, Socket}; 341 | {error, Reason} -> 342 | Client ! {reconnect_failed, Reason}, 343 | timer:sleep(ReconnectSleep), 344 | reconnect_loop(Client, State); 345 | %% Something bad happened when connecting, like Redis might be 346 | %% loading the dataset and we got something other than 'OK' in 347 | %% auth or select 348 | _ -> 349 | timer:sleep(ReconnectSleep), 350 | reconnect_loop(Client, State) 351 | end. 352 | 353 | 354 | send_to_controller(_Msg, #state{controlling_process=undefined}) -> 355 | ok; 356 | send_to_controller(Msg, #state{controlling_process={_Ref, Pid}}) -> 357 | Pid ! Msg. 358 | -------------------------------------------------------------------------------- /test/eredis_parser_tests.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Parser tests. In particular tests for partial responses. This would 3 | %% probably be a very good candidate for testing with quickcheck or 4 | %% properl. 5 | %% 6 | 7 | -module(eredis_parser_tests). 8 | 9 | -include("eredis.hrl"). 10 | -include_lib("eunit/include/eunit.hrl"). 11 | 12 | -import(eredis_parser, [parse/2, init/0, parse_bulk/1, parse_bulk/2, 13 | parse_multibulk/1, parse_multibulk/2, buffer_create/0, buffer_create/1]). 14 | 15 | 16 | % parse a binary one byte at a time 17 | one_byte_parse(B) -> 18 | one_byte_parse(init(), B). 19 | 20 | one_byte_parse(S, <<>>) -> 21 | parse(S, <<>>); 22 | one_byte_parse(S, <>) -> 23 | parse(S, <>); 24 | one_byte_parse(S, <>) -> 25 | case parse(S, <>) of 26 | {continue, NewState} -> 27 | one_byte_parse(NewState, B); 28 | {ok, Value, Rest, NewState} -> 29 | {ok, Value, <>, NewState}; 30 | {error, Err, Rest, NewState} -> 31 | {error, Err, <>, NewState}; 32 | Other -> 33 | Other 34 | end. 35 | 36 | 37 | parse_bulk_test() -> 38 | B = <<"$3\r\nbar\r\n">>, 39 | ?assertEqual({ok, <<"bar">>, #pstate{}}, parse(#pstate{}, B)). 40 | 41 | parse_split_bulk_test() -> 42 | State1 = init(), 43 | B1 = <<"$3\r\n">>, 44 | B2 = <<"bar\r\n">>, 45 | 46 | {continue, State2} = parse(State1, B1), 47 | Buffer = buffer_create(<<"\r\n">>), 48 | ?assertEqual(#pstate{state = bulk_continue, continuation_data = {3, Buffer}}, 49 | State2), 50 | 51 | ?assertMatch({ok, <<"bar">>, _}, parse(State2, B2)). 52 | 53 | 54 | parse_very_split_bulk_test() -> 55 | State1 = init(), 56 | B1 = <<"$1">>, 57 | B2 = <<"3\r\n">>, 58 | B3 = <<"foobarbazquux\r\n">>, %% 13 bytes 59 | 60 | Buffer1 = buffer_create(<<"$1">>), 61 | ?assertEqual({continue, 62 | #pstate{state = bulk_continue, 63 | continuation_data = {incomplete_size, Buffer1}}}, 64 | parse(State1, B1)), 65 | {continue, State2} = parse(State1, B1), 66 | 67 | Buffer2 = buffer_create(<<"\r\n">>), 68 | ?assertEqual({continue, 69 | #pstate{state = bulk_continue, 70 | continuation_data = {13, Buffer2}}}, 71 | parse(State2, B2)), 72 | {continue, State3} = parse(State2, B2), 73 | 74 | ?assertMatch({ok, <<"foobarbazquux">>, _}, parse(State3, B3)). 75 | 76 | 77 | too_much_data_test() -> 78 | B = <<"$1\r\n1\r\n$1\r\n2\r\n$1\r\n3\r\n">>, 79 | ?assertEqual({ok, <<"1">>, <<"$1\r\n2\r\n$1\r\n3\r\n">>}, parse_bulk(B)). 80 | 81 | too_much_data_in_continuation_test() -> 82 | B1 = <<"$1\r\n">>, 83 | B2 = <<"1\r\n$1\r\n2\r\n$1\r\n3\r\n">>, 84 | 85 | Buffer = buffer_create(<<"\r\n">>), 86 | ?assertEqual({continue, {1, Buffer}}, parse_bulk(B1)), 87 | {continue, ContinuationData1} = parse_bulk(B1), 88 | 89 | ?assertEqual({ok, <<"1">>, <<"$1\r\n2\r\n$1\r\n3\r\n">>}, 90 | parse_bulk(ContinuationData1, B2)). 91 | 92 | bulk_test_() -> 93 | B = <<"$3\r\nbar\r\n">>, 94 | ?_assertEqual({ok, <<"bar">>, <<>>}, parse_bulk(B)). 95 | 96 | bulk_split_test() -> 97 | B1 = <<"$3\r\n">>, 98 | B2 = <<"bar\r\n">>, 99 | 100 | Buffer = buffer_create(<<"\r\n">>), 101 | ?assertEqual({continue, {3, Buffer}}, parse_bulk(B1)), 102 | {continue, Res} = parse_bulk(B1), 103 | ?assertEqual({ok, <<"bar">>, <<>>}, parse_bulk(Res, B2)). 104 | 105 | bulk_very_split_test() -> 106 | B1 = <<"$1">>, 107 | B2 = <<"3\r\n">>, 108 | B3 = <<"foobarbazquux\r\n">>, %% 13 bytes 109 | 110 | Buffer1 = buffer_create(<<"$1">>), 111 | ?assertEqual({continue, {incomplete_size, Buffer1}}, parse_bulk(B1)), 112 | {continue, ContinuationData1} = parse_bulk(B1), 113 | 114 | Buffer2 = buffer_create(<<"\r\n">>), 115 | ?assertEqual({continue, {13, Buffer2}}, parse_bulk(ContinuationData1, B2)), 116 | {continue, ContinuationData2} = parse_bulk(ContinuationData1, B2), 117 | 118 | ?assertEqual({ok, <<"foobarbazquux">>, <<>>}, parse_bulk(ContinuationData2, B3)). 119 | 120 | bulk_split_on_newline_test() -> 121 | B1 = <<"$13\r\nfoobarbazquux">>, 122 | B2 = <<"\r\n">>, %% 13 bytes 123 | 124 | Buffer = buffer_create(<<"\r\nfoobarbazquux">>), 125 | ?assertEqual({continue, {13, Buffer}}, parse_bulk(B1)), 126 | {continue, ContinuationData1} = parse_bulk(B1), 127 | ?assertEqual({ok, <<"foobarbazquux">>, <<>>}, parse_bulk(ContinuationData1, B2)). 128 | 129 | 130 | bulk_nil_test() -> 131 | B = <<"$-1\r\n">>, 132 | ?assertEqual({ok, undefined, init()}, parse(init(), B)). 133 | 134 | bulk_nil_chunked_test() -> 135 | State1 = init(), 136 | B1 = <<"$-1">>, 137 | B2 = <<"\r\n">>, 138 | Buffer = buffer_create(<<"$-1">>), 139 | ?assertEqual({continue, #pstate{state = bulk_continue, 140 | continuation_data = {incomplete_size,Buffer}}}, 141 | parse(State1, B1)), 142 | 143 | {continue, State2} = parse(State1, B1), 144 | 145 | ?assertEqual({ok, undefined, init()}, parse(State2, B2)). 146 | 147 | bulk_nil_with_extra_test() -> 148 | B = <<"$-1\r\n$3\r\nfoo\r\n">>, 149 | ?assertEqual({ok, undefined, <<"$3\r\nfoo\r\n">>, init()}, parse(init(), B)). 150 | 151 | bulk_crap_test() -> 152 | B = <<"\r\n">>, 153 | ?assertEqual({error, unknown_response}, parse(init(), B)). 154 | 155 | 156 | 157 | 158 | multibulk_test() -> 159 | %% [{1, 1}, {2, 2}, {3, 3}] 160 | B = <<"*3\r\n$1\r\n1\r\n$1\r\n2\r\n$1\r\n3\r\n">>, 161 | ?assertEqual({ok, [<<"1">>, <<"2">>, <<"3">>], <<>>}, parse_multibulk(B)). 162 | 163 | multibulk_parse_test() -> 164 | %% [{1, 1}, {2, 2}, {3, 3}] 165 | B = <<"*3\r\n$1\r\n1\r\n$1\r\n2\r\n$1\r\n3\r\n">>, 166 | ?assertEqual({ok, [<<"1">>, <<"2">>, <<"3">>], #pstate{}}, parse(init(), B)). 167 | 168 | multibulk_one_byte_parse_test() -> 169 | %% [{1, 1}, {2, 2}, {3, 3}] 170 | B = <<"*3\r\n$1\r\n1\r\n$1\r\n2\r\n$1\r\n3\r\n">>, 171 | ?assertEqual({ok, [<<"1">>, <<"2">>, <<"3">>], #pstate{}}, 172 | one_byte_parse(B)). 173 | 174 | nested_multibulk_test() -> 175 | %% [[1, 2], [3, 4]] 176 | B = <<"*2\r\n*2\r\n$1\r\n1\r\n$1\r\n2\r\n*2\r\n$1\r\n3\r\n$1\r\n4\r\n">>, 177 | ?assertEqual({ok, [[<<"1">>, <<"2">>], [<<"3">>, <<"4">>]], <<>>}, 178 | parse_multibulk(B)). 179 | 180 | nested_multibulk_parse_test() -> 181 | %% [[1, 2], [3, 4]] 182 | B = <<"*2\r\n*2\r\n$1\r\n1\r\n$1\r\n2\r\n*2\r\n$1\r\n3\r\n$1\r\n4\r\n">>, 183 | ?assertEqual({ok, [[<<"1">>, <<"2">>], [<<"3">>, <<"4">>]], #pstate{}}, 184 | parse(init(), B)). 185 | 186 | nested_multibulk_one_byte_parse_test() -> 187 | %% [[1, 2], [3, 4]] 188 | B = <<"*2\r\n*2\r\n$1\r\n1\r\n$1\r\n2\r\n*2\r\n$1\r\n3\r\n$1\r\n4\r\n">>, 189 | ?assertEqual({ok, [[<<"1">>, <<"2">>], [<<"3">>, <<"4">>]], #pstate{}}, 190 | one_byte_parse(B)). 191 | 192 | multibulk_split_parse_test() -> 193 | %% [{1, 1}, {2, 2}, {3, 3}] 194 | B1 = <<"*3\r\n$1\r\n1\r\n$1">>, 195 | B2 = <<"\r\n2\r\n$1\r\n3\r\n">>, 196 | 197 | State1 = init(), 198 | 199 | Buffer = buffer_create(<<"$1">>), 200 | ?assertEqual({continue, 201 | #pstate{state = multibulk_continue, 202 | continuation_data = 203 | {in_parsing_bulks,2,Buffer,[<<"1">>]}}}, 204 | parse(State1, B1)), 205 | 206 | {continue, State2} = parse(State1, B1), 207 | 208 | ?assertMatch({ok, [<<"1">>, <<"2">>, <<"3">>], _}, parse(State2, B2)). 209 | 210 | multibulk_split_test() -> 211 | %% Split into 2 parts: <<"*3\r\n$1\r\n1\r\n$1\r\n2\r\n$1\r\n3\r\n">> 212 | B1 = <<"*3\r\n$1\r\n1\r\n$1">>, 213 | B2 = <<"\r\n2\r\n$1\r\n3\r\n">>, 214 | 215 | {continue, ContinuationData1} = parse_multibulk(B1), 216 | Result = parse_multibulk(ContinuationData1, B2), 217 | ?assertEqual({ok, [<<"1">>, <<"2">>, <<"3">>], <<>>}, Result). 218 | 219 | multibulk_very_split_test() -> 220 | %% Split into 4 parts: <<"*3\r\n$1\r\n1\r\n$1\r\n2\r\n$1\r\n3\r\n">> 221 | B1 = <<"*">>, 222 | B2 = <<"3\r\n$1\r">>, 223 | B3 = <<"\n1\r\n$1\r\n2\r\n$1">>, 224 | B4 = <<"\r\n3\r\n">>, 225 | 226 | Buffer = buffer_create(<<"*">>), 227 | ?assertEqual({continue, {incomplete_size, Buffer}}, parse_multibulk(B1)), 228 | {continue, ContinuationData1} = parse_multibulk(B1), 229 | {continue, ContinuationData2} = parse_multibulk(ContinuationData1, B2), 230 | {continue, ContinuationData3} = parse_multibulk(ContinuationData2, B3), 231 | 232 | Result = parse_multibulk(ContinuationData3, B4), 233 | ?assertEqual({ok, [<<"1">>, <<"2">>, <<"3">>], <<>>}, Result). 234 | 235 | multibulk_newline_split_test() -> 236 | %% Split into 4 parts: <<"*3\r\n$1\r\n1\r\n$1\r\n2\r\n$1\r\n3\r\n">> 237 | B1 = <<"*2\r\n$1\r\n1">>, 238 | B2 = <<"\r\n$1\r\n2\r\n">>, 239 | Buffer = buffer_create(<<"$1\r\n1">>), 240 | ?assertEqual({continue, {in_parsing_bulks, 2, Buffer, []}}, 241 | parse_multibulk(B1)), 242 | 243 | {continue, ContinuationData1} = parse_multibulk(B1), 244 | 245 | ?assertEqual({ok, [<<"1">>, <<"2">>], <<>>}, parse_multibulk(ContinuationData1, B2)). 246 | 247 | multibulk_nil_test() -> 248 | B = <<"*-1\r\n">>, 249 | ?assertEqual({ok, undefined, <<>>}, parse_multibulk(B)). 250 | 251 | multibulk_nil_parse_test() -> 252 | B = <<"*-1\r\n">>, 253 | ?assertEqual({ok, undefined, #pstate{}}, parse(init(), B)). 254 | 255 | big_chunks_test() -> 256 | %% Real-world example, MGET 1..200 257 | B1 = <<"*200\r\n$1\r\n1\r\n$1\r\n2\r\n$1\r\n3\r\n$1\r\n4\r\n$1\r\n5\r\n$1\r\n6\r\n$1\r\n7\r\n$1\r\n8\r\n$1\r\n9\r\n$2\r\n10\r\n$2\r\n11\r\n$2\r\n12\r\n$2\r\n13\r\n$2\r\n14\r\n$2\r\n15\r\n$2\r\n16\r\n$2\r\n17\r\n$2\r\n18\r\n$2\r\n19\r\n$2\r\n20\r\n$2\r\n21\r\n$2\r\n22\r\n$2\r\n23\r\n$2\r\n24\r\n$2\r\n25\r\n$2\r\n26\r\n$2\r\n27\r\n$2\r\n28\r\n$2\r\n29\r\n$2\r\n30\r\n$2\r\n31\r\n$2\r\n32\r\n$2\r\n33\r\n$2\r\n34\r\n$2\r\n35\r\n$2\r\n36\r\n$2\r\n37\r\n$2\r\n38\r\n$2\r\n39\r\n$2\r\n40\r\n$2\r\n41\r\n$2\r\n42\r\n$2\r\n43\r\n$2\r\n44\r\n$2\r\n45\r\n$2\r\n46\r\n$2\r\n47\r\n$2\r\n48\r\n$2\r\n49\r\n$2\r\n50\r\n$2\r\n51\r\n$2\r\n52\r\n$2\r\n53\r\n$2\r\n54\r\n$2\r\n55\r\n$2\r\n56\r\n$2\r\n57\r\n$2\r\n58\r\n$2\r\n59\r\n$2\r\n60\r\n$2\r\n61\r\n$2\r\n62\r\n$2\r\n63\r\n$2\r\n64\r\n$2\r\n65\r\n$2\r\n66\r\n$2\r\n67\r\n$2\r\n68\r\n$2\r\n69\r\n$2\r\n70\r\n$2\r\n71\r\n$2\r\n72\r\n$2\r\n73\r\n$2\r\n74\r\n$2\r\n75\r\n$2\r\n76\r\n$2\r\n77\r\n$2\r\n78\r\n$2\r\n79\r\n$2\r\n80\r\n$2\r\n81\r\n$2\r\n82\r\n$2\r\n83\r\n$2\r\n84\r\n$2\r\n85\r\n$2\r\n86\r\n$2\r\n87\r\n$2\r\n88\r\n$2\r\n89\r\n$2\r\n90\r\n$2\r\n91\r\n$2\r\n92\r\n$2\r\n93\r\n$2\r\n94\r\n$2\r\n95\r\n$2\r\n96\r\n$2\r\n97\r\n$2\r\n98\r\n$2\r\n99\r\n$3\r\n100\r\n$3\r\n101\r\n$3\r\n102\r\n$3\r\n103\r\n$3\r\n104\r\n$3\r\n105\r\n$3\r\n106\r\n$3\r\n107\r\n$3\r\n108\r\n$3\r\n109\r\n$3\r\n110\r\n$3\r\n111\r\n$3\r\n112\r\n$3\r\n113\r\n$3\r\n114\r\n$3\r\n115\r\n$3\r\n116\r\n$3\r\n117\r\n$3\r\n118\r\n$3\r\n119\r\n$3\r\n120\r\n$3\r\n121\r\n$3\r\n122\r\n$3\r\n123\r\n$3\r\n124\r\n$3\r\n125\r\n$3\r\n126\r\n$3\r\n127\r\n$3\r\n128\r\n$3\r\n129\r\n$3\r\n130\r\n$3\r\n131\r\n$3\r\n132\r\n$3\r\n133\r\n$3\r\n134\r\n$3\r\n135\r\n$3\r\n136\r\n$3\r\n137\r\n$3\r\n138\r\n$3\r\n139\r\n$3\r\n140\r\n$3\r\n141\r\n$3\r\n142\r\n$3\r\n143\r\n$3\r\n144\r\n$3\r\n145\r\n$3\r\n146\r\n$3\r\n147\r\n$3\r\n148\r\n$3\r\n149\r\n$3\r\n150\r\n$3\r\n151\r\n$3\r\n152\r\n$3\r\n153\r\n$3\r\n154\r\n$3\r\n155\r\n$3\r\n156\r\n$3\r\n157\r\n$3\r\n158\r\n$3\r\n159\r\n$3\r\n160\r\n$3\r\n161\r\n$3\r\n162\r\n$3\r\n163\r\n$3\r\n164\r\n$3\r\n165\r\n$3\r\n166\r\n$3\r\n167\r\n$3\r\n168\r\n$3\r\n169\r\n$3\r\n170\r\n$3\r\n171\r\n$3\r\n172\r\n$3\r\n173\r\n$3\r\n1">>, 258 | B2 = <<"74\r\n$3\r\n175\r\n$3\r\n176\r\n$3\r\n177\r\n$3\r\n178\r\n$3\r\n179\r\n$3\r\n180\r\n$3\r\n181\r\n$3\r\n182\r\n$3\r\n183\r\n$3\r\n184\r\n$3\r\n185\r\n$3\r\n186\r\n$3\r\n187\r\n$3\r\n188\r\n$3\r\n189\r\n$3\r\n190\r\n$3\r\n191\r\n$3\r\n192\r\n$3\r\n193\r\n$3\r\n194\r\n$3\r\n195\r\n$3\r\n196\r\n$3\r\n197\r\n$3\r\n198\r\n$3\r\n199\r\n$3\r\n200\r\n">>, 259 | ExpectedValues = [list_to_binary(integer_to_list(N)) || N <- lists:seq(1, 200)], 260 | State1 = init(), 261 | 262 | ?assertMatch({continue, 263 | #pstate{state = multibulk_continue, 264 | continuation_data = {in_parsing_bulks, 27, _, _}}}, 265 | parse(State1, B1)), 266 | {continue, State2} = parse(State1, B1), 267 | 268 | ?assertMatch({ok, ExpectedValues, #pstate{state = undefined, 269 | continuation_data = undefined}}, 270 | parse(State2, B2)). 271 | 272 | 273 | chunk_test() -> 274 | B1 = <<"*500\r\n$1\r\n1\r\n$1\r\n2\r\n$1\r\n3\r\n$1\r\n4\r\n$1\r\n5\r\n$1\r\n6\r\n$1\r\n7\r\n$1\r\n8\r\n$1\r\n9\r\n$2\r\n10\r\n$2\r\n11\r\n$2\r\n12\r\n$2\r\n13\r\n$2\r\n14\r\n$2\r\n15\r\n$2\r\n16\r\n$2\r\n17\r\n$2\r\n18\r\n$2\r\n19\r\n$2\r\n20\r\n$2\r\n21\r\n$2\r\n22\r\n$2\r\n23\r\n$2\r\n24\r\n$2\r\n25\r\n$2\r\n26\r\n$2\r\n27\r\n$2\r\n28\r\n$2\r\n29\r\n$2\r\n30\r\n$2\r\n31\r\n$2\r\n32\r\n$2\r\n33\r\n$2\r\n34\r\n$2\r\n35\r\n$2\r\n36\r\n$2\r\n37\r\n$2\r\n38\r\n$2\r\n39\r\n$2\r\n40\r\n$2\r\n41\r\n$2\r\n42\r\n$2\r\n43\r\n$2\r\n44\r\n$2\r\n45\r\n$2\r\n46\r\n$2\r\n47\r\n$2\r\n48\r\n$2\r\n49\r\n$2\r\n50\r\n$2\r\n51\r\n$2\r\n52\r\n$2\r\n53\r\n$2\r\n54\r\n$2\r\n55\r\n$2\r\n56\r\n$2\r\n57\r\n$2\r\n58\r\n$2\r\n59\r\n$2\r\n60\r\n$2\r\n61\r\n$2\r\n62\r\n$2\r\n63\r\n$2\r\n64\r\n$2\r\n65\r\n$2\r\n66\r\n$2\r\n67\r\n$2\r\n68\r\n$2\r\n69\r\n$2\r\n70\r\n$2\r\n71\r\n$2\r\n72\r\n$2\r\n73\r\n$2\r\n74\r\n$2\r\n75\r\n$2\r\n76\r\n$2\r\n77\r\n$2\r\n78\r\n$2\r\n79\r\n$2\r\n80\r\n$2\r\n81\r\n$2\r\n82\r\n$2\r\n83\r\n$2\r\n84\r\n$2\r\n85\r\n$2\r\n86\r\n$2\r\n87\r\n$2\r\n88\r\n$2\r\n89\r\n$2\r\n90\r\n$2\r\n91\r\n$2\r\n92\r\n$2\r\n93\r\n$2\r\n94\r\n$2\r\n95\r\n$2\r\n96\r\n$2\r\n97\r\n$2\r\n98\r\n$2\r\n99\r\n$3\r\n100\r\n$3\r\n101\r\n$3\r\n102\r\n$3\r\n103\r\n$3\r\n104\r\n$3\r\n105\r\n$3\r\n106\r\n$3\r\n107\r\n$3\r\n108\r\n$3\r\n109\r\n$3\r\n110\r\n$3\r\n111\r\n$3\r\n112\r\n$3\r\n113\r\n$3\r\n114\r\n$3\r\n115\r\n$3\r\n116\r\n$3\r\n117\r\n$3\r\n118\r\n$3\r\n119\r\n$3\r\n120\r\n$3\r\n121\r\n$3\r\n122\r\n$3\r\n123\r\n$3\r\n124\r\n$3\r\n125\r\n$3\r\n126\r\n$3\r\n127\r\n$3\r\n128\r\n$3\r\n129\r\n$3\r\n130\r\n$3\r\n131\r\n$3\r\n132\r\n$3\r\n133\r\n$3\r\n134\r\n$3\r\n135\r\n$3\r\n136\r\n$3\r\n137\r\n$3\r\n138\r\n$3\r\n139\r\n$3\r\n140\r\n$3\r\n141\r\n$3\r\n142\r\n$3\r\n143\r\n$3\r\n144\r\n$3\r\n145\r\n$3\r\n146\r\n$3\r\n147\r\n$3\r\n148\r\n$3\r\n149\r\n$3\r\n150\r\n$3\r\n151\r\n$3\r\n152\r\n$3\r\n153\r\n$3\r\n154\r\n$3\r\n155\r\n$3\r\n156\r\n$3\r\n157\r\n$3\r\n158\r\n$3\r\n159\r\n$3\r\n160\r\n$3\r\n161\r\n$3\r\n162\r\n$3\r\n163\r\n$3\r\n164\r\n$3\r\n165\r\n$3\r\n166\r\n$3\r\n167\r\n$3\r\n168\r\n$3\r\n169\r\n$3\r\n170\r\n$3\r\n171\r\n$3\r\n172\r\n$3\r\n173\r\n$3\r\n1">>, 275 | B2 = <<"74\r\n$3\r\n175\r\n$3\r\n176\r\n$3\r\n177\r\n$3\r\n178\r\n$3\r\n179\r\n$3\r\n180\r\n$3\r\n181\r\n$3\r\n182\r\n$3\r\n183\r\n$3\r\n184\r\n$3\r\n185\r\n$3\r\n186\r\n$3\r\n187\r\n$3\r\n188\r\n$3\r\n189\r\n$3\r\n190\r\n$3\r\n191\r\n$3\r\n192\r\n$3\r\n193\r\n$3\r\n194\r\n$3\r\n195\r\n$3\r\n196\r\n$3\r\n197\r\n$3\r\n198\r\n$3\r\n199\r\n$3\r\n200\r\n$3\r\n201\r\n$3\r\n202\r\n$3\r\n203\r\n$3\r\n204\r\n$3\r\n205\r\n$3\r\n206\r\n$3\r\n207\r\n$3\r\n208\r\n$3\r\n209\r\n$3\r\n210\r\n$3\r\n211\r\n$3\r\n212\r\n$3\r\n213\r\n$3\r\n214\r\n$3\r\n215\r\n$3\r\n216\r\n$3\r\n217\r\n$3\r\n218\r\n$3\r\n219\r\n$3\r\n220\r\n$3\r\n221\r\n$3\r\n222\r\n$3\r\n223\r\n$3\r\n224\r\n$3\r\n225\r\n$3\r\n226\r\n$3\r\n227\r\n$3\r\n228\r\n$3\r\n229\r\n$3\r\n230\r\n$3\r\n231\r\n$3\r\n232\r\n$3\r\n233\r\n$3\r\n234\r\n$3\r\n235\r\n$3\r\n236\r\n$3\r\n237\r\n$3\r\n238\r\n$3\r\n239\r\n$3\r\n240\r\n$3\r\n241\r\n$3\r\n242\r\n$3\r\n243\r\n$3\r\n244\r\n$3\r\n245\r\n$3\r\n246\r\n$3\r\n247\r\n$3\r\n248\r\n$3\r\n249\r\n$3\r\n250\r\n$3\r\n251\r\n$3\r\n252\r\n$3\r\n253\r\n$3\r\n254\r\n$3\r\n255\r\n$3\r\n256\r\n$3\r\n257\r\n$3\r\n258\r\n$3\r\n259\r\n$3\r\n260\r\n$3\r\n261\r\n$3\r\n262\r\n$3\r\n263\r\n$3\r\n264\r\n$3\r\n265\r\n$3\r\n266\r\n$3\r\n267\r\n$3\r\n268\r\n$3\r\n269\r\n$3\r\n270\r\n$3\r\n271\r\n$3\r\n272\r\n$3\r\n273\r\n$3\r\n274\r\n$3\r\n275\r\n$3\r\n276\r\n$3\r\n277\r\n$3\r\n278\r\n$3\r\n279\r\n$3\r\n280\r\n$3\r\n281\r\n$3\r\n282\r\n$3\r\n283\r\n$3\r\n284\r\n$3\r\n285\r\n$3\r\n286\r\n$3\r\n287\r\n$3\r\n288\r\n$3\r\n289\r\n$3\r\n290\r\n$3\r\n291\r\n$3\r\n292\r\n$3\r\n293\r\n$3\r\n294\r\n$3\r\n295\r\n$3\r\n296\r\n$3\r\n297\r\n$3\r\n298\r\n$3\r\n299\r\n$3\r\n300\r\n$3\r\n301\r\n$3\r\n302\r\n$3\r\n303\r\n$3\r\n304\r\n$3\r\n305\r\n$3\r\n306\r\n$3\r\n307\r\n$3\r\n308\r\n$3\r\n309\r\n$3\r\n310\r\n$3\r\n311\r\n$3\r\n312\r\n$3\r\n313\r\n$3\r\n314\r\n$3\r\n315\r\n$3\r\n316\r\n$3\r\n317\r\n$3\r\n318\r\n$3\r\n319\r\n$3\r\n320\r\n$3\r\n321\r\n$3\r\n322\r\n$3\r\n323\r\n$3\r\n324\r\n$3\r\n325\r\n$3\r\n326\r\n$3\r\n327\r\n$3\r\n328\r\n$3\r\n329\r\n$3\r\n330\r\n$3\r\n331\r\n$3\r\n332\r\n$3\r\n333\r\n$3\r\n334\r\n$3\r\n335\r\n$3\r\n336">>, 276 | B3 = <<"\r\n$3\r\n337\r\n$3\r\n338\r\n$3\r\n339\r\n$3\r\n340\r\n$3\r\n341\r\n$3\r\n342\r\n$3\r\n343\r\n$3\r\n344\r\n$3\r\n345\r\n$3\r\n346\r\n$3\r\n347\r\n$3\r\n348\r\n$3\r\n349\r\n$3\r\n350\r\n$3\r\n351\r\n$3\r\n352\r\n$3\r\n353\r\n$3\r\n354\r\n$3\r\n355\r\n$3\r\n356\r\n$3\r\n357\r\n$3\r\n358\r\n$3\r\n359\r\n$3\r\n360\r\n$3\r\n361\r\n$3\r\n362\r\n$3\r\n363\r\n$3\r\n364\r\n$3\r\n365\r\n$3\r\n366\r\n$3\r\n367\r\n$3\r\n368\r\n$3\r\n369\r\n$3\r\n370\r\n$3\r\n371\r\n$3\r\n372\r\n$3\r\n373\r\n$3\r\n374\r\n$3\r\n375\r\n$3\r\n376\r\n$3\r\n377\r\n$3\r\n378\r\n$3\r\n379\r\n$3\r\n380\r\n$3\r\n381\r\n$3\r\n382\r\n$3\r\n383\r\n$3\r\n384\r\n$3\r\n385\r\n$3\r\n386\r\n$3\r\n387\r\n$3\r\n388\r\n$3\r\n389\r\n$3\r\n390\r\n$3\r\n391\r\n$3\r\n392\r\n$3\r\n393\r\n$3\r\n394\r\n$3\r\n395\r\n$3\r\n396\r\n$3\r\n397\r\n$3\r\n398\r\n$3\r\n399\r\n$3\r\n400\r\n$3\r\n401\r\n$3\r\n402\r\n$3\r\n403\r\n$3\r\n404\r\n$3\r\n405\r\n$3\r\n406\r\n$3\r\n407\r\n$3\r\n408\r\n$3\r\n409\r\n$3\r\n410\r\n$3\r\n411\r\n$3\r\n412\r\n$3\r\n413\r\n$3\r\n414\r\n$3\r\n415\r\n$3\r\n416\r\n$3\r\n417\r\n$3\r\n418\r\n$3\r\n419\r\n$3\r\n420\r\n$3\r\n421\r\n$3\r\n422\r\n$3\r\n423\r\n$3\r\n424\r\n$3\r\n425\r\n$3\r\n426\r\n$3\r\n427\r\n$3\r\n428\r\n$3\r\n429\r\n$3\r\n430\r\n$3\r\n431\r\n$3\r\n432\r\n$3\r\n433\r\n$3\r\n434\r\n$3\r\n435\r\n$3\r\n436\r\n$3\r\n437\r\n$3\r\n438\r\n$3\r\n439\r\n$3\r\n440\r\n$3\r\n441\r\n$3\r\n442\r\n$3\r\n443\r\n$3\r\n444\r\n$3\r\n445\r\n$3\r\n446\r\n$3\r\n447\r\n$3\r\n448\r\n$3\r\n449\r\n$3\r\n450\r\n$3\r\n451\r\n$3\r\n452\r\n$3\r\n453\r\n$3\r\n454\r\n$3\r\n455\r\n$3\r\n456\r\n$3\r\n457\r\n$3\r\n458\r\n$3\r\n459\r\n$3\r\n460\r\n$3\r\n461\r\n$3\r\n462\r\n$3\r\n463\r\n$3\r\n464\r\n$3\r\n465\r\n$3\r\n466\r\n$3\r\n467\r\n$3\r\n468\r\n$3\r\n469\r\n$3\r\n470\r\n$3\r\n471\r\n$3\r\n472\r\n$3\r\n473\r\n$3\r\n474\r\n$3\r\n475\r\n$3\r\n476\r\n$3\r\n477\r\n$3\r\n478\r\n$3\r\n479\r\n$3\r\n480\r\n$3\r\n481\r\n$3\r\n482\r\n$3\r\n483\r\n$3\r\n484\r\n$3\r\n485\r\n$3\r\n486\r\n$3\r\n487\r\n$3\r\n488\r\n$3\r\n489\r\n$3\r\n490\r\n$3\r\n491\r\n$3\r\n492\r\n$3\r\n493\r\n$3\r\n494\r\n$3\r\n495\r\n$3\r\n496\r\n$3\r\n497\r\n$3\r\n498\r\n">>, 277 | 278 | {continue, ContinuationData1} = parse_multibulk(B1), 279 | {continue, ContinuationData2} = parse_multibulk(ContinuationData1, B2), 280 | 281 | EmptyBuffer = buffer_create(), 282 | ?assertMatch({continue, {in_parsing_bulks, 2, EmptyBuffer, _}}, 283 | parse_multibulk(ContinuationData2, B3)). 284 | 285 | %% @doc: Test a binary string which contains \r\n inside it's data 286 | binary_safe_test() -> 287 | B = <<"$14\r\nfoobar\r\nbarbaz\r\n">>, 288 | ?assertEqual({ok, <<"foobar\r\nbarbaz">>, init()}, parse(init(), B)). 289 | 290 | 291 | status_test() -> 292 | B = <<"+OK\r\n">>, 293 | ?assertEqual({ok, <<"OK">>, init()}, parse(init(), B)). 294 | 295 | status_chunked_test() -> 296 | B1 = <<"+O">>, 297 | B2 = <<"K\r\n">>, 298 | State1 = init(), 299 | 300 | ?assertEqual({continue, #pstate{state = status_continue, 301 | continuation_data = {incomplete_simple, buffer_create(<<"O">>)}}}, 302 | parse(State1, B1)), 303 | {continue, State2} = parse(State1, B1), 304 | ?assertEqual({ok, <<"OK">>, init()}, parse(State2, B2)). 305 | 306 | error_test() -> 307 | B = <<"-ERR wrong number of arguments for 'get' command\r\n">>, 308 | ?assertEqual({error, <<"ERR wrong number of arguments for 'get' command">>, init()}, 309 | parse(init(), B)). 310 | 311 | integer_test() -> 312 | B = <<":2\r\n">>, 313 | ?assertEqual({ok, <<"2">>, init()}, parse(init(), B)). 314 | 315 | integer_reply_inside_multibulk_test() -> 316 | B = <<"*2\r\n:1\r\n:1\r\n">>, 317 | ?assertEqual({ok, [<<"1">>, <<"1">>], init()}, parse(init(), B)). 318 | 319 | status_inside_multibulk_test() -> 320 | B = <<"*2\r\n+OK\r\n:1\r\n">>, 321 | ?assertEqual({ok, [<<"OK">>, <<"1">>], init()}, parse(init(), B)). 322 | 323 | error_inside_multibulk_test() -> 324 | B = <<"*2\r\n-ERR foobar\r\n:1\r\n">>, 325 | ?assertEqual({ok, [<<"ERR foobar">>, <<"1">>], init()}, parse(init(), B)). 326 | -------------------------------------------------------------------------------- /test/eredis_sub_tests.erl: -------------------------------------------------------------------------------- 1 | -module(eredis_sub_tests). 2 | 3 | -include_lib("eunit/include/eunit.hrl"). 4 | -include("eredis.hrl"). 5 | -include("eredis_sub.hrl"). 6 | 7 | -import(eredis, [create_multibulk/1]). 8 | 9 | c() -> 10 | Res = eredis:start_link(), 11 | ?assertMatch({ok, _}, Res), 12 | {ok, C} = Res, 13 | C. 14 | 15 | s() -> 16 | Res = eredis_sub:start_link("127.0.0.1", 6379, ""), 17 | ?assertMatch({ok, _}, Res), 18 | {ok, C} = Res, 19 | C. 20 | 21 | add_channels(Sub, Channels) -> 22 | ok = eredis_sub:controlling_process(Sub), 23 | ok = eredis_sub:subscribe(Sub, Channels), 24 | lists:foreach( 25 | fun (C) -> 26 | receive M -> 27 | ?assertEqual({subscribed, C, Sub}, M), 28 | eredis_sub:ack_message(Sub) 29 | end 30 | end, Channels). 31 | 32 | pubsub_test() -> 33 | Pub = c(), 34 | Sub = s(), 35 | add_channels(Sub, [<<"chan1">>, <<"chan2">>]), 36 | ok = eredis_sub:controlling_process(Sub), 37 | 38 | ?assertEqual({ok, <<"1">>}, eredis:q(Pub, ["PUBLISH", chan1, msg])), 39 | receive 40 | {message, _, _, _} = M -> 41 | ?assertEqual({message, <<"chan1">>, <<"msg">>, Sub}, M) 42 | after 10 -> 43 | throw(timeout) 44 | end, 45 | 46 | receive 47 | Msg -> 48 | throw({unexpected_message, Msg}) 49 | after 5 -> 50 | ok 51 | end, 52 | eredis_sub:stop(Sub). 53 | 54 | %% Push size so high, the queue will be used 55 | pubsub2_test() -> 56 | Pub = c(), 57 | Sub = s(), 58 | add_channels(Sub, [<<"chan">>]), 59 | ok = eredis_sub:controlling_process(Sub), 60 | lists:foreach( 61 | fun(_) -> 62 | Msg = binary:copy(<<"0">>, 2048), 63 | ?assertEqual({ok, <<"1">>}, eredis:q(Pub, [publish, chan, Msg])) 64 | end, lists:seq(1, 500)), 65 | Msgs = recv_all(Sub), 66 | ?assertEqual(500, length(Msgs)), 67 | eredis_sub:stop(Sub). 68 | 69 | pubsub_manage_subscribers_test() -> 70 | Pub = c(), 71 | Sub = s(), 72 | add_channels(Sub, [<<"chan">>]), 73 | unlink(Sub), 74 | Self = self(), 75 | ?assertMatch(#state{controlling_process={_, Self}}, get_state(Sub)), 76 | S1 = subscriber(Sub), 77 | ok = eredis_sub:controlling_process(Sub, S1), 78 | #state{controlling_process={_, S1}} = get_state(Sub), 79 | S2 = subscriber(Sub), 80 | ok = eredis_sub:controlling_process(Sub, S2), 81 | #state{controlling_process={_, S2}} = get_state(Sub), 82 | eredis:q(Pub, ["PUBLISH", chan, msg1]), 83 | S1 ! stop, 84 | ok = wait_for_stop(S1), 85 | eredis:q(Pub, ["PUBLISH", chan, msg2]), 86 | ?assertEqual({message, <<"chan">>, <<"msg1">>, Sub}, wait_for_msg(S2)), 87 | ?assertEqual({message, <<"chan">>, <<"msg2">>, Sub}, wait_for_msg(S2)), 88 | S2 ! stop, 89 | ok = wait_for_stop(S2), 90 | Ref = erlang:monitor(process, Sub), 91 | receive {'DOWN', Ref, process, Sub, _} -> ok end. 92 | 93 | 94 | pubsub_connect_disconnect_messages_test() -> 95 | Pub = c(), 96 | Sub = s(), 97 | add_channels(Sub, [<<"chan">>]), 98 | S = subscriber(Sub), 99 | ok = eredis_sub:controlling_process(Sub, S), 100 | eredis:q(Pub, ["PUBLISH", chan, msg]), 101 | wait_for_msg(S), 102 | #state{socket=Sock} = get_state(Sub), 103 | gen_tcp:close(Sock), 104 | Sub ! {tcp_closed, Sock}, 105 | ?assertEqual({eredis_disconnected, Sub}, wait_for_msg(S)), 106 | ?assertEqual({eredis_reconnect_attempt, Sub}, wait_for_msg(S)), 107 | ?assertEqual({eredis_connected, Sub}, wait_for_msg(S)), 108 | eredis_sub:stop(Sub). 109 | 110 | 111 | 112 | drop_queue_test() -> 113 | Pub = c(), 114 | {ok, Sub} = eredis_sub:start_link("127.0.0.1", 6379, "", 100, 10, drop), 115 | add_channels(Sub, [<<"foo">>]), 116 | ok = eredis_sub:controlling_process(Sub), 117 | 118 | [eredis:q(Pub, [publish, foo, N]) || N <- lists:seq(1, 12)], 119 | 120 | receive M1 -> ?assertEqual({message,<<"foo">>,<<"1">>, Sub}, M1) end, 121 | receive M2 -> ?assertEqual({dropped, 11}, M2) end, 122 | eredis_sub:stop(Sub). 123 | 124 | 125 | crash_queue_test() -> 126 | Pub = c(), 127 | {ok, Sub} = eredis_sub:start_link("127.0.0.1", 6379, "", 100, 10, exit), 128 | add_channels(Sub, [<<"foo">>]), 129 | 130 | true = unlink(Sub), 131 | ok = eredis_sub:controlling_process(Sub), 132 | Ref = erlang:monitor(process, Sub), 133 | 134 | [eredis:q(Pub, [publish, foo, N]) || N <- lists:seq(1, 12)], 135 | 136 | receive M1 -> ?assertEqual({message,<<"foo">>,<<"1">>, Sub}, M1) end, 137 | receive M2 -> ?assertEqual({'DOWN', Ref, process, Sub, max_queue_size}, M2) end. 138 | 139 | 140 | 141 | dynamic_channels_test() -> 142 | Pub = c(), 143 | Sub = s(), 144 | ok = eredis_sub:controlling_process(Sub), 145 | 146 | eredis:q(Pub, [publish, newchan, foo]), 147 | 148 | receive {message, <<"foo">>, _, _} -> ?assert(false) 149 | after 5 -> ok end, 150 | 151 | %% We do the following twice to show that subscribing to the same channel 152 | %% doesn't cause the channel to show up twice 153 | lists:foreach(fun(_) -> 154 | eredis_sub:subscribe(Sub, [<<"newchan">>, <<"otherchan">>]), 155 | receive M1 -> ?assertEqual({subscribed, <<"newchan">>, Sub}, M1) end, 156 | eredis_sub:ack_message(Sub), 157 | receive M2 -> ?assertEqual({subscribed, <<"otherchan">>, Sub}, M2) end, 158 | eredis_sub:ack_message(Sub), 159 | 160 | {ok, Channels} = eredis_sub:channels(Sub), 161 | ?assertEqual(true, lists:member(<<"otherchan">>, Channels)), 162 | ?assertEqual(true, lists:member(<<"newchan">>, Channels)), 163 | ?assertEqual(2, length(Channels)) 164 | end, lists:seq(0, 1)), 165 | 166 | eredis:q(Pub, [publish, newchan, foo]), 167 | ?assertEqual([{message, <<"newchan">>, <<"foo">>, Sub}], recv_all(Sub)), 168 | eredis:q(Pub, [publish, otherchan, foo]), 169 | ?assertEqual([{message, <<"otherchan">>, <<"foo">>, Sub}], recv_all(Sub)), 170 | 171 | eredis_sub:unsubscribe(Sub, [<<"otherchan">>]), 172 | eredis_sub:ack_message(Sub), 173 | receive M3 -> ?assertEqual({unsubscribed, <<"otherchan">>, Sub}, M3) end, 174 | 175 | ?assertEqual({ok, [<<"newchan">>]}, eredis_sub:channels(Sub)). 176 | 177 | 178 | recv_all(Sub) -> 179 | recv_all(Sub, []). 180 | 181 | recv_all(Sub, Acc) -> 182 | receive 183 | {message, _, _, _} = InMsg -> 184 | eredis_sub:ack_message(Sub), 185 | recv_all(Sub, [InMsg | Acc]) 186 | after 5 -> 187 | lists:reverse(Acc) 188 | end. 189 | 190 | subscriber(Client) -> 191 | Test = self(), 192 | Pid = spawn(fun () -> subscriber(Client, Test) end), 193 | spawn(fun() -> 194 | Ref = erlang:monitor(process, Pid), 195 | receive 196 | {'DOWN', Ref, _, _, _} -> 197 | Test ! {stopped, Pid} 198 | end 199 | end), 200 | Pid. 201 | 202 | subscriber(Client, Test) -> 203 | receive 204 | stop -> 205 | ok; 206 | Msg -> 207 | Test ! {got_message, self(), Msg}, 208 | eredis_sub:ack_message(Client), 209 | subscriber(Client, Test) 210 | end. 211 | 212 | wait_for_msg(Subscriber) -> 213 | receive 214 | {got_message, Subscriber, Msg} -> 215 | Msg 216 | end. 217 | 218 | wait_for_stop(Subscriber) -> 219 | receive 220 | {stopped, Subscriber} -> 221 | ok 222 | end. 223 | 224 | get_state(Pid) 225 | when is_pid(Pid) -> 226 | {status, _, _, [_, _, _, _, State]} = sys:get_status(Pid), 227 | get_state(State); 228 | get_state([{data, [{"State", State}]} | _]) -> 229 | State; 230 | get_state([_|Rest]) -> 231 | get_state(Rest). 232 | 233 | 234 | 235 | 236 | 237 | % Tests for Pattern Subscribe 238 | 239 | add_channels_pattern(Sub, Channels) -> 240 | ok = eredis_sub:controlling_process(Sub), 241 | ok = eredis_sub:psubscribe(Sub, Channels), 242 | lists:foreach( 243 | fun (C) -> 244 | receive M -> 245 | ?assertEqual({subscribed, C, Sub}, M), 246 | eredis_sub:ack_message(Sub) 247 | end 248 | end, Channels). 249 | 250 | 251 | 252 | 253 | 254 | pubsub_pattern_test() -> 255 | Pub = c(), 256 | Sub = s(), 257 | add_channels_pattern(Sub, [<<"chan1*">>, <<"chan2*">>]), 258 | ok = eredis_sub:controlling_process(Sub), 259 | 260 | ?assertEqual({ok, <<"1">>}, eredis:q(Pub, ["PUBLISH", <<"chan123">>, <<"msg">>])), 261 | receive 262 | {pmessage, _Pattern, _Channel, _Message, _} = M -> 263 | ?assertEqual({pmessage, <<"chan1*">>,<<"chan123">>, <<"msg">>, Sub}, M) 264 | after 10 -> 265 | throw(timeout) 266 | end, 267 | 268 | eredis_sub:punsubscribe(Sub, [<<"chan1*">> , <<"chan2*">>]), 269 | eredis_sub:ack_message(Sub), 270 | eredis_sub:ack_message(Sub), 271 | receive {unsubscribed,_,_} = M2 -> ?assertEqual({unsubscribed, <<"chan1*">>, Sub}, M2) end, 272 | eredis_sub:ack_message(Sub), 273 | receive {unsubscribed,_,_} = M3 -> ?assertEqual({unsubscribed, <<"chan2*">>, Sub}, M3) end, 274 | eredis_sub:ack_message(Sub), 275 | 276 | ?assertEqual({ok, <<"0">>}, eredis:q(Pub, ["PUBLISH", <<"chan123">>, <<"msg">>])), 277 | receive 278 | Msg -> throw({unexpected_message, Msg}) 279 | after 10 -> 280 | ok 281 | end, 282 | 283 | eredis_sub:stop(Sub). 284 | 285 | 286 | 287 | 288 | -------------------------------------------------------------------------------- /test/eredis_tests.erl: -------------------------------------------------------------------------------- 1 | -module(eredis_tests). 2 | 3 | -include_lib("eunit/include/eunit.hrl"). 4 | -include("eredis.hrl"). 5 | 6 | -import(eredis, [create_multibulk/1]). 7 | 8 | connect_test() -> 9 | ?assertMatch({ok, _}, eredis:start_link("127.0.0.1", 6379)), 10 | ?assertMatch({ok, _}, eredis:start_link("localhost", 6379)). 11 | 12 | connect_socket_options_test() -> 13 | ?assertMatch({ok, _}, eredis:start_link([{socket_options, [{keepalive, true}]}])), 14 | ?assertMatch({ok, _}, eredis:start_link("localhost", 6379, 0, "",100, 5000, [{keepalive, true}])). 15 | 16 | get_set_test() -> 17 | C = c(), 18 | ?assertMatch({ok, _}, eredis:q(C, ["DEL", foo])), 19 | 20 | ?assertEqual({ok, undefined}, eredis:q(C, ["GET", foo])), 21 | ?assertEqual({ok, <<"OK">>}, eredis:q(C, ["SET", foo, bar])), 22 | ?assertEqual({ok, <<"bar">>}, eredis:q(C, ["GET", foo])). 23 | 24 | 25 | delete_test() -> 26 | C = c(), 27 | ?assertMatch({ok, _}, eredis:q(C, ["DEL", foo])), 28 | 29 | ?assertEqual({ok, <<"OK">>}, eredis:q(C, ["SET", foo, bar])), 30 | ?assertEqual({ok, <<"1">>}, eredis:q(C, ["DEL", foo])), 31 | ?assertEqual({ok, undefined}, eredis:q(C, ["GET", foo])). 32 | 33 | mset_mget_test() -> 34 | C = c(), 35 | Keys = lists:seq(1, 1000), 36 | 37 | ?assertMatch({ok, _}, eredis:q(C, ["DEL" | Keys])), 38 | 39 | KeyValuePairs = [[K, K*2] || K <- Keys], 40 | ExpectedResult = [list_to_binary(integer_to_list(K * 2)) || K <- Keys], 41 | 42 | ?assertEqual({ok, <<"OK">>}, eredis:q(C, ["MSET" | lists:flatten(KeyValuePairs)])), 43 | ?assertEqual({ok, ExpectedResult}, eredis:q(C, ["MGET" | Keys])), 44 | ?assertMatch({ok, _}, eredis:q(C, ["DEL" | Keys])). 45 | 46 | exec_test() -> 47 | C = c(), 48 | 49 | ?assertMatch({ok, _}, eredis:q(C, ["LPUSH", "k1", "b"])), 50 | ?assertMatch({ok, _}, eredis:q(C, ["LPUSH", "k1", "a"])), 51 | ?assertMatch({ok, _}, eredis:q(C, ["LPUSH", "k2", "c"])), 52 | 53 | ?assertEqual({ok, <<"OK">>}, eredis:q(C, ["MULTI"])), 54 | ?assertEqual({ok, <<"QUEUED">>}, eredis:q(C, ["LRANGE", "k1", "0", "-1"])), 55 | ?assertEqual({ok, <<"QUEUED">>}, eredis:q(C, ["LRANGE", "k2", "0", "-1"])), 56 | 57 | ExpectedResult = [[<<"a">>, <<"b">>], [<<"c">>]], 58 | 59 | ?assertEqual({ok, ExpectedResult}, eredis:q(C, ["EXEC"])), 60 | 61 | ?assertMatch({ok, _}, eredis:q(C, ["DEL", "k1", "k2"])). 62 | 63 | exec_nil_test() -> 64 | C1 = c(), 65 | C2 = c(), 66 | 67 | ?assertEqual({ok, <<"OK">>}, eredis:q(C1, ["WATCH", "x"])), 68 | ?assertMatch({ok, _}, eredis:q(C2, ["INCR", "x"])), 69 | ?assertEqual({ok, <<"OK">>}, eredis:q(C1, ["MULTI"])), 70 | ?assertEqual({ok, <<"QUEUED">>}, eredis:q(C1, ["GET", "x"])), 71 | ?assertEqual({ok, undefined}, eredis:q(C1, ["EXEC"])), 72 | ?assertMatch({ok, _}, eredis:q(C1, ["DEL", "x"])). 73 | 74 | pipeline_test() -> 75 | C = c(), 76 | 77 | P1 = [["SET", a, "1"], 78 | ["LPUSH", b, "3"], 79 | ["LPUSH", b, "2"]], 80 | 81 | ?assertEqual([{ok, <<"OK">>}, {ok, <<"1">>}, {ok, <<"2">>}], 82 | eredis:qp(C, P1)), 83 | 84 | P2 = [["MULTI"], 85 | ["GET", a], 86 | ["LRANGE", b, "0", "-1"], 87 | ["EXEC"]], 88 | 89 | ?assertEqual([{ok, <<"OK">>}, 90 | {ok, <<"QUEUED">>}, 91 | {ok, <<"QUEUED">>}, 92 | {ok, [<<"1">>, [<<"2">>, <<"3">>]]}], 93 | eredis:qp(C, P2)), 94 | 95 | ?assertMatch({ok, _}, eredis:q(C, ["DEL", a, b])). 96 | 97 | pipeline_mixed_test() -> 98 | C = c(), 99 | P1 = [["LPUSH", c, "1"] || _ <- lists:seq(1, 100)], 100 | P2 = [["LPUSH", d, "1"] || _ <- lists:seq(1, 100)], 101 | Expect = [{ok, list_to_binary(integer_to_list(I))} || I <- lists:seq(1, 100)], 102 | spawn(fun () -> 103 | erlang:yield(), 104 | ?assertEqual(Expect, eredis:qp(C, P1)) 105 | end), 106 | spawn(fun () -> 107 | ?assertEqual(Expect, eredis:qp(C, P2)) 108 | end), 109 | timer:sleep(10), 110 | ?assertMatch({ok, _}, eredis:q(C, ["DEL", c, d])). 111 | 112 | q_noreply_test() -> 113 | C = c(), 114 | ?assertEqual(ok, eredis:q_noreply(C, ["GET", foo])), 115 | ?assertEqual(ok, eredis:q_noreply(C, ["SET", foo, bar])), 116 | %% Even though q_noreply doesn't wait, it is sent before subsequent requests: 117 | ?assertEqual({ok, <<"bar">>}, eredis:q(C, ["GET", foo])). 118 | 119 | q_async_test() -> 120 | C = c(), 121 | ?assertEqual({ok, <<"OK">>}, eredis:q(C, ["SET", foo, bar])), 122 | ?assertEqual(ok, eredis:q_async(C, ["GET", foo], self())), 123 | receive 124 | {response, Msg} -> 125 | ?assertEqual(Msg, {ok, <<"bar">>}), 126 | ?assertMatch({ok, _}, eredis:q(C, ["DEL", foo])) 127 | end. 128 | 129 | c() -> 130 | Res = eredis:start_link(), 131 | ?assertMatch({ok, _}, Res), 132 | {ok, C} = Res, 133 | C. 134 | 135 | 136 | 137 | c_no_reconnect() -> 138 | Res = eredis:start_link("127.0.0.1", 6379, 0, "", no_reconnect), 139 | ?assertMatch({ok, _}, Res), 140 | {ok, C} = Res, 141 | C. 142 | 143 | multibulk_test_() -> 144 | [?_assertEqual(<<"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n">>, 145 | list_to_binary(create_multibulk(["SET", "foo", "bar"]))), 146 | ?_assertEqual(<<"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n">>, 147 | list_to_binary(create_multibulk(['SET', foo, bar]))), 148 | 149 | ?_assertEqual(<<"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\n123\r\n">>, 150 | list_to_binary(create_multibulk(['SET', foo, 123]))), 151 | 152 | ?_assertThrow({cannot_store_floats, 123.5}, 153 | list_to_binary(create_multibulk(['SET', foo, 123.5]))) 154 | ]. 155 | 156 | undefined_database_test() -> 157 | ?assertMatch({ok,_}, eredis:start_link("localhost", 6379, undefined)). 158 | 159 | connection_failure_during_start_no_reconnect_test() -> 160 | process_flag(trap_exit, true), 161 | Res = eredis:start_link("localhost", 6378, 0, "", no_reconnect), 162 | ?assertMatch({error, _}, Res), 163 | IsDead = receive {'EXIT', _, _} -> died 164 | after 1000 -> still_alive end, 165 | process_flag(trap_exit, false), 166 | ?assertEqual(died, IsDead). 167 | 168 | connection_failure_during_start_reconnect_test() -> 169 | process_flag(trap_exit, true), 170 | Res = eredis:start_link("localhost", 6378, 0, "", 100), 171 | ?assertMatch({ok, _}, Res), 172 | {ok, ClientPid} = Res, 173 | IsDead = receive {'EXIT', ClientPid, _} -> died 174 | after 400 -> still_alive end, 175 | process_flag(trap_exit, false), 176 | ?assertEqual(still_alive, IsDead). 177 | 178 | tcp_closed_test() -> 179 | C = c(), 180 | tcp_closed_rig(C). 181 | 182 | tcp_closed_no_reconnect_test() -> 183 | C = c_no_reconnect(), 184 | tcp_closed_rig(C). 185 | 186 | tcp_closed_rig(C) -> 187 | %% fire async requests to add to redis client queue and then trick 188 | %% the client into thinking the connection to redis has been 189 | %% closed. This behavior can be observed when Redis closes an idle 190 | %% connection just as a traffic burst starts. 191 | DoSend = fun(tcp_closed) -> 192 | C ! {tcp_closed, fake_socket}; 193 | (Cmd) -> 194 | eredis:q(C, Cmd) 195 | end, 196 | %% attach an id to each message for later 197 | Msgs = [{1, ["GET", "foo"]}, 198 | {2, ["GET", "bar"]}, 199 | {3, tcp_closed}], 200 | Pids = [ remote_query(DoSend, M) || M <- Msgs ], 201 | Results = gather_remote_queries(Pids), 202 | ?assertEqual({error, tcp_closed}, proplists:get_value(1, Results)), 203 | ?assertEqual({error, tcp_closed}, proplists:get_value(2, Results)). 204 | 205 | remote_query(Fun, {Id, Cmd}) -> 206 | Parent = self(), 207 | spawn(fun() -> 208 | Result = Fun(Cmd), 209 | Parent ! {self(), Id, Result} 210 | end). 211 | 212 | gather_remote_queries(Pids) -> 213 | gather_remote_queries(Pids, []). 214 | 215 | gather_remote_queries([], Acc) -> 216 | Acc; 217 | gather_remote_queries([Pid | Rest], Acc) -> 218 | receive 219 | {Pid, Id, Result} -> 220 | gather_remote_queries(Rest, [{Id, Result} | Acc]) 221 | after 222 | 10000 -> 223 | error({gather_remote_queries, timeout}) 224 | end. 225 | --------------------------------------------------------------------------------