celluloid_pubsub_redis_adapter 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,73 @@
1
+ require 'bundler/setup'
2
+ require 'celluloid_pubsub'
3
+ require 'celluloid_pubsub_redis_adapter'
4
+ require 'logger'
5
+
6
+ debug_enabled = ENV['DEBUG'].present? && ENV['DEBUG'].to_s == 'true'
7
+ use_redis = ENV['USE_REDIS'].to_s == 'true' ? 'redis': ''
8
+ log_file_path = File.join(File.expand_path(File.dirname(__FILE__)), 'log', 'celluloid_pubsub.log')
9
+
10
+ # actor that subscribes to a channel
11
+ class Subscriber
12
+ include Celluloid
13
+ include Celluloid::Logger
14
+
15
+ def initialize(options = {})
16
+ @client = CelluloidPubsub::Client.new({ actor: Actor.current, channel: 'test_channel' }.merge(options))
17
+ end
18
+
19
+ def on_message(message)
20
+ if @client.succesfull_subscription?(message)
21
+ puts "subscriber got successful subscription #{message.inspect}"
22
+ @client.publish('test_channel2', 'data' => ' subscriber got successfull subscription') # the message needs to be a Hash
23
+ else
24
+ puts "subscriber got message #{message.inspect}"
25
+ end
26
+ end
27
+
28
+ def on_close(code, reason)
29
+ puts "websocket connection closed: #{code.inspect}, #{reason.inspect}"
30
+ terminate
31
+ end
32
+
33
+
34
+ end
35
+
36
+ # actor that publishes a message in a channel
37
+ class Publisher
38
+ include Celluloid
39
+ include Celluloid::Logger
40
+
41
+ def initialize(options = {})
42
+ @client = CelluloidPubsub::Client.new({ actor: Actor.current, channel: 'test_channel2' }.merge(options))
43
+ end
44
+
45
+ def on_message(message)
46
+ if @client.succesfull_subscription?(message)
47
+ puts "publisher got successful subscription #{message.inspect}"
48
+ @client.publish('test_channel', 'data' => ' my_message') # the message needs to be a Hash
49
+ else
50
+ puts "publisher got message #{message.inspect}"
51
+ end
52
+ end
53
+
54
+ def on_close(code, reason)
55
+ puts "websocket connection closed: #{code.inspect}, #{reason.inspect}"
56
+ terminate
57
+ end
58
+
59
+ end
60
+
61
+
62
+ CelluloidPubsub::WebServer.supervise_as(:web_server, enable_debug: debug_enabled, adapter: use_redis,log_file_path: log_file_path )
63
+ Subscriber.supervise_as(:subscriber, enable_debug: debug_enabled)
64
+ Publisher.supervise_as(:publisher, enable_debug: debug_enabled)
65
+ signal_received = false
66
+
67
+ Signal.trap('INT') do
68
+ puts "\nAn interrupt signal has been triggered!"
69
+ signal_received = true
70
+ end
71
+
72
+ sleep 0.1 until signal_received
73
+ puts 'Exited succesfully! =)'
@@ -0,0 +1,2 @@
1
+ ENV['USE_REDIS'] = 'false'
2
+ require_relative './shared_classes'
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'celluloid_pubsub_redis_adapter'
@@ -0,0 +1,282 @@
1
+ require 'celluloid_pubsub/reactor'
2
+ require 'celluloid_pubsub/helper'
3
+ module CelluloidPubsub
4
+ # reactor used for redis pubsub
5
+ # @!attribute connected
6
+ # @return [Boolean] returns true if already connected to redis
7
+ # @!attribute connection
8
+ # @return [EM::Hiredis] The connection used for redis
9
+ class RedisReactor < CelluloidPubsub::Reactor
10
+ include Celluloid
11
+ include Celluloid::IO
12
+ include Celluloid::Logger
13
+ include CelluloidPubsub::Helper
14
+
15
+ attr_accessor :connected, :connection
16
+
17
+ alias_method :connected?, :connected
18
+
19
+ # returns true if already connected to redis otherwise false
20
+ #
21
+ # @return [Boolean] returns true if already connected to redis otherwise false
22
+ #
23
+ # @api public
24
+ def connected
25
+ @connected ||= false
26
+ end
27
+
28
+ # method used to unsubscribe from a channel
29
+ # @see #redis_action
30
+ #
31
+ # @return [void]
32
+ #
33
+ # @api public
34
+ def unsubscribe(channel)
35
+ super
36
+ async.redis_action('unsubscribe', channel)
37
+ end
38
+
39
+ # method used to subscribe to a channel
40
+ # @see #redis_action
41
+ #
42
+ # @return [void]
43
+ #
44
+ # @api public
45
+ def add_subscriber_to_channel(channel, message)
46
+ super
47
+ async.redis_action('subscribe', channel, message)
48
+ end
49
+
50
+ # method used to unsubscribe from a channel
51
+ # @see #redis_action
52
+ #
53
+ # @return [void]
54
+ #
55
+ # @api public
56
+ def unsubscribe_from_channel(channel)
57
+ super
58
+ async.redis_action('unsubscribe', channel)
59
+ end
60
+
61
+ # method used to unsubscribe from all channels
62
+ # @see #redis_action
63
+ #
64
+ # @return [void]
65
+ #
66
+ # @api public
67
+ def unsubscribe_all
68
+ info 'clearing connections'
69
+ shutdown
70
+ end
71
+
72
+ # method used to shutdown the reactor and unsubscribe from all channels
73
+ # @see #redis_action
74
+ #
75
+ # @return [void]
76
+ #
77
+ # @api public
78
+ def shutdown
79
+ @channels.dup.each do |channel|
80
+ redis_action('unsubscribe', channel)
81
+ end if @channels.present?
82
+ super
83
+ end
84
+
85
+ # method used to publish event using redis
86
+ #
87
+ # @return [void]
88
+ #
89
+ # @api public
90
+ def publish_event(topic, data)
91
+ return if topic.blank? || data.blank?
92
+ connect_to_redis do |connection|
93
+ connection.publish(topic, data)
94
+ end
95
+ rescue => exception
96
+ log_debug("could not publish message #{message} into topic #{current_topic} because of #{exception.inspect}")
97
+ end
98
+
99
+ private
100
+
101
+ # method used to run the enventmachine and setup the exception handler
102
+ # @see #run_the_eventmachine
103
+ # @see #setup_em_exception_handler
104
+ #
105
+ # @param [Proc] block the block that will use the connection
106
+ #
107
+ # @return [void]
108
+ #
109
+ # @api private
110
+ def connect_to_redis(&block)
111
+ require 'eventmachine'
112
+ require 'em-hiredis'
113
+ run_the_eventmachine(&block)
114
+ setup_em_exception_handler
115
+ end
116
+
117
+ # method used to connect to redis and yield the connection
118
+ #
119
+ # @param [Proc] block the block that will use the connection
120
+ #
121
+ # @return [void]
122
+ #
123
+ # @api private
124
+ def run_the_eventmachine(&block)
125
+ EM.run do
126
+ @connection ||= EM::Hiredis.connect
127
+ @connected = true
128
+ block.call @connection
129
+ end
130
+ end
131
+
132
+ # method used to setup the eventmachine exception handler
133
+ #
134
+ # @return [void]
135
+ #
136
+ # @api private
137
+ def setup_em_exception_handler
138
+ EM.error_handler do |error|
139
+ debug error unless filtered_error?(error)
140
+ end
141
+ end
142
+
143
+ # method used to fetch the pubsub client from the connection and yield it
144
+ #
145
+ # @return [void]
146
+ #
147
+ # @api private
148
+ def fetch_pubsub
149
+ connect_to_redis do |connection|
150
+ @pubsub ||= connection.pubsub
151
+ yield @pubsub if block_given?
152
+ end
153
+ end
154
+
155
+ # method used to fetch the pubsub client from the connection and yield it
156
+ # @see #action_subscribe
157
+ #
158
+ # @param [string] action The action that will be checked
159
+ # @param [string] channel The channel that reactor has subscribed to
160
+ # @param [string] message The initial message used to subscribe
161
+ #
162
+ # @return [void]
163
+ #
164
+ # @api private
165
+ def action_success(action, channel, message)
166
+ action_subscribe?(action) ? message.merge('client_action' => 'successful_subscription', 'channel' => channel) : nil
167
+ end
168
+
169
+ # method used execute an action (subscribe or unsubscribe ) to redis
170
+ # @see #prepare_redis_action
171
+ # @see #action_success
172
+ # @see #register_subscription_callbacks
173
+ #
174
+ # @param [string] action The action that will be checked
175
+ # @param [string] channel The channel that reactor has subscribed to
176
+ # @param [string] message The initial message used to subscribe
177
+ #
178
+ # @return [void]
179
+ #
180
+ # @api private
181
+ def redis_action(action, channel = nil, message = {})
182
+ fetch_pubsub do |pubsub|
183
+ callback = prepare_redis_action(pubsub, action)
184
+ success_message = action_success(action, channel, message)
185
+ args = action_subscribe?(action) ? [channel, callback] : [channel]
186
+ subscription = pubsub.send(action, *args)
187
+ register_subscription_callbacks(subscription, action, success_message)
188
+ end
189
+ end
190
+
191
+ # method used check if the action is subscribe and write the incoming message to be websocket or log the message otherwise
192
+ # @see #log_unsubscriptions
193
+ # @see #action_subscribe
194
+ #
195
+ # @param [String] action The action that will be checked if it is subscribed
196
+ #
197
+ # @return [void]
198
+ #
199
+ # @api private
200
+ def prepare_redis_action(pubsub, action)
201
+ log_unsubscriptions(pubsub)
202
+ proc do |subscribed_message|
203
+ action_subscribe?(action) ? (@websocket << subscribed_message) : log_debug(message)
204
+ end
205
+ end
206
+
207
+ # method used to listen to unsubscriptions and log them to log file
208
+ # @see #register_redis_callback
209
+ # @see #register_redis_error_callback
210
+ #
211
+ # @param [EM::Hiredis::PubsubClient] pubsub The pubsub client that will be used to listen to unsubscriptions
212
+ #
213
+ # @return [void]
214
+ #
215
+ # @api private
216
+ def log_unsubscriptions(pubsub)
217
+ pubsub.on(:unsubscribe) do |subscribed_channel, remaining_subscriptions|
218
+ log_debug [:unsubscribe_happened, subscribed_channel, remaining_subscriptions].inspect
219
+ end
220
+ end
221
+
222
+ # method used registers the sucess and error callabacks
223
+ # @see #register_redis_callback
224
+ # @see #register_redis_error_callback
225
+ #
226
+ # @param [EM::DefaultDeferrable] subscription The subscription object
227
+ # @param [string] action The action that will be checked
228
+ # @param [string] sucess_message The initial message used to subscribe
229
+ #
230
+ # @return [void]
231
+ #
232
+ # @api private
233
+ def register_subscription_callbacks(subscription, action, sucess_message = nil)
234
+ register_redis_callback(subscription, action, sucess_message)
235
+ register_redis_error_callback(subscription, action)
236
+ end
237
+
238
+ # the method will return true if debug is enabled
239
+ #
240
+ #
241
+ # @return [Boolean] returns true if debug is enabled otherwise false
242
+ #
243
+ # @api public
244
+ def debug_enabled?
245
+ @server.debug_enabled?
246
+ end
247
+
248
+ # method used to register a success callback and if action is subscribe will write
249
+ # back to the websocket a message that will say it is a successful_subscription
250
+ # If action is something else, will log the incoming message
251
+ # @see #log_debug
252
+ #
253
+ # @param [EM::DefaultDeferrable] subscription The subscription object
254
+ # @param [string] sucess_message The initial message used to subscribe
255
+ #
256
+ # @return [void]
257
+ #
258
+ # @api private
259
+ def register_redis_callback(subscription, action, sucess_message = nil)
260
+ subscription.callback do |subscriptions_ids|
261
+ if sucess_message.present?
262
+ @websocket << sucess_message.merge('subscriptions' => subscriptions_ids).to_json
263
+ else
264
+ log_debug "#{action} success #{sucess_message.inspect}"
265
+ end
266
+ end
267
+ end
268
+
269
+ # Register an error callback on the deferrable object and logs to file the incoming message
270
+ # @see #log_debug
271
+ #
272
+ # @param [EM::DefaultDeferrable] subscription The subscription object
273
+ # @param [string] action The action that will be checked
274
+ #
275
+ # @return [void]
276
+ #
277
+ # @api private
278
+ def register_redis_error_callback(subscription, action)
279
+ subscription.errback { |reply| log_debug "#{action} error #{reply.inspect}" }
280
+ end
281
+ end
282
+ end
@@ -0,0 +1,27 @@
1
+ # Returns the version of the gem as a <tt>Gem::Version</tt>
2
+ module CelluloidPubsubRedisAdapter
3
+ # it prints the gem version as a string
4
+ #
5
+ # @return [String]
6
+ #
7
+ # @api public
8
+ def self.gem_version
9
+ Gem::Version.new VERSION::STRING
10
+ end
11
+
12
+ # module used to generate the version string
13
+ # provides a easy way of getting the major, minor and tiny
14
+ module VERSION
15
+ # major release version
16
+ MAJOR = 0
17
+ # minor release version
18
+ MINOR = 0
19
+ # tiny release version
20
+ TINY = 1
21
+ # prelease version ( set this only if it is a prelease)
22
+ PRE = nil
23
+
24
+ # generates the version string
25
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
26
+ end
27
+ end
@@ -0,0 +1,2 @@
1
+ require 'celluloid_pubsub'
2
+ Gem.find_files('celluloid_pubsub_redis_adapter/**/*.rb').each { |path| require path }
@@ -0,0 +1,221 @@
1
+ # encoding:utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe CelluloidPubsub::RedisReactor do
6
+ let(:websocket) { mock }
7
+ let(:server) { mock }
8
+
9
+ before(:each) do
10
+ subject.stubs(:async).returns(subject)
11
+ server.stubs(:debug_enabled?).returns(false)
12
+ server.stubs(:async).returns(server)
13
+ server.stubs(:handle_dispatched_message)
14
+ server.stubs(:subscribers).returns({})
15
+ server.stubs(:redis_enabled?).returns(false)
16
+ websocket.stubs(:read)
17
+ websocket.stubs(:url)
18
+ websocket.stubs(:close)
19
+ websocket.stubs(:closed?).returns(false)
20
+ server.stubs(:alive?).returns(true)
21
+ subject.stubs(:inspect).returns(subject)
22
+ subject.stubs(:run)
23
+ subject.work(websocket, server)
24
+ subject.stubs(:unsubscribe_from_channel).returns(true)
25
+ Celluloid::Actor.stubs(:kill).returns(true)
26
+ end
27
+
28
+ describe '#work' do
29
+ it 'works ' do
30
+ subject.expects(:run)
31
+ subject.work(websocket, server)
32
+ expect(subject.websocket).to eq websocket
33
+ expect(subject.server).to eq server
34
+ expect(subject.channels).to eq []
35
+ end
36
+ end
37
+
38
+ # describe '#rub' do
39
+ # let(:data) { 'some message' }
40
+ #
41
+ # it 'works ' do
42
+ # subject.unstub(:run)
43
+ # websocket.stubs(:read).returns(data)
44
+ # subject.expects(:handle_websocket_message).with(data)
45
+ # subject.run
46
+ # end
47
+ # end
48
+
49
+ describe '#parse_json_data' do
50
+ let(:data) { 'some message' }
51
+ let(:expected) { data.to_json }
52
+
53
+ it 'works with hash ' do
54
+ JSON.expects(:parse).with(data).returns(expected)
55
+ actual = subject.parse_json_data(data)
56
+ expect(actual).to eq expected
57
+ end
58
+
59
+ it 'works with exception parsing ' do
60
+ JSON.expects(:parse).with(data).raises(StandardError)
61
+ actual = subject.parse_json_data(data)
62
+ expect(actual).to eq data
63
+ end
64
+ end
65
+
66
+ describe '#handle_websocket_message' do
67
+ let(:data) { 'some message' }
68
+ let(:json_data) { { a: 'b' } }
69
+
70
+ it 'handle_websocket_message' do
71
+ subject.expects(:parse_json_data).with(data).returns(json_data)
72
+ subject.expects(:handle_parsed_websocket_message).with(json_data)
73
+ subject.handle_websocket_message(data)
74
+ end
75
+ end
76
+
77
+ describe '#handle_parsed_websocket_message' do
78
+ it 'handle_websocket_message with a hash' do
79
+ data = { 'client_action' => 'b' }
80
+ data.expects(:stringify_keys).returns(data)
81
+ subject.expects(:delegate_action).with(data)
82
+ subject.handle_parsed_websocket_message(data)
83
+ end
84
+
85
+ it 'handle_websocket_message with something else than a hash' do
86
+ data = 'some message'
87
+ subject.expects(:handle_unknown_action).with(data)
88
+ subject.handle_parsed_websocket_message(data)
89
+ end
90
+ end
91
+
92
+ describe '#delegate_action' do
93
+ it 'unsubscribes all' do
94
+ data = { 'client_action' => 'unsubscribe_all' }
95
+ subject.expects(:unsubscribe_all).returns('bla')
96
+ subject.delegate_action(data)
97
+ end
98
+
99
+ it 'unsubscribes all' do
100
+ data = { 'client_action' => 'unsubscribe', 'channel' => 'some channel' }
101
+ subject.expects(:unsubscribe).with(data['channel'])
102
+ subject.delegate_action(data)
103
+ end
104
+
105
+ it 'subscribes to channell' do
106
+ data = { 'client_action' => 'subscribe', 'channel' => 'some channel' }
107
+ subject.expects(:start_subscriber).with(data['channel'], data)
108
+ subject.delegate_action(data)
109
+ end
110
+
111
+ it 'publish' do
112
+ data = { 'client_action' => 'publish', 'channel' => 'some channel', 'data' => 'some data' }
113
+ subject.expects(:publish_event).with(data['channel'], data['data'].to_json)
114
+ subject.delegate_action(data)
115
+ end
116
+
117
+ it 'handles unknown' do
118
+ data = { 'client_action' => 'some action', 'channel' => 'some channel' }
119
+ subject.expects(:handle_unknown_action).with(data)
120
+ subject.delegate_action(data)
121
+ end
122
+ end
123
+
124
+ describe '#handle_unknown_action' do
125
+ it 'handles unknown' do
126
+ data = 'some data'
127
+ server.expects(:handle_dispatched_message)
128
+ subject.handle_unknown_action(data)
129
+ end
130
+ end
131
+
132
+ describe '#unsubscribe_client' do
133
+ let(:channel) { 'some channel' }
134
+ it 'returns nil' do
135
+ act = subject.unsubscribe('')
136
+ expect(act).to eq(nil)
137
+ end
138
+
139
+ it 'unsubscribes' do
140
+ subject.channels.stubs(:blank?).returns(false)
141
+ subject.channels.expects(:delete).with(channel)
142
+ act = subject.unsubscribe(channel)
143
+ expect(act).to eq([])
144
+ end
145
+
146
+ it 'unsubscribes' do
147
+ subject.channels.stubs(:blank?).returns(true)
148
+ subject.websocket.expects(:close)
149
+ act = subject.unsubscribe(channel)
150
+ expect(act).to eq([])
151
+ end
152
+
153
+ it 'unsubscribes' do
154
+ subject.channels.stubs(:blank?).returns(false)
155
+ subject.channels.stubs(:delete)
156
+ server.stubs(:subscribers).returns("#{channel}" => [{ reactor: subject }])
157
+ subject.unsubscribe(channel)
158
+ expect(server.subscribers[channel]).to eq([])
159
+ end
160
+ end
161
+
162
+ describe '#shutdown' do
163
+ it 'shutdowns' do
164
+ subject.expects(:terminate)
165
+ subject.shutdown
166
+ end
167
+ end
168
+
169
+ describe '#start_subscriber' do
170
+ let(:channel) { 'some channel' }
171
+ let(:message) { { a: 'b' } }
172
+
173
+ it 'subscribes ' do
174
+ act = subject.start_subscriber('', message)
175
+ expect(act).to eq(nil)
176
+ end
177
+
178
+ it 'subscribes ' do
179
+ subject.stubs(:add_subscriber_to_channel).with(channel, message)
180
+ server.stubs(:redis_enabled?).returns(false)
181
+ subject.websocket.expects(:<<).with(message.merge('client_action' => 'successful_subscription', 'channel' => channel).to_json)
182
+ subject.start_subscriber(channel, message)
183
+ end
184
+
185
+ # it 'raises error' do
186
+ # subject.stubs(:add_subscriber_to_channel).raises(StandardError)
187
+ #
188
+ # expect do
189
+ # subject.start_subscriber(channel, message)
190
+ # end.to raise_error(StandardError) { |e|
191
+ # expect(e.message).to include(channel)
192
+ # }
193
+ # end
194
+ end
195
+
196
+ describe '#add_subscriber_to_channel' do
197
+ let(:channel) { 'some channel' }
198
+ let(:message) { { a: 'b' } }
199
+ let(:subscribers) { mock }
200
+
201
+ it 'adds subscribed' do
202
+ CelluloidPubsub::Registry.channels.stubs(:include?).with(channel).returns(false)
203
+ CelluloidPubsub::Registry.channels.expects(:<<).with(channel)
204
+ subject.expects(:channel_subscribers).with(channel).returns(subscribers)
205
+ subscribers.expects(:push).with(reactor: subject, message: message)
206
+ subject.add_subscriber_to_channel(channel, message)
207
+ expect(subject.channels).to include(channel)
208
+ end
209
+ end
210
+
211
+ describe '#unsubscribe_all' do
212
+ let(:channel) { 'some channel' }
213
+ let(:message) { { a: 'b' } }
214
+
215
+ it 'adds subscribed' do
216
+ CelluloidPubsub::Registry.stubs(:channels).returns([channel])
217
+ subject.expects(:unsubscribe_from_channel).with(channel)
218
+ subject.unsubscribe_all
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,47 @@
1
+ # Configure Rails Envinronment
2
+ ENV['RAILS_ENV'] = 'test'
3
+
4
+ require 'simplecov'
5
+ require 'simplecov-summary'
6
+ require 'coveralls'
7
+
8
+ # require "codeclimate-test-reporter"
9
+ formatters = [SimpleCov::Formatter::HTMLFormatter]
10
+
11
+ formatters << Coveralls::SimpleCov::Formatter # if ENV['TRAVIS']
12
+ # formatters << CodeClimate::TestReporter::Formatter # if ENV['CODECLIMATE_REPO_TOKEN'] && ENV['TRAVIS']
13
+
14
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[*formatters]
15
+
16
+ Coveralls.wear!
17
+ SimpleCov.start 'rails' do
18
+ add_filter 'spec'
19
+
20
+ at_exit {}
21
+ end
22
+
23
+ # CodeClimate::TestReporter.configure do |config|
24
+ # config.logger.level = Logger::WARN
25
+ # end
26
+ # CodeClimate::TestReporter.start
27
+
28
+ require 'bundler/setup'
29
+ require 'celluloid_pubsub'
30
+ require 'celluloid_pubsub_redis_adapter'
31
+
32
+ RSpec.configure do |config|
33
+ require 'rspec/expectations'
34
+ config.include RSpec::Matchers
35
+
36
+ config.mock_with :mocha
37
+
38
+ config.after(:suite) do
39
+ if SimpleCov.running
40
+ silence_stream(STDOUT) do
41
+ SimpleCov::Formatter::HTMLFormatter.new.format(SimpleCov.result)
42
+ end
43
+
44
+ SimpleCov::Formatter::SummaryFormatter.new.format(SimpleCov.result)
45
+ end
46
+ end
47
+ end