cs-em-hiredis 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,51 @@
1
+ module EventMachine::Hiredis
2
+ class Client < BaseClient
3
+ def self.connect(host = 'localhost', port = 6379)
4
+ new(host, port).connect
5
+ end
6
+
7
+ def monitor(&blk)
8
+ @monitoring = true
9
+ method_missing(:monitor, &blk)
10
+ end
11
+
12
+ def info
13
+ df = method_missing(:info)
14
+ df.callback { |response|
15
+ info = {}
16
+ response.each_line do |line|
17
+ key, value = line.split(":", 2)
18
+ info[key.to_sym] = value.chomp
19
+ end
20
+ df.succeed(info)
21
+ }
22
+ df.callback { |info| yield info } if block_given?
23
+ df
24
+ end
25
+
26
+ # Gives access to a richer interface for pubsub subscriptions on a
27
+ # separate redis connection
28
+ #
29
+ def pubsub
30
+ @pubsub ||= begin
31
+ PubsubClient.new(@host, @port, @password, @db).connect
32
+ end
33
+ end
34
+
35
+ def subscribe(*channels)
36
+ raise "Use pubsub client"
37
+ end
38
+
39
+ def unsubscribe(*channels)
40
+ raise "Use pubsub client"
41
+ end
42
+
43
+ def psubscribe(channel)
44
+ raise "Use pubsub client"
45
+ end
46
+
47
+ def punsubscribe(channel)
48
+ raise "Use pubsub client"
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,70 @@
1
+ require 'hiredis/reader'
2
+
3
+ module EventMachine::Hiredis
4
+ class Connection < EM::Connection
5
+ include EventMachine::Hiredis::EventEmitter
6
+
7
+ def initialize(host, port)
8
+ super
9
+ @host, @port = host, port
10
+ end
11
+
12
+ def reconnect(host, port)
13
+ super
14
+ @host, @port = host, port
15
+ end
16
+
17
+ def connection_completed
18
+ EM::Hiredis.logger.debug("#{to_s}: connection open")
19
+ @reader = ::Hiredis::Reader.new
20
+ emit(:connected)
21
+ end
22
+
23
+ def receive_data(data)
24
+ @reader.feed(data)
25
+ until (reply = @reader.gets) == false
26
+ emit(:message, reply)
27
+ end
28
+ end
29
+
30
+ def unbind
31
+ EM::Hiredis.logger.debug("#{to_s}: connection unbound")
32
+ emit(:closed)
33
+ end
34
+
35
+ def send_command(sym, *args)
36
+ send_data(command(sym, *args))
37
+ end
38
+
39
+ def to_s
40
+ "Redis connection #{@host}:#{@port}"
41
+ end
42
+
43
+ protected
44
+
45
+ COMMAND_DELIMITER = "\r\n"
46
+
47
+ def command(*args)
48
+ command = []
49
+ command << "*#{args.size}"
50
+
51
+ args.each do |arg|
52
+ arg = arg.to_s
53
+ command << "$#{string_size arg}"
54
+ command << arg
55
+ end
56
+
57
+ command.join(COMMAND_DELIMITER) + COMMAND_DELIMITER
58
+ end
59
+
60
+ if "".respond_to?(:bytesize)
61
+ def string_size(string)
62
+ string.to_s.bytesize
63
+ end
64
+ else
65
+ def string_size(string)
66
+ string.to_s.size
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,29 @@
1
+ module EventMachine::Hiredis
2
+ module EventEmitter
3
+ def on(event, &listener)
4
+ _listeners[event] << listener
5
+ end
6
+
7
+ def emit(event, *args)
8
+ _listeners[event].each { |l| l.call(*args) }
9
+ end
10
+
11
+ def remove_listener(event, &listener)
12
+ _listeners[event].delete(listener)
13
+ end
14
+
15
+ def remove_all_listeners(event)
16
+ _listeners.delete(event)
17
+ end
18
+
19
+ def listeners(event)
20
+ _listeners[event]
21
+ end
22
+
23
+ private
24
+
25
+ def _listeners
26
+ @_listeners ||= Hash.new { |h,k| h[k] = [] }
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,89 @@
1
+ module EM::Hiredis
2
+ # Distributed lock built on redis
3
+ class Lock
4
+ def onexpire(&blk); @onexpire = blk; end
5
+
6
+ def initialize(redis, key, timeout)
7
+ @redis, @key, @timeout = redis, key, timeout
8
+ @locked = false
9
+ @expiry = nil
10
+ end
11
+
12
+ # Acquire the lock
13
+ #
14
+ # It is ok to call acquire again before the lock expires, which will attempt to extend the existing lock.
15
+ #
16
+ # Returns a deferrable which either succeeds if the lock can be acquired, or fails if it cannot. In both cases the expiry timestamp is returned (for the new lock or for the expired one respectively)
17
+ def acquire
18
+ df = EM::DefaultDeferrable.new
19
+ expiry = new_expiry
20
+ @redis.setnx(@key, expiry) { |setnx|
21
+ if setnx == 1
22
+ lock_acquired(expiry)
23
+ df.succeed(expiry)
24
+ else
25
+ attempt_to_acquire_existing_lock(df)
26
+ end
27
+ }
28
+ return df
29
+ end
30
+
31
+ # Release the lock
32
+ def unlock
33
+ EM.cancel_timer(@expire_timer) if @expire_timer
34
+
35
+ if @locked && Time.now.to_i < @expiry
36
+ EM::Hiredis.logger.debug "Lock: released #{@key}"
37
+ @redis.del(@key)
38
+ else
39
+ EM::Hiredis.logger.debug "Lock: could not release #{@key}"
40
+ end
41
+ end
42
+
43
+ # This should not be used in normal operation - force clear
44
+ def clear
45
+ @redis.del(@key)
46
+ end
47
+
48
+ private
49
+
50
+ def attempt_to_acquire_existing_lock(df)
51
+ @redis.get(@key) { |expiry_1|
52
+ expiry_1 = expiry_1.to_i
53
+ if expiry_1 == @expiry || expiry_1 < Time.now.to_i
54
+ # Either the lock was ours or the lock has already expired
55
+ expiry = new_expiry
56
+ @redis.getset(@key, expiry) { |expiry_2|
57
+ expiry_2 = expiry_2.to_i
58
+ if expiry_2 == @expiry || expiry_2 < Time.now.to_i
59
+ lock_acquired(expiry)
60
+ df.succeed(expiry)
61
+ else
62
+ # Another client got there first
63
+ EM::Hiredis.logger.debug "Lock: failed to acquire #{@key}"
64
+ df.fail(expiry_2)
65
+ end
66
+ }
67
+ else
68
+ EM::Hiredis.logger.debug "Lock: failed to acquire #{@key}"
69
+ df.fail(expiry_1)
70
+ end
71
+ }
72
+ end
73
+
74
+ def new_expiry
75
+ Time.now.to_i + @timeout + 1
76
+ end
77
+
78
+ def lock_acquired(expiry)
79
+ EM::Hiredis.logger.debug "Lock: acquired #{@key}"
80
+ @locked = true
81
+ @expiry = expiry
82
+ EM.cancel_timer(@expire_timer) if @expire_timer
83
+ @expire_timer = EM.add_timer(@timeout) {
84
+ EM::Hiredis.logger.debug "Lock: #{@key} will expire in 1s"
85
+ @onexpire.call if @onexpire
86
+ }
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,187 @@
1
+ module EventMachine::Hiredis
2
+ class PubsubClient < BaseClient
3
+ PUBSUB_MESSAGES = %w{message pmessage subscribe unsubscribe psubscribe punsubscribe}.freeze
4
+
5
+ def initialize(host='localhost', port='6379', password=nil, db=nil)
6
+ @subs, @psubs = [], []
7
+ @pubsub_defs = Hash.new { |h,k| h[k] = [] }
8
+ super
9
+ end
10
+
11
+ def connect
12
+ @sub_callbacks = Hash.new { |h, k| h[k] = [] }
13
+ @psub_callbacks = Hash.new { |h, k| h[k] = [] }
14
+
15
+ # Resubsubscribe to channels on reconnect
16
+ on(:reconnected) {
17
+ raw_send_command(:subscribe, *@subs) if @subs.any?
18
+ raw_send_command(:psubscribe, *@psubs) if @psubs.any?
19
+ }
20
+
21
+ super
22
+ end
23
+
24
+ # Subscribe to a pubsub channel
25
+ #
26
+ # If an optional proc / block is provided then it will be called when a
27
+ # message is received on this channel
28
+ #
29
+ # @return [Deferrable] Redis subscribe call
30
+ #
31
+ def subscribe(channel, proc = nil, &block)
32
+ if cb = proc || block
33
+ @sub_callbacks[channel] << cb
34
+ end
35
+ @subs << channel
36
+ raw_send_command(:subscribe, channel)
37
+ return pubsub_deferrable(channel)
38
+ end
39
+
40
+ # Unsubscribe all callbacks for a given channel
41
+ #
42
+ # @return [Deferrable] Redis unsubscribe call
43
+ #
44
+ def unsubscribe(channel)
45
+ @sub_callbacks.delete(channel)
46
+ @subs.delete(channel)
47
+ raw_send_command(:unsubscribe, channel)
48
+ return pubsub_deferrable(channel)
49
+ end
50
+
51
+ # Unsubscribe a given callback from a channel. Will unsubscribe from redis
52
+ # if there are no remaining subscriptions on this channel
53
+ #
54
+ # @return [Deferrable] Succeeds when the unsubscribe has completed or
55
+ # fails if callback could not be found. Note that success may happen
56
+ # immediately in the case that there are other callbacks for the same
57
+ # channel (and therefore no unsubscription from redis is necessary)
58
+ #
59
+ def unsubscribe_proc(channel, proc)
60
+ df = EM::DefaultDeferrable.new
61
+ if @sub_callbacks[channel].delete(proc)
62
+ if @sub_callbacks[channel].any?
63
+ # Succeed deferrable immediately - no need to unsubscribe
64
+ df.succeed
65
+ else
66
+ unsubscribe(channel).callback { |_|
67
+ df.succeed
68
+ }
69
+ end
70
+ else
71
+ df.fail
72
+ end
73
+ return df
74
+ end
75
+
76
+ # Pattern subscribe to a pubsub channel
77
+ #
78
+ # If an optional proc / block is provided then it will be called (with the
79
+ # channel name and message) when a message is received on a matching
80
+ # channel
81
+ #
82
+ # @return [Deferrable] Redis psubscribe call
83
+ #
84
+ def psubscribe(pattern, proc = nil, &block)
85
+ if cb = proc || block
86
+ @psub_callbacks[pattern] << cb
87
+ end
88
+ @psubs << pattern
89
+ raw_send_command(:psubscribe, pattern)
90
+ return pubsub_deferrable(pattern)
91
+ end
92
+
93
+ # Pattern unsubscribe all callbacks for a given pattern
94
+ #
95
+ # @return [Deferrable] Redis punsubscribe call
96
+ #
97
+ def punsubscribe(pattern)
98
+ @psub_callbacks.delete(pattern)
99
+ @psubs.delete(pattern)
100
+ raw_send_command(:punsubscribe, pattern)
101
+ return pubsub_deferrable(pattern)
102
+ end
103
+
104
+ # Unsubscribe a given callback from a pattern. Will unsubscribe from redis
105
+ # if there are no remaining subscriptions on this pattern
106
+ #
107
+ # @return [Deferrable] Succeeds when the punsubscribe has completed or
108
+ # fails if callback could not be found. Note that success may happen
109
+ # immediately in the case that there are other callbacks for the same
110
+ # pattern (and therefore no punsubscription from redis is necessary)
111
+ #
112
+ def punsubscribe_proc(pattern, proc)
113
+ df = EM::DefaultDeferrable.new
114
+ if @psub_callbacks[pattern].delete(proc)
115
+ if @psub_callbacks[pattern].any?
116
+ # Succeed deferrable immediately - no need to punsubscribe
117
+ df.succeed
118
+ else
119
+ punsubscribe(pattern).callback { |_|
120
+ df.succeed
121
+ }
122
+ end
123
+ else
124
+ df.fail
125
+ end
126
+ return df
127
+ end
128
+
129
+ private
130
+
131
+ # Send a command to redis without adding a deferrable for it. This is
132
+ # useful for commands for which replies work or need to be treated
133
+ # differently
134
+ def raw_send_command(sym, *args)
135
+ if @connected
136
+ @connection.send_command(sym, *args)
137
+ else
138
+ callback do
139
+ @connection.send_command(sym, *args)
140
+ end
141
+ end
142
+ return nil
143
+ end
144
+
145
+ def pubsub_deferrable(channel)
146
+ df = EM::DefaultDeferrable.new
147
+ @pubsub_defs[channel].push(df)
148
+ df
149
+ end
150
+
151
+ def handle_reply(reply)
152
+ if reply && PUBSUB_MESSAGES.include?(reply[0]) # reply can be nil
153
+ # Note: pmessage is the only message with 4 arguments
154
+ kind, subscription, d1, d2 = *reply
155
+
156
+ case kind.to_sym
157
+ when :message
158
+ if @sub_callbacks.has_key?(subscription)
159
+ @sub_callbacks[subscription].each { |cb| cb.call(d1) }
160
+ end
161
+ # Arguments are channel, message payload
162
+ emit(:message, subscription, d1)
163
+ when :pmessage
164
+ if @psub_callbacks.has_key?(subscription)
165
+ @psub_callbacks[subscription].each { |cb| cb.call(d1, d2) }
166
+ end
167
+ # Arguments are original pattern, channel, message payload
168
+ emit(:pmessage, subscription, d1, d2)
169
+ else
170
+ if @pubsub_defs[subscription].any?
171
+ df = @pubsub_defs[subscription].shift
172
+ df.succeed(d1)
173
+ # Cleanup empty arrays
174
+ if @pubsub_defs[subscription].empty?
175
+ @pubsub_defs.delete(subscription)
176
+ end
177
+ end
178
+
179
+ # Also emit the event, as an alternative to using the deferrables
180
+ emit(kind.to_sym, subscription, d1)
181
+ end
182
+ else
183
+ super
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,5 @@
1
+ module EventMachine
2
+ module Hiredis
3
+ VERSION = "0.1.2"
4
+ end
5
+ end
@@ -0,0 +1,115 @@
1
+ require 'spec_helper'
2
+
3
+ describe EM::Hiredis::BaseClient do
4
+ it "should be able to connect to redis (required for all tests!)" do
5
+ em {
6
+ redis = EM::Hiredis.connect
7
+ redis.callback {
8
+ done
9
+ }
10
+ redis.errback {
11
+ puts "CHECK THAT THE REDIS SERVER IS RUNNING ON PORT 6379"
12
+ fail
13
+ }
14
+ }
15
+ end
16
+
17
+ it "should emit an event on reconnect failure, with the retry count" do
18
+ # Assumes there is no redis server on 9999
19
+ connect(1, "redis://localhost:9999/") do |redis|
20
+ expected = 1
21
+ redis.on(:reconnect_failed) { |count|
22
+ count.should == expected
23
+ expected += 1
24
+ done if expected == 3
25
+ }
26
+ end
27
+ end
28
+
29
+ it "should emit disconnected when the connection closes" do
30
+ connect do |redis|
31
+ redis.on(:disconnected) {
32
+ done
33
+ }
34
+ redis.close_connection
35
+ end
36
+ end
37
+
38
+ it "should fail the client deferrable after 4 unsuccessful attempts" do
39
+ connect(1, "redis://localhost:9999/") do |redis|
40
+ events = []
41
+ redis.on(:reconnect_failed) { |count|
42
+ events << count
43
+ }
44
+ redis.errback { |error|
45
+ error.class.should == EM::Hiredis::Error
46
+ error.message.should == 'Could not connect after 4 attempts'
47
+ events.should == [1,2,3,4]
48
+ done
49
+ }
50
+ end
51
+ end
52
+
53
+ it "should fail commands immediately when in failed state" do
54
+ connect(1, "redis://localhost:9999/") do |redis|
55
+ redis.fail
56
+ redis.get('foo').errback { |error|
57
+ error.class.should == EM::Hiredis::Error
58
+ error.message.should == 'Redis connection in failed state'
59
+ done
60
+ }
61
+ end
62
+ end
63
+
64
+ it "should fail queued commands when entering failed state" do
65
+ connect(1, "redis://localhost:9999/") do |redis|
66
+ redis.get('foo').errback { |error|
67
+ error.class.should == EM::Hiredis::Error
68
+ error.message.should == 'Redis connection in failed state'
69
+ done
70
+ }
71
+ redis.fail
72
+ end
73
+ end
74
+
75
+ it "should allow reconfiguring the client at runtime" do
76
+ connect(1, "redis://localhost:9999/") do |redis|
77
+ redis.on(:reconnect_failed) {
78
+ redis.configure("redis://localhost:6379/9")
79
+ redis.info {
80
+ done
81
+ }
82
+ }
83
+ end
84
+ end
85
+
86
+ it "should allow connection to be reconnected" do
87
+ connect do |redis|
88
+ redis.on(:reconnected) {
89
+ done
90
+ }
91
+ # Wait for first connection to complete
92
+ redis.callback {
93
+ redis.reconnect_connection
94
+ }
95
+ end
96
+ end
97
+
98
+ it "should wrap error responses returned by redis" do
99
+ connect do |redis|
100
+ redis.sadd('foo', 'bar') {
101
+ df = redis.get('foo')
102
+ df.callback {
103
+ fail "Should have received error response from redis"
104
+ }
105
+ df.errback { |e|
106
+ e.class.should == EM::Hiredis::Error
107
+ e.message.should == 'Error reply from redis'
108
+ # This is the wrapped error from redis:
109
+ e.redis_error.message.should == 'ERR Operation against a key holding the wrong kind of value'
110
+ done
111
+ }
112
+ }
113
+ end
114
+ end
115
+ end