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 +5 -5
- data/.travis.yml +6 -10
- data/README.md +0 -1
- data/broadcaster.gemspec +2 -3
- data/lib/broadcaster.rb +51 -20
- data/spec/broadcast_spec.rb +31 -18
- data/spec/minitest_helper.rb +49 -0
- metadata +5 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ceb2fcd033b6405112a208cc1feed602dce814168f445235e07a1274848b66ff
|
4
|
+
data.tar.gz: 767ecb5b8008dcd2d4de5d7da5a194535212005acab53f29e70211658041dc36
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
8
|
-
- 2.4
|
9
|
-
- 2.5
|
10
|
-
- 2.6
|
11
|
-
-
|
12
|
-
- jruby-9.2.
|
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
|
[](https://travis-ci.org/gabynaiman/broadcaster)
|
5
5
|
[](https://coveralls.io/r/gabynaiman/broadcaster?branch=master)
|
6
6
|
[](https://codeclimate.com/github/gabynaiman/broadcaster)
|
7
|
-
[](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.
|
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 '
|
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
|
-
@
|
20
|
-
@
|
21
|
-
@subscriptions =
|
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
|
-
|
28
|
-
|
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
|
-
|
35
|
-
|
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(
|
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
|
-
|
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
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
-
|
86
|
-
|
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
|
data/spec/broadcast_spec.rb
CHANGED
@@ -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(
|
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 '
|
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
|
data/spec/minitest_helper.rb
CHANGED
@@ -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.
|
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:
|
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: '
|
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: '
|
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
|
-
|
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
|