broadcaster 1.0.1 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 90a6948f7efefca09f7a8dae08c85402844b7cdc
4
- data.tar.gz: 27d4e8d9c47555c9b0a2e5153702eb96486e6a87
2
+ SHA256:
3
+ metadata.gz: ceb2fcd033b6405112a208cc1feed602dce814168f445235e07a1274848b66ff
4
+ data.tar.gz: 767ecb5b8008dcd2d4de5d7da5a194535212005acab53f29e70211658041dc36
5
5
  SHA512:
6
- metadata.gz: 201f6d54a4208e4c30456102d6bcd39bc07e5769976957eeee807372939e6d48cb66cbb6a7df3028ea25fc8b05af0a2aa7748b98a2a3d11ee1df40147e2f0ab9
7
- data.tar.gz: ae3d206cee7a2517e48dd1af011ba8f7db7a2d6bebf2cd41bea69a75b27402a0a9da1ad15f32d0362a8a0928f72ad9648fb9c7426304c4a6c73902d008a7c8d5
6
+ metadata.gz: 61c646e1eef1b4a3ae2a393b53c0432ae5607497799280b8f99aaa8a63206d868874082665aa5c070fff674d51816cd9445377aeed36553968421ca97ad5df78
7
+ data.tar.gz: 312d25d807de4f462f6c87cfe63ffac69de515db14be52c18603781051557d99ea3e94d2d0cb509c974e9e1558d2f1f7faa18cc604d6b6a6916b99deb7cc9378
data/.travis.yml CHANGED
@@ -4,12 +4,12 @@ rvm:
4
4
  - 2.0
5
5
  - 2.1
6
6
  - 2.2
7
- - 2.3.0
8
- - 2.4.0
9
- - 2.5.0
10
- - 2.6.0
11
- - jruby-9.1.17.0
12
- - jruby-9.2.7.0
7
+ - 2.3
8
+ - 2.4
9
+ - 2.5
10
+ - 2.6
11
+ - 2.7
12
+ - jruby-9.2.9.0
13
13
  - ruby-head
14
14
  - jruby-head
15
15
 
@@ -19,9 +19,5 @@ matrix:
19
19
  - rvm: ruby-head
20
20
  - rvm: jruby-head
21
21
 
22
- before_install:
23
- - rvm all-gemsets do gem uninstall bundler -ax || true
24
- - gem install bundler -v "< 2"
25
-
26
22
  services:
27
23
  - redis-server
data/README.md CHANGED
@@ -4,7 +4,6 @@
4
4
  [![Build Status](https://travis-ci.org/gabynaiman/broadcaster.svg?branch=master)](https://travis-ci.org/gabynaiman/broadcaster)
5
5
  [![Coverage Status](https://coveralls.io/repos/gabynaiman/broadcaster/badge.svg?branch=master)](https://coveralls.io/r/gabynaiman/broadcaster?branch=master)
6
6
  [![Code Climate](https://codeclimate.com/github/gabynaiman/broadcaster.svg)](https://codeclimate.com/github/gabynaiman/broadcaster)
7
- [![Dependency Status](https://gemnasium.com/gabynaiman/broadcaster.svg)](https://gemnasium.com/gabynaiman/broadcaster)
8
7
 
9
8
  Broadcasting based on Redis PubSub
10
9
 
data/broadcaster.gemspec CHANGED
@@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = 'broadcaster'
7
- spec.version = '1.0.1'
7
+ spec.version = '1.0.2'
8
8
  spec.authors = ['Gabriel Naiman']
9
9
  spec.email = ['gabynaiman@gmail.com']
10
10
  spec.summary = 'Broadcasting based on Redis PubSub'
@@ -20,8 +20,7 @@ Gem::Specification.new do |spec|
20
20
  spec.add_dependency 'redic', '~> 1.5'
21
21
  spec.add_dependency 'class_config', '~> 0.0'
22
22
 
23
- spec.add_development_dependency 'bundler', '~> 1.12'
24
- spec.add_development_dependency 'rake', '~> 11.0'
23
+ spec.add_development_dependency 'rake', '~> 12.0'
25
24
  spec.add_development_dependency 'minitest', '~> 5.0', '< 5.11'
26
25
  spec.add_development_dependency 'minitest-colorin', '~> 0.1'
27
26
  spec.add_development_dependency 'minitest-line', '~> 0.6'
data/lib/broadcaster.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'redic'
2
2
  require 'class_config'
3
3
  require 'logger'
4
+ require 'securerandom'
4
5
 
5
6
  class Broadcaster
6
7
 
@@ -9,30 +10,33 @@ class Broadcaster
9
10
  attr_config :redis_client, Redic
10
11
  attr_config :redis_settings, 'redis://localhost:6379'
11
12
  attr_config :logger, Logger.new(STDOUT)
13
+ attr_config :reconnection_timeout, 10
12
14
 
13
15
  attr_reader :id
14
16
 
15
17
  def initialize(options={})
16
18
  @id = options.fetch(:id, SecureRandom.uuid)
17
19
  @logger = options.fetch(:logger, Broadcaster.logger)
20
+ @logger_name = "Broadcaster (#{@id})"
18
21
  @redis_client = options.fetch(:redis_client, Broadcaster.redis_client)
19
- @publisher = @redis_client.new options.fetch(:redis_settings, Broadcaster.redis_settings)
20
- @subscriber = @redis_client.new options.fetch(:redis_settings, Broadcaster.redis_settings)
21
- @subscriptions = Hash.new { |h,k| h[k] = {} }
22
+ @redis_settings = options.fetch(:redis_settings, Broadcaster.redis_settings)
23
+ @publisher = establish_connection
24
+ @subscriptions = {}
22
25
  @mutex = Mutex.new
23
26
  listen
24
27
  end
25
28
 
26
29
  def publish(channel, message)
27
- logger.debug(self.class) { "Published | #{scoped(channel)} | #{message}" }
28
- publisher.call 'PUBLISH', scoped(channel), Marshal.dump(message)
30
+ publisher.call! 'PUBLISH', scoped(channel), Marshal.dump(message)
31
+ logger.debug(logger_name) { "Published | #{scoped(channel)} | #{message}" }
29
32
  end
30
33
 
31
34
  def subscribe(channel, callable=nil, &block)
32
35
  mutex.synchronize do
33
36
  SecureRandom.uuid.tap do |subscription_id|
34
- logger.debug(self.class) { "Subscribed | #{scoped(channel)} | #{subscription_id}" }
35
- subscriptions[scoped(channel)][subscription_id] = callable || block
37
+ channel_subscriptions = subscriptions[scoped(channel)] ||= {}
38
+ channel_subscriptions[subscription_id] = callable || block
39
+ logger.debug(logger_name) { "Subscribed | #{scoped(channel)} | #{subscription_id}" }
36
40
  end
37
41
  end
38
42
  end
@@ -41,9 +45,9 @@ class Broadcaster
41
45
  mutex.synchronize do
42
46
  channel, _ = subscriptions.detect { |k,v| v.key? subscription_id }
43
47
  if channel
44
- logger.debug(self.class) { "Unsubscribed | #{channel} | #{subscription_id}" }
45
48
  block = subscriptions[channel].delete subscription_id
46
49
  subscriptions.delete_if { |k,v| v.empty? }
50
+ logger.debug(logger_name) { "Unsubscribed | #{channel} | #{subscription_id}" }
47
51
  block
48
52
  end
49
53
  end
@@ -51,42 +55,69 @@ class Broadcaster
51
55
 
52
56
  def unsubscribe_all
53
57
  mutex.synchronize do
54
- logger.debug(self.class) { 'Unsubscribed all' }
58
+ logger.debug(logger_name) { 'Unsubscribed all' }
55
59
  subscriptions.clear
56
60
  end
57
61
  end
58
62
 
59
63
  private
60
64
 
61
- attr_reader :publisher, :subscriber, :subscriptions, :mutex, :logger
65
+ attr_reader :publisher, :subscriber, :subscriptions, :mutex, :logger, :logger_name, :redis_client, :redis_settings
66
+
67
+ def establish_connection
68
+ redis_client.new(redis_settings).tap { |redis| redis.call! 'PING' }
69
+ end
62
70
 
63
71
  def scoped(channel)
64
72
  "#{id}:#{channel}"
65
73
  end
66
74
 
67
75
  def listen
68
- logger.debug(self.class) { 'Start listening' }
69
- subscriber.call 'PSUBSCRIBE', scoped('*')
76
+ subscriber = establish_connection
77
+ subscriber.call! 'PSUBSCRIBE', scoped('*')
78
+
79
+ logger.info(logger_name) { 'Listener started' }
70
80
 
71
81
  Thread.new do
72
82
  loop do
73
83
  begin
74
84
  notification = subscriber.client.read
85
+
75
86
  channel = notification[2]
76
87
  message = Marshal.load notification[3]
77
- logger.debug(self.class) { "Broadcasting (#{subscriptions[channel].count}) | #{channel} | #{message}" }
78
- subscriptions[channel].each do |subscription_id, block|
79
- begin
80
- block.call message
81
- rescue => ex
82
- logger.error(self.class) { "Failed | #{channel} | #{subscription_id} | #{message}\n#{ex.class}: #{ex.message}\n#{ex.backtrace.join("\n")}" }
88
+
89
+ current_subscriptions = mutex.synchronize do
90
+ subscriptions.key?(channel) ? subscriptions[channel].dup : nil
91
+ end
92
+
93
+ if current_subscriptions
94
+ logger.debug(logger_name) { "Broadcasting (#{current_subscriptions.count}) | #{channel} | #{message}" }
95
+
96
+ current_subscriptions.each do |subscription_id, block|
97
+ begin
98
+ block.call message
99
+ rescue => ex
100
+ logger.error(logger_name) { "Failed | #{channel} | #{subscription_id} | #{message}\n#{ex.class}: #{ex.message}\n#{ex.backtrace.join("\n")}" }
101
+ end
83
102
  end
84
103
  end
85
- rescue java.nio.channels.ClosedSelectorException
86
- # JRuby - Safe closing thread
104
+
105
+ rescue => ex
106
+ logger.error(logger_name) { ex }
107
+ break
87
108
  end
88
109
  end
110
+
111
+ logger.warn(logger_name) { 'Listener broken' }
112
+ listen
89
113
  end
114
+
115
+ rescue => ex
116
+ logger.error(logger_name) { ex }
117
+ sleep Broadcaster.reconnection_timeout
118
+
119
+ logger.info(logger_name) { 'Listener reconnectig' }
120
+ retry
90
121
  end
91
122
 
92
123
  end
@@ -2,26 +2,14 @@ require 'minitest_helper'
2
2
 
3
3
  describe Broadcaster do
4
4
 
5
- class Receiver
6
- attr_reader :messages
7
-
8
- def initialize
9
- @messages = []
10
- end
11
-
12
- def call(message)
13
- messages << message
14
- end
15
- end
16
-
17
5
  def wait_for(&block)
18
- Timeout.timeout(3) do
6
+ Timeout.timeout(1) do
19
7
  while !block.call
20
8
  sleep 0.001
21
9
  end
22
10
  end
23
11
  end
24
-
12
+
25
13
  it 'Multiple channels' do
26
14
  broadcaster = Broadcaster.new
27
15
 
@@ -40,7 +28,7 @@ describe Broadcaster do
40
28
 
41
29
  wait_for { received_messages.values.flatten.count == 4 }
42
30
 
43
- received_messages.must_equal 'channel_1' => ['message 1', 'message 4'],
31
+ received_messages.must_equal 'channel_1' => ['message 1', 'message 4'],
44
32
  'channel_2' => ['message 2', 'message 3']
45
33
  end
46
34
 
@@ -69,7 +57,7 @@ describe Broadcaster do
69
57
 
70
58
  subscriptions = 2.times.map do |i|
71
59
  broadcaster.subscribe 'channel_1' do |message|
72
- received_messages[i] << message
60
+ received_messages[i] << message
73
61
  end
74
62
  end
75
63
 
@@ -94,7 +82,7 @@ describe Broadcaster do
94
82
 
95
83
  2.times.map do
96
84
  broadcaster.subscribe 'channel_1' do |message|
97
- received_messages << message
85
+ received_messages << message
98
86
  end
99
87
  end
100
88
 
@@ -149,7 +137,7 @@ describe Broadcaster do
149
137
  1 => ['message 1', 'message 2']
150
138
  end
151
139
 
152
- it 'Custom redis connection' do
140
+ it 'Invalid redis connection' do
153
141
  error = proc { Broadcaster.new redis_settings: 'redis://invalid_host:6379' }.must_raise StandardError
154
142
  error.message.must_match 'invalid_host'
155
143
  end
@@ -170,4 +158,29 @@ describe Broadcaster do
170
158
  receiver.messages.must_equal ['message 1', 'message 2', 'message 3']
171
159
  end
172
160
 
161
+ it 'Automatic reconnection' do
162
+ broadcaster = Broadcaster.new redis_client: MockRedis
163
+
164
+ received_messages = []
165
+
166
+ broadcaster.subscribe 'channel_1' do |message|
167
+ received_messages << message
168
+ end
169
+
170
+ broadcaster.publish 'channel_1', 'message 1'
171
+
172
+ MockRedis.stop
173
+
174
+ error = proc { broadcaster.publish 'channel_1', 'message 2' }.must_raise RuntimeError
175
+ error.message.must_equal 'Redis connection error'
176
+
177
+ MockRedis.start
178
+
179
+ broadcaster.publish 'channel_1', 'message 3'
180
+
181
+ wait_for { received_messages.count == 2 }
182
+
183
+ received_messages.must_equal ['message 1', 'message 3']
184
+ end
185
+
173
186
  end
@@ -12,4 +12,53 @@ require 'timeout'
12
12
 
13
13
  Broadcaster.configure do |config|
14
14
  config.logger = Logger.new '/dev/null'
15
+ config.reconnection_timeout = 0.001
16
+ end
17
+
18
+ class Receiver
19
+ attr_reader :messages
20
+
21
+ def initialize
22
+ @messages = []
23
+ end
24
+
25
+ def call(message)
26
+ messages << message
27
+ end
28
+ end
29
+
30
+ class MockRedis
31
+
32
+ class << self
33
+
34
+ def start
35
+ @running = true
36
+ sleep 0.05
37
+ end
38
+
39
+ def stop
40
+ @running = false
41
+ Redic.new.call 'CLIENT', 'KILL', 'TYPE', 'pubsub'
42
+ sleep 0.05
43
+ end
44
+
45
+ def running?
46
+ @running.nil? ? true : @running
47
+ end
48
+
49
+ end
50
+
51
+ def initialize(*args)
52
+ @redis = Redic.new(*args)
53
+ end
54
+
55
+ def call!(*args)
56
+ raise 'Redis connection error' unless self.class.running?
57
+ @redis.call!(*args)
58
+ end
59
+
60
+ def client
61
+ @redis.client
62
+ end
63
+
15
64
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: broadcaster
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gabriel Naiman
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-06-25 00:00:00.000000000 Z
11
+ date: 2021-03-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redic
@@ -38,34 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0.0'
41
- - !ruby/object:Gem::Dependency
42
- name: bundler
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '1.12'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '1.12'
55
41
  - !ruby/object:Gem::Dependency
56
42
  name: rake
57
43
  requirement: !ruby/object:Gem::Requirement
58
44
  requirements:
59
45
  - - "~>"
60
46
  - !ruby/object:Gem::Version
61
- version: '11.0'
47
+ version: '12.0'
62
48
  type: :development
63
49
  prerelease: false
64
50
  version_requirements: !ruby/object:Gem::Requirement
65
51
  requirements:
66
52
  - - "~>"
67
53
  - !ruby/object:Gem::Version
68
- version: '11.0'
54
+ version: '12.0'
69
55
  - !ruby/object:Gem::Dependency
70
56
  name: minitest
71
57
  requirement: !ruby/object:Gem::Requirement
@@ -195,8 +181,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
195
181
  - !ruby/object:Gem::Version
196
182
  version: '0'
197
183
  requirements: []
198
- rubyforge_project:
199
- rubygems_version: 2.5.1
184
+ rubygems_version: 3.0.6
200
185
  signing_key:
201
186
  specification_version: 4
202
187
  summary: Broadcasting based on Redis PubSub