cs-em-hiredis 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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