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,6 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .DS_Store
6
+ *.swp
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/LICENCE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (C) 2011 by Martyn Loughran
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
@@ -0,0 +1,85 @@
1
+ Getting started
2
+ ===============
3
+
4
+ Connect to redis:
5
+
6
+ require 'em-hiredis'
7
+ redis = EM::Hiredis.connect
8
+
9
+ Or, connect to redis with a redis URL (for a different host, port, password, DB)
10
+
11
+ redis = EM::Hiredis.connect("redis://:secretpassword@example.com:9000/4")
12
+
13
+ The client is a deferrable which succeeds when the underlying connection is established so you can bind to this. This isn't necessary however - any commands sent before the connection is established (or while reconnecting) will be sent to redis on connect.
14
+
15
+ redis.callback { puts "Redis now connected" }
16
+
17
+ All redis commands are available without any remapping of names
18
+
19
+ redis.set('foo', 'bar').callback {
20
+ redis.get('foo').callback { |value|
21
+ p [:returned, value]
22
+ }
23
+ }
24
+
25
+ As a shortcut, if you're only interested in binding to the success case you can simply provide a block to any command
26
+
27
+ redis.get('foo') { |value|
28
+ p [:returned, value]
29
+ }
30
+
31
+ Handling failure
32
+ ----------------
33
+
34
+ All commands return a deferrable. In the case that redis replies with an error (for example you called a hash operation against a set), or in the case that the redis connection is broken before the command returns, the deferrable will fail. If you care about the failure case you should bind to the errback - for example:
35
+
36
+ redis.sadd('aset', 'member').callback {
37
+ response_deferrable = redis.hget('aset', 'member')
38
+ response_deferrable.errback { |e|
39
+ p e # => #<RuntimeError: ERR Operation against a key holding the wrong kind of value>
40
+ }
41
+ }
42
+
43
+ Pubsub
44
+ ------
45
+
46
+ This example should explain things. Once a redis connection is in a pubsub state, you must make sure you only send pubsub commands.
47
+
48
+ redis = EM::Hiredis.connect
49
+ subscriber = EM::Hiredis.connect
50
+
51
+ subscriber.subscribe('bar.0')
52
+ subscriber.psubscribe('bar.*')
53
+
54
+ subscriber.on(:message) { |channel, message|
55
+ p [:message, channel, message]
56
+ }
57
+
58
+ subscriber.on(:pmessage) { |key, channel, message|
59
+ p [:pmessage, key, channel, message]
60
+ }
61
+
62
+ EM.add_periodic_timer(1) {
63
+ redis.publish("bar.#{rand(2)}", "hello").errback { |e|
64
+ p [:publisherror, e]
65
+ }
66
+ }
67
+
68
+ Hacking
69
+ -------
70
+
71
+ Hacking on em-hiredis is pretty simple, make sure you have Bundler installed:
72
+
73
+ gem install bundler
74
+ bundle
75
+
76
+ In order to run the tests you need to have a local redis server running on port 6379. Run all the tests:
77
+
78
+ # WARNING: The tests call flushdb on db 9 - this clears all keys!
79
+ bundle exec rake
80
+
81
+ To run an individual test:
82
+
83
+ bundle exec rspec spec/redis_commands_spec.rb
84
+
85
+ Many thanks to the em-redis gem for getting this gem bootstrapped with some tests.
@@ -0,0 +1,11 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+
6
+ desc 'Default: run specs.'
7
+ task :default => :spec
8
+
9
+ desc "Run specs"
10
+ RSpec::Core::RakeTask.new do |t|
11
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "em-hiredis/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "cs-em-hiredis"
7
+ s.version = EventMachine::Hiredis::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Martyn Loughran", "Yuri Niyazov"]
10
+ s.email = ["me@mloughran.com", "yuri.niyazov@gmail.com"]
11
+ s.homepage = "http://github.com/yn/em-hiredis"
12
+ s.summary = %q{Eventmachine redis client}
13
+ s.description = %q{Eventmachine redis client using hiredis native parser}
14
+
15
+ s.add_dependency 'hiredis', '~> 0.4.0'
16
+
17
+ s.add_development_dependency 'em-spec', '~> 0.2.5'
18
+ s.add_development_dependency 'rspec', '~> 2.6.0'
19
+ s.add_development_dependency 'rake'
20
+
21
+ s.files = `git ls-files`.split("\n")
22
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
23
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
24
+ s.require_paths = ["lib"]
25
+ end
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ $:.unshift(File.expand_path('../../lib', __FILE__))
3
+ require 'em-hiredis'
4
+
5
+ EM.run {
6
+ redis = EM::Hiredis.connect
7
+
8
+ # If you pass a block to subscribe it will be called whenever a message
9
+ # is received on this channel
10
+ redis.pubsub.subscribe('foo') { |message|
11
+ puts "Block received #{message}"
12
+ }
13
+
14
+ # You can also pass any other object which responds to call if you wish
15
+ callback = Proc.new { |message|
16
+ "Proc received #{message}"
17
+ }
18
+ df = redis.pubsub.subscribe('foo', callback)
19
+
20
+ # All calls return a deferrable
21
+ df.callback { |reply|
22
+ p [:subscription_succeeded, reply]
23
+ }
24
+
25
+ # Passing such an object is useful if you want to unsubscribe
26
+ redis.pubsub.unsubscribe_proc('foo', callback)
27
+
28
+ # Or if you want to call a method on a certain object
29
+ class Thing
30
+ def receive_message(message)
31
+ puts "Thing received #{message}"
32
+ end
33
+ end
34
+ redis.pubsub.subscribe('bar', Thing.new.method(:receive_message))
35
+
36
+ # You can also get all the following raw events:
37
+ # message pmessage subscribe unsubscribe psubscribe punsubscribe
38
+ redis.pubsub.on(:message) { |channel, message|
39
+ p [:message_received, channel, message]
40
+ }
41
+ redis.pubsub.on(:unsubscribe) { |channel, remaining_subscriptions|
42
+ p [:unsubscribe_happened, channel, remaining_subscriptions]
43
+ }
44
+
45
+ EM.add_timer(1) {
46
+ # You can also unsubscribe completely from a channel
47
+ redis.pubsub.unsubscribe('foo')
48
+
49
+ # Publishing events
50
+ redis.publish('bar', 'Hello')
51
+ }
52
+ }
@@ -0,0 +1,49 @@
1
+ require 'eventmachine'
2
+
3
+ module EventMachine
4
+ module Hiredis
5
+ class Error < RuntimeError
6
+ # In the case of error responses from Redis, the RuntimeError returned
7
+ # by ::Hiredis will be wrapped
8
+ attr_accessor :redis_error
9
+ end
10
+
11
+ class << self
12
+ attr_accessor :reconnect_timeout
13
+ end
14
+ self.reconnect_timeout = 0.5
15
+
16
+ def self.setup(uri = nil)
17
+ uri = uri || ENV["REDIS_URL"] || "redis://127.0.0.1:6379/0"
18
+ client = Client.new
19
+ client.configure(uri)
20
+ client
21
+ end
22
+
23
+ def self.connect(uri = nil)
24
+ client = setup(uri)
25
+ client.connect
26
+ client
27
+ end
28
+
29
+ def self.logger=(logger)
30
+ @@logger = logger
31
+ end
32
+
33
+ def self.logger
34
+ @@logger ||= begin
35
+ require 'logger'
36
+ log = Logger.new(STDOUT)
37
+ log.level = Logger::WARN
38
+ log
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ require 'em-hiredis/event_emitter'
45
+ require 'em-hiredis/connection'
46
+ require 'em-hiredis/base_client'
47
+ require 'em-hiredis/client'
48
+ require 'em-hiredis/pubsub_client'
49
+ require 'em-hiredis/lock'
@@ -0,0 +1,199 @@
1
+ require 'uri'
2
+
3
+ module EventMachine::Hiredis
4
+ # Emits the following events
5
+ #
6
+ # * :connected - on successful connection or reconnection
7
+ # * :reconnected - on successful reconnection
8
+ # * :disconnected - no longer connected, when previously in connected state
9
+ # * :reconnect_failed(failure_number) - a reconnect attempt failed
10
+ # This event is passed number of failures so far (1,2,3...)
11
+ # * :monitor
12
+ #
13
+ class BaseClient
14
+ include EventEmitter
15
+ include EM::Deferrable
16
+
17
+ attr_reader :host, :port, :password, :db
18
+
19
+ def initialize(host='localhost', port='6379', password=nil, db=nil)
20
+ @host, @port, @password, @db = host, port, password, db
21
+ @defs = []
22
+ @command_queue = []
23
+
24
+ @closing_connection = false
25
+ @reconnect_failed_count = 0
26
+ @reconnect_timer = nil
27
+ @failed = false
28
+
29
+ self.on(:failed) {
30
+ @failed = true
31
+ @command_queue.each do |df, _, _|
32
+ df.fail(Error.new("Redis connection in failed state"))
33
+ end
34
+ @command_queue = []
35
+ }
36
+ end
37
+
38
+ # Configure the redis connection to use
39
+ #
40
+ # In usual operation, the uri should be passed to initialize. This method
41
+ # is useful for example when failing over to a slave connection at runtime
42
+ #
43
+ def configure(uri_string)
44
+ uri = URI(uri_string)
45
+ @host = uri.host
46
+ @port = uri.port
47
+ @password = uri.password
48
+ path = uri.path[1..-1]
49
+ @db = path.empty? ? nil : path
50
+ end
51
+
52
+ def connect
53
+ @connection = EM.connect(@host, @port, Connection, @host, @port)
54
+
55
+ @connection.on(:closed) do
56
+ if @connected
57
+ @defs.each { |d| d.fail(Error.new("Redis disconnected")) }
58
+ @defs = []
59
+ @deferred_status = nil
60
+ @connected = false
61
+ unless @closing_connection
62
+ reconnect
63
+ end
64
+ emit(:disconnected)
65
+ EM::Hiredis.logger.info("#{@connection.to_s} disconnected")
66
+ else
67
+ unless @closing_connection
68
+ @reconnect_failed_count += 1
69
+ @reconnect_timer = EM.add_timer(EM::Hiredis.reconnect_timeout) {
70
+ @reconnect_timer = nil
71
+ reconnect
72
+ }
73
+ emit(:reconnect_failed, @reconnect_failed_count)
74
+ EM::Hiredis.logger.info("#{@connection.to_s} reconnect failed")
75
+
76
+ if @reconnect_failed_count >= 4
77
+ emit(:failed)
78
+ self.fail(Error.new("Could not connect after 4 attempts"))
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ @connection.on(:connected) do
85
+ @connected = true
86
+ @reconnect_failed_count = 0
87
+ @failed = false
88
+
89
+ select(@db) if @db
90
+ auth(@password) if @password
91
+
92
+ @command_queue.each do |df, command, args|
93
+ @connection.send_command(command, *args)
94
+ @defs.push(df)
95
+ end
96
+ @command_queue = []
97
+
98
+ emit(:connected)
99
+ EM::Hiredis.logger.info("#{@connection.to_s} connected")
100
+ succeed
101
+
102
+ if @reconnecting
103
+ @reconnecting = false
104
+ emit(:reconnected)
105
+ end
106
+ end
107
+
108
+ @connection.on(:message) do |reply|
109
+ if RuntimeError === reply
110
+ raise "Replies out of sync: #{reply.inspect}" if @defs.empty?
111
+ deferred = @defs.shift
112
+ error = Error.new("Error reply from redis")
113
+ error.redis_error = reply
114
+ deferred.fail(error) if deferred
115
+ else
116
+ handle_reply(reply)
117
+ end
118
+ end
119
+
120
+ @connected = false
121
+ @reconnecting = false
122
+
123
+ return self
124
+ end
125
+
126
+ # Indicates that commands have been sent to redis but a reply has not yet
127
+ # been received
128
+ #
129
+ # This can be useful for example to avoid stopping the
130
+ # eventmachine reactor while there are outstanding commands
131
+ #
132
+ def pending_commands?
133
+ @connected && @defs.size > 0
134
+ end
135
+
136
+ def connected?
137
+ @connected
138
+ end
139
+
140
+ def select(db, &blk)
141
+ @db = db
142
+ method_missing(:select, db, &blk)
143
+ end
144
+
145
+ def auth(password, &blk)
146
+ @password = password
147
+ method_missing(:auth, password, &blk)
148
+ end
149
+
150
+ def close_connection
151
+ EM.cancel_timer(@reconnect_timer) if @reconnect_timer
152
+ @closing_connection = true
153
+ @connection.close_connection_after_writing
154
+ end
155
+
156
+ def reconnect_connection
157
+ EM.cancel_timer(@reconnect_timer) if @reconnect_timer
158
+ reconnect
159
+ end
160
+
161
+ private
162
+
163
+ def method_missing(sym, *args)
164
+ deferred = EM::DefaultDeferrable.new
165
+ # Shortcut for defining the callback case with just a block
166
+ deferred.callback { |result| yield(result) } if block_given?
167
+
168
+ if @connected
169
+ @connection.send_command(sym, *args)
170
+ @defs.push(deferred)
171
+ elsif @failed
172
+ deferred.fail(Error.new("Redis connection in failed state"))
173
+ else
174
+ @command_queue << [deferred, sym, args]
175
+ end
176
+
177
+ deferred
178
+ end
179
+
180
+ def reconnect
181
+ @reconnecting = true
182
+ @connection.reconnect @host, @port
183
+ EM::Hiredis.logger.info("#{@connection.to_s} reconnecting")
184
+ end
185
+
186
+ def handle_reply(reply)
187
+ if @defs.empty?
188
+ if @monitoring
189
+ emit(:monitor, reply)
190
+ else
191
+ raise "Replies out of sync: #{reply.inspect}"
192
+ end
193
+ else
194
+ deferred = @defs.shift
195
+ deferred.succeed(reply) if deferred
196
+ end
197
+ end
198
+ end
199
+ end