celluloid_pubsub_redis_adapter 0.0.1

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,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