broadcaster 1.0.2 → 1.1.0
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 +4 -4
- data/.github/workflows/ci.yml +55 -0
- data/.gitignore +1 -0
- data/README.md +87 -4
- data/Rakefile +7 -5
- data/broadcaster.gemspec +14 -3
- data/lib/broadcaster/version.rb +3 -0
- data/lib/broadcaster/watchdog.rb +47 -0
- data/lib/broadcaster.rb +48 -9
- data/spec/broadcast_spec.rb +53 -2
- data/spec/minitest_helper.rb +47 -3
- metadata +10 -37
- data/.coveralls.yml +0 -2
- data/.travis.yml +0 -23
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8db15deb65dc4589963f402e1db172de1fd5cc31c8e46f99e34897a5dc1f3409
|
|
4
|
+
data.tar.gz: 57fb1bf72218bbfe631fd8547488b4408921ccec0cce2d09d389eb1cc5996f6b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 581d6bcc885ee1366c70e73bd70fad9ba4072b525046e6dde9bb0e0d7091c47edaacc643ee31b06658591c53b41d2a212be04ca7479f77b948f0da8b423e3bed
|
|
7
|
+
data.tar.gz: 012557be64bfdcda584ac04cea11f456cbc2c0f3f5eab7e83659260288259be837c8bf76e5dc548da1b5fe4d31aa5f11f2afae410651d20815358dfe77b99920
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [master]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
strategy:
|
|
14
|
+
fail-fast: false
|
|
15
|
+
matrix:
|
|
16
|
+
ruby-version: ['2.3', '2.4', '2.5', '2.6', '2.7', '3.0', '3.1', '3.2', '3.3', '3.4']
|
|
17
|
+
include:
|
|
18
|
+
- ruby-version: '4.0'
|
|
19
|
+
allow-failure: true
|
|
20
|
+
- ruby-version: ruby-head
|
|
21
|
+
allow-failure: true
|
|
22
|
+
- ruby-version: jruby-head
|
|
23
|
+
allow-failure: true
|
|
24
|
+
|
|
25
|
+
continue-on-error: ${{ matrix.allow-failure || false }}
|
|
26
|
+
|
|
27
|
+
services:
|
|
28
|
+
redis:
|
|
29
|
+
image: redis
|
|
30
|
+
ports:
|
|
31
|
+
- 6379:6379
|
|
32
|
+
options: >-
|
|
33
|
+
--health-cmd "redis-cli ping"
|
|
34
|
+
--health-interval 10s
|
|
35
|
+
--health-timeout 5s
|
|
36
|
+
--health-retries 5
|
|
37
|
+
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v4
|
|
40
|
+
|
|
41
|
+
- name: Set up Ruby ${{ matrix.ruby-version }}
|
|
42
|
+
uses: ruby/setup-ruby@v1
|
|
43
|
+
with:
|
|
44
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
45
|
+
bundler-cache: true
|
|
46
|
+
|
|
47
|
+
- name: Run tests
|
|
48
|
+
run: bundle exec rake spec
|
|
49
|
+
|
|
50
|
+
- name: Upload coverage to Codecov
|
|
51
|
+
if: matrix.ruby-version == '3.4'
|
|
52
|
+
uses: codecov/codecov-action@v5
|
|
53
|
+
with:
|
|
54
|
+
files: coverage/.resultset.json
|
|
55
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
data/.gitignore
CHANGED
data/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Broadcaster
|
|
2
2
|
|
|
3
3
|
[](https://rubygems.org/gems/broadcaster)
|
|
4
|
-
[](https://coveralls.io/r/gabynaiman/broadcaster?branch=master)
|
|
6
|
-
[](https://codeclimate.com/github/gabynaiman/broadcaster)
|
|
4
|
+
[](https://github.com/gabynaiman/broadcaster/actions/workflows/ci.yml)
|
|
7
5
|
|
|
8
6
|
Broadcasting based on Redis PubSub
|
|
9
7
|
|
|
8
|
+
Requires a running Redis server.
|
|
9
|
+
|
|
10
10
|
## Installation
|
|
11
11
|
|
|
12
12
|
Add this line to your application's Gemfile:
|
|
@@ -40,6 +40,46 @@ broadcaster.unsubscribe subscription_id
|
|
|
40
40
|
broadcaster.unsubscribe_all
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
Any callable object can be used instead of a block:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
class Receiver
|
|
47
|
+
def call(message)
|
|
48
|
+
puts message
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
broadcaster.subscribe 'channel_1', Receiver.new
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Messages are serialized with `Marshal`, so any serializable Ruby object can be published.
|
|
56
|
+
|
|
57
|
+
Each subscriber runs isolated from the others: if one raises an error it is logged
|
|
58
|
+
and the message is still delivered to the remaining subscribers.
|
|
59
|
+
|
|
60
|
+
## Scoping
|
|
61
|
+
|
|
62
|
+
Channels are scoped by the broadcaster `id`. Without an explicit `id` each instance
|
|
63
|
+
gets a random one, so instances are isolated from each other:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
broadcaster_1 = Broadcaster.new
|
|
67
|
+
broadcaster_2 = Broadcaster.new
|
|
68
|
+
|
|
69
|
+
broadcaster_2.subscribe('channel_1') { |message| puts message }
|
|
70
|
+
broadcaster_1.publish 'channel_1', 'text' # not received by broadcaster_2
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Sharing the same `id` makes them see each other's messages, even across processes:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
broadcaster_1 = Broadcaster.new id: 'my_app'
|
|
77
|
+
broadcaster_2 = Broadcaster.new id: 'my_app'
|
|
78
|
+
|
|
79
|
+
broadcaster_2.subscribe('channel_1') { |message| puts message }
|
|
80
|
+
broadcaster_1.publish 'channel_1', 'text' # received by broadcaster_2
|
|
81
|
+
```
|
|
82
|
+
|
|
43
83
|
## Global configuration
|
|
44
84
|
|
|
45
85
|
```ruby
|
|
@@ -52,13 +92,24 @@ Broadcaster.configure do |config|
|
|
|
52
92
|
end
|
|
53
93
|
```
|
|
54
94
|
|
|
95
|
+
| Setting | Default | Description |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `redis_client` | `Redic` | Redis client class |
|
|
98
|
+
| `redis_settings` | `'redis://localhost:6379'` | Connection settings passed to the client |
|
|
99
|
+
| `logger` | `Logger.new(STDOUT)` | Logger used by every broadcaster |
|
|
100
|
+
| `reconnection_timeout` | `10` | Seconds to wait before retrying a broken listener connection |
|
|
101
|
+
| `watchdog_interval` | `30` | Seconds between watchdog checks |
|
|
102
|
+
| `watchdog_timeout` | `120` | Seconds without listener activity before restarting it |
|
|
103
|
+
|
|
55
104
|
## Options
|
|
56
105
|
|
|
57
106
|
```ruby
|
|
58
107
|
options = {
|
|
59
108
|
id: 'my_app', # Shared broadcaster for multiple processes
|
|
60
109
|
redis_settings: 'redis://host_name:6379', # Custom redis connection
|
|
61
|
-
logger: Logger.new('/file.log')
|
|
110
|
+
logger: Logger.new('/file.log'), # Custom logger
|
|
111
|
+
watchdog_interval: 30, # Watchdog tuning for this instance
|
|
112
|
+
watchdog_timeout: 120
|
|
62
113
|
# or
|
|
63
114
|
redis_client: Redic::Sentinels,
|
|
64
115
|
redis_settings: hosts: [sentinel_1, sentinel_2], master_name: 'mymaster'
|
|
@@ -67,6 +118,38 @@ options = {
|
|
|
67
118
|
Broadcaster.new options
|
|
68
119
|
```
|
|
69
120
|
|
|
121
|
+
`reconnection_timeout` can only be set globally.
|
|
122
|
+
|
|
123
|
+
## Reconnection
|
|
124
|
+
|
|
125
|
+
The listener runs on its own thread. If the connection to Redis drops, it is
|
|
126
|
+
re-established automatically after `reconnection_timeout` seconds, keeping the
|
|
127
|
+
current subscriptions. Publishing while the connection is down raises an error.
|
|
128
|
+
|
|
129
|
+
## Watchdog
|
|
130
|
+
|
|
131
|
+
Reconnection relies on the connection raising an error. A half-open socket never
|
|
132
|
+
does: if the TCP connection is silently dropped, or a Redis Sentinel failover
|
|
133
|
+
promotes a new master, the listener stays blocked reading from a socket that will
|
|
134
|
+
never deliver another message. The process looks healthy while no message is
|
|
135
|
+
received, and nothing recovers on its own.
|
|
136
|
+
|
|
137
|
+
The watchdog covers that case. It publishes a ping on its own channel every
|
|
138
|
+
`watchdog_interval` seconds, and if no message reaches the listener for
|
|
139
|
+
`watchdog_timeout` seconds it restarts the listener over a new connection —
|
|
140
|
+
which also re-resolves the current master when using `Redic::Sentinels`.
|
|
141
|
+
|
|
142
|
+
`watchdog_timeout` must be comfortably greater than `watchdog_interval`, so that a
|
|
143
|
+
single lost ping does not trigger a restart.
|
|
144
|
+
|
|
145
|
+
## Shutdown
|
|
146
|
+
|
|
147
|
+
Stops the watchdog and the listener, and closes both connections:
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
broadcaster.shutdown
|
|
151
|
+
```
|
|
152
|
+
|
|
70
153
|
## Contributing
|
|
71
154
|
|
|
72
155
|
Bug reports and pull requests are welcome on GitHub at https://github.com/gabynaiman/broadcaster.
|
data/Rakefile
CHANGED
|
@@ -3,6 +3,7 @@ require 'rake/testtask'
|
|
|
3
3
|
|
|
4
4
|
Rake::TestTask.new(:spec) do |t|
|
|
5
5
|
t.libs << 'spec'
|
|
6
|
+
t.libs << 'lib'
|
|
6
7
|
t.pattern = 'spec/**/*_spec.rb'
|
|
7
8
|
t.verbose = false
|
|
8
9
|
t.warning = false
|
|
@@ -13,11 +14,12 @@ Rake::TestTask.new(:spec) do |t|
|
|
|
13
14
|
t.options << "-l #{ENV['LINE']} " if ENV['LINE'] && ENV['TEST']
|
|
14
15
|
end
|
|
15
16
|
|
|
17
|
+
task default: :spec
|
|
18
|
+
|
|
19
|
+
desc 'IRB console'
|
|
16
20
|
task :console do
|
|
17
|
-
require '
|
|
21
|
+
require 'irb'
|
|
18
22
|
require 'broadcaster'
|
|
19
23
|
ARGV.clear
|
|
20
|
-
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
task default: :spec
|
|
24
|
+
IRB.start
|
|
25
|
+
end
|
data/broadcaster.gemspec
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# coding: utf-8
|
|
2
2
|
lib = File.expand_path('../lib', __FILE__)
|
|
3
3
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require 'broadcaster/version'
|
|
4
5
|
|
|
5
6
|
Gem::Specification.new do |spec|
|
|
6
7
|
spec.name = 'broadcaster'
|
|
7
|
-
spec.version =
|
|
8
|
+
spec.version = Broadcaster::VERSION
|
|
8
9
|
spec.authors = ['Gabriel Naiman']
|
|
9
10
|
spec.email = ['gabynaiman@gmail.com']
|
|
10
11
|
spec.summary = 'Broadcasting based on Redis PubSub'
|
|
@@ -12,6 +13,8 @@ Gem::Specification.new do |spec|
|
|
|
12
13
|
spec.homepage = 'https://github.com/gabynaiman/broadcaster'
|
|
13
14
|
spec.license = 'MIT'
|
|
14
15
|
|
|
16
|
+
spec.required_ruby_version = '>= 2.3.0'
|
|
17
|
+
|
|
15
18
|
spec.files = `git ls-files -z`.split("\x0")
|
|
16
19
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
17
20
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
@@ -20,11 +23,19 @@ Gem::Specification.new do |spec|
|
|
|
20
23
|
spec.add_dependency 'redic', '~> 1.5'
|
|
21
24
|
spec.add_dependency 'class_config', '~> 0.0'
|
|
22
25
|
|
|
26
|
+
if RUBY_VERSION >= '4.0'
|
|
27
|
+
spec.add_dependency 'logger'
|
|
28
|
+
spec.add_dependency 'securerandom'
|
|
29
|
+
end
|
|
30
|
+
|
|
23
31
|
spec.add_development_dependency 'rake', '~> 12.0'
|
|
24
32
|
spec.add_development_dependency 'minitest', '~> 5.0', '< 5.11'
|
|
25
33
|
spec.add_development_dependency 'minitest-colorin', '~> 0.1'
|
|
26
34
|
spec.add_development_dependency 'minitest-line', '~> 0.6'
|
|
27
35
|
spec.add_development_dependency 'simplecov', '~> 0.12'
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
|
|
37
|
+
if RUBY_VERSION >= '3.3'
|
|
38
|
+
spec.add_development_dependency 'mutex_m'
|
|
39
|
+
spec.add_development_dependency 'ostruct'
|
|
40
|
+
end
|
|
30
41
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
class Broadcaster
|
|
2
|
+
class Watchdog
|
|
3
|
+
|
|
4
|
+
CHANNEL = '__watchdog__'.freeze
|
|
5
|
+
|
|
6
|
+
def initialize(broadcaster, interval:, timeout:, logger:)
|
|
7
|
+
@broadcaster = broadcaster
|
|
8
|
+
@interval = interval
|
|
9
|
+
@timeout = timeout
|
|
10
|
+
@logger = logger
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def start
|
|
14
|
+
@thread = Thread.new do
|
|
15
|
+
loop do
|
|
16
|
+
sleep interval
|
|
17
|
+
check
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def stop
|
|
23
|
+
thread.kill
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
attr_reader :broadcaster, :interval, :timeout, :logger, :thread
|
|
29
|
+
|
|
30
|
+
def check
|
|
31
|
+
broadcaster.publish CHANNEL, ping: Time.now.to_f
|
|
32
|
+
|
|
33
|
+
return unless unresponsive?
|
|
34
|
+
|
|
35
|
+
logger.warn(self.class.name) { "No notifications received for #{timeout}s" }
|
|
36
|
+
broadcaster.restart_listener
|
|
37
|
+
|
|
38
|
+
rescue => ex
|
|
39
|
+
logger.error(self.class.name) { "#{ex.class}: #{ex.message}" }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def unresponsive?
|
|
43
|
+
Time.now - broadcaster.last_notification_at > timeout
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/broadcaster.rb
CHANGED
|
@@ -3,6 +3,9 @@ require 'class_config'
|
|
|
3
3
|
require 'logger'
|
|
4
4
|
require 'securerandom'
|
|
5
5
|
|
|
6
|
+
require_relative 'broadcaster/version'
|
|
7
|
+
require_relative 'broadcaster/watchdog'
|
|
8
|
+
|
|
6
9
|
class Broadcaster
|
|
7
10
|
|
|
8
11
|
extend ClassConfig
|
|
@@ -11,8 +14,10 @@ class Broadcaster
|
|
|
11
14
|
attr_config :redis_settings, 'redis://localhost:6379'
|
|
12
15
|
attr_config :logger, Logger.new(STDOUT)
|
|
13
16
|
attr_config :reconnection_timeout, 10
|
|
17
|
+
attr_config :watchdog_interval, 30
|
|
18
|
+
attr_config :watchdog_timeout, 120
|
|
14
19
|
|
|
15
|
-
attr_reader :id
|
|
20
|
+
attr_reader :id, :last_notification_at
|
|
16
21
|
|
|
17
22
|
def initialize(options={})
|
|
18
23
|
@id = options.fetch(:id, SecureRandom.uuid)
|
|
@@ -23,7 +28,12 @@ class Broadcaster
|
|
|
23
28
|
@publisher = establish_connection
|
|
24
29
|
@subscriptions = {}
|
|
25
30
|
@mutex = Mutex.new
|
|
26
|
-
|
|
31
|
+
@watchdog = Watchdog.new self, logger: logger,
|
|
32
|
+
interval: options.fetch(:watchdog_interval, Broadcaster.watchdog_interval),
|
|
33
|
+
timeout: options.fetch(:watchdog_timeout, Broadcaster.watchdog_timeout)
|
|
34
|
+
|
|
35
|
+
start_listener
|
|
36
|
+
watchdog.start
|
|
27
37
|
end
|
|
28
38
|
|
|
29
39
|
def publish(channel, message)
|
|
@@ -60,28 +70,57 @@ class Broadcaster
|
|
|
60
70
|
end
|
|
61
71
|
end
|
|
62
72
|
|
|
73
|
+
def restart_listener
|
|
74
|
+
logger.warn(logger_name) { 'Restarting listener' }
|
|
75
|
+
|
|
76
|
+
listener.kill if listener && listener != Thread.current
|
|
77
|
+
disconnect subscriber
|
|
78
|
+
|
|
79
|
+
start_listener
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def shutdown
|
|
83
|
+
watchdog.stop
|
|
84
|
+
listener.kill
|
|
85
|
+
|
|
86
|
+
disconnect subscriber
|
|
87
|
+
disconnect publisher
|
|
88
|
+
|
|
89
|
+
logger.info(logger_name) { 'Shutdown' }
|
|
90
|
+
end
|
|
91
|
+
|
|
63
92
|
private
|
|
64
93
|
|
|
65
|
-
attr_reader :publisher, :subscriber, :
|
|
94
|
+
attr_reader :publisher, :subscriber, :listener, :watchdog, :subscriptions, :mutex, :logger, :logger_name,
|
|
95
|
+
:redis_client, :redis_settings
|
|
66
96
|
|
|
67
97
|
def establish_connection
|
|
68
98
|
redis_client.new(redis_settings).tap { |redis| redis.call! 'PING' }
|
|
69
99
|
end
|
|
70
100
|
|
|
101
|
+
def disconnect(connection)
|
|
102
|
+
connection.client.disconnect!
|
|
103
|
+
rescue => ex
|
|
104
|
+
logger.error(logger_name) { ex }
|
|
105
|
+
end
|
|
106
|
+
|
|
71
107
|
def scoped(channel)
|
|
72
108
|
"#{id}:#{channel}"
|
|
73
109
|
end
|
|
74
110
|
|
|
75
|
-
def
|
|
76
|
-
subscriber = establish_connection
|
|
111
|
+
def start_listener
|
|
112
|
+
@subscriber = establish_connection
|
|
77
113
|
subscriber.call! 'PSUBSCRIBE', scoped('*')
|
|
78
114
|
|
|
79
115
|
logger.info(logger_name) { 'Listener started' }
|
|
80
116
|
|
|
81
|
-
|
|
117
|
+
@last_notification_at = Time.now
|
|
118
|
+
|
|
119
|
+
@listener = Thread.new do
|
|
82
120
|
loop do
|
|
83
121
|
begin
|
|
84
122
|
notification = subscriber.client.read
|
|
123
|
+
@last_notification_at = Time.now
|
|
85
124
|
|
|
86
125
|
channel = notification[2]
|
|
87
126
|
message = Marshal.load notification[3]
|
|
@@ -109,15 +148,15 @@ class Broadcaster
|
|
|
109
148
|
end
|
|
110
149
|
|
|
111
150
|
logger.warn(logger_name) { 'Listener broken' }
|
|
112
|
-
|
|
151
|
+
restart_listener
|
|
113
152
|
end
|
|
114
153
|
|
|
115
154
|
rescue => ex
|
|
116
155
|
logger.error(logger_name) { ex }
|
|
117
156
|
sleep Broadcaster.reconnection_timeout
|
|
118
157
|
|
|
119
|
-
logger.info(logger_name) { 'Listener
|
|
158
|
+
logger.info(logger_name) { 'Listener reconnecting' }
|
|
120
159
|
retry
|
|
121
160
|
end
|
|
122
161
|
|
|
123
|
-
end
|
|
162
|
+
end
|
data/spec/broadcast_spec.rb
CHANGED
|
@@ -2,8 +2,8 @@ require 'minitest_helper'
|
|
|
2
2
|
|
|
3
3
|
describe Broadcaster do
|
|
4
4
|
|
|
5
|
-
def wait_for(&block)
|
|
6
|
-
Timeout.timeout(
|
|
5
|
+
def wait_for(timeout=1, &block)
|
|
6
|
+
Timeout.timeout(timeout) do
|
|
7
7
|
while !block.call
|
|
8
8
|
sleep 0.001
|
|
9
9
|
end
|
|
@@ -183,4 +183,55 @@ describe Broadcaster do
|
|
|
183
183
|
received_messages.must_equal ['message 1', 'message 3']
|
|
184
184
|
end
|
|
185
185
|
|
|
186
|
+
describe 'Watchdog' do
|
|
187
|
+
|
|
188
|
+
def with_blocked_listener(options={})
|
|
189
|
+
HangingRedis.reset
|
|
190
|
+
|
|
191
|
+
broadcaster = Broadcaster.new options.merge(redis_client: HangingRedis)
|
|
192
|
+
wait_for { HangingRedis.blocked_reads > 0 }
|
|
193
|
+
HangingRedis.hanging = false
|
|
194
|
+
|
|
195
|
+
yield broadcaster
|
|
196
|
+
|
|
197
|
+
ensure
|
|
198
|
+
broadcaster.shutdown if broadcaster
|
|
199
|
+
HangingRedis.reset
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
it 'Restarts a listener blocked on a silent connection' do
|
|
203
|
+
with_blocked_listener watchdog_interval: 0.05, watchdog_timeout: 0.2 do |broadcaster|
|
|
204
|
+
received_messages = []
|
|
205
|
+
|
|
206
|
+
broadcaster.subscribe 'channel_1' do |message|
|
|
207
|
+
received_messages << message
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
wait_for(10) do
|
|
211
|
+
broadcaster.publish 'channel_1', 'message 1'
|
|
212
|
+
sleep 0.1
|
|
213
|
+
received_messages.any?
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
received_messages.must_include 'message 1'
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
it 'Does not restart the listener before the timeout elapses' do
|
|
221
|
+
with_blocked_listener watchdog_interval: 0.05, watchdog_timeout: 60 do |broadcaster|
|
|
222
|
+
received_messages = []
|
|
223
|
+
|
|
224
|
+
broadcaster.subscribe 'channel_1' do |message|
|
|
225
|
+
received_messages << message
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
broadcaster.publish 'channel_1', 'message 1'
|
|
229
|
+
sleep 0.5
|
|
230
|
+
|
|
231
|
+
received_messages.must_be_empty
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
end
|
|
236
|
+
|
|
186
237
|
end
|
data/spec/minitest_helper.rb
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
require 'simplecov'
|
|
2
|
-
require 'coveralls'
|
|
3
2
|
|
|
4
|
-
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new [SimpleCov::Formatter::HTMLFormatter, Coveralls::SimpleCov::Formatter]
|
|
5
3
|
SimpleCov.start
|
|
6
4
|
|
|
7
5
|
require 'broadcaster'
|
|
8
6
|
require 'minitest/autorun'
|
|
9
7
|
require 'minitest/colorin'
|
|
10
|
-
require 'pry-nav'
|
|
11
8
|
require 'timeout'
|
|
12
9
|
|
|
13
10
|
Broadcaster.configure do |config|
|
|
@@ -61,4 +58,51 @@ class MockRedis
|
|
|
61
58
|
@redis.client
|
|
62
59
|
end
|
|
63
60
|
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class HangingRedis
|
|
64
|
+
|
|
65
|
+
class << self
|
|
66
|
+
|
|
67
|
+
attr_writer :hanging
|
|
68
|
+
attr_accessor :blocked_reads
|
|
69
|
+
|
|
70
|
+
def hanging?
|
|
71
|
+
@hanging.nil? ? true : @hanging
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def reset
|
|
75
|
+
@hanging = nil
|
|
76
|
+
@blocked_reads = 0
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def blocked_reads
|
|
80
|
+
@blocked_reads ||= 0
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def initialize(*args)
|
|
86
|
+
@redis = Redic.new(*args)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def call!(*args)
|
|
90
|
+
@redis.call!(*args)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def client
|
|
94
|
+
self
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def read
|
|
98
|
+
return @redis.client.read unless self.class.hanging?
|
|
99
|
+
|
|
100
|
+
self.class.blocked_reads += 1
|
|
101
|
+
sleep
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def disconnect!
|
|
105
|
+
@redis.client.disconnect!
|
|
106
|
+
end
|
|
107
|
+
|
|
64
108
|
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.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gabriel Naiman
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-08-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: redic
|
|
@@ -114,34 +114,6 @@ dependencies:
|
|
|
114
114
|
- - "~>"
|
|
115
115
|
- !ruby/object:Gem::Version
|
|
116
116
|
version: '0.12'
|
|
117
|
-
- !ruby/object:Gem::Dependency
|
|
118
|
-
name: coveralls
|
|
119
|
-
requirement: !ruby/object:Gem::Requirement
|
|
120
|
-
requirements:
|
|
121
|
-
- - "~>"
|
|
122
|
-
- !ruby/object:Gem::Version
|
|
123
|
-
version: '0.8'
|
|
124
|
-
type: :development
|
|
125
|
-
prerelease: false
|
|
126
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
127
|
-
requirements:
|
|
128
|
-
- - "~>"
|
|
129
|
-
- !ruby/object:Gem::Version
|
|
130
|
-
version: '0.8'
|
|
131
|
-
- !ruby/object:Gem::Dependency
|
|
132
|
-
name: pry-nav
|
|
133
|
-
requirement: !ruby/object:Gem::Requirement
|
|
134
|
-
requirements:
|
|
135
|
-
- - "~>"
|
|
136
|
-
- !ruby/object:Gem::Version
|
|
137
|
-
version: '0.2'
|
|
138
|
-
type: :development
|
|
139
|
-
prerelease: false
|
|
140
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
141
|
-
requirements:
|
|
142
|
-
- - "~>"
|
|
143
|
-
- !ruby/object:Gem::Version
|
|
144
|
-
version: '0.2'
|
|
145
117
|
description: Broadcasting based on Redis PubSub
|
|
146
118
|
email:
|
|
147
119
|
- gabynaiman@gmail.com
|
|
@@ -149,24 +121,25 @@ executables: []
|
|
|
149
121
|
extensions: []
|
|
150
122
|
extra_rdoc_files: []
|
|
151
123
|
files:
|
|
152
|
-
- ".
|
|
124
|
+
- ".github/workflows/ci.yml"
|
|
153
125
|
- ".gitignore"
|
|
154
126
|
- ".ruby-gemset"
|
|
155
127
|
- ".ruby-version"
|
|
156
|
-
- ".travis.yml"
|
|
157
128
|
- Gemfile
|
|
158
129
|
- LICENSE.txt
|
|
159
130
|
- README.md
|
|
160
131
|
- Rakefile
|
|
161
132
|
- broadcaster.gemspec
|
|
162
133
|
- lib/broadcaster.rb
|
|
134
|
+
- lib/broadcaster/version.rb
|
|
135
|
+
- lib/broadcaster/watchdog.rb
|
|
163
136
|
- spec/broadcast_spec.rb
|
|
164
137
|
- spec/minitest_helper.rb
|
|
165
138
|
homepage: https://github.com/gabynaiman/broadcaster
|
|
166
139
|
licenses:
|
|
167
140
|
- MIT
|
|
168
141
|
metadata: {}
|
|
169
|
-
post_install_message:
|
|
142
|
+
post_install_message:
|
|
170
143
|
rdoc_options: []
|
|
171
144
|
require_paths:
|
|
172
145
|
- lib
|
|
@@ -174,15 +147,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
174
147
|
requirements:
|
|
175
148
|
- - ">="
|
|
176
149
|
- !ruby/object:Gem::Version
|
|
177
|
-
version:
|
|
150
|
+
version: 2.3.0
|
|
178
151
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
179
152
|
requirements:
|
|
180
153
|
- - ">="
|
|
181
154
|
- !ruby/object:Gem::Version
|
|
182
155
|
version: '0'
|
|
183
156
|
requirements: []
|
|
184
|
-
rubygems_version: 3.0.
|
|
185
|
-
signing_key:
|
|
157
|
+
rubygems_version: 3.0.9
|
|
158
|
+
signing_key:
|
|
186
159
|
specification_version: 4
|
|
187
160
|
summary: Broadcasting based on Redis PubSub
|
|
188
161
|
test_files:
|
data/.coveralls.yml
DELETED
data/.travis.yml
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
language: ruby
|
|
2
|
-
|
|
3
|
-
rvm:
|
|
4
|
-
- 2.0
|
|
5
|
-
- 2.1
|
|
6
|
-
- 2.2
|
|
7
|
-
- 2.3
|
|
8
|
-
- 2.4
|
|
9
|
-
- 2.5
|
|
10
|
-
- 2.6
|
|
11
|
-
- 2.7
|
|
12
|
-
- jruby-9.2.9.0
|
|
13
|
-
- ruby-head
|
|
14
|
-
- jruby-head
|
|
15
|
-
|
|
16
|
-
matrix:
|
|
17
|
-
fast_finish: true
|
|
18
|
-
allow_failures:
|
|
19
|
-
- rvm: ruby-head
|
|
20
|
-
- rvm: jruby-head
|
|
21
|
-
|
|
22
|
-
services:
|
|
23
|
-
- redis-server
|