message_bus 2.0.0.beta.4 → 2.0.0.beta.5
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of message_bus might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG +5 -0
- data/README.md +35 -1
- data/Rakefile +5 -1
- data/assets/message-bus.js +15 -10
- data/examples/chat/Gemfile +3 -0
- data/lib/message_bus.rb +1 -1
- data/lib/message_bus/backends/memory.rb +286 -0
- data/lib/message_bus/backends/postgres.rb +10 -23
- data/lib/message_bus/backends/redis.rb +29 -14
- data/lib/message_bus/version.rb +1 -1
- data/spec/lib/message_bus/backends/postgres_spec.rb +43 -30
- data/spec/lib/message_bus/backends/redis_spec.rb +7 -1
- data/spec/lib/message_bus/client_spec.rb +1 -1
- data/spec/lib/message_bus/multi_process_spec.rb +4 -2
- data/spec/lib/message_bus/rack/middleware_spec.rb +3 -2
- data/spec/lib/message_bus_spec.rb +34 -33
- data/vendor/assets/javascripts/message-bus.js +15 -10
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b91a7d621707536a33738a736ffd00bba4639d46
|
4
|
+
data.tar.gz: b5a445c066617c126b4bfaeb6444ad0f473ec670
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: adb5ec754024c6a62873a98dd2e3978d669f057bda86ffac0a6842787a9b8e14d0280315d6f499f40da9e86ed0a412c8ce0675262318f9330d023e2e58afe4c3
|
7
|
+
data.tar.gz: 50c1ee4d6b4e485198011c08a5632014321f34ceb388dd93568513d2762d1d9c539a0a3c7b4d2e6e74d7485e4ce5d3ace9d7f0e9bf8be6812df5f4d72f27c10a
|
data/CHANGELOG
CHANGED
@@ -1,5 +1,10 @@
|
|
1
1
|
29-02-2016
|
2
2
|
|
3
|
+
- Version 2.0.0.beta.5
|
4
|
+
|
5
|
+
- Fix: JavaScript unsubscribe was not updating publicly visible MessageBus.callbacks @sam
|
6
|
+
- Fix: When MessageBus is talking to a readonly redis buffering may cause a infinite loop @tgxworld
|
7
|
+
|
3
8
|
- Version 2.0.0.beta.4
|
4
9
|
|
5
10
|
- Feature: allow verbose redis logging by specifying `enable_redis_logger` in config, default disabled
|
data/README.md
CHANGED
@@ -18,6 +18,10 @@ Live chat demo per [examples/chat](https://github.com/SamSaffron/message_bus/tre
|
|
18
18
|
|
19
19
|
**Yes**, MessageBus uses Rack Hijack, this interface allows us to take control of the underlying socket. MessageBus can handle thousands of concurrent long polls on all popular Ruby webservers. MessageBus runs as middleware in your Rack (or by extension Rails) application and does not require a dedicated server. Background work is minimized to ensure it does not interfere with existing non MessageBus traffic.
|
20
20
|
|
21
|
+
## Is this used in production at scale?
|
22
|
+
|
23
|
+
**Yes**, MessageBus was extracted out of [Discourse](http://www.discourse.org/) and is used in thousands of production Discourse sites at scale.
|
24
|
+
|
21
25
|
## Installation
|
22
26
|
|
23
27
|
Add this line to your application's Gemfile:
|
@@ -216,7 +220,19 @@ message_bus also supports PostgreSQL as the backend:
|
|
216
220
|
MessageBus.configure(backend: :postgres, backend_options: {user: 'message_bus', dbname: 'message_bus'})
|
217
221
|
```
|
218
222
|
|
219
|
-
The PostgreSQL client message_bus uses is [ruby-pg](https://bitbucket.org/ged/ruby-pg), so you can visit it's repo to see what options you can configure
|
223
|
+
The PostgreSQL client message_bus uses is [ruby-pg](https://bitbucket.org/ged/ruby-pg), so you can visit it's repo to see what options you can configure inside `:backend_options`.
|
224
|
+
|
225
|
+
A `:clear_every` option is also supported, which only clears the backlogs on every number of requests given. So if you set `clear_every: 100`, the backlog will only be cleared every 100 requests. This can improve performance in cases where exact backlog clearing are not required.
|
226
|
+
|
227
|
+
### Memory
|
228
|
+
|
229
|
+
message_bus also supports an in-memory backend. This can be used for testing or simple single-process environments that do not require persistence.
|
230
|
+
|
231
|
+
```ruby
|
232
|
+
MessageBus.configure(backend: :memory)
|
233
|
+
```
|
234
|
+
|
235
|
+
The `:clear_every` option supported by the PostgreSQL backend is also supported by the in-memory backend.
|
220
236
|
|
221
237
|
### Forking/threading app servers
|
222
238
|
|
@@ -237,6 +253,23 @@ if defined?(PhusionPassenger)
|
|
237
253
|
end
|
238
254
|
```
|
239
255
|
|
256
|
+
MessageBus uses long polling which needs to be configured in Passenger
|
257
|
+
|
258
|
+
* for passenger version < 5.0.21
|
259
|
+
|
260
|
+
`PhusionPassenger.advertised_concurrency_level = 0` to application.rb
|
261
|
+
|
262
|
+
* for passenger version > 5.0.21
|
263
|
+
|
264
|
+
```
|
265
|
+
location /message-bus {
|
266
|
+
passenger_app_group_name foo_websocket;
|
267
|
+
passenger_force_max_concurrent_requests_per_process 0;
|
268
|
+
}
|
269
|
+
```
|
270
|
+
to nginx.conf.
|
271
|
+
For more information see [Passenger documentation](https://www.phusionpassenger.com/library/config/nginx/tuning_sse_and_websockets/)
|
272
|
+
|
240
273
|
#### Puma
|
241
274
|
```ruby
|
242
275
|
# path/to/your/config/puma.rb
|
@@ -261,6 +294,7 @@ end
|
|
261
294
|
|
262
295
|
If you are looking to contribute to this project here are some ideas
|
263
296
|
|
297
|
+
- Add a test suite for JavaScript message-bus.js
|
264
298
|
- Build backends for other providers (zeromq, rabbitmq, disque)
|
265
299
|
- Improve and properly document admin dashboard (add opt-in stats, better diagnostics into queues)
|
266
300
|
- Improve general documentation (Add examples, refine existing examples)
|
data/Rakefile
CHANGED
@@ -17,12 +17,16 @@ run_spec = proc do |backend|
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
-
task :spec => [:spec_redis, :spec_postgres]
|
20
|
+
task :spec => [:spec_redis, :spec_postgres, :spec_memory]
|
21
21
|
|
22
22
|
task :spec_redis do
|
23
23
|
run_spec.call('redis')
|
24
24
|
end
|
25
25
|
|
26
|
+
task :spec_memory do
|
27
|
+
run_spec.call('memory')
|
28
|
+
end
|
29
|
+
|
26
30
|
task :spec_postgres do
|
27
31
|
run_spec.call('postgres')
|
28
32
|
end
|
data/assets/message-bus.js
CHANGED
@@ -345,22 +345,26 @@ window.MessageBus = (function() {
|
|
345
345
|
last_id: lastId
|
346
346
|
});
|
347
347
|
if (me.longPoll) {
|
348
|
-
|
348
|
+
me.longPoll.abort();
|
349
349
|
}
|
350
|
+
|
351
|
+
return func;
|
350
352
|
},
|
351
353
|
|
352
354
|
// Unsubscribe from a channel
|
353
355
|
unsubscribe: function(channel, func) {
|
354
|
-
// TODO
|
356
|
+
// TODO allow for globbing in the middle of a channel name
|
357
|
+
// like /something/*/something
|
358
|
+
// at the moment we only support globbing /something/*
|
355
359
|
var glob;
|
356
360
|
if (channel.indexOf("*", channel.length - 1) !== -1) {
|
357
361
|
channel = channel.substr(0, channel.length - 1);
|
358
362
|
glob = true;
|
359
363
|
}
|
360
364
|
|
361
|
-
var
|
365
|
+
var removed = false;
|
362
366
|
|
363
|
-
for (var i=
|
367
|
+
for (var i=callbacks.length-1; i>=0; i--) {
|
364
368
|
|
365
369
|
callback = callbacks[i];
|
366
370
|
var keep;
|
@@ -375,16 +379,17 @@ window.MessageBus = (function() {
|
|
375
379
|
keep = true;
|
376
380
|
}
|
377
381
|
|
378
|
-
if (keep) {
|
379
|
-
|
382
|
+
if (!keep) {
|
383
|
+
callbacks.splice(i,1);
|
384
|
+
removed = true;
|
380
385
|
}
|
381
386
|
}
|
382
387
|
|
383
|
-
|
384
|
-
|
385
|
-
if (me.longPoll) {
|
386
|
-
return me.longPoll.abort();
|
388
|
+
if (removed && me.longPoll) {
|
389
|
+
me.longPoll.abort();
|
387
390
|
}
|
391
|
+
|
392
|
+
return removed;
|
388
393
|
}
|
389
394
|
};
|
390
395
|
|
data/lib/message_bus.rb
CHANGED
@@ -473,7 +473,7 @@ module MessageBus::Implementation
|
|
473
473
|
globals, locals, local_globals, global_globals = nil
|
474
474
|
|
475
475
|
@mutex.synchronize do
|
476
|
-
|
476
|
+
return if @destroyed
|
477
477
|
next unless @subscriptions
|
478
478
|
|
479
479
|
globals = @subscriptions[nil]
|
@@ -0,0 +1,286 @@
|
|
1
|
+
module MessageBus::Memory; end
|
2
|
+
|
3
|
+
class MessageBus::Memory::Client
|
4
|
+
class Listener
|
5
|
+
attr_reader :do_sub, :do_unsub, :do_message
|
6
|
+
|
7
|
+
def subscribe(&block)
|
8
|
+
@do_sub = block
|
9
|
+
end
|
10
|
+
|
11
|
+
def unsubscribe(&block)
|
12
|
+
@do_unsub = block
|
13
|
+
end
|
14
|
+
|
15
|
+
def message(&block)
|
16
|
+
@do_message = block
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(config)
|
21
|
+
@mutex = Mutex.new
|
22
|
+
@listeners = []
|
23
|
+
reset!
|
24
|
+
end
|
25
|
+
|
26
|
+
def add(channel, value)
|
27
|
+
listeners = nil
|
28
|
+
id = nil
|
29
|
+
sync do
|
30
|
+
id = @global_id += 1
|
31
|
+
chan(channel) << [id, value]
|
32
|
+
listeners = @listeners.dup
|
33
|
+
end
|
34
|
+
msg = MessageBus::Message.new id, id, channel, value
|
35
|
+
payload = msg.encode
|
36
|
+
listeners.each{|l| l.push(payload)}
|
37
|
+
id
|
38
|
+
end
|
39
|
+
|
40
|
+
def clear_global_backlog(backlog_id, num_to_keep)
|
41
|
+
if backlog_id > num_to_keep
|
42
|
+
oldest = backlog_id - num_to_keep
|
43
|
+
sync do
|
44
|
+
@channels.each_value do |entries|
|
45
|
+
entries.delete_if{|id, _| id <= oldest}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def clear_channel_backlog(channel, backlog_id, num_to_keep)
|
53
|
+
oldest = backlog_id - num_to_keep
|
54
|
+
sync{chan(channel).delete_if{|id, _| id <= oldest}}
|
55
|
+
nil
|
56
|
+
end
|
57
|
+
|
58
|
+
def backlog(channel, backlog_id)
|
59
|
+
sync{chan(channel).select{|id, _| id > backlog_id}}
|
60
|
+
end
|
61
|
+
|
62
|
+
def global_backlog(backlog_id)
|
63
|
+
sync do
|
64
|
+
@channels.dup.flat_map do |channel, messages|
|
65
|
+
messages.select{|id, _| id > backlog_id}.map{|id, value| [id, channel, value]}
|
66
|
+
end.sort
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def get_value(channel, id)
|
71
|
+
sync{chan(channel).find{|i, _| i == id}.last}
|
72
|
+
end
|
73
|
+
|
74
|
+
# Dangerous, drops the message_bus table containing the backlog if it exists.
|
75
|
+
def reset!
|
76
|
+
sync do
|
77
|
+
@global_id = 0
|
78
|
+
@channels = {}
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def max_id(channel=nil)
|
83
|
+
if channel
|
84
|
+
sync do
|
85
|
+
if entry = chan(channel).last
|
86
|
+
entry.first
|
87
|
+
end
|
88
|
+
end
|
89
|
+
else
|
90
|
+
sync{@global_id - 1}
|
91
|
+
end || 0
|
92
|
+
end
|
93
|
+
|
94
|
+
def subscribe
|
95
|
+
listener = Listener.new
|
96
|
+
yield listener
|
97
|
+
|
98
|
+
q = Queue.new
|
99
|
+
sync do
|
100
|
+
@listeners << q
|
101
|
+
end
|
102
|
+
|
103
|
+
listener.do_sub.call
|
104
|
+
while msg = q.pop
|
105
|
+
listener.do_message.call(nil, msg)
|
106
|
+
end
|
107
|
+
listener.do_unsub.call
|
108
|
+
sync do
|
109
|
+
@listeners.delete(q)
|
110
|
+
end
|
111
|
+
|
112
|
+
nil
|
113
|
+
end
|
114
|
+
|
115
|
+
def unsubscribe
|
116
|
+
sync{@listeners.each{|l| l.push(nil)}}
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def chan(channel)
|
122
|
+
@channels[channel] ||= []
|
123
|
+
end
|
124
|
+
|
125
|
+
def sync
|
126
|
+
@mutex.synchronize{yield}
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
class MessageBus::Memory::ReliablePubSub
|
131
|
+
attr_reader :subscribed
|
132
|
+
attr_accessor :max_backlog_size, :max_global_backlog_size, :clear_every
|
133
|
+
|
134
|
+
UNSUB_MESSAGE = "$$UNSUBSCRIBE"
|
135
|
+
|
136
|
+
# max_backlog_size is per multiplexed channel
|
137
|
+
def initialize(config = {}, max_backlog_size = 1000)
|
138
|
+
@config = config
|
139
|
+
@max_backlog_size = max_backlog_size
|
140
|
+
@max_global_backlog_size = 2000
|
141
|
+
# after 7 days inactive backlogs will be removed
|
142
|
+
@clear_every = config[:clear_every] || 1
|
143
|
+
end
|
144
|
+
|
145
|
+
def new_connection
|
146
|
+
MessageBus::Memory::Client.new(@config)
|
147
|
+
end
|
148
|
+
|
149
|
+
def backend
|
150
|
+
:memory
|
151
|
+
end
|
152
|
+
|
153
|
+
def after_fork
|
154
|
+
nil
|
155
|
+
end
|
156
|
+
|
157
|
+
def client
|
158
|
+
@client ||= new_connection
|
159
|
+
end
|
160
|
+
|
161
|
+
# use with extreme care, will nuke all of the data
|
162
|
+
def reset!
|
163
|
+
client.reset!
|
164
|
+
end
|
165
|
+
|
166
|
+
def publish(channel, data, queue_in_memory=true)
|
167
|
+
client = self.client
|
168
|
+
backlog_id = client.add(channel, data)
|
169
|
+
if backlog_id % clear_every == 0
|
170
|
+
client.clear_global_backlog(backlog_id, @max_global_backlog_size)
|
171
|
+
client.clear_channel_backlog(channel, backlog_id, @max_backlog_size)
|
172
|
+
end
|
173
|
+
|
174
|
+
backlog_id
|
175
|
+
end
|
176
|
+
|
177
|
+
def last_id(channel)
|
178
|
+
client.max_id(channel)
|
179
|
+
end
|
180
|
+
|
181
|
+
def backlog(channel, last_id = nil)
|
182
|
+
items = client.backlog channel, last_id.to_i
|
183
|
+
|
184
|
+
items.map! do |id, data|
|
185
|
+
MessageBus::Message.new id, id, channel, data
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def global_backlog(last_id = nil)
|
190
|
+
last_id = last_id.to_i
|
191
|
+
|
192
|
+
items = client.global_backlog last_id.to_i
|
193
|
+
|
194
|
+
items.map! do |id, channel, data|
|
195
|
+
MessageBus::Message.new id, id, channel, data
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def get_message(channel, message_id)
|
200
|
+
if data = client.get_value(channel, message_id)
|
201
|
+
MessageBus::Message.new message_id, message_id, channel, data
|
202
|
+
else
|
203
|
+
nil
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def subscribe(channel, last_id = nil)
|
208
|
+
# trivial implementation for now,
|
209
|
+
# can cut down on connections if we only have one global subscriber
|
210
|
+
raise ArgumentError unless block_given?
|
211
|
+
|
212
|
+
global_subscribe(last_id) do |m|
|
213
|
+
yield m if m.channel == channel
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def process_global_backlog(highest_id)
|
218
|
+
if highest_id > client.max_id
|
219
|
+
highest_id = 0
|
220
|
+
end
|
221
|
+
|
222
|
+
global_backlog(highest_id).each do |old|
|
223
|
+
yield old
|
224
|
+
highest_id = old.global_id
|
225
|
+
end
|
226
|
+
|
227
|
+
highest_id
|
228
|
+
end
|
229
|
+
|
230
|
+
def global_unsubscribe
|
231
|
+
client.unsubscribe
|
232
|
+
@subscribed = false
|
233
|
+
end
|
234
|
+
|
235
|
+
def global_subscribe(last_id=nil, &blk)
|
236
|
+
raise ArgumentError unless block_given?
|
237
|
+
highest_id = last_id
|
238
|
+
|
239
|
+
begin
|
240
|
+
client.subscribe do |on|
|
241
|
+
h = {}
|
242
|
+
|
243
|
+
on.subscribe do
|
244
|
+
if highest_id
|
245
|
+
process_global_backlog(highest_id) do |m|
|
246
|
+
h[m.global_id] = true
|
247
|
+
yield m
|
248
|
+
end
|
249
|
+
end
|
250
|
+
@subscribed = true
|
251
|
+
end
|
252
|
+
|
253
|
+
on.unsubscribe do
|
254
|
+
@subscribed = false
|
255
|
+
end
|
256
|
+
|
257
|
+
on.message do |c,m|
|
258
|
+
m = MessageBus::Message.decode m
|
259
|
+
|
260
|
+
# we have 3 options
|
261
|
+
#
|
262
|
+
# 1. message came in the correct order GREAT, just deal with it
|
263
|
+
# 2. message came in the incorrect order COMPLICATED, wait a tiny bit and clear backlog
|
264
|
+
# 3. message came in the incorrect order and is lowest than current highest id, reset
|
265
|
+
|
266
|
+
if h
|
267
|
+
# If already yielded during the clear backlog when subscribing,
|
268
|
+
# don't yield a duplicate copy.
|
269
|
+
unless h.delete(m.global_id)
|
270
|
+
h = nil if h.empty?
|
271
|
+
yield m
|
272
|
+
end
|
273
|
+
else
|
274
|
+
yield m
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
rescue => error
|
279
|
+
MessageBus.logger.warn "#{error} subscribe failed, reconnecting in 1 second. Call stack\n#{error.backtrace.join("\n")}"
|
280
|
+
sleep 1
|
281
|
+
retry
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
MessageBus::BACKENDS[:memory] = self
|
286
|
+
end
|
@@ -103,25 +103,19 @@ class MessageBus::Postgres::Client
|
|
103
103
|
sync{@listening_on[channel] = obj}
|
104
104
|
listener = Listener.new
|
105
105
|
yield listener
|
106
|
-
pid = Process.pid
|
107
106
|
|
108
107
|
conn = raw_pg_connection
|
109
108
|
conn.exec "LISTEN #{channel}"
|
110
109
|
listener.do_sub.call
|
111
110
|
while listening_on?(channel, obj)
|
112
111
|
conn.wait_for_notify(10) do |_,_,payload|
|
112
|
+
break unless listening_on?(channel, obj)
|
113
113
|
listener.do_message.call(nil, payload)
|
114
114
|
end
|
115
|
-
break if pid != Process.pid
|
116
115
|
end
|
117
116
|
listener.do_unsub.call
|
118
117
|
|
119
|
-
|
120
|
-
sync{INHERITED_CONNECTIONS << conn}
|
121
|
-
else
|
122
|
-
conn.exec "UNLISTEN #{channel}"
|
123
|
-
end
|
124
|
-
|
118
|
+
conn.exec "UNLISTEN #{channel}"
|
125
119
|
nil
|
126
120
|
end
|
127
121
|
|
@@ -139,7 +133,7 @@ class MessageBus::Postgres::Client
|
|
139
133
|
end
|
140
134
|
|
141
135
|
def create_table(conn)
|
142
|
-
conn.exec 'CREATE TABLE message_bus (id bigserial PRIMARY KEY, channel text NOT NULL, value text NOT NULL, added_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL)'
|
136
|
+
conn.exec 'CREATE TABLE message_bus (id bigserial PRIMARY KEY, channel text NOT NULL, value text NOT NULL CHECK (octet_length(value) >= 2), added_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL)'
|
143
137
|
conn.exec 'CREATE INDEX table_channel_id_index ON message_bus (channel, id)'
|
144
138
|
conn.exec 'CREATE INDEX table_added_at_index ON message_bus (added_at)'
|
145
139
|
nil
|
@@ -150,7 +144,6 @@ class MessageBus::Postgres::Client
|
|
150
144
|
if current_pid != @pid
|
151
145
|
@pid = current_pid
|
152
146
|
sync do
|
153
|
-
@listening_on.clear
|
154
147
|
INHERITED_CONNECTIONS.concat(@available)
|
155
148
|
@available.clear
|
156
149
|
end
|
@@ -214,9 +207,7 @@ end
|
|
214
207
|
|
215
208
|
class MessageBus::Postgres::ReliablePubSub
|
216
209
|
attr_reader :subscribed
|
217
|
-
attr_accessor :
|
218
|
-
:max_global_backlog_size, :max_in_memory_publish_backlog,
|
219
|
-
:max_backlog_age
|
210
|
+
attr_accessor :max_backlog_size, :max_global_backlog_size, :max_backlog_age, :clear_every
|
220
211
|
|
221
212
|
UNSUB_MESSAGE = "$$UNSUBSCRIBE"
|
222
213
|
|
@@ -229,15 +220,9 @@ class MessageBus::Postgres::ReliablePubSub
|
|
229
220
|
@config = config
|
230
221
|
@max_backlog_size = max_backlog_size
|
231
222
|
@max_global_backlog_size = 2000
|
232
|
-
@max_publish_retries = 10
|
233
|
-
@max_publish_wait = 500 #ms
|
234
|
-
@max_in_memory_publish_backlog = 1000
|
235
|
-
@in_memory_backlog = []
|
236
|
-
@lock = Mutex.new
|
237
|
-
@flush_backlog_thread = nil
|
238
223
|
# after 7 days inactive backlogs will be removed
|
239
224
|
@max_backlog_age = 604800
|
240
|
-
@
|
225
|
+
@clear_every = config[:clear_every] || 1
|
241
226
|
end
|
242
227
|
|
243
228
|
def new_connection
|
@@ -272,9 +257,11 @@ class MessageBus::Postgres::ReliablePubSub
|
|
272
257
|
msg = MessageBus::Message.new backlog_id, backlog_id, channel, data
|
273
258
|
payload = msg.encode
|
274
259
|
client.publish postgresql_channel_name, payload
|
275
|
-
|
276
|
-
|
277
|
-
|
260
|
+
if backlog_id % clear_every == 0
|
261
|
+
client.clear_global_backlog(backlog_id, @max_global_backlog_size)
|
262
|
+
client.expire(@max_backlog_age)
|
263
|
+
client.clear_channel_backlog(channel, backlog_id, @max_backlog_size)
|
264
|
+
end
|
278
265
|
|
279
266
|
backlog_id
|
280
267
|
end
|
@@ -10,9 +10,7 @@ require 'redis'
|
|
10
10
|
module MessageBus::Redis; end
|
11
11
|
class MessageBus::Redis::ReliablePubSub
|
12
12
|
attr_reader :subscribed
|
13
|
-
attr_accessor :
|
14
|
-
:max_global_backlog_size, :max_in_memory_publish_backlog,
|
15
|
-
:max_backlog_age
|
13
|
+
attr_accessor :max_backlog_size, :max_global_backlog_size, :max_in_memory_publish_backlog, :max_backlog_age
|
16
14
|
|
17
15
|
UNSUB_MESSAGE = "$$UNSUBSCRIBE"
|
18
16
|
|
@@ -33,8 +31,6 @@ class MessageBus::Redis::ReliablePubSub
|
|
33
31
|
end
|
34
32
|
@max_backlog_size = max_backlog_size
|
35
33
|
@max_global_backlog_size = 2000
|
36
|
-
@max_publish_retries = 10
|
37
|
-
@max_publish_wait = 500 #ms
|
38
34
|
@max_in_memory_publish_backlog = 1000
|
39
35
|
@in_memory_backlog = []
|
40
36
|
@lock = Mutex.new
|
@@ -151,11 +147,21 @@ class MessageBus::Redis::ReliablePubSub
|
|
151
147
|
end
|
152
148
|
|
153
149
|
def ensure_backlog_flushed
|
154
|
-
|
150
|
+
flushed = false
|
151
|
+
|
152
|
+
while !flushed
|
155
153
|
try_again = false
|
156
154
|
|
155
|
+
if is_readonly?
|
156
|
+
sleep 1
|
157
|
+
next
|
158
|
+
end
|
159
|
+
|
157
160
|
@lock.synchronize do
|
158
|
-
|
161
|
+
if @in_memory_backlog.length == 0
|
162
|
+
flushed = true
|
163
|
+
break
|
164
|
+
end
|
159
165
|
|
160
166
|
begin
|
161
167
|
publish(*@in_memory_backlog[0],false)
|
@@ -171,13 +177,6 @@ class MessageBus::Redis::ReliablePubSub
|
|
171
177
|
|
172
178
|
@in_memory_backlog.delete_at(0) unless try_again
|
173
179
|
end
|
174
|
-
|
175
|
-
if try_again
|
176
|
-
sleep 0.005
|
177
|
-
# in case we are not connected to the correct server
|
178
|
-
# which can happen when sharing ips
|
179
|
-
pub_redis.client.reconnect
|
180
|
-
end
|
181
180
|
end
|
182
181
|
ensure
|
183
182
|
@lock.synchronize do
|
@@ -342,5 +341,21 @@ class MessageBus::Redis::ReliablePubSub
|
|
342
341
|
end
|
343
342
|
end
|
344
343
|
|
344
|
+
private
|
345
|
+
|
346
|
+
def is_readonly?
|
347
|
+
key = "__mb_is_readonly".freeze
|
348
|
+
|
349
|
+
begin
|
350
|
+
# in case we are not connected to the correct server
|
351
|
+
# which can happen when sharing ips
|
352
|
+
pub_redis.client.reconnect
|
353
|
+
pub_redis.client.call([:set, key, '1'])
|
354
|
+
false
|
355
|
+
rescue Redis::CommandError => e
|
356
|
+
return true if e.message =~ /^READONLY/
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
345
360
|
MessageBus::BACKENDS[:redis] = self
|
346
361
|
end
|
data/lib/message_bus/version.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require_relative '../../../spec_helper'
|
2
2
|
require 'message_bus'
|
3
3
|
|
4
|
-
if MESSAGE_BUS_CONFIG[:backend] == :postgres
|
4
|
+
if MESSAGE_BUS_CONFIG[:backend] == :postgres || MESSAGE_BUS_CONFIG[:backend] == :memory
|
5
5
|
describe PUB_SUB_CLASS do
|
6
6
|
|
7
7
|
def new_test_bus
|
@@ -56,24 +56,37 @@ describe PUB_SUB_CLASS do
|
|
56
56
|
it "should truncate channels correctly" do
|
57
57
|
@bus.max_backlog_size = 2
|
58
58
|
4.times do |t|
|
59
|
-
@bus.publish "/foo", t
|
59
|
+
@bus.publish "/foo", "1#{t}"
|
60
60
|
end
|
61
61
|
|
62
62
|
@bus.backlog("/foo").to_a.must_equal [
|
63
|
-
MessageBus::Message.new(3,3,'/foo','
|
64
|
-
MessageBus::Message.new(4,4,'/foo','
|
63
|
+
MessageBus::Message.new(3,3,'/foo','12'),
|
64
|
+
MessageBus::Message.new(4,4,'/foo','13'),
|
65
65
|
]
|
66
66
|
end
|
67
67
|
|
68
68
|
it "should truncate global backlog correctly" do
|
69
69
|
@bus.max_global_backlog_size = 2
|
70
|
-
@bus.publish "/foo", "
|
71
|
-
@bus.publish "/bar", "
|
72
|
-
@bus.publish "/baz", "
|
70
|
+
@bus.publish "/foo", "11"
|
71
|
+
@bus.publish "/bar", "21"
|
72
|
+
@bus.publish "/baz", "31"
|
73
73
|
|
74
74
|
@bus.global_backlog.length.must_equal 2
|
75
75
|
end
|
76
76
|
|
77
|
+
it "should support clear_every setting" do
|
78
|
+
@bus.clear_every = 5
|
79
|
+
@bus.max_global_backlog_size = 2
|
80
|
+
@bus.publish "/foo", "11"
|
81
|
+
@bus.publish "/bar", "21"
|
82
|
+
@bus.publish "/baz", "31"
|
83
|
+
@bus.publish "/bar", "41"
|
84
|
+
@bus.global_backlog.length.must_equal 4
|
85
|
+
|
86
|
+
@bus.publish "/baz", "51"
|
87
|
+
@bus.global_backlog.length.must_equal 2
|
88
|
+
end
|
89
|
+
|
77
90
|
it "should be able to grab a message by id" do
|
78
91
|
id1 = @bus.publish "/foo", "bar"
|
79
92
|
id2 = @bus.publish "/foo", "baz"
|
@@ -97,14 +110,14 @@ describe PUB_SUB_CLASS do
|
|
97
110
|
|
98
111
|
it "should correctly omit dropped messages from the global backlog" do
|
99
112
|
@bus.max_backlog_size = 1
|
100
|
-
@bus.publish "/foo", "
|
101
|
-
@bus.publish "/foo", "
|
102
|
-
@bus.publish "/bar", "
|
103
|
-
@bus.publish "/bar", "
|
113
|
+
@bus.publish "/foo", "a1"
|
114
|
+
@bus.publish "/foo", "b1"
|
115
|
+
@bus.publish "/bar", "a1"
|
116
|
+
@bus.publish "/bar", "b1"
|
104
117
|
|
105
118
|
@bus.global_backlog.to_a.must_equal [
|
106
|
-
MessageBus::Message.new(2, 2, "/foo", "
|
107
|
-
MessageBus::Message.new(4, 4, "/bar", "
|
119
|
+
MessageBus::Message.new(2, 2, "/foo", "b1"),
|
120
|
+
MessageBus::Message.new(4, 4, "/bar", "b1")
|
108
121
|
]
|
109
122
|
end
|
110
123
|
|
@@ -112,9 +125,9 @@ describe PUB_SUB_CLASS do
|
|
112
125
|
threads = []
|
113
126
|
4.times do
|
114
127
|
threads << Thread.new do
|
115
|
-
bus =
|
128
|
+
bus = @bus
|
116
129
|
25.times {
|
117
|
-
bus.publish "/foo", "
|
130
|
+
bus.publish "/foo", ".."
|
118
131
|
}
|
119
132
|
end
|
120
133
|
end
|
@@ -124,17 +137,17 @@ describe PUB_SUB_CLASS do
|
|
124
137
|
end
|
125
138
|
|
126
139
|
it "should be able to subscribe globally with recovery" do
|
127
|
-
@bus.publish("/foo", "
|
128
|
-
@bus.publish("/bar", "
|
140
|
+
@bus.publish("/foo", "11")
|
141
|
+
@bus.publish("/bar", "12")
|
129
142
|
got = []
|
130
143
|
|
131
144
|
t = Thread.new do
|
132
|
-
|
145
|
+
@bus.global_subscribe(0) do |msg|
|
133
146
|
got << msg
|
134
147
|
end
|
135
148
|
end
|
136
149
|
|
137
|
-
@bus.publish("/bar", "
|
150
|
+
@bus.publish("/bar", "13")
|
138
151
|
|
139
152
|
wait_for(100) do
|
140
153
|
got.length == 3
|
@@ -143,7 +156,7 @@ describe PUB_SUB_CLASS do
|
|
143
156
|
t.kill
|
144
157
|
|
145
158
|
got.length.must_equal 3
|
146
|
-
got.map{|m| m.data}.must_equal ["
|
159
|
+
got.map{|m| m.data}.must_equal ["11","12","13"]
|
147
160
|
end
|
148
161
|
|
149
162
|
it "should be able to encode and decode messages properly" do
|
@@ -152,17 +165,17 @@ describe PUB_SUB_CLASS do
|
|
152
165
|
end
|
153
166
|
|
154
167
|
it "should handle subscribe on single channel, with recovery" do
|
155
|
-
@bus.publish("/foo", "
|
156
|
-
@bus.publish("/bar", "
|
168
|
+
@bus.publish("/foo", "11")
|
169
|
+
@bus.publish("/bar", "12")
|
157
170
|
got = []
|
158
171
|
|
159
172
|
t = Thread.new do
|
160
|
-
|
173
|
+
@bus.subscribe("/foo",0) do |msg|
|
161
174
|
got << msg
|
162
175
|
end
|
163
176
|
end
|
164
177
|
|
165
|
-
@bus.publish("/foo", "
|
178
|
+
@bus.publish("/foo", "13")
|
166
179
|
|
167
180
|
wait_for(100) do
|
168
181
|
got.length == 2
|
@@ -170,15 +183,15 @@ describe PUB_SUB_CLASS do
|
|
170
183
|
|
171
184
|
t.kill
|
172
185
|
|
173
|
-
got.map{|m| m.data}.must_equal ["
|
186
|
+
got.map{|m| m.data}.must_equal ["11","13"]
|
174
187
|
end
|
175
188
|
|
176
189
|
it "should not get backlog if subscribe is called without params" do
|
177
|
-
@bus.publish("/foo", "
|
190
|
+
@bus.publish("/foo", "11")
|
178
191
|
got = []
|
179
192
|
|
180
193
|
t = Thread.new do
|
181
|
-
|
194
|
+
@bus.subscribe("/foo") do |msg|
|
182
195
|
got << msg
|
183
196
|
end
|
184
197
|
end
|
@@ -187,7 +200,7 @@ describe PUB_SUB_CLASS do
|
|
187
200
|
# I thought about adding a subscribed callback, but outside of testing it matters less
|
188
201
|
sleep 0.05
|
189
202
|
|
190
|
-
@bus.publish("/foo", "
|
203
|
+
@bus.publish("/foo", "12")
|
191
204
|
|
192
205
|
wait_for(100) do
|
193
206
|
got.length == 1
|
@@ -195,12 +208,12 @@ describe PUB_SUB_CLASS do
|
|
195
208
|
|
196
209
|
t.kill
|
197
210
|
|
198
|
-
got.map{|m| m.data}.must_equal ["
|
211
|
+
got.map{|m| m.data}.must_equal ["12"]
|
199
212
|
end
|
200
213
|
|
201
214
|
it "should allow us to get last id on a channel" do
|
202
215
|
@bus.last_id("/foo").must_equal 0
|
203
|
-
@bus.publish("/foo", "
|
216
|
+
@bus.publish("/foo", "11")
|
204
217
|
@bus.last_id("/foo").must_equal 1
|
205
218
|
end
|
206
219
|
|
@@ -26,16 +26,22 @@ describe PUB_SUB_CLASS do
|
|
26
26
|
@bus.pub_redis.slaveof "127.0.0.80", "666"
|
27
27
|
@bus.max_in_memory_publish_backlog = 2
|
28
28
|
|
29
|
+
current_threads = Thread.list
|
30
|
+
current_threads_length = current_threads.count
|
31
|
+
|
29
32
|
3.times do
|
30
33
|
result = @bus.publish "/foo", "bar"
|
31
34
|
result.must_equal nil
|
35
|
+
Thread.list.length.must_equal (current_threads_length + 1)
|
32
36
|
end
|
33
37
|
|
34
38
|
@bus.pub_redis.slaveof "no", "one"
|
35
39
|
sleep 0.01
|
36
40
|
|
37
|
-
|
41
|
+
(Thread.list - current_threads).each(&:join)
|
42
|
+
Thread.list.length.must_equal current_threads_length
|
38
43
|
|
44
|
+
@bus.backlog("/foo", 0).map(&:data).must_equal ["bar","bar"]
|
39
45
|
end
|
40
46
|
end
|
41
47
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require_relative '../../spec_helper'
|
2
2
|
require 'message_bus'
|
3
3
|
|
4
|
+
unless MESSAGE_BUS_CONFIG[:backend] == :memory
|
4
5
|
describe PUB_SUB_CLASS do
|
5
6
|
def self.error!
|
6
7
|
@error = true
|
@@ -44,7 +45,7 @@ describe PUB_SUB_CLASS do
|
|
44
45
|
new_bus.reset!
|
45
46
|
begin
|
46
47
|
pids = (1..10).map{spawn_child}
|
47
|
-
expected_responses = pids.map{|x| (0...10).map{|i| "#{i}-#{x}"}}.flatten
|
48
|
+
expected_responses = pids.map{|x| (0...10).map{|i| "0#{i}-#{x}"}}.flatten
|
48
49
|
unexpected_responses = []
|
49
50
|
bus = new_bus
|
50
51
|
t = Thread.new do
|
@@ -56,7 +57,7 @@ describe PUB_SUB_CLASS do
|
|
56
57
|
end
|
57
58
|
end
|
58
59
|
end
|
59
|
-
10.times{|i| bus.publish("/echo", i
|
60
|
+
10.times{|i| bus.publish("/echo", "0#{i}")}
|
60
61
|
wait_for 4000 do
|
61
62
|
expected_responses.empty?
|
62
63
|
end
|
@@ -83,3 +84,4 @@ describe PUB_SUB_CLASS do
|
|
83
84
|
end
|
84
85
|
end
|
85
86
|
end
|
87
|
+
end
|
@@ -10,7 +10,7 @@ describe MessageBus::Rack::Middleware do
|
|
10
10
|
|
11
11
|
before do
|
12
12
|
bus = @bus = MessageBus::Instance.new
|
13
|
-
@bus.
|
13
|
+
@bus.configure(MESSAGE_BUS_CONFIG)
|
14
14
|
@bus.long_polling_enabled = false
|
15
15
|
|
16
16
|
e_m = extra_middleware
|
@@ -23,9 +23,10 @@ describe MessageBus::Rack::Middleware do
|
|
23
23
|
|
24
24
|
@async_middleware = builder.to_app
|
25
25
|
@message_bus_middleware = @async_middleware.app
|
26
|
+
@bus.reset!
|
26
27
|
end
|
27
28
|
|
28
|
-
after do
|
29
|
+
after do
|
29
30
|
@message_bus_middleware.stop_listener
|
30
31
|
@bus.reset!
|
31
32
|
@bus.destroy
|
@@ -10,7 +10,7 @@ describe MessageBus do
|
|
10
10
|
@bus.site_id_lookup do
|
11
11
|
"magic"
|
12
12
|
end
|
13
|
-
@bus.
|
13
|
+
@bus.configure(MESSAGE_BUS_CONFIG)
|
14
14
|
end
|
15
15
|
|
16
16
|
after do
|
@@ -43,11 +43,11 @@ describe MessageBus do
|
|
43
43
|
@bus.publish("/chuck", {:norris => true})
|
44
44
|
@bus.publish("/chuck", {:norris => true})
|
45
45
|
|
46
|
-
@bus.reliable_pub_sub.
|
46
|
+
@bus.reliable_pub_sub.reset!
|
47
47
|
|
48
48
|
@bus.publish("/chuck", {:yeager => true})
|
49
49
|
|
50
|
-
wait_for(2000){ data["yeager"]}
|
50
|
+
wait_for(2000){ data && data["yeager"]}
|
51
51
|
|
52
52
|
data["yeager"].must_equal true
|
53
53
|
|
@@ -184,39 +184,40 @@ describe MessageBus do
|
|
184
184
|
|
185
185
|
end
|
186
186
|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
@bus.publish("/hello", "world")
|
194
|
-
|
195
|
-
wait_for(2000){ data }
|
187
|
+
unless MESSAGE_BUS_CONFIG[:backend] == :memory
|
188
|
+
it "should support forking properly do" do
|
189
|
+
data = nil
|
190
|
+
@bus.subscribe do |msg|
|
191
|
+
data = msg.data
|
192
|
+
end
|
196
193
|
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
@bus.
|
194
|
+
@bus.publish("/hello", "world")
|
195
|
+
|
196
|
+
wait_for(2000){ data }
|
197
|
+
|
198
|
+
if child = Process.fork
|
199
|
+
wait_for(2000) { data == "ready" }
|
200
|
+
@bus.publish("/hello", "world1")
|
201
|
+
wait_for(2000) { data == "got it" }
|
202
|
+
data.must_equal "got it"
|
203
|
+
Process.wait(child)
|
204
|
+
else
|
205
|
+
begin
|
206
|
+
@bus.after_fork
|
207
|
+
@bus.publish("/hello", "ready")
|
208
|
+
wait_for(2000) { data == "world1" }
|
209
|
+
if(data=="world1")
|
210
|
+
@bus.publish("/hello", "got it")
|
211
|
+
end
|
212
|
+
|
213
|
+
$stdout.reopen("/dev/null", "w")
|
214
|
+
$stderr.reopen("/dev/null", "w")
|
215
|
+
|
216
|
+
ensure
|
217
|
+
exit!(0)
|
210
218
|
end
|
211
|
-
|
212
|
-
$stdout.reopen("/dev/null", "w")
|
213
|
-
$stderr.reopen("/dev/null", "w")
|
214
|
-
|
215
|
-
ensure
|
216
|
-
exit!(0)
|
217
219
|
end
|
218
|
-
end
|
219
220
|
|
221
|
+
end
|
220
222
|
end
|
221
|
-
|
222
223
|
end
|
@@ -345,22 +345,26 @@ window.MessageBus = (function() {
|
|
345
345
|
last_id: lastId
|
346
346
|
});
|
347
347
|
if (me.longPoll) {
|
348
|
-
|
348
|
+
me.longPoll.abort();
|
349
349
|
}
|
350
|
+
|
351
|
+
return func;
|
350
352
|
},
|
351
353
|
|
352
354
|
// Unsubscribe from a channel
|
353
355
|
unsubscribe: function(channel, func) {
|
354
|
-
// TODO
|
356
|
+
// TODO allow for globbing in the middle of a channel name
|
357
|
+
// like /something/*/something
|
358
|
+
// at the moment we only support globbing /something/*
|
355
359
|
var glob;
|
356
360
|
if (channel.indexOf("*", channel.length - 1) !== -1) {
|
357
361
|
channel = channel.substr(0, channel.length - 1);
|
358
362
|
glob = true;
|
359
363
|
}
|
360
364
|
|
361
|
-
var
|
365
|
+
var removed = false;
|
362
366
|
|
363
|
-
for (var i=
|
367
|
+
for (var i=callbacks.length-1; i>=0; i--) {
|
364
368
|
|
365
369
|
callback = callbacks[i];
|
366
370
|
var keep;
|
@@ -375,16 +379,17 @@ window.MessageBus = (function() {
|
|
375
379
|
keep = true;
|
376
380
|
}
|
377
381
|
|
378
|
-
if (keep) {
|
379
|
-
|
382
|
+
if (!keep) {
|
383
|
+
callbacks.splice(i,1);
|
384
|
+
removed = true;
|
380
385
|
}
|
381
386
|
}
|
382
387
|
|
383
|
-
|
384
|
-
|
385
|
-
if (me.longPoll) {
|
386
|
-
return me.longPoll.abort();
|
388
|
+
if (removed && me.longPoll) {
|
389
|
+
me.longPoll.abort();
|
387
390
|
}
|
391
|
+
|
392
|
+
return removed;
|
388
393
|
}
|
389
394
|
};
|
390
395
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: message_bus
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.0.beta.
|
4
|
+
version: 2.0.0.beta.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sam Saffron
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-03-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -79,6 +79,7 @@ files:
|
|
79
79
|
- examples/bench/puma.rb
|
80
80
|
- examples/bench/unicorn.conf.rb
|
81
81
|
- examples/bench/wrk.sample
|
82
|
+
- examples/chat/Gemfile
|
82
83
|
- examples/chat/chat.rb
|
83
84
|
- examples/chat/config.ru
|
84
85
|
- examples/chat/docker_container/chat.yml
|
@@ -86,6 +87,7 @@ files:
|
|
86
87
|
- examples/minimal/Gemfile
|
87
88
|
- examples/minimal/config.ru
|
88
89
|
- lib/message_bus.rb
|
90
|
+
- lib/message_bus/backends/memory.rb
|
89
91
|
- lib/message_bus/backends/postgres.rb
|
90
92
|
- lib/message_bus/backends/redis.rb
|
91
93
|
- lib/message_bus/client.rb
|