From 553acfbc124db2942e6af8d0497a7a8216156f8a Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Tue, 15 Jul 2014 12:20:43 +0100 Subject: [PATCH 01/44] Try to find out what's happening --- lib/em-hiredis/lock.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/em-hiredis/lock.rb b/lib/em-hiredis/lock.rb index 6df816d..da0a77c 100644 --- a/lib/em-hiredis/lock.rb +++ b/lib/em-hiredis/lock.rb @@ -57,7 +57,11 @@ def unlock df = EM::DefaultDeferrable.new @redis.lock_release([@key], [@token]).callback { |keys_removed| - if keys_removed > 0 + # DEBUGGING WTF + if keys_removed.is_a? String + EM::Hiredis.logger.error "#{to_s}: Received String where expected int [#{keys_removed}]" + df.fail("WTF") + elsif keys_removed > 0 EM::Hiredis.logger.debug "#{to_s} released" df.succeed else From 4401f08ec114c98583ac3c046ea2c6f0f9def069 Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Thu, 16 Oct 2014 09:38:42 +0100 Subject: [PATCH 02/44] Update spec to be compatible with redis 2.8 "ERR" in case of bad type has become more specific "WRONGTYPE" --- spec/base_client_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/base_client_spec.rb b/spec/base_client_spec.rb index d764eac..171935a 100644 --- a/spec/base_client_spec.rb +++ b/spec/base_client_spec.rb @@ -105,11 +105,11 @@ df.errback { |e| e.class.should == EM::Hiredis::RedisError e.should be_kind_of(EM::Hiredis::Error) - msg = "ERR Operation against a key holding the wrong kind of value" - e.message.should == msg + msg = /Operation against a key holding the wrong kind of value/ + e.message.should =~ msg # This is the wrapped error from redis: e.redis_error.class.should == RuntimeError - e.redis_error.message.should == msg + e.redis_error.message.should =~ msg done } } From 99d9b8a93e0f718a92b66cf481d34cd1008dd6ba Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Thu, 16 Oct 2014 10:32:41 +0100 Subject: [PATCH 03/44] Fix reconnect for exceptions Depending on the type of problem creating a connection (DNS resolution vs timeout vs ...), EventMachine may create a connection and then report failure via a callback, or it may throw an exception immediately. This commit fixes our handling of the second case, during reconnects. Initial connection may still throw. --- lib/em-hiredis/base_client.rb | 69 +++++++++++++++++++---------------- spec/base_client_spec.rb | 14 +++++++ 2 files changed, 52 insertions(+), 31 deletions(-) diff --git a/lib/em-hiredis/base_client.rb b/lib/em-hiredis/base_client.rb index 85e9175..2fc227a 100644 --- a/lib/em-hiredis/base_client.rb +++ b/lib/em-hiredis/base_client.rb @@ -73,36 +73,7 @@ def connect @auto_reconnect = true @connection = EM.connect(@host, @port, Connection, @host, @port) - @connection.on(:closed) do - cancel_inactivity_checks - if @connected - @defs.each { |d| d.fail(Error.new("Redis disconnected")) } - @defs = [] - @deferred_status = nil - @connected = false - if @auto_reconnect - # Next tick avoids reconnecting after for example EM.stop - EM.next_tick { reconnect } - end - emit(:disconnected) - EM::Hiredis.logger.info("#{@connection} Disconnected") - else - if @auto_reconnect - @reconnect_failed_count += 1 - @reconnect_timer = EM.add_timer(EM::Hiredis.reconnect_timeout) { - @reconnect_timer = nil - reconnect - } - emit(:reconnect_failed, @reconnect_failed_count) - EM::Hiredis.logger.info("#{@connection} Reconnect failed") - - if @reconnect_failed_count >= 4 - emit(:failed) - self.fail(Error.new("Could not connect after 4 attempts")) - end - end - end - end + @connection.on(:closed) { handle_disconnect } @connection.on(:connected) do @connected = true @@ -223,8 +194,13 @@ def method_missing(sym, *args) def reconnect @reconnecting = true - @connection.reconnect @host, @port EM::Hiredis.logger.info("#{@connection} Reconnecting") + begin + @connection.reconnect @host, @port + rescue EventMachine::ConnectionError => e + EM::Hiredis.logger.error("Error during connect: #{e.to_s}") + EM.next_tick { handle_disconnect } + end end def cancel_inactivity_checks @@ -260,5 +236,36 @@ def handle_reply(reply) deferred.succeed(reply) if deferred end end + + def handle_disconnect + cancel_inactivity_checks + if @connected + @defs.each { |d| d.fail(Error.new("Redis disconnected")) } + @defs = [] + @deferred_status = nil + @connected = false + if @auto_reconnect + # Next tick avoids reconnecting after for example EM.stop + EM.next_tick { reconnect } + end + emit(:disconnected) + EM::Hiredis.logger.info("#{@connection} Disconnected") + else + if @auto_reconnect + @reconnect_failed_count += 1 + @reconnect_timer = EM.add_timer(EM::Hiredis.reconnect_timeout) { + @reconnect_timer = nil + reconnect + } + emit(:reconnect_failed, @reconnect_failed_count) + EM::Hiredis.logger.info("#{@connection} Reconnect failed") + + if @reconnect_failed_count >= 4 + emit(:failed) + self.fail(Error.new("Could not connect after 4 attempts")) + end + end + end + end end end diff --git a/spec/base_client_spec.rb b/spec/base_client_spec.rb index 171935a..617a6ad 100644 --- a/spec/base_client_spec.rb +++ b/spec/base_client_spec.rb @@ -26,6 +26,20 @@ end end + it "should emit an event on reconnect failure, with the retry count (DNS resolution)" do + # Assumes there is no redis server on 9999 + connect(1, "redis://localhost:6379/") do |redis| + expected = 1 + redis.on(:reconnect_failed) { |count| + count.should == expected + expected += 1 + done if expected == 3 + } + + redis.reconnect!("redis://not-a-host:9999/") + end + end + it "should emit disconnected when the connection closes" do connect do |redis| redis.on(:disconnected) { From fb062109bd389a459a75358dca816ea18393e490 Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Thu, 16 Oct 2014 11:26:19 +0100 Subject: [PATCH 04/44] More correct error detection --- lib/em-hiredis/lock.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/em-hiredis/lock.rb b/lib/em-hiredis/lock.rb index da0a77c..0f03269 100644 --- a/lib/em-hiredis/lock.rb +++ b/lib/em-hiredis/lock.rb @@ -58,8 +58,8 @@ def unlock df = EM::DefaultDeferrable.new @redis.lock_release([@key], [@token]).callback { |keys_removed| # DEBUGGING WTF - if keys_removed.is_a? String - EM::Hiredis.logger.error "#{to_s}: Received String where expected int [#{keys_removed}]" + if !keys_removed.is_a?(Fixnum) + EM::Hiredis.logger.error "#{to_s}: Received String where expected int [#{keys_removed.inspect}]" df.fail("WTF") elsif keys_removed > 0 EM::Hiredis.logger.debug "#{to_s} released" From 2f10bafdb1702f8f7f4d0c77cc1cdb93e33b7e3b Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Thu, 16 Oct 2014 11:36:35 +0100 Subject: [PATCH 05/44] Avoid an unnecessary closure --- lib/em-hiredis/base_client.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/em-hiredis/base_client.rb b/lib/em-hiredis/base_client.rb index 2fc227a..4f2fea5 100644 --- a/lib/em-hiredis/base_client.rb +++ b/lib/em-hiredis/base_client.rb @@ -73,7 +73,7 @@ def connect @auto_reconnect = true @connection = EM.connect(@host, @port, Connection, @host, @port) - @connection.on(:closed) { handle_disconnect } + @connection.on(:closed, &method(:handle_disconnect)) @connection.on(:connected) do @connected = true From b7671ae861cc8e9b28f1aabc626b2a7f7ee5922f Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Tue, 2 Dec 2014 19:34:57 +0000 Subject: [PATCH 06/44] Dirt simple pre-processing for lua scripts --- lib/em-hiredis/client.rb | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/em-hiredis/client.rb b/lib/em-hiredis/client.rb index 3fda02c..89a4caa 100644 --- a/lib/em-hiredis/client.rb +++ b/lib/em-hiredis/client.rb @@ -8,13 +8,26 @@ def self.connect(host = 'localhost', port = 6379) def self.load_scripts_from(dir) Dir.glob("#{dir}/*.lua").each do |f| - name = Regexp.new(/([^\/]*)\.lua$/).match(f)[1] - lua = File.open(f, 'r').read + name = File.basename(f, '.lua') + lua = load_script(f) EM::Hiredis.logger.debug { "Registering script: #{name}" } EM::Hiredis::Client.register_script(name, lua) end end + def self.load_script(file) + script_text = File.open(file, 'r').read + + inc_path = File.dirname(file) + while (m = /^-- #include (.*)$/.match(script_text)) + inc_file = m[1] + inc_body = File.read("#{inc_path}/#{inc_file}") + to_replace = Regexp.new("^-- #include #{inc_file}$") + script_text = script_text.gsub(to_replace, "#{inc_body}\n") + end + script_text + end + def self.register_script(name, lua) sha = Digest::SHA1.hexdigest(lua) self.send(:define_method, name.to_sym) { |keys, args=[]| From d23793de56987251704a8f5bcd910dee1ca51fbf Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Wed, 3 Dec 2014 10:15:27 +0000 Subject: [PATCH 07/44] Readme notes on lua includes --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index a976ee3..11b7b38 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,25 @@ If you pass a block to `subscribe` or `psubscribe`, the passed block will be cal It's possible to subscribe to the same channel multiple time and just unsubscribe a single callback using `unsubscribe_proc` or `punsubscribe_proc`. +## Lua scripting + +When loading scripts from a directory with `EventMachine::Hiredis::Client.load_scripts_from`, the scripts loaded undergo very simple preprocessing, replacing any occurrence of an "include directive" literally with the contents of the referenced file before sending the script to redis. + +``` +-- #include file/name.lua +``` + +The filename is expressed relative to the directory of scripts being loaded. + +### Recommendations for library code + +The implementation is extremely simple, so some sensible recommendations are: + +- Put library code in a subdirectory, or using an extension other than `.lua`, to prevent library scripts being loaded as their own commands. +- Declare only "classes" into the top level scope in library code, and preferably only one per script with the classname matching the script name, to minimise possible naming collisions +- Attach free function implementations to members of a declared class as a namespace. +- Remember while developing that line numbers reported back from redis will be offset + ## Developing Hacking on em-hiredis is pretty simple, make sure you have Bundler installed: From 746da437e8dc0fc2215b6b8818289890b17a7ece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Ledwo=C5=84?= Date: Wed, 10 Dec 2014 18:32:02 +0100 Subject: [PATCH 08/44] Consider connected only after selecting database --- lib/em-hiredis/base_client.rb | 68 +++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/lib/em-hiredis/base_client.rb b/lib/em-hiredis/base_client.rb index 4f2fea5..4cca340 100644 --- a/lib/em-hiredis/base_client.rb +++ b/lib/em-hiredis/base_client.rb @@ -76,28 +76,16 @@ def connect @connection.on(:closed, &method(:handle_disconnect)) @connection.on(:connected) do - @connected = true - @reconnect_failed_count = 0 - @failed = false - - select(@db) unless @db == 0 - auth(@password) if @password - - @command_queue.each do |df, command, args| - @connection.send_command(command, args) - @defs.push(df) - end - @command_queue = [] - - schedule_inactivity_checks - - emit(:connected) - EM::Hiredis.logger.info("#{@connection} Connected") - succeed - - if @reconnecting - @reconnecting = false - emit(:reconnected) + if @db != 0 + df = EM::DefaultDeferrable.new + send_command(df, :select, @db) + df.callback { + handle_connected + }.errback { + handle_disconnect + } + else + handle_connected end end @@ -175,14 +163,18 @@ def configure_inactivity_check(trigger_secs, response_timeout) private + def send_command(df, sym, *args) + @connection.send_command(sym, args) + @defs.push(df) + end + def method_missing(sym, *args) deferred = EM::DefaultDeferrable.new # Shortcut for defining the callback case with just a block deferred.callback { |result| yield(result) } if block_given? if @connected - @connection.send_command(sym, args) - @defs.push(deferred) + send_command(deferred, sym, *args) elsif @failed deferred.fail(Error.new("Redis connection in failed state")) else @@ -237,6 +229,30 @@ def handle_reply(reply) end end + def handle_connected + @connected = true + @reconnect_failed_count = 0 + @failed = false + + auth(@password) if @password + + @command_queue.each do |df, command, args| + send_command(df, command, *args) + end + @command_queue = [] + + schedule_inactivity_checks + + emit(:connected) + EM::Hiredis.logger.info("#{@connection} Connected") + succeed + + if @reconnecting + @reconnecting = false + emit(:reconnected) + end + end + def handle_disconnect cancel_inactivity_checks if @connected @@ -246,7 +262,7 @@ def handle_disconnect @connected = false if @auto_reconnect # Next tick avoids reconnecting after for example EM.stop - EM.next_tick { reconnect } + EM.next_tick { reconnect! } end emit(:disconnected) EM::Hiredis.logger.info("#{@connection} Disconnected") @@ -255,7 +271,7 @@ def handle_disconnect @reconnect_failed_count += 1 @reconnect_timer = EM.add_timer(EM::Hiredis.reconnect_timeout) { @reconnect_timer = nil - reconnect + reconnect! } emit(:reconnect_failed, @reconnect_failed_count) EM::Hiredis.logger.info("#{@connection} Reconnect failed") From 39c66094c2314eedda0aa7e91c0e32b380eca184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Ledwo=C5=84?= Date: Thu, 11 Dec 2014 16:37:06 +0100 Subject: [PATCH 09/44] Fix specs broken by last commit --- spec/base_client_spec.rb | 8 +++++--- spec/live_redis_protocol_spec.rb | 13 +++++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/spec/base_client_spec.rb b/spec/base_client_spec.rb index 617a6ad..f2559fe 100644 --- a/spec/base_client_spec.rb +++ b/spec/base_client_spec.rb @@ -42,10 +42,12 @@ it "should emit disconnected when the connection closes" do connect do |redis| - redis.on(:disconnected) { - done + redis.on(:connected) { + redis.on(:disconnected) { + done + } + redis.close_connection } - redis.close_connection end end diff --git a/spec/live_redis_protocol_spec.rb b/spec/live_redis_protocol_spec.rb index a940c92..c366817 100644 --- a/spec/live_redis_protocol_spec.rb +++ b/spec/live_redis_protocol_spec.rb @@ -515,12 +515,13 @@ def set(&blk) it "should fail deferred commands" do errored = false connect do |redis| - op = redis.blpop 'empty_list' - op.callback { fail } - op.errback { EM.stop } - - redis.close_connection - + redis.on(:connected) { + op = redis.blpop 'empty_list' + op.callback { fail } + op.errback { EM.stop } + + redis.close_connection + } EM.add_timer(1) { fail } end end From ca7497803b459a93ac1b470c5f99c53a2c844e2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Ledwon=CC=81?= Date: Thu, 11 Dec 2014 17:12:46 +0100 Subject: [PATCH 10/44] Add a spec for connecting while redis is busy --- spec/live_redis_protocol_spec.rb | 44 ++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/spec/live_redis_protocol_spec.rb b/spec/live_redis_protocol_spec.rb index c366817..c1e2f6d 100644 --- a/spec/live_redis_protocol_spec.rb +++ b/spec/live_redis_protocol_spec.rb @@ -526,3 +526,47 @@ def set(&blk) end end end + +describe EventMachine::Hiredis, "when redis is blocked by a lua script" do + it "should select the correct db" do + script = <<-EOF + local t0 = tonumber(redis.call("time")[1]) + while tonumber(redis.call("time")[1]) < t0 + 1 do + local a = 1 + end + EOF + + # set reconnect timeout to a higher value to avoid too many reconnections + reconnect_timeout = EM::Hiredis.reconnect_timeout + EM::Hiredis.reconnect_timeout = 0.5 + + connect(9) do |redis1| + timeout(2) + + redis1.config("get", "lua-time-limit").callback { |original_limit| + redis1.config("set", "lua-time-limit", 500).callback { + redis1.eval(script, 0) # run the script, it should take a second + EM.add_timer(0.1) { # wait for the script to start running + connect(9) do |redis2| + redis2.set("test", "545").callback { + redis2.select(0) + redis2.get("test").callback { |test_value0| + test_value0.should be_nil + } + redis2.select(9) + redis2.get("test").callback { |test_value9| + test_value9.should == "545" + EM::Hiredis.reconnect_timeout = reconnect_timeout + redis1.config("set", "lua-time-limit", original_limit) + done + } + }.errback { |e| + fail e + } + end + } + } + } + end + end +end From 988a5ab1f4df408714ab28d4dbfd4b967e62b236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Ledwo=C5=84?= Date: Fri, 12 Dec 2014 12:22:56 +0100 Subject: [PATCH 11/44] Improve recently updated live protocol specs --- spec/live_redis_protocol_spec.rb | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/spec/live_redis_protocol_spec.rb b/spec/live_redis_protocol_spec.rb index c1e2f6d..6fb6124 100644 --- a/spec/live_redis_protocol_spec.rb +++ b/spec/live_redis_protocol_spec.rb @@ -518,7 +518,7 @@ def set(&blk) redis.on(:connected) { op = redis.blpop 'empty_list' op.callback { fail } - op.errback { EM.stop } + op.errback { done } redis.close_connection } @@ -530,10 +530,14 @@ def set(&blk) describe EventMachine::Hiredis, "when redis is blocked by a lua script" do it "should select the correct db" do script = <<-EOF - local t0 = tonumber(redis.call("time")[1]) - while tonumber(redis.call("time")[1]) < t0 + 1 do - local a = 1 - end + local to_micro = function(t) + return tonumber(t[1])*1000000 + tonumber(t[2]) + end + local t0 = to_micro(redis.call("time")) + local tnow = t0 + repeat + tnow = to_micro(redis.call("time")) + until tnow - t0 >= 1000000 EOF # set reconnect timeout to a higher value to avoid too many reconnections From 46ab9dade6ff2f6b608e535c0e23cf90d376fcd1 Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Tue, 3 Feb 2015 13:35:02 +0000 Subject: [PATCH 12/44] Expose LUA script text and sha --- lib/em-hiredis/client.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/em-hiredis/client.rb b/lib/em-hiredis/client.rb index 89a4caa..164d9b1 100644 --- a/lib/em-hiredis/client.rb +++ b/lib/em-hiredis/client.rb @@ -33,6 +33,12 @@ def self.register_script(name, lua) self.send(:define_method, name.to_sym) { |keys, args=[]| eval_script(lua, sha, keys, args) } + self.send(:define_method, "#{name}_script".to_sym) { + lua + } + self.send(:define_method, "#{name}_sha".to_sym) { + sha + } end def register_script(name, lua) From 6c5ee84bab08c94bb2e40f15af382461fd57661e Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Tue, 3 Feb 2015 13:57:42 +0000 Subject: [PATCH 13/44] Add a method to ensure a script is loaded --- lib/em-hiredis/client.rb | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lib/em-hiredis/client.rb b/lib/em-hiredis/client.rb index 164d9b1..a47680a 100644 --- a/lib/em-hiredis/client.rb +++ b/lib/em-hiredis/client.rb @@ -64,6 +64,33 @@ def eval_script(lua, lua_sha, keys, args) df end + def ensure_script(script_name) + df = EM::DefaultDeferrable.new + method_missing( + :script, + 'exists', + self.send("#{script_name}_sha".to_sym) + ).callback { |ret| + # ret is an array of 0 or 1s representing existence for each script arg passed + if ret[0] == 0 + method_missing( + :script, + 'load', + self.send("#{script_name}_script".to_sym) + ).callback { + df.succeed + }.errback { |e| + df.fail(e) + } + else + df.succeed + end + }.errback { |e| + df.fail(e) + } + df + end + def monitor(&blk) @monitoring = true method_missing(:monitor, &blk) From ccda64c30214877f2dcd14201899b47c63cc2553 Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Tue, 17 Feb 2015 10:35:37 +0000 Subject: [PATCH 14/44] Take the optimism out of the tests If an assertion is placed in a callback, but the errback is invoked, the assertion never happens, the test should fail, but it passes. Attach a lot of errbacks. Some broken tests are revealed. --- spec/redis_commands_spec.rb | 820 ++++++++++++++++++------------------ 1 file changed, 410 insertions(+), 410 deletions(-) diff --git a/spec/redis_commands_spec.rb b/spec/redis_commands_spec.rb index 31e48ad..7ec8c3d 100644 --- a/spec/redis_commands_spec.rb +++ b/spec/redis_commands_spec.rb @@ -3,21 +3,21 @@ describe EventMachine::Hiredis, "commands" do it "pings" do connect do |redis| - redis.ping { |r| r.should == 'PONG'; done } + redis.ping.callback { |r| r.should == 'PONG'; done } end end it "SETs and GETs a key" do connect do |redis| - redis.set('foo', 'nik') - redis.get('foo') { |r| r.should == 'nik'; done } + redis.set('foo', 'nik').errback { fail } + redis.get('foo').callback { |r| r.should == 'nik'; done } end end it "handles trailing newline characters" do connect do |redis| - redis.set('foo', "bar\n") - redis.get('foo') { |r| r.should == "bar\n"; done } + redis.set('foo', "bar\n").errback { fail } + redis.get('foo').callback { |r| r.should == "bar\n"; done } end end @@ -28,10 +28,10 @@ if RUBY_VERSION > "1.9" string.force_encoding("UTF-8") end - redis.set('foo', string) - redis.get('foo') { |r| r.should == string } + redis.set('foo', string).errback { fail } + redis.get('foo').callback { |r| r.should == string }.errback { fail } end - redis.ping { done } + redis.ping.callback { done } end end @@ -39,111 +39,111 @@ connect do |redis| timeout(3) - redis.setex('foo', 1, 'bar') - redis.get('foo') { |r| r.should == 'bar' } + redis.setex('foo', 1, 'bar').errback { fail } + redis.get('foo').callback { |r| r.should == 'bar' }.errback { fail } EventMachine.add_timer(2) do - redis.get('foo') { |r| r.should == nil } - redis.ping { done } + redis.get('foo').callback { |r| r.should == nil }.errback { fail } + redis.ping.callback { done } end end end it "gets TTL for a key" do connect do |redis| - redis.setex('foo', 1, 'bar') - redis.ttl('foo') { |r| r.should == 1; done } + redis.setex('foo', 1, 'bar').errback { fail } + redis.ttl('foo').callback { |r| r.should == 1; done } end end it "can SETNX" do connect do |redis| - redis.set('foo', 'nik') - redis.get('foo') { |r| r.should == 'nik' } - redis.setnx 'foo', 'bar' - redis.get('foo') { |r| r.should == 'nik' } + redis.set('foo', 'nik').errback { fail } + redis.get('foo').callback { |r| r.should == 'nik' }.errback { fail } + redis.setnx('foo', 'bar').errback { fail } + redis.get('foo').callback { |r| r.should == 'nik' }.errback { fail } - redis.ping { done } + redis.ping.callback { done } end end it "can GETSET" do connect do |redis| - redis.set('foo', 'bar') - redis.getset('foo', 'baz') { |r| r.should == 'bar' } - redis.get('foo') { |r| r.should == 'baz'; done } + redis.set('foo', 'bar').errback { fail } + redis.getset('foo', 'baz').callback { |r| r.should == 'bar' }.errback { fail } + redis.get('foo').callback { |r| r.should == 'baz'; done } end end it "can INCR a key" do connect do |redis| - redis.del('counter') - redis.incr('counter') { |r| r.should == 1 } - redis.incr('counter') { |r| r.should == 2 } - redis.incr('counter') { |r| r.should == 3 } + redis.del('counter').errback { fail } + redis.incr('counter').callback { |r| r.should == 1 }.errback { fail } + redis.incr('counter').callback { |r| r.should == 2 }.errback { fail } + redis.incr('counter').callback { |r| r.should == 3 }.errback { fail } - redis.ping { done } + redis.ping.callback { done } end end it "can INCRBY a key" do connect do |redis| - redis.del('counter') - redis.incrby('counter', 1) { |r| r.should == 1 } - redis.incrby('counter', 2) { |r| r.should == 3 } - redis.incrby('counter', 3) { |r| r.should == 6 } + redis.del('counter').errback { fail } + redis.incrby('counter', 1).callback { |r| r.should == 1 }.errback { fail } + redis.incrby('counter', 2).callback { |r| r.should == 3 }.errback { fail } + redis.incrby('counter', 3).callback { |r| r.should == 6 }.errback { fail } - redis.ping { done } + redis.ping.callback { done } end end it "can DECR a key" do connect do |redis| - redis.del('counter') - redis.incr('counter') { |r| r.should == 1 } - redis.incr('counter') { |r| r.should == 2 } - redis.incr('counter') { |r| r.should == 3 } - redis.decr('counter') { |r| r.should == 2 } - redis.decrby('counter', 2) { |r| r.should == 0; done } + redis.del('counter').errback { fail } + redis.incr('counter').callback { |r| r.should == 1 }.errback { fail } + redis.incr('counter').callback { |r| r.should == 2 }.errback { fail } + redis.incr('counter').callback { |r| r.should == 3 }.errback { fail } + redis.decr('counter').callback { |r| r.should == 2 }.errback { fail } + redis.decrby('counter', 2).callback { |r| r.should == 0; done } end end it "can RANDOMKEY" do connect do |redis| - redis.set('foo', 'bar') - redis.randomkey { |r| r.should_not == nil; done } + redis.set('foo', 'bar').errback { fail } + redis.randomkey.callback { |r| r.should_not == nil; done } end end it "can RENAME a key" do connect do |redis| - redis.del 'foo' - redis.del 'bar' - redis.set('foo', 'hi') - redis.rename 'foo', 'bar' - redis.get('bar') { |r| r.should == 'hi' ; done } + redis.del('foo').errback { fail } + redis.del('bar').errback { fail } + redis.set('foo', 'hi').errback { fail } + redis.rename('foo', 'bar').errback { fail } + redis.get('bar').callback { |r| r.should == 'hi' ; done } end end it "can RENAMENX a key" do connect do |redis| - redis.del 'foo' - redis.del 'bar' - redis.set('foo', 'hi') - redis.set('bar', 'ohai') - redis.renamenx 'foo', 'bar' - redis.get('bar') { |r| r.should == 'ohai' ; done } + redis.del('foo').errback { fail } + redis.del('bar').errback { fail } + redis.set('foo', 'hi').errback { fail } + redis.set('bar', 'ohai').errback { fail } + redis.renamenx('foo', 'bar').errback { fail } + redis.get('bar').callback { |r| r.should == 'ohai' ; done } end end it "can get DBSIZE of the database" do connect do |redis| - redis.set('foo1', 'bar') - redis.set('foo2', 'baz') - redis.set('foo3', 'bat') - redis.dbsize do |r| + redis.set('foo1', 'bar').errback { fail } + redis.set('foo2', 'baz').errback { fail } + redis.set('foo3', 'bat').errback { fail } + redis.dbsize.callback { |r| r.should == 3 done - end + } end end @@ -151,12 +151,12 @@ connect do |redis| timeout(3) - redis.set('foo', 'bar') - redis.expire 'foo', 1 - redis.get('foo') { |r| r.should == "bar" } + redis.set('foo', 'bar').errback { fail } + redis.expire('foo', 1).errback { fail } + redis.get('foo').callback { |r| r.should == "bar" }.errback { fail } EventMachine.add_timer(2) do - redis.get('foo') { |r| r.should == nil } - redis.ping { done } + redis.get('foo').callback { |r| r.should == nil }.errback { fail } + redis.ping.callback { done } end end end @@ -164,603 +164,603 @@ it "can check if a key EXISTS" do connect do |redis| - redis.set 'foo', 'nik' - redis.exists('foo') { |r| r.should == 1 } - redis.del 'foo' - redis.exists('foo') { |r| r.should == 0 ; done } + redis.set('foo', 'nik').errback { fail } + redis.exists('foo').callback { |r| r.should == 1 }.errback { fail } + redis.del('foo').errback { fail } + redis.exists('foo').callback { |r| r.should == 0 ; done } end end it "can list KEYS" do connect do |redis| - redis.keys("f*") { |keys| keys.each { |key| @r.del key } } - redis.set('f', 'nik') - redis.set('fo', 'nak') - redis.set('foo', 'qux') - redis.keys("f*") { |r| r.sort.should == ['f', 'fo', 'foo'].sort } + redis.keys("f*") { |keys| keys.each { |key| @r.del key } }.errback { fail } + redis.set('f', 'nik').errback { fail } + redis.set('fo', 'nak').errback { fail } + redis.set('foo', 'qux').errback { fail } + redis.keys("f*").callback { |r| r.sort.should == ['f', 'fo', 'foo'].sort }.errback { fail } - redis.ping { done } + redis.ping.callback { done } end end it "returns a random key (RANDOMKEY)" do connect do |redis| - redis.set("foo", "bar") - redis.randomkey do |r| - redis.exists(r) do |e| + redis.set("foo", "bar").errback { fail } + redis.randomkey { |r| + redis.exists(r) { |e| e.should == 1 done - end - end + } + }.errback { fail } end end it "should be able to check the TYPE of a key" do connect do |redis| - redis.set('foo', 'nik') - redis.type('foo') { |r| r.should == "string" } - redis.del 'foo' - redis.type('foo') { |r| r.should == "none" ; done } + redis.set('foo', 'nik').errback { fail } + redis.type('foo').callback { |r| r.should == "string" }.errback { fail } + redis.del('foo').errback { fail } + redis.type('foo').callback { |r| r.should == "none" ; done } end end it "pushes to the head of a list (LPUSH)" do connect do |redis| - redis.lpush "list", 'hello' - redis.lpush "list", 42 - redis.type('list') { |r| r.should == "list" } - redis.llen('list') { |r| r.should == 2 } - redis.lpop('list') { |r| r.should == '42'; done } + redis.lpush("list", 'hello').errback { fail } + redis.lpush("list", 42).errback { fail } + redis.type('list').callback { |r| r.should == "list" }.errback { fail } + redis.llen('list').callback { |r| r.should == 2 }.errback { fail } + redis.lpop('list').callback { |r| r.should == '42'; done } end end it "pushes to the tail of a list (RPUSH)" do connect do |redis| - redis.rpush "list", 'hello' - redis.type('list') { |r| r.should == "list" } - redis.llen('list') { |r| r.should == 1 ; done } + redis.rpush("list", 'hello').errback { fail } + redis.type('list').callback { |r| r.should == "list" }.errback { fail } + redis.llen('list').callback { |r| r.should == 1 ; done } end end it "pops the tail of a list (RPOP)" do connect do |redis| - redis.rpush "list", 'hello' - redis.rpush"list", 'goodbye' - redis.type('list') { |r| r.should == "list" } - redis.llen('list') { |r| r.should == 2 } - redis.rpop('list') { |r| r.should == 'goodbye'; done } + redis.rpush("list", 'hello').errback { fail } + redis.rpush("list", 'goodbye').errback { fail } + redis.type('list').callback { |r| r.should == "list" }.errback { fail } + redis.llen('list').callback { |r| r.should == 2 }.errback { fail } + redis.rpop('list').callback { |r| r.should == 'goodbye'; done } end end it "pop the head of a list (LPOP)" do connect do |redis| - redis.rpush "list", 'hello' - redis.rpush "list", 'goodbye' - redis.type('list') { |r| r.should == "list" } - redis.llen('list') { |r| r.should == 2 } - redis.lpop('list') { |r| r.should == 'hello'; done } + redis.rpush("list", 'hello').errback { fail } + redis.rpush("list", 'goodbye').errback { fail } + redis.type('list').callback { |r| r.should == "list" }.errback { fail } + redis.llen('list').callback { |r| r.should == 2 }.errback { fail } + redis.lpop('list').callback { |r| r.should == 'hello'; done } end end it "gets the length of a list (LLEN)" do connect do |redis| - redis.rpush "list", 'hello' - redis.rpush "list", 'goodbye' - redis.type('list') { |r| r.should == "list" } - redis.llen('list') { |r| r.should == 2 ; done } + redis.rpush("list", 'hello').errback { fail } + redis.rpush("list", 'goodbye').errback { fail } + redis.type('list').callback { |r| r.should == "list" }.errback { fail } + redis.llen('list').callback { |r| r.should == 2 ; done } end end it "gets a range of values from a list (LRANGE)" do connect do |redis| - redis.rpush "list", 'hello' - redis.rpush "list", 'goodbye' - redis.rpush "list", '1' - redis.rpush "list", '2' - redis.rpush "list", '3' - redis.type('list') { |r| r.should == "list" } - redis.llen('list') { |r| r.should == 5 } - redis.lrange('list', 2, -1) { |r| r.should == ['1', '2', '3']; done } + redis.rpush("list", 'hello').errback { fail } + redis.rpush("list", 'goodbye').errback { fail } + redis.rpush("list", '1').errback { fail } + redis.rpush("list", '2').errback { fail } + redis.rpush("list", '3').errback { fail } + redis.type('list').callback { |r| r.should == "list" }.errback { fail } + redis.llen('list').callback { |r| r.should == 5 }.errback { fail } + redis.lrange('list', 2, -1).callback { |r| r.should == ['1', '2', '3']; done } end end it "trims a list (LTRIM)" do connect do |redis| - redis.rpush "list", 'hello' - redis.rpush "list", 'goodbye' - redis.rpush "list", '1' - redis.rpush "list", '2' - redis.rpush "list", '3' - redis.type('list') { |r| r.should == "list" } - redis.llen('list') { |r| r.should == 5 } - redis.ltrim 'list', 0, 1 - redis.llen('list') { |r| r.should == 2 } - redis.lrange('list', 0, -1) { |r| r.should == ['hello', 'goodbye']; done } + redis.rpush("list", 'hello').errback { fail } + redis.rpush("list", 'goodbye').errback { fail } + redis.rpush("list", '1').errback { fail } + redis.rpush("list", '2').errback { fail } + redis.rpush("list", '3').errback { fail } + redis.type('list').callback { |r| r.should == "list" }.errback { fail } + redis.llen('list').callback { |r| r.should == 5 }.errback { fail } + redis.ltrim('list', 0, 1).errback { fail } + redis.llen('list').callback { |r| r.should == 2 }.errback { fail } + redis.lrange('list', 0, -1).callback { |r| r.should == ['hello', 'goodbye']; done } end end it "gets a value by indexing into a list (LINDEX)" do connect do |redis| - redis.rpush "list", 'hello' - redis.rpush "list", 'goodbye' - redis.type('list') { |r| r.should == "list" } - redis.llen('list') { |r| r.should == 2 } - redis.lindex('list', 1) { |r| r.should == 'goodbye'; done } + redis.rpush("list", 'hello').errback { fail } + redis.rpush("list", 'goodbye').errback { fail } + redis.type('list').callback { |r| r.should == "list" }.errback { fail } + redis.llen('list').callback { |r| r.should == 2 }.errback { fail } + redis.lindex('list', 1).callback { |r| r.should == 'goodbye'; done } end end it "sets a value by indexing into a list (LSET)" do connect do |redis| - redis.rpush "list", 'hello' - redis.rpush "list", 'hello' - redis.type('list') { |r| r.should == "list" } - redis.llen('list') { |r| r.should == 2 } - redis.lset('list', 1, 'goodbye') { |r| r.should == 'OK' } - redis.lindex('list', 1) { |r| r.should == 'goodbye'; done } + redis.rpush("list", 'hello').errback { fail } + redis.rpush("list", 'hello').errback { fail } + redis.type('list').callback { |r| r.should == "list" }.errback { fail } + redis.llen('list').callback { |r| r.should == 2 }.errback { fail } + redis.lset('list', 1, 'goodbye').callback { |r| r.should == 'OK' }.errback { fail } + redis.lindex('list', 1).callback { |r| r.should == 'goodbye'; done } end end it "removes values from a list (LREM)" do connect do |redis| - redis.rpush "list", 'hello' - redis.rpush "list", 'goodbye' - redis.type('list') { |r| r.should == "list" } - redis.llen('list') { |r| r.should == 2 } - redis.lrem('list', 1, 'hello') { |r| r.should == 1 } - redis.lrange('list', 0, -1) { |r| r.should == ['goodbye']; done } + redis.rpush("list", 'hello').errback { fail } + redis.rpush("list", 'goodbye').errback { fail } + redis.type('list').callback { |r| r.should == "list" }.errback { fail } + redis.llen('list').callback { |r| r.should == 2 }.errback { fail } + redis.lrem('list', 1, 'hello').callback { |r| r.should == 1 }.errback { fail } + redis.lrange('list', 0, -1).callback { |r| r.should == ['goodbye']; done } end end it "pops values from a list and push them onto a temp list(RPOPLPUSH)" do connect do |redis| - redis.rpush "list", 'one' - redis.rpush "list", 'two' - redis.rpush "list", 'three' - redis.type('list') { |r| r.should == "list" } - redis.llen('list') { |r| r.should == 3 } - redis.lrange('list', 0, -1) { |r| r.should == ['one', 'two', 'three'] } - redis.lrange('tmp', 0, -1) { |r| r.should == [] } - redis.rpoplpush('list', 'tmp') { |r| r.should == 'three' } - redis.lrange('tmp', 0, -1) { |r| r.should == ['three'] } - redis.rpoplpush('list', 'tmp') { |r| r.should == 'two' } - redis.lrange('tmp', 0, -1) { |r| r.should == ['two', 'three'] } - redis.rpoplpush('list', 'tmp') { |r| r.should == 'one' } - redis.lrange('tmp', 0, -1) { |r| r.should == ['one', 'two', 'three']; done } + redis.rpush("list", 'one').errback { fail } + redis.rpush("list", 'two').errback { fail } + redis.rpush("list", 'three').errback { fail } + redis.type('list').callback { |r| r.should == "list" }.errback { fail } + redis.llen('list').callback { |r| r.should == 3 }.errback { fail } + redis.lrange('list', 0, -1).callback { |r| r.should == ['one', 'two', 'three'] }.errback { fail } + redis.lrange('tmp', 0, -1).callback { |r| r.should == [] }.errback { fail } + redis.rpoplpush('list', 'tmp').callback { |r| r.should == 'three' }.errback { fail } + redis.lrange('tmp', 0, -1).callback { |r| r.should == ['three'] }.errback { fail } + redis.rpoplpush('list', 'tmp').callback { |r| r.should == 'two' }.errback { fail } + redis.lrange('tmp', 0, -1).callback { |r| r.should == ['two', 'three'] }.errback { fail } + redis.rpoplpush('list', 'tmp').callback { |r| r.should == 'one' }.errback { fail } + redis.lrange('tmp', 0, -1).callback { |r| r.should == ['one', 'two', 'three']; done } end end it "adds members to a set (SADD)" do connect do |redis| - redis.sadd "set", 'key1' - redis.sadd "set", 'key2' - redis.type('set') { |r| r.should == "set" } - redis.scard('set') { |r| r.should == 2 } - redis.smembers('set') { |r| r.sort.should == ['key1', 'key2'].sort; done } + redis.sadd("set", 'key1').errback { fail } + redis.sadd("set", 'key2').errback { fail } + redis.type('set').callback { |r| r.should == "set" }.errback { fail } + redis.scard('set').callback { |r| r.should == 2 }.errback { fail } + redis.smembers('set').callback { |r| r.sort.should == ['key1', 'key2'].sort; done } end end it "deletes members to a set (SREM)" do connect do |redis| - redis.sadd "set", 'key1' - redis.sadd "set", 'key2' - redis.type('set') { |r| r.should == "set" } - redis.scard('set') { |r| r.should == 2 } - redis.smembers('set') { |r| r.sort.should == ['key1', 'key2'].sort } - redis.srem('set', 'key1') - redis.scard('set') { |r| r.should == 1 } - redis.smembers('set') { |r| r.should == ['key2']; done } + redis.sadd("set", 'key1').errback { fail } + redis.sadd("set", 'key2').errback { fail } + redis.type('set').callback { |r| r.should == "set" }.errback { fail } + redis.scard('set').callback { |r| r.should == 2 }.errback { fail } + redis.smembers('set').callback { |r| r.sort.should == ['key1', 'key2'].sort }.errback { fail } + redis.srem('set', 'key1').errback { fail } + redis.scard('set').callback { |r| r.should == 1 }.errback { fail } + redis.smembers('set').callback { |r| r.should == ['key2']; done } end end it "returns and remove random key from set (SPOP)" do connect do |redis| - redis.sadd "set_pop", "key1" - redis.sadd "set_pop", "key2" - redis.spop("set_pop") { |r| r.should_not == nil } - redis.scard("set_pop") { |r| r.should == 1; done } + redis.sadd("set_pop", "key1").errback { fail } + redis.sadd("set_pop", "key2").errback { fail } + redis.spop("set_pop").callback { |r| r.should_not == nil }.errback { fail } + redis.scard("set_pop").callback { |r| r.should == 1; done } end end it "returns random key without delete the key from a set (SRANDMEMBER)" do connect do |redis| - redis.sadd "set_srandmember", "key1" - redis.sadd "set_srandmember", "key2" - redis.srandmember("set_srandmember") { |r| r.should_not == nil } - redis.scard("set_srandmember") { |r| r.should == 2; done } + redis.sadd("set_srandmember", "key1").errback { fail } + redis.sadd("set_srandmember", "key2").errback { fail } + redis.srandmember("set_srandmember").callback { |r| r.should_not == nil }.errback { fail } + redis.scard("set_srandmember").callback { |r| r.should == 2; done } end end it "counts the members of a set (SCARD)" do connect do |redis| - redis.sadd "set", 'key1' - redis.sadd "set", 'key2' - redis.type('set') { |r| r.should == "set" } - redis.scard('set') { |r| r.should == 2; done } + redis.sadd("set", 'key1').errback { fail } + redis.sadd("set", 'key2').errback { fail } + redis.type('set').callback { |r| r.should == "set" }.errback { fail } + redis.scard('set').callback { |r| r.should == 2; done } end end it "tests for set membership (SISMEMBER)" do connect do |redis| - redis.sadd "set", 'key1' - redis.sadd "set", 'key2' - redis.type('set') { |r| r.should == "set" } - redis.scard('set') { |r| r.should == 2 } - redis.sismember('set', 'key1') { |r| r.should == 1 } - redis.sismember('set', 'key2') { |r| r.should == 1 } - redis.sismember('set', 'notthere') { |r| r.should == 0; done } + redis.sadd("set", 'key1').errback { fail } + redis.sadd("set", 'key2').errback { fail } + redis.type('set').callback { |r| r.should == "set" }.errback { fail } + redis.scard('set').callback { |r| r.should == 2 }.errback { fail } + redis.sismember('set', 'key1').callback { |r| r.should == 1 }.errback { fail } + redis.sismember('set', 'key2').callback { |r| r.should == 1 }.errback { fail } + redis.sismember('set', 'notthere').callback { |r| r.should == 0; done } end end it "intersects sets (SINTER)" do connect do |redis| - redis.sadd "set", 'key1' - redis.sadd "set", 'key2' - redis.sadd "set2", 'key2' - redis.sinter('set', 'set2') { |r| r.should == ['key2']; done } + redis.sadd("set", 'key1').errback { fail } + redis.sadd("set", 'key2').errback { fail } + redis.sadd("set2", 'key2').errback { fail } + redis.sinter('set', 'set2').callback { |r| r.should == ['key2']; done } end end it "intersects set and stores the results in a key (SINTERSTORE)" do connect do |redis| - redis.sadd "set", 'key1' - redis.sadd "set", 'key2' - redis.sadd "set2", 'key2' - redis.sinterstore('newone', 'set', 'set2') { |r| r.should == 1 } - redis.smembers('newone') { |r| r.should == ['key2']; done } + redis.sadd("set", 'key1').errback { fail } + redis.sadd("set", 'key2').errback { fail } + redis.sadd("set2", 'key2').errback { fail } + redis.sinterstore('newone', 'set', 'set2').callback { |r| r.should == 1 }.errback { fail } + redis.smembers('newone').callback { |r| r.should == ['key2']; done } end end it "performs set unions (SUNION)" do connect do |redis| - redis.sadd "set", 'key1' - redis.sadd "set", 'key2' - redis.sadd "set2", 'key2' - redis.sadd "set2", 'key3' - redis.sunion('set', 'set2') { |r| r.sort.should == ['key1','key2','key3'].sort; done } + redis.sadd("set", 'key1').errback { fail } + redis.sadd("set", 'key2').errback { fail } + redis.sadd("set2", 'key2').errback { fail } + redis.sadd("set2", 'key3').errback { fail } + redis.sunion('set', 'set2').callback { |r| r.sort.should == ['key1','key2','key3'].sort; done } end end it "performs a set union and store the results in a key (SUNIONSTORE)" do connect do |redis| - redis.sadd "set", 'key1' - redis.sadd "set", 'key2' - redis.sadd "set2", 'key2' - redis.sadd "set2", 'key3' - redis.sunionstore('newone', 'set', 'set2') { |r| r.should == 3 } - redis.smembers('newone') { |r| r.sort.should == ['key1','key2','key3'].sort; done } + redis.sadd("set", 'key1').errback { fail } + redis.sadd("set", 'key2').errback { fail } + redis.sadd("set2", 'key2').errback { fail } + redis.sadd("set2", 'key3').errback { fail } + redis.sunionstore('newone', 'set', 'set2').callback { |r| r.should == 3 }.errback { fail } + redis.smembers('newone').callback { |r| r.sort.should == ['key1','key2','key3'].sort; done } end end it "takes a set difference (SDIFF)" do connect do |redis| - redis.sadd "set", 'a' - redis.sadd "set", 'b' - redis.sadd "set2", 'b' - redis.sadd "set2", 'c' - redis.sdiff('set', 'set2') { |r| r.should == ['a']; done } + redis.sadd("set", 'a').errback { fail } + redis.sadd("set", 'b').errback { fail } + redis.sadd("set2", 'b').errback { fail } + redis.sadd("set2", 'c').errback { fail } + redis.sdiff('set', 'set2').callback { |r| r.should == ['a']; done } end end it "takes set difference and store the results in a key (SDIFFSTORE)" do connect do |redis| - redis.sadd "set", 'a' - redis.sadd "set", 'b' - redis.sadd "set2", 'b' - redis.sadd "set2", 'c' - redis.sdiffstore('newone', 'set', 'set2') - redis.smembers('newone') { |r| r.should == ['a']; done } + redis.sadd("set", 'a').errback { fail } + redis.sadd("set", 'b').errback { fail } + redis.sadd("set2", 'b').errback { fail } + redis.sadd("set2", 'c').errback { fail } + redis.sdiffstore('newone', 'set', 'set2').errback { fail } + redis.smembers('newone').callback { |r| r.should == ['a']; done } end end it "moves elements from one set to another (SMOVE)" do connect do |redis| - redis.sadd 'set1', 'a' - redis.sadd 'set1', 'b' - redis.sadd 'set2', 'x' - redis.smove('set1', 'set2', 'a') { |r| r.should == 1 } - redis.sismember('set2', 'a') { |r| r.should == 1 } + redis.sadd('set1', 'a').errback { fail } + redis.sadd('set1', 'b').errback { fail } + redis.sadd('set2', 'x').errback { fail } + redis.smove('set1', 'set2', 'a').callback { |r| r.should == 1 }.errback { fail } + redis.sismember('set2', 'a').callback { |r| r.should == 1 }.errback { fail } redis.del('set1') { done } end end it "counts the members of a zset" do connect do |redis| - redis.sadd "set", 'key1' - redis.sadd "set", 'key2' - redis.zadd 'zset', 1, 'set' - redis.zcount('zset') { |r| r.should == 1 } - redis.del('set') + redis.sadd("set", 'key1').errback { fail } + redis.sadd("set", 'key2').errback { fail } + redis.zadd('zset', 1, 'set').errback { fail } + redis.zcount('zset').callback { |r| r.should == 1 }.errback { fail } + redis.del('set').errback { fail } redis.del('zset') { done } end end it "adds members to a zset" do connect do |redis| - redis.sadd "set", 'key1' - redis.sadd "set", 'key2' - redis.zadd 'zset', 1, 'set' - redis.zrange('zset', 0, 1) { |r| r.should == ['set'] } - redis.zcount('zset') { |r| r.should == 1 } - redis.del('set') + redis.sadd("set", 'key1').errback { fail } + redis.sadd("set", 'key2').errback { fail } + redis.zadd('zset', 1, 'set').errback { fail } + redis.zrange('zset', 0, 1).callback { |r| r.should == ['set'] }.errback { fail } + redis.zcount('zset').callback { |r| r.should == 1 }.errback { fail } + redis.del('set').errback { fail } redis.del('zset') { done } end end it "deletes members to a zset" do connect do |redis| - redis.sadd "set", 'key1' - redis.sadd "set", 'key2' - redis.type?('set') { |r| r.should == "set" } - redis.sadd "set2", 'key3' - redis.sadd "set2", 'key4' - redis.type?('set2') { |r| r.should == "set" } - redis.zadd 'zset', 1, 'set' - redis.zcount('zset') { |r| r.should == 1 } - redis.zadd 'zset', 2, 'set2' - redis.zcount('zset') { |r| r.should == 2 } - redis.zset_delete 'zset', 'set' - redis.zcount('zset') { |r| r.should == 1 } - redis.del('set') - redis.del('set2') + redis.sadd("set", 'key1').errback { fail } + redis.sadd("set", 'key2').errback { fail } + redis.type?('set').callback { |r| r.should == "set" }.errback { fail } + redis.sadd("set2", 'key3').errback { fail } + redis.sadd("set2", 'key4').errback { fail } + redis.type?('set2').callback { |r| r.should == "set" }.errback { fail } + redis.zadd('zset', 1, 'set').errback { fail } + redis.zcount('zset').callback { |r| r.should == 1 }.errback { fail } + redis.zadd('zset', 2, 'set2').errback { fail } + redis.zcount('zset').callback { |r| r.should == 2 }.errback { fail } + redis.zset_delete('zset', 'set').errback { fail } + redis.zcount('zset').callback { |r| r.should == 1 }.errback { fail } + redis.del('set').errback { fail } + redis.del('set2').errback { fail } redis.del('zset') { done } end end it "gets a range of values from a zset" do connect do |redis| - redis.sadd "set", 'key1' - redis.sadd "set", 'key2' - redis.sadd "set2", 'key3' - redis.sadd "set2", 'key4' - redis.sadd "set3", 'key1' - redis.type?('set') { |r| r.should == 'set' } - redis.type?('set2') { |r| r.should == 'set' } - redis.type?('set3') { |r| r.should == 'set' } - redis.zadd 'zset', 1, 'set' - redis.zadd 'zset', 2, 'set2' - redis.zadd 'zset', 3, 'set3' - redis.zcount('zset') { |r| r.should == 3 } - redis.zrange('zset', 0, 3) { |r| r.should == ['set', 'set2', 'set3'] } - redis.del('set') - redis.del('set2') - redis.del('set3') + redis.sadd("set", 'key1').errback { fail } + redis.sadd("set", 'key2').errback { fail } + redis.sadd("set2", 'key3').errback { fail } + redis.sadd("set2", 'key4').errback { fail } + redis.sadd("set3", 'key1').errback { fail } + redis.type?('set').callback { |r| r.should == 'set' }.errback { fail } + redis.type?('set2').callback { |r| r.should == 'set' }.errback { fail } + redis.type?('set3').callback { |r| r.should == 'set' }.errback { fail } + redis.zadd('zset', 1, 'set').errback { fail } + redis.zadd('zset', 2, 'set2').errback { fail } + redis.zadd('zset', 3, 'set3').errback { fail } + redis.zcount('zset').callback { |r| r.should == 3 }.errback { fail } + redis.zrange('zset', 0, 3).callback { |r| r.should == ['set', 'set2', 'set3'] }.errback { fail } + redis.del('set').errback { fail } + redis.del('set2').errback { fail } + redis.del('set3').errback { fail } redis.del('zset') { done } end end it "gets a reverse range of values from a zset" do connect do |redis| - redis.sadd "set", 'key1' - redis.sadd "set", 'key2' - redis.sadd "set2", 'key3' - redis.sadd "set2", 'key4' - redis.sadd "set3", 'key1' - redis.type?('set') { |r| r.should == 'set' } - redis.type?('set2') { |r| r.should == 'set' } - redis.type?('set3') { |r| r.should == 'set' } - redis.zadd 'zset', 1, 'set' - redis.zadd 'zset', 2, 'set2' - redis.zadd 'zset', 3, 'set3' - redis.zcount('zset') { |r| r.should == 3 } - redis.zrevrange('zset', 0, 3) { |r| r.should == ['set3', 'set2', 'set'] } - redis.del('set') - redis.del('set2') - redis.del('set3') + redis.sadd("set", 'key1').errback { fail } + redis.sadd("set", 'key2').errback { fail } + redis.sadd("set2", 'key3').errback { fail } + redis.sadd("set2", 'key4').errback { fail } + redis.sadd("set3", 'key1').errback { fail } + redis.type?('set').callback { |r| r.should == 'set' }.errback { fail } + redis.type?('set2').callback { |r| r.should == 'set' }.errback { fail } + redis.type?('set3').callback { |r| r.should == 'set' }.errback { fail } + redis.zadd('zset', 1, 'set').errback { fail } + redis.zadd('zset', 2, 'set2').errback { fail } + redis.zadd('zset', 3, 'set3').errback { fail } + redis.zcount('zset').callback { |r| r.should == 3 }.errback { fail } + redis.zrevrange('zset', 0, 3).callback { |r| r.should == ['set3', 'set2', 'set'] }.errback { fail } + redis.del('set').errback { fail } + redis.del('set2').errback { fail } + redis.del('set3').errback { fail } redis.del('zset') { done } end end it "gets a range by score of values from a zset" do connect do |redis| - redis.sadd "set", 'key1' - redis.sadd "set", 'key2' - redis.sadd "set2", 'key3' - redis.sadd "set2", 'key4' - redis.sadd "set3", 'key1' - redis.sadd "set4", 'key4' - redis.zadd 'zset', 1, 'set' - redis.zadd 'zset', 2, 'set2' - redis.zadd 'zset', 3, 'set3' - redis.zadd 'zset', 4, 'set4' - redis.zcount('zset') { |r| r.should == 4 } - redis.zrangebyscore('zset', 2, 3) { |r| r.should == ['set2', 'set3'] } - redis.del('set') - redis.del('set2') - redis.del('set3') - redis.del('set4') + redis.sadd("set", 'key1').errback { fail } + redis.sadd("set", 'key2').errback { fail } + redis.sadd("set2", 'key3').errback { fail } + redis.sadd("set2", 'key4').errback { fail } + redis.sadd("set3", 'key1').errback { fail } + redis.sadd("set4", 'key4').errback { fail } + redis.zadd('zset', 1, 'set').errback { fail } + redis.zadd('zset', 2, 'set2').errback { fail } + redis.zadd('zset', 3, 'set3').errback { fail } + redis.zadd('zset', 4, 'set4').errback { fail } + redis.zcount('zset').callback { |r| r.should == 4 }.errback { fail } + redis.zrangebyscore('zset', 2, 3).callback { |r| r.should == ['set2', 'set3'] }.errback { fail } + redis.del('set').errback { fail } + redis.del('set2').errback { fail } + redis.del('set3').errback { fail } + redis.del('set4').errback { fail } redis.del('zset') { done } end end it "gets a score for a specific value in a zset (ZSCORE)" do connect do |redis| - redis.zadd "zset", 23, "value" - redis.zscore("zset", "value") { |r| r.should == "23" } + redis.zadd("zset", 23, "value").errback { fail } + redis.zscore("zset", "value").callback { |r| r.should == "23" }.errback { fail } - redis.zscore("zset", "value2") { |r| r.should == nil } - redis.zscore("unknown_zset", "value") { |r| r.should == nil } + redis.zscore("zset", "value2").callback { |r| r.should == nil }.errback { fail } + redis.zscore("unknown_zset", "value").callback { |r| r.should == nil }.errback { fail } - redis.del("zset") { done } + redis.del("zset").callback { done } end end it "increments a range score of a zset (ZINCRBY)" do connect do |redis| # create a new zset - redis.zincrby "hackers", 1965, "Yukihiro Matsumoto" - redis.zscore("hackers", "Yukihiro Matsumoto") { |r| r.should == "1965" } + redis.zincrby("hackers", 1965, "Yukihiro Matsumoto").errback { fail } + redis.zscore("hackers", "Yukihiro Matsumoto").callback { |r| r.should == "1965" }.errback { fail } # add a new element - redis.zincrby "hackers", 1912, "Alan Turing" - redis.zscore("hackers", "Alan Turing") { |r| r.should == "1912" } + redis.zincrby("hackers", 1912, "Alan Turing").errback { fail } + redis.zscore("hackers", "Alan Turing").callback { |r| r.should == "1912" }.errback { fail } # update the score - redis.zincrby "hackers", 100, "Alan Turing" # yeah, we are making Turing a bit younger - redis.zscore("hackers", "Alan Turing") { |r| r.should == "2012" } + redis.zincrby("hackers", 100, "Alan Turing").errback { fail } # yeah, we are making Turing a bit younger + redis.zscore("hackers", "Alan Turing").callback { |r| r.should == "2012" }.errback { fail } # attempt to update a key that's not a zset - redis.set("i_am_not_a_zet", "value") + redis.set("i_am_not_a_zet", "value").errback { fail } # shouldn't raise error anymore - redis.zincrby("i_am_not_a_zet", 23, "element") { |r| r.should == nil } + redis.zincrby("i_am_not_a_zet", 23, "element").callback { |r| r.should == nil }.errback { fail } - redis.del("hackers") + redis.del("hackers").errback { fail } redis.del("i_am_not_a_zet") { done } end end it "provides info (INFO)" do connect do |redis| - redis.info do |r| + redis.info.callback { |r| [:redis_version, :total_connections_received, :connected_clients, :total_commands_processed, :connected_slaves, :uptime_in_seconds, :used_memory, :uptime_in_days].each do |x| r.keys.include?(x).should == true end done - end + } end end it "provides commandstats (INFO COMMANDSTATS)" do connect do |redis| - redis.info_commandstats do |r| + redis.info_commandstats { |r| r[:get][:calls].should be_a_kind_of(Integer) r[:get][:usec].should be_a_kind_of(Integer) r[:get][:usec_per_call].should be_a_kind_of(Float) done - end + } end end it "flushes the database (FLUSHDB)" do connect do |redis| - redis.set('key1', 'keyone') - redis.set('key2', 'keytwo') - redis.keys('*') { |r| r.sort.should == ['key1', 'key2'].sort } - redis.flushdb - redis.keys('*') { |r| r.should == []; done } + redis.set('key1', 'keyone').errback { fail } + redis.set('key2', 'keytwo').errback { fail } + redis.keys('*').callback { |r| r.sort.should == ['key1', 'key2'].sort }.errback { fail } + redis.flushdb.errback { fail } + redis.keys('*').callback { |r| r.should == []; done } end end it "SELECTs database" do connect do |redis| - redis.set("foo", "bar") do |set_response| - redis.select("10") do |select_response| - redis.get("foo") do |get_response| + redis.set("foo", "bar").callback { |set_response| + redis.select("10").callback { |select_response| + redis.get("foo").callback { |get_response| get_response.should == nil; done - end - end - end + } + } + } end end it "SELECTs database without a callback" do connect do |redis| - redis.select("9") - redis.incr("foo") do |response| + redis.select("9").errback { fail } + redis.incr("foo").callback { |response| response.should == 1 done - end + } end end it "provides the last save time (LASTSAVE)" do connect do |redis| - redis.lastsave do |savetime| + redis.lastsave.callback { |savetime| Time.at(savetime).class.should == Time Time.at(savetime).should <= Time.now done - end + }.errback { fail } end end it "can MGET keys" do connect do |redis| - redis.set('foo', 1000) - redis.set('bar', 2000) - redis.mget('foo', 'bar') { |r| r.should == ['1000', '2000'] } - redis.mget('foo', 'bar', 'baz') { |r| r.should == ['1000', '2000', nil] } - redis.ping { done } + redis.set('foo', 1000).errback { fail } + redis.set('bar', 2000).errback { fail } + redis.mget('foo', 'bar').callback { |r| r.should == ['1000', '2000'] }.errback { fail } + redis.mget('foo', 'bar', 'baz').callback { |r| r.should == ['1000', '2000', nil] }.errback { fail } + redis.ping.callback { done } end end it "can MSET values" do connect do |redis| - redis.mset "key1", "value1", "key2", "value2" - redis.get('key1') { |r| r.should == "value1" } - redis.get('key2') { |r| r.should == "value2"; done } + redis.mset("key1", "value1", "key2", "value2").errback { fail } + redis.get('key1').callback { |r| r.should == "value1" }.errback { fail } + redis.get('key2').callback { |r| r.should == "value2"; done } end end it "can MSETNX values" do connect do |redis| - redis.msetnx "keynx1", "valuenx1", "keynx2", "valuenx2" - redis.mget('keynx1', 'keynx2') { |r| r.should == ["valuenx1", "valuenx2"] } + redis.msetnx("keynx1", "valuenx1", "keynx2", "valuenx2").errback { fail } + redis.mget('keynx1', 'keynx2').callback { |r| r.should == ["valuenx1", "valuenx2"] }.errback { fail } - redis.set("keynx1", "value1") - redis.set("keynx2", "value2") - redis.msetnx "keynx1", "valuenx1", "keynx2", "valuenx2" - redis.mget('keynx1', 'keynx2') { |r| r.should == ["value1", "value2"]; done } + redis.set("keynx1", "value1").errback { fail } + redis.set("keynx2", "value2").errback { fail } + redis.msetnx("keynx1", "valuenx1", "keynx2", "valuenx2").errback { fail } + redis.mget('keynx1', 'keynx2').callback { |r| r.should == ["value1", "value2"]; done } end end it "can BGSAVE" do connect do |redis| - redis.bgsave do |r| + redis.bgsave.callback { |r| ['OK', 'Background saving started'].include?(r).should == true done - end + }.errback { fail } end end it "can ECHO" do connect do |redis| - redis.echo("message in a bottle\n") { |r| r.should == "message in a bottle\n"; done } + redis.echo("message in a bottle\n").callback { |r| r.should == "message in a bottle\n"; done } end end it "runs MULTI without a block" do connect do |redis| redis.multi - redis.get("key1") { |r| r.should == "QUEUED" } + redis.get("key1").callback { |r| r.should == "QUEUED" }.errback { fail } redis.discard { done } end end it "runs MULTI/EXEC" do connect do |redis| - redis.multi - redis.set "key1", "value1" - redis.exec + redis.multi.errback { fail } + redis.set("key1", "value1").errback { fail } + redis.exec.errback { fail } - redis.get("key1") { |r| r.should == "value1" } + redis.get("key1").callback { |r| r.should == "value1" }.errback { fail } begin - redis.multi - redis.set "key2", "value2" + redis.multi.errback { fail } + redis.set("key2", "value2").errback { fail } raise "Some error" - redis.set "key3", "value3" + redis.set("key3", "value3") redis.exec rescue - redis.discard + redis.discard.errback { fail } end - redis.get("key2") { |r| r.should == nil } - redis.get("key3") { |r| r.should == nil; done} + redis.get("key2").callback { |r| r.should == nil }.errback { fail } + redis.get("key3").callback { |r| r.should == nil; done } end end it "sets and get hash values" do connect do |redis| - redis.hset("rush", "signals", "1982") { |r| r.should == 1 } - redis.hexists("rush", "signals") { |r| r.should == 1 } - redis.hget("rush", "signals") { |r| r.should == "1982"; done } + redis.hset("rush", "signals", "1982").callback { |r| r.should == 1 }.errback { fail } + redis.hexists("rush", "signals").callback { |r| r.should == 1 }.errback { fail } + redis.hget("rush", "signals").callback { |r| r.should == "1982"; done } end end it "deletes hash values" do connect do |redis| - redis.hset("rush", "YYZ", "1981") - redis.hdel("rush", "YYZ") { |r| r.should == 1 } - redis.hexists("rush", "YYZ") { |r| r.should == 0; done } + redis.hset("rush", "YYZ", "1981").errback { fail } + redis.hdel("rush", "YYZ").callback { |r| r.should == 1 }.errback { fail } + redis.hexists("rush", "YYZ").callback { |r| r.should == 0; done } end end end @@ -768,38 +768,38 @@ describe EventMachine::Hiredis, "with hash values" do def set(&blk) connect do |redis| - redis.hset("rush", "permanent waves", "1980") - redis.hset("rush", "moving pictures", "1981") - redis.hset("rush", "signals", "1982") + redis.hset("rush", "permanent waves", "1980").errback { fail } + redis.hset("rush", "moving pictures", "1981").errback { fail } + redis.hset("rush", "signals", "1982").errback { fail } blk.call(redis) end end it "gets the length of the hash" do set do |redis| - redis.hlen("rush") { |r| r.should == 3 } - redis.hlen("yyz") { |r| r.should == 0; done } + redis.hlen("rush").callback { |r| r.should == 3 }.errback { fail } + redis.hlen("yyz").callback { |r| r.should == 0; done } end end it "gets the keys and values of the hash" do set do |redis| - redis.hkeys("rush") { |r| r.should == ["permanent waves", "moving pictures", "signals"] } - redis.hvals("rush") { |r| r.should == %w[1980 1981 1982] } - redis.hvals("yyz") { |r| r.should == []; done } + redis.hkeys("rush").callback { |r| r.should == ["permanent waves", "moving pictures", "signals"] }.errback { fail } + redis.hvals("rush").callback { |r| r.should == %w[1980 1981 1982] }.errback { fail } + redis.hvals("yyz").callback { |r| r.should == []; done } end end it "returns all hash values" do set do |redis| - redis.hgetall("rush") do |r| + redis.hgetall("rush").callback { |r| r.should == [ "permanent waves", "1980", "moving pictures", "1981", "signals" , "1982" ] - end - redis.hgetall("yyz") { |r| r.should == []; done } + }.errback { fail } + redis.hgetall("yyz").callback { |r| r.should == []; done } end end end @@ -807,28 +807,28 @@ def set(&blk) describe EventMachine::Hiredis, "with nested multi-bulk response" do def set(&blk) connect do |redis| - redis.set 'user:one:id', 'id-one' - redis.set 'user:two:id', 'id-two' - redis.sadd "user:one:interests", "first-interest" - redis.sadd "user:one:interests", "second-interest" - redis.sadd "user:two:interests", "third-interest" + redis.set('user:one:id', 'id-one').errback { fail } + redis.set('user:two:id', 'id-two').errback { fail } + redis.sadd("user:one:interests", "first-interest").errback { fail } + redis.sadd("user:one:interests", "second-interest").errback { fail } + redis.sadd("user:two:interests", "third-interest").errback { fail } blk.call(redis) end end it "returns array of arrays" do set do |redis| - redis.multi - redis.smembers "user:one:interests" - redis.smembers "user:two:interests" - redis.exec do |interests_one, interests_two| + redis.multi.errback { fail } + redis.smembers("user:one:interests") + redis.smembers("user:two:interests") + redis.exec.callback { |interests_one, interests_two| interests_one.sort.should == ["first-interest", "second-interest"] interests_two.should == ['third-interest'] - end - redis.mget("user:one:id", "user:two:id") do |user_ids| + }.errback { fail } + redis.mget("user:one:id", "user:two:id").callback { |user_ids| user_ids.should == ['id-one', 'id-two'] done - end + } end end end @@ -860,33 +860,33 @@ def set(&blk) context "with some simple sorting data" do def set(&blk) connect do |redis| - redis.set('dog_1', 'louie') - redis.rpush 'Dogs', 1 - redis.set('dog_2', 'lucy') - redis.rpush 'Dogs', 2 - redis.set('dog_3', 'max') - redis.rpush 'Dogs', 3 - redis.set('dog_4', 'taj') - redis.rpush 'Dogs', 4 + redis.set('dog_1', 'louie').errback { fail } + redis.rpush('Dogs', 1).errback { fail } + redis.set('dog_2', 'lucy').errback { fail } + redis.rpush('Dogs', 2).errback { fail } + redis.set('dog_3', 'max').errback { fail } + redis.rpush('Dogs', 3).errback { fail } + redis.set('dog_4', 'taj').errback { fail } + redis.rpush('Dogs', 4).errback { fail } blk.call(redis) end end it "sorts with a limit" do set do |redis| - redis.sort('Dogs', "GET", 'dog_*', "LIMIT", "0", "1") do |r| + redis.sort('Dogs', "GET", 'dog_*', "LIMIT", "0", "1").callback { |r| r.should == ['louie'] done - end + } end end it "sorts with a limit and order" do set do |redis| - redis.sort('Dogs', "GET", 'dog_*', "LIMIT", "0", "1", "desc", "alpha") do |r| + redis.sort('Dogs', "GET", 'dog_*', "LIMIT", "0", "1", "desc", "alpha").callback { |r| r.should == ['taj'] done - end + } end end end @@ -894,37 +894,37 @@ def set(&blk) context "with more complex sorting data" do def set(&blk) connect do |redis| - redis.set('dog:1:name', 'louie') - redis.set('dog:1:breed', 'mutt') - redis.rpush 'dogs', 1 - redis.set('dog:2:name', 'lucy') - redis.set('dog:2:breed', 'poodle') - redis.rpush 'dogs', 2 - redis.set('dog:3:name', 'max') - redis.set('dog:3:breed', 'hound') - redis.rpush 'dogs', 3 - redis.set('dog:4:name', 'taj') - redis.set('dog:4:breed', 'terrier') - redis.rpush 'dogs', 4 + redis.set('dog:1:name', 'louie').errback { fail } + redis.set('dog:1:breed', 'mutt').errback { fail } + redis.rpush('dogs', 1).errback { fail } + redis.set('dog:2:name', 'lucy').errback { fail } + redis.set('dog:2:breed', 'poodle').errback { fail } + redis.rpush('dogs', 2).errback { fail } + redis.set('dog:3:name', 'max').errback { fail } + redis.set('dog:3:breed', 'hound').errback { fail } + redis.rpush('dogs', 3).errback { fail } + redis.set('dog:4:name', 'taj').errback { fail } + redis.set('dog:4:breed', 'terrier').errback { fail } + redis.rpush('dogs', 4).errback { fail } blk.call(redis) end end it "handles multiple GETs" do set do |redis| - redis.sort('dogs', 'GET', 'dog:*:name', 'GET', 'dog:*:breed', 'LIMIT', '0', '1') do |r| + redis.sort('dogs', 'GET', 'dog:*:name', 'GET', 'dog:*:breed', 'LIMIT', '0', '1').callback { |r| r.should == ['louie', 'mutt'] done - end + } end end it "handles multiple GETs with an order" do set do |redis| - redis.sort('dogs', 'GET', 'dog:*:name', 'GET', 'dog:*:breed', 'LIMIT', '0', '1', 'desc', 'alpha') do |r| + redis.sort('dogs', 'GET', 'dog:*:name', 'GET', 'dog:*:breed', 'LIMIT', '0', '1', 'desc', 'alpha').callback { |r| r.should == ['taj', 'terrier'] done - end + } end end end From f29f191e286ef9ba349d26f6d6cde8440012e572 Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Tue, 17 Feb 2015 10:48:00 +0000 Subject: [PATCH 15/44] Update tests to match actual behaviour and interface --- spec/redis_commands_spec.rb | 38 ++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/spec/redis_commands_spec.rb b/spec/redis_commands_spec.rb index 7ec8c3d..51a91a5 100644 --- a/spec/redis_commands_spec.rb +++ b/spec/redis_commands_spec.rb @@ -468,7 +468,7 @@ redis.sadd("set", 'key1').errback { fail } redis.sadd("set", 'key2').errback { fail } redis.zadd('zset', 1, 'set').errback { fail } - redis.zcount('zset').callback { |r| r.should == 1 }.errback { fail } + redis.zcount('zset', '-inf', '+inf').callback { |r| r.should == 1 }.errback { fail } redis.del('set').errback { fail } redis.del('zset') { done } end @@ -480,26 +480,26 @@ redis.sadd("set", 'key2').errback { fail } redis.zadd('zset', 1, 'set').errback { fail } redis.zrange('zset', 0, 1).callback { |r| r.should == ['set'] }.errback { fail } - redis.zcount('zset').callback { |r| r.should == 1 }.errback { fail } + redis.zcount('zset', '-inf', '+inf').callback { |r| r.should == 1 }.errback { fail } redis.del('set').errback { fail } redis.del('zset') { done } end end - it "deletes members to a zset" do + it "deletes members from a zset" do connect do |redis| redis.sadd("set", 'key1').errback { fail } redis.sadd("set", 'key2').errback { fail } - redis.type?('set').callback { |r| r.should == "set" }.errback { fail } + redis.type('set').callback { |r| r.should == "set" }.errback { fail } redis.sadd("set2", 'key3').errback { fail } redis.sadd("set2", 'key4').errback { fail } - redis.type?('set2').callback { |r| r.should == "set" }.errback { fail } + redis.type('set2').callback { |r| r.should == "set" }.errback { fail } redis.zadd('zset', 1, 'set').errback { fail } - redis.zcount('zset').callback { |r| r.should == 1 }.errback { fail } + redis.zcount('zset', '-inf', '+inf').callback { |r| r.should == 1 }.errback { fail } redis.zadd('zset', 2, 'set2').errback { fail } - redis.zcount('zset').callback { |r| r.should == 2 }.errback { fail } - redis.zset_delete('zset', 'set').errback { fail } - redis.zcount('zset').callback { |r| r.should == 1 }.errback { fail } + redis.zcount('zset', '-inf', '+inf').callback { |r| r.should == 2 }.errback { fail } + redis.zrem('zset', 'set').errback { fail } + redis.zcount('zset', '-inf', '+inf').callback { |r| r.should == 1 }.errback { fail } redis.del('set').errback { fail } redis.del('set2').errback { fail } redis.del('zset') { done } @@ -513,13 +513,13 @@ redis.sadd("set2", 'key3').errback { fail } redis.sadd("set2", 'key4').errback { fail } redis.sadd("set3", 'key1').errback { fail } - redis.type?('set').callback { |r| r.should == 'set' }.errback { fail } - redis.type?('set2').callback { |r| r.should == 'set' }.errback { fail } - redis.type?('set3').callback { |r| r.should == 'set' }.errback { fail } + redis.type('set').callback { |r| r.should == 'set' }.errback { fail } + redis.type('set2').callback { |r| r.should == 'set' }.errback { fail } + redis.type('set3').callback { |r| r.should == 'set' }.errback { fail } redis.zadd('zset', 1, 'set').errback { fail } redis.zadd('zset', 2, 'set2').errback { fail } redis.zadd('zset', 3, 'set3').errback { fail } - redis.zcount('zset').callback { |r| r.should == 3 }.errback { fail } + redis.zcount('zset', '-inf', '+inf').callback { |r| r.should == 3 }.errback { fail } redis.zrange('zset', 0, 3).callback { |r| r.should == ['set', 'set2', 'set3'] }.errback { fail } redis.del('set').errback { fail } redis.del('set2').errback { fail } @@ -535,13 +535,13 @@ redis.sadd("set2", 'key3').errback { fail } redis.sadd("set2", 'key4').errback { fail } redis.sadd("set3", 'key1').errback { fail } - redis.type?('set').callback { |r| r.should == 'set' }.errback { fail } - redis.type?('set2').callback { |r| r.should == 'set' }.errback { fail } - redis.type?('set3').callback { |r| r.should == 'set' }.errback { fail } + redis.type('set').callback { |r| r.should == 'set' }.errback { fail } + redis.type('set2').callback { |r| r.should == 'set' }.errback { fail } + redis.type('set3').callback { |r| r.should == 'set' }.errback { fail } redis.zadd('zset', 1, 'set').errback { fail } redis.zadd('zset', 2, 'set2').errback { fail } redis.zadd('zset', 3, 'set3').errback { fail } - redis.zcount('zset').callback { |r| r.should == 3 }.errback { fail } + redis.zcount('zset', '-inf', '+inf').callback { |r| r.should == 3 }.errback { fail } redis.zrevrange('zset', 0, 3).callback { |r| r.should == ['set3', 'set2', 'set'] }.errback { fail } redis.del('set').errback { fail } redis.del('set2').errback { fail } @@ -562,7 +562,7 @@ redis.zadd('zset', 2, 'set2').errback { fail } redis.zadd('zset', 3, 'set3').errback { fail } redis.zadd('zset', 4, 'set4').errback { fail } - redis.zcount('zset').callback { |r| r.should == 4 }.errback { fail } + redis.zcount('zset', '-inf', '+inf').callback { |r| r.should == 4 }.errback { fail } redis.zrangebyscore('zset', 2, 3).callback { |r| r.should == ['set2', 'set3'] }.errback { fail } redis.del('set').errback { fail } redis.del('set2').errback { fail } @@ -601,7 +601,7 @@ # attempt to update a key that's not a zset redis.set("i_am_not_a_zet", "value").errback { fail } # shouldn't raise error anymore - redis.zincrby("i_am_not_a_zet", 23, "element").callback { |r| r.should == nil }.errback { fail } + redis.zincrby("i_am_not_a_zet", 23, "element").errback { |e| e.message.should =~ /WRONGTYPE/ }.callback { fail } redis.del("hackers").errback { fail } redis.del("i_am_not_a_zet") { done } From b7c31f07375043b6c02e4bb0aaad3f181c72932f Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Tue, 17 Feb 2015 15:03:14 +0000 Subject: [PATCH 16/44] Enable travis --- .travis.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..5c4eb72 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: ruby +rvm: + - 2.1.0 + - 2.0.0 + - 1.9.3 From 2e41c5d1574b72eb1a2d6e43e490e4aea1fe3b9c Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Tue, 17 Feb 2015 15:21:02 +0000 Subject: [PATCH 17/44] Notify hipchat on travis build result --- .travis.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5c4eb72..2581eff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,9 @@ language: ruby rvm: - - 2.1.0 - - 2.0.0 - - 1.9.3 +- 2.1.0 +- 2.0.0 +- 1.9.3 +notifications: + hipchat: + rooms: + secure: CwoFDMZL7fLXLTxwb8lXVcArTpEZ8CjxXXwtjwoNo3RZ7QDa/0Wos7XVo8rL8Q6O5mTBHOL1XEZljqhvow20dZxLikrAtO3Tapnqhcgcb143vG3uvCAqO8wa/0WBm/7+3uslunL3Pm2Q0YA02Lxh7XClPsmVXHCbebYQsWDiILI= From 49304a67db40f214751ba61de1b5b136c8c401c7 Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Tue, 17 Feb 2015 15:35:38 +0000 Subject: [PATCH 18/44] Streamline travis notifications --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 2581eff..874c7cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,3 +7,5 @@ notifications: hipchat: rooms: secure: CwoFDMZL7fLXLTxwb8lXVcArTpEZ8CjxXXwtjwoNo3RZ7QDa/0Wos7XVo8rL8Q6O5mTBHOL1XEZljqhvow20dZxLikrAtO3Tapnqhcgcb143vG3uvCAqO8wa/0WBm/7+3uslunL3Pm2Q0YA02Lxh7XClPsmVXHCbebYQsWDiILI= + template: + - '%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message} (Details/Change view)' From 38286884c069ad0b4fb38839cff83f38c32bb8ef Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Wed, 11 Feb 2015 15:08:37 +0000 Subject: [PATCH 19/44] Rewrite to improve connection handling This commit reworks much of em-hiredis to deal properly with several issues which we have come across in the last few months, including: Outstanding responses were tracked per client, but are actually per connection - a manual reconnect (calling close_connection) on the connection previously cleared the queue of pending responses, even though some of the responses might still be awaiting processing by eventmachine, getting requests and responses out of sync, with disasterous results. The pending responses are now tracked per connection. Failing to issue (for example) the PASSWORD or SELECT commands when creating a new connection did not abort the connection, leading the client to become connected but all reads and writes to either fail (authentication) or take place in the default db (select). These commands are now issued and their responses checked, failure counts as a connection failure and triggers a reconnect attempt. Over a long period of time the feature set had grown considerably, as well as the amount of edge case handling. This lead to a BaseClient class with a large set of concerns, complex state modelled as multiple booleans and a confusing interface for configuration and manual reconnection. The initialisation problem outlined above was solved incrementally, but the cost of following the logic was very high - when we were bitten by the response matching problem, the decision was taken to rework the code and seperate concerns as much as possible. This has led to: - No inheritance of pubsub connections and clients from regular ones - Connection management, inactivity checking, command issuing and queuing and state management (of active subscriptions) being seperated as appropriate. Considerable emphasis was placed on testing and producing a testable client, given the core importance of this library to the Pusher system. Seperation of concerns considerably aided the unit testing of components and unit testing in turn allowed the testing of many previously difficult to test cases, particularly in terms of connection initialisation ordering and failure. Some existing features have been removed. Their re-implementation should not be onerous if they are judged to be important by other uses of the client. This was available inconsistently, not when using the suggested helper methods for constructing clients, and not when reconfiguring a client. As there were two competing methods of configuration and URI was universal, it was kept at the expense of the individual values method to simplify the interface. Like pubsub, the MONITOR command switches the redis connection to what is essentially a different protocol - standard commands can no longer be issued, as their responses cannot be matched. It has been left out of the re-implementation as we did not have a use case for the programmatic use of MONITOR. It would not be difficult to re-introduce, and if this is desired, it should be done as a third connection/client class pair because of the "protocol differences" outlined here. em-hiredis offers a "best effort" approach to redis subscriptions. Because connection loss is abstracted from the user, messages may be lost at any time, so the "success" of a subscription does not offer any particular guarantees. Additionally, the redis pubsub protocol does not provide for notifying of the failure to process a subscribe or unsubscribe command. In order to simplify the code and in light of these limitations, the deferrable responses from subscribe and unsubscribe commands have been removed. Clients which wish to track the state of their subscriptions may still bind to the "low level" events and match them to their own requests - this also promotes the idea that the user should be wary of disconnections, because events are emitted for re-subscription after reconnection, making this common case more prominent. We suggest a bump of version to 1.0.0. This change bakes a large number of incremental feature additions and error handling cases accumulated throughout the 0.x.y series in to a model which is built around their existance from the start. --- CHANGELOG.md | 32 +- em-hiredis.gemspec | 4 +- lib/em-hiredis.rb | 25 +- lib/em-hiredis/base_client.rb | 287 ------------ lib/em-hiredis/client.rb | 156 ------- lib/em-hiredis/connection.rb | 69 --- lib/em-hiredis/connection_manager.rb | 178 ++++++++ lib/em-hiredis/pubsub_client.rb | 407 +++++++++++------- lib/em-hiredis/pubsub_connection.rb | 121 ++++++ lib/em-hiredis/redis_client.rb | 381 ++++++++++++++++ lib/em-hiredis/redis_connection.rb | 103 +++++ .../support/cancellable_deferrable.rb | 61 +++ lib/em-hiredis/{ => support}/event_emitter.rb | 0 lib/em-hiredis/support/inactivity_checker.rb | 52 +++ lib/em-hiredis/support/state_machine.rb | 33 ++ lib/em-hiredis/version.rb | 2 +- spec/client_conn_spec.rb | 203 +++++++++ spec/client_server_spec.rb | 402 +++++++++++++++++ spec/connection_manager_spec.rb | 402 +++++++++++++++++ spec/connection_spec.rb | 56 --- spec/inactivity_check_spec.rb | 66 --- spec/inactivity_checker_spec.rb | 122 ++++++ spec/{ => live}/base_client_spec.rb | 47 +- spec/{ => live}/lock_spec.rb | 0 spec/live/pubsub_spec.rb | 209 +++++++++ .../redis_commands_more_spec.rb} | 8 +- spec/{ => live}/redis_commands_spec.rb | 23 - spec/pubsub_client_conn_spec.rb | 297 +++++++++++++ spec/pubsub_connection_spec.rb | 209 +++++++++ spec/pubsub_spec.rb | 314 -------------- spec/redis_connection_spec.rb | 215 +++++++++ spec/spec_helper.rb | 8 +- spec/support/connection_helper.rb | 10 +- spec/support/inprocess_redis_mock.rb | 83 ---- spec/support/mock_connection.rb | 97 +++++ spec/support/networked_redis_mock.rb | 101 +++++ spec/support/redis_mock.rb | 65 --- spec/support/time_mock_eventmachine.rb | 84 ++++ 38 files changed, 3582 insertions(+), 1350 deletions(-) delete mode 100644 lib/em-hiredis/base_client.rb delete mode 100644 lib/em-hiredis/client.rb delete mode 100644 lib/em-hiredis/connection.rb create mode 100644 lib/em-hiredis/connection_manager.rb create mode 100644 lib/em-hiredis/pubsub_connection.rb create mode 100644 lib/em-hiredis/redis_client.rb create mode 100644 lib/em-hiredis/redis_connection.rb create mode 100644 lib/em-hiredis/support/cancellable_deferrable.rb rename lib/em-hiredis/{ => support}/event_emitter.rb (100%) create mode 100644 lib/em-hiredis/support/inactivity_checker.rb create mode 100644 lib/em-hiredis/support/state_machine.rb create mode 100644 spec/client_conn_spec.rb create mode 100644 spec/client_server_spec.rb create mode 100644 spec/connection_manager_spec.rb delete mode 100644 spec/connection_spec.rb delete mode 100644 spec/inactivity_check_spec.rb create mode 100644 spec/inactivity_checker_spec.rb rename spec/{ => live}/base_client_spec.rb (68%) rename spec/{ => live}/lock_spec.rb (100%) create mode 100644 spec/live/pubsub_spec.rb rename spec/{live_redis_protocol_spec.rb => live/redis_commands_more_spec.rb} (97%) rename spec/{ => live}/redis_commands_spec.rb (98%) create mode 100644 spec/pubsub_client_conn_spec.rb create mode 100644 spec/pubsub_connection_spec.rb delete mode 100644 spec/pubsub_spec.rb create mode 100644 spec/redis_connection_spec.rb delete mode 100644 spec/support/inprocess_redis_mock.rb create mode 100644 spec/support/mock_connection.rb create mode 100644 spec/support/networked_redis_mock.rb delete mode 100644 spec/support/redis_mock.rb create mode 100644 spec/support/time_mock_eventmachine.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index a0377e0..fcc46a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## 1.0.0 (2015-02-25) + +[BUGFIX] Replies becoming out of sync after voluntary reconnect. + +[CHANGE] Clients are now configured through the use of URIs only (not individual host, port, db params) + + The previous interface was inconsistently applied, for example clients could be constructed using + individual params, but only re-configured using a uri. + +[CHANGE] Client's public interface simplified considerably wrt connect / reconnect / reconfigure. + + Use `connect` to connect, `reconnect` to force reconnection and `reconnect(uri)` to force reconnection + to a different server. + +[CHANGE] Pubsub interface no longer returns deferrables for subscribe methods. + + Rationale: Redis pubsub subscriptions can only ever be provided on a "best effort" basis where + reconnection-handling is provided - while reconnection takes place, messages will no be received. + If it is important to be aware of these periods, binding to a combination of :disconnected and + :subscribe on the pubsub client will allow one to deduce when the subscription is active. + + The deferrable interface was also awkward in terms of issuing subscribe commands for multiple + channels at once. + +[NEW] Inactivity timeouts: + + Trigger activity on idle connections and force reconnect if no response is found. + Particularly beneficial for pubsub connections where there may be no activity from the server for + extended periods and this is otherwise indistinguishable from a hung TCP connection. + ## 0.2.1 (2013-04-22) [NEW] Support for connecting to redis on a unix socket. @@ -15,7 +45,7 @@ * Clients now emit the following events: connected, reconnected, disconnected, reconnect_failed (passes the number of consecutive failures) * Client is considered failed after 4 consecutive failures * Fails all queued commands when client failed - * Can now reconfiure and reconnect an exising client + * Can now reconfigure and reconnect an exising client * Reconnect timeout can be configured (defaults to 0.5s) [NEW] Added `EM::Hiredis::Lock` and `EM::Hiredis::PersistentLock` diff --git a/em-hiredis.gemspec b/em-hiredis.gemspec index 694c388..cfa2181 100644 --- a/em-hiredis.gemspec +++ b/em-hiredis.gemspec @@ -6,8 +6,8 @@ Gem::Specification.new do |s| s.name = "em-hiredis" s.version = EventMachine::Hiredis::VERSION s.platform = Gem::Platform::RUBY - s.authors = ["Martyn Loughran"] - s.email = ["me@mloughran.com"] + s.authors = ["Martyn Loughran", "Mike Pye"] + s.email = ["me@mloughran.com", "mail@mdpye.co.uk"] s.homepage = "http://github.com/mloughran/em-hiredis" s.summary = %q{Eventmachine redis client} s.description = %q{Eventmachine redis client using hiredis native parser} diff --git a/lib/em-hiredis.rb b/lib/em-hiredis.rb index d305a1d..9a0c17c 100644 --- a/lib/em-hiredis.rb +++ b/lib/em-hiredis.rb @@ -16,11 +16,9 @@ class << self end self.reconnect_timeout = 0.5 - def self.setup(uri = nil) + def self.setup(uri = nil, activity_timeout = nil, response_timeout = nil) uri = uri || ENV["REDIS_URL"] || "redis://127.0.0.1:6379/0" - client = Client.new - client.configure(uri) - client + Client.new(uri, activity_timeout, response_timeout) end # Connects to redis and returns a client instance @@ -34,10 +32,9 @@ def self.setup(uri = nil) # Unix socket uris are supported, e.g. unix:///tmp/redis.sock, however # it's not possible to set the db or password - use initialize instead in # this case - def self.connect(uri = nil) - client = setup(uri) + def self.connect(uri = nil, activity_timeout = nil, response_timeout = nil) + client = setup(uri, activity_timeout, response_timeout) client.connect - client end def self.logger=(logger) @@ -58,8 +55,14 @@ def self.logger end end -require 'em-hiredis/event_emitter' -require 'em-hiredis/connection' -require 'em-hiredis/base_client' -require 'em-hiredis/client' +require 'digest/sha1' +require 'hiredis/reader' +require 'em-hiredis/support/event_emitter' +require 'em-hiredis/support/cancellable_deferrable' +require 'em-hiredis/support/inactivity_checker' +require 'em-hiredis/support/state_machine' +require 'em-hiredis/connection_manager' +require 'em-hiredis/redis_connection' +require 'em-hiredis/pubsub_connection' +require 'em-hiredis/redis_client' require 'em-hiredis/pubsub_client' diff --git a/lib/em-hiredis/base_client.rb b/lib/em-hiredis/base_client.rb deleted file mode 100644 index 4cca340..0000000 --- a/lib/em-hiredis/base_client.rb +++ /dev/null @@ -1,287 +0,0 @@ -require 'uri' - -module EventMachine::Hiredis - # Emits the following events - # - # * :connected - on successful connection or reconnection - # * :reconnected - on successful reconnection - # * :disconnected - no longer connected, when previously in connected state - # * :reconnect_failed(failure_number) - a reconnect attempt failed - # This event is passed number of failures so far (1,2,3...) - # * :monitor - # - class BaseClient - include EventEmitter - include EM::Deferrable - - attr_reader :host, :port, :password, :db - - def initialize(host = 'localhost', port = 6379, password = nil, db = nil) - @host, @port, @password, @db = host, port, password, db - @defs = [] - @command_queue = [] - - @reconnect_failed_count = 0 - @reconnect_timer = nil - @failed = false - - @inactive_seconds = 0 - - self.on(:failed) { - @failed = true - @command_queue.each do |df, _, _| - df.fail(Error.new("Redis connection in failed state")) - end - @command_queue = [] - } - end - - # Configure the redis connection to use - # - # In usual operation, the uri should be passed to initialize. This method - # is useful for example when failing over to a slave connection at runtime - # - def configure(uri_string) - uri = URI(uri_string) - - if uri.scheme == "unix" - @host = uri.path - @port = nil - else - @host = uri.host - @port = uri.port - @password = uri.password - path = uri.path[1..-1] - @db = path.to_i # Empty path => 0 - end - end - - # Disconnect then reconnect the redis connection. - # - # Pass optional uri - e.g. to connect to a different redis server. - # Any pending redis commands will be failed, but during the reconnection - # new commands will be queued and sent after connected. - # - def reconnect!(new_uri = nil) - @connection.close_connection - configure(new_uri) if new_uri - @auto_reconnect = true - EM.next_tick { reconnect_connection } - end - - def connect - @auto_reconnect = true - @connection = EM.connect(@host, @port, Connection, @host, @port) - - @connection.on(:closed, &method(:handle_disconnect)) - - @connection.on(:connected) do - if @db != 0 - df = EM::DefaultDeferrable.new - send_command(df, :select, @db) - df.callback { - handle_connected - }.errback { - handle_disconnect - } - else - handle_connected - end - end - - @connection.on(:message) do |reply| - if RuntimeError === reply - raise "Replies out of sync: #{reply.inspect}" if @defs.empty? - deferred = @defs.shift - error = RedisError.new(reply.message) - error.redis_error = reply - deferred.fail(error) if deferred - else - @inactive_seconds = 0 - handle_reply(reply) - end - end - - @connected = false - @reconnecting = false - - return self - end - - # Indicates that commands have been sent to redis but a reply has not yet - # been received - # - # This can be useful for example to avoid stopping the - # eventmachine reactor while there are outstanding commands - # - def pending_commands? - @connected && @defs.size > 0 - end - - def connected? - @connected - end - - def select(db, &blk) - @db = db - method_missing(:select, db, &blk) - end - - def auth(password, &blk) - @password = password - method_missing(:auth, password, &blk) - end - - def close_connection - EM.cancel_timer(@reconnect_timer) if @reconnect_timer - @auto_reconnect = false - @connection.close_connection_after_writing - end - - # Note: This method doesn't disconnect if already connected. You probably - # want to use `reconnect!` - def reconnect_connection - @auto_reconnect = true - EM.cancel_timer(@reconnect_timer) if @reconnect_timer - reconnect - end - - # Starts an inactivity checker which will ping redis if nothing has been - # heard on the connection for `trigger_secs` seconds and forces a reconnect - # after a further `response_timeout` seconds if we still don't hear anything. - def configure_inactivity_check(trigger_secs, response_timeout) - raise ArgumentError('trigger_secs must be > 0') unless trigger_secs.to_i > 0 - raise ArgumentError('response_timeout must be > 0') unless response_timeout.to_i > 0 - - @inactivity_trigger_secs = trigger_secs.to_i - @inactivity_response_timeout = response_timeout.to_i - - # Start the inactivity check now only if we're already conected, otherwise - # the connected event will schedule it. - schedule_inactivity_checks if @connected - end - - private - - def send_command(df, sym, *args) - @connection.send_command(sym, args) - @defs.push(df) - end - - def method_missing(sym, *args) - deferred = EM::DefaultDeferrable.new - # Shortcut for defining the callback case with just a block - deferred.callback { |result| yield(result) } if block_given? - - if @connected - send_command(deferred, sym, *args) - elsif @failed - deferred.fail(Error.new("Redis connection in failed state")) - else - @command_queue << [deferred, sym, args] - end - - deferred - end - - def reconnect - @reconnecting = true - EM::Hiredis.logger.info("#{@connection} Reconnecting") - begin - @connection.reconnect @host, @port - rescue EventMachine::ConnectionError => e - EM::Hiredis.logger.error("Error during connect: #{e.to_s}") - EM.next_tick { handle_disconnect } - end - end - - def cancel_inactivity_checks - EM.cancel_timer(@inactivity_timer) if @inactivity_timer - @inactivity_timer = nil - end - - def schedule_inactivity_checks - if @inactivity_trigger_secs - @inactive_seconds = 0 - @inactivity_timer = EM.add_periodic_timer(1) { - @inactive_seconds += 1 - if @inactive_seconds > @inactivity_trigger_secs + @inactivity_response_timeout - EM::Hiredis.logger.error "#{@connection} No response to ping, triggering reconnect" - reconnect! - elsif @inactive_seconds > @inactivity_trigger_secs - EM::Hiredis.logger.debug "#{@connection} Connection inactive, triggering ping" - ping - end - } - end - end - - def handle_reply(reply) - if @defs.empty? - if @monitoring - emit(:monitor, reply) - else - raise "Replies out of sync: #{reply.inspect}" - end - else - deferred = @defs.shift - deferred.succeed(reply) if deferred - end - end - - def handle_connected - @connected = true - @reconnect_failed_count = 0 - @failed = false - - auth(@password) if @password - - @command_queue.each do |df, command, args| - send_command(df, command, *args) - end - @command_queue = [] - - schedule_inactivity_checks - - emit(:connected) - EM::Hiredis.logger.info("#{@connection} Connected") - succeed - - if @reconnecting - @reconnecting = false - emit(:reconnected) - end - end - - def handle_disconnect - cancel_inactivity_checks - if @connected - @defs.each { |d| d.fail(Error.new("Redis disconnected")) } - @defs = [] - @deferred_status = nil - @connected = false - if @auto_reconnect - # Next tick avoids reconnecting after for example EM.stop - EM.next_tick { reconnect! } - end - emit(:disconnected) - EM::Hiredis.logger.info("#{@connection} Disconnected") - else - if @auto_reconnect - @reconnect_failed_count += 1 - @reconnect_timer = EM.add_timer(EM::Hiredis.reconnect_timeout) { - @reconnect_timer = nil - reconnect! - } - emit(:reconnect_failed, @reconnect_failed_count) - EM::Hiredis.logger.info("#{@connection} Reconnect failed") - - if @reconnect_failed_count >= 4 - emit(:failed) - self.fail(Error.new("Could not connect after 4 attempts")) - end - end - end - end - end -end diff --git a/lib/em-hiredis/client.rb b/lib/em-hiredis/client.rb deleted file mode 100644 index a47680a..0000000 --- a/lib/em-hiredis/client.rb +++ /dev/null @@ -1,156 +0,0 @@ -require 'digest/sha1' - -module EventMachine::Hiredis - class Client < BaseClient - def self.connect(host = 'localhost', port = 6379) - new(host, port).connect - end - - def self.load_scripts_from(dir) - Dir.glob("#{dir}/*.lua").each do |f| - name = File.basename(f, '.lua') - lua = load_script(f) - EM::Hiredis.logger.debug { "Registering script: #{name}" } - EM::Hiredis::Client.register_script(name, lua) - end - end - - def self.load_script(file) - script_text = File.open(file, 'r').read - - inc_path = File.dirname(file) - while (m = /^-- #include (.*)$/.match(script_text)) - inc_file = m[1] - inc_body = File.read("#{inc_path}/#{inc_file}") - to_replace = Regexp.new("^-- #include #{inc_file}$") - script_text = script_text.gsub(to_replace, "#{inc_body}\n") - end - script_text - end - - def self.register_script(name, lua) - sha = Digest::SHA1.hexdigest(lua) - self.send(:define_method, name.to_sym) { |keys, args=[]| - eval_script(lua, sha, keys, args) - } - self.send(:define_method, "#{name}_script".to_sym) { - lua - } - self.send(:define_method, "#{name}_sha".to_sym) { - sha - } - end - - def register_script(name, lua) - sha = Digest::SHA1.hexdigest(lua) - singleton = class << self; self end - singleton.send(:define_method, name.to_sym) { |keys, args=[]| - eval_script(lua, sha, keys, args) - } - end - - def eval_script(lua, lua_sha, keys, args) - df = EM::DefaultDeferrable.new - method_missing(:evalsha, lua_sha, keys.size, *keys, *args).callback( - &df.method(:succeed) - ).errback { |e| - if e.kind_of?(RedisError) && e.redis_error.message.start_with?("NOSCRIPT") - self.eval(lua, keys.size, *keys, *args) - .callback(&df.method(:succeed)).errback(&df.method(:fail)) - else - df.fail(e) - end - } - df - end - - def ensure_script(script_name) - df = EM::DefaultDeferrable.new - method_missing( - :script, - 'exists', - self.send("#{script_name}_sha".to_sym) - ).callback { |ret| - # ret is an array of 0 or 1s representing existence for each script arg passed - if ret[0] == 0 - method_missing( - :script, - 'load', - self.send("#{script_name}_script".to_sym) - ).callback { - df.succeed - }.errback { |e| - df.fail(e) - } - else - df.succeed - end - }.errback { |e| - df.fail(e) - } - df - end - - def monitor(&blk) - @monitoring = true - method_missing(:monitor, &blk) - end - - def info - df = method_missing(:info) - df.callback { |response| - info = {} - response.each_line do |line| - key, value = line.split(":", 2) - info[key.to_sym] = value.chomp if value - end - df.succeed(info) - } - df.callback { |info| yield info } if block_given? - df - end - - def info_commandstats(&blk) - hash_processor = lambda do |response| - commands = {} - response.each_line do |line| - command, data = line.split(':') - if data - c = commands[command.sub('cmdstat_', '').to_sym] = {} - data.split(',').each do |d| - k, v = d.split('=') - c[k.to_sym] = v =~ /\./ ? v.to_f : v.to_i - end - end - end - blk.call(commands) - end - method_missing(:info, 'commandstats', &hash_processor) - end - - # Gives access to a richer interface for pubsub subscriptions on a - # separate redis connection - # - def pubsub - @pubsub ||= begin - PubsubClient.new(@host, @port, @password, @db).connect - end - end - - def subscribe(*channels) - raise "Use pubsub client" - end - - def unsubscribe(*channels) - raise "Use pubsub client" - end - - def psubscribe(channel) - raise "Use pubsub client" - end - - def punsubscribe(channel) - raise "Use pubsub client" - end - end -end diff --git a/lib/em-hiredis/connection.rb b/lib/em-hiredis/connection.rb deleted file mode 100644 index 062db61..0000000 --- a/lib/em-hiredis/connection.rb +++ /dev/null @@ -1,69 +0,0 @@ -require 'hiredis/reader' - -module EventMachine::Hiredis - class Connection < EM::Connection - include EventMachine::Hiredis::EventEmitter - - def initialize(host, port) - super - @host, @port = host, port - @name = "[em-hiredis #{@host}:#{@port}]" - end - - def reconnect(host, port) - super - @host, @port = host, port - end - - def connection_completed - @reader = ::Hiredis::Reader.new - emit(:connected) - end - - def receive_data(data) - @reader.feed(data) - until (reply = @reader.gets) == false - emit(:message, reply) - end - end - - def unbind - emit(:closed) - end - - def send_command(command, args) - send_data(command(command, *args)) - end - - def to_s - @name - end - - protected - - COMMAND_DELIMITER = "\r\n" - - def command(*args) - command = [] - command << "*#{args.size}" - - args.each do |arg| - arg = arg.to_s - command << "$#{string_size arg}" - command << arg - end - - command.join(COMMAND_DELIMITER) + COMMAND_DELIMITER - end - - if "".respond_to?(:bytesize) - def string_size(string) - string.to_s.bytesize - end - else - def string_size(string) - string.to_s.size - end - end - end -end diff --git a/lib/em-hiredis/connection_manager.rb b/lib/em-hiredis/connection_manager.rb new file mode 100644 index 0000000..e59d860 --- /dev/null +++ b/lib/em-hiredis/connection_manager.rb @@ -0,0 +1,178 @@ +require 'uri' + +module EventMachine::Hiredis + # Manages EventMachine connections in order to provide reconnections. + # + # Emits the following events + # - :connected - on successful connection or reconnection + # - :reconnected - on successful reconnection + # - :disconnected - no longer connected, when previously in connected state + # - :reconnect_failed(failure_number) - a reconnect attempt failed + # This event is passed number of failures so far (1,2,3...) + class ConnectionManager + include EventEmitter + + TRANSITIONS = [ + # first connect call + [ :initial, :connecting ], + # TCP connect, or initialisation commands fail + [ :connecting, :disconnected ], + # manual reconnect while connecting + [ :connecting, :connecting ], + # Connection ready for use by clients + [ :connecting, :connected ], + # connection lost + [ :connected, :disconnected ], + # attempting automatic reconnect + [ :disconnected, :connecting ], + # all automatic reconnection attempts failed + [ :disconnected, :failed ], + # manual call of reconnect after failure + [ :failed, :connecting ], + + # Calling close + [ :initial, :stopped ], + [ :connecting, :stopped ], + [ :connected, :stopped ], + [ :disconnected, :stopped ], + [ :failed, :stopped ], + ] + + # connection_factory: an object which responds to `call` by returning a + # deferrable which succeeds with a connected and initialised instance + # of EMConnection or fails if the connection was unsuccessful. + # Failures will be retried + def initialize(connection_factory, em = EM) + @em = em + @connection_factory = connection_factory + + @reconnect_attempt = 0 + + @sm = StateMachine.new + TRANSITIONS.each { |t| @sm.transition(*t) } + + @sm.on(:connecting, &method(:on_connecting)) + @sm.on(:connected, &method(:on_connected)) + @sm.on(:disconnected, &method(:on_disconnected)) + @sm.on(:failed, &method(:on_failed)) + end + + def connect + @sm.update_state(:connecting) + end + + def reconnect + case @sm.state + when :initial + connect + when :connecting + @connect_operation.cancel + @sm.update_state(:connecting) + when :connected + @connection.close_connection + when :disconnected + @sm.update_state(:connecting) + when :failed + @sm.update_state(:connecting) + end + end + + def close + case @sm.state + when :initial + @sm.update_state(:stopped) + when :connecting + @connect_operation.cancel + @sm.update_state(:stopped) + when :connected + @connection.remove_all_listeners(:disconnected) + @connection.close_connection + @sm.update_state(:stopped) + when :disconnected + @sm.update_state(:stopped) + when :failed + @sm.update_state(:stopped) + end + end + + def state + @sm.state + end + + # Access to the underlying connection. + def connection + raise "Not connected, currently #{@sm.state}" unless @sm.state == :connected + @connection + end + + def pending_commands + if @sm.state == :connected + @connection.pending_responses + else + 0 + end + end + + protected + + def on_connecting(prev_state) + if @reconnect_timer + @em.cancel_timer(@reconnect_timer) + @reconnect_timer = nil + end + + @connect_operation = + CancellableDeferrable.new(@connection_factory.call).callback { |connection| + @connection = connection + @sm.update_state(:connected) + + connection.on(:disconnected) { + @sm.update_state(:disconnected) if @connection == connection + } + }.callback_cancelled { |connection| + connection.close_connection + }.errback { |e| + @sm.update_state(:disconnected) + } + end + + def on_connected(prev_state) + emit(:connected) + if @reconnect_attempt > 0 + emit(:reconnected) + @reconnect_attempt = 0 + end + end + + def on_failed(prev_state) + emit(:failed) + end + + def on_disconnected(prev_state) + @connection = nil + + emit(:disconnected) if prev_state == :connected + emit(:reconnect_failed, @reconnect_attempt) if @reconnect_attempt > 0 + + delay_reconnect = prev_state != :connected + + # External agents have the opportunity to call reconnect and change the + # state when we emit :disconnected and :reconnected, so we should only + # proceed here if our state has not been touched. + return unless @sm.state == :disconnected + + if @reconnect_attempt > 3 + @sm.update_state(:failed) + else + @reconnect_attempt += 1 + if delay_reconnect + @reconnect_timer = @em.add_timer(EventMachine::Hiredis.reconnect_timeout) { + @sm.update_state(:connecting) + } + else + @sm.update_state(:connecting) + end + end + end + end +end diff --git a/lib/em-hiredis/pubsub_client.rb b/lib/em-hiredis/pubsub_client.rb index 2f0fb20..72809df 100644 --- a/lib/em-hiredis/pubsub_client.rb +++ b/lib/em-hiredis/pubsub_client.rb @@ -1,201 +1,274 @@ +require 'uri' + module EventMachine::Hiredis - class PubsubClient < BaseClient - PUBSUB_MESSAGES = %w{message pmessage subscribe unsubscribe psubscribe punsubscribe}.freeze + # Emits the following events: + # + # Life cycle events: + # - :connected - on successful connection or reconnection + # - :reconnected - on successful reconnection + # - :disconnected - no longer connected, when previously in connected state + # - :reconnect_failed(failure_number) - a reconnect attempt failed + # This event is passed number of failures so far (1,2,3...) + # - :failed - on failing the final reconnect attempt + # + # Subscription events: + # - :message(channel, message) - on receiving message on channel which has an active subscription + # - :pmessage(pattern, channel, message) - on receiving message on channel which has an active subscription due to pattern + # - :subscribe(channel) - on confirmation of subscription to channel + # - :psubscribe(pattern) - on confirmation of subscription to pattern + # - :unsubscribe(channel) - on confirmation of unsubscription from channel + # - :punsubscribe(pattern) - on confirmation of unsubscription from pattern + # + # Note that :subscribe and :psubscribe will be emitted after a reconnection for + # all subscriptions which were active at the time the connection was lost. + class PubsubClient + include EventEmitter + include EventMachine::Deferrable - PING_CHANNEL = '__em-hiredis-ping' + attr_reader :host, :port, :password - def initialize(host='localhost', port='6379', password=nil, db=nil) - @subs, @psubs = [], [] - @pubsub_defs = Hash.new { |h,k| h[k] = [] } - super - end + # uri: + # the redis server to connect to, redis://[:password@]host[:port][/db] + # inactivity_trigger_secs: + # the number of seconds of inactivity before triggering a ping to the server + # inactivity_response_timeout: + # the number of seconds after a ping at which to terminate the connection + # if there is still no activity + def initialize( + uri, + inactivity_trigger_secs = nil, + inactivity_response_timeout = nil, + em = EventMachine) - def connect - @sub_callbacks = Hash.new { |h, k| h[k] = [] } - @psub_callbacks = Hash.new { |h, k| h[k] = [] } - - # Resubsubscribe to channels on reconnect - on(:reconnected) { - raw_send_command(:subscribe, @subs) if @subs.any? - raw_send_command(:psubscribe, @psubs) if @psubs.any? + @em = em + configure(uri) + + @inactivity_trigger_secs = inactivity_trigger_secs + @inactivity_response_timeout = inactivity_response_timeout + + # Subscribed channels and patterns to their callbacks + # nil is a valid "callback", required because even if the user is using + # emitted events rather than callbacks to consume their messages, we still + # need to mark the fact that we are subscribed. + @subscriptions = Hash.new { |h, k| h[k] = [] } + @psubscriptions = Hash.new { |h, k| h[k] = [] } + + @connection_manager = ConnectionManager.new(method(:factory_connection), em) + + @connection_manager.on(:connected) { + EM::Hiredis.logger.info("#{@name} - Connected") + emit(:connected) + set_deferred_status(:succeeded) + } + + @connection_manager.on(:disconnected) { + EM::Hiredis.logger.info("#{@name} - Disconnected") + emit(:disconnected) + } + @connection_manager.on(:reconnected) { + EM::Hiredis.logger.info("#{@name} - Reconnected") + emit(:reconnected) + } + @connection_manager.on(:reconnect_failed) { |count| + EM::Hiredis.logger.warn("#{@name} - Reconnect failed, attempt #{count}") + emit(:reconnect_failed, count) + } + + @connection_manager.on(:failed) { + EM::Hiredis.logger.error("#{@name} - Connection failed") + emit(:failed) + set_deferred_status(:failed, Error.new('Could not connect after 4 attempts')) } - - super - end - - # Subscribe to a pubsub channel - # - # If an optional proc / block is provided then it will be called when a - # message is received on this channel - # - # @return [Deferrable] Redis subscribe call - # - def subscribe(channel, proc = nil, &block) - if cb = proc || block - @sub_callbacks[channel] << cb - end - @subs << channel - raw_send_command(:subscribe, [channel]) - return pubsub_deferrable(channel) end - - # Unsubscribe all callbacks for a given channel - # - # @return [Deferrable] Redis unsubscribe call + + # Connect to the configured redis server. Returns a deferrable which + # completes upon successful connections or fails after all reconnect attempts + # are exhausted. # - def unsubscribe(channel) - @sub_callbacks.delete(channel) - @subs.delete(channel) - raw_send_command(:unsubscribe, [channel]) - return pubsub_deferrable(channel) + # Commands may be issued before or during connection, they will be queued + # and submitted to the server once the connection is active. + def connect + @connection_manager.connect + return self end - # Unsubscribe a given callback from a channel. Will unsubscribe from redis - # if there are no remaining subscriptions on this channel - # - # @return [Deferrable] Succeeds when the unsubscribe has completed or - # fails if callback could not be found. Note that success may happen - # immediately in the case that there are other callbacks for the same - # channel (and therefore no unsubscription from redis is necessary) - # - def unsubscribe_proc(channel, proc) - df = EM::DefaultDeferrable.new - if @sub_callbacks[channel].delete(proc) - if @sub_callbacks[channel].any? - # Succeed deferrable immediately - no need to unsubscribe - df.succeed - else - unsubscribe(channel).callback { |_| - df.succeed - } - end - else - df.fail - end - return df + # Reconnect, either: + # - because the client has reached a failed state, but you believe the + # underlying problem to be resolved + # - with an optional different uri, because you wish to tear down the + # connection and connect to a different redis server, perhaps as part of + # a failover + def reconnect(uri = nil) + configure(uri) if uri + @connection_manager.reconnect end - # Pattern subscribe to a pubsub channel - # - # If an optional proc / block is provided then it will be called (with the - # channel name and message) when a message is received on a matching - # channel - # - # @return [Deferrable] Redis psubscribe call - # - def psubscribe(pattern, proc = nil, &block) - if cb = proc || block - @psub_callbacks[pattern] << cb - end - @psubs << pattern - raw_send_command(:psubscribe, [pattern]) - return pubsub_deferrable(pattern) + # Terminate the client permanently + def close + @connection_manager.close + end + + ## Exposed state + + def pending_commands + @connection_manager.pending_commands + end + + def pending_commands? + return pending_commands > 0 + end + + ## Commands + + def subscribe(channel, proc = nil, &blk) + cb = proc || blk + subscribe_impl(:subscribe, @subscriptions, channel, cb) + end + + def psubscribe(pattern, proc = nil, &blk) + cb = proc || blk + subscribe_impl(:psubscribe, @psubscriptions, pattern, cb) + end + + def unsubscribe(channel) + unsubscribe_impl(:unsubscribe, @subscriptions, channel) end - # Pattern unsubscribe all callbacks for a given pattern - # - # @return [Deferrable] Redis punsubscribe call - # def punsubscribe(pattern) - @psub_callbacks.delete(pattern) - @psubs.delete(pattern) - raw_send_command(:punsubscribe, [pattern]) - return pubsub_deferrable(pattern) + unsubscribe_impl(:punsubscribe, @psubscriptions, pattern) + end + + def unsubscribe_proc(channel, proc) + unsubscribe_proc_impl(:unsubscribe, @subscriptions, channel, proc) end - # Unsubscribe a given callback from a pattern. Will unsubscribe from redis - # if there are no remaining subscriptions on this pattern - # - # @return [Deferrable] Succeeds when the punsubscribe has completed or - # fails if callback could not be found. Note that success may happen - # immediately in the case that there are other callbacks for the same - # pattern (and therefore no punsubscription from redis is necessary) - # def punsubscribe_proc(pattern, proc) + unsubscribe_proc_impl(:punsubscribe, @psubscriptions, pattern, proc) + end + + protected + + def configure(uri_string) + uri = URI(uri_string) + + @host = uri.host + @port = uri.port + @password = uri.password + + if @name + EM::Hiredis.logger.info("#{@name} - Reconfiguring to #{uri_string}") + else + EM::Hiredis.logger.info("#{uri_string} (pubsub) - Configured") + end + @name = "#{uri_string} (pubsub)" + end + + def factory_connection df = EM::DefaultDeferrable.new - if @psub_callbacks[pattern].delete(proc) - if @psub_callbacks[pattern].any? - # Succeed deferrable immediately - no need to punsubscribe - df.succeed - else - punsubscribe(pattern).callback { |_| - df.succeed + + begin + connection = @em.connect( + @host, + @port, + PubsubConnection, + @inactivity_trigger_secs, + @inactivity_response_timeout, + @name + ) + + connection.on(:connected) { + maybe_auth(connection).callback { + + connection.on(:message, &method(:message_callbacks)) + connection.on(:pmessage, &method(:pmessage_callbacks)) + + [ :message, + :pmessage, + :subscribe, + :unsubscribe, + :psubscribe, + :punsubscribe + ].each do |command| + connection.on(command) { |*args| + emit(command, *args) + } + end + + connection.send_command(:subscribe, *@subscriptions.keys) if @subscriptions.any? + connection.send_command(:psubscribe, *@psubscriptions.keys) if @psubscriptions.any? + + df.succeed(connection) + }.errback { |e| + # Failure to auth counts as a connection failure + connection.close_connection + df.fail(e) } - end - else - df.fail + } + + connection.on(:connection_failed) { + df.fail('Connection failed') + } + rescue EventMachine::ConnectionError => e + df.fail(e) end + return df end - # Pubsub connections to not support even the PING command, but it is useful, - # especially with read-only connections like pubsub, to be able to check that - # the TCP connection is still usefully alive. - # - # This is not particularly elegant, but it's probably the best we can do - # for now. Ping support for pubsub connections is being considerred: - # https://github.com/antirez/redis/issues/420 - def ping - subscribe(PING_CHANNEL).callback { - unsubscribe(PING_CHANNEL) - } + def subscribe_impl(type, subscriptions, channel, cb) + if subscriptions.include?(channel) + # Short circuit issuing the command if we're already subscribed + subscriptions[channel] << cb + elsif @connection_manager.state == :failed + raise('Redis connection in failed state') + elsif @connection_manager.state == :connected + @connection_manager.connection.send_command(type, channel) + subscriptions[channel] << cb + else + # We will issue subscription command when we connect + subscriptions[channel] << cb + end end - private - - # Send a command to redis without adding a deferrable for it. This is - # useful for commands for which replies work or need to be treated - # differently - def raw_send_command(sym, args) - if @connected - @connection.send_command(sym, args) - else - callback do - @connection.send_command(sym, args) + def unsubscribe_impl(type, subscriptions, channel) + if subscriptions.include?(channel) + subscriptions.delete(channel) + if @connection_manager.state == :connected + @connection_manager.connection.send_command(type, channel) end end - return nil end - def pubsub_deferrable(channel) - df = EM::DefaultDeferrable.new - @pubsub_defs[channel].push(df) - df - end - - def handle_reply(reply) - if reply && PUBSUB_MESSAGES.include?(reply[0]) # reply can be nil - # Note: pmessage is the only message with 4 arguments - kind, subscription, d1, d2 = *reply - - case kind.to_sym - when :message - if @sub_callbacks.has_key?(subscription) - @sub_callbacks[subscription].each { |cb| cb.call(d1) } - end - # Arguments are channel, message payload - emit(:message, subscription, d1) - when :pmessage - if @psub_callbacks.has_key?(subscription) - @psub_callbacks[subscription].each { |cb| cb.call(d1, d2) } - end - # Arguments are original pattern, channel, message payload - emit(:pmessage, subscription, d1, d2) - else - if @pubsub_defs[subscription].any? - df = @pubsub_defs[subscription].shift - df.succeed(d1) - # Cleanup empty arrays - if @pubsub_defs[subscription].empty? - @pubsub_defs.delete(subscription) - end - end + def unsubscribe_proc_impl(type, subscriptions, channel, proc) + removed = subscriptions[channel].delete(proc) - # Also emit the event, as an alternative to using the deferrables - emit(kind.to_sym, subscription, d1) - end + # Kill the redis subscription if that was the last callback + if removed && subscriptions[channel].empty? + unsubscribe_impl(type, subscriptions, channel) + end + end + + def message_callbacks(channel, message) + cbs = @subscriptions[channel] + if cbs + cbs.each { |cb| cb.call(message) if cb } + end + end + + def pmessage_callbacks(pattern, channel, message) + cbs = @psubscriptions[pattern] + if cbs + cbs.each { |cb| cb.call(channel, message) if cb } + end + end + + def maybe_auth(connection) + if @password + connection.auth(@password) else - super + df = EM::DefaultDeferrable.new + df.succeed + df end end end diff --git a/lib/em-hiredis/pubsub_connection.rb b/lib/em-hiredis/pubsub_connection.rb new file mode 100644 index 0000000..b7464dc --- /dev/null +++ b/lib/em-hiredis/pubsub_connection.rb @@ -0,0 +1,121 @@ +module EventMachine::Hiredis + module PubsubConnection + include EventMachine::Hiredis::EventEmitter + + PUBSUB_COMMANDS = %w{subscribe unsubscribe psubscribe punsubscribe}.freeze + PUBSUB_MESSAGES = (PUBSUB_COMMANDS + %w{message pmessage}).freeze + + PING_CHANNEL = '__em-hiredis-ping' + + def initialize(inactivity_trigger_secs = nil, + inactivity_response_timeout = 2, + name = 'unnamed connection') + + @name = name + @reader = ::Hiredis::Reader.new + + @connected = false + + @inactivity_checker = InactivityChecker.new(inactivity_trigger_secs, inactivity_response_timeout) + @inactivity_checker.on(:activity_timeout) { + EM::Hiredis.logger.debug("#{@name} - Sending ping") + send_command('subscribe', PING_CHANNEL) + send_command('unsubscribe', PING_CHANNEL) + } + @inactivity_checker.on(:response_timeout) { + EM::Hiredis.logger.warn("#{@name} - Closing connection because of inactivity timeout") + close_connection + } + end + + def send_command(command, *channels) + if PUBSUB_COMMANDS.include?(command.to_s) + send_data(marshal(command, *channels)) + else + raise "Cannot send command '#{command}' on Pubsub connection" + end + end + + def pending_responses + # Connection is read only, we only issue subscribes and unsubscribes + # and we don't count their issue vs completion, so there can be no + # meaningful responses pending. + 0 + end + + # We special case AUTH, as it is the only req-resp model command which we + # allow, and it must be issued on an otherwise unused connection + def auth(password) + df = @auth_df = EM::DefaultDeferrable.new + send_data(marshal('auth', password)) + return df + end + + # EM::Connection callback + def connection_completed + @connected = true + emit(:connected) + + @inactivity_checker.start + end + + # EM::Connection callback + def receive_data(data) + @inactivity_checker.activity + + @reader.feed(data) + until (reply = @reader.gets) == false + handle_response(reply) + end + end + + # EM::Connection callback + def unbind + @inactivity_checker.stop + + if @connected + emit(:disconnected) + else + emit(:connection_failed) + end + end + + protected + + COMMAND_DELIMITER = "\r\n" + + def marshal(*args) + command = [] + command << "*#{args.size}" + + args.each do |arg| + arg = arg.to_s + command << "$#{arg.to_s.bytesize}" + command << arg + end + + command.join(COMMAND_DELIMITER) + COMMAND_DELIMITER + end + + def handle_response(reply) + if @auth_df + # If we're awaiting a response to auth, we will not have sent any other commands + if reply.kind_of?(RuntimeError) + e = EM::Hiredis::RedisError.new(reply.message) + e.redis_error = reply + @auth_df.fail(e) + else + @auth_df.succeed(reply) + end + @auth_df = nil + else + type = reply[0] + if PUBSUB_MESSAGES.include?(type) + emit(type.to_sym, *reply[1..-1]) + else + EM::Hireds.logger.error("#{@name} - unrecognised response: #{reply.inspect}") + end + end + end + end +end \ No newline at end of file diff --git a/lib/em-hiredis/redis_client.rb b/lib/em-hiredis/redis_client.rb new file mode 100644 index 0000000..702c2b1 --- /dev/null +++ b/lib/em-hiredis/redis_client.rb @@ -0,0 +1,381 @@ +require 'uri' + +module EventMachine::Hiredis + # Emits the following events: + # + # - :connected - on successful connection or reconnection + # - :reconnected - on successful reconnection + # - :disconnected - no longer connected, when previously in connected state + # - :reconnect_failed(failure_number) - a reconnect attempt failed + # This event is passed number of failures so far (1,2,3...) + # - :failed - on failing the final reconnect attempt + class Client + include EventEmitter + include EventMachine::Deferrable + + attr_reader :host, :port, :password, :db + + # uri: + # the redis server to connect to, redis://[:password@]host[:port][/db] + # inactivity_trigger_secs: + # the number of seconds of inactivity before triggering a ping to the server + # inactivity_response_timeout: + # the number of seconds after a ping at which to terminate the connection + # if there is still no activity + def initialize( + uri, + inactivity_trigger_secs = nil, + inactivity_response_timeout = nil, + em = EventMachine) + + @em = em + configure(uri) + + @inactivity_trigger_secs = inactivity_trigger_secs + @inactivity_response_timeout = inactivity_response_timeout + + # Commands received while we are not initialized, to be sent once we are + @command_queue = [] + + @connection_manager = ConnectionManager.new(method(:factory_connection), em) + + @connection_manager.on(:connected) { + EM::Hiredis.logger.info("#{@name} - Connected") + emit(:connected) + set_deferred_status(:succeeded) + } + + @connection_manager.on(:disconnected) { + EM::Hiredis.logger.info("#{@name} - Disconnected") + emit(:disconnected) + } + @connection_manager.on(:reconnected) { + EM::Hiredis.logger.info("#{@name} - Reconnected") + emit(:reconnected) + } + @connection_manager.on(:reconnect_failed) { |count| + EM::Hiredis.logger.warn("#{@name} - Reconnect failed, attempt #{count}") + emit(:reconnect_failed, count) + } + + @connection_manager.on(:failed) { + EM::Hiredis.logger.error("#{@name} - Connection failed") + @command_queue.each { |df, _, _| + df.fail(EM::Hiredis::Error.new('Redis connection in failed state')) + } + @command_queue.clear + + emit(:failed) + set_deferred_status(:failed, Error.new('Could not connect after 4 attempts')) + } + end + + # Connect to the configured redis server. Returns a deferrable which + # completes upon successful connections or fails after all reconnect attempts + # are exhausted. + # + # Commands may be issued before or during connection, they will be queued + # and submitted to the server once the connection is active. + def connect + @connection_manager.connect + return self + end + + # Reconnect, either: + # - because the client has reached a failed state, but you believe the + # underlying problem to be resolved + # - with an optional different uri, because you wish to tear down the + # connection and connect to a different redis server, perhaps as part of + # a failover + def reconnect(uri = nil) + configure(uri) if uri + @connection_manager.reconnect + end + + # Terminate the client permanently + def close + @connection_manager.close + end + + ## Exposed state + + def pending_commands + @connection_manager.pending_commands + end + + def pending_commands? + return pending_commands > 0 + end + + ## Commands which require extra logic or convenience + + def select(db, &blk) + process_command('select', db, &blk).callback { + @db = db + } + end + + def auth(password, &blk) + process_command('auth', password, &blk).callback { + @password = password + } + end + + def info + df = method_missing(:info) + df.callback { |response| + info = {} + response.each_line do |line| + key, value = line.split(":", 2) + info[key.to_sym] = value.chomp if value + end + df.succeed(info) + } + df.callback { |info| yield info } if block_given? + df + end + + def info_commandstats(&blk) + hash_processor = lambda do |response| + commands = {} + response.each_line do |line| + command, data = line.split(':') + if data + c = commands[command.sub('cmdstat_', '').to_sym] = {} + data.split(',').each do |d| + k, v = d.split('=') + c[k.to_sym] = v =~ /\./ ? v.to_f : v.to_i + end + end + end + blk.call(commands) + end + method_missing(:info, 'commandstats', &hash_processor) + end + + # Commands which are not supported + + def monitor + # If the command were issued it would break the request-response model + raise 'monitor command not supported' + end + + def subscribe(*channels) + raise "Use pubsub client" + end + + def unsubscribe(*channels) + raise "Use pubsub client" + end + + def psubscribe(*pattern) + raise "Use pubsub client" + end + + def punsubscribe(*pattern) + raise "Use pubsub client" + end + + # Gives access to a richer interface for pubsub subscriptions on a + # separate redis connection + def pubsub + @pubsub ||= begin + uri = URI("redis://#{@host}:#{@port}/") + uri.password = @password if @password + PubsubClient.new(uri).connect + end + end + + # Lua script support + + def self.load_scripts_from(dir) + Dir.glob("#{dir}/*.lua").each do |f| + name = File.basename(f, '.lua') + lua = load_script(f) + EM::Hiredis.logger.debug { "Registering script: #{name}" } + EM::Hiredis::Client.register_script(name, lua) + end + end + + def self.load_script(file) + script_text = File.open(file, 'r').read + + inc_path = File.dirname(file) + while (m = /^-- #include (.*)$/.match(script_text)) + inc_file = m[1] + inc_body = File.read("#{inc_path}/#{inc_file}") + to_replace = Regexp.new("^-- #include #{inc_file}$") + script_text = script_text.gsub(to_replace, "#{inc_body}\n") + end + script_text + end + + def self.register_script(name, lua) + sha = Digest::SHA1.hexdigest(lua) + self.send(:define_method, name.to_sym) { |keys, args=[]| + eval_script(lua, sha, keys, args) + } + self.send(:define_method, "#{name}_script".to_sym) { + lua + } + self.send(:define_method, "#{name}_sha".to_sym) { + sha + } + end + + def register_script(name, lua) + sha = Digest::SHA1.hexdigest(lua) + singleton = class << self; self end + singleton.send(:define_method, name.to_sym) { |keys, args=[]| + eval_script(lua, sha, keys, args) + } + end + + def eval_script(lua, lua_sha, keys, args) + df = EM::DefaultDeferrable.new + method_missing(:evalsha, lua_sha, keys.size, *keys, *args).callback( + &df.method(:succeed) + ).errback { |e| + if e.kind_of?(RedisError) && e.redis_error.message.start_with?("NOSCRIPT") + self.eval(lua, keys.size, *keys, *args) + .callback(&df.method(:succeed)).errback(&df.method(:fail)) + else + df.fail(e) + end + } + df + end + + def ensure_script(script_name) + df = EM::DefaultDeferrable.new + method_missing( + :script, + 'exists', + self.send("#{script_name}_sha".to_sym) + ).callback { |ret| + # ret is an array of 0 or 1s representing existence for each script arg passed + if ret[0] == 0 + method_missing( + :script, + 'load', + self.send("#{script_name}_script".to_sym) + ).callback { + df.succeed + }.errback { |e| + df.fail(e) + } + else + df.succeed + end + }.errback { |e| + df.fail(e) + } + df + end + + protected + + def configure(uri_string) + uri = URI(uri_string) + + path = uri.path[1..-1] + db = path.to_i # Empty path => 0 + + @host = uri.host + @port = uri.port + @password = uri.password + @db = db + + if @name + EM::Hiredis.logger.info("#{@name} - Reconfiguring to #{uri_string}") + else + EM::Hiredis.logger.info("#{uri_string} - Configured") + end + @name = uri_string + end + + def factory_connection + df = EM::DefaultDeferrable.new + + begin + connection = @em.connect( + @host, + @port, + RedisConnection, + @inactivity_trigger_secs, + @inactivity_response_timeout, + @name + ) + + connection.on(:connected) { + maybe_auth(connection).callback { + maybe_select(connection).callback { + @command_queue.each { |df, command, args| + connection.send_command(df, command, args) + } + @command_queue.clear + + df.succeed(connection) + }.errback { |e| + # Failure to select db counts as a connection failure + connection.close_connection + df.fail(e) + } + }.errback { |e| + # Failure to auth counts as a connection failure + connection.close_connection + df.fail(e) + } + } + + connection.on(:connection_failed) { + df.fail('Connection failed') + } + rescue EventMachine::ConnectionError => e + df.fail(e) + end + + return df + end + + def process_command(command, *args, &blk) + df = EM::DefaultDeferrable.new + # Shortcut for defining the callback case with just a block + df.callback(&blk) if blk + + if @connection_manager.state == :failed + df.fail(EM::Hiredis::Error.new('Redis connection in failed state')) + elsif @connection_manager.state == :connected + @connection_manager.connection.send_command(df, command, args) + else + @command_queue << [df, command, args] + end + + return df + end + + alias_method :method_missing, :process_command + + def maybe_auth(connection) + if @password + connection.send_command(EM::DefaultDeferrable.new, 'auth', @password) + else + noop + end + end + + def maybe_select(connection) + if @db != 0 + connection.send_command(EM::DefaultDeferrable.new, 'select', @db) + else + noop + end + end + + def noop + df = EM::DefaultDeferrable.new + df.succeed + df + end + end +end diff --git a/lib/em-hiredis/redis_connection.rb b/lib/em-hiredis/redis_connection.rb new file mode 100644 index 0000000..30ba52c --- /dev/null +++ b/lib/em-hiredis/redis_connection.rb @@ -0,0 +1,103 @@ +module EventMachine::Hiredis + module RedisConnection + include EventMachine::Hiredis::EventEmitter + + def initialize(inactivity_trigger_secs = nil, + inactivity_response_timeout = 2, + name = 'unnamed connection') + + @name = name + # Parser for incoming replies + @reader = ::Hiredis::Reader.new + # Queue of deferrables awaiting replies + @response_queue = [] + + @connected = false + + @inactivity_checker = InactivityChecker.new(inactivity_trigger_secs, inactivity_response_timeout) + @inactivity_checker.on(:activity_timeout) { + EM::Hiredis.logger.debug("#{@name} - Sending ping") + send_command(EM::DefaultDeferrable.new, 'ping', []) + } + @inactivity_checker.on(:response_timeout) { + EM::Hiredis.logger.warn("#{@name} - Closing connection because of inactivity timeout") + close_connection + } + end + + def send_command(df, command, args) + @response_queue.push(df) + send_data(marshal(command, *args)) + return df + end + + def pending_responses + @response_queue.length + end + + # EM::Connection callback + def connection_completed + @connected = true + emit(:connected) + + @inactivity_checker.start + end + + # EM::Connection callback + def receive_data(data) + @inactivity_checker.activity + + @reader.feed(data) + until (reply = @reader.gets) == false + handle_response(reply) + end + end + + # EM::Connection callback + def unbind + @inactivity_checker.stop + + @response_queue.each { |df| df.fail(EM::Hiredis::Error.new('Redis connection lost')) } + @response_queue.clear + + if @connected + emit(:disconnected) + else + emit(:connection_failed) + end + end + + protected + + COMMAND_DELIMITER = "\r\n" + + def marshal(*args) + command = [] + command << "*#{args.size}" + + args.each do |arg| + arg = arg.to_s + command << "$#{arg.to_s.bytesize}" + command << arg + end + + command.join(COMMAND_DELIMITER) + COMMAND_DELIMITER + end + + def handle_response(reply) + df = @response_queue.shift + if df + if reply.kind_of?(RuntimeError) + e = EM::Hiredis::RedisError.new(reply.message) + e.redis_error = reply + df.fail(e) + else + df.succeed(reply) + end + else + emit(:replies_out_of_sync) + close_connection + end + end + end +end diff --git a/lib/em-hiredis/support/cancellable_deferrable.rb b/lib/em-hiredis/support/cancellable_deferrable.rb new file mode 100644 index 0000000..947a60c --- /dev/null +++ b/lib/em-hiredis/support/cancellable_deferrable.rb @@ -0,0 +1,61 @@ +# Wraps a deferrable and allows the switching of callbacks and errbacks +# depending on whether #cancel is called before the completion of the deferrable. +# +# Allows one to conveniently start an async operation which creates a resource +# rather than just returning a value and attach callbacks for 4 cases: +# +# callback - receive the desired resource. +# callback_cancelled - clean up if the resource was successfully created, but we +# have decided we no longer want it. +# errback - deal with failure to create the resource (retry perhaps). +# errback_cancelled - deal with failure to create the resource when we no longer +# wanted it anyway, probably do nothing. + +module EM::Hiredis + class CancellableDeferrable + def initialize(df) + @df = df + @cancelled = false + end + + def cancel + @cancelled = true + end + + def callback(&blk) + @df.callback { |*args| + unless @cancelled + blk.call(*args) + end + } + self + end + + def callback_cancelled(&blk) + @df.callback { |*args| + if @cancelled + blk.call(*args) + end + } + self + end + + def errback(&blk) + @df.errback { |*args| + unless @cancelled + blk.call(*args) + end + } + self + end + + def errback_cancelled(&blk) + @df.errback { |*args| + if @cancelled + blk.call(*args) + end + } + self + end + end +end diff --git a/lib/em-hiredis/event_emitter.rb b/lib/em-hiredis/support/event_emitter.rb similarity index 100% rename from lib/em-hiredis/event_emitter.rb rename to lib/em-hiredis/support/event_emitter.rb diff --git a/lib/em-hiredis/support/inactivity_checker.rb b/lib/em-hiredis/support/inactivity_checker.rb new file mode 100644 index 0000000..dae086b --- /dev/null +++ b/lib/em-hiredis/support/inactivity_checker.rb @@ -0,0 +1,52 @@ +module EventMachine::Hiredis + # Time inactivity and trigger ping action, triggering disconnect + # action if this does not prompt activity. + # + # If initialized with inactivity_timeout_secs = nil, does nothing + # + # Emits: + # activity_timeout: when inactive for > inactivity_timeout_secs + # response_timeout: when inactive for > response_timeout_secs after + # activity_timeout emitted + # + # Both events are emitted with the number of seconds inactive as argument + class InactivityChecker + include EventMachine::Hiredis::EventEmitter + + def initialize(inactivity_timeout_secs, response_timeout_secs, em = EM) + @em = em + if inactivity_timeout_secs + raise ArgumentError('inactivity_timeout_secs must be > 0') unless inactivity_timeout_secs > 0 + raise ArgumentError('response_timeout_secs must be > 0') unless response_timeout_secs > 0 + @inactivity_timeout_secs = inactivity_timeout_secs + @response_timeout_secs = response_timeout_secs + end + end + + def activity + @inactive_seconds = 0 + end + + def start + return unless @inactivity_timeout_secs + + @inactive_seconds = 0 + @inactivity_timer = @em.add_periodic_timer(1) { + @inactive_seconds += 1 + if @inactive_seconds > @inactivity_timeout_secs + @response_timeout_secs + emit(:response_timeout, @inactive_seconds) + @inactive_seconds = 0 # or we'll continue to fire each second + elsif @inactive_seconds > @inactivity_timeout_secs + emit(:activity_timeout, @inactive_seconds) + end + } + end + + def stop + if @inactivity_timer + @em.cancel_timer(@inactivity_timer) + @inactivity_timer = nil + end + end + end +end diff --git a/lib/em-hiredis/support/state_machine.rb b/lib/em-hiredis/support/state_machine.rb new file mode 100644 index 0000000..688b80d --- /dev/null +++ b/lib/em-hiredis/support/state_machine.rb @@ -0,0 +1,33 @@ +require 'set' + +module EventMachine::Hiredis + class StateMachine + include EventMachine::Hiredis::EventEmitter + + attr_reader :state + + def initialize + @transitions = {} + @state = :initial + @all_states = Set.new([:initial]) + end + + def transition(from, to) + @all_states.add(from) + @all_states.add(to) + @transitions[from] ||= [] + @transitions[from].push(to) + end + + def update_state(to) + raise "Invalid state #{to}" unless @all_states.include?(to) + + allowed = @transitions[@state] && @transitions[@state].include?(to) + raise "No such transition #{@state} #{to}" unless allowed + + old_state = @state + @state = to + emit(to, old_state) + end + end +end diff --git a/lib/em-hiredis/version.rb b/lib/em-hiredis/version.rb index 849cd9d..04dfbd7 100644 --- a/lib/em-hiredis/version.rb +++ b/lib/em-hiredis/version.rb @@ -1,5 +1,5 @@ module EventMachine module Hiredis - VERSION = "0.2.1" + VERSION = "1.0.0" end end diff --git a/spec/client_conn_spec.rb b/spec/client_conn_spec.rb new file mode 100644 index 0000000..126e27c --- /dev/null +++ b/spec/client_conn_spec.rb @@ -0,0 +1,203 @@ +require 'spec_helper' + +describe EM::Hiredis::Client do + default_timeout 4 + + class ClientTestConnection + include EM::Hiredis::RedisConnection + include EM::Hiredis::MockConnection + end + + # Create expected_connections connections, inject them in order in to the + # client as it creates new ones + def mock_connections(expected_connections) + em = EM::Hiredis::MockConnectionEM.new(expected_connections, ClientTestConnection) + + yield EM::Hiredis::Client.new('redis://localhost:6379/9', nil, nil, em), em.connections + + em.connections.each { |c| c._expectations_met! } + end + + it 'should queue commands issued while reconnecting' do + mock_connections(2) { |client, (conn_a, conn_b)| + # Both connections expect to receive 'select' first + # But pings 3 and 4 and issued between conn_a being disconnected + # and conn_b completing its connection + conn_a._expect('select 9') + conn_a._expect('ping 1') + conn_a._expect('ping 2') + + conn_b._expect('select 9') + conn_b._expect('ping 3') + conn_b._expect('ping 4') + + client.connect + conn_a.connection_completed + + client.ping(1) + client.ping(2) + + conn_a.unbind + + client.ping(3) + client.ping(4) + + conn_b.connection_completed + } + end + + context 'failed state' do + default_timeout 2 + + it 'should be fail queued commands when entering the state' do + mock_connections(5) { |client, connections| + client.connect + + # Queue command that will later fail + got_errback = false + client.ping.errback { |e| + e.message.should == 'Redis connection in failed state' + got_errback = true + } + + # THEN fail all connection attempts + connections.each { |c| c.unbind } + + got_errback.should == true + } + end + + it 'should be possible to recover' do + mock_connections(6) { |client, connections| + failing_connections = connections[0..4] + good_connection = connections[5] + + # Connect and fail 5 times + client.connect + failing_connections.each { |c| c.unbind } + + # We should now be in the failed state + got_errback = false + client.ping.errback { |e| + e.message.should == 'Redis connection in failed state' + got_errback = true + } + + good_connection._expect('select 9') + good_connection._expect('ping') + + # But after calling connect and completing the connection, we are functional again + client.connect + good_connection.connection_completed + + got_callback = false + client.ping.callback { + got_callback = true + } + + got_errback.should == true + got_callback.should == true + } + end + + it 'should queue commands once attempting to recover' do + mock_connections(6) { |client, connections| + failing_connections = connections[0..4] + good_connection = connections[5] + + # Connect and fail 5 times + client.connect + failing_connections.each { |c| c.unbind } + + # We sohuld now be in the failed state + got_errback = false + client.ping.errback { |e| + e.message.should == 'Redis connection in failed state' + got_errback = true + } + + good_connection._expect('select 9') + good_connection._expect('ping') + + # But after calling connect, we queue commands even though the connection + # is not yet complete + client.connect + + got_callback = false + client.ping.callback { + got_callback = true + } + + good_connection.connection_completed + + got_errback.should == true + got_callback.should == true + } + end + end + + context 'disconnects from em' do + it 'should retry when connecting' do + mock_connections(2) { |client, (conn_a, conn_b)| + connected = false + client.connect.callback { + connected = true + }.errback { + fail('Connection failed') + } + + # not connected yet + conn_a.unbind + + conn_b._expect('select 9') + conn_b.connection_completed + + connected.should == true + } + end + + it 'should retry when partially set up' do + mock_connections(2) { |client, (conn_a, conn_b)| + conn_a._expect_no_response('select 9') + + connected = false + client.connect.callback { + connected = true + } + + conn_a.connection_completed + # awaiting response to 'select' + conn_a.unbind + + conn_b._expect('select 9') + conn_b.connection_completed + + connected.should == true + } + end + + it 'should reconnect once connected' do + mock_connections(2) { |client, (conn_a, conn_b)| + conn_a._expect('select 9') + + client.connect.errback { + fail('Connection failed') + } + + reconnected = false + client.on(:reconnected) { + reconnected = true + } + + conn_a.connection_completed + # awaiting response to 'select' + conn_a.unbind + + conn_b._expect('select 9') + conn_b.connection_completed + + reconnected.should == true + } + end + end +end diff --git a/spec/client_server_spec.rb b/spec/client_server_spec.rb new file mode 100644 index 0000000..340a3a6 --- /dev/null +++ b/spec/client_server_spec.rb @@ -0,0 +1,402 @@ +require 'spec_helper' + +describe EM::Hiredis::Client do + + def recording_server(replies = {}) + em { + yield NetworkedRedisMock::RedisMock.new(replies) + } + end + + context 'initial connections' do + default_timeout 1 + + it 'should not connect on construction' do + recording_server { |server| + client = EM::Hiredis::Client.new('redis://localhost:6381/9') + server.connection_count.should == 0 + done + } + end + + it 'should be connected when connect is called' do + recording_server { |server| + client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client.connect.callback { + server.connection_count.should == 1 + done + }.errback { |e| + fail(e) + } + } + end + + it 'should issue select command before succeeding connection' do + recording_server { |server| + client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client.connect.callback { + server.connection_count.should == 1 + server.received[0].should == 'select 9' + done + }.errback { |e| + fail(e) + } + } + end + + it 'should issue select command before emitting :connected' do + recording_server { |server| + client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client.on(:connected) { + server.connection_count.should == 1 + server.received[0].should == 'select 9' + done + } + client.connect + } + end + end + + context 'disconnection' do + default_timeout 1 + + it 'should emit :disconnected when the connection disconnects' do + recording_server { |server| + client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client.on(:disconnected) { + done + } + client.connect.callback { + server.kill_connections + } + } + end + end + + context 'reconnection' do + default_timeout 1 + + it 'should create a new connection if the existing one reports it has failed' do + recording_server { |server| + client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client.connect.callback { + server.kill_connections + } + EM.add_timer(0.1) { + server.connection_count.should == 2 + done + } + } + end + + it 'should emit both connected and reconnected' do + recording_server { |server| + client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client.connect.callback { + callbacks = [] + client.on(:connected) { + callbacks.push(:connected) + if callbacks.sort == [:connected, :reconnected] + done + end + } + client.on(:reconnected) { + callbacks.push(:reconnected) + if callbacks.sort == [:connected, :reconnected] + done + end + } + + server.kill_connections + } + } + end + + context 'failing from initial connect attempt' do + default_timeout 1 + + it 'should make 4 attempts, emitting :reconnect_failed with a count' do + em { + client = EM::Hiredis::Client.new('redis://localhost:9999') # assumes nothing listening on 9999 + + expected = 1 + client.on(:reconnect_failed) { |count| + count.should == expected + expected += 1 + done if count == 4 + } + + client.connect + } + end + + it 'after 4 unsuccessful attempts should emit :failed' do + em { + client = EM::Hiredis::Client.new('redis://localhost:9999') # assumes nothing listening on 9999 + + reconnect_count = 0 + client.on(:reconnect_failed) { |count| + reconnect_count += 1 + } + client.on(:failed) { + reconnect_count.should == 4 + done + } + + client.connect + } + end + + it 'should attempt reconnect on DNS resolution failure' do + em { + client = EM::Hiredis::Client.new('redis://not-a-host:6381') # assumes not-a-host is... well, you get the idea + + reconnect_count = 0 + client.on(:reconnect_failed) { |count| + reconnect_count += 1 + } + client.on(:failed) { + reconnect_count.should == 4 + done + } + + client.connect + } + end + + it 'should recover from DNS resolution failure' do + recording_server { |server| + EM.stub(:connect).and_raise(EventMachine::ConnectionError.new) + client = EM::Hiredis::Client.new('redis://localhost:6381/9') + + client.on(:reconnect_failed) { + EM.rspec_reset + } + + client.connect + client.ping.callback { + done + } + } + end + end + + context 'failing after initially being connected' do + default_timeout 1 + + it 'should make 4 attempts, emitting :reconnect_failed with a count' do + recording_server { |server| + client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client.connect.callback { + server.stop + server.kill_connections + + expected = 1 + client.on(:reconnect_failed) { |count| + count.should == expected + expected += 1 + done if count == 4 + } + } + } + end + + it 'after 4 unsuccessful attempts should emit :failed' do + recording_server { |server| + client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client.connect.callback { + server.stop + server.kill_connections + + reconnect_count = 0 + client.on(:reconnect_failed) { |count| + reconnect_count += 1 + } + client.on(:failed) { + reconnect_count.should == 4 + done + } + } + } + end + end + + it 'should fail commands immediately when in a failed state' do + recording_server { |server| + client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client.connect.callback { + client.on(:failed) { + client.get('foo').errback { |e| + e.message.should == 'Redis connection in failed state' + done + } + } + + server.stop + server.kill_connections + } + } + end + + it 'should be possible to trigger reconnect on request' do + recording_server { |server| + client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client.connect.callback { + client.on(:reconnected) { + server.connection_count.should == 2 + done + } + + client.reconnect + } + } + end + + it 'should do something sensible???' do + recording_server { |server| + client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client.reconnect + client.ping.callback { + done + } + } + end + + it 'should keep responses matched when connection is lost' do + recording_server('get f' => '+hello') { |server| + client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client.connect.callback { + client.get('a') + client.get('b').callback { + client.get('c') + server.kill_connections + client.get('d') + client.get('e') + client.on(:reconnected) { + client.get('f').callback { |v| + v.should == 'hello' + done + } + } + } + } + } + end + end + + context 'commands' do + default_timeout 1 + + it 'should be able to send commands' do + recording_server { |server| + client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client.connect.callback { + client.set('test', 'value').callback { + done + } + } + } + end + + it 'should queue commands called before connect is called' do + recording_server { |server| + client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client.set('test', 'value').callback { + client.ping.callback { + done + } + } + + client.connect + } + end + end + + context 'db selection' do + default_timeout 1 + + it 'should support alternative dbs' do + recording_server { |server| + client = EM::Hiredis::Client.new('redis://localhost:6381/4') + client.connect.callback { + server.received.should == ['select 4'] + done + } + } + end + + it 'should execute db selection first' do + recording_server { |server| + client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client.set('test', 'value').callback { + client.ping.callback { + server.received.should == [ + 'select 9', + 'set test value', + 'ping'] + done + } + } + + client.connect + } + end + + it 'should class db selection failure as a connection failure' do + recording_server('select 9' => '-ERR no such db') { |server| + client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client.connect.errback { |e| + done + } + } + end + + it 'should re-select db on reconnection' do + recording_server { |server| + client = EM::Hiredis::Client.new('redis://localhost:6381/4') + client.connect.callback { + client.ping.callback { + client.on(:reconnected) { + client.ping.callback { + server.connection_count.should == 2 + server.received.should == [ + 'select 4', + 'ping', + 'disconnect', + 'select 4', + 'ping' + ] + done + } + } + server.kill_connections + } + } + } + end + + it 'should remember a change in the selected db' do + recording_server { |server| + client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client.connect.callback { + client.select(4).callback { + client.on(:reconnected) { + client.ping.callback { + server.connection_count.should == 2 + server.received.should == [ + 'select 9', + 'select 4', + 'disconnect', + 'select 4', + 'ping' + ] + done + } + } + server.kill_connections + } + } + } + end + end +end diff --git a/spec/connection_manager_spec.rb b/spec/connection_manager_spec.rb new file mode 100644 index 0000000..9181da1 --- /dev/null +++ b/spec/connection_manager_spec.rb @@ -0,0 +1,402 @@ +require 'spec_helper' + +# NB much of the connection manager is currently "integration tested" via +# tests on the clients. More unit tests would be beneficial if the behaviour +# evolves at all. + +describe EM::Hiredis::ConnectionManager do + + class Box + def put(callable) + @callable = callable + end + def call + @callable.call + end + end + + class RSpec::Mocks::Mock + def expect_event_registration(event) + callback = Box.new + self.should_receive(:on) do |e, &cb| + e.should == event + callback.put(cb) + end + return callback + end + end + + context 'forcing reconnection' do + it 'should be successful when disconnected from initial attempt failure' do + em = EM::Hiredis::TimeMockEventMachine.new + conn_factory = mock('connection factory') + + manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) + + initial_conn_df = EM::DefaultDeferrable.new + second_conn_df = EM::DefaultDeferrable.new + + conn_factory.should_receive(:call).and_return( + initial_conn_df, + second_conn_df + ) + + manager.connect + + initial_conn_df.fail('Testing') + manager.state.should == :disconnected + + manager.reconnect + + manager.state.should == :connecting + + # Which will succeed + conn = mock('connection') + conn.expect_event_registration(:disconnected) + second_conn_df.succeed(conn) + + manager.state.should == :connected + + # Reconnect timers should have been cancelled + em.remaining_timers.should == 0 + end + + it 'should be successful when disconnected from existing connection, triggered on disconnected' do + em = EM::Hiredis::TimeMockEventMachine.new + conn_factory = mock('connection factory') + + manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) + + initial_conn_df = EM::DefaultDeferrable.new + second_conn_df = EM::DefaultDeferrable.new + + conn_factory.should_receive(:call).and_return( + initial_conn_df, + second_conn_df + ) + + manager.connect + + initial_conn = mock('initial connection') + disconnected_callback = initial_conn.expect_event_registration(:disconnected) + initial_conn_df.succeed(initial_conn) + + manager.state.should == :connected + + manager.on(:disconnected) { + manager.state.should == :disconnected + + manager.reconnect + + manager.state.should == :connecting + + second_conn = mock('second connection') + second_conn.expect_event_registration(:disconnected) + second_conn_df.succeed(second_conn) + } + + disconnected_callback.call + + manager.state.should == :connected + + # Reconnect timers should have been cancelled + em.remaining_timers.should == 0 + end + + it 'should be successful when disconnected from existing connection, triggered on reconnect_failed' do + em = EM::Hiredis::TimeMockEventMachine.new + conn_factory = mock('connection factory') + + manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) + + initial_conn_df = EM::DefaultDeferrable.new + fail_conn_df = EM::DefaultDeferrable.new + second_conn_df = EM::DefaultDeferrable.new + + conn_factory.should_receive(:call).and_return( + initial_conn_df, + fail_conn_df, + second_conn_df + ) + + manager.connect + + initial_conn = mock('initial connection') + disconnected_callback = initial_conn.expect_event_registration(:disconnected) + initial_conn_df.succeed(initial_conn) + + manager.state.should == :connected + + manager.on(:reconnect_failed) { + manager.state.should == :disconnected + + manager.reconnect + + manager.state.should == :connecting + + second_conn = mock('second connection') + second_conn.expect_event_registration(:disconnected) + second_conn_df.succeed(second_conn) + } + + fail_conn_df.fail('Testing') + disconnected_callback.call + + manager.state.should == :connected + + # Reconnect timers should have been cancelled + em.remaining_timers.should == 0 + end + + it 'should cancel the connection in progress when already connecting' do + em = EM::Hiredis::TimeMockEventMachine.new + + conn_factory = mock('connection factory') + manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) + + initial_conn_df = EM::DefaultDeferrable.new + second_conn_df = EM::DefaultDeferrable.new + conn_factory.should_receive(:call).and_return( + initial_conn_df, + second_conn_df + ) + + manager.connect + + manager.state.should == :connecting + + manager.reconnect + + manager.state.should == :connecting + + in_progress_connection = mock('in progress connection') + # the connection in progress when we called reconnect should be + # immediately closed, because we might have reconfigured to connect to + # something different + in_progress_connection.should_receive(:close_connection) + initial_conn_df.succeed(in_progress_connection) + + # now we're trying to connect the replacement connection + manager.state.should == :connecting + + new_connection = mock('replacement connection') + new_connection.expect_event_registration(:disconnected) + second_conn_df.succeed(new_connection) + + manager.state.should == :connected + end + + it 'should reconnect again when already connecting and in-progress connection fails' do + em = EM::Hiredis::TimeMockEventMachine.new + + conn_factory = mock('connection factory') + manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) + + initial_conn_df = EM::DefaultDeferrable.new + second_conn_df = EM::DefaultDeferrable.new + conn_factory.should_receive(:call).and_return( + initial_conn_df, + second_conn_df + ) + + manager.connect + + manager.state.should == :connecting + + manager.reconnect + + # Fail the initial connection + initial_conn_df.fail('Testing') + + # now we're trying to connect the replacement connection + manager.state.should == :connecting + + new_connection = mock('replacement connection') + new_connection.expect_event_registration(:disconnected) + second_conn_df.succeed(new_connection) + + manager.state.should == :connected + end + + it 'should be successful when reconnecting' do + em = EM::Hiredis::TimeMockEventMachine.new + + conn_factory = mock('connection factory') + manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) + + initial_conn_df = EM::DefaultDeferrable.new + auto_reconnect_df = EM::DefaultDeferrable.new + manual_reconnect_df = EM::DefaultDeferrable.new + conn_factory.should_receive(:call).and_return( + initial_conn_df, + auto_reconnect_df, + manual_reconnect_df + ) + + manager.connect + + initial_connection = mock('initial connection') + disconnect_callback = initial_connection.expect_event_registration(:disconnected) + initial_conn_df.succeed(initial_connection) + + manager.state.should == :connected + + # Connection lost, automatic reconnection + disconnect_callback.call + + # Manual reconnection before automatic one is complete + manager.reconnect + + manager.state.should == :connecting + + auto_reconnect_df.fail('Testing') + + # now we're trying to connect the replacement connection + manager.state.should == :connecting + + new_connection = mock('replacement connection') + new_connection.expect_event_registration(:disconnected) + manual_reconnect_df.succeed(new_connection) + + manager.state.should == :connected + end + + it 'should be successful when connected' do + em = EM::Hiredis::TimeMockEventMachine.new + conn_factory = mock('connection factory') + + manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) + + initial_conn_df = EM::DefaultDeferrable.new + second_conn_df = EM::DefaultDeferrable.new + conn_factory.should_receive(:call).and_return( + initial_conn_df, + second_conn_df + ) + + manager.connect + + initial_conn = mock('initial connection') + disconnected_callback = initial_conn.expect_event_registration(:disconnected) + initial_conn_df.succeed(initial_conn) + + manager.state.should == :connected + + # Calling reconnect will ask the current connection to close + initial_conn.should_receive(:close_connection) + + manager.reconnect + + # Now the connection has finished closing... + disconnected_callback.call + + # ...we complete the reconnect + manager.state.should == :connecting + + second_conn = mock('second connection') + second_conn.should_receive(:on).with(:disconnected) + second_conn_df.succeed(second_conn) + + manager.state.should == :connected + + # Reconnect timers should have been cancelled + em.remaining_timers.should == 0 + end + + it 'should be successful when failed during initial connect' do + em = EM::Hiredis::TimeMockEventMachine.new + conn_factory = mock('connection factory') + + manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) + + # Five failed attempts takes us to failed, then one successful at the end + fail_conn_df = EM::DefaultDeferrable.new + succeed_conn_df = EM::DefaultDeferrable.new + + conn_factory.should_receive(:call).and_return( + fail_conn_df, + fail_conn_df, + fail_conn_df, + fail_conn_df, + fail_conn_df, + succeed_conn_df + ) + + manager.connect + + fail_conn_df.fail('Testing') + + # Go through 4 retries scheduled via timer + em.evaluate_ticks + em.evaluate_ticks + em.evaluate_ticks + em.evaluate_ticks + + manager.state.should == :failed + + manager.reconnect + + conn = mock('connection') + conn.expect_event_registration(:disconnected) + succeed_conn_df.succeed(conn) + + manager.state.should == :connected + + # Reconnect timers should have been cancelled + em.remaining_timers.should == 0 + end + + it 'should be successful when failed after initial success' do + em = EM::Hiredis::TimeMockEventMachine.new + + conn_factory = mock('connection factory') + manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) + + # Connect successfully, then five failed attempts takes us to failed, + # then one successful at the end + fail_conn_df = EM::DefaultDeferrable.new + initial_conn_df = EM::DefaultDeferrable.new + second_conn_df = EM::DefaultDeferrable.new + + conn_factory.should_receive(:call).and_return( + initial_conn_df, + fail_conn_df, + fail_conn_df, + fail_conn_df, + fail_conn_df, + second_conn_df + ) + + manager.connect + + initial_conn = mock('initial connection') + disconnected_cb = initial_conn.expect_event_registration(:disconnected) + initial_conn_df.succeed(initial_conn) + + manager.state.should == :connected + + disconnected_cb.call + + fail_conn_df.fail('Testing') + + # Go through 4 retries scheduled via timer + em.evaluate_ticks + em.evaluate_ticks + em.evaluate_ticks + em.evaluate_ticks + + manager.state.should == :failed + + manager.reconnect + + second_conn = mock('second connection') + second_conn.expect_event_registration(:disconnected) + second_conn_df.succeed(second_conn) + + manager.state.should == :connected + + # Reconnect timers should have been cancelled + em.remaining_timers.should == 0 + end + end +end diff --git a/spec/connection_spec.rb b/spec/connection_spec.rb deleted file mode 100644 index e2a5f13..0000000 --- a/spec/connection_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'spec_helper' - -describe EventMachine::Hiredis, "connecting" do - let(:replies) do - # shove db number into PING reply since redis has no way - # of exposing the currently selected DB - replies = { - :select => lambda { |db| $db = db; "+OK" }, - :ping => lambda { "+PONG #{$db}" }, - :auth => lambda { |password| $auth = password; "+OK" }, - :get => lambda { |key| $auth == "secret" ? "$3\r\nbar" : "$-1" }, - } - end - - def connect_to_mock(url, &blk) - redis_mock(replies) do - connect(1, url, &blk) - end - end - - it "doesn't call select by default" do - connect_to_mock("redis://localhost:6380/") do |redis| - redis.ping do |response| - response.should == "PONG " - done - end - end - end - - it "selects the right db" do - connect_to_mock("redis://localhost:6380/9") do |redis| - redis.ping do |response| - response.should == "PONG 9" - done - end - end - end - - it "authenticates with a password" do - connect_to_mock("redis://:secret@localhost:6380/9") do |redis| - redis.get("foo") do |response| - response.should == "bar" - done - end - end - end - - it "rejects a bad password" do - connect_to_mock("redis://:failboat@localhost:6380/9") do |redis| - redis.get("foo") do |response| - response.should be_nil - done - end - end - end -end diff --git a/spec/inactivity_check_spec.rb b/spec/inactivity_check_spec.rb deleted file mode 100644 index 5f251d4..0000000 --- a/spec/inactivity_check_spec.rb +++ /dev/null @@ -1,66 +0,0 @@ -require 'spec_helper' -require 'support/inprocess_redis_mock' - -def connect_mock(timeout = 10, url = "redis://localhost:6381", server = nil, &blk) - em(timeout) do - IRedisMock.start - redis = EventMachine::Hiredis.connect(url) - blk.call(redis) - IRedisMock.stop - end -end - -describe EM::Hiredis::BaseClient do - it "should ping after activity timeout reached" do - connect_mock do |redis| - redis.configure_inactivity_check(2, 1) - EM.add_timer(4) { - IRedisMock.received.should include("ping") - done - } - end - end - - it "should not ping before activity timeout reached" do - connect_mock do |redis| - redis.configure_inactivity_check(3, 1) - EM.add_timer(2) { - IRedisMock.received.should_not include("ping") - done - } - end - end - - it "should ping after timeout reached even though command has been sent (no response)" do - connect_mock do |redis| - redis.configure_inactivity_check(2, 1) - IRedisMock.pause # no responses from now on - - EM.add_timer(1.5) { - redis.get "test" - } - - EM.add_timer(4) { - IRedisMock.received.should include("ping") - done - } - end - end - - it "should trigger a reconnect when there's no response to ping" do - connect_mock do |redis| - redis.configure_inactivity_check(2, 1) - IRedisMock.pause # no responses from now on - - EM.add_timer(1.5) { - redis.get "test" - } - - EM.add_timer(5) { - IRedisMock.received.should include("disconnect") - done - } - end - end - -end diff --git a/spec/inactivity_checker_spec.rb b/spec/inactivity_checker_spec.rb new file mode 100644 index 0000000..08f6db5 --- /dev/null +++ b/spec/inactivity_checker_spec.rb @@ -0,0 +1,122 @@ +require 'spec_helper' + +describe EM::Hiredis::InactivityChecker do + def setup(inactivity_time, reponse_time) + em = EM::Hiredis::TimeMockEventMachine.new + subject = EM::Hiredis::InactivityChecker.new(inactivity_time, reponse_time, em) + yield(subject, em) + end + + it 'should emit after activity timeout reached' do + setup(3, 1) do |checker, em| + emitted = false + checker.on(:activity_timeout) { + emitted = true + em.current_time.should == 4 + checker.stop + } + + checker.start + em.evaluate_ticks + + emitted.should == true + end + end + + it 'should not ping before activity timeout exceeded' do + setup(4, 1) do |checker, em| + checker.on(:activity_timeout) { + fail + } + + em.add_timer(4) { + checker.stop + } + + checker.start + em.evaluate_ticks + + em.current_time.should == 4 + end end + + it 'should not ping if there is activity' do + setup(3, 1) do |checker, em| + checker.on(:activity_timeout) { + fail + } + + em.add_timer(2) { + checker.activity + } + + em.add_timer(4) { + checker.activity + } + + em.add_timer(6) { + checker.stop + } + + checker.start + em.evaluate_ticks + + em.current_time.should == 6 + end + end + + it 'should emit after response timeout exceeded' do + setup(3, 1) do |checker, em| + emitted = [] + checker.on(:activity_timeout) { + emitted << :activity_timeout + + em.current_time.should == 4 + } + checker.on(:response_timeout) { + emitted << :response_timeout + + em.current_time.should == 5 + checker.stop + } + + checker.start + em.evaluate_ticks + + emitted.should == [:activity_timeout, :response_timeout] + end + end + + it 'should emit after period of activity followed by inactivity' do + setup(3, 1) do |checker, em| + em.add_timer(2) { + checker.activity + } + + em.add_timer(4) { + checker.activity + } + + em.add_timer(6) { + checker.activity + } + + emitted = [] + checker.on(:activity_timeout) { + emitted << :activity_timeout + + em.current_time.should == 10 + } + checker.on(:response_timeout) { + emitted << :response_timeout + + em.current_time.should == 11 + checker.stop + } + + checker.start + em.evaluate_ticks + + emitted.should == [:activity_timeout, :response_timeout] + end + end +end diff --git a/spec/base_client_spec.rb b/spec/live/base_client_spec.rb similarity index 68% rename from spec/base_client_spec.rb rename to spec/live/base_client_spec.rb index f2559fe..d1a491b 100644 --- a/spec/base_client_spec.rb +++ b/spec/live/base_client_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe EM::Hiredis::BaseClient do +describe EM::Hiredis::Client do it "should be able to connect to redis (required for all tests!)" do em { redis = EM::Hiredis.connect @@ -25,28 +25,19 @@ } end end - + it "should emit an event on reconnect failure, with the retry count (DNS resolution)" do - # Assumes there is no redis server on 9999 + # Assumes there is no host for 'not-a-host' connect(1, "redis://localhost:6379/") do |redis| expected = 1 redis.on(:reconnect_failed) { |count| count.should == expected expected += 1 - done if expected == 3 + done if expected == 5 } - redis.reconnect!("redis://not-a-host:9999/") - end - end - - it "should emit disconnected when the connection closes" do - connect do |redis| - redis.on(:connected) { - redis.on(:disconnected) { - done - } - redis.close_connection + redis.callback { + redis.reconnect("redis://not-a-host:9999/") } end end @@ -66,32 +57,10 @@ end end - it "should fail commands immediately when in failed state" do - connect(1, "redis://localhost:9999/") do |redis| - redis.fail - redis.get('foo').errback { |error| - error.class.should == EM::Hiredis::Error - error.message.should == 'Redis connection in failed state' - done - } - end - end - - it "should fail queued commands when entering failed state" do - connect(1, "redis://localhost:9999/") do |redis| - redis.get('foo').errback { |error| - error.class.should == EM::Hiredis::Error - error.message.should == 'Redis connection in failed state' - done - } - redis.fail - end - end - it "should allow reconfiguring the client at runtime" do connect(1, "redis://localhost:9999/") do |redis| redis.on(:reconnect_failed) { - redis.configure("redis://localhost:6379/9") + redis.reconnect("redis://localhost:6379/9") redis.info { done } @@ -106,7 +75,7 @@ } # Wait for first connection to complete redis.callback { - redis.reconnect_connection + redis.reconnect } end end diff --git a/spec/lock_spec.rb b/spec/live/lock_spec.rb similarity index 100% rename from spec/lock_spec.rb rename to spec/live/lock_spec.rb diff --git a/spec/live/pubsub_spec.rb b/spec/live/pubsub_spec.rb new file mode 100644 index 0000000..2faf411 --- /dev/null +++ b/spec/live/pubsub_spec.rb @@ -0,0 +1,209 @@ +require 'spec_helper' + +describe EventMachine::Hiredis::PubsubClient, '(un)subscribe' do + describe "subscribing" do + + it "should run the passed block when message received" do + connect do |redis| + redis.pubsub.subscribe('channel') { |message| + message.should == 'hello' + done + } + + redis.pubsub.on(:subscribe) { + redis.publish('channel', 'hello') + } + end + end + + it "should run the passed proc when message received on channel" do + connect do |redis| + proc = Proc.new { |message| + message.should == 'hello' + done + } + redis.pubsub.subscribe('channel', proc) + + redis.pubsub.on(:subscribe) { + redis.publish('channel', 'hello') + } + end + end + end + + it "should expose raw pubsub events from redis" do + channel = 'channel' + callback_count = 0 + connect do |redis| + redis.pubsub.on(:subscribe) { |channel, subscription_count| + # 2. Get subscribe callback + callback_count += 1 + channel.should == channel + subscription_count.should == 1 + + # 3. Publish on channel + redis.publish(channel, 'foo') + } + + redis.pubsub.on(:message) { |channel, message| + # 4. Get message callback + callback_count += 1 + channel.should == channel + message.should == 'foo' + + callback_count.should == 2 + done + } + + # 1. Subscribe to channel + redis.pubsub.subscribe(channel) + end + end + + it "should resubscribe to all channels on reconnect" do + callback_count = 0 + connect do |redis| + # 1. Subscribe to channels + redis.pubsub.callback { + redis.pubsub.subscribe('channel1') { + done if (callback_count += 1) == 2 + } + redis.pubsub.subscribe('channel2') { + done if (callback_count += 1) == 2 + } + + # 2. Subscriptions complete. Now force disconnect + redis.pubsub.reconnect + + EM.add_timer(0.1) { + # 3. After giving time to reconnect publish to both channels + redis.publish('channel1', 'foo') + redis.publish('channel2', 'bar') + } + } + end + end +end + +describe EventMachine::Hiredis::PubsubClient, 'p(un)subscribe' do + describe "psubscribing" do + it "should run the passed block when message received" do + connect do |redis| + redis.pubsub.psubscribe("channel:*") { |channel, message| + channel.should == 'channel:foo' + message.should == 'hello' + done + } + + redis.pubsub.on(:psubscribe) { + redis.publish('channel:foo', 'hello') + } + end + end + + it "should run the passed proc when message received on channel" do + connect do |redis| + proc = Proc.new { |channel, message| + channel.should == 'channel:foo' + message.should == 'hello' + done + } + redis.pubsub.psubscribe("channel:*", proc) + redis.publish('channel:foo', 'hello') + end + end + end + + describe "punsubscribing" do + it "should allow punsubscribing a single callback without punsubscribing from redis" do + connect do |redis| + proc1 = Proc.new { |channel, message| fail } + proc2 = Proc.new { |channel, message| + channel.should == 'channel:foo' + message.should == 'hello' + done + } + redis.pubsub.psubscribe('channel:*', proc1) + redis.pubsub.psubscribe('channel:*', proc2) + redis.pubsub.punsubscribe_proc('channel:*', proc1) + redis.publish('channel:foo', 'hello') + end + end + + it "should allow punsubscribing from redis channel, including all callbacks, and return deferrable for redis punsubscribe" do + connect do |redis| + # Raw pubsub event + redis.pubsub.on('pmessage') { |pattern, channel, message| fail } + # Block subscription + redis.pubsub.psubscribe('channel') { |c, m| fail } # block + # Proc example + redis.pubsub.psubscribe('channel', Proc.new { |c, m| fail }) + + redis.pubsub.punsubscribe('channel') + + redis.publish('channel', 'hello').callback { + EM.add_timer(0.1) { + done + } + } + end + end + end + + it "should expose raw pattern pubsub events from redis" do + callback_count = 0 + connect do |redis| + redis.pubsub.on(:psubscribe) { |pattern, subscription_count| + # 2. Get subscribe callback + callback_count += 1 + pattern.should == "channel:*" + subscription_count.should == 1 + + # 3. Publish on channel + redis.publish('channel:foo', 'foo') + } + + redis.pubsub.on(:pmessage) { |pattern, channel, message| + # 4. Get message callback + callback_count += 1 + pattern.should == 'channel:*' + channel.should == 'channel:foo' + message.should == 'foo' + + callback_count.should == 2 + done + } + + # 1. Subscribe to channel + redis.pubsub.psubscribe('channel:*') + end + end + + it "should resubscribe to all pattern subscriptions on reconnect" do + callback_count = 0 + connect do |redis| + redis.pubsub.callback { + # 1. Subscribe to channels + redis.pubsub.psubscribe('foo:*') { |channel, message| + channel.should == 'foo:a' + message.should == 'hello foo' + done if (callback_count += 1) == 2 + } + redis.pubsub.psubscribe('bar:*') { |channel, message| + channel.should == 'bar:b' + message.should == 'hello bar' + done if (callback_count += 1) == 2 + } + + # 2. Subscriptions complete. Now force disconnect + redis.pubsub.reconnect + + EM.add_timer(0.1) { + # 3. After giving time to reconnect publish to both channels + redis.publish('foo:a', 'hello foo') + redis.publish('bar:b', 'hello bar') + } + } + end + end +end diff --git a/spec/live_redis_protocol_spec.rb b/spec/live/redis_commands_more_spec.rb similarity index 97% rename from spec/live_redis_protocol_spec.rb rename to spec/live/redis_commands_more_spec.rb index 6fb6124..5e7951d 100644 --- a/spec/live_redis_protocol_spec.rb +++ b/spec/live/redis_commands_more_spec.rb @@ -496,7 +496,9 @@ def set(&blk) it "select previously selected dataset" do connect(3) do |redis| #simulate disconnect - redis.set('foo', 'a') { redis.close_connection_after_writing } + redis.set('foo', 'a') { + redis.instance_variable_get(:@connection_manager).connection.close_connection_after_writing + } EventMachine.add_timer(2) do redis.get('foo') do |r| @@ -515,12 +517,12 @@ def set(&blk) it "should fail deferred commands" do errored = false connect do |redis| - redis.on(:connected) { + redis.callback { op = redis.blpop 'empty_list' op.callback { fail } op.errback { done } - redis.close_connection + redis.instance_variable_get(:@connection_manager).connection.close_connection } EM.add_timer(1) { fail } end diff --git a/spec/redis_commands_spec.rb b/spec/live/redis_commands_spec.rb similarity index 98% rename from spec/redis_commands_spec.rb rename to spec/live/redis_commands_spec.rb index 51a91a5..b8a6ee0 100644 --- a/spec/redis_commands_spec.rb +++ b/spec/live/redis_commands_spec.rb @@ -833,29 +833,6 @@ def set(&blk) end end -describe EventMachine::Hiredis, "monitor" do - it "returns monitored commands" do - connect do |redis| - # 1. Create 2nd connection to send traffic to monitor - redis2 = EventMachine::Hiredis.connect("redis://localhost:6379/") - redis2.callback { - # 2. Monitor after command has connected - redis.monitor do |reply| - reply.should == "OK" - - # 3. Command which should show up in monitor output - redis2.get('foo') - end - } - - redis.on(:monitor) do |line| - line.should =~ /foo/ - done - end - end - end -end - describe EventMachine::Hiredis, "sorting" do context "with some simple sorting data" do def set(&blk) diff --git a/spec/pubsub_client_conn_spec.rb b/spec/pubsub_client_conn_spec.rb new file mode 100644 index 0000000..66129c9 --- /dev/null +++ b/spec/pubsub_client_conn_spec.rb @@ -0,0 +1,297 @@ +require 'spec_helper' + +describe EM::Hiredis::PubsubClient do + + class PubsubTestConnection + include EM::Hiredis::PubsubConnection + include EM::Hiredis::MockConnection + end + + # Create expected_connections connections, inject them in order in to the + # client as it creates new ones + def mock_connections(expected_connections, uri = 'redis://localhost:6379') + em = EM::Hiredis::MockConnectionEM.new(expected_connections, PubsubTestConnection) + + yield EM::Hiredis::PubsubClient.new(uri, nil, nil, em), em.connections + + em.connections.each { |c| c._expectations_met! } + end + + context '(un)subscribing' do + it "should unsubscribe all callbacks for a channel on unsubscribe" do + mock_connections(1) do |client, (connection)| + client.connect + connection.connection_completed + + connection._expect_pubsub('subscribe channel') + connection._expect_pubsub('unsubscribe channel') + + # Block subscription + client.subscribe('channel') { |m| fail } + # Proc example + client.subscribe('channel', Proc.new { |m| fail }) + + client.unsubscribe('channel') + connection.emit(:message, 'channel', 'hello') + end + end + + it "should allow selective unsubscription" do + mock_connections(1) do |client, (connection)| + client.connect + connection.connection_completed + + connection._expect_pubsub('subscribe channel') + + received_messages = 0 + + # Block subscription + client.subscribe('channel') { |m| received_messages += 1 } # block + # Proc example (will be unsubscribed again before message is sent) + proc = Proc.new { |m| fail } + client.subscribe('channel', proc) + + client.unsubscribe_proc('channel', proc) + connection.emit(:message, 'channel', 'hello') + + received_messages.should == 1 + end + end + + it "should unsubscribe from redis when all subscriptions for a channel are unsubscribed" do + mock_connections(1) do |client, (connection)| + client.connect + connection.connection_completed + + connection._expect_pubsub('subscribe channel') + + proc_a = Proc.new { |m| fail } + client.subscribe('channel', proc_a) + proc_b = Proc.new { |m| fail } + client.subscribe('channel', proc_b) + + # Unsubscribe first + client.unsubscribe_proc('channel', proc_a) + + # Unsubscribe second, should unsubscribe in redis + connection._expect_pubsub('unsubscribe channel') + client.unsubscribe_proc('channel', proc_b) + + # Check callbacks were removed + connection.emit(:message, 'channel', 'hello') + end + end + + it "should punsubscribe all callbacks for a pattern on punsubscribe" do + mock_connections(1) do |client, (connection)| + client.connect + connection.connection_completed + + connection._expect_pubsub('psubscribe channel:*') + + # Block subscription + client.psubscribe('channel:*') { |m| fail } + # Proc example + client.psubscribe('channel:*', Proc.new { |m| fail }) + + connection._expect_pubsub('punsubscribe channel:*') + client.punsubscribe('channel:*') + + connection.emit(:pmessage, 'channel:*', 'channel:hello', 'hello') + end + end + + it "should allow selective punsubscription" do + mock_connections(1) do |client, (connection)| + client.connect + connection.connection_completed + + connection._expect_pubsub('psubscribe channel:*') + + received_messages = 0 + + # Block subscription + client.psubscribe('channel:*') { |m| received_messages += 1 } + # Proc example + proc = Proc.new { |m| fail } + client.psubscribe('channel:*', proc) + + client.punsubscribe_proc('channel:*', proc) + connection.emit(:pmessage, 'channel:*', 'channel:hello', 'hello') + + received_messages.should == 1 + end + end + + it "should punsubscribe from redis when all psubscriptions for a pattern are punsubscribed" do + mock_connections(1) do |client, (connection)| + client.connect + connection.connection_completed + + connection._expect_pubsub('psubscribe channel:*') + + proc_a = Proc.new { |m| fail } + client.psubscribe('channel:*', proc_a) + proc_b = Proc.new { |m| fail } + client.psubscribe('channel:*', proc_b) + + # Unsubscribe first + client.punsubscribe_proc('channel:*', proc_a) + + # Unsubscribe second, should unsubscribe in redis + connection._expect_pubsub('punsubscribe channel:*') + client.punsubscribe_proc('channel:*', proc_b) + + # Check callbacks were removed + connection.emit(:pmessage, 'channel:*', 'channel:hello', 'hello') + end + end + end + + context 'reconnection' do + it 'should resubscribe all existing on reconnection' do + mock_connections(2) do |client, (conn_a, conn_b)| + client.connect + conn_a.connection_completed + + channels = %w{foo bar baz} + patterns = %w{foo:* bar:*:baz} + + received_subs = [] + + # Make some subscriptions to various channels and patterns + channels.each do |c| + conn_a._expect_pubsub("subscribe #{c}") + client.subscribe(c) { |message| + received_subs << c + } + end + + patterns.each do |p| + conn_a._expect_pubsub("psubscribe #{p}") + client.psubscribe(p) { |channel, message| + received_subs << p + } + end + + # Check that those subscriptions receive messages + channels.each do |c| + conn_a.emit(:message, c, 'message content') + received_subs.select { |e| e == c }.length.should == 1 + end + + patterns.each do |p| + channel = p.gsub('*', 'test') + conn_a.emit(:pmessage, p, channel, 'message content') + received_subs.select { |e| e == p }.length.should == 1 + end + + # Trigger a reconnection + conn_a.unbind + + # All subs previously made should be re-made + conn_b._expect_pubsub("subscribe #{channels.join(' ')}") + conn_b._expect_pubsub("psubscribe #{patterns.join(' ')}") + + conn_b.connection_completed + + # Check the callbacks are still attached correctly + channels.each do |c| + conn_b.emit(:message, c, 'message content') + received_subs.select { |e| e == c }.length.should == 2 + end + + patterns.each do |p| + channel = p.gsub('*', 'test') + conn_b.emit(:pmessage, p, channel, 'message content') + received_subs.select { |e| e == p }.length.should == 2 + end + + end + end + end + + context 'auth' do + it 'should auth if password provided' do + mock_connections(1, 'redis://:mypass@localhost:6379') do |client, (connection)| + connection._expect('auth mypass') + + connected = false + client.connect.callback { + connected = true + } + connection.connection_completed + + connected.should == true + end + end + + it 'should issue pubsub commands as usual after authentication' do + mock_connections(1, 'redis://:mypass@localhost:6379') do |client, (connection)| + connection._expect('auth mypass') + + connected = false + client.connect.callback { + connected = true + } + connection.connection_completed + connected.should == true + + connection._expect_pubsub('subscribe channel') + + message_received = nil + # Block subscription + client.subscribe('channel') { |m| + message_received = m + } + + connection.emit(:message, 'channel', 'hello') + + message_received.should == 'hello' + end + end + + it 'should issue pubsub commands issued before connection completion after authentication' do + mock_connections(1, 'redis://:mypass@localhost:6379') do |client, (connection)| + connection._expect('auth mypass') + + connected = false + client.connect.callback { + connected = true + } + + connection._expect_pubsub('subscribe channel') + + message_received = nil + # Block subscription + client.subscribe('channel') { |m| + message_received = m + } + + connection.connection_completed + connected.should == true + + connection.emit(:message, 'channel', 'hello') + + message_received.should == 'hello' + end + end + + it 'should reconnect if auth command fails' do + mock_connections(2, 'redis://:mypass@localhost:6379') do |client, (conn_a, conn_b)| + conn_a._expect('auth mypass', RuntimeError.new('OOPS')) + conn_b._expect('auth mypass') + + connected = false + client.connect.callback { + connected = true + } + conn_a.connection_completed + connected.should == false + + conn_b.connection_completed + connected.should == true + end + end + end +end diff --git a/spec/pubsub_connection_spec.rb b/spec/pubsub_connection_spec.rb new file mode 100644 index 0000000..9b97380 --- /dev/null +++ b/spec/pubsub_connection_spec.rb @@ -0,0 +1,209 @@ +require 'spec_helper' + +describe EM::Hiredis::PubsubConnection do + + class TestPubsubConnection + include EM::Hiredis::PubsubConnection + + attr_accessor :sent, :closed + + def send_data(data) + @sent ||= [] + @sent << data + end + + def close_connection + @closed = true + end + end + + it 'should marshall command to send' do + con = TestPubsubConnection.new + con.send_command('subscribe', 'test') + con.sent[0].should == "*2\r\n$9\r\nsubscribe\r\n$4\r\ntest\r\n" + end + + it 'should emit subscribe responses as they arrive' do + con = TestPubsubConnection.new + con.connection_completed + + received = false + con.on(:subscribe) { |channel| + channel.should == 'test' + received = true + } + + con.receive_data("*2\r\n$9\r\nsubscribe\r\n$4\r\ntest\r\n") + received.should == true + end + + it 'should emit unsubscribe responses as they arrive' do + con = TestPubsubConnection.new + con.connection_completed + + received = false + con.on(:unsubscribe) { |channel| + channel.should == 'test' + received = true + } + + con.receive_data("*2\r\n$11\r\nunsubscribe\r\n$4\r\ntest\r\n") + received.should == true + end + + it 'should emit psubscribe responses as they arrive' do + con = TestPubsubConnection.new + con.connection_completed + + received = false + con.on(:psubscribe) { |channel| + channel.should == 'test:*' + received = true + } + + con.receive_data("*2\r\n$10\r\npsubscribe\r\n$6\r\ntest:*\r\n") + received.should == true + end + + it 'should emit punsubscribe responses as they arrive' do + con = TestPubsubConnection.new + con.connection_completed + + received = false + con.on(:punsubscribe) { |channel| + channel.should == 'test:*' + received = true + } + + con.receive_data("*2\r\n$12\r\npunsubscribe\r\n$6\r\ntest:*\r\n") + received.should == true + end + + it 'should emit messages as they arrive' do + con = TestPubsubConnection.new + con.connection_completed + + received = false + con.on(:message) { |channel, message| + channel.should == 'test' + message.should == 'my message' + received = true + } + + con.receive_data("*3\r\n$7\r\nmessage\r\n$4\r\ntest\r\n$10\r\nmy message\r\n") + received.should == true + end + + it 'should emit pmessages as they arrive' do + con = TestPubsubConnection.new + con.connection_completed + + received = false + con.on(:pmessage) { |pattern, channel, message| + pattern.should == 'test*' + channel.should == 'test' + received = true + } + + con.receive_data("*4\r\n$8\r\npmessage\r\n$5\r\ntest*\r\n$4\r\ntest\r\n$10\r\nmy message\r\n") + received.should == true + end + + it 'should emit :disconnected when the connection closes' do + con = TestPubsubConnection.new + con.connection_completed + + emitted = false + con.on(:disconnected) { + emitted = true + } + + con.unbind + emitted.should == true + end + + context 'inactivity checks' do + default_timeout 5 + + it 'should fire after an initial period of inactivity' do + em { + con = TestPubsubConnection.new(1, 1) + con.connection_completed + + EM.add_timer(3) { + con.sent.should include("*2\r\n$9\r\nsubscribe\r\n$17\r\n__em-hiredis-ping\r\n") + done + } + } + end + + it 'should not fire after activity' do + em { + con = TestPubsubConnection.new(1, 1) + con.connection_completed + + EM.add_timer(1.5) { + con.send_command('subscribe', 'test') + con.receive_data("*2\r\n$9\r\nsubscribe\r\n$4\r\ntest\r\n") + } + + EM.add_timer(3) { + con.sent.should_not include("*2\r\n$9\r\nsubscribe\r\n$17\r\n__em-hiredis-ping\r\n") + done + } + } + end + + it 'should fire after a later period of inactivity' do + em { + con = TestPubsubConnection.new(1, 1) + con.connection_completed + + EM.add_timer(1.5) { + con.send_command('subscribe', 'test') + con.receive_data("*2\r\n$9\r\nsubscribe\r\n$4\r\ntest\r\n") + } + + EM.add_timer(3) { + con.sent.should_not include("*2\r\n$9\r\nsubscribe\r\n$17\r\n__em-hiredis-ping\r\n") + } + + EM.add_timer(4) { + con.sent.should include("*2\r\n$9\r\nsubscribe\r\n$17\r\n__em-hiredis-ping\r\n") + done + } + } + end + + it 'should close the connection if inactivity persists' do + em { + con = TestPubsubConnection.new(1, 1) + con.connection_completed + + EM.add_timer(4) { + con.sent.should include("*2\r\n$9\r\nsubscribe\r\n$17\r\n__em-hiredis-ping\r\n") + con.closed.should == true + done + } + } + end + + it 'should not close the connection if there is activity after ping' do + em { + con = TestPubsubConnection.new(1, 1) + con.connection_completed + + EM.add_timer(2.5) { + con.send_command('subscribe', 'test') + con.receive_data("*2\r\n$9\r\nsubscribe\r\n$4\r\ntest\r\n") + } + + EM.add_timer(4) { + con.sent.should include("*2\r\n$9\r\nsubscribe\r\n$17\r\n__em-hiredis-ping\r\n") + con.closed.should_not == true + done + } + } + end + end +end diff --git a/spec/pubsub_spec.rb b/spec/pubsub_spec.rb deleted file mode 100644 index 214b71a..0000000 --- a/spec/pubsub_spec.rb +++ /dev/null @@ -1,314 +0,0 @@ -require 'spec_helper' - -describe EventMachine::Hiredis::PubsubClient, '(un)subscribe' do - describe "subscribing" do - it "should return deferrable which succeeds with subscribe call result" do - connect do |redis| - df = redis.pubsub.subscribe("channel") { } - df.should be_kind_of(EventMachine::DefaultDeferrable) - df.callback { |subscription_count| - # Subscribe response from redis - indicates that subscription has - # succeeded and that the current connection has a single - # subscription - subscription_count.should == 1 - done - } - end - end - - it "should run the passed block when message received" do - connect do |redis| - redis.pubsub.subscribe("channel") { |message| - message.should == 'hello' - done - }.callback { - redis.publish('channel', 'hello') - } - end - end - - it "should run the passed proc when message received on channel" do - connect do |redis| - proc = Proc.new { |message| - message.should == 'hello' - done - } - redis.pubsub.subscribe("channel", proc).callback { - redis.publish('channel', 'hello') - } - end - end - end - - describe "unsubscribing" do - it "should allow unsubscribing a single callback without unsubscribing from redis" do - connect do |redis| - proc1 = Proc.new { |message| fail } - proc2 = Proc.new { |message| - message.should == 'hello' - done - } - redis.pubsub.subscribe("channel", proc1) - redis.pubsub.subscribe("channel", proc2).callback { - redis.pubsub.unsubscribe_proc("channel", proc1) - redis.publish("channel", "hello") - } - end - end - - it "should unsubscribe from redis on last proc unsubscription" do - connect do |redis| - proc = Proc.new { |message| } - redis.pubsub.subscribe("channel", proc).callback { |subs_count| - subs_count.should == 1 - redis.pubsub.unsubscribe_proc("channel", proc).callback { - # Slightly awkward way to check that unsubscribe happened: - redis.pubsub.subscribe('channel2').callback { |count| - # If count is 1 this implies that channel unsubscribed - count.should == 1 - done - } - } - } - end - end - - it "should allow unsubscribing from redis channel, including all callbacks, and return deferrable for redis unsubscribe" do - connect do |redis| - # Raw pubsub event - redis.pubsub.on('message') { |channel, message| fail } - # Block subscription - redis.pubsub.subscribe("channel") { |m| fail } # block - # Proc example - df = redis.pubsub.subscribe("channel", Proc.new { |m| fail }) - - df.callback { - redis.pubsub.unsubscribe("channel").callback { |remaining_subs| - remaining_subs.should == 0 - redis.publish("channel", "hello") { - done - } - } - } - end - end - end - - it "should expose raw pubsub events from redis" do - channel = "channel" - callback_count = 0 - connect do |redis| - redis.pubsub.on(:subscribe) { |channel, subscription_count| - # 2. Get subscribe callback - callback_count += 1 - channel.should == channel - subscription_count.should == 1 - - # 3. Publish on channel - redis.publish(channel, 'foo') - } - - redis.pubsub.on(:message) { |channel, message| - # 4. Get message callback - callback_count += 1 - channel.should == channel - message.should == 'foo' - - callback_count.should == 2 - done - } - - # 1. Subscribe to channel - redis.pubsub.subscribe(channel) - end - end - - it "should resubscribe to all channels on reconnect" do - callback_count = 0 - connect do |redis| - # 1. Subscribe to channels - redis.pubsub.subscribe('channel1') { - callback_count += 1 - } - redis.pubsub.subscribe('channel2') { - callback_count += 1 - EM.next_tick { - # 4. Success if both messages have been received - callback_count.should == 2 - done - } - }.callback { |subscription_count| - subscription_count.should == 2 - # 2. Subscriptions complete. Now force disconnect - redis.pubsub.instance_variable_get(:@connection).close_connection - - EM.add_timer(0.1) { - # 3. After giving time to reconnect publish to both channels - redis.publish('channel1', 'foo') - redis.publish('channel2', 'bar') - } - - } - - end - end -end - -describe EventMachine::Hiredis::PubsubClient, 'p(un)subscribe' do - describe "psubscribing" do - it "should return deferrable which succeeds with psubscribe call result" do - connect do |redis| - df = redis.pubsub.psubscribe("channel") { } - df.should be_kind_of(EventMachine::DefaultDeferrable) - df.callback { |subscription_count| - # Subscribe response from redis - indicates that subscription has - # succeeded and that the current connection has a single - # subscription - subscription_count.should == 1 - done - } - end - end - - it "should run the passed block when message received" do - connect do |redis| - redis.pubsub.psubscribe("channel:*") { |channel, message| - channel.should == 'channel:foo' - message.should == 'hello' - done - }.callback { - redis.publish('channel:foo', 'hello') - } - end - end - - it "should run the passed proc when message received on channel" do - connect do |redis| - proc = Proc.new { |channel, message| - channel.should == 'channel:foo' - message.should == 'hello' - done - } - redis.pubsub.psubscribe("channel:*", proc).callback { - redis.publish('channel:foo', 'hello') - } - end - end - end - - describe "punsubscribing" do - it "should allow punsubscribing a single callback without punsubscribing from redis" do - connect do |redis| - proc1 = Proc.new { |channel, message| fail } - proc2 = Proc.new { |channel, message| - channel.should == 'channel:foo' - message.should == 'hello' - done - } - redis.pubsub.psubscribe("channel:*", proc1) - redis.pubsub.psubscribe("channel:*", proc2).callback { - redis.pubsub.punsubscribe_proc("channel:*", proc1) - redis.publish("channel:foo", "hello") - } - end - end - - it "should punsubscribe from redis on last proc punsubscription" do - connect do |redis| - proc = Proc.new { |message| } - redis.pubsub.psubscribe("channel:*", proc).callback { |subs_count| - subs_count.should == 1 - redis.pubsub.punsubscribe_proc("channel:*", proc).callback { - # Slightly awkward way to check that unsubscribe happened: - redis.pubsub.psubscribe('channel2').callback { |count| - # If count is 1 this implies that channel unsubscribed - count.should == 1 - done - } - } - } - end - end - - it "should allow punsubscribing from redis channel, including all callbacks, and return deferrable for redis punsubscribe" do - connect do |redis| - # Raw pubsub event - redis.pubsub.on('pmessage') { |pattern, channel, message| fail } - # Block subscription - redis.pubsub.psubscribe("channel") { |c, m| fail } # block - # Proc example - df = redis.pubsub.psubscribe("channel", Proc.new { |c, m| fail }) - - df.callback { - redis.pubsub.punsubscribe("channel").callback { |remaining_subs| - remaining_subs.should == 0 - redis.publish("channel", "hello") { - done - } - } - } - end - end - end - - it "should expose raw pattern pubsub events from redis" do - callback_count = 0 - connect do |redis| - redis.pubsub.on(:psubscribe) { |pattern, subscription_count| - # 2. Get subscribe callback - callback_count += 1 - pattern.should == "channel:*" - subscription_count.should == 1 - - # 3. Publish on channel - redis.publish('channel:foo', 'foo') - } - - redis.pubsub.on(:pmessage) { |pattern, channel, message| - # 4. Get message callback - callback_count += 1 - pattern.should == 'channel:*' - channel.should == 'channel:foo' - message.should == 'foo' - - callback_count.should == 2 - done - } - - # 1. Subscribe to channel - redis.pubsub.psubscribe('channel:*') - end - end - - it "should resubscribe to all pattern subscriptions on reconnect" do - callback_count = 0 - connect do |redis| - # 1. Subscribe to channels - redis.pubsub.psubscribe('foo:*') { |channel, message| - channel.should == 'foo:a' - message.should == 'hello foo' - callback_count += 1 - } - redis.pubsub.psubscribe('bar:*') { |channel, message| - channel.should == 'bar:b' - message.should == 'hello bar' - callback_count += 1 - EM.next_tick { - # 4. Success if both messages have been received - callback_count.should == 2 - done - } - }.callback { |subscription_count| - subscription_count.should == 2 - # 2. Subscriptions complete. Now force disconnect - redis.pubsub.instance_variable_get(:@connection).close_connection - - EM.add_timer(0.1) { - # 3. After giving time to reconnect publish to both channels - redis.publish('foo:a', 'hello foo') - redis.publish('bar:b', 'hello bar') - } - } - end - end -end diff --git a/spec/redis_connection_spec.rb b/spec/redis_connection_spec.rb new file mode 100644 index 0000000..c05a094 --- /dev/null +++ b/spec/redis_connection_spec.rb @@ -0,0 +1,215 @@ +require 'spec_helper' + +describe EM::Hiredis::RedisConnection do + + class TestRedisConnection + include EM::Hiredis::RedisConnection + + attr_accessor :sent, :closed + + def send_data(data) + @sent ||= [] + @sent << data + end + + def close_connection + @closed = true + end + end + + it 'should marshall command to send' do + con = TestRedisConnection.new + con.send_command(EM::DefaultDeferrable.new, 'set', ['x', 'true']) + con.sent[0].should == "*3\r\n$3\r\nset\r\n$1\r\nx\r\n$4\r\ntrue\r\n" + end + + it 'should succeed deferrable when response arrives' do + con = TestRedisConnection.new + con.connection_completed + + df = mock + con.send_command(df, 'set', ['x', 'true']) + + df.should_receive(:succeed) + + con.receive_data("+OK\r\n") + end + + it 'should succeed deferrables in order responses arrive' do + con = TestRedisConnection.new + con.connection_completed + + df_a = mock + df_b = mock + con.send_command(df_a, 'set', ['x', 'true']) + con.send_command(df_b, 'set', ['x', 'true']) + + responses = [] + df_a.should_receive(:succeed) { + responses << :a + } + df_b.should_receive(:succeed) { + responses << :b + responses.should == [:a, :b] + } + + con.receive_data("+OK\r\n") + con.receive_data("+OK\r\n") + end + + it 'should pass response args to succeeded deferrable' do + con = TestRedisConnection.new + con.connection_completed + + df = mock + con.send_command(df, 'get', ['x']) + + df.should_receive(:succeed).with('true') + + con.receive_data("$4\r\ntrue\r\n") + end + + it 'should fail deferrable on error response' do + con = TestRedisConnection.new + con.connection_completed + + df = mock + con.send_command(df, 'bar', ['x']) + + df.should_receive(:fail) { |e| + e.class.should == EM::Hiredis::RedisError + e.message.should == 'ERR bad command' + } + + con.receive_data("-ERR bad command\r\n") + end + + it 'should close the connection if replies are out of sync' do + con = TestRedisConnection.new + con.connection_completed + + df = mock + con.send_command(df, 'get', ['x']) + + df.should_receive(:succeed).with('true') + + con.receive_data("$4\r\ntrue\r\n") + con.receive_data("$19\r\ndidn't ask for this\r\n") + + con.closed.should == true + end + + it 'should emit :disconnected when the connection closes' do + con = TestRedisConnection.new + con.connection_completed + + emitted = false + con.on(:disconnected) { + emitted = true + } + + con.unbind + emitted.should == true + end + + it 'fail all pending responses when the connection closes' do + con = TestRedisConnection.new + con.connection_completed + + dfs = [mock, mock, mock] + dfs.each do |df| + con.send_command(df, 'get', ['x']) + df.should_receive(:fail) { |e| + e.class.should == EM::Hiredis::Error + e.message.should == 'Redis connection lost' + } + end + + con.unbind + end + + context 'inactivity checks' do + default_timeout 5 + + it 'should fire after an initial period of inactivity' do + em { + con = TestRedisConnection.new(1, 1) + con.connection_completed + + EM.add_timer(3) { + con.sent.should include("*1\r\n$4\r\nping\r\n") + done + } + } + end + + it 'should not fire after activity' do + em { + con = TestRedisConnection.new(1, 1) + con.connection_completed + + EM.add_timer(1.5) { + con.send_command(EM::DefaultDeferrable.new, 'get', ['x']) + con.receive_data("*1\r\n$4\r\ntest\r\n") + } + + EM.add_timer(3) { + con.sent.should_not include("*1\r\n$4\r\nping\r\n") + done + } + } + end + + it 'should fire after a later period of inactivity' do + em { + con = TestRedisConnection.new(1, 1) + con.connection_completed + + EM.add_timer(1.5) { + con.send_command(EM::DefaultDeferrable.new, 'get', ['x']) + con.receive_data("*1\r\n$4\r\ntest\r\n") + } + + EM.add_timer(3) { + con.sent.should_not include("*1\r\n$4\r\nping\r\n") + } + + EM.add_timer(4) { + con.sent.should include("*1\r\n$4\r\nping\r\n") + done + } + } + end + + it 'should close the connection if inactivity persists' do + em { + con = TestRedisConnection.new(1, 1) + con.connection_completed + + EM.add_timer(4) { + con.sent.should include("*1\r\n$4\r\nping\r\n") + con.closed.should == true + done + } + } + end + + it 'should not close the connection if there is activity after ping' do + em { + con = TestRedisConnection.new(1, 1) + con.connection_completed + + EM.add_timer(2.5) { + con.send_command(EM::DefaultDeferrable.new, 'get', ['x']) + con.receive_data("*1\r\n$4\r\ntest\r\n") + } + + EM.add_timer(4) { + con.sent.should include("*1\r\n$4\r\nping\r\n") + con.closed.should_not == true + done + } + } + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c23c99a..a128fb7 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,15 +3,19 @@ require 'rspec' require 'em-spec/rspec' +require 'support/mock_connection' require 'support/connection_helper' -require 'support/redis_mock' +require 'support/networked_redis_mock' +require 'support/time_mock_eventmachine' require 'stringio' RSpec.configure do |config| config.include ConnectionHelper config.include EventMachine::SpecHelper - config.include RedisMock::Helper end # This speeds the tests up a bit EM::Hiredis.reconnect_timeout = 0.01 + +# Keep the tests quiet, decrease the level to investigate failures +EM::Hiredis.logger.level = Logger::FATAL \ No newline at end of file diff --git a/spec/support/connection_helper.rb b/spec/support/connection_helper.rb index 8309051..85e66f5 100644 --- a/spec/support/connection_helper.rb +++ b/spec/support/connection_helper.rb @@ -1,11 +1,11 @@ module ConnectionHelper # Use db 9 for tests to avoid flushing the main db # It would be nice if there was a standard db number for testing... - def connect(timeout = 1, url = "redis://localhost:6379/9", &blk) - em(timeout) do - redis = EventMachine::Hiredis.connect(url) + def connect(timeout = 1, uri = 'redis://localhost:6379/9') + em(timeout) { + redis = EM::Hiredis.connect(uri) redis.flushdb - blk.call(redis) - end + yield redis + } end end diff --git a/spec/support/inprocess_redis_mock.rb b/spec/support/inprocess_redis_mock.rb deleted file mode 100644 index 2210396..0000000 --- a/spec/support/inprocess_redis_mock.rb +++ /dev/null @@ -1,83 +0,0 @@ -module IRedisMock - def self.start(replies = {}) - @sig = EventMachine::start_server("127.0.0.1", 6381, Connection) - @received = [] - @replies = replies - @paused = false - end - - def self.stop - EventMachine::stop_server(@sig) - end - - def self.received - @received ||= [] - end - - def self.pause - @paused = true - end - def self.unpause - @paused = false - end - - def self.paused - @paused - end - - def self.replies - @replies - end - - class Connection < EventMachine::Connection - def initialize - @data = "" - @parts = [] - end - - def unbind - IRedisMock.received << 'disconnect' - end - - def receive_data(data) - @data << data - - while (idx = @data.index("\r\n")) - @parts << @data[0..idx-1] - @data = @data[idx+2..-1] - end - - while @parts.length > 0 - throw "commands out of sync" unless @parts[0][0] == '*' - - num_parts = @parts[0][1..-1].to_i * 2 + 1 - return if @parts.length < num_parts - - command_parts = @parts[0..num_parts] - @parts = @parts[num_parts..-1] - - # Discard length declarations - command_line = - command_parts - .reject { |p| p[0] == '*' || p[0] == '$' } - .join ' ' - - if IRedisMock.replies.member?(command_line) - reply = IRedisMock.replies[command_line] - else - reply = "+OK" - end - - p "[#{command_line}] => [#{reply}]" - - IRedisMock.received << command_line - - if IRedisMock.paused - puts "Paused, therefore not sending [#{reply}]" - else - send_data "#{reply}\r\n" - end - end - end - end -end diff --git a/spec/support/mock_connection.rb b/spec/support/mock_connection.rb new file mode 100644 index 0000000..d80dabb --- /dev/null +++ b/spec/support/mock_connection.rb @@ -0,0 +1,97 @@ +module EventMachine::Hiredis + + module MockConnection + def send_data(data) + @expectations ||= [] + expectation = @expectations.shift + if expectation + data.to_s.should == expectation[:command] + + # Normal commands get one response each + handle_response(expectation[:response]) if expectation[:response] + + # But some pubsub commands can trigger many responses + # (e.g. 'subscribe a b c' gets one subscribe response for each of a b and c) + if expectation[:multi_response] + expectation[:multi_response].each do |response| + handle_response(response) + end + end + else + raise "Unexpected command #{data.inspect}" + end + end + + def marshal(*args) + args.flatten.join(' ') + end + + def close_connection + unbind + end + + # Expect a command a respond with specified response + def _expect(command, response = 'OK') + @expectations ||= [] + @expectations << { command: command, response: response } + end + + # Expect a command and do not respond + def _expect_no_response(command) + _expect(command, nil) + end + + # Expect and command and response with same + # This is the basic form of the redis pubsub protocol's acknowledgements + def _expect_pubsub(command) + @expectations ||= [] + + parts = command.split(' ') + if parts.length == 2 + @expectations << { command: command, response: parts } + else + channels = parts[1..-1] + @expectations << { + command: command, + multi_response: channels.map { |channel| [parts[0], channel] } + } + end + end + + def _expectations_met! + if @expectations && @expectations.length > 0 + fail("Did not receive expected command #{@expectations.shift}") + end + end + end + + class MockConnectionEM + attr_reader :connections + + def initialize(expected_connections, conn_class) + @timers = Set.new + @connections = [] + expected_connections.times { @connections << conn_class.new } + @connection_index = 0 + end + + def connect(host, port, connection_class, *args) + connection = @connections[@connection_index] + @connection_index += 1 + connection + end + + def add_timer(delay, &blk) + timer = Object.new + @timers.add(timer) + blk.call + + return timer + end + + def cancel_timer(timer) + marker = @timers.delete(timer) + marker.should_not == nil + end + end +end diff --git a/spec/support/networked_redis_mock.rb b/spec/support/networked_redis_mock.rb new file mode 100644 index 0000000..129f83c --- /dev/null +++ b/spec/support/networked_redis_mock.rb @@ -0,0 +1,101 @@ +module NetworkedRedisMock + + class RedisMock + attr_reader :replies, :paused + + def initialize(replies = {}) + @sig = EventMachine::start_server("127.0.0.1", 6381, Connection, self) { |con| + @connections.push(con) + } + @connections = [] + @received = [] + @connection_count = 0 + @replies = replies + @paused = false + end + + def stop + EventMachine::stop_server(@sig) + end + + def received + @received ||= [] + end + + def connection_received + @connection_count += 1 + end + + def connection_count + @connection_count + end + + def pause + @paused = true + end + + def unpause + @paused = false + end + + def kill_connections + @connections.each { |c| c.close_connection } + @connections.clear + end + end + + class Connection < EventMachine::Connection + def initialize(redis_mock) + @redis_mock = redis_mock + @data = "" + @parts = [] + end + + def post_init + @redis_mock.connection_received + end + + def unbind + @redis_mock.received << 'disconnect' + end + + def receive_data(data) + @data << data + + while (idx = @data.index("\r\n")) + @parts << @data[0..idx-1] + @data = @data[idx+2..-1] + end + + while @parts.length > 0 + throw "commands out of sync" unless @parts[0][0] == '*' + + num_parts = @parts[0][1..-1].to_i * 2 + 1 + return if @parts.length < num_parts + + command_parts = @parts[0..num_parts] + @parts = @parts[num_parts..-1] + + # Discard length declarations + command_line = + command_parts + .reject { |p| p[0] == '*' || p[0] == '$' } + .join ' ' + + if @redis_mock.replies.member?(command_line) + reply = @redis_mock.replies[command_line] + elsif command_line == '_DISCONNECT' + close_connection + else + reply = "+OK" + end + + @redis_mock.received << command_line + + unless @redis_mock.paused + send_data "#{reply}\r\n" + end + end + end + end +end diff --git a/spec/support/redis_mock.rb b/spec/support/redis_mock.rb deleted file mode 100644 index 9d69337..0000000 --- a/spec/support/redis_mock.rb +++ /dev/null @@ -1,65 +0,0 @@ -# nabbed from redis-rb, thanks! -require "socket" - -module RedisMock - def self.start(port = 6380) - server = TCPServer.new("127.0.0.1", port) - - loop do - session = server.accept - - while line = session.gets - parts = Array.new(line[1..-3].to_i) do - bytes = session.gets[1..-3].to_i - argument = session.read(bytes) - session.read(2) # Discard \r\n - argument - end - - response = yield(*parts) - - if response.nil? - session.shutdown(Socket::SHUT_RDWR) - break - else - session.write(response) - session.write("\r\n") - end - end - end - end - - module Helper - # Forks the current process and starts a new mock Redis server on - # port 6380. - # - # The server will reply with a `+OK` to all commands, but you can - # customize it by providing a hash. For example: - # - # redis_mock(:ping => lambda { "+PONG" }) do - # assert_equal "PONG", Redis.new(:port => 6380).ping - # end - # - def redis_mock(replies = {}) - begin - pid = fork do - trap("TERM") { exit } - - RedisMock.start do |command, *args| - (replies[command.to_sym] || lambda { |*_| "+OK" }).call(*args) - end - end - - sleep 1 # Give time for the socket to start listening. - - yield - - ensure - if pid - Process.kill("TERM", pid) - Process.wait(pid) - end - end - end - end -end diff --git a/spec/support/time_mock_eventmachine.rb b/spec/support/time_mock_eventmachine.rb new file mode 100644 index 0000000..688aa1c --- /dev/null +++ b/spec/support/time_mock_eventmachine.rb @@ -0,0 +1,84 @@ +# Collects timers and then invokes their callbacks in order of increasing time +# +# Useful for quickly testing timer based logic AS LONG AS THE CURRENT TIME +# IS NOT USED. i.e. no calls to Time.now or similar +module EventMachine::Hiredis + class Timer + attr_reader :due + + def initialize(due, callback) + @due = due + @callback = callback + end + + def fire(now) + @callback.call + true + end + end + + class PeriodicTimer + def initialize(period, callback, now) + @period = period + @callback = callback + @last_fired = now + end + + def fire(now) + @last_fired = now + @callback.call + false + end + + def due + @last_fired + @period + end + end + + class TimeMockEventMachine + attr_reader :current_time + + def initialize + @current_time = 0 + @timers = [] + end + + def add_timer(delay, &blk) + t = Timer.new(@current_time + delay, blk) + @timers.push(t) + return t + end + + def add_periodic_timer(period, &blk) + t = PeriodicTimer.new(period, blk, @current_time) + @timers.push(t) + return t + end + + def cancel_timer(t) + @timers.delete(t) + end + + def evaluate_ticks + until @timers.empty? + sort! + + t = @timers.first + @current_time = t.due + + remove = t.fire(@current_time) + @timers.delete(t) if remove + end + end + + def remaining_timers + @timers.length + end + + private + + def sort! + @timers.sort_by!(&:due) + end + end +end \ No newline at end of file From 4c9d7798bcbc95b8eb65558d1ff1ba7c544d85a6 Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Thu, 12 Mar 2015 09:12:06 +0000 Subject: [PATCH 20/44] Clean up logic --- lib/em-hiredis/connection_manager.rb | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/lib/em-hiredis/connection_manager.rb b/lib/em-hiredis/connection_manager.rb index e59d860..73d13d1 100644 --- a/lib/em-hiredis/connection_manager.rb +++ b/lib/em-hiredis/connection_manager.rb @@ -79,20 +79,13 @@ def reconnect def close case @sm.state - when :initial - @sm.update_state(:stopped) when :connecting @connect_operation.cancel - @sm.update_state(:stopped) when :connected @connection.remove_all_listeners(:disconnected) @connection.close_connection - @sm.update_state(:stopped) - when :disconnected - @sm.update_state(:stopped) - when :failed - @sm.update_state(:stopped) end + @sm.update_state(:stopped) end def state From 6517da2cf21e4fe1888a8b66fa17b47f45edb350 Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Tue, 17 Mar 2015 17:12:15 +0000 Subject: [PATCH 21/44] Prevent the leaking of internal collections via implicit return After 3: 1, 2, 3... We all hate implicit return! --- lib/em-hiredis/pubsub_client.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/em-hiredis/pubsub_client.rb b/lib/em-hiredis/pubsub_client.rb index 72809df..bc9981a 100644 --- a/lib/em-hiredis/pubsub_client.rb +++ b/lib/em-hiredis/pubsub_client.rb @@ -228,6 +228,8 @@ def subscribe_impl(type, subscriptions, channel, cb) # We will issue subscription command when we connect subscriptions[channel] << cb end + + return nil end def unsubscribe_impl(type, subscriptions, channel) @@ -237,6 +239,8 @@ def unsubscribe_impl(type, subscriptions, channel) @connection_manager.connection.send_command(type, channel) end end + + return nil end def unsubscribe_proc_impl(type, subscriptions, channel, proc) @@ -246,6 +250,8 @@ def unsubscribe_proc_impl(type, subscriptions, channel, proc) if removed && subscriptions[channel].empty? unsubscribe_impl(type, subscriptions, channel) end + + return nil end def message_callbacks(channel, message) From 65182b4626baa846081a9ccccd571f80924a4810 Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Wed, 18 Mar 2015 10:28:19 +0000 Subject: [PATCH 22/44] Resubscribe in chunks of 5000 channels Guards against potential stack overflows and arity limits when subscribed to huge numbers of channels --- lib/em-hiredis/pubsub_client.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/em-hiredis/pubsub_client.rb b/lib/em-hiredis/pubsub_client.rb index bc9981a..9ba58fa 100644 --- a/lib/em-hiredis/pubsub_client.rb +++ b/lib/em-hiredis/pubsub_client.rb @@ -194,8 +194,12 @@ def factory_connection } end - connection.send_command(:subscribe, *@subscriptions.keys) if @subscriptions.any? - connection.send_command(:psubscribe, *@psubscriptions.keys) if @psubscriptions.any? + @subscriptions.keys.each_slice(5000) { |slice| + connection.send_command(:subscribe, *slice) + } + @psubscriptions.keys.each_slice(5000) { |slice| + connection.send_command(:psubscribe, *slice) + } df.succeed(connection) }.errback { |e| From 412b86d14b8e4512a2c4603691a0b14f8f50275f Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Mon, 23 Mar 2015 14:52:24 +0000 Subject: [PATCH 23/44] Don't check if callback list is nil The default values for the hashes are set to be `[]` --- lib/em-hiredis/pubsub_client.rb | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/em-hiredis/pubsub_client.rb b/lib/em-hiredis/pubsub_client.rb index 9ba58fa..ea56470 100644 --- a/lib/em-hiredis/pubsub_client.rb +++ b/lib/em-hiredis/pubsub_client.rb @@ -259,17 +259,11 @@ def unsubscribe_proc_impl(type, subscriptions, channel, proc) end def message_callbacks(channel, message) - cbs = @subscriptions[channel] - if cbs - cbs.each { |cb| cb.call(message) if cb } - end + @subscriptions[channel].each { |cb| cb.call(message) if cb } end def pmessage_callbacks(pattern, channel, message) - cbs = @psubscriptions[pattern] - if cbs - cbs.each { |cb| cb.call(channel, message) if cb } - end + @psubscriptions[pattern].each { |cb| cb.call(channel, message) if cb } end def maybe_auth(connection) From ed2c18a03d05c504b6f6e50ca47aa3ec1e6c262e Mon Sep 17 00:00:00 2001 From: Daniel Waterworth Date: Wed, 25 Mar 2015 16:59:21 +0000 Subject: [PATCH 24/44] Stop the `Hash.new` magic This fixes a race condition when we unsubscribe and then receive a message from redis. In that case, the subscription would be recreated. --- lib/em-hiredis/pubsub_client.rb | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/em-hiredis/pubsub_client.rb b/lib/em-hiredis/pubsub_client.rb index ea56470..a5f9137 100644 --- a/lib/em-hiredis/pubsub_client.rb +++ b/lib/em-hiredis/pubsub_client.rb @@ -50,8 +50,8 @@ def initialize( # nil is a valid "callback", required because even if the user is using # emitted events rather than callbacks to consume their messages, we still # need to mark the fact that we are subscribed. - @subscriptions = Hash.new { |h, k| h[k] = [] } - @psubscriptions = Hash.new { |h, k| h[k] = [] } + @subscriptions = {} + @psubscriptions = {} @connection_manager = ConnectionManager.new(method(:factory_connection), em) @@ -227,10 +227,10 @@ def subscribe_impl(type, subscriptions, channel, cb) raise('Redis connection in failed state') elsif @connection_manager.state == :connected @connection_manager.connection.send_command(type, channel) - subscriptions[channel] << cb + subscriptions[channel] = [cb] else # We will issue subscription command when we connect - subscriptions[channel] << cb + subscriptions[channel] = [cb] end return nil @@ -259,11 +259,15 @@ def unsubscribe_proc_impl(type, subscriptions, channel, proc) end def message_callbacks(channel, message) - @subscriptions[channel].each { |cb| cb.call(message) if cb } + if @subscriptions.include?(channel) + @subscriptions[channel].each { |cb| cb.call(message) if cb } + end end def pmessage_callbacks(pattern, channel, message) - @psubscriptions[pattern].each { |cb| cb.call(channel, message) if cb } + if @psubscriptions.include?(pattern) + @psubscriptions[pattern].each { |cb| cb.call(channel, message) if cb } + end end def maybe_auth(connection) From abd1e1299342f6c69e36da7c3d33a7d146e5c722 Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Wed, 29 Jul 2015 12:46:08 +0100 Subject: [PATCH 25/44] Reset reconnection attempt when explicitly asked to reconnect --- lib/em-hiredis/connection_manager.rb | 1 + 1 file changed, 1 insertion(+) mode change 100644 => 100755 lib/em-hiredis/connection_manager.rb diff --git a/lib/em-hiredis/connection_manager.rb b/lib/em-hiredis/connection_manager.rb old mode 100644 new mode 100755 index 73d13d1..806f769 --- a/lib/em-hiredis/connection_manager.rb +++ b/lib/em-hiredis/connection_manager.rb @@ -62,6 +62,7 @@ def connect end def reconnect + @reconnect_attempt = 0 case @sm.state when :initial connect From c0bd0c63889ea4bd765529fbdaf1e15aa05286d4 Mon Sep 17 00:00:00 2001 From: Vivan Kumar Date: Thu, 1 Oct 2015 15:24:14 +0100 Subject: [PATCH 26/44] Only unsubscribe a proc if there is an existing channel subscription If there is no channel in the subscriptions object and we try to unsubscribe a proc from it, it will throw an error. --- lib/em-hiredis/pubsub_client.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/em-hiredis/pubsub_client.rb b/lib/em-hiredis/pubsub_client.rb index a5f9137..89573f2 100644 --- a/lib/em-hiredis/pubsub_client.rb +++ b/lib/em-hiredis/pubsub_client.rb @@ -248,11 +248,13 @@ def unsubscribe_impl(type, subscriptions, channel) end def unsubscribe_proc_impl(type, subscriptions, channel, proc) - removed = subscriptions[channel].delete(proc) + if subscriptions.include?(channel) + subscriptions[channel].delete(proc) - # Kill the redis subscription if that was the last callback - if removed && subscriptions[channel].empty? - unsubscribe_impl(type, subscriptions, channel) + # Kill the redis subscription if that was the last callback + if subscriptions[channel].empty? + unsubscribe_impl(type, subscriptions, channel) + end end return nil From beffde354a629752e6140d8749bcbfe1a583798b Mon Sep 17 00:00:00 2001 From: Vivan Kumar Date: Thu, 1 Oct 2015 17:15:15 +0100 Subject: [PATCH 27/44] Spec - unsubscribing proc for an unexisting channel subscription --- spec/pubsub_client_conn_spec.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spec/pubsub_client_conn_spec.rb b/spec/pubsub_client_conn_spec.rb index 66129c9..e9d32bb 100644 --- a/spec/pubsub_client_conn_spec.rb +++ b/spec/pubsub_client_conn_spec.rb @@ -36,6 +36,15 @@ def mock_connections(expected_connections, uri = 'redis://localhost:6379') end end + it "should not error when trying to unsubscribe a proc from a channel subscription that does not exist" do + mock_connections(1) do |client, (connection)| + client.connect + connection.connection_completed + + lambda { client.unsubscribe_proc('channel', Proc.new { |m| fail }) }.should_not raise_error + end + end + it "should allow selective unsubscription" do mock_connections(1) do |client, (connection)| client.connect From 0ca739b452796a5fa3b2660e2bb4d8b0c9819fcf Mon Sep 17 00:00:00 2001 From: Vivan Kumar Date: Wed, 7 Oct 2015 15:16:04 +0100 Subject: [PATCH 28/44] Change resubscribe batch size to 1000 --- lib/em-hiredis/pubsub_client.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/em-hiredis/pubsub_client.rb b/lib/em-hiredis/pubsub_client.rb index 89573f2..2a3bd29 100644 --- a/lib/em-hiredis/pubsub_client.rb +++ b/lib/em-hiredis/pubsub_client.rb @@ -25,6 +25,8 @@ class PubsubClient include EventEmitter include EventMachine::Deferrable + RESUBSCRIBE_BATCH_SIZE = 1000 + attr_reader :host, :port, :password # uri: @@ -194,10 +196,10 @@ def factory_connection } end - @subscriptions.keys.each_slice(5000) { |slice| + @subscriptions.keys.each_slice(RESUBSCRIBE_BATCH_SIZE) { |slice| connection.send_command(:subscribe, *slice) } - @psubscriptions.keys.each_slice(5000) { |slice| + @psubscriptions.keys.each_slice(RESUBSCRIBE_BATCH_SIZE) { |slice| connection.send_command(:psubscribe, *slice) } From b8f18f86563a58edb37192fb9d7ffabdd6e94c78 Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Wed, 17 Feb 2016 16:01:25 +0000 Subject: [PATCH 29/44] Don't shadow variables --- lib/em-hiredis/redis_client.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/em-hiredis/redis_client.rb b/lib/em-hiredis/redis_client.rb index 702c2b1..f594316 100644 --- a/lib/em-hiredis/redis_client.rb +++ b/lib/em-hiredis/redis_client.rb @@ -310,8 +310,8 @@ def factory_connection connection.on(:connected) { maybe_auth(connection).callback { maybe_select(connection).callback { - @command_queue.each { |df, command, args| - connection.send_command(df, command, args) + @command_queue.each { |command_df, command, args| + connection.send_command(command_df, command, args) } @command_queue.clear From b8bbffdc371686f7ed8ffbb7069fb53d1854a02c Mon Sep 17 00:00:00 2001 From: Anya Zenkina Date: Thu, 16 May 2019 10:33:48 +0100 Subject: [PATCH 30/44] make reconnect_attempts reconfigurable --- lib/em-hiredis/connection_manager.rb | 16 +++++++++++++--- lib/em-hiredis/pubsub_client.rb | 8 ++++++-- lib/em-hiredis/redis_client.rb | 8 ++++++-- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/lib/em-hiredis/connection_manager.rb b/lib/em-hiredis/connection_manager.rb index 806f769..e85e492 100755 --- a/lib/em-hiredis/connection_manager.rb +++ b/lib/em-hiredis/connection_manager.rb @@ -12,6 +12,8 @@ module EventMachine::Hiredis class ConnectionManager include EventEmitter + DEFAULT_RECONNECT_ATTEMPTS = 3 + TRANSITIONS = [ # first connect call [ :initial, :connecting ], @@ -42,7 +44,9 @@ class ConnectionManager # deferrable which succeeds with a connected and initialised instance # of EMConnection or fails if the connection was unsuccessful. # Failures will be retried - def initialize(connection_factory, em = EM) + def initialize(connection_factory, em = EM, reconnect_attempts) + + @reconnect_attempts = initialise_reconnect_attempts(reconnect_attempts) @em = em @connection_factory = connection_factory @@ -154,8 +158,7 @@ def on_disconnected(prev_state) # state when we emit :disconnected and :reconnected, so we should only # proceed here if our state has not been touched. return unless @sm.state == :disconnected - - if @reconnect_attempt > 3 + if @reconnect_attempt > @reconnect_attempts @sm.update_state(:failed) else @reconnect_attempt += 1 @@ -168,5 +171,12 @@ def on_disconnected(prev_state) end end end + + private + + def initialise_reconnect_attempts(reconnect_attempts) + reconnect_attempts ||= DEFAULT_RECONNECT_ATTEMPTS + return reconnect_attempts + end end end diff --git a/lib/em-hiredis/pubsub_client.rb b/lib/em-hiredis/pubsub_client.rb index 2a3bd29..bbca8a3 100644 --- a/lib/em-hiredis/pubsub_client.rb +++ b/lib/em-hiredis/pubsub_client.rb @@ -36,11 +36,15 @@ class PubsubClient # inactivity_response_timeout: # the number of seconds after a ping at which to terminate the connection # if there is still no activity + # reconnect_attempts: + # the number of how many reconnect attempts it should complete + # before declaring a connection as failed. def initialize( uri, inactivity_trigger_secs = nil, inactivity_response_timeout = nil, - em = EventMachine) + em = EventMachine, + reconnect_attempts = nil) @em = em configure(uri) @@ -55,7 +59,7 @@ def initialize( @subscriptions = {} @psubscriptions = {} - @connection_manager = ConnectionManager.new(method(:factory_connection), em) + @connection_manager = ConnectionManager.new(method(:factory_connection), em, reconnect_attempts) @connection_manager.on(:connected) { EM::Hiredis.logger.info("#{@name} - Connected") diff --git a/lib/em-hiredis/redis_client.rb b/lib/em-hiredis/redis_client.rb index f594316..0e35138 100644 --- a/lib/em-hiredis/redis_client.rb +++ b/lib/em-hiredis/redis_client.rb @@ -22,11 +22,15 @@ class Client # inactivity_response_timeout: # the number of seconds after a ping at which to terminate the connection # if there is still no activity + # reconnect_attempts: + # the number of how many reconnect attempts it should complete + # before declaring a connection as failed. def initialize( uri, inactivity_trigger_secs = nil, inactivity_response_timeout = nil, - em = EventMachine) + em = EventMachine, + reconnect_attempts = nil) @em = em configure(uri) @@ -37,7 +41,7 @@ def initialize( # Commands received while we are not initialized, to be sent once we are @command_queue = [] - @connection_manager = ConnectionManager.new(method(:factory_connection), em) + @connection_manager = ConnectionManager.new(method(:factory_connection), em, reconnect_attempts) @connection_manager.on(:connected) { EM::Hiredis.logger.info("#{@name} - Connected") From ab3f97d1432a2642a810aaf6c69dbdb3d6d49f96 Mon Sep 17 00:00:00 2001 From: Anya Zenkina Date: Wed, 10 Jul 2019 11:19:54 +0100 Subject: [PATCH 31/44] add ping on establishing connection v1 --- lib/em-hiredis/pubsub_client.rb | 2 +- lib/em-hiredis/pubsub_connection.rb | 14 ++++-- lib/em-hiredis/redis_client.rb | 21 +++++--- lib/em-hiredis/redis_connection.rb | 7 ++- spec/client_conn_spec.rb | 13 ++++- spec/client_server_spec.rb | 75 ++++++++++++++++++----------- spec/connection_manager_spec.rb | 54 ++++++++++----------- spec/live/pubsub_spec.rb | 2 +- spec/pubsub_client_conn_spec.rb | 44 +++++++++++++++++ 9 files changed, 160 insertions(+), 72 deletions(-) diff --git a/lib/em-hiredis/pubsub_client.rb b/lib/em-hiredis/pubsub_client.rb index bbca8a3..1ff710c 100644 --- a/lib/em-hiredis/pubsub_client.rb +++ b/lib/em-hiredis/pubsub_client.rb @@ -184,7 +184,7 @@ def factory_connection connection.on(:connected) { maybe_auth(connection).callback { - + connection.ping_with_pubsub connection.on(:message, &method(:message_callbacks)) connection.on(:pmessage, &method(:pmessage_callbacks)) diff --git a/lib/em-hiredis/pubsub_connection.rb b/lib/em-hiredis/pubsub_connection.rb index b7464dc..ea627cb 100644 --- a/lib/em-hiredis/pubsub_connection.rb +++ b/lib/em-hiredis/pubsub_connection.rb @@ -2,7 +2,7 @@ module EventMachine::Hiredis module PubsubConnection include EventMachine::Hiredis::EventEmitter - PUBSUB_COMMANDS = %w{subscribe unsubscribe psubscribe punsubscribe}.freeze + PUBSUB_COMMANDS = %w{ping subscribe unsubscribe psubscribe punsubscribe}.freeze PUBSUB_MESSAGES = (PUBSUB_COMMANDS + %w{message pmessage}).freeze PING_CHANNEL = '__em-hiredis-ping' @@ -19,8 +19,7 @@ def initialize(inactivity_trigger_secs = nil, @inactivity_checker = InactivityChecker.new(inactivity_trigger_secs, inactivity_response_timeout) @inactivity_checker.on(:activity_timeout) { EM::Hiredis.logger.debug("#{@name} - Sending ping") - send_command('subscribe', PING_CHANNEL) - send_command('unsubscribe', PING_CHANNEL) + ping_with_pubsub } @inactivity_checker.on(:response_timeout) { EM::Hiredis.logger.warn("#{@name} - Closing connection because of inactivity timeout") @@ -28,6 +27,11 @@ def initialize(inactivity_trigger_secs = nil, } end + def ping_with_pubsub + send_command('subscribe', PING_CHANNEL) + send_command('unsubscribe', PING_CHANNEL) + end + def send_command(command, *channels) if PUBSUB_COMMANDS.include?(command.to_s) send_data(marshal(command, *channels)) @@ -113,9 +117,9 @@ def handle_response(reply) if PUBSUB_MESSAGES.include?(type) emit(type.to_sym, *reply[1..-1]) else - EM::Hireds.logger.error("#{@name} - unrecognised response: #{reply.inspect}") + EM::Hiredis.logger.error("#{@name} - unrecognised response: #{reply.inspect}") end end end end -end \ No newline at end of file +end diff --git a/lib/em-hiredis/redis_client.rb b/lib/em-hiredis/redis_client.rb index 0e35138..624bc5f 100644 --- a/lib/em-hiredis/redis_client.rb +++ b/lib/em-hiredis/redis_client.rb @@ -313,15 +313,21 @@ def factory_connection connection.on(:connected) { maybe_auth(connection).callback { - maybe_select(connection).callback { - @command_queue.each { |command_df, command, args| - connection.send_command(command_df, command, args) + connection.ping.callback { + maybe_select(connection).callback { + @command_queue.each { |command_df, command, args| + connection.send_command(command_df, command, args) + } + @command_queue.clear + + df.succeed(connection) + }.errback { |e| + # Failure to select db counts as a connection failure + connection.close_connection + df.fail(e) } - @command_queue.clear - - df.succeed(connection) }.errback { |e| - # Failure to select db counts as a connection failure + # Failure to ping counts as a connection failure connection.close_connection df.fail(e) } @@ -368,6 +374,7 @@ def maybe_auth(connection) end end + def maybe_select(connection) if @db != 0 connection.send_command(EM::DefaultDeferrable.new, 'select', @db) diff --git a/lib/em-hiredis/redis_connection.rb b/lib/em-hiredis/redis_connection.rb index 30ba52c..bd24435 100644 --- a/lib/em-hiredis/redis_connection.rb +++ b/lib/em-hiredis/redis_connection.rb @@ -17,7 +17,7 @@ def initialize(inactivity_trigger_secs = nil, @inactivity_checker = InactivityChecker.new(inactivity_trigger_secs, inactivity_response_timeout) @inactivity_checker.on(:activity_timeout) { EM::Hiredis.logger.debug("#{@name} - Sending ping") - send_command(EM::DefaultDeferrable.new, 'ping', []) + ping } @inactivity_checker.on(:response_timeout) { EM::Hiredis.logger.warn("#{@name} - Closing connection because of inactivity timeout") @@ -31,6 +31,10 @@ def send_command(df, command, args) return df end + def ping + send_command(EM::DefaultDeferrable.new, 'ping', []) + end + def pending_responses @response_queue.length end @@ -100,4 +104,5 @@ def handle_response(reply) end end end + end diff --git a/spec/client_conn_spec.rb b/spec/client_conn_spec.rb index 126e27c..26c6c59 100644 --- a/spec/client_conn_spec.rb +++ b/spec/client_conn_spec.rb @@ -23,10 +23,12 @@ def mock_connections(expected_connections) # Both connections expect to receive 'select' first # But pings 3 and 4 and issued between conn_a being disconnected # and conn_b completing its connection + conn_a._expect('ping') conn_a._expect('select 9') conn_a._expect('ping 1') conn_a._expect('ping 2') + conn_b._expect('ping') conn_b._expect('select 9') conn_b._expect('ping 3') conn_b._expect('ping 4') @@ -83,14 +85,16 @@ def mock_connections(expected_connections) got_errback = true } - good_connection._expect('select 9') good_connection._expect('ping') + good_connection._expect('select 9') # But after calling connect and completing the connection, we are functional again client.connect good_connection.connection_completed + got_callback = false + good_connection._expect('ping') client.ping.callback { got_callback = true } @@ -116,6 +120,7 @@ def mock_connections(expected_connections) got_errback = true } + good_connection._expect('ping') good_connection._expect('select 9') good_connection._expect('ping') @@ -149,6 +154,7 @@ def mock_connections(expected_connections) # not connected yet conn_a.unbind + conn_b._expect('ping') conn_b._expect('select 9') conn_b.connection_completed @@ -158,7 +164,7 @@ def mock_connections(expected_connections) it 'should retry when partially set up' do mock_connections(2) { |client, (conn_a, conn_b)| - conn_a._expect_no_response('select 9') + conn_a._expect_no_response('ping') connected = false client.connect.callback { @@ -169,6 +175,7 @@ def mock_connections(expected_connections) # awaiting response to 'select' conn_a.unbind + conn_b._expect('ping') conn_b._expect('select 9') conn_b.connection_completed @@ -178,6 +185,7 @@ def mock_connections(expected_connections) it 'should reconnect once connected' do mock_connections(2) { |client, (conn_a, conn_b)| + conn_a._expect('ping') conn_a._expect('select 9') client.connect.errback { @@ -193,6 +201,7 @@ def mock_connections(expected_connections) # awaiting response to 'select' conn_a.unbind + conn_b._expect('ping') conn_b._expect('select 9') conn_b.connection_completed diff --git a/spec/client_server_spec.rb b/spec/client_server_spec.rb index 340a3a6..13dc6a9 100644 --- a/spec/client_server_spec.rb +++ b/spec/client_server_spec.rb @@ -13,7 +13,7 @@ def recording_server(replies = {}) it 'should not connect on construction' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') server.connection_count.should == 0 done } @@ -21,7 +21,7 @@ def recording_server(replies = {}) it 'should be connected when connect is called' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.connect.callback { server.connection_count.should == 1 done @@ -31,12 +31,13 @@ def recording_server(replies = {}) } end - it 'should issue select command before succeeding connection' do + it 'should issue ping command before succeeding connection' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.connect.callback { server.connection_count.should == 1 - server.received[0].should == 'select 9' + server.received[0].should == 'ping' + server.received[1].should == 'select 9' done }.errback { |e| fail(e) @@ -44,12 +45,25 @@ def recording_server(replies = {}) } end - it 'should issue select command before emitting :connected' do + it 'should issue ping command before succeeding connection if no db' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381') + client.connect.callback { + server.connection_count.should == 1 + server.received[0].should == 'ping' + done + }.errback { |e| + fail(e) + } + } + end + + it 'should issue pinf command before emitting :connected' do + recording_server { |server| + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.on(:connected) { server.connection_count.should == 1 - server.received[0].should == 'select 9' + server.received[0].should == 'ping' done } client.connect @@ -62,7 +76,7 @@ def recording_server(replies = {}) it 'should emit :disconnected when the connection disconnects' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.on(:disconnected) { done } @@ -78,7 +92,7 @@ def recording_server(replies = {}) it 'should create a new connection if the existing one reports it has failed' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.connect.callback { server.kill_connections } @@ -91,7 +105,7 @@ def recording_server(replies = {}) it 'should emit both connected and reconnected' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.connect.callback { callbacks = [] client.on(:connected) { @@ -117,7 +131,7 @@ def recording_server(replies = {}) it 'should make 4 attempts, emitting :reconnect_failed with a count' do em { - client = EM::Hiredis::Client.new('redis://localhost:9999') # assumes nothing listening on 9999 + client = EM::Hiredis::Client.new('redis://127.0.0.1:9999') # assumes nothing listening on 9999 expected = 1 client.on(:reconnect_failed) { |count| @@ -132,7 +146,7 @@ def recording_server(replies = {}) it 'after 4 unsuccessful attempts should emit :failed' do em { - client = EM::Hiredis::Client.new('redis://localhost:9999') # assumes nothing listening on 9999 + client = EM::Hiredis::Client.new('redis://127.0.0.1:9999') # assumes nothing listening on 9999 reconnect_count = 0 client.on(:reconnect_failed) { |count| @@ -167,7 +181,7 @@ def recording_server(replies = {}) it 'should recover from DNS resolution failure' do recording_server { |server| EM.stub(:connect).and_raise(EventMachine::ConnectionError.new) - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.on(:reconnect_failed) { EM.rspec_reset @@ -186,7 +200,7 @@ def recording_server(replies = {}) it 'should make 4 attempts, emitting :reconnect_failed with a count' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.connect.callback { server.stop server.kill_connections @@ -203,7 +217,7 @@ def recording_server(replies = {}) it 'after 4 unsuccessful attempts should emit :failed' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.connect.callback { server.stop server.kill_connections @@ -223,7 +237,7 @@ def recording_server(replies = {}) it 'should fail commands immediately when in a failed state' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.connect.callback { client.on(:failed) { client.get('foo').errback { |e| @@ -240,7 +254,7 @@ def recording_server(replies = {}) it 'should be possible to trigger reconnect on request' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.connect.callback { client.on(:reconnected) { server.connection_count.should == 2 @@ -254,7 +268,7 @@ def recording_server(replies = {}) it 'should do something sensible???' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.reconnect client.ping.callback { done @@ -264,7 +278,7 @@ def recording_server(replies = {}) it 'should keep responses matched when connection is lost' do recording_server('get f' => '+hello') { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.connect.callback { client.get('a') client.get('b').callback { @@ -289,7 +303,7 @@ def recording_server(replies = {}) it 'should be able to send commands' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.connect.callback { client.set('test', 'value').callback { done @@ -300,7 +314,7 @@ def recording_server(replies = {}) it 'should queue commands called before connect is called' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.set('test', 'value').callback { client.ping.callback { done @@ -317,9 +331,9 @@ def recording_server(replies = {}) it 'should support alternative dbs' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/4') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/4') client.connect.callback { - server.received.should == ['select 4'] + server.received.should == ['ping','select 4'] done } } @@ -327,10 +341,11 @@ def recording_server(replies = {}) it 'should execute db selection first' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.set('test', 'value').callback { client.ping.callback { server.received.should == [ + 'ping', 'select 9', 'set test value', 'ping'] @@ -344,7 +359,7 @@ def recording_server(replies = {}) it 'should class db selection failure as a connection failure' do recording_server('select 9' => '-ERR no such db') { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.connect.errback { |e| done } @@ -353,16 +368,18 @@ def recording_server(replies = {}) it 'should re-select db on reconnection' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/4') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/4') client.connect.callback { client.ping.callback { client.on(:reconnected) { client.ping.callback { server.connection_count.should == 2 server.received.should == [ + 'ping', 'select 4', 'ping', 'disconnect', + 'ping', 'select 4', 'ping' ] @@ -377,16 +394,18 @@ def recording_server(replies = {}) it 'should remember a change in the selected db' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.connect.callback { client.select(4).callback { client.on(:reconnected) { client.ping.callback { server.connection_count.should == 2 server.received.should == [ + 'ping', 'select 9', 'select 4', 'disconnect', + 'ping', 'select 4', 'ping' ] diff --git a/spec/connection_manager_spec.rb b/spec/connection_manager_spec.rb index 9181da1..db87cae 100644 --- a/spec/connection_manager_spec.rb +++ b/spec/connection_manager_spec.rb @@ -29,7 +29,7 @@ def expect_event_registration(event) context 'forcing reconnection' do it 'should be successful when disconnected from initial attempt failure' do em = EM::Hiredis::TimeMockEventMachine.new - conn_factory = mock('connection factory') + conn_factory = double('connection factory') manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) @@ -51,7 +51,7 @@ def expect_event_registration(event) manager.state.should == :connecting # Which will succeed - conn = mock('connection') + conn = double('connection') conn.expect_event_registration(:disconnected) second_conn_df.succeed(conn) @@ -63,7 +63,7 @@ def expect_event_registration(event) it 'should be successful when disconnected from existing connection, triggered on disconnected' do em = EM::Hiredis::TimeMockEventMachine.new - conn_factory = mock('connection factory') + conn_factory = double('connection factory') manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) @@ -77,7 +77,7 @@ def expect_event_registration(event) manager.connect - initial_conn = mock('initial connection') + initial_conn = double('initial connection') disconnected_callback = initial_conn.expect_event_registration(:disconnected) initial_conn_df.succeed(initial_conn) @@ -90,12 +90,12 @@ def expect_event_registration(event) manager.state.should == :connecting - second_conn = mock('second connection') + second_conn = double('second connection') second_conn.expect_event_registration(:disconnected) second_conn_df.succeed(second_conn) } - disconnected_callback.call + disconnected_callback.call manager.state.should == :connected @@ -105,7 +105,7 @@ def expect_event_registration(event) it 'should be successful when disconnected from existing connection, triggered on reconnect_failed' do em = EM::Hiredis::TimeMockEventMachine.new - conn_factory = mock('connection factory') + conn_factory = double('connection factory') manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) @@ -121,7 +121,7 @@ def expect_event_registration(event) manager.connect - initial_conn = mock('initial connection') + initial_conn = double('initial connection') disconnected_callback = initial_conn.expect_event_registration(:disconnected) initial_conn_df.succeed(initial_conn) @@ -134,13 +134,13 @@ def expect_event_registration(event) manager.state.should == :connecting - second_conn = mock('second connection') + second_conn = double('second connection') second_conn.expect_event_registration(:disconnected) second_conn_df.succeed(second_conn) } fail_conn_df.fail('Testing') - disconnected_callback.call + disconnected_callback.call manager.state.should == :connected @@ -151,7 +151,7 @@ def expect_event_registration(event) it 'should cancel the connection in progress when already connecting' do em = EM::Hiredis::TimeMockEventMachine.new - conn_factory = mock('connection factory') + conn_factory = double('connection factory') manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) initial_conn_df = EM::DefaultDeferrable.new @@ -169,7 +169,7 @@ def expect_event_registration(event) manager.state.should == :connecting - in_progress_connection = mock('in progress connection') + in_progress_connection = double('in progress connection') # the connection in progress when we called reconnect should be # immediately closed, because we might have reconfigured to connect to # something different @@ -179,7 +179,7 @@ def expect_event_registration(event) # now we're trying to connect the replacement connection manager.state.should == :connecting - new_connection = mock('replacement connection') + new_connection = double('replacement connection') new_connection.expect_event_registration(:disconnected) second_conn_df.succeed(new_connection) @@ -189,7 +189,7 @@ def expect_event_registration(event) it 'should reconnect again when already connecting and in-progress connection fails' do em = EM::Hiredis::TimeMockEventMachine.new - conn_factory = mock('connection factory') + conn_factory = double('connection factory') manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) initial_conn_df = EM::DefaultDeferrable.new @@ -211,7 +211,7 @@ def expect_event_registration(event) # now we're trying to connect the replacement connection manager.state.should == :connecting - new_connection = mock('replacement connection') + new_connection = double('replacement connection') new_connection.expect_event_registration(:disconnected) second_conn_df.succeed(new_connection) @@ -221,7 +221,7 @@ def expect_event_registration(event) it 'should be successful when reconnecting' do em = EM::Hiredis::TimeMockEventMachine.new - conn_factory = mock('connection factory') + conn_factory = double('connection factory') manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) initial_conn_df = EM::DefaultDeferrable.new @@ -235,7 +235,7 @@ def expect_event_registration(event) manager.connect - initial_connection = mock('initial connection') + initial_connection = double('initial connection') disconnect_callback = initial_connection.expect_event_registration(:disconnected) initial_conn_df.succeed(initial_connection) @@ -254,7 +254,7 @@ def expect_event_registration(event) # now we're trying to connect the replacement connection manager.state.should == :connecting - new_connection = mock('replacement connection') + new_connection = double('replacement connection') new_connection.expect_event_registration(:disconnected) manual_reconnect_df.succeed(new_connection) @@ -263,7 +263,7 @@ def expect_event_registration(event) it 'should be successful when connected' do em = EM::Hiredis::TimeMockEventMachine.new - conn_factory = mock('connection factory') + conn_factory = double('connection factory') manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) @@ -276,7 +276,7 @@ def expect_event_registration(event) manager.connect - initial_conn = mock('initial connection') + initial_conn = double('initial connection') disconnected_callback = initial_conn.expect_event_registration(:disconnected) initial_conn_df.succeed(initial_conn) @@ -293,7 +293,7 @@ def expect_event_registration(event) # ...we complete the reconnect manager.state.should == :connecting - second_conn = mock('second connection') + second_conn = double('second connection') second_conn.should_receive(:on).with(:disconnected) second_conn_df.succeed(second_conn) @@ -305,7 +305,7 @@ def expect_event_registration(event) it 'should be successful when failed during initial connect' do em = EM::Hiredis::TimeMockEventMachine.new - conn_factory = mock('connection factory') + conn_factory = double('connection factory') manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) @@ -336,7 +336,7 @@ def expect_event_registration(event) manager.reconnect - conn = mock('connection') + conn = double('connection') conn.expect_event_registration(:disconnected) succeed_conn_df.succeed(conn) @@ -349,7 +349,7 @@ def expect_event_registration(event) it 'should be successful when failed after initial success' do em = EM::Hiredis::TimeMockEventMachine.new - conn_factory = mock('connection factory') + conn_factory = double('connection factory') manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) # Connect successfully, then five failed attempts takes us to failed, @@ -369,7 +369,7 @@ def expect_event_registration(event) manager.connect - initial_conn = mock('initial connection') + initial_conn = double('initial connection') disconnected_cb = initial_conn.expect_event_registration(:disconnected) initial_conn_df.succeed(initial_conn) @@ -389,12 +389,12 @@ def expect_event_registration(event) manager.reconnect - second_conn = mock('second connection') + second_conn = double('second connection') second_conn.expect_event_registration(:disconnected) second_conn_df.succeed(second_conn) manager.state.should == :connected - + # Reconnect timers should have been cancelled em.remaining_timers.should == 0 end diff --git a/spec/live/pubsub_spec.rb b/spec/live/pubsub_spec.rb index 2faf411..0219361 100644 --- a/spec/live/pubsub_spec.rb +++ b/spec/live/pubsub_spec.rb @@ -51,7 +51,7 @@ channel.should == channel message.should == 'foo' - callback_count.should == 2 + callback_count.should == 3 done } diff --git a/spec/pubsub_client_conn_spec.rb b/spec/pubsub_client_conn_spec.rb index e9d32bb..a49c823 100644 --- a/spec/pubsub_client_conn_spec.rb +++ b/spec/pubsub_client_conn_spec.rb @@ -21,6 +21,10 @@ def mock_connections(expected_connections, uri = 'redis://localhost:6379') it "should unsubscribe all callbacks for a channel on unsubscribe" do mock_connections(1) do |client, (connection)| client.connect + + connection._expect_pubsub("subscribe __em-hiredis-ping") + connection._expect_pubsub("unsubscribe __em-hiredis-ping") + connection.connection_completed connection._expect_pubsub('subscribe channel') @@ -39,6 +43,10 @@ def mock_connections(expected_connections, uri = 'redis://localhost:6379') it "should not error when trying to unsubscribe a proc from a channel subscription that does not exist" do mock_connections(1) do |client, (connection)| client.connect + + connection._expect_pubsub("subscribe __em-hiredis-ping") + connection._expect_pubsub("unsubscribe __em-hiredis-ping") + connection.connection_completed lambda { client.unsubscribe_proc('channel', Proc.new { |m| fail }) }.should_not raise_error @@ -48,6 +56,9 @@ def mock_connections(expected_connections, uri = 'redis://localhost:6379') it "should allow selective unsubscription" do mock_connections(1) do |client, (connection)| client.connect + connection._expect_pubsub("subscribe __em-hiredis-ping") + connection._expect_pubsub("unsubscribe __em-hiredis-ping") + connection.connection_completed connection._expect_pubsub('subscribe channel') @@ -70,6 +81,9 @@ def mock_connections(expected_connections, uri = 'redis://localhost:6379') it "should unsubscribe from redis when all subscriptions for a channel are unsubscribed" do mock_connections(1) do |client, (connection)| client.connect + + connection._expect_pubsub("subscribe __em-hiredis-ping") + connection._expect_pubsub("unsubscribe __em-hiredis-ping") connection.connection_completed connection._expect_pubsub('subscribe channel') @@ -94,6 +108,9 @@ def mock_connections(expected_connections, uri = 'redis://localhost:6379') it "should punsubscribe all callbacks for a pattern on punsubscribe" do mock_connections(1) do |client, (connection)| client.connect + + connection._expect_pubsub("subscribe __em-hiredis-ping") + connection._expect_pubsub("unsubscribe __em-hiredis-ping") connection.connection_completed connection._expect_pubsub('psubscribe channel:*') @@ -113,6 +130,9 @@ def mock_connections(expected_connections, uri = 'redis://localhost:6379') it "should allow selective punsubscription" do mock_connections(1) do |client, (connection)| client.connect + + connection._expect_pubsub("subscribe __em-hiredis-ping") + connection._expect_pubsub("unsubscribe __em-hiredis-ping") connection.connection_completed connection._expect_pubsub('psubscribe channel:*') @@ -135,6 +155,9 @@ def mock_connections(expected_connections, uri = 'redis://localhost:6379') it "should punsubscribe from redis when all psubscriptions for a pattern are punsubscribed" do mock_connections(1) do |client, (connection)| client.connect + connection._expect_pubsub("subscribe __em-hiredis-ping") + connection._expect_pubsub("unsubscribe __em-hiredis-ping") + connection.connection_completed connection._expect_pubsub('psubscribe channel:*') @@ -161,6 +184,10 @@ def mock_connections(expected_connections, uri = 'redis://localhost:6379') it 'should resubscribe all existing on reconnection' do mock_connections(2) do |client, (conn_a, conn_b)| client.connect + + conn_a._expect_pubsub("subscribe __em-hiredis-ping") + conn_a._expect_pubsub("unsubscribe __em-hiredis-ping") + conn_a.connection_completed channels = %w{foo bar baz} @@ -198,6 +225,9 @@ def mock_connections(expected_connections, uri = 'redis://localhost:6379') # Trigger a reconnection conn_a.unbind + conn_b._expect_pubsub("subscribe __em-hiredis-ping") + conn_b._expect_pubsub("unsubscribe __em-hiredis-ping") + # All subs previously made should be re-made conn_b._expect_pubsub("subscribe #{channels.join(' ')}") conn_b._expect_pubsub("psubscribe #{patterns.join(' ')}") @@ -229,6 +259,8 @@ def mock_connections(expected_connections, uri = 'redis://localhost:6379') client.connect.callback { connected = true } + connection._expect('subscribe __em-hiredis-ping') + connection._expect('unsubscribe __em-hiredis-ping') connection.connection_completed connected.should == true @@ -243,6 +275,9 @@ def mock_connections(expected_connections, uri = 'redis://localhost:6379') client.connect.callback { connected = true } + connection._expect_pubsub('subscribe __em-hiredis-ping') + connection._expect_pubsub('unsubscribe __em-hiredis-ping') + connection.connection_completed connected.should == true @@ -269,6 +304,8 @@ def mock_connections(expected_connections, uri = 'redis://localhost:6379') connected = true } + connection._expect_pubsub('subscribe __em-hiredis-ping') + connection._expect_pubsub('unsubscribe __em-hiredis-ping') connection._expect_pubsub('subscribe channel') message_received = nil @@ -288,18 +325,25 @@ def mock_connections(expected_connections, uri = 'redis://localhost:6379') it 'should reconnect if auth command fails' do mock_connections(2, 'redis://:mypass@localhost:6379') do |client, (conn_a, conn_b)| + conn_a._expect('auth mypass', RuntimeError.new('OOPS')) conn_b._expect('auth mypass') + connected = false client.connect.callback { connected = true } + + conn_b._expect('subscribe __em-hiredis-ping') + conn_b._expect('unsubscribe __em-hiredis-ping') + conn_a.connection_completed connected.should == false conn_b.connection_completed connected.should == true + end end end From db2d1a7d3d4118f1720727ac80911208fd77fdc2 Mon Sep 17 00:00:00 2001 From: Anya Zenkina Date: Wed, 7 Aug 2019 14:30:38 +0100 Subject: [PATCH 32/44] ping for pubsub --- lib/em-hiredis/pubsub_client.rb | 54 +++++++++++++++++------------ lib/em-hiredis/pubsub_connection.rb | 27 +++++++++++---- 2 files changed, 52 insertions(+), 29 deletions(-) diff --git a/lib/em-hiredis/pubsub_client.rb b/lib/em-hiredis/pubsub_client.rb index 1ff710c..7a0aa72 100644 --- a/lib/em-hiredis/pubsub_client.rb +++ b/lib/em-hiredis/pubsub_client.rb @@ -181,33 +181,41 @@ def factory_connection @inactivity_response_timeout, @name ) - + puts "Connecting..." connection.on(:connected) { + puts "maybe auth?" maybe_auth(connection).callback { - connection.ping_with_pubsub - connection.on(:message, &method(:message_callbacks)) - connection.on(:pmessage, &method(:pmessage_callbacks)) - - [ :message, - :pmessage, - :subscribe, - :unsubscribe, - :psubscribe, - :punsubscribe - ].each do |command| - connection.on(command) { |*args| - emit(command, *args) + puts "inside maybe_auth" + puts "connection.on ping callback" + connection.ping.callback { + puts "inside connection.on ping callback!" + connection.on(:message, &method(:message_callbacks)) + connection.on(:pmessage, &method(:pmessage_callbacks)) + + [ :message, + :pmessage, + :subscribe, + :unsubscribe, + :psubscribe, + :punsubscribe + ].each do |command| + connection.on(command) { |*args| + emit(command, *args) + } + end + + @subscriptions.keys.each_slice(RESUBSCRIBE_BATCH_SIZE) { |slice| + connection.send_command(:subscribe, *slice) } - end - - @subscriptions.keys.each_slice(RESUBSCRIBE_BATCH_SIZE) { |slice| - connection.send_command(:subscribe, *slice) - } - @psubscriptions.keys.each_slice(RESUBSCRIBE_BATCH_SIZE) { |slice| - connection.send_command(:psubscribe, *slice) + @psubscriptions.keys.each_slice(RESUBSCRIBE_BATCH_SIZE) { |slice| + connection.send_command(:psubscribe, *slice) + } + df.succeed(connection) + }.errback { + # Failure to auth counts as a connection failure + connection.close_connection + df.fail(e) } - - df.succeed(connection) }.errback { |e| # Failure to auth counts as a connection failure connection.close_connection diff --git a/lib/em-hiredis/pubsub_connection.rb b/lib/em-hiredis/pubsub_connection.rb index ea627cb..34cbba6 100644 --- a/lib/em-hiredis/pubsub_connection.rb +++ b/lib/em-hiredis/pubsub_connection.rb @@ -19,7 +19,8 @@ def initialize(inactivity_trigger_secs = nil, @inactivity_checker = InactivityChecker.new(inactivity_trigger_secs, inactivity_response_timeout) @inactivity_checker.on(:activity_timeout) { EM::Hiredis.logger.debug("#{@name} - Sending ping") - ping_with_pubsub + send_command('subscribe', PING_CHANNEL) + send_command('unsubscribe', PING_CHANNEL) } @inactivity_checker.on(:response_timeout) { EM::Hiredis.logger.warn("#{@name} - Closing connection because of inactivity timeout") @@ -27,11 +28,6 @@ def initialize(inactivity_trigger_secs = nil, } end - def ping_with_pubsub - send_command('subscribe', PING_CHANNEL) - send_command('unsubscribe', PING_CHANNEL) - end - def send_command(command, *channels) if PUBSUB_COMMANDS.include?(command.to_s) send_data(marshal(command, *channels)) @@ -55,6 +51,16 @@ def auth(password) return df end + + def ping + puts "ping method in pubsub_connection" + df = @ping_df = EM::DefaultDeferrable.new + puts "send_data(marshal('ping'))" + send_data(marshal('ping')) + puts "return df" + return df + end + # EM::Connection callback def connection_completed @connected = true @@ -112,6 +118,15 @@ def handle_response(reply) @auth_df.succeed(reply) end @auth_df = nil + elsif @ping_df + if reply.kind_of?(StandardError) + e = EM::Hiredis::RedisError.new(reply.message) + e.redis_error = reply + @ping_df.fail(e) + else + @ping_df.succeed(reply) + end + @ping_df = nil else type = reply[0] if PUBSUB_MESSAGES.include?(type) From 74650fb287fa2c1af2dd051eeb51c1284bb16fc4 Mon Sep 17 00:00:00 2001 From: Anya Zenkina Date: Wed, 7 Aug 2019 16:39:00 +0100 Subject: [PATCH 33/44] implement timeout to the df function --- README.md | 5 ----- lib/em-hiredis/pubsub_client.rb | 13 ++++--------- lib/em-hiredis/pubsub_connection.rb | 13 ++++--------- lib/em-hiredis/redis_client.rb | 2 +- 4 files changed, 9 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 11b7b38..ada9a12 100644 --- a/README.md +++ b/README.md @@ -79,11 +79,6 @@ This configures a `PING` command to be sent if 5 seconds elapse without receivin This configuration is per client, you may choose different value for clients with different expected traffic patterns, or activate it on some and not at all on others. -### PING and Pubsub - -Because the Redis Pubsub protocol limits the set of valid commands on a connection once it is in "Pubsub" mode, `PING` is not supported in this case (though it may be in future, see https://github.com/antirez/redis/issues/420). In order to create some valid request-response traffic on the connection, a Pubsub connection will issue `SUBSCRIBE "__em-hiredis-ping"`, followed by a corresponding `UNSUBSCRIBE` immediately on success of the subscribe. -While less than ideal, this is the case where an application layer inactivity check is most valuable, and so the trade off is reasonable until `PING` is supported correctly on Pubsub connections. - ### Close to the metal Basically just bind to `:message` and `:pmessage` events: diff --git a/lib/em-hiredis/pubsub_client.rb b/lib/em-hiredis/pubsub_client.rb index 7a0aa72..88d59b9 100644 --- a/lib/em-hiredis/pubsub_client.rb +++ b/lib/em-hiredis/pubsub_client.rb @@ -45,7 +45,6 @@ def initialize( inactivity_response_timeout = nil, em = EventMachine, reconnect_attempts = nil) - @em = em configure(uri) @@ -63,6 +62,7 @@ def initialize( @connection_manager.on(:connected) { EM::Hiredis.logger.info("#{@name} - Connected") + emit(:connected) set_deferred_status(:succeeded) } @@ -181,14 +181,9 @@ def factory_connection @inactivity_response_timeout, @name ) - puts "Connecting..." connection.on(:connected) { - puts "maybe auth?" maybe_auth(connection).callback { - puts "inside maybe_auth" - puts "connection.on ping callback" - connection.ping.callback { - puts "inside connection.on ping callback!" + connection.ping.timeout(@inactivity_response_timeout).callback { connection.on(:message, &method(:message_callbacks)) connection.on(:pmessage, &method(:pmessage_callbacks)) @@ -211,8 +206,8 @@ def factory_connection connection.send_command(:psubscribe, *slice) } df.succeed(connection) - }.errback { - # Failure to auth counts as a connection failure + }.errback { |e| + # Failure to ping counts as a connection failure connection.close_connection df.fail(e) } diff --git a/lib/em-hiredis/pubsub_connection.rb b/lib/em-hiredis/pubsub_connection.rb index 34cbba6..e9f59a2 100644 --- a/lib/em-hiredis/pubsub_connection.rb +++ b/lib/em-hiredis/pubsub_connection.rb @@ -3,14 +3,13 @@ module PubsubConnection include EventMachine::Hiredis::EventEmitter PUBSUB_COMMANDS = %w{ping subscribe unsubscribe psubscribe punsubscribe}.freeze - PUBSUB_MESSAGES = (PUBSUB_COMMANDS + %w{message pmessage}).freeze + PUBSUB_MESSAGES = (PUBSUB_COMMANDS + %w{message pmessage pong}).freeze PING_CHANNEL = '__em-hiredis-ping' def initialize(inactivity_trigger_secs = nil, inactivity_response_timeout = 2, name = 'unnamed connection') - @name = name @reader = ::Hiredis::Reader.new @@ -19,8 +18,7 @@ def initialize(inactivity_trigger_secs = nil, @inactivity_checker = InactivityChecker.new(inactivity_trigger_secs, inactivity_response_timeout) @inactivity_checker.on(:activity_timeout) { EM::Hiredis.logger.debug("#{@name} - Sending ping") - send_command('subscribe', PING_CHANNEL) - send_command('unsubscribe', PING_CHANNEL) + send_command('ping') } @inactivity_checker.on(:response_timeout) { EM::Hiredis.logger.warn("#{@name} - Closing connection because of inactivity timeout") @@ -53,11 +51,8 @@ def auth(password) def ping - puts "ping method in pubsub_connection" df = @ping_df = EM::DefaultDeferrable.new - puts "send_data(marshal('ping'))" - send_data(marshal('ping')) - puts "return df" + send_command('ping') return df end @@ -119,7 +114,7 @@ def handle_response(reply) end @auth_df = nil elsif @ping_df - if reply.kind_of?(StandardError) + if reply.kind_of?(Exception) e = EM::Hiredis::RedisError.new(reply.message) e.redis_error = reply @ping_df.fail(e) diff --git a/lib/em-hiredis/redis_client.rb b/lib/em-hiredis/redis_client.rb index 624bc5f..a3fc5ea 100644 --- a/lib/em-hiredis/redis_client.rb +++ b/lib/em-hiredis/redis_client.rb @@ -313,7 +313,7 @@ def factory_connection connection.on(:connected) { maybe_auth(connection).callback { - connection.ping.callback { + connection.ping.timeout(2).callback { maybe_select(connection).callback { @command_queue.each { |command_df, command, args| connection.send_command(command_df, command, args) From 295e34f9a8362839c961a4600dc227466d559de1 Mon Sep 17 00:00:00 2001 From: Anya Zenkina Date: Thu, 8 Aug 2019 11:09:20 +0100 Subject: [PATCH 34/44] add timeout var --- lib/em-hiredis/redis_client.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/em-hiredis/redis_client.rb b/lib/em-hiredis/redis_client.rb index a3fc5ea..471e8c0 100644 --- a/lib/em-hiredis/redis_client.rb +++ b/lib/em-hiredis/redis_client.rb @@ -313,7 +313,7 @@ def factory_connection connection.on(:connected) { maybe_auth(connection).callback { - connection.ping.timeout(2).callback { + connection.ping.timeout(@inactivity_response_timeout).callback { maybe_select(connection).callback { @command_queue.each { |command_df, command, args| connection.send_command(command_df, command, args) From 807a8dcbd8f779ceed3c61115309e3c04d4407d1 Mon Sep 17 00:00:00 2001 From: Anya Zenkina Date: Mon, 12 Aug 2019 10:59:42 +0100 Subject: [PATCH 35/44] fix tests fix spec/client_conn_spec.rb fix spec/client_server_spec.rb fix ./spec/live/redis_commands_spec.rb fix tests --- lib/em-hiredis/pubsub_client.rb | 1 + lib/em-hiredis/pubsub_connection.rb | 2 - lib/em-hiredis/redis_client.rb | 1 + spec/client_conn_spec.rb | 9 +++-- spec/client_server_spec.rb | 59 ++++++++++++++++------------- spec/connection_manager_spec.rb | 48 +++++++++++------------ spec/live/pubsub_spec.rb | 2 +- spec/live/redis_commands_spec.rb | 3 +- spec/pubsub_client_conn_spec.rb | 50 ++++++++++-------------- spec/pubsub_connection_spec.rb | 12 +++--- 10 files changed, 94 insertions(+), 93 deletions(-) diff --git a/lib/em-hiredis/pubsub_client.rb b/lib/em-hiredis/pubsub_client.rb index 88d59b9..23ce8a4 100644 --- a/lib/em-hiredis/pubsub_client.rb +++ b/lib/em-hiredis/pubsub_client.rb @@ -94,6 +94,7 @@ def initialize( # Commands may be issued before or during connection, they will be queued # and submitted to the server once the connection is active. def connect + @inactivity_response_timeout = 2 if @inactivity_response_timeout.nil? @connection_manager.connect return self end diff --git a/lib/em-hiredis/pubsub_connection.rb b/lib/em-hiredis/pubsub_connection.rb index e9f59a2..3525abd 100644 --- a/lib/em-hiredis/pubsub_connection.rb +++ b/lib/em-hiredis/pubsub_connection.rb @@ -5,8 +5,6 @@ module PubsubConnection PUBSUB_COMMANDS = %w{ping subscribe unsubscribe psubscribe punsubscribe}.freeze PUBSUB_MESSAGES = (PUBSUB_COMMANDS + %w{message pmessage pong}).freeze - PING_CHANNEL = '__em-hiredis-ping' - def initialize(inactivity_trigger_secs = nil, inactivity_response_timeout = 2, name = 'unnamed connection') diff --git a/lib/em-hiredis/redis_client.rb b/lib/em-hiredis/redis_client.rb index 471e8c0..d4dd108 100644 --- a/lib/em-hiredis/redis_client.rb +++ b/lib/em-hiredis/redis_client.rb @@ -81,6 +81,7 @@ def initialize( # Commands may be issued before or during connection, they will be queued # and submitted to the server once the connection is active. def connect + @inactivity_response_timeout = 2 if @inactivity_response_timeout.nil? @connection_manager.connect return self end diff --git a/spec/client_conn_spec.rb b/spec/client_conn_spec.rb index 26c6c59..5b3fcca 100644 --- a/spec/client_conn_spec.rb +++ b/spec/client_conn_spec.rb @@ -11,11 +11,14 @@ class ClientTestConnection # Create expected_connections connections, inject them in order in to the # client as it creates new ones def mock_connections(expected_connections) - em = EM::Hiredis::MockConnectionEM.new(expected_connections, ClientTestConnection) + em { + em = EM::Hiredis::MockConnectionEM.new(expected_connections, ClientTestConnection) - yield EM::Hiredis::Client.new('redis://localhost:6379/9', nil, nil, em), em.connections + yield EM::Hiredis::Client.new('redis://localhost:6379/9', nil, nil, em), em.connections - em.connections.each { |c| c._expectations_met! } + em.connections.each { |c| c._expectations_met! } + done + } end it 'should queue commands issued while reconnecting' do diff --git a/spec/client_server_spec.rb b/spec/client_server_spec.rb index 13dc6a9..1d6043b 100644 --- a/spec/client_server_spec.rb +++ b/spec/client_server_spec.rb @@ -2,6 +2,13 @@ describe EM::Hiredis::Client do + around(:each) do |test| + EM.run do + test.run + EM.stop + end + end + def recording_server(replies = {}) em { yield NetworkedRedisMock::RedisMock.new(replies) @@ -13,7 +20,7 @@ def recording_server(replies = {}) it 'should not connect on construction' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') + client = EM::Hiredis::Client.new('redis://localhost:6381/9') server.connection_count.should == 0 done } @@ -21,7 +28,7 @@ def recording_server(replies = {}) it 'should be connected when connect is called' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') + client = EM::Hiredis::Client.new('redis://localhost:6381/9') client.connect.callback { server.connection_count.should == 1 done @@ -31,9 +38,9 @@ def recording_server(replies = {}) } end - it 'should issue ping command before succeeding connection' do + it 'should issue ping and select command before succeeding connection' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') + client = EM::Hiredis::Client.new('redis://localhost:6381/9') client.connect.callback { server.connection_count.should == 1 server.received[0].should == 'ping' @@ -47,7 +54,7 @@ def recording_server(replies = {}) it 'should issue ping command before succeeding connection if no db' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://127.0.0.1:6381') + client = EM::Hiredis::Client.new('redis://localhost:6381') client.connect.callback { server.connection_count.should == 1 server.received[0].should == 'ping' @@ -58,9 +65,9 @@ def recording_server(replies = {}) } end - it 'should issue pinf command before emitting :connected' do + it 'should issue ping command before emitting :connected' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') + client = EM::Hiredis::Client.new('redis://localhost:6381/9') client.on(:connected) { server.connection_count.should == 1 server.received[0].should == 'ping' @@ -76,7 +83,7 @@ def recording_server(replies = {}) it 'should emit :disconnected when the connection disconnects' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') + client = EM::Hiredis::Client.new('redis://localhost:6381/9') client.on(:disconnected) { done } @@ -92,7 +99,7 @@ def recording_server(replies = {}) it 'should create a new connection if the existing one reports it has failed' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') + client = EM::Hiredis::Client.new('redis://localhost:6381/9') client.connect.callback { server.kill_connections } @@ -105,7 +112,7 @@ def recording_server(replies = {}) it 'should emit both connected and reconnected' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') + client = EM::Hiredis::Client.new('redis://localhost:6381/9') client.connect.callback { callbacks = [] client.on(:connected) { @@ -131,7 +138,7 @@ def recording_server(replies = {}) it 'should make 4 attempts, emitting :reconnect_failed with a count' do em { - client = EM::Hiredis::Client.new('redis://127.0.0.1:9999') # assumes nothing listening on 9999 + client = EM::Hiredis::Client.new('redis://localhost:9999') # assumes nothing listening on 9999 expected = 1 client.on(:reconnect_failed) { |count| @@ -146,7 +153,7 @@ def recording_server(replies = {}) it 'after 4 unsuccessful attempts should emit :failed' do em { - client = EM::Hiredis::Client.new('redis://127.0.0.1:9999') # assumes nothing listening on 9999 + client = EM::Hiredis::Client.new('redis://localhost:9999') # assumes nothing listening on 9999 reconnect_count = 0 client.on(:reconnect_failed) { |count| @@ -181,7 +188,7 @@ def recording_server(replies = {}) it 'should recover from DNS resolution failure' do recording_server { |server| EM.stub(:connect).and_raise(EventMachine::ConnectionError.new) - client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') + client = EM::Hiredis::Client.new('redis://localhost:6381/9') client.on(:reconnect_failed) { EM.rspec_reset @@ -200,7 +207,7 @@ def recording_server(replies = {}) it 'should make 4 attempts, emitting :reconnect_failed with a count' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') + client = EM::Hiredis::Client.new('redis://localhost:6381/9') client.connect.callback { server.stop server.kill_connections @@ -217,7 +224,7 @@ def recording_server(replies = {}) it 'after 4 unsuccessful attempts should emit :failed' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') + client = EM::Hiredis::Client.new('redis://localhost:6381/9') client.connect.callback { server.stop server.kill_connections @@ -237,7 +244,7 @@ def recording_server(replies = {}) it 'should fail commands immediately when in a failed state' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') + client = EM::Hiredis::Client.new('redis://localhost:6381/9') client.connect.callback { client.on(:failed) { client.get('foo').errback { |e| @@ -254,7 +261,7 @@ def recording_server(replies = {}) it 'should be possible to trigger reconnect on request' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') + client = EM::Hiredis::Client.new('redis://localhost:6381/9') client.connect.callback { client.on(:reconnected) { server.connection_count.should == 2 @@ -268,7 +275,7 @@ def recording_server(replies = {}) it 'should do something sensible???' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') + client = EM::Hiredis::Client.new('redis://localhost:6381/9') client.reconnect client.ping.callback { done @@ -278,7 +285,7 @@ def recording_server(replies = {}) it 'should keep responses matched when connection is lost' do recording_server('get f' => '+hello') { |server| - client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') + client = EM::Hiredis::Client.new('redis://localhost:6381/9') client.connect.callback { client.get('a') client.get('b').callback { @@ -303,7 +310,7 @@ def recording_server(replies = {}) it 'should be able to send commands' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') + client = EM::Hiredis::Client.new('redis://localhost:6381/9') client.connect.callback { client.set('test', 'value').callback { done @@ -314,7 +321,7 @@ def recording_server(replies = {}) it 'should queue commands called before connect is called' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') + client = EM::Hiredis::Client.new('redis://localhost:6381/9') client.set('test', 'value').callback { client.ping.callback { done @@ -331,7 +338,7 @@ def recording_server(replies = {}) it 'should support alternative dbs' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/4') + client = EM::Hiredis::Client.new('redis://localhost:6381/4') client.connect.callback { server.received.should == ['ping','select 4'] done @@ -341,7 +348,7 @@ def recording_server(replies = {}) it 'should execute db selection first' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') + client = EM::Hiredis::Client.new('redis://localhost:6381/9') client.set('test', 'value').callback { client.ping.callback { server.received.should == [ @@ -359,7 +366,7 @@ def recording_server(replies = {}) it 'should class db selection failure as a connection failure' do recording_server('select 9' => '-ERR no such db') { |server| - client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') + client = EM::Hiredis::Client.new('redis://localhost:6381/9') client.connect.errback { |e| done } @@ -368,7 +375,7 @@ def recording_server(replies = {}) it 'should re-select db on reconnection' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/4') + client = EM::Hiredis::Client.new('redis://localhost:6381/4') client.connect.callback { client.ping.callback { client.on(:reconnected) { @@ -394,7 +401,7 @@ def recording_server(replies = {}) it 'should remember a change in the selected db' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') + client = EM::Hiredis::Client.new('redis://localhost:6381/9') client.connect.callback { client.select(4).callback { client.on(:reconnected) { diff --git a/spec/connection_manager_spec.rb b/spec/connection_manager_spec.rb index db87cae..bcd98cd 100644 --- a/spec/connection_manager_spec.rb +++ b/spec/connection_manager_spec.rb @@ -29,7 +29,7 @@ def expect_event_registration(event) context 'forcing reconnection' do it 'should be successful when disconnected from initial attempt failure' do em = EM::Hiredis::TimeMockEventMachine.new - conn_factory = double('connection factory') + conn_factory = mock('connection factory') manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) @@ -51,7 +51,7 @@ def expect_event_registration(event) manager.state.should == :connecting # Which will succeed - conn = double('connection') + conn = mock('connection') conn.expect_event_registration(:disconnected) second_conn_df.succeed(conn) @@ -63,7 +63,7 @@ def expect_event_registration(event) it 'should be successful when disconnected from existing connection, triggered on disconnected' do em = EM::Hiredis::TimeMockEventMachine.new - conn_factory = double('connection factory') + conn_factory = mock('connection factory') manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) @@ -77,7 +77,7 @@ def expect_event_registration(event) manager.connect - initial_conn = double('initial connection') + initial_conn = mock('initial connection') disconnected_callback = initial_conn.expect_event_registration(:disconnected) initial_conn_df.succeed(initial_conn) @@ -90,7 +90,7 @@ def expect_event_registration(event) manager.state.should == :connecting - second_conn = double('second connection') + second_conn = mock('second connection') second_conn.expect_event_registration(:disconnected) second_conn_df.succeed(second_conn) } @@ -105,7 +105,7 @@ def expect_event_registration(event) it 'should be successful when disconnected from existing connection, triggered on reconnect_failed' do em = EM::Hiredis::TimeMockEventMachine.new - conn_factory = double('connection factory') + conn_factory = mock('connection factory') manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) @@ -121,7 +121,7 @@ def expect_event_registration(event) manager.connect - initial_conn = double('initial connection') + initial_conn = mock('initial connection') disconnected_callback = initial_conn.expect_event_registration(:disconnected) initial_conn_df.succeed(initial_conn) @@ -134,7 +134,7 @@ def expect_event_registration(event) manager.state.should == :connecting - second_conn = double('second connection') + second_conn = mock('second connection') second_conn.expect_event_registration(:disconnected) second_conn_df.succeed(second_conn) } @@ -151,7 +151,7 @@ def expect_event_registration(event) it 'should cancel the connection in progress when already connecting' do em = EM::Hiredis::TimeMockEventMachine.new - conn_factory = double('connection factory') + conn_factory = mock('connection factory') manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) initial_conn_df = EM::DefaultDeferrable.new @@ -169,7 +169,7 @@ def expect_event_registration(event) manager.state.should == :connecting - in_progress_connection = double('in progress connection') + in_progress_connection = mock('in progress connection') # the connection in progress when we called reconnect should be # immediately closed, because we might have reconfigured to connect to # something different @@ -179,7 +179,7 @@ def expect_event_registration(event) # now we're trying to connect the replacement connection manager.state.should == :connecting - new_connection = double('replacement connection') + new_connection = mock('replacement connection') new_connection.expect_event_registration(:disconnected) second_conn_df.succeed(new_connection) @@ -189,7 +189,7 @@ def expect_event_registration(event) it 'should reconnect again when already connecting and in-progress connection fails' do em = EM::Hiredis::TimeMockEventMachine.new - conn_factory = double('connection factory') + conn_factory = mock('connection factory') manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) initial_conn_df = EM::DefaultDeferrable.new @@ -211,7 +211,7 @@ def expect_event_registration(event) # now we're trying to connect the replacement connection manager.state.should == :connecting - new_connection = double('replacement connection') + new_connection = mock('replacement connection') new_connection.expect_event_registration(:disconnected) second_conn_df.succeed(new_connection) @@ -221,7 +221,7 @@ def expect_event_registration(event) it 'should be successful when reconnecting' do em = EM::Hiredis::TimeMockEventMachine.new - conn_factory = double('connection factory') + conn_factory = mock('connection factory') manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) initial_conn_df = EM::DefaultDeferrable.new @@ -235,7 +235,7 @@ def expect_event_registration(event) manager.connect - initial_connection = double('initial connection') + initial_connection = mock('initial connection') disconnect_callback = initial_connection.expect_event_registration(:disconnected) initial_conn_df.succeed(initial_connection) @@ -254,7 +254,7 @@ def expect_event_registration(event) # now we're trying to connect the replacement connection manager.state.should == :connecting - new_connection = double('replacement connection') + new_connection = mock('replacement connection') new_connection.expect_event_registration(:disconnected) manual_reconnect_df.succeed(new_connection) @@ -263,7 +263,7 @@ def expect_event_registration(event) it 'should be successful when connected' do em = EM::Hiredis::TimeMockEventMachine.new - conn_factory = double('connection factory') + conn_factory = mock('connection factory') manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) @@ -276,7 +276,7 @@ def expect_event_registration(event) manager.connect - initial_conn = double('initial connection') + initial_conn = mock('initial connection') disconnected_callback = initial_conn.expect_event_registration(:disconnected) initial_conn_df.succeed(initial_conn) @@ -293,7 +293,7 @@ def expect_event_registration(event) # ...we complete the reconnect manager.state.should == :connecting - second_conn = double('second connection') + second_conn = mock('second connection') second_conn.should_receive(:on).with(:disconnected) second_conn_df.succeed(second_conn) @@ -305,7 +305,7 @@ def expect_event_registration(event) it 'should be successful when failed during initial connect' do em = EM::Hiredis::TimeMockEventMachine.new - conn_factory = double('connection factory') + conn_factory = mock('connection factory') manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) @@ -336,7 +336,7 @@ def expect_event_registration(event) manager.reconnect - conn = double('connection') + conn = mock('connection') conn.expect_event_registration(:disconnected) succeed_conn_df.succeed(conn) @@ -349,7 +349,7 @@ def expect_event_registration(event) it 'should be successful when failed after initial success' do em = EM::Hiredis::TimeMockEventMachine.new - conn_factory = double('connection factory') + conn_factory = mock('connection factory') manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) # Connect successfully, then five failed attempts takes us to failed, @@ -369,7 +369,7 @@ def expect_event_registration(event) manager.connect - initial_conn = double('initial connection') + initial_conn = mock('initial connection') disconnected_cb = initial_conn.expect_event_registration(:disconnected) initial_conn_df.succeed(initial_conn) @@ -389,7 +389,7 @@ def expect_event_registration(event) manager.reconnect - second_conn = double('second connection') + second_conn = mock('second connection') second_conn.expect_event_registration(:disconnected) second_conn_df.succeed(second_conn) diff --git a/spec/live/pubsub_spec.rb b/spec/live/pubsub_spec.rb index 0219361..2faf411 100644 --- a/spec/live/pubsub_spec.rb +++ b/spec/live/pubsub_spec.rb @@ -51,7 +51,7 @@ channel.should == channel message.should == 'foo' - callback_count.should == 3 + callback_count.should == 2 done } diff --git a/spec/live/redis_commands_spec.rb b/spec/live/redis_commands_spec.rb index b8a6ee0..326b0c9 100644 --- a/spec/live/redis_commands_spec.rb +++ b/spec/live/redis_commands_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' -describe EventMachine::Hiredis, "commands" do +describe EM::Hiredis, "commands" do + it "pings" do connect do |redis| redis.ping.callback { |r| r.should == 'PONG'; done } diff --git a/spec/pubsub_client_conn_spec.rb b/spec/pubsub_client_conn_spec.rb index a49c823..a23472b 100644 --- a/spec/pubsub_client_conn_spec.rb +++ b/spec/pubsub_client_conn_spec.rb @@ -10,21 +10,23 @@ class PubsubTestConnection # Create expected_connections connections, inject them in order in to the # client as it creates new ones def mock_connections(expected_connections, uri = 'redis://localhost:6379') - em = EM::Hiredis::MockConnectionEM.new(expected_connections, PubsubTestConnection) + em { + em = EM::Hiredis::MockConnectionEM.new(expected_connections, PubsubTestConnection) - yield EM::Hiredis::PubsubClient.new(uri, nil, nil, em), em.connections + yield EM::Hiredis::PubsubClient.new(uri, nil, nil, em), em.connections - em.connections.each { |c| c._expectations_met! } + em.connections.each { |c| c._expectations_met! } + done + } end + context '(un)subscribing' do it "should unsubscribe all callbacks for a channel on unsubscribe" do mock_connections(1) do |client, (connection)| client.connect - connection._expect_pubsub("subscribe __em-hiredis-ping") - connection._expect_pubsub("unsubscribe __em-hiredis-ping") - + connection._expect("ping") connection.connection_completed connection._expect_pubsub('subscribe channel') @@ -44,8 +46,7 @@ def mock_connections(expected_connections, uri = 'redis://localhost:6379') mock_connections(1) do |client, (connection)| client.connect - connection._expect_pubsub("subscribe __em-hiredis-ping") - connection._expect_pubsub("unsubscribe __em-hiredis-ping") + connection._expect("ping") connection.connection_completed @@ -56,8 +57,7 @@ def mock_connections(expected_connections, uri = 'redis://localhost:6379') it "should allow selective unsubscription" do mock_connections(1) do |client, (connection)| client.connect - connection._expect_pubsub("subscribe __em-hiredis-ping") - connection._expect_pubsub("unsubscribe __em-hiredis-ping") + connection._expect("ping") connection.connection_completed @@ -82,8 +82,7 @@ def mock_connections(expected_connections, uri = 'redis://localhost:6379') mock_connections(1) do |client, (connection)| client.connect - connection._expect_pubsub("subscribe __em-hiredis-ping") - connection._expect_pubsub("unsubscribe __em-hiredis-ping") + connection._expect("ping") connection.connection_completed connection._expect_pubsub('subscribe channel') @@ -109,8 +108,7 @@ def mock_connections(expected_connections, uri = 'redis://localhost:6379') mock_connections(1) do |client, (connection)| client.connect - connection._expect_pubsub("subscribe __em-hiredis-ping") - connection._expect_pubsub("unsubscribe __em-hiredis-ping") + connection._expect("ping") connection.connection_completed connection._expect_pubsub('psubscribe channel:*') @@ -131,8 +129,7 @@ def mock_connections(expected_connections, uri = 'redis://localhost:6379') mock_connections(1) do |client, (connection)| client.connect - connection._expect_pubsub("subscribe __em-hiredis-ping") - connection._expect_pubsub("unsubscribe __em-hiredis-ping") + connection._expect("ping") connection.connection_completed connection._expect_pubsub('psubscribe channel:*') @@ -155,8 +152,7 @@ def mock_connections(expected_connections, uri = 'redis://localhost:6379') it "should punsubscribe from redis when all psubscriptions for a pattern are punsubscribed" do mock_connections(1) do |client, (connection)| client.connect - connection._expect_pubsub("subscribe __em-hiredis-ping") - connection._expect_pubsub("unsubscribe __em-hiredis-ping") + connection._expect("ping") connection.connection_completed @@ -185,8 +181,7 @@ def mock_connections(expected_connections, uri = 'redis://localhost:6379') mock_connections(2) do |client, (conn_a, conn_b)| client.connect - conn_a._expect_pubsub("subscribe __em-hiredis-ping") - conn_a._expect_pubsub("unsubscribe __em-hiredis-ping") + conn_a._expect("ping") conn_a.connection_completed @@ -225,8 +220,7 @@ def mock_connections(expected_connections, uri = 'redis://localhost:6379') # Trigger a reconnection conn_a.unbind - conn_b._expect_pubsub("subscribe __em-hiredis-ping") - conn_b._expect_pubsub("unsubscribe __em-hiredis-ping") + conn_b._expect("ping") # All subs previously made should be re-made conn_b._expect_pubsub("subscribe #{channels.join(' ')}") @@ -259,8 +253,7 @@ def mock_connections(expected_connections, uri = 'redis://localhost:6379') client.connect.callback { connected = true } - connection._expect('subscribe __em-hiredis-ping') - connection._expect('unsubscribe __em-hiredis-ping') + connection._expect("ping") connection.connection_completed connected.should == true @@ -275,8 +268,7 @@ def mock_connections(expected_connections, uri = 'redis://localhost:6379') client.connect.callback { connected = true } - connection._expect_pubsub('subscribe __em-hiredis-ping') - connection._expect_pubsub('unsubscribe __em-hiredis-ping') + connection._expect("ping") connection.connection_completed connected.should == true @@ -304,8 +296,7 @@ def mock_connections(expected_connections, uri = 'redis://localhost:6379') connected = true } - connection._expect_pubsub('subscribe __em-hiredis-ping') - connection._expect_pubsub('unsubscribe __em-hiredis-ping') + connection._expect("ping") connection._expect_pubsub('subscribe channel') message_received = nil @@ -335,8 +326,7 @@ def mock_connections(expected_connections, uri = 'redis://localhost:6379') connected = true } - conn_b._expect('subscribe __em-hiredis-ping') - conn_b._expect('unsubscribe __em-hiredis-ping') + conn_b._expect("ping") conn_a.connection_completed connected.should == false diff --git a/spec/pubsub_connection_spec.rb b/spec/pubsub_connection_spec.rb index 9b97380..18969f5 100644 --- a/spec/pubsub_connection_spec.rb +++ b/spec/pubsub_connection_spec.rb @@ -131,7 +131,7 @@ def close_connection con.connection_completed EM.add_timer(3) { - con.sent.should include("*2\r\n$9\r\nsubscribe\r\n$17\r\n__em-hiredis-ping\r\n") + con.sent.should include("*1\r\n$4\r\nping\r\n") done } } @@ -148,7 +148,7 @@ def close_connection } EM.add_timer(3) { - con.sent.should_not include("*2\r\n$9\r\nsubscribe\r\n$17\r\n__em-hiredis-ping\r\n") + con.sent.should_not include("*2\r\n$9\r\nsubscribe\r\n$17\r\n*1\r\n$4\r\nping\r\n") done } } @@ -165,11 +165,11 @@ def close_connection } EM.add_timer(3) { - con.sent.should_not include("*2\r\n$9\r\nsubscribe\r\n$17\r\n__em-hiredis-ping\r\n") + con.sent.should_not include("*1\r\n$4\r\nping\r\n") } EM.add_timer(4) { - con.sent.should include("*2\r\n$9\r\nsubscribe\r\n$17\r\n__em-hiredis-ping\r\n") + con.sent.should include("*1\r\n$4\r\nping\r\n") done } } @@ -181,7 +181,7 @@ def close_connection con.connection_completed EM.add_timer(4) { - con.sent.should include("*2\r\n$9\r\nsubscribe\r\n$17\r\n__em-hiredis-ping\r\n") + con.sent.should include("*1\r\n$4\r\nping\r\n") con.closed.should == true done } @@ -199,7 +199,7 @@ def close_connection } EM.add_timer(4) { - con.sent.should include("*2\r\n$9\r\nsubscribe\r\n$17\r\n__em-hiredis-ping\r\n") + con.sent.should include("*1\r\n$4\r\nping\r\n") con.closed.should_not == true done } From 27038eb3758b8c3a98c2324cbe1a7f0b5bed8427 Mon Sep 17 00:00:00 2001 From: Anya Zenkina Date: Tue, 13 Aug 2019 10:33:38 +0100 Subject: [PATCH 36/44] add default_response_timeout --- lib/em-hiredis/pubsub_client.rb | 5 +-- lib/em-hiredis/pubsub_connection.rb | 3 ++ lib/em-hiredis/redis_client.rb | 5 +-- spec/client_server_spec.rb | 55 +++++++++++++---------------- 4 files changed, 33 insertions(+), 35 deletions(-) diff --git a/lib/em-hiredis/pubsub_client.rb b/lib/em-hiredis/pubsub_client.rb index 23ce8a4..6b24270 100644 --- a/lib/em-hiredis/pubsub_client.rb +++ b/lib/em-hiredis/pubsub_client.rb @@ -26,6 +26,7 @@ class PubsubClient include EventMachine::Deferrable RESUBSCRIBE_BATCH_SIZE = 1000 + DEFAULT_RESPONSE_TIMEOUT = 2 # seconds attr_reader :host, :port, :password @@ -49,7 +50,8 @@ def initialize( configure(uri) @inactivity_trigger_secs = inactivity_trigger_secs - @inactivity_response_timeout = inactivity_response_timeout + + @inactivity_response_timeout = inactivity_response_timeout || DEFAULT_RESPONSE_TIMEOUT # Subscribed channels and patterns to their callbacks # nil is a valid "callback", required because even if the user is using @@ -94,7 +96,6 @@ def initialize( # Commands may be issued before or during connection, they will be queued # and submitted to the server once the connection is active. def connect - @inactivity_response_timeout = 2 if @inactivity_response_timeout.nil? @connection_manager.connect return self end diff --git a/lib/em-hiredis/pubsub_connection.rb b/lib/em-hiredis/pubsub_connection.rb index 3525abd..fefe8ba 100644 --- a/lib/em-hiredis/pubsub_connection.rb +++ b/lib/em-hiredis/pubsub_connection.rb @@ -101,6 +101,8 @@ def marshal(*args) end def handle_response(reply) + # In a password-protected Redis server it starts accepting commands only after auth succeeds. + # Therefore it is not possible for ping_df to complete before auth_df if @auth_df # If we're awaiting a response to auth, we will not have sent any other commands if reply.kind_of?(RuntimeError) @@ -131,3 +133,4 @@ def handle_response(reply) end end end + diff --git a/lib/em-hiredis/redis_client.rb b/lib/em-hiredis/redis_client.rb index d4dd108..a33a21b 100644 --- a/lib/em-hiredis/redis_client.rb +++ b/lib/em-hiredis/redis_client.rb @@ -13,6 +13,8 @@ class Client include EventEmitter include EventMachine::Deferrable + DEFAULT_RESPONSE_TIMEOUT = 2 # seconds + attr_reader :host, :port, :password, :db # uri: @@ -36,7 +38,7 @@ def initialize( configure(uri) @inactivity_trigger_secs = inactivity_trigger_secs - @inactivity_response_timeout = inactivity_response_timeout + @inactivity_response_timeout = inactivity_response_timeout || DEFAULT_RESPONSE_TIMEOUT # Commands received while we are not initialized, to be sent once we are @command_queue = [] @@ -81,7 +83,6 @@ def initialize( # Commands may be issued before or during connection, they will be queued # and submitted to the server once the connection is active. def connect - @inactivity_response_timeout = 2 if @inactivity_response_timeout.nil? @connection_manager.connect return self end diff --git a/spec/client_server_spec.rb b/spec/client_server_spec.rb index 1d6043b..cb580c9 100644 --- a/spec/client_server_spec.rb +++ b/spec/client_server_spec.rb @@ -2,13 +2,6 @@ describe EM::Hiredis::Client do - around(:each) do |test| - EM.run do - test.run - EM.stop - end - end - def recording_server(replies = {}) em { yield NetworkedRedisMock::RedisMock.new(replies) @@ -20,7 +13,7 @@ def recording_server(replies = {}) it 'should not connect on construction' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') server.connection_count.should == 0 done } @@ -28,7 +21,7 @@ def recording_server(replies = {}) it 'should be connected when connect is called' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.connect.callback { server.connection_count.should == 1 done @@ -40,7 +33,7 @@ def recording_server(replies = {}) it 'should issue ping and select command before succeeding connection' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.connect.callback { server.connection_count.should == 1 server.received[0].should == 'ping' @@ -54,7 +47,7 @@ def recording_server(replies = {}) it 'should issue ping command before succeeding connection if no db' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381') client.connect.callback { server.connection_count.should == 1 server.received[0].should == 'ping' @@ -67,7 +60,7 @@ def recording_server(replies = {}) it 'should issue ping command before emitting :connected' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.on(:connected) { server.connection_count.should == 1 server.received[0].should == 'ping' @@ -83,7 +76,7 @@ def recording_server(replies = {}) it 'should emit :disconnected when the connection disconnects' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.on(:disconnected) { done } @@ -99,7 +92,7 @@ def recording_server(replies = {}) it 'should create a new connection if the existing one reports it has failed' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.connect.callback { server.kill_connections } @@ -112,7 +105,7 @@ def recording_server(replies = {}) it 'should emit both connected and reconnected' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.connect.callback { callbacks = [] client.on(:connected) { @@ -138,7 +131,7 @@ def recording_server(replies = {}) it 'should make 4 attempts, emitting :reconnect_failed with a count' do em { - client = EM::Hiredis::Client.new('redis://localhost:9999') # assumes nothing listening on 9999 + client = EM::Hiredis::Client.new('redis://127.0.0.1:9999') # assumes nothing listening on 9999 expected = 1 client.on(:reconnect_failed) { |count| @@ -153,7 +146,7 @@ def recording_server(replies = {}) it 'after 4 unsuccessful attempts should emit :failed' do em { - client = EM::Hiredis::Client.new('redis://localhost:9999') # assumes nothing listening on 9999 + client = EM::Hiredis::Client.new('redis://127.0.0.1:9999') # assumes nothing listening on 9999 reconnect_count = 0 client.on(:reconnect_failed) { |count| @@ -188,7 +181,7 @@ def recording_server(replies = {}) it 'should recover from DNS resolution failure' do recording_server { |server| EM.stub(:connect).and_raise(EventMachine::ConnectionError.new) - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.on(:reconnect_failed) { EM.rspec_reset @@ -207,7 +200,7 @@ def recording_server(replies = {}) it 'should make 4 attempts, emitting :reconnect_failed with a count' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.connect.callback { server.stop server.kill_connections @@ -224,7 +217,7 @@ def recording_server(replies = {}) it 'after 4 unsuccessful attempts should emit :failed' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.connect.callback { server.stop server.kill_connections @@ -244,7 +237,7 @@ def recording_server(replies = {}) it 'should fail commands immediately when in a failed state' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.connect.callback { client.on(:failed) { client.get('foo').errback { |e| @@ -261,7 +254,7 @@ def recording_server(replies = {}) it 'should be possible to trigger reconnect on request' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.connect.callback { client.on(:reconnected) { server.connection_count.should == 2 @@ -275,7 +268,7 @@ def recording_server(replies = {}) it 'should do something sensible???' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.reconnect client.ping.callback { done @@ -285,7 +278,7 @@ def recording_server(replies = {}) it 'should keep responses matched when connection is lost' do recording_server('get f' => '+hello') { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.connect.callback { client.get('a') client.get('b').callback { @@ -310,7 +303,7 @@ def recording_server(replies = {}) it 'should be able to send commands' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.connect.callback { client.set('test', 'value').callback { done @@ -321,7 +314,7 @@ def recording_server(replies = {}) it 'should queue commands called before connect is called' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.set('test', 'value').callback { client.ping.callback { done @@ -338,7 +331,7 @@ def recording_server(replies = {}) it 'should support alternative dbs' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/4') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/4') client.connect.callback { server.received.should == ['ping','select 4'] done @@ -348,7 +341,7 @@ def recording_server(replies = {}) it 'should execute db selection first' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.set('test', 'value').callback { client.ping.callback { server.received.should == [ @@ -366,7 +359,7 @@ def recording_server(replies = {}) it 'should class db selection failure as a connection failure' do recording_server('select 9' => '-ERR no such db') { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.connect.errback { |e| done } @@ -375,7 +368,7 @@ def recording_server(replies = {}) it 'should re-select db on reconnection' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/4') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/4') client.connect.callback { client.ping.callback { client.on(:reconnected) { @@ -401,7 +394,7 @@ def recording_server(replies = {}) it 'should remember a change in the selected db' do recording_server { |server| - client = EM::Hiredis::Client.new('redis://localhost:6381/9') + client = EM::Hiredis::Client.new('redis://127.0.0.1:6381/9') client.connect.callback { client.select(4).callback { client.on(:reconnected) { From 0df64ac22479d6a2482ca693f8e822cbbc426f53 Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Tue, 19 May 2020 11:19:35 +0100 Subject: [PATCH 37/44] Pin rake to a version which is supported with RSpec 2.x --- em-hiredis.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/em-hiredis.gemspec b/em-hiredis.gemspec index cfa2181..176cf8f 100644 --- a/em-hiredis.gemspec +++ b/em-hiredis.gemspec @@ -16,7 +16,7 @@ Gem::Specification.new do |s| s.add_development_dependency 'em-spec', '~> 0.2.5' s.add_development_dependency 'rspec', '~> 2.6.0' - s.add_development_dependency 'rake' + s.add_development_dependency 'rake', '~> 10' s.rubyforge_project = "em-hiredis" From 58d520a008c96d2f51ff8e50d154c249514aa781 Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Tue, 19 May 2020 11:20:53 +0100 Subject: [PATCH 38/44] Fix optional args using named args Adding a non-optional positional arg after an optional one broke things. If two agrs are specified, they are assumed to be the two non-optionals (first and third), meaning the second can't be specified. --- lib/em-hiredis/connection_manager.rb | 11 ++--------- lib/em-hiredis/pubsub_client.rb | 6 +++++- lib/em-hiredis/redis_client.rb | 8 ++++++-- spec/connection_manager_spec.rb | 18 +++++++++--------- 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/lib/em-hiredis/connection_manager.rb b/lib/em-hiredis/connection_manager.rb index e85e492..43fe78b 100755 --- a/lib/em-hiredis/connection_manager.rb +++ b/lib/em-hiredis/connection_manager.rb @@ -44,12 +44,12 @@ class ConnectionManager # deferrable which succeeds with a connected and initialised instance # of EMConnection or fails if the connection was unsuccessful. # Failures will be retried - def initialize(connection_factory, em = EM, reconnect_attempts) + def initialize(connection_factory:, reconnect_attempts: nil, em: EM) - @reconnect_attempts = initialise_reconnect_attempts(reconnect_attempts) @em = em @connection_factory = connection_factory + @reconnect_attempts = reconnect_attempts || DEFAULT_RECONNECT_ATTEMPTS @reconnect_attempt = 0 @sm = StateMachine.new @@ -171,12 +171,5 @@ def on_disconnected(prev_state) end end end - - private - - def initialise_reconnect_attempts(reconnect_attempts) - reconnect_attempts ||= DEFAULT_RECONNECT_ATTEMPTS - return reconnect_attempts - end end end diff --git a/lib/em-hiredis/pubsub_client.rb b/lib/em-hiredis/pubsub_client.rb index 6b24270..ebf498e 100644 --- a/lib/em-hiredis/pubsub_client.rb +++ b/lib/em-hiredis/pubsub_client.rb @@ -60,7 +60,11 @@ def initialize( @subscriptions = {} @psubscriptions = {} - @connection_manager = ConnectionManager.new(method(:factory_connection), em, reconnect_attempts) + @connection_manager = ConnectionManager.new( + connection_factory: method(:factory_connection), + reconnect_attempts: reconnect_attempts, + em: em, + ) @connection_manager.on(:connected) { EM::Hiredis.logger.info("#{@name} - Connected") diff --git a/lib/em-hiredis/redis_client.rb b/lib/em-hiredis/redis_client.rb index a33a21b..10fcc2b 100644 --- a/lib/em-hiredis/redis_client.rb +++ b/lib/em-hiredis/redis_client.rb @@ -14,7 +14,7 @@ class Client include EventMachine::Deferrable DEFAULT_RESPONSE_TIMEOUT = 2 # seconds - + attr_reader :host, :port, :password, :db # uri: @@ -43,7 +43,11 @@ def initialize( # Commands received while we are not initialized, to be sent once we are @command_queue = [] - @connection_manager = ConnectionManager.new(method(:factory_connection), em, reconnect_attempts) + @connection_manager = ConnectionManager.new( + connection_factory: method(:factory_connection), + reconnect_attempts: reconnect_attempts, + em: em, + ) @connection_manager.on(:connected) { EM::Hiredis.logger.info("#{@name} - Connected") diff --git a/spec/connection_manager_spec.rb b/spec/connection_manager_spec.rb index bcd98cd..79fbb58 100644 --- a/spec/connection_manager_spec.rb +++ b/spec/connection_manager_spec.rb @@ -31,7 +31,7 @@ def expect_event_registration(event) em = EM::Hiredis::TimeMockEventMachine.new conn_factory = mock('connection factory') - manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) + manager = EM::Hiredis::ConnectionManager.new(connection_factory: conn_factory, em: em) initial_conn_df = EM::DefaultDeferrable.new second_conn_df = EM::DefaultDeferrable.new @@ -65,7 +65,7 @@ def expect_event_registration(event) em = EM::Hiredis::TimeMockEventMachine.new conn_factory = mock('connection factory') - manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) + manager = EM::Hiredis::ConnectionManager.new(connection_factory: conn_factory, em: em) initial_conn_df = EM::DefaultDeferrable.new second_conn_df = EM::DefaultDeferrable.new @@ -107,7 +107,7 @@ def expect_event_registration(event) em = EM::Hiredis::TimeMockEventMachine.new conn_factory = mock('connection factory') - manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) + manager = EM::Hiredis::ConnectionManager.new(connection_factory: conn_factory, em: em) initial_conn_df = EM::DefaultDeferrable.new fail_conn_df = EM::DefaultDeferrable.new @@ -152,7 +152,7 @@ def expect_event_registration(event) em = EM::Hiredis::TimeMockEventMachine.new conn_factory = mock('connection factory') - manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) + manager = EM::Hiredis::ConnectionManager.new(connection_factory: conn_factory, em: em) initial_conn_df = EM::DefaultDeferrable.new second_conn_df = EM::DefaultDeferrable.new @@ -190,7 +190,7 @@ def expect_event_registration(event) em = EM::Hiredis::TimeMockEventMachine.new conn_factory = mock('connection factory') - manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) + manager = EM::Hiredis::ConnectionManager.new(connection_factory: conn_factory, em: em) initial_conn_df = EM::DefaultDeferrable.new second_conn_df = EM::DefaultDeferrable.new @@ -222,7 +222,7 @@ def expect_event_registration(event) em = EM::Hiredis::TimeMockEventMachine.new conn_factory = mock('connection factory') - manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) + manager = EM::Hiredis::ConnectionManager.new(connection_factory: conn_factory, em: em) initial_conn_df = EM::DefaultDeferrable.new auto_reconnect_df = EM::DefaultDeferrable.new @@ -265,7 +265,7 @@ def expect_event_registration(event) em = EM::Hiredis::TimeMockEventMachine.new conn_factory = mock('connection factory') - manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) + manager = EM::Hiredis::ConnectionManager.new(connection_factory: conn_factory, em: em) initial_conn_df = EM::DefaultDeferrable.new second_conn_df = EM::DefaultDeferrable.new @@ -307,7 +307,7 @@ def expect_event_registration(event) em = EM::Hiredis::TimeMockEventMachine.new conn_factory = mock('connection factory') - manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) + manager = EM::Hiredis::ConnectionManager.new(connection_factory: conn_factory, em: em) # Five failed attempts takes us to failed, then one successful at the end fail_conn_df = EM::DefaultDeferrable.new @@ -350,7 +350,7 @@ def expect_event_registration(event) em = EM::Hiredis::TimeMockEventMachine.new conn_factory = mock('connection factory') - manager = EM::Hiredis::ConnectionManager.new(conn_factory, em) + manager = EM::Hiredis::ConnectionManager.new(connection_factory: conn_factory, em: em) # Connect successfully, then five failed attempts takes us to failed, # then one successful at the end From 78af89a9f0d7c816c21610f39eabe73cba7d4d6f Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Tue, 19 May 2020 11:22:24 +0100 Subject: [PATCH 39/44] Fixnum is deprecated in Ruby 2.4+ --- lib/em-hiredis/lock.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/em-hiredis/lock.rb b/lib/em-hiredis/lock.rb index 0f03269..79c6e5d 100644 --- a/lib/em-hiredis/lock.rb +++ b/lib/em-hiredis/lock.rb @@ -13,7 +13,7 @@ class Lock def onexpire(&blk); @onexpire = blk; end def initialize(redis, key, timeout) - unless timeout.kind_of?(Fixnum) && timeout >= 1 + unless timeout.kind_of?(Integer) && timeout >= 1 raise "Timeout must be an integer and >= 1s" end @redis, @key, @timeout = redis, key, timeout @@ -58,7 +58,7 @@ def unlock df = EM::DefaultDeferrable.new @redis.lock_release([@key], [@token]).callback { |keys_removed| # DEBUGGING WTF - if !keys_removed.is_a?(Fixnum) + if !keys_removed.is_a?(Integer) EM::Hiredis.logger.error "#{to_s}: Received String where expected int [#{keys_removed.inspect}]" df.fail("WTF") elsif keys_removed > 0 From 447011b440254dbe1ff620d0e491ed176dab11d4 Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Tue, 19 May 2020 11:23:00 +0100 Subject: [PATCH 40/44] Test with recent Ruby versions --- .gitignore | 1 + .ruby-version | 1 + .travis.yml | 6 +++--- 3 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 .ruby-version diff --git a/.gitignore b/.gitignore index 5c51697..a0fa402 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ Gemfile.lock pkg/* .DS_Store *.swp +vendor diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..5154b3f --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.6 diff --git a/.travis.yml b/.travis.yml index 874c7cf..f5e4876 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: ruby rvm: -- 2.1.0 -- 2.0.0 -- 1.9.3 +- 2.4 +- 2.5 +- 2.6 notifications: hipchat: rooms: From c7aa5a5f71754b8b1d69491adfabf4a34c3e557d Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Tue, 19 May 2020 11:36:11 +0100 Subject: [PATCH 41/44] Upgrade hiredis --- em-hiredis.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/em-hiredis.gemspec b/em-hiredis.gemspec index 176cf8f..9cc47fd 100644 --- a/em-hiredis.gemspec +++ b/em-hiredis.gemspec @@ -12,7 +12,7 @@ Gem::Specification.new do |s| s.summary = %q{Eventmachine redis client} s.description = %q{Eventmachine redis client using hiredis native parser} - s.add_dependency 'hiredis', '~> 0.4.0' + s.add_dependency 'hiredis', '~> 0.6.0' s.add_development_dependency 'em-spec', '~> 0.2.5' s.add_development_dependency 'rspec', '~> 2.6.0' From 254335a0cf4ed0f43297df23bc37899100e1f487 Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Mon, 8 Jun 2020 12:31:02 +0100 Subject: [PATCH 42/44] Remove hipchat notification from travis config --- .travis.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index f5e4876..aa86232 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,9 +3,3 @@ rvm: - 2.4 - 2.5 - 2.6 -notifications: - hipchat: - rooms: - secure: CwoFDMZL7fLXLTxwb8lXVcArTpEZ8CjxXXwtjwoNo3RZ7QDa/0Wos7XVo8rL8Q6O5mTBHOL1XEZljqhvow20dZxLikrAtO3Tapnqhcgcb143vG3uvCAqO8wa/0WBm/7+3uslunL3Pm2Q0YA02Lxh7XClPsmVXHCbebYQsWDiILI= - template: - - '%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message} (Details/Change view)' From 2a4acc314fa885f361a9fdaa061139b708be206e Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Mon, 8 Jun 2020 12:33:04 +0100 Subject: [PATCH 43/44] Provide a redis instance to test again in travis --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index aa86232..d6bcab8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,3 +3,5 @@ rvm: - 2.4 - 2.5 - 2.6 +services: + - redis-server From 738d39dc768d6de945c90c5812cff23720d28af3 Mon Sep 17 00:00:00 2001 From: Michael Pye Date: Mon, 8 Jun 2020 13:28:22 +0100 Subject: [PATCH 44/44] Allow 200ms leeway testing lock timeout --- spec/live/lock_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/live/lock_spec.rb b/spec/live/lock_spec.rb index e2156ef..4192321 100644 --- a/spec/live/lock_spec.rb +++ b/spec/live/lock_spec.rb @@ -74,7 +74,7 @@ def new_lock it "times out" do start(3) { new_lock.acquire.callback { - EM.add_timer(2) { + EM.add_timer(2.2) { new_lock.acquire.callback { done }.errback { |e|