broadcaster 1.0.1 → 1.0.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.
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